外观
项目 11:Ansible Roles 工程化实践
约 3888 字大约 13 分钟
AnsibleRoles工程化site.yml
2026-06-06
项目目标
第 10 课完成了单个 init_server.yml Playbook,但随着初始化任务增多,一个文件里混杂了用户管理、SSH 配置、防火墙规则和运行时安装——维护困难且难以复用。
本课把第 10 课的 Playbook 重构为 Ansible Roles 工程:按照 common、security、firewall、runtime 等职责拆分,编写 site.yml 统一调度,并通过 tags 实现按模块选择性执行,使用 Ansible Vault 保护敏感变量。
环境说明:控制端为 WSL Ubuntu,受控端为 Docker 容器(3 台 Ubuntu 节点),Ansible 通过 localhost 端口映射连接各容器。
最终提交完整的 roles/ 目录、site.yml、变量文件和运行截图。
一、从 Playbook 到 Roles:为什么要拆分
第 10 课的 init_server.yml 把所有 task 写在一个文件里,这在任务数量较少时可以接受。但当初始化的内容从 4 个模块扩展到 10 个模块时,单文件 Playbook 的三个痛点会越来越明显:
| 痛点 | 单文件 Playbook | Roles 工程 |
|---|---|---|
| 可读性 | 一个文件几百行,上下翻找 task | 每个 role 职责单一,目录即文档 |
| 可复用性 | 换一个项目要把 task 复制粘贴 | 把 security role 直接拷贝到新项目即可 |
| 可测试性 | 改一个 task 要跑整个 Playbook | 用 --tags 只跑要测试的 role |
| 团队协作 | 多人修改同一文件容易冲突 | 每人维护自己负责的 role 目录 |
Ansible Roles 本质上是一种约定优于配置的目录结构。你不需要在 site.yml 中声明每个 role 包含哪些文件——只要目录结构符合约定,Ansible 就会自动加载 tasks/main.yml、vars/main.yml、templates/ 等。
1.1 Roles 的自动加载机制
Ansible Roles 自动加载机制
Ansible 如何按特定顺序自动发现和执行 Role 中的任务、变量、模板和 Handlers
📂 roles/common/
⬜
tasks/main.yml
⬜
handlers/main.yml
(可选)
⬜
defaults/main.yml
(可选)
⬜
vars/main.yml
(可选)
⬜
templates/
(可选)
⬜
files/
(可选)
⬜
meta/main.yml
(可选)
site.yml
- hosts: all roles: - role: commonAnsible 引擎日志
Ansible 在执行 role 时,会按以下顺序自动搜索并加载文件:
关键在于:每个目录都是可选的。如果一个 role 没有 templates,目录里就不需要 templates/ 文件夹;如果一个 role 没有自定义变量,就不需要 vars/main.yml。Ansible 不会因为缺少某个文件而报错——它只会加载存在的文件。
二、Roles 目录结构
问题:Ansible 中用于存放工程化角色目录的顶层文件夹名是什么?
一个完整的 role 包含以下标准目录:
| 目录/文件 | 加载方式 | 职责 |
|---|---|---|
tasks/main.yml | 自动加载(必须存在) | role 的主任务列表 |
handlers/main.yml | 自动加载 | 该 role 专属的 handlers |
vars/main.yml | 自动加载 | 角色内部变量(优先级高于 defaults) |
defaults/main.yml | 自动加载 | 角色默认变量(优先级最低,便于覆盖) |
templates/ | template 模块引用 | Jinja2 模板文件 |
files/ | copy/script 模块引用 | 静态文件(公钥、脚本、二进制) |
meta/main.yml | 自动加载 | 声明 role 依赖和元信息 |
tests/ | 不自动加载 | 测试用的 Playbook 和 Inventory |
2.1 defaults 与 vars 的变量作用域
Ansible 变量优先级机制 (Variable Scope)
像漏斗一样层层筛选:高优先级的定义将覆盖低优先级
变量来源 (优先级从低到高 ↓)
层级 1
Role defaults/main.yml
ssh_port: 22层级 2
group_vars/all.yml
未定义
层级 3
host_vars/node1.yml
ssh_port: 2222层级 4
Role vars/main.yml
未定义
层级 5
Playbook vars (site.yml)
ssh_port: 8888层级 6
命令行 -e "ssh_port=9999"
ssh_port: 9999⚙️ Ansible 变量解析引擎
目前解析到的最终值:
等待解析...
这是 Roles 中最容易混淆的知识点。同一个变量可以出现在多个位置,Ansible 的变量优先级如下(从高到低):
| 优先级 | 位置 | 典型用途 |
|---|---|---|
| 最高 | 命令行 -e "var=value" | 临时覆盖,调试用 |
| ↑ | Playbook 中的 vars | 此 Playbook 特有的覆盖 |
| ↑ | group_vars/ | 按主机组区分的配置 |
| ↑ | host_vars/ | 按主机区分的配置 |
| ↑ | Role vars/main.yml | 角色内部硬性变量,很难被覆盖 |
| 最低 | Role defaults/main.yml | 角色默认值,容易被覆盖 |
关键差异:vars/main.yml 中的变量不容易被覆盖,它非常适合"这个 role 正常运行必须用这个值"的场景。defaults/main.yml 中的变量最容易被覆盖,适合"一般情况用这个值,但允许外部覆盖"的场景。
本课建议:所有端口、用户名、软件包列表等业务配置放到 defaults/main.yml,让 group_vars/ 和 site.yml 可以灵活覆盖。 不要把业务参数写死在 vars/main.yml,那样后续复用时会非常痛苦。
2.2 本课四个 Role 的职责划分
| Role | 职责 | 来源(第 10 课对应 task) |
|---|---|---|
common | 基础软件安装(vim, git, curl 等)、apt 缓存更新 | 安装基础软件和安全组件 |
security | 创建运维用户、配置 sudo、写入 SSH 公钥、下发 SSH Drop-in 模板 | 创建运维用户、配置 sudo 免密、写入 SSH 公钥、下发 SSH 安全 drop-in 模板 |
firewall | UFW 放行端口、设置默认策略、启用 UFW | 放行 SSH 端口、放行业务端口、设置 UFW 默认入站拒绝、启用 UFW |
runtime | 安装配置 fail2ban、容器运行时准备 | 配置 fail2ban sshd jail、启动 fail2ban |
common role 排在第一位,因为后续所有 role 都依赖基础工具已安装。security 排在 firewall 前面,因为防火墙需要知道 SSH 最终端口才能正确放行。
三、编写 site.yml
site.yml 是 Roles 工程的总入口,它只负责编排 role 的执行顺序和传递顶层变量。
问题:Ansible Roles 工程中用于统一调用所有角色并编排执行顺序的入口文件叫什么?
site.yml 的结构很简单:
---
- name: 初始化服务器集群
hosts: servers
become: true
roles:
- role: common
tags: [common]
- role: security
tags: [security]
- role: firewall
tags: [firewall]
- role: runtime
tags: [runtime]3.1 tags 的三种用法
tags 是 Roles 工程中最实用的功能之一,它允许你选择性执行某些 role 或 task:
# 只执行 security 相关的 role
ansible-playbook -i inventory.ini site.yml --tags security
# 执行 security 和 firewall,跳过 common 和 runtime
ansible-playbook -i inventory.ini site.yml --tags security,firewall
# 跳过耗时的 firewall 配置
ansible-playbook -i inventory.ini site.yml --skip-tags firewall
# 列出所有 tags
ansible-playbook -i inventory.ini site.yml --list-tagstags 可以打在 role 级别(如上面 site.yml 中的 tags: [security]),也可以打在 role 内部的单个 task 上:
# roles/security/tasks/main.yml
- name: 创建运维用户
ansible.builtin.user:
name: "{{ ops_user }}"
tags: [user, always]
- name: 下发 SSH 配置模板
ansible.builtin.template:
src: sshd-atsb.conf.j2
dest: /etc/ssh/sshd_config.d/99-atsb-security.conf
tags: [ssh, config]always 是一个特殊 tag——标记为 always 的 task 即使在 --skip-tags 中也会执行。适合放"无论用户选择跳过什么,这个检查都必须做"的安全 task。
3.2 group_vars 集中管理配置
当多个 role 需要共享配置(例如 SSH 端口在 security 和 firewall 中都需要),最好的做法是把共享配置放到 group_vars/all.yml:
# group_vars/all.yml
ops_user: ops
ssh_port: 2222
public_key_file: "{{ playbook_dir }}/keys/ops_ed25519.pub"
business_ports:
- 80
- 443
base_packages:
- vim
- git
- curl
- wget
- ca-certificates
- ufw
- fail2ban这样做的好处是:所有 role 都能读取这些变量,但每个 role 的 defaults/main.yml 可以为这些变量提供默认值——如果 group_vars/all.yml 定义了就用 group_vars 的值,否则回退到 role 默认值。
四、各 Role 的实现
4.1 common Role
common role 负责安装所有 role 都依赖的基础软件包:
问题:Ansible Role 中必须存在的、用于定义任务列表的文件名是什么?
common 的 defaults/main.yml:
# roles/common/defaults/main.yml
base_packages:
- vim
- git
- curl
- wget
- ca-certificates
- ufw
- fail2ban
- apt-transport-https
- software-properties-common4.2 security Role
security role 负责运维用户和 SSH 安全配置。它需要用到模板文件 sshd-atsb.conf.j2(与第 10 课相同):
问题:第 10 课和第 11 课中用于生成 SSH 安全 Drop-in 配置的 Jinja2 模板文件名是什么?
模板文件放在 roles/security/templates/sshd-atsb.conf.j2:
Port {{ ssh_port }}
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no模板路径的自动查找
在 role 的 task 中使用 template 模块时,如果 src 不包含路径前缀(如 src: sshd-atsb.conf.j2),Ansible 会从当前 role 的 templates/ 目录中查找。如果用 src: templates/sshd-atsb.conf.j2 反而会找错。这是 Roles 中最常见的文件路径错误。
4.3 firewall Role
firewall role 负责 UFW 规则管理:
问题:UFW 中用于放行端口规则的前两个命令单词是什么?
firewall 的 handlers/main.yml:
# roles/firewall/handlers/main.yml
- name: reload ufw
ansible.builtin.command: ufw reload
changed_when: true4.4 runtime Role
runtime role 负责 fail2ban 配置与服务管理:
问题:Linux 中监控日志并自动封禁恶意爆破 IP 的开源工具叫什么?
runtime 的 handlers/main.yml:
# roles/runtime/handlers/main.yml
- name: restart fail2ban
ansible.builtin.service:
name: fail2ban
state: restarted五、Ansible Vault 保护敏感数据
在真实项目中,group_vars/ 中可能包含数据库密码、API Token、云平台密钥等敏感信息。Ansible Vault 可以加密整个变量文件,只有持有密码的人才能解密和执行。
5.1 创建加密文件
# 创建加密的变量文件(会提示输入密码)
ansible-vault create group_vars/all/vault.yml
# 编辑已有加密文件
ansible-vault edit group_vars/all/vault.yml
# 加密一个已存在的明文文件
ansible-vault encrypt group_vars/all/secrets.yml问题:Ansible 中用于加密和保护敏感变量文件的命令前缀是什么?
5.2 使用加密文件执行 Playbook
# 交互式输入密码
ansible-playbook -i inventory.ini site.yml --ask-vault-pass
# 从文件读取密码(适合 CI/CD)
ansible-playbook -i inventory.ini site.yml --vault-password-file .vault_pass
# 使用环境变量
echo "my_vault_password" > .vault_pass
chmod 600 .vault_pass5.3 Vault 密码管理安全原则
| 原则 | 做法 | 违规后果 |
|---|---|---|
| 密码文件不入库 | .vault_pass 加入 .gitignore | 密码泄露到仓库历史中 |
| 文件权限最小 | chmod 600 .vault_pass | 同一台机器的其他用户可读取密码 |
| CI/CD 用环境变量 | --vault-password-file <(echo $VAULT_PASS) | 密钥硬编码在 CI 配置文件 |
| 定期轮换密码 | ansible-vault rekey | 长期不换的密码更容易泄露 |
六、Docker 测试环境(WSL 内运行)
本课沿用第 10 课的 Docker 三节点环境。控制端就是你的 WSL Ubuntu 本身,三台受控节点通过 Docker 容器在 WSL 内运行,通过 localhost 的不同端口映射区分。
6.1 快速搭建
先在项目根目录生成本课专用 SSH 密钥。把密钥放在项目 keys/ 目录里,后续 Inventory、Docker Compose 和 Role 都引用同一份文件,避免 ~/.ssh/ops_ed25519.pub 不存在时被 Docker 误创建成目录。
mkdir -p keys
test -f keys/ops_ed25519 || ssh-keygen -t ed25519 -f keys/ops_ed25519 -N "" -q
chmod 600 keys/ops_ed25519在 docker/ 目录中保存下面的 Dockerfile:
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
openssh-server \
python3 \
python3-apt \
sudo \
iptables \
rsyslog \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash ansible && \
echo 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansible
RUN mkdir /var/run/sshd
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
RUN mkdir -p /home/ansible/.ssh && \
chmod 700 /home/ansible/.ssh && \
chown -R ansible:ansible /home/ansible/.ssh
EXPOSE 22
CMD ["/bin/bash", "-c", "rsyslogd && /usr/sbin/sshd && tail -f /dev/null"]再保存 docker/docker-compose.yml:
问题:Docker 中用于根据 docker-compose.yml 启动多容器环境的命令是什么?
启动三台未初始化服务器:
cd docker
docker compose up -d --build
cd ..三台容器与 WSL localhost 的端口映射如下:
| 容器 | SSH 端口(初始) | SSH 端口(加固后) |
|---|---|---|
| node1 | localhost:2201 | localhost:2211 |
| node2 | localhost:2202 | localhost:2212 |
| node3 | localhost:2203 | localhost:2213 |
inventory.ini 需要同时写入初始端口和加固后的 Docker 映射端口。security role 会先通过 2201-2203 登录,重启 SSH 后自动切换到 2211-2213 继续执行后续 role。
[servers]
node1 ansible_host=127.0.0.1 ansible_port=2201 secure_ansible_port=2211 ansible_user=ansible
node2 ansible_host=127.0.0.1 ansible_port=2202 secure_ansible_port=2212 ansible_user=ansible
node3 ansible_host=127.0.0.1 ansible_port=2203 secure_ansible_port=2213 ansible_user=ansible
[servers:vars]
ansible_ssh_private_key_file=./keys/ops_ed25519
ansible_python_interpreter=/usr/bin/python3ansible.cfg 放在 ansible-roles/ 根目录:
[defaults]
inventory = inventory.ini
host_key_checking = False
roles_path = roles
retry_files_enabled = False
[privilege_escalation]
become = True6.2 确认连通性
# 从 WSL Ubuntu 控制端 ping 三台 Docker 受控节点
ansible all -m ping -i inventory.ini预期输出:三台节点均返回 SUCCESS。
七、运行与验证
7.1 项目目录结构检查
cd ansible-roles
tree -L 3预期输出(简化):
ansible-roles/
├── site.yml
├── inventory.ini
├── ansible.cfg
├── keys/
│ ├── ops_ed25519
│ └── ops_ed25519.pub
├── group_vars/
│ └── all.yml
├── roles/
│ ├── common/
│ │ ├── tasks/main.yml
│ │ └── defaults/main.yml
│ ├── security/
│ │ ├── tasks/main.yml
│ │ ├── templates/sshd-atsb.conf.j2
│ │ └── defaults/main.yml
│ ├── firewall/
│ │ ├── tasks/main.yml
│ │ ├── handlers/main.yml
│ │ └── defaults/main.yml
│ └── runtime/
│ ├── tasks/main.yml
│ ├── handlers/main.yml
│ └── defaults/main.yml
└── docker/
├── Dockerfile
└── docker-compose.yml7.2 语法检查
ansible-playbook site.yml --syntax-checkRoles 的语法检查与普通 Playbook 完全相同——Ansible 会自动递归检查每个 role 的 tasks/main.yml。
7.3 预演
ansible-playbook site.yml --check7.4 正式执行
ansible-playbook site.yml | tee outputs/roles-run.log7.5 按 tags 选择性执行
# 只执行安全加固
ansible-playbook site.yml --tags security
# 跳过防火墙(课堂 UFW 练习场景)
ansible-playbook site.yml --skip-tags firewall
# 列出所有可用 tags
ansible-playbook site.yml --list-tags7.6 使用 Vault 加密执行
# 先加密敏感变量文件
ansible-vault encrypt group_vars/all/secrets.yml
# 输入 Vault 密码执行
ansible-playbook site.yml --ask-vault-pass7.7 验证目标服务器
ansible all -m command -a "id ops" -i inventory.ini
ansible all -m command -a "sshd -T" --become -i inventory.ini
ansible all -m command -a "ufw status" --become -i inventory.ini
ansible all -m command -a "service fail2ban status" --become -i inventory.ini八、提交物清单
| 提交物 | 要求 |
|---|---|
roles/ 完整目录 | 至少包含 common、security、firewall、runtime 四个 role,每个 role 有 tasks/main.yml 和 defaults/main.yml |
site.yml | 正确编排 role 执行顺序,每个 role 有对应 tag |
group_vars/all.yml | 集中管理共享变量,变量名使用统一的命名风格 |
inventory.ini | 至少配置 3 台节点 |
| 运行日志 | 完整执行记录,能看到每个 role 的 task 执行状态 |
| 截图 | 至少包含语法检查、--list-tags、tags 选择性执行的输出 |
| Ansible Vault 演示 | ansible-vault encrypt 和 --ask-vault-pass 执行的截图 |
九、故障排查
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
ERROR! the role 'xxx' was not found | roles 目录路径不对 | 确保 roles/ 与 site.yml 同级,或配置 ansible.cfg 中 roles_path |
template 模块找不到模板文件 | 路径写成了 templates/sshd-atsb.conf.j2 | Role 内的模板只写文件名即可:src: sshd-atsb.conf.j2 |
vars 定义的变量没有被覆盖 | 变量定义在 vars/main.yml 而非 defaults/main.yml | 业务配置全部移到 defaults/main.yml,让 group_vars 可以覆盖 |
--tags 不生效 | tag 没有打在 role 级别或 task 级别 | 用 --list-tags 确认 tag 是否已注册 |
| handler 没有被触发 | notify 的名称与 handler 的 name 不完全一致 | 检查 task 中 notify: restart ssh 与 handler 中 name: restart ssh 是否完全匹配 |
ansible-vault encrypt 后执行报 Decryption failed | 密码错误或 vault 文件损坏 | 用 ansible-vault view 确认能否解密 |
| role 执行顺序不符合预期 | site.yml 中 roles 列表的顺序就是执行顺序 | 检查 roles 列表排序,common 必须排第一 |
十、进阶任务
探索任务 1:添加 role 依赖声明
在 runtime role 的 meta/main.yml 中声明对 common role 的依赖:
dependencies:
- role: common然后从 site.yml 中移除 common role 的显式声明。执行 Playbook,观察 Ansible 是否自动先执行 common。理解 meta/main.yml 依赖声明与 site.yml 显式调用的区别。
探索任务 2:实现多环境配置
创建 group_vars/production.yml 和 group_vars/staging.yml,在 Inventory 中划分 [production] 和 [staging] 组。为不同环境设置不同的 ssh_port、business_ports 和基础软件列表,验证同一套 Roles 在不同环境下的行为差异。
探索任务 3:编写 role 的 tests
在某个 role 下创建 tests/ 目录,包含 test.yml(测试 Playbook)和 inventory(测试 Inventory)。用 ansible-playbook -i tests/inventory tests/test.yml 独立测试该 role,理解"每个 role 可以独立验证"的工程化价值。
探索任务 4:用 ansible-galaxy 初始化 role 骨架
使用 ansible-galaxy init roles/nginx 创建一个新的 nginx role 骨架,观察它自动生成的标准目录结构与手工创建的目录结构是否一致。理解 ansible-galaxy 作为 Ansible 生态中的"脚手架工具"的作用。
