别再手动改配置文件了,Ansible 模板系统一次搞定所有环境
一个让人血压飙升的场景
先讲个故事。
上个月帮一个朋友排查线上问题——他们有个 Nginx 服务突然挂了,502 错误一片。
登录服务器一看,nginx.conf 里 upstream 指向的是旧 IP 地址。问了一圈,发现是上周运维改了个后端地址,改完以后忘了同步到生产环境的 Nginx 配置。更离谱的是,开发环境和测试环境的配置也不一样,三个环境三个版本,每个都是手动复制粘贴改出来的。
我问他:"你们 Nginx 配置怎么管理的?"
他说:"呃……用 SSH 登录上去改啊。"
这个场景我见过太多太多了。
小团队刚开始都是这么干的——SSH 上去 vim nginx.conf,改一个参数,:wq,nginx -s reload。简单直接。但等你有了三五台服务器,两三个环境,这套玩法就会让你崩溃。
Ansible 的模板系统(Jinja2)就是来解决这个问题的。它不是让你不写配置文件了,而是让你写一次,到处运行。
先搞清楚:什么是模板?
简单来说,模板就是一个"带变量的配置文件"。你写一份配置文件模板,把需要变化的部分(IP、端口、域名、路径等等)用变量占位,然后 Ansible 在运行的时候把这些变量替换成真实值。
听起来很简单对吧?
但它的威力远不止"替换变量"这么简单。Jinja2 模板引擎支持条件判断、循环、过滤器、继承……你可以用它写出非常灵活的配置模板。
在 Ansible 里,模板文件以 .j2 结尾,放在 templates/ 目录下。然后用 template 模块来渲染它。
入门案例:一个 Nginx 配置模板
假设你有三个环境:dev、staging、prod。每个环境的 Nginx 配置大同小异,但端口、域名、后端地址不一样。
传统做法:
用 Ansible 模板的做法:
先创建模板文件 templates/nginx.conf.j2:
upstream backend {
{% for server in nginx_upstream_servers %}
server {{ server }} weight=1;
{% endfor %}
}
server {
listen {{ nginx_port }};
server_name {{ nginx_server_name }};
location / {
proxy_pass http://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 nginx_enable_ssl %}
listen 443 ssl;
ssl_certificate /etc/ssl/certs/{{ nginx_domain }}.pem;
ssl_certificate_key /etc/ssl/private/{{ nginx_domain }}.key;
{% endif %}
access_log /var/log/nginx/{{ nginx_domain }}_access.log;
error_log /var/log/nginx/{{ nginx_domain }}_error.log;
}
然后为每个环境定义变量。放在 group_vars/ 目录下:
# group_vars/dev.yml
nginx_port: 8080
nginx_server_name: dev.example.com
nginx_upstream_servers:
- "127.0.0.1:3000"
nginx_enable_ssl: false
nginx_domain: dev.example.com
# group_vars/staging.yml
nginx_port: 80
nginx_server_name: staging.example.com
nginx_upstream_servers:
- "10.0.1.10:3000"
- "10.0.1.11:3000"
nginx_enable_ssl: true
nginx_domain: staging.example.com
# group_vars/prod.yml
nginx_port: 80
nginx_server_name: example.com
nginx_upstream_servers:
- "10.0.2.10:3000"
- "10.0.2.11:3000"
- "10.0.2.12:3000"
nginx_enable_ssl: true
nginx_domain: example.com
最后写一个简单的 Playbook:
---
- name: 部署 Nginx 配置
hosts: all
become: yes
tasks:
- name: 渲染并部署 Nginx 配置
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: reload nginx
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
然后分别跑:
ansible-playbook -i inventory/dev.ini nginx_deploy.yml
ansible-playbook -i inventory/staging.ini nginx_deploy.yml
ansible-playbook -i inventory/prod.ini nginx_deploy.yml
看到区别了吗?一份模板文件,三套变量,三个环境独立执行。以后要改配置,只需要改模板文件,然后重新跑一遍 Playbook,所有环境自动更新。
再进一步:用模板管理你的 Python/Node 应用配置
Nginx 只是冰山一角。Jinja2 模板最大的价值在于,它可以用在任何配置文件上。
比如一个 Django 应用的 settings.py,通常要管理数据库连接、Redis 地址、密钥、调试模式、日志级别等等。不同环境这些值都不同。传统的 .env 文件管理方法很容易出现"开发环境跑得好好的,上生产就崩"的情况。
用模板来解决:写一个 local_settings.py.j2:
DEBUG = {{ app_debug | lower }}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.{{ db_engine }}',
'NAME': '{{ db_name }}',
'USER': '{{ db_user }}',
'PASSWORD': '{{ db_password }}',
'HOST': '{{ db_host }}',
'PORT': {{ db_port }},
}
}
{% if redis_enabled %}
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://{{ redis_host }}:{{ redis_port }}/{{ redis_db }}',
}
}
{% endif %}
LOGGING['root']['level'] = '{{ log_level }}'
开发环境变量:
app_debug: true
db_engine: sqlite3
db_name: dev.db
# 数据库其他字段留空或设默认值
redis_enabled: false
log_level: DEBUG
生产环境变量:
app_debug: false
db_engine: mysql
db_name: myapp_prod
db_user: myapp
db_password: "{{ vault_db_password }}"
db_host: 10.0.3.100
db_port: 3306
redis_enabled: true
redis_host: 10.0.3.101
redis_port: 6379
redis_db: 0
log_level: WARNING
注意到 vault_db_password 了吗?这是 Ansible Vault 加密的变量。敏感信息(数据库密码、API Key、密钥)可以用 ansible-vault 加密后放在变量文件里,Playbook 运行时自动解密。这样你甚至可以把变量文件提交到 Git 仓库里——别人看不到明文,但 Playbook 可以正常使用。
模板的高级技巧
当你开始用模板管理配置之后,你会遇到一些"单变量替换不够用"的场景。Jinja2 提供了丰富的功能来应对:
技巧 1:循环生成列表配置
比如 Redis 集群的 sentinel 配置,你不知道有多少个节点的时候:
sentinel monitor mymaster {{ redis_master_ip }} {{ redis_master_port }} 2
{% for sentinel in redis_sentinels %}
sentinel known-sentinel mymaster {{ sentinel }}
{% endfor %}
sentinel down-after-milliseconds mymaster {{ sentinel_down_after | default(5000) }}
sentinel failover-timeout mymaster {{ sentinel_failover_timeout | default(180000) }}
技巧 2:条件渲染
同一个模板适应不同规模的环境:
worker_processes {{ nginx_worker_processes | default('auto') }};
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
{% if nginx_use_epoll %}
use epoll;
{% endif %}
}
http {
{% if nginx_enable_gzip %}
gzip on;
gzip_types text/plain text/css application/json application/javascript;
{% endif %}
{% if nginx_rate_limiting %}
limit_req_zone $binary_remote_addr zone=mylimit:10m rate={{ nginx_rate_limit }};
{% endif %}
技巧 3:默认值
用 default() 过滤器设置默认值,这样变量文件里可以只写需要覆盖的部分:
max_connections = {{ postgres_max_connections | default(100) }}
shared_buffers = {{ postgres_shared_buffers | default('256MB') }}
effective_cache_size = {{ postgres_effective_cache_size | default('1GB') }}
技巧 4:从 host_vars 获取服务器级别的差异
有时候同环境的不同服务器也需要不同的配置(比如内存大小不同):
# host_vars/web01.yml
nginx_worker_connections: 2048
app_memory_limit: 512m
# host_vars/web02.yml
nginx_worker_connections: 4096
app_memory_limit: 1g
模板执行时,Ansible 会自动合并 group_vars 和 host_vars,host_vars 的优先级更高。所以你的模板不用做任何特殊处理——自动就是服务器感知的。
实战案例:一个完整的 Redis 配置管理
结合上面学到的技巧,我们来做一个完整的 Redis 配置管理方案。
模板文件 templates/redis.conf.j2:
# Redis 配置文件 - 由 Ansible 模板管理,禁止手动修改
port {{ redis_port | default(6379) }}
daemonize no
pidfile /var/run/redis/redis-server.pid
loglevel {{ redis_loglevel | default('notice') }}
logfile {{ redis_logfile | default('/var/log/redis/redis-server.log') }}
# 持久化配置
{% if redis_persistence == 'rdb' %}
save 900 1
save 300 10
save 60 10000
rdbcompression yes
dbfilename dump.rdb
{% elif redis_persistence == 'aof' %}
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
{% elif redis_persistence == 'both' %}
save 900 1
save 300 10
save 60 10000
rdbcompression yes
dbfilename dump.rdb
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
{% endif %}
# 内存管理
maxmemory {{ redis_maxmemory | default('256mb') }}
maxmemory-policy {{ redis_maxmemory_policy | default('allkeys-lru') }}
# 安全
{% if redis_password is defined %}
requirepass {{ redis_password }}
{% endif %}
rename-command FLUSHALL ""
rename-command FLUSHDB ""
# 连接限制
maxclients {{ redis_maxclients | default(10000) }}
timeout {{ redis_timeout | default(300) }}
tcp-keepalive {{ redis_tcp_keepalive | default(60) }}
然后为不同环境设置不同的变量组合。开发环境用最小的内存、最精简的配置。生产环境开 AOF、大内存、强密码。
有了这套方案,你再也不用 SSH 上去改 redis.conf 了。改配置只需要改变量文件,然后跑一条命令。而且所有改动都会记录在 Ansible 的执行日志里,出了事可以快速回溯。
模板 vs 直接复制文件:什么时候用什么
并不是所有配置文件都需要模板。Ansible 有两种方式来管理配置文件:
一个简单的方法论:如果你的配置文件里有一个数字或者字符串需要根据环境变化,就上模板。如果永远不变,copy 就行。
踩坑总结:用模板常见的 5 个坑
这些坑我基本都踩过,分享出来让大家少走弯路:
default(),要么在上线前用 ansible-playbook --syntax-check 验证模板。
坑 2:模板缩进被 Jinja2 破坏。 Jinja2 的 {% %} 标签会引入空行。解决:在标签里加 -,比如 {% for item in items -%} 和 {%- endfor %} 来吃掉多余的空行。
坑 3:Jinja2 和 Nginx 的语法冲突。 Nginx 配置里也可能用到 {{ }} 变量语法(比如在 log_format 里)。可以在模板中用 {% raw %}...{% endraw %} 包裹那些不需要 Jinja2 处理的部分。
坑 4:重新加载服务失败没有回滚。 渲染后的配置文件有问题,reload 失败可能导致服务中断。最佳实践:部署前先用 nginx -t 验证配置是否有效,然后再 reload。
坑 5:变量文件太多找不到北。 环境多了以后,变量文件容易变得散乱。建议按角色和层次组织:group_vars/all.yml 放全局默认值,group_vars/prod.yml 放环境覆盖,host_vars/ 放单机差异。
工作流建议:怎么把模板管起来
最后分享一个我推荐的项目结构:
ansible-config/
├── inventory/
│ ├── dev.ini
│ ├── staging.ini
│ └── prod.ini
├── group_vars/
│ ├── all.yml
│ ├── dev.yml
│ ├── staging.yml
│ └── prod.yml
├── host_vars/
│ ├── web01.yml
│ └── web02.yml
├── templates/
│ ├── nginx.conf.j2
│ ├── redis.conf.j2
│ ├── mysql.cnf.j2
│ └── app_config.yml.j2
├── deploy_nginx.yml
├── deploy_redis.yml
├── deploy_app.yml
├── ansible.cfg
└── README.md
把这个项目放到 Git 仓库里。团队的每个成员都可以 clone、修改、提交。用 Git 分支来管理配置变更——比如"feature/upgrade-nginx"分支测试好以后,合并到 main 分支再部署。
更高级一点,可以在 CI/CD 流水线里加一个步骤:每次有人提交 PR 修改了模板,自动跑一遍 ansible-playbook --syntax-check --check 来验证。这样配置错误在上线前就被拦截了。
写在最后
回到开头那个 502 事故。
排查完以后,我帮他们把 Nginx、Redis、应用配置全部用 Ansible 模板管理了起来。从那以后,这个团队再也没因为"配置文件忘了同步"出过事故。
那个朋友后来跟我说:"早知道模板这么好用,我就不用手动改两年配置文件了。"
你如果现在还在一台一台登录服务器改配置,认真考虑一下这套方案。花半天时间迁移,接下来好几年都省心。
有什么问题或者自己的踩坑经历,欢迎留言交流。
夜雨聆风