Ansible Roles 与 Jinja2:部署剧本工程化实战
单文件剧本写到三百行的时候,问题开始集中爆发:NFS 的部署逻辑和 rsync 的混在同一份文件里,改一处要通读全文;换台机器复用,全靠复制粘贴。后来我把所有剧本按官方的 roles 规范重构了一遍,顺带用 Jinja2 解决了”每台机器配置不一样”的老大难。这篇把重构的思路和三个实战案例整理出来。
一、为什么要 roles
roles 解决的是解耦:把”装 Nginx””装 MySQL””基础优化”各自拆成独立模块,谁需要就引用谁。
对比一下两种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| # 重构前:所有步骤堆在一个剧本里 deploy_all.yml ├── 基础优化 30 行 ├── NFS 安装配置 60 行 ├── rsync 安装配置 80 行 └── 应用部署 100 行
# 重构后:按角色拆分 roles/ ├── base/ # 基础优化 ├── install_nfs/ # NFS ├── rsync/ # rsync └── app/ # 应用 site.yml # 一个入口文件把角色串起来
|
好处很直接:新项目要用基础优化,直接引用 base 角色;改 NFS 的逻辑不会碰到 rsync 的代码。每个角色最好只干一件事,这是 roles 的核心纪律。
二、Roles 的目录结构
每个角色就是一整套标准目录,各目录各司其职:
1 2 3 4 5 6 7 8 9
| roles/install_nfs/ ├── tasks/main.yml # 任务清单(核心) ├── handlers/main.yml # 触发器 ├── templates/exports.j2 # Jinja2 模板(带变量的配置文件) ├── files/ # 静态文件(原样分发的) ├── vars/main.yml # 变量 ├── defaults/main.yml # 低优先级默认变量 ├── meta/main.yml # 依赖关系 └── README.md
|
两个容易混的目录:files/ 放原样分发的文件,templates/ 放要渲染变量的模板(后缀 .j2)。用 copy 模块从 files 取文件,用 template 模块从 templates 取模板,取错目录会直接报找不到文件。
vars 和 defaults 的区别在优先级:vars/main.yml 优先级高(角色内部用),defaults/main.yml 优先级低(给使用者留的覆盖口子)。对外发布的角色,变量放 defaults 让别人能覆盖;内部实现细节放 vars。
用命令生成骨架最省事:
1
| ansible-galaxy init install_nfs
|
三、Jinja2:让配置文件适配每台机器
问题场景:100 台机器要装 Nginx,每台的监听地址、后端节点列表都不一样。把配置文件写死,等于要维护 100 份;用 Jinja2 模板,一份模板渲染出 100 份。
template 模块和 copy 模块的区别就一句话:template 会解析文件里的变量,copy 原样复制。之前推送 rsync 备份脚本想让脚本里带上主机名,用 copy 推过去就是字面的 {{ ansible_fqdn }},换成 template 才渲染成实际主机名。
Jinja2 的三种语法:
1 2 3
| {{ EXPR }} 输出变量值 {% for %}/{% if %} 逻辑控制 {# 注释 #}
|
第一个例子:按机器渲染 motd
1 2 3 4
| - name: 分发 motd template: src: ./motd.j2 dest: /etc/motd
|
1 2 3
| Welcome to {{ ansible_fqdn }} This system total mem is: {{ ansible_memtotal_mb }} MB This system free mem is: {{ ansible_memfree_mb }} MB
|
登录每台机器,看到的欢迎语带自己的主机名和实际内存——模板的价值一眼可见。
第二个例子:for 循环生成负载均衡后端
1 2 3 4 5
| upstream {{ server_name }} { {% for n in range(21) %} server 172.18.1.{{ n }}:{{ up_port }}; {% endfor %} }
|
配合剧本执行,一份模板直接把整段 upstream 节点列表渲染出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| - hosts: lb_group vars: http_port: 80 server_name: www.example.com up_port: 80 tasks: - name: 渲染负载均衡配置 template: src: ./www.example.com.conf.j2 dest: /etc/nginx/conf.d/www.example.com.conf notify: reload nginx
handlers: - name: reload nginx systemd: name: nginx state: reloaded
|
生产上更稳的做法是用 groups 遍历主机列表,而不是手写 range——节点增减只改清单,配置自动跟着变。
第三个例子:if 判断区分主备
keepalived 的主备配置差异(MASTER/BACKUP、优先级)用一段 if 搞定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| global_defs { router_id {{ ansible_fqdn }} }
vrrp_instance VI_1 { {% if ansible_fqdn == "lb01" %} state MASTER priority 150 {% else %} state BACKUP priority 100 {% endif %} interface eth0 virtual_router_id 50 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 10.0.8.3 } }
|
一份模板推给两台机器,主备身份自动分配。记住一个限制:Jinja2 的条件和循环只能写在模板文件里,不能写在 playbook 里——playbook 里的逻辑要用 when 和 loop。
四、实战一:用 roles 重构 NFS 部署
1
| ansible-galaxy init install_nfs
|
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
| --- - name: 安装 NFS 服务端 yum: name: nfs-utils state: present
- name: 渲染 exports 配置 template: src: exports.j2 dest: /etc/exports notify: 重启 NFS 服务
- name: 创建共享目录 file: path: "{{ share_dir }}" state: directory owner: www group: www mode: '0755'
- name: 启动 NFS 服务 systemd: name: nfs state: started enabled: yes
|
1 2 3 4 5
| - name: 重启 NFS 服务 systemd: name: nfs state: restarted
|
1 2
| # roles/install_nfs/templates/exports.j2 {{ share_dir }} {{ share_ip }}(rw,sync,all_squash,anonuid=666,anongid=666)
|
1 2 3
| share_dir: /data share_ip: 172.18.1.31
|
1 2 3 4 5 6 7 8
| - name: 部署 NFS 服务端 hosts: "{{ target_group | default('all') }}" remote_user: root roles: - role: install_nfs when: "'nfs01' in inventory_hostname" tags: nfs
|
1
| ansible-playbook -i inventories/prod/hosts -t nfs site.yml
|
注意上面 share_dir、share_ip 这些变量的用法:任务文件里不写死路径和 IP,全部引用变量,变量值放在 vars 或 group_vars 里。这样做完,同一套角色换个环境只改变量文件。
五、实战二:rsync 备份服务端 + 客户端
这是最典型的一组”服务端配置 + 客户端脚本”的搭档部署:
1 2 3 4 5
| - hosts: prod_backup remote_user: root roles: - rsync
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| - name: 安装 rsync yum: name: rsync state: present
- name: 分发配置与认证文件(循环带不同权限) copy: src: "{{ item.src }}" dest: "/etc/{{ item.dest }}" mode: "{{ item.mode }}" loop: - { src: "rsyncd.conf", dest: "rsyncd.conf", mode: "0644" } - { src: "rsync.passwd", dest: "rsync.passwd", mode: "0600" } notify: 重启 rsync 服务
- name: 启动 rsync 服务 systemd: name: rsyncd state: started enabled: yes
|
一个细节值得强调:rsyncd.conf 和 rsync.passwd 的权限不一样,用字典循环把”文件 + 目标 + 权限”打包成一组参数,一个任务搞定。认证文件 0600 是硬要求,rsync 对密码文件权限过宽会直接拒绝启动。
服务端跑通之后,客户端侧是把备份脚本推下去、加进 crontab:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| - hosts: prod_web tasks: - name: 下发备份脚本 copy: src: ./backup.sh dest: /root/backup.sh mode: '0755'
- name: 添加定时备份任务 cron: name: "rsync-backup-daily" user: root minute: "30" hour: "1" job: "/bin/bash /root/backup.sh >> /var/log/backup.log 2>&1"
|
部署完用 ansible-playbook --syntax-check 过一遍语法,再用 -C 干跑确认变更范围,最后才正式执行——这套”检查三连”用在所有生产剧本上。
六、实战三:playbook 部署 LAMP
最后看一个完整项目:用剧本部署 httpd + PHP + MariaDB + WordPress:
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
| - hosts: prod_web tasks: - name: 安装 LAMP 组件 yum: name: "{{ packages }}" vars: packages: - httpd - mariadb-server - php - php-mysql - php-pdo
- name: 启动 httpd systemd: name: httpd state: started enabled: yes
- name: 启动 mariadb systemd: name: mariadb state: started enabled: yes
- name: 下载 WordPress get_url: url: "http://files.example.com/software/wordpress-5.0.3-zh_CN.tar.gz" dest: /var/www/html
- name: 解压 unarchive: src: /var/www/html/wordpress-5.0.3-zh_CN.tar.gz dest: /var/www/html remote_src: yes
|
这个例子里所有服务写在一个文件中,只适合练手。按前面讲的 roles 纪律,生产环境应该拆成 httpd、php、mariadb 三个独立角色,各自带自己的任务、模板和变量——这样任何项目要装 PHP,引用一下就行。
项目跑通后可以顺着往深挖两步:用 mysql_db 和 mysql_user 模块建库建账号(代码里不用再手敲 grant 语句),再把 WordPress 的数据库连接信息用 Jinja2 模板渲染进 wp-config.php。一套完整的”部署 + 配置”闭环就形成了。
七、角色依赖与社区角色
角色之间可以声明依赖。比如 WordPress 依赖 Nginx 和 PHP,写在 meta 里:
1 2 3 4
| dependencies: - { role: nginx } - { role: php }
|
执行到 wordpress 角色时,Ansible 会先跑 nginx 和 php——部署顺序不用在 site.yml 里手工排。
社区角色库 Ansible Galaxy 值得会用:
1 2 3 4
| ansible-galaxy search nginx ansible-galaxy info geerlingguy.nginx ansible-galaxy install geerlingguy.nginx ansible-galaxy list
|
社区角色适合做参考和起步,但生产环境我建议核心角色自己写:社区角色的变量、行为不一定符合你的规范,出了问题也不好排查。参考它的目录组织和变量设计,比直接用更值。
收尾的一点感受
从单文件剧本到 roles 工程化,核心变化就三点:一个角色只干一件事、可变内容全部进变量、配置差异交给 Jinja2 模板。做到这三点,你的一套部署代码就能从几台机器的练习环境,平滑地复制到上百台的业务环境——这才是自动化工具真正的价值所在。