深色模式
配置生成模板
摘要:几十台机器、多个环境的配置如果靠复制粘贴修改,迟早会出现「某台机器配置不一致」。本文用 Python
string.Template和 Jinja2 两种方式实现配置批量生成,并强调变量分离、渲染前校验与幂等写入。
适用环境
bash
python3 --version
pip install jinja2 pyyaml
mkdir -p /tmp/cfg/{templates,out}1
2
3
2
3
操作步骤
一、把「变」与「不变」分开
text
不变:配置的骨架、参数顺序、注释说明 → 放进模板
变化:IP、端口、域名、环境开关 → 放进变量文件1
2
2
二、用标准库 string.Template(无依赖)
python
#!/usr/bin/env python3
"""用 string.Template 批量渲染配置文件(无第三方依赖)。"""
import argparse
import json
import os
from string import Template
TEMPLATE = """# 由脚本生成,请勿手工修改
# 生成时间: ${generated_at}
[server]
listen = ${listen_port}
worker = ${worker_count}
env = ${env}
[upstream]
api = ${api_endpoint}
timeout_ms = ${timeout_ms}
"""
def render(values: dict, template: str) -> str:
tpl = Template(template)
# safe_substitute 在变量缺失时保留占位符而不是抛错,便于发现漏配
return tpl.safe_substitute(values)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--vars", required=True, help="变量 JSON 文件")
ap.add_argument("--out", required=True, help="输出目录")
args = ap.parse_args()
data = json.load(open(args.vars, encoding="utf-8"))
os.makedirs(args.out, exist_ok=True)
for name, values in data.items():
import datetime
values.setdefault("generated_at", datetime.datetime.now().isoformat(timespec="seconds"))
content = render(values, TEMPLATE)
if "${" in content:
raise SystemExit(f"{name} 存在未替换的占位符,请检查变量文件")
path = os.path.join(args.out, f"{name}.conf")
with open(path, "w", encoding="utf-8") as f:
f.write(content)
print("生成", path)
if __name__ == "__main__":
main()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
变量文件:
json
{
"web-prod": {"listen_port": 8080, "worker_count": 8, "env": "prod",
"api_endpoint": "https://api.example.com", "timeout_ms": 3000},
"web-staging": {"listen_port": 8080, "worker_count": 2, "env": "staging",
"api_endpoint": "https://api-staging.example.com", "timeout_ms": 5000}
}1
2
3
4
5
6
2
3
4
5
6
注意
渲染后一定要检查是否还有未替换的 ${...} 占位符。变量文件少一个键时,模板会静默生成一份「看起来正常但值为空」的配置,上线后才暴露。
三、用 Jinja2(支持条件与循环)
复杂场景(列表、条件分支、继承)用 Jinja2 更合适:
bash
pip install jinja2 pyyaml1
python
#!/usr/bin/env python3
"""用 Jinja2 + 环境清单批量生成 Nginx 配置。"""
import argparse
import os
from datetime import datetime
import yaml
from jinja2 import Environment, FileSystemLoader, StrictUndefined
NGINX_TPL = """# 自动生成,请勿手工修改
upstream {{ app }} {
{% for h in backends %}
server {{ h }}:{{ port }} max_fails=3 fail_timeout=10s;
{% endfor %}
}
server {
listen {{ listen }};
server_name {{ domain }};
location / {
proxy_pass http://{{ app }};
proxy_connect_timeout {{ timeout }}s;
}
{% if enable_tls %}
listen 443 ssl;
ssl_certificate {{ cert }};
ssl_certificate_key {{ key }};
{% endif %}
}
"""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--inventory", required=True, help="环境清单 YAML")
ap.add_argument("--out", required=True)
args = ap.parse_args()
inv = yaml.safe_load(open(args.inventory, encoding="utf-8"))
env = Environment(loader=FileSystemLoader("."),
undefined=StrictUndefined, # 变量缺失直接报错
trim_blocks=True, lstrip_blocks=True)
os.makedirs(args.out, exist_ok=True)
for name, cfg in inv["apps"].items():
cfg.setdefault("generated_at", datetime.now().isoformat(timespec="seconds"))
content = env.from_string(NGINX_TPL).render(**cfg)
path = os.path.join(args.out, f"{name}.conf")
open(path, "w", encoding="utf-8").write(content)
print("生成", path)
if __name__ == "__main__":
main()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
清单文件:
yaml
apps:
order:
app: order
listen: 80
port: 8080
domain: order.example.com
backends: [10.0.1.11, 10.0.1.12]
timeout: 3
enable_tls: true
cert: /etc/nginx/certs/order.pem
key: /etc/nginx/certs/order.key
gateway:
app: gateway
listen: 80
port: 9000
domain: gw.example.com
backends: [10.0.1.21]
timeout: 5
enable_tls: false1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
四、渲染前校验
bash
# Nginx 配置语法检查(最实用的一道保险)
nginx -t -c /etc/nginx/nginx.conf
# 其它服务也大多提供 configtest
sshd -t
named-checkconf
python3 -c "import json,sys; json.load(open(sys.argv[1]))" cfg.json1
2
3
4
5
6
7
2
3
4
5
6
7
五、幂等写入:内容不变就不要写
python
def write_if_changed(path: str, content: str) -> bool:
"""内容一致则不写,返回是否发生了变更"""
if os.path.exists(path):
with open(path, encoding="utf-8") as f:
if f.read() == content:
print(f"未变更 {path}")
return False
with open(path, "w", encoding="utf-8") as f:
f.write(content)
print(f"已更新 {path}")
return True1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
这样做的好处:不会无故更新文件 mtime,避免触发不必要的服务重载;在 Git 中也不会出现空 diff。
六、生成后重载服务
bash
# 先校验再重载,避免一次配置错误打挂整个服务
nginx -t && systemctl reload nginx
# 失败时回滚
nginx -t || { cp -a /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf; systemctl reload nginx; }1
2
3
4
2
3
4
危险
绝不要在未做语法校验的情况下直接 systemctl reload。配置错误会导致 reload 后服务不可用,而生成脚本往往是一次影响全部机器的。
验证
- [ ] 渲染输出中不含未替换占位符
- [ ] 清单里删掉一个变量后,Jinja2 的
StrictUndefined能报错 - [ ] 重复执行两次,第二次显示「未变更」
- [ ] 生成后
nginx -t通过
bash
python3 gen.py --inventory inv.yaml --out /tmp/cfg/out && ls /tmp/cfg/out
python3 gen.py --inventory inv.yaml --out /tmp/cfg/out # 第二次应提示未变更1
2
2
常见坑
- 变量缺失静默通过:务必用
StrictUndefined或渲染后检查占位符。 - 模板里写死环境相关值:换个环境就要改模板,应全部通过变量注入。
- 生成即覆盖:没有备份和校验,一次错误生成覆盖全部正确配置。
- 忘记重载或重载失败:生成完没让配置生效,或重载失败没回滚。
- 敏感值进模板:证书私钥、密码应只放路径引用,不写进模板或清单。