Jinja2 模板实战
High Contrast
Dark Mode
Light Mode
Sepia
Forest
1 min read223 words

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 加密