Files
Qiniu/technical/xinfra-preview/ansible-playbook-basics.md

516 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [ansible, playbook, infra, automation, iaas]
create time: 2026-07-04 13:30
---
# Ansible Playbook 基础
## 概述
Ansible 是一个无 Agent 的自动化运维工具,通过 SSH 协议远程管理主机。**Playbook** 是 Ansible 的核心概念——用 YAML 描述的自动化任务编排文件。xinfra 的进阶目标中要求使用 Ansible Playbook 部署 MySQL Server、PostgreSQL 和 Redis Cluster,这是实现数据库层**基础设施即代码(IaC)**的关键技能。
## 核心概念
### 工作原理
```mermaid
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 连接会话——临时模块脚本仍通过原始身份推送,但实际运行时切换身份。
```yaml
# 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 结构骨架
```yaml
- 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 |
### 变量优先级体系(由低到高)
```yaml
# 变量加载顺序(低 → 高):
# 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
# 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 模板实战
```ini
# 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` 兼容更好。
#### 基础用法
```yaml
- 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": ...}, ...]` 结构:
```yaml
- 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`:
> ```yaml
> loop: "{{ query('inventory_hostnames', 'all') }}"
> # 或
> loop: "{{ lookup('inventory_hostnames', 'all', wantlist=True) }}"
> ```
#### 循环控制(loop_control)
```yaml
- 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` 不是用来遍历数据的——它是 **轮询重试** 模式,适用于可能暂时失败、最终会成功的操作(如等待服务启动):
```yaml
- 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` 后需手动处理:
> ```yaml
> loop: "{{ [1, [2, 3], 4] | flatten(1) }}"
> ```
### 条件判断(when)
```yaml
- 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 机制详解
```yaml
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 结束后执行一次**。这正是幂等性设计的一部分——避免重复重启服务。
### 结果捕获与错误处理
```yaml
# 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 触发:
```yaml
# ❌ 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 等价物,适合需要 **原子化操作 + 自动回滚** 的场景:
```yaml
- 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` 检查
错误写法:
```yaml
- ansible.builtin.command: systemctl start mysqld # ❌ 每次都会尝试启动,即使已在运行
```
正确写法:
```yaml
- ansible.builtin.service: # ✅ Ansible 自带的 service 模块会检查当前状态
name: mysqld
state: started
```
### 2. Inventory 分层管理
把开发、测试、生产环境的机器分开管理:
```ini
# 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
```bash
# --check 只做预演,不实际执行
ansible-playbook deploy.yml --check --diff
```
### 4. 警惕 shell/command 失去幂等性
```yaml
# ❌ 每次运行都会在日志末尾追加一行
- 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 指南](https://github.com/ansible/ansible-documentation/tree/devel/docs/docsite/rst/playbook_guide) — Ansible 文档源码,涵盖从入门到高级语法的全部章节
- https://www.ansible.com/docs — Ansible 官方文档入口
- [Ansible Vault 文档](https://docs.ansible.com/ansible/latest/cli/ansible-vault.html) — 加密敏感凭据(密码、密钥),解决 Become 中的安全合规问题
- [入门教程](https://www.cnblogs.com/easonscx/p/10622781.html) — Ansible 基础用法参考