一、Jinja2 核心介绍
1. 什么是Jinja2
Jinja2是一款基于Python的模板引擎,支持动态生成文本文件(如配置文件、报告、脚本),核心能力是将「静态模板」与「动态变量/逻辑」结合,输出个性化内容。在Ansible中,Jinja2是默认模板引擎,专门用于解决「批量配置差异化」问题(比如不同主机生成不同的Nginx/MySQL配置)。
2. Ansible中Jinja2的核心价值
- 告别手动修改配置文件:通过变量自动填充主机IP、系统信息等差异化内容;
- 支持逻辑控制:用条件/循环实现「不同主机/场景输出不同配置」;
- 与Ansible深度集成:可直接调用Ansible收集的「事实变量」(如主机接口、内存、系统版本)。
3. 基础使用规范
- 模板文件后缀:约定为
.j2(如httpd.conf.j2、hosts.j2); - 渲染方式:Ansible通过
template模块读取.j2模板,结合变量渲染后生成最终文件; - 前置条件:需开启
gather_facts: yes(Ansible默认开启),才能收集主机事实变量(如IP、内存)。
二、Jinja2 核心语法
1. 语法符号速查表
| 符号格式 | 作用 | 示例 | 注意事项 |
|---|---|---|---|
{``{ 变量 }} |
输出变量值(插值) | {``{ ansible_ens160.ipv4.address }} |
变量不存在会报错,建议先判断存在性 |
{% 控制语句 %} |
逻辑控制(条件/循环) | {% if 条件 %} {% for 变量 in 列表 %} |
必须用对应关键字闭合(如 endif) |
{# 注释 #} |
模板内注释,渲染后自动忽略 | {# 优先使用bond0接口IP #} |
区别于文件自身的注释(如#) |
2. 变量来源(Ansible场景)
Jinja2模板中的变量主要来自3类,可直接调用:
- 事实变量(Facts) :Ansible自动收集的主机信息(如
ansible_hostname主机名、ansible_memtotal_mb总内存、ansible_distribution系统发行版); - 自定义变量 :Playbook中
vars定义的变量、清单(inventory)中定义的主机/组变量; - 特殊变量 :Ansible内置变量(如
groups.all所有主机、hostvars[主机名]其他主机的变量)。
三、条件语句(if/elif/else)
1. 核心语法(按场景分类)
(1)单分支:满足条件才输出
jinja2
{# 语法结构 #}
{% if 条件表达式 %}
满足条件时输出的内容
{% endif %} {# 必须闭合,否则模板渲染失败 #}
{# 实战示例:仅CentOS系统输出yum源配置 #}
{% if ansible_distribution == "CentOS" %}
[base]
name=CentOS-Base
baseurl=https://mirrors.aliyun.com/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7
{% endif %}
(2)双分支:二选一输出
jinja2
{# 语法结构 #}
{% if 条件表达式 %}
满足条件的内容
{% else %}
不满足条件的默认内容
{% endif %}
{# 实战示例:根据是否有SSL证书决定监听端口 #}
{% if ansible_facts['files']['/etc/nginx/ssl/server.crt'] is defined %}
listen 443 ssl;
{% else %}
listen 80;
{% endif %}
(3)多分支:按优先级匹配条件
jinja2
{# 语法结构 #}
{% if 条件1 %}
满足条件1的内容
{% elif 条件2 %}
满足条件2的内容
{% elif 条件3 %}
满足条件3的内容
{% else %}
所有条件都不满足的默认内容
{% endif %}
{# 实战示例:优先使用bond0,其次ens160,最后0.0.0.0 #}
{% if ansible_bond0 is defined %}
Listen {{ ansible_bond0.ipv4.address }}:80
{% elif ansible_ens160 is defined %}
Listen {{ ansible_ens160.ipv4.address }}:80
{% else %}
Listen 0.0.0.0:80
{% endif %}
2. 常用条件表达式
| 表达式 | 含义 | 示例 |
|---|---|---|
变量 is defined |
判断变量是否存在 | ansible_bond0 is defined |
变量 == 值 |
等值判断(字符串/数字) | ansible_hostname == "node01" |
变量 != 值 |
不等值判断 | ansible_os_family != "RedHat" |
变量 in 列表 |
包含判断 | ansible_hostname in ["node01","node02"] |
变量 >/</>=/<= 值 |
数值比较 | ansible_memtotal_mb > 8192 |
3. 实战案例:按系统生成差异化文件
需求
所有主机生成 /opt/os.txt:CentOS系统内容为 OS: CentOS,Ubuntu为 OS: Ubuntu,其他系统为 OS: Unknown。
步骤1:编写Jinja2模板(os.txt.j2)
jinja2
{# 基于系统发行版生成差异化内容 #}
{% if ansible_distribution == "CentOS" %}
OS: CentOS
{% elif ansible_distribution == "Ubuntu" %}
OS: Ubuntu
{% else %}
OS: Unknown
{% endif %}
步骤2:编写Ansible Playbook(create_os_file.yml)
yaml
- hosts: all
gather_facts: yes # 必须开启,才能获取ansible_distribution变量
tasks:
- name: 生成系统标识文件
template:
src: os.txt.j2 # 本地模板文件路径
dest: /opt/os.txt # 目标主机生成的文件路径
mode: 0644 # 文件权限
步骤3:执行与验证
bash
# 执行Playbook
ansible-playbook create_os_file.yml
# 验证node01的文件内容
ansible node01 -m command -a "cat /opt/os.txt"
# 预期输出(CentOS主机):OS: CentOS
四、循环语句(for)
1. 核心语法
Jinja2 for循环仅支持遍历「列表/可迭代对象」,用于批量生成重复格式的内容,语法如下:
jinja2
{# 基础语法:遍历可迭代对象,每次将元素赋值给变量 #}
{% for 变量名 in 可迭代对象 %}
循环体内容(可调用变量属性)
{% endfor %} {# 必须闭合,否则报错 #}
{# 简化示例:遍历自定义端口列表 #}
{% for port in [80,443,8080] %}
port {{ port }}
{% endfor %}
2. Ansible常用可迭代对象
| 可迭代对象 | 含义 | 适用场景 |
|---|---|---|
groups.all |
Ansible清单中所有主机 | 生成全局hosts文件 |
groups['组名'] |
清单中指定组的主机(如webservers) | 生成Nginx upstream节点列表 |
ansible_mounts |
主机挂载点列表 | 生成挂载点巡检报告 |
自定义列表 |
如 [80,443,8080] |
批量生成端口配置 |
ansible_interfaces |
主机网络接口列表 | 批量输出接口IP |
3. 实战案例1:批量生成端口配置文件
需求
所有主机生成 /opt/ports.txt,内容为80、443、8080、9090四个端口,每行一个(用for循环实现)。
步骤1:编写模板(ports.txt.j2)
jinja2
{# 遍历自定义端口列表,批量生成每行一个端口 #}
{% for port in [80,443,8080,9090] %}
{{ port }}
{% endfor %}
步骤2:编写Playbook(create_ports_file.yml)
yaml
- hosts: all
gather_facts: no # 无需收集事实变量,可关闭提升速度
tasks:
- name: 生成端口文件
template:
src: ports.txt.j2
dest: /opt/ports.txt
mode: 0644
步骤3:验证
bash
ansible-playbook create_ports_file.yml
# 查看node01的ports.txt
ansible node01 -m command -a "cat /opt/ports.txt"
# 预期输出:
# 80
# 443
# 8080
# 9090
4. 实战案例2:批量生成hosts文件
需求
所有主机生成 /opt/hosts,包含Ansible清单内所有主机的「IP + FQDN + 主机名」。
步骤1:编写模板(hosts.j2)
jinja2
{# 批量生成hosts文件,遍历所有主机 #}
# Ansible自动生成 {{ ansible_date_time.date }}
{% for host in groups.all %}
{{ hostvars[host].ansible_ens160.ipv4.address }} {{ hostvars[host].ansible_fqdn }} {{ hostvars[host].ansible_hostname }}
{% endfor %}
- 关键说明:
hostvars[host]是Ansible特殊变量,用于获取「指定主机」的事实变量(比如获取node02的IP,而不是当前主机)。
步骤2:编写Playbook(generate_hosts.yml)
yaml
- hosts: all
gather_facts: yes # 需获取IP/FQDN,必须开启
tasks:
- name: 生成hosts文件
template:
src: hosts.j2
dest: /opt/hosts
mode: 0644
步骤3:验证
bash
ansible-playbook generate_hosts.yml
# 查看node01的hosts文件
ansible node01 -m command -a "cat /opt/hosts"
# 预期输出:
# Ansible自动生成 2026-01-06
# 172.17.0.146 node01.example.com node01
# 172.17.0.149 node02.example.com node02
五、条件+循环 综合实战
需求
对 dbservers 组主机生成 /opt/mysql.cnf:
- 循环生成MySQL从库列表(排除本机);
- 根据主机内存调整参数:内存>8192MB时,
innodb_buffer_pool_size=6G,否则为2G。
步骤1:编写模板(mysql.cnf.j2)
jinja2
{# MySQL配置模板 - 综合条件+循环 #}
[mysqld]
# 基础配置
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
# 根据内存调整缓存池大小
{% if ansible_memtotal_mb > 8192 %}
innodb_buffer_pool_size=6G
{% else %}
innodb_buffer_pool_size=2G
{% endif %}
# 从库列表(排除本机)
{% for host in groups['dbservers'] %}
{% if host != ansible_hostname %}
slave_host={{ hostvars[host].ansible_ens160.ipv4.address }}
{% endif %}
{% endfor %}
步骤2:编写Playbook(config_mysql.yml)
yaml
- hosts: dbservers # 仅针对dbservers组
gather_facts: yes # 需获取内存、IP、主机名
tasks:
- name: 生成MySQL配置文件
template:
src: mysql.cnf.j2
dest: /opt/mysql.cnf
mode: 0644
步骤3:验证
假设 dbservers 组包含 node03(内存16G)、node04(内存4G):
bash
ansible-playbook config_mysql.yml
# 查看node03的配置(内存>8G)
ansible node03 -m command -a "cat /opt/mysql.cnf"
# 关键输出:
# innodb_buffer_pool_size=6G
# slave_host=172.17.0.150(node04的IP)
# 查看node04的配置(内存<8G)
ansible node04 -m command -a "cat /opt/mysql.cnf"
# 关键输出:
# innodb_buffer_pool_size=2G
# slave_host=172.17.0.148(node03的IP)
六、避坑指南(高频问题)
- 语法闭合问题 :条件/循环语句必须用
{% endif %}/{% endfor %}闭合,少写会导致模板渲染失败; - 变量不存在报错 :使用变量前建议用
is defined判断(如{% if ansible_bond0 is defined %}); - 事实变量为空 :忘记开启
gather_facts: yes,导致无法获取IP、内存等变量; - 注释被渲染 :模板内注释必须用
{# 内容 #},若用#会被保留到最终文件中; - 循环中引用其他主机变量 :需用
hostvars[host],直接写ansible_ens160只会取当前主机的值。
七、典型使用场景总结
| 场景 | 核心语法 | 示例 |
|---|---|---|
| 差异化配置文件 | if/elif/else | 不同系统生成不同yum/apt源 |
| 批量生成节点列表 | for循环 | Nginx upstream、Keepalived节点 |
| 硬件适配参数 | 条件+数值比较 | 根据内存调整MySQL/Redis参数 |
| 批量输出资源报告 | for循环 | 磁盘/挂载点/端口巡检报告 |
| 混合场景(排除/筛选) | 条件+循环 | 生成从库列表(排除本机) |
关键点回顾
- Jinja2在Ansible中核心用于「动态生成差异化配置」,模板后缀为
.j2,依赖template模块渲染; - 核心语法分三类:
{``{ 变量 }}插值、{% 控制语句 %}逻辑、{# 注释 #}模板注释; - 条件语句解决「不同场景输出不同内容」,循环语句解决「批量生成重复内容」,两者结合可覆盖绝大多数运维配置场景;
- 使用前需确保开启
gather_facts: yes,变量使用前建议用is defined判断存在性,避免报错。