TT Lab
Get started
Learn Learning paths Courses

Ansible in Practice

Jinja2 — From Data to Config File

Continue in TT Lab

Summary in one line

A template breaks "a configuration file that differs per environment" into one frame + per-environment data.

Why this is needed

If you keep a separate nginx configuration for each of dev, stage, and prod, the three files drift apart. Every time a backend server is added you have to edit three places, and if you miss one, only that environment gets flooded with traffic. A template keeps the server list as data and lets you write the frame only once. When backends are added, only one line of data grows.

How it works

Jinja2 uses only three kinds of syntax.

On top of this come filters. You chain them with a pipe, as in {{ name | upper }}, {{ timeout | default(30) }}, {{ data | to_nice_json }}. The default filter is especially important — it gives a safe fallback value to a value that may be undefined, so that <no value> or an error does not end up in the output when the variable is missing.

The most frequent problem is whitespace. A control statement such as {% for %} occupies a line of its own, so blank lines are left in the rendered result. In a YAML configuration, those blank lines can even break the file. There are two solutions. Use {%- / -%} in the statement to strip the whitespace before and after it, or turn on trim_blocks/lstrip_blocks for the template module.

Two options of the template module are also worth knowing, because they prevent accidents. validate checks the rendered result with a specified command and puts the file in place only if it passes. backup: true keeps a copy before overwriting. The moment you deploy configuration files automatically, a path opens by which a bad configuration can kill the service. validate is the cheapest safeguard that blocks that path.

What it looks like in the field

First, a template whose rendered result changes every time. If you put a timestamp in a comment, the file changes every run, the handler runs every time, and the service restarts every time. There are plenty of cases where a zero-downtime deployment collapses because of this single line.

Second, a template that uses facts. If you use values from ansible_facts as they are, a different configuration comes out automatically for each server. However, in special environments facts can produce unexpected values, so set up a line of defense with default.

Third, putting too much logic into a template. When ifs are stacked five levels deep, that is not a template but a program. It is better for maintenance to lift such branching up into the variable computation stage and keep the template simple.

Filters you use often in templates

The value of Jinja2 comes from its filters. Knowing just these covers most needs.

{{ port | default(8080) }}              값이 없으면 기본값
{{ name | mandatory }}                  없으면 에러 — 조용한 빈 값을 막는다
{{ items | join(',') }}                 목록을 문자열로
{{ config | to_nice_yaml(indent=2) }}   딕셔너리를 YAML 블록으로
{{ secret | b64encode }}                쿠버네티스 시크릿용
{{ path | basename }}                   경로 조각
{{ hosts | map(attribute='ip') | list }} 목록에서 필드만 뽑기

The habit of using mandatory is important. When a variable is missing, Jinja2 by default inserts an empty string. That produces a broken configuration such as listen ;, and the problem only shows up when the service restarts.

If you set this in ansible.cfg, an undefined variable immediately becomes an error.

[defaults]
error_on_undefined_vars = True

How to handle whitespace

Most of the blank lines that pile up in a generated file are caused by control structures.

{% for h in hosts %}
server {{ h }};
{% endfor %}

Written this way, a newline is left for every {% %} line. Add hyphens to remove them.

{% for h in hosts -%}
server {{ h }};
{% endfor -%}

Ansible's template module does not turn on trim_blocks by default, so if you need it, set it on the first line of the template.

#jinja2: trim_blocks: True, lstrip_blocks: True

How to test a template

Look at the result before you deploy.

# 렌더링 결과만 보기 (파일을 쓰지 않는다)
ansible -i inv web -m template -a "src=nginx.conf.j2 dest=/tmp/out.conf" --check --diff

# 문법 검사
ansible-playbook site.yml --syntax-check

# 실제로 무엇이 바뀌는지
ansible-playbook site.yml --check --diff

The --check --diff combination is the most useful. It shows you what would change without changing anything. If you do not run this before a production deployment, a single line of configuration can stop the service.

Then add a step that verifies the generated configuration with that program's own checker.

- name: nginx 설정
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: 'nginx -t -c %s'      # ← 실패하면 파일을 바꾸지 않는다
  notify: reload nginx

validate checks a temporary file and moves it into place only when it passes. A broken configuration never reaches the disk.

What you will do in the next lab

Starting from basic rendering, you use filters, loops, and conditions in turn, and use whitespace control to make the output clean. You use facts and inventory_hostname in a template, and use validate to keep a bad configuration from being put in place. Finally, you generate a whole nginx configuration from the backend list in a group variable.