Ansible Jinja2 模板 #

什么是 Jinja2? #

Jinja2 是 Python 的模板引擎,Ansible 使用它来生成动态配置文件。模板文件使用 .j2 扩展名,包含变量、条件、循环等逻辑。

text
┌─────────────────────────────────────────────────────────────┐
│                      模板工作流程                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  模板文件 (.j2)          变量数据          渲染引擎          │
│  ──────────────         ──────────        ──────────        │
│  server {               nginx_port: 80    Jinja2            │
│    listen {{ port }};   server_name: web  Engine            │
│    server_name {{ name }};                                  │
│  }                                                          │
│                                                              │
│                          ↓                                   │
│                                                              │
│                    生成的配置文件                            │
│                    ────────────────                         │
│                    server {                                 │
│                      listen 80;                             │
│                      server_name web;                       │
│                    }                                        │
│                                                              │
└─────────────────────────────────────────────────────────────┘

基本语法 #

变量 #

jinja2
{# templates/config.j2 #}

{# 输出变量 #}
ServerName {{ server_name }}
Port {{ port }}

{# 访问字典 #}
Database: {{ database.host }}:{{ database.port }}

{# 访问列表元素 #}
First server: {{ servers[0] }}

{# 属性访问 #}
{{ ansible_facts.hostname }}

注释 #

jinja2
{# 单行注释 - 不会出现在输出中 #}

{#
  多行注释
  这些内容不会出现在生成的文件中
#}

# 普通注释会出现在输出中
# 这是一个配置文件

template 模块 #

基本使用 #

yaml
tasks:
  - name: Deploy configuration file
    template:
      src: templates/nginx.conf.j2
      dest: /etc/nginx/nginx.conf
      owner: root
      group: root
      mode: '0644'
      backup: yes
    notify: Restart Nginx

模板参数 #

yaml
tasks:
  - name: Deploy with all options
    template:
      src: app.conf.j2
      dest: /etc/app/app.conf
      owner: appuser
      group: appuser
      mode: '0644'
      backup: yes
      validate: 'nginx -t -c %s'
      variable_start_string: '{%'
      variable_end_string: '%}'
      block_start_string: '{%'
      block_end_string: '%}'

条件判断 #

if 语句 #

jinja2
{# templates/nginx.conf.j2 #}
server {
    listen {{ nginx_port }};
    server_name {{ server_name }};
    
    {% if ssl_enabled %}
    listen 443 ssl;
    ssl_certificate {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
    {% endif %}
    
    {% if environment == 'production' %}
    # Production settings
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log warn;
    {% elif environment == 'staging' %}
    # Staging settings
    access_log /var/log/nginx/staging_access.log;
    error_log /var/log/nginx/staging_error.log;
    {% else %}
    # Development settings
    access_log off;
    {% endif %}
}

条件表达式 #

jinja2
{# 使用内联条件 #}
worker_processes {{ ansible_facts.processor_vcpus if nginx_worker_processes == 'auto' else nginx_worker_processes }};

{# 使用过滤器条件 #}
debug_mode: {{ 'true' if debug_enabled else 'false' }}

{# 复杂条件 #}
{% if ansible_facts.memtotal_mb > 8192 and environment == 'production' %}
cache_size: 2g
{% else %}
cache_size: 512m
{% endif %}

if-elif-else #

jinja2
{% if ansible_facts.os_family == 'Debian' %}
# Debian/Ubuntu configuration
user www-data;
{% elif ansible_facts.os_family == 'RedHat' %}
# RHEL/CentOS configuration
user nginx;
{% else %}
# Default configuration
user nobody;
{% endif %}

循环 #

for 循环 #

jinja2
{# 遍历列表 #}
{% for server in upstream_servers %}
server {{ server }};
{% endfor %}

{# 遍历字典 #}
{% for key, value in config.items() %}
{{ key }} = {{ value }}
{% endfor %}

{# 遍历字典列表 #}
{% for vhost in vhosts %}
server {
    listen {{ vhost.port }};
    server_name {{ vhost.server_name }};
    root {{ vhost.root }};
}
{% endfor %}

循环变量 #

jinja2
{% for item in items %}
Item {{ loop.index }}: {{ item }}
{# loop.index    - 当前索引(从1开始) #}
{# loop.index0   - 当前索引(从0开始) #}
{# loop.first    - 是否是第一个元素 #}
{# loop.last     - 是否是最后一个元素 #}
{# loop.length   - 列表长度 #}
{# loop.cycle    - 循环值 #}
{% endfor %}

{# 示例 #}
{% for server in servers %}
Server {{ loop.index }}/{{ loop.length }}: {{ server }}
{% if not loop.last %}, {% endif %}
{% endfor %}

循环控制 #

jinja2
{# for-else #}
{% for user in users %}
User: {{ user.name }}
{% else %}
No users found
{% endfor %}

{# 跳过元素 #}
{% for item in items %}
{% if item.disabled %}
{% continue %}
{% endif %}
Item: {{ item.name }}
{% endfor %}

{# 退出循环 #}
{% for item in items %}
{% if item.critical %}
ERROR: Critical item found
{% break %}
{% endif %}
Item: {{ item.name }}
{% endfor %}

嵌套循环 #

jinja2
{% for group in groups %}
Group: {{ group.name }}
{% for user in group.users %}
  - {{ user }}
{% endfor %}
{% endfor %}

过滤器 #

字符串过滤器 #

jinja2
{# 大小写转换 #}
{{ name | upper }}           {# MYAPP #}
{{ name | lower }}           {# myapp #}
{{ name | capitalize }}      {# Myapp #}
{{ name | title }}           {# My App #}

{# 替换 #}
{{ url | replace('http://', 'https://') }}

{# 截断 #}
{{ description | truncate(50) }}

{# 分割 #}
{{ csv_string | split(',') }}

{# 去除空白 #}
{{ value | trim }}
{{ value | strip }}

列表过滤器 #

jinja2
{# 排序 #}
{{ items | sort }}
{{ items | sort(reverse=true) }}
{{ users | sort(attribute='name') }}

{# 过滤 #}
{{ items | select('defined') }}
{{ numbers | select('odd') }}
{{ items | reject('none') }}

{# 映射 #}
{{ users | map(attribute='name') | list }}
{{ items | map('upper') | list }}

{# 合并 #}
{{ items | join(', ') }}

{# 统计 #}
{{ items | length }}
{{ items | min }}
{{ items | max }}
{{ numbers | sum }}

{# 去重 #}
{{ items | unique | list }}

{# 合并列表 #}
{{ list1 + list2 }}
{{ list1 | union(list2) }}

字典过滤器 #

jinja2
{# 转换为列表 #}
{{ my_dict | dict2items }}
{# [{'key': 'k1', 'value': 'v1'}, ...] #}

{# 列表转字典 #}
{{ items | items2dict }}
{# {'k1': 'v1', 'k2': 'v2'} #}

{# 合并字典 #}
{{ dict1 | combine(dict2) }}
{{ dict1 | combine(dict2, recursive=true) }}

{# 获取键值 #}
{{ my_dict | dict_keys | list }}
{{ my_dict | dict_values | list }}

数字过滤器 #

jinja2
{# 数学运算 #}
{{ value | int }}
{{ value | float }}
{{ value | abs }}
{{ value | round(2) }}
{{ value | round(2, 'floor') }}

{# 随机 #}
{{ items | random }}
{{ range(10) | random }}

默认值 #

jinja2
{# 默认值 #}
{{ undefined_var | default('default_value') }}
{{ empty_var | default('default', true) }}

{# 必须定义 #}
{{ required_var | mandatory }}

{# 省略 #}
{{ param | default(omit) }}

类型转换 #

jinja2
{{ value | string }}
{{ value | int }}
{{ value | float }}
{{ value | bool }}
{{ value | to_json }}
{{ value | to_yaml }}
{{ value | to_nice_json }}
{{ value | to_nice_yaml }}

空白控制 #

控制空白 #

jinja2
{# 默认 - 保留换行 #}
{% for item in items %}
Item: {{ item }}
{% endfor %}

{# 输出:
Item: item1
Item: item2
#}

{# 使用 - 去除空白 #}
{% for item in items -%}
Item: {{ item }}
{%- endfor %}

{# 输出:
Item: item1
Item: item2
#}

{# 完全去除换行 #}
{%- for item in items -%}
Item: {{ item }}
{%- endfor -%}

{# 输出: Item: item1Item: item2 #}

trim_blocks 和 lstrip_blocks #

yaml
# ansible.cfg
[defaults]
jinja2_trim_blocks = True
jinja2_lstrip_blocks = True

#

定义宏 #

jinja2
{# 定义可重用的宏 #}
{% macro server_block(name, port, root) %}
server {
    listen {{ port }};
    server_name {{ name }};
    root {{ root }};
    
    location / {
        try_files $uri $uri/ =404;
    }
}
{% endmacro %}

{# 使用宏 #}
{{ server_block('example.com', 80, '/var/www/example') }}
{{ server_block('api.example.com', 80, '/var/www/api') }}

宏参数 #

jinja2
{% macro config_entry(key, value, comment='') %}
{# {{ key }}: {{ value }}{% if comment %} # {{ comment }}{% endif %} #}
{{ key }}: {{ value }}{% if comment %}  # {{ comment }}{% endif %}
{% endmacro %}

{{ config_entry('port', 8080, 'Application port') }}
{{ config_entry('debug', false) }}

宏文件 #

jinja2
{# templates/macros.j2 #}
{% macro input(name, value='', type='text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}

{% macro textarea(name, value='', rows=4) %}
<textarea name="{{ name }}" rows="{{ rows }}">{{ value }}</textarea>
{% endmacro %}

{# 在其他模板中导入 #}
{% from 'macros.j2' import input, textarea %}
{{ input('username') }}
{{ textarea('description') }}

包含和继承 #

include #

jinja2
{# 包含其他模板 #}
{% include 'header.j2' %}

server {
    listen {{ port }};
    {% include 'server_common.j2' %}
}

{# 包含并传递变量 #}
{% include 'item.j2' with context %}
{% include 'item.j2' without context %}

extends 继承 #

jinja2
{# templates/base.conf.j2 #}
# Base Configuration
# Generated by Ansible

{% block header %}
# Header section
{% endblock %}

{% block main %}
# Main configuration
{% endblock %}

{% block footer %}
# Footer section
{% endblock %}

{# templates/app.conf.j2 #}
{% extends 'base.conf.j2' %}

{% block header %}
# Application Header
# App: {{ app_name }}
{% endblock %}

{% block main %}
# Application Settings
port: {{ app_port }}
debug: {{ debug_mode }}
{% endblock %}

实际示例 #

Nginx 配置模板 #

jinja2
{# templates/nginx.conf.j2 #}
# Nginx configuration
# Managed by Ansible - DO NOT EDIT MANUALLY

user {{ nginx_user }};
worker_processes {{ nginx_worker_processes | default('auto') }};
pid /run/nginx.pid;
error_log {{ nginx_log_dir }}/error.log;

events {
    worker_connections {{ nginx_worker_connections | default(1024) }};
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    {% if nginx_gzip_enabled | default(true) %}
    gzip on;
    gzip_types text/plain text/css application/json application/javascript;
    {% endif %}
    
    {% for upstream in nginx_upstreams | default([]) %}
    upstream {{ upstream.name }} {
        {% for server in upstream.servers %}
        server {{ server.host }}:{{ server.port }}{% if server.options %} {{ server.options }}{% endif %};
        {% endfor %}
    }
    {% endfor %}
    
    {% for vhost in nginx_vhosts %}
    server {
        listen {{ vhost.port | default(80) }};
        server_name {{ vhost.server_name }};
        root {{ vhost.root | default('/var/www/html') }};
        
        {% if vhost.ssl | default(false) %}
        listen {{ vhost.ssl_port | default(443) }} ssl;
        ssl_certificate {{ vhost.ssl_cert }};
        ssl_certificate_key {{ vhost.ssl_key }};
        {% endif %}
        
        {% for location in vhost.locations | default([]) %}
        location {{ location.path }} {
            {% if location.proxy_pass %}
            proxy_pass {{ location.proxy_pass }};
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            {% elif location.try_files %}
            try_files {{ location.try_files }};
            {% endif %}
        }
        {% endfor %}
    }
    {% endfor %}
}

应用配置模板 #

jinja2
{# templates/app.env.j2 #}
# Application Environment Configuration
# Generated: {{ ansible_date_time.iso8601 }}

# Application
APP_NAME={{ app_name }}
APP_ENV={{ environment }}
APP_DEBUG={{ debug_mode | default(false) | lower }}
APP_URL={{ app_url | default('http://localhost') }}

# Database
DB_CONNECTION={{ db_connection | default('mysql') }}
DB_HOST={{ database.host }}
DB_PORT={{ database.port }}
DB_DATABASE={{ database.name }}
DB_USERNAME={{ database.user }}
DB_PASSWORD={{ database.password }}

# Cache
CACHE_DRIVER={{ cache_driver | default('redis') }}
REDIS_HOST={{ redis_host | default('127.0.0.1') }}
REDIS_PASSWORD={{ redis_password | default('null') }}
REDIS_PORT={{ redis_port | default(6379) }}

# Queue
QUEUE_CONNECTION={{ queue_driver | default('redis') }}

# Mail
MAIL_MAILER={{ mail_driver | default('smtp') }}
MAIL_HOST={{ mail_host | default('smtp.mailtrap.io') }}
MAIL_PORT={{ mail_port | default(587) }}
MAIL_USERNAME={{ mail_username }}
MAIL_PASSWORD={{ mail_password }}
MAIL_ENCRYPTION={{ mail_encryption | default('tls') }}

# Custom Variables
{% for key, value in custom_vars.items() | default({}) %}
{{ key | upper }}={{ value }}
{% endfor %}

YAML 配置模板 #

jinja2
{# templates/config.yml.j2 #}
# {{ app_name }} Configuration
# Generated by Ansible

app:
  name: {{ app_name }}
  version: {{ app_version }}
  environment: {{ environment }}

server:
  host: {{ server_host | default('0.0.0.0') }}
  port: {{ server_port | default(8080) }}
  {% if ssl_enabled %}
  ssl:
    enabled: true
    cert: {{ ssl_cert_path }}
    key: {{ ssl_key_path }}
  {% endif %}

database:
  {% for db in databases %}
  - name: {{ db.name }}
    host: {{ db.host }}
    port: {{ db.port | default(3306) }}
    user: {{ db.user }}
    password: {{ db.password }}
    {% if db.options %}
    options:
      {% for key, value in db.options.items() %}
      {{ key }}: {{ value }}
      {% endfor %}
    {% endif %}
  {% endfor %}

logging:
  level: {{ log_level | default('info') }}
  format: {{ log_format | default('json') }}
  output: {{ log_output | default('/var/log/app/app.log') }}

features:
  {% for feature, enabled in features.items() | default({}) %}
  {{ feature }}: {{ enabled | lower }}
  {% endfor %}

模板验证 #

验证语法 #

yaml
tasks:
  - name: Validate template syntax
    template:
      src: templates/nginx.conf.j2
      dest: /tmp/nginx.conf.test
      validate: nginx -t -c %s
    check_mode: yes

预览模板 #

yaml
tasks:
  - name: Preview template
    debug:
      msg: "{{ lookup('template', 'templates/app.conf.j2') }}"

最佳实践 #

1. 使用注释 #

jinja2
{# 文件头注释 #}
# {{ app_name }} Configuration
# Managed by Ansible - DO NOT EDIT MANUALLY
# Template: {{ ansible_managed }}
# Generated: {{ ansible_date_time.iso8601 }}

2. 合理使用空白控制 #

jinja2
{# 使用 - 控制空白 #}
{% for item in items -%}
{{ item }}
{% endfor %}

3. 提供默认值 #

jinja2
{# 使用 default 过滤器 #}
port: {{ app_port | default(8080) }}
debug: {{ debug_mode | default(false) | lower }}

4. 组织模板文件 #

text
templates/
├── nginx/
│   ├── nginx.conf.j2
│   ├── site.conf.j2
│   └── ssl.conf.j2
├── app/
│   ├── config.yml.j2
│   ├── env.j2
│   └── logging.conf.j2
└── macros/
    ├── common.j2
    └── nginx.j2

下一步 #

现在你已经掌握了 Jinja2 模板,接下来学习 Roles 角色 了解如何组织和管理复杂的 Ansible 项目!

最后更新:2026-03-29