18 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-07-04 13:30 |
Ansible Playbook 基础
概述
Ansible 是一个无 Agent 的自动化运维工具,通过 SSH 协议远程管理主机。Playbook 是 Ansible 的核心概念——用 YAML 描述的自动化任务编排文件。xinfra 的进阶目标中要求使用 Ansible Playbook 部署 MySQL Server、PostgreSQL 和 Redis Cluster,这是实现数据库层**基础设施即代码(IaC)**的关键技能。
核心概念
工作原理
graph LR
Controller[控制节点 / Laptop or CI Host]
SSH[SSH 连接]
Nodes[(被控主机列表)]
Controller -->|读取 Playbook YAML| SSH
SSH -->|推送临时模块脚本| Nodes
Nodes -->|返回 JSON 结果| Controller
关键特点:
- 无 Agent:不需要在被控机器上安装任何软件,仅依赖 Python + SSH
- 幂等性(Idempotent):同一个 Playbook 执行多次效果相同,不会重复创建或修改
- 模块化:每个操作封装为独立模块(如
ansible.builtin.shell、ansible.builtin.yum、ansible.builtin.template)
特权提升(Become)
become 是 Ansible 的 跨平台权限提升机制,允许非 root 用户登录主机后切换到目标账户执行任务。不改变初始 SSH 连接会话——临时模块脚本仍通过原始身份推送,但实际运行时切换身份。
# Playbook 级别 — 所有任务以 root 执行
- hosts: all_mysql_servers
become: yes
# Task 级别 — 精细控制
- name: Run command as postgres user
ansible.builtin.command: /usr/bin/pg_dump mydb
become: yes
become_user: postgres
become_method: sudo # su | doas | pfexec (OpenSolaris)
become_flags: '-s /bin/sh' # 额外标志位
| 参数 | 作用 | 示例值 |
|---|---|---|
| become | 是否启用权限提升 | yes / true |
| become_user | 切换的目标用户 | root, postgres, apache |
| become_method | 底层实现方式 | sudo(默认), su, pfexec |
| become_flags | 传递给底层方法的附加参数 | -s /bin/sh |
[!warning] 安全注意事项
- 密码不能明文存储:敏感凭据应通过 Ansible Vault 加密(见延伸阅读链接)
- Pipelining:在
/etc/ansible/ansible.cfg中开启pipelining = True,可避免临时文件写入磁盘,减少安全风险- 每台主机仅能启用一种方法:同时配置
sudo+su会导致冲突- 最佳实践:SSH 连入时已有较高权限 → 按需向下切换,比全程用 root 更安全
Playbook 结构骨架
- hosts: all_mysql_servers
become: yes # 提升 root 权限执行
vars: # 变量定义
mysql_port: 3306
mysql_data_dir: /data/mysql
roles: # 引用角色
- common-setup # 系统级准备
- install-mysql # MySQL 安装
tasks: # 显式任务列表
- name: Ensure MySQL is running
ansible.builtin.service: # 推荐写全称(命名空间规范)
name: mysqld
state: started
notify: restart mysql # 触发 handler
handlers: # 响应通知动作
- name: restart mysql
ansible.builtin.service:
name: mysqld
state: restarted
关键概念对照
| 概念 | 作用 | 类比 |
|---|---|---|
| Hosts | 指定目标机器组(Inventory 中定义) | SSH target |
| Tasks | 按顺序执行的操作 | 脚本命令 |
| Roles | 可复用的任务集合 | 函数库 |
| Handlers | 监听通知的触发器(常用于重启服务) | 事件回调 |
| Modules | 单功能执行单元 | 子命令 |
| Inventory | 被控主机清单 | SSH config 中的 host |
变量优先级体系(由低到高)
# 变量加载顺序(低 → 高):
# 1. host_vars/<hostname>/all.yml — 特定主机变量
# 2. group_vars/<group>/all.yml — 主机组共享变量
# 3. roles/<role>/defaults/main.yml — 角色默认值(可被覆盖)
# 4. Playbook 顶层 vars — Playbook 内联变量
# 5. extra_vars (-e 参数) — 运行时传入,优先级最高
[!tip] 为什么需要多级变量? 同一个
mysql_port在开发环境可能是3307,生产环境是3306。group_vars 按环境隔离 + defaults 提供安全兜底——这是大型 Ansible 项目中最常用的变量管理模式。
变量引用语法
# YAML 文件中用双大括号引用
- name: Set MySQL port
debug:
msg: "MySQL is running on port {{ mysql_port }}"
# 在模板文件(.j2)中同样使用
port = {{ mysql_port }}
# 支持字典访问和索引
- name: Access nested variables
debug:
msg: "{{ db_config.host }},{{ db_config.credentials.password }}"
MySQL 部署的角色拆分示例
一个典型的 MySQL 部署 Playbook 会拆成多个 Role:
roles/
├── common-setup/
│ ├── tasks/main.yml # 时间同步、内核参数调优、防火墙规则
│ └── templates/ # 系统级配置文件模板
├── install-mysql/
│ ├── tasks/main.yml # 安装、配置主从复制
│ └── files/ # 二进制包或 RPM 安装包
└── config-mysql/
├── tasks/main.yml # 编写 my.cnf(Jinja2 模板)
└── templates/my.cnf.j2 # 根据变量动态生成配置
每个 Role 可以独立测试和复用,不同服务的安装只需替换中间两个 Role。
Jinja2 模板实战
# my.cnf.j2
[mysqld]
port = {{ mysql_port | default(3306) }}
datadir = {{ mysql_data_dir | default('/var/lib/mysql') }}
server-id = {{ inventory_hostname | regex_replace('\\D', '') | int }}
{% if role == 'master' %}
log-bin = mysql-bin
binlog-format = ROW
{% elif role == 'slave' %}
read-only = 1
relay-log = relay-bin
{% endif %}
[!tip] 为什么需要 Jinja2 模板? 同一套 Playbook 要用于 Master 和 Slave,它们的配置有差异。模板 + 变量注入比硬编码多个 Playbook 更灵活——这也是 IaC 的核心思想:一套代码,多种环境。
控制流
循环(Loops)
Ansible 推荐使用简洁的 loop 语法替代旧式的 with_items。loop 在 2.5 版本引入,功能等价于 with_list,但更简洁且与 lookup 兼容更好。
基础用法
- name: Install multiple packages
ansible.builtin.yum:
name: "{{ item }}"
state: present
loop:
- vim
- curl
- git
- wget
# 字典列表循环 — 适合批量创建用户/配置
- name: Create database users
community.mysql.mysql_user:
name: "{{ item.name }}"
password: "{{ item.password }}"
priv: "{{ item.priv }}"
state: present
loop: "{{ db_users }}" # 引用 vars 中定义的变量列表
字典迭代(dict2items)
当需要遍历 YAML 字典时,用 dict2items 将其转为 [{"key": ..., "value": ...}, ...] 结构:
- name: Configure servers from dictionary
ansible.builtin.debug:
msg: "{{ item.key }}: IP={{ item.value.ip_address }}, role={{ item.value.role }}"
loop: "{{ server_configs | dict2items }}"
vars:
server_configs:
web_01:
ip_address: "10.1.1.50"
role: "frontend"
web_02:
ip_address: "10.1.1.51"
role: "backend"
[!tip] query vs lookup
loop要求返回一个 list。使用 lookup 插件时,推荐用query(2.5+),它保证返回列表;如果用lookup,需加wantlist=True:loop: "{{ query('inventory_hostnames', 'all') }}" # 或 loop: "{{ lookup('inventory_hostnames', 'all', wantlist=True) }}"
循环控制(loop_control)
- name: Create cloud instances with delay
digital_ocean:
name: "{{ item.name }}"
state: present
loop:
- { name: server1, disk: 3gb }
- { name: server2, disk: 4gb }
loop_control:
label: "{{ item.name }}" # 自定义输出标识(避免冗长日志)
pause: 3 # 每项之间等待 N 秒(避免瞬时并发压力)
loop_var: outer_item # 指定外层循环变量名(嵌套循环防冲突)
自动重试(until 循环)
until 不是用来遍历数据的——它是 轮询重试 模式,适用于可能暂时失败、最终会成功的操作(如等待服务启动):
- name: Wait for service to be ready
shell: /usr/bin/check_service_ready
register: result
until: result.stdout.find("ready") != -1
retries: 5 # 最大尝试次数
delay: 10 # 每次间隔秒数
注册变量在循环中的结构是嵌套的:{{ my_loop.results[0].stdout }}、{{ my_loop.results[1].rc }} 等,后续用 when: item.rc != 0 过滤即可。
[!warning] with_items → loop 迁移注意
with_items有隐式扁平化行为:with_items: [1, [2,3], 4]会展平为[1, 2, 3, 4]。切换到loop后需手动处理:loop: "{{ [1, [2, 3], 4] | flatten(1) }}"
条件判断(when)
- name: Configure slave replication
template:
src: my.cnf.j2
dest: /etc/my.cnf
when: inventory_hostname in groups['mysql_slaves']
# 复杂条件
- name: Only on CentOS 7
debug:
msg: "CentOS detected"
when:
- ansible_os_family == "RedHat"
- ansible_distribution_major_version == "7"
[!warning] 何时不用 when? 如果
when只是过滤几行数据,优先在 Jinja2 模板内部用{% if %}处理,而不是写一个全量任务再套when——后者会在所有目标主机上都执行一次无意义的模块调用。
notify / handlers 机制详解
tasks:
- name: Update Nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: reload nginx # ← 仅设置"标记",不立即执行
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded # ← 仅在 Playbook 阶段结束时触发
[!info] notify 的关键语义 即使多个 task 都
notify同一个 handler,handler 也只会在每个 Play 结束后执行一次。这正是幂等性设计的一部分——避免重复重启服务。
结果捕获与错误处理
# register — 将命令输出保存到变量
- name: Check if MySQL data dir exists
ansible.builtin.stat:
path: /data/mysql
register: mysql_data_check
- name: Initialize data dir if missing
ansible.builtin.file:
path: /data/mysql
state: directory
owner: mysql
group: mysql
when: not mysql_data_check.stat.exists
# ignore_errors — 允许任务失败不中断 Playbook
- name: Try to stop already-stopped service
ansible.builtin.service:
name: mysqld
state: stopped
ignore_errors: yes
# failed_when — 自定义失败判定逻辑
- name: Check API health
ansible.builtin.uri:
url: http://localhost:8080/health
register: health_check
failed_when: "'unhealthy' in health_check.content"
# set_fact — 动态设置变量供后续任务使用
- name: Capture private IP
ansible.builtin.set_fact:
node_private_ip: "{{ ansible_default_ipv4.address }}"
[!tip] register 的经典使用场景
stat模块检查文件存在 →when判断决定是否创建 → 这就是 "先查后做" 的 Ansible 范式,完美体现幂等性原则。
状态控制(changed_when / any_errors_fatal)
除了 failed_when 外,Ansible 还允许通过 changed_when 精确控制任务的变更状态——这直接影响统计报告和 handler 触发:
# ❌ command 永远是 "changed",即使数据库连接无变化
- name: Check DB connection
command: mysql -e "SELECT 1"
# ✅ 仅在真正有变更时标记为 changed
- name: Check DB connection
shell: mysql -e "SELECT 1"
register: db_check
changed_when: "'ERROR' in db_check.stdout"
# 强制不报告变更(常用于健康检查类任务)
- name: Health check endpoint
uri:
url: http://localhost:8080/health
changed_when: false
| 参数 | 作用域 | 说明 |
|---|---|---|
| ignore_errors | Task 级 | 失败继续执行,不中断 Play |
| ignore_unreachable | Play 级 | 跳过网络不可达的主机,继续其他主机 |
| any_errors_fatal | Play/Block 级 | 任一主机报错即终止全部执行 |
| max_fail_percentage | Play 级 | 失败超过阈值(%)时停止 |
| changed_when | Task 级 | 条件性覆盖 "已变更" 报告 |
| failed_when | Task 级 | 条件性覆盖 "失败" 判定 |
Block / Rescue / Always — 结构化异常处理
这是编程中 try/catch/finally 的 Ansible 等价物,适合需要 原子化操作 + 自动回滚 的场景:
- name: Deploy application with rollback
block:
- name: Backup current release
archive:
path: /opt/app/current
dest: /opt/backups/release-$(date +%s).tar.gz
register: backup_result
- name: Extract new version
unarchive:
src: new-app-v2.tar.gz
dest: /opt/app/releases/v2
remote_src: yes
- name: Symlink to active release
file:
src: /opt/app/releases/v2
dest: /opt/app/current
state: link
rescue:
- name: Log failure details
debug:
msg: "Task '{{ ansible_failed_task.name }}' failed on {{ inventory_hostname }}"
- name: Rollback to previous release
file:
src: /opt/app/releases/v1
dest: /opt/app/current
state: link
when: backup_result is succeeded # 仅当备份成功时才尝试还原
always:
- name: Notify deployment team
ansible.builtin.command: >
curl -X POST https://hooks.slack.com/deploy-webhook
-d '{"text":"Deployment {{ ansible_failed_task.name if ansible_failed_task else 'succeeded' }} on {{ inventory_hostname}}"}'
changed_when: false
[!info] 语义对比表
编程概念 Ansible 关键字 触发条件 try {}block:正常任务流,始终执行 catch {}rescue:仅当前置任务返回 failed时触发finally {}always:无论成败均执行
- Rescue 中的恢复若成功,Play 会继续——但统计日志仍记录一次故障
- 语法错误、断连等情况不会触发 Rescue,与编程语言不同
- Handler 可以在 Rescue 中被调用(通过元数据命令触发)
常用模块速查
[!info] 命名空间规范(Namespace Prefix) Ansible 2.10+ 开始引入
collections概念,推荐始终使用前缀格式:
ansible.builtin.copy— 内置模块community.general.selinux— 社区集合- 现代 Playbook 中优先写全称;如果省略前缀,Ansible 会按内置 → 已安装的集合顺序自动匹配。
| 模块 | 用途 | 关键参数 |
|---|---|---|
| ansible.builtin.copy | 本地文件复制到远程 | src, dest, owner, mode |
| ansible.builtin.template | Jinja2 模板渲染后复制 | src (.j2), dest, owner, mode |
| ansible.builtin.file | 管理文件/目录权限和属性 | state: touch/file/directory/link/absent, mode |
| ansible.builtin.yum / ansible.builtin.apt | 包管理 | name, state: present/installed/latest/absent |
| ansible.builtin.service | 管理服务生命周期 | name, state: started/stopped/restarted/reloaded |
| ansible.builtin.lineinfile | 确保某行存在或不存在于文件中 | path, line, state, regexp |
| ansible.builtin.replace | 正则替换文件内容 | path, regexp, replace |
| ansible.builtin.command / ansible.builtin.shell | 执行命令(慎用) | cmd, chdir |
| ansible.builtin.get_url / ansible.builtin.uri | HTTP 下载 / API 调用 | url, dest, method, status_code |
| ansible.builtin.user | 管理系统用户 | name, state, groups, shell |
| ansible.builtin.cron | 管理 crontab | name, minute, job, state |
| ansible.builtin.debug | 调试输出 | msg, var |
| ansible.builtin.stat | 检查文件/路径属性 | path, returns .stat.exists |
| community.mysql.mysql_user | MySQL 用户管理 | name, password, priv, state |
| community.postgresql.postgresql_user | PostgreSQL 用户管理 | name, password, role_attr |
[!warning] command vs shell 的选择
command不经过 Shell 解释器(更安全,推荐优先使用),shell才支持管道\|、重定向>、变量扩展$VAR。能用模块完成的操作绝不碰 command/shell。
常见陷阱与最佳实践
1. 不要跳过 state 检查
错误写法:
- ansible.builtin.command: systemctl start mysqld # ❌ 每次都会尝试启动,即使已在运行
正确写法:
- ansible.builtin.service: # ✅ Ansible 自带的 service 模块会检查当前状态
name: mysqld
state: started
2. Inventory 分层管理
把开发、测试、生产环境的机器分开管理:
# inventory/prod
[mysql_prod]
mysql-prod-01 ansible_host=10.0.1.11
mysql-prod-02 ansible_host=10.0.1.12
[redis_prod]
redis-prod-01 ansible_host=10.0.1.21
3. 首次运行时务必先 Dry Run
# --check 只做预演,不实际执行
ansible-playbook deploy.yml --check --diff
4. 警惕 shell/command 失去幂等性
# ❌ 每次运行都会在日志末尾追加一行
- ansible.builtin.shell: echo "deployed" >> /tmp/deploy-history
# ✅ 用 file module 保证状态一致性
- ansible.builtin.file:
path: /tmp/deploy-marker
state: touched
modification_time: preserve
延伸阅读
- technical/xinfra-preview/k8s-rke2-fundamentals — K8s 内的实例 vs Ansible 管理的裸机/VM
- technical/xinfra-preview/cachecloud-overview — CacheCloud 管 Redis,Ansible 负责底层部署
- technical/xinfra-preview/cloud-dm-overview — Ansible 部署的 MySQL 由 CloudDM 做 SQL 审核
- 官方 Playbook 指南 — Ansible 文档源码,涵盖从入门到高级语法的全部章节
- https://www.ansible.com/docs — Ansible 官方文档入口
- Ansible Vault 文档 — 加密敏感凭据(密码、密钥),解决 Become 中的安全合规问题
- 入门教程 — Ansible 基础用法参考