--- 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//all.yml — 特定主机变量 # 2. group_vars//all.yml — 主机组共享变量 # 3. roles//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 基础用法参考