本节摘要:角色是 Ansible 的代码组织单元:一组约定目录承载任务、变量、模板、文件与 handlers,play 只需一行调用。本节解剖标准目录结构,讲清 defaults 与 vars 的关键区别,示范参数化与依赖声明,并动手把一份剧本重构为角色。
判断该不该把某段剧本做成角色,看两个信号:同一段逻辑出现在第二个地方;这段逻辑开始拥有自己的配置参数。两者都指向同一个动作——把它封装成"接参数、做事情"的单元。角色正是这个单元的官方形态:引擎看到角色名,就按约定目录自动加载其中的任务、变量、模板与 handlers,你不需要写任何 include 语句。
标准目录结构(不要求全部存在,用哪个建哪个):
roles/nginx/ ├── defaults/ # 默认变量:优先级最低,天生为覆盖而生 │ └── main.yml ├── vars/ # 角色内部变量:优先级高,不该被外部覆盖 │ └── main.yml ├── tasks/ # 任务主体(入口固定为 main.yml) │ └── main.yml ├── handlers/ │ └── main.yml ├── templates/ # 模板文件(角色内路径免写全路径) ├── files/ # 直接分发的静态文件 ├── meta/ # 元数据:依赖、最低引擎版本 │ └── main.yml └── README.md # 参数说明——不写参数说明的角色迟早被重写
调用端极其简洁:
--- - name: web 层初始化 hosts: web roles: - role: nginx vars: nginx_worker_processes: 4 nginx_sites: - name: stardust port: 8080
这两个目录是角色设计的核心,也是最常见的误用点。defaults 里的变量优先级全站最低,任何上层(清单、play、额外变量)都能覆盖它——它的定位是"我的默认值,欢迎改写"。vars 里的变量优先级高于 play 级变量,外部几乎覆盖不了——它的定位是"我的内部常量与派生值,请勿动"。设计角色的第一件事就是划分这两类:凡属于"这个角色的用户可能想按环境调整"的,进 defaults;凡属于"内部计算结果或固定映射"的,进 vars。
# defaults/main.yml —— 可被环境覆盖的默认值 nginx_worker_processes: "{{ ansible_processor_vcpus | default(2) }}" nginx_worker_connections: 1024 nginx_sites: [] # vars/main.yml —— 内部常量与派生值 nginx_conf_dir: /etc/nginx/conf.d nginx_package_name: nginx-full
注意 defaults 里那个事实引用:默认值按目标机核数自适应,覆盖方不需要知道核数也能拿到合理行为——好的默认值应当"不覆盖也正确"。
tasks/main.yml 是角色执行入口,长了就拆文件再 include:
# tasks/main.yml --- - name: 安装 nginx ansible.builtin.apt: name: "{{ nginx_package_name }}" state: present - name: 引入站点配置任务 ansible.builtin.include_tasks: sites.yml - name: 确保服务运行 ansible.builtin.systemd: name: nginx state: started enabled: true
拆分的粒度建议按"能力域"而不是按行数:安装一块、站点配置一块、加固一块,每块一个文件。角色内部的模板与文件引用享受路径捷径——templates 目录下的文件在任务里直接写文件名,引擎自动到角色目录找。
meta/main.yml 声明角色的元信息。最有用的是角色依赖:
# meta/main.yml dependencies: - role: common_baseline - role: firewall vars: firewall_open_ports: [80, 443]
调用 nginx 角色时,引擎先装 common_baseline 与 firewall(后者带定制的端口参数)再执行 nginx 本体。依赖让"部署应用前必须先有基线与防火墙"这类制度性约束写进代码。依赖会传递,注意避免 A 依赖 B、B 又依赖 A 的环。
角色里的任务打上 tag 后,调用方可以 ansible-playbook --tags config 只跑配置部分、--skip-tags reboot 跳过重启。写法是在任务或 role 上加 tags 关键字。一套实用的 tag 词汇表:install、config、start、audit 四类覆盖大多数角色,全团队统一词汇比 tag 多更重要。

动手任务:把第 3.3 节的发布剧本中"渲染运行配置"与"重启应用"两个动作连同模板抽成 app_runtime 角色,参数化应用名与端口,原剧本改为调用角色。完成后对照三问自查:defaults 里是否有"不覆盖也正确"的值?vars 里是否混进了本该开放覆盖的参数?README 是否说明了每个参数的类型与默认值?
何时该写角色、何时不该,给一组成对的信号帮助判断。该写角色的信号:同一段逻辑出现第二处;这段逻辑拥有自己的配置参数;新人理解系统时反复问到同一块"它是怎么部署的"。不该写的反信号:逻辑只服务一个 play 且看不到第二消费者;参数超过两个且都取固定值(没有可变性就没有封装价值);拆角色的主要动机是"主剧本文件太长"(这是拆任务文件的理由,不是拆角色的理由——4.1 开头的判断标准里,角色是复用单元,不是文件整理工具)。反信号比信号更值得背下来:实践中一半的过度角色化来自把"整理代码"误当成"组织资产"。另一个实践校验是"角色命名测试":如果这个角色无法用一个不含"以及、然后、顺便"的名字说清职责(比如 nginx 而不是 nginx和监控和日志轮转),说明它装的不是一个概念——拆掉重想,比带着模糊边界运行划算。