Jinja2 模板实战
核心问题:服务器配置文件(Nginx conf、systemd service、.env 文件)怎样根据不同环境自动生成,不再手动维护多份副本?
为什么用模板而非 copy
| 对比 | copy 模块 | template 模块 |
|---|---|---|
| 文件内容 | 静态,原样复制 | 动态,Jinja2 变量替换 |
| 适用场景 | SSL 证书、静态资源 | 配置文件、启动脚本 |
| 多环境 | 需要维护多份文件 | 一份模板 + 不同变量 |
| 可读性 | 直接看文件内容 | 需要了解 Jinja2 语法 |
Jinja2 基础语法
变量输出
{{ variable }} {# 输出变量值 #}
{{ user.name }} {# 字典/对象属性 #}
{{ users[0] }} {# 列表下标 #}
{{ config['key'] }} {# 字典访问(key 含特殊字符时用) #}
过滤器(Filters)
{{ name | upper }} {# 转大写 #}
{{ name | lower }} {# 转小写 #}
{{ name | default('unknown') }} {# 默认值 #}
{{ path | basename }} {# /etc/nginx/nginx.conf → nginx.conf #}
{{ path | dirname }} {# /etc/nginx/nginx.conf → /etc/nginx #}
{{ number | int }} {# 转整数 #}
{{ number | string }} {# 转字符串 #}
{{ list | join(', ') }} {# 列表拼接 #}
{{ list | length }} {# 列表长度 #}
{{ dict | to_json }} {# 转 JSON #}
{{ dict | to_nice_yaml }} {# 转易读 YAML #}
{{ var | bool }} {# 转布尔值("yes"/"true"/1 → True)#}
{{ var | quote }} {# Shell 安全引号包裹 #}
{{ password | password_hash('sha512') }} {# 生成密码哈希 #}
控制结构
{# 条件判断 #}
{% if ssl_enabled %}
listen 443 ssl;
ssl_certificate /etc/ssl/{{ domain }}.crt;
{% elif http2_enabled %}
listen 80 http2;
{% else %}
listen 80;
{% endif %}
{# 循环 #}
{% for server in upstream_servers %}
server {{ server.ip }}:{{ server.port }} weight={{ server.weight | default(1) }};
{% endfor %}
{# 循环带索引 #}
{% for item in items %}
{{ loop.index }}: {{ item }} {# loop.index 从 1 开始 #}
{{ loop.index0 }}: {{ item }} {# loop.index0 从 0 开始 #}
{% if loop.last %}最后一项{% endif %}
{% endfor %}
实战示例
Nginx 虚拟主机配置
{# templates/vhost.conf.j2 #}
upstream {{ item.name }}_backend {
{% for server in item.upstream | default([{'host': '127.0.0.1', 'port': item.port}]) %}
server {{ server.host }}:{{ server.port }};
{% endfor %}
keepalive 32;
}
server {
listen 80;
server_name {{ item.domain }};
{% if item.ssl | default(false) %}
# HTTP → HTTPS 重定向
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ item.domain }};
ssl_certificate /etc/letsencrypt/live/{{ item.domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ item.domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
{% endif %}
access_log /var/log/nginx/{{ item.domain }}.access.log;
error_log /var/log/nginx/{{ item.domain }}.error.log;
location / {
proxy_pass http://{{ item.name }}_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
{% if item.timeout is defined %}
proxy_read_timeout {{ item.timeout }};
proxy_send_timeout {{ item.timeout }};
{% endif %}
}
{% if item.static_dir is defined %}
location /static/ {
alias {{ item.static_dir }}/;
expires 30d;
add_header Cache-Control "public, no-transform";
}
{% endif %}
}
# group_vars/webservers.yml
vhosts:
- name: api
domain: api.example.com
port: 3000
ssl: true
timeout: 60s
- name: admin
domain: admin.example.com
port: 8080
ssl: true
static_dir: /var/www/admin/static
Systemd 服务文件
{# templates/app.service.j2 #}
[Unit]
Description={{ app_name }} Application
Documentation=https://github.com/{{ github_org }}/{{ app_name }}
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User={{ app_user | default('www-data') }}
Group={{ app_user | default('www-data') }}
WorkingDirectory={{ app_dir }}
# 环境变量
Environment="NODE_ENV={{ target_env }}"
Environment="PORT={{ app_port }}"
{% for key, value in app_env_vars.items() %}
Environment="{{ key }}={{ value }}"
{% endfor %}
ExecStart={{ node_binary | default('/usr/bin/node') }} {{ app_dir }}/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier={{ app_name }}
# 资源限制
{% if app_memory_limit is defined %}
MemoryMax={{ app_memory_limit }}
{% endif %}
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
.env 环境变量文件
{# templates/.env.j2 #}
# Generated by Ansible — DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }} | Date: {{ ansible_date_time.iso8601 }}
APP_ENV={{ target_env }}
APP_PORT={{ app_port }}
APP_SECRET={{ app_secret }}
# Database
DATABASE_URL=postgresql://{{ db_user }}:{{ db_password }}@{{ db_host }}:{{ db_port | default(5432) }}/{{ db_name }}
DATABASE_POOL_SIZE={{ db_pool_size | default(10) }}
# Redis
REDIS_URL=redis://{{ redis_host }}:{{ redis_port | default(6379) }}/{{ redis_db | default(0) }}
# S3
AWS_REGION={{ aws_region }}
AWS_S3_BUCKET={{ s3_bucket }}
# Feature Flags
{% for flag, enabled in feature_flags.items() %}
FEATURE_{{ flag | upper }}={{ 'true' if enabled else 'false' }}
{% endfor %}
Prometheus 告警规则
{# templates/alert_rules.yml.j2 #}
groups:
- name: {{ app_name }}.rules
interval: 60s
rules:
{% for slo in slos %}
- alert: {{ slo.name }}ErrorRateTooHigh
expr: |
rate(http_requests_total{job="{{ app_name }}", status=~"5.."}[5m])
/
rate(http_requests_total{job="{{ app_name }}"}[5m])
> {{ slo.error_budget | default(0.01) }}
for: {{ slo.for | default('5m') }}
labels:
severity: {{ slo.severity | default('warning') }}
annotations:
summary: "{{ app_name }} error rate too high"
description: "Error rate is {{ '{{ $value | humanizePercentage }}' }}"
{% endfor %}
lookup 插件:读取外部数据
tasks:
- name: 读取本地 SSH 公钥
authorized_key:
user: deploy
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
- name: 读取环境变量
debug:
msg: "AWS_REGION = {{ lookup('env', 'AWS_REGION') }}"
- name: 读取 Vault 密钥(HashiCorp Vault)
set_fact:
db_password: "{{ lookup('hashi_vault', 'secret=prod/db/password') }}"
- name: 生成随机密码
set_fact:
random_token: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters,digits') }}"
模板调试技巧
# 先用 debug 验证模板渲染结果,再写入文件
- name: 预览生成的配置内容
debug:
msg: "{{ lookup('template', 'templates/nginx.conf.j2') }}"
# 使用 assert 检查变量存在
- name: 检查必要变量
assert:
that:
- db_host is defined
- db_host | length > 0
- app_port | int > 0
fail_msg: "缺少必要变量,请检查 group_vars 配置"
下一节:多环境管理与 Vault 加密