> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Ansible 完全指南
- URL: https://blog.vercanti.com/ansible-wan-quan-zhi-nan/
- Published: 2026-08-28T14:34:19.000Z
- Updated: 2026-08-28T14:56:16.000Z
- Description: Ansible 通过 SSH 连接被管理节点，无需在目标机器上安装任何代理程序。控制节点（安装 Ansible 的机器）直接通过 SSH 执行任务，任务完成后不留任何持久进程。 核心特性： 动态 Inventory 通过可执行脚本或插件从外部系统（AWS、GCP、CMDB）动态获取主机列表。 aws_ec2.yml 示例： Task 字段说明： 基于 Jinja2 模板引擎渲染文件，支持变量、条件、循环。 模板文件示例（templates/nginx.conf.j2）： apt 模块 state 字段： 优先级越高，值越能覆盖低优先级的同名变量： gro
- Author: yellowdog
- Tags: DevOps

> 官方文档：<https://docs.ansible.com/ansible/latest/>  
> 适用版本：Ansible 2.15+（2026-05-07 核实）

## 核心概念

### 无 Agent 架构

Ansible 通过 SSH 连接被管理节点，无需在目标机器上安装任何代理程序。控制节点（安装 Ansible 的机器）直接通过 SSH 执行任务，任务完成后不留任何持久进程。

核心特性：

- 无 Agent，目标机器只需 Python（2.7+ 或 3.5+）和 SSH
- 幂等性：多次执行同一 Playbook 结果相同
- 声明式配置：描述目标状态，而非具体步骤
- 基于 YAML，可读性强

### 核心组件关系

```
Inventory（目标主机清单）
    |
    v
Playbook（剧本）
    |
    +-- Play（针对一组主机的任务集合）
         |
         +-- Task（调用 Module 的单个步骤）
         |    |
         |    +-- Module（执行具体操作的功能单元，如 apt、copy、service）
         |
         +-- Handler（由 notify 触发的特殊 Task，如重启服务）
         |
         +-- Role（可复用的 Task/变量/文件/模板集合）

```

| 组件        | 说明                                     |
| --------- | -------------------------------------- |
| Inventory | 定义被管理的主机及分组，可以是文件或动态脚本                 |
| Playbook  | YAML 文件，包含一个或多个 Play                   |
| Play      | 将一组 Task 映射到一组主机上执行                    |
| Task      | 调用单个 Module 完成一个操作步骤                   |
| Module    | Ansible 内置或第三方的功能单元，执行实际操作             |
| Handler   | 特殊 Task，只在被 notify 时触发，且同一 Play 中只执行一次 |
| Role      | 按约定目录结构组织的 Task、变量、文件、模板的可复用单元         |

---

## Inventory

### INI 格式

```ini
# 独立主机（无分组）
192.168.1.5

[webservers]
web-01.example.com
web-02.example.com ansible_host=192.168.1.11

[dbservers]
db-01.example.com ansible_host=192.168.1.20 ansible_port=2222

# 范围语法（web-01 到 web-05）
[cache]
redis-[01:05].example.com

# 嵌套组：[父组名:children]
[production:children]
webservers
dbservers

# 组变量：[组名:vars]
[webservers:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/prod_key
http_port=80

```

### YAML 格式

```yaml
# inventory.yml
all:
  children:
    webservers:
      hosts:
        web-01.example.com:
          ansible_host: 192.168.1.10
          http_port: 80
        web-02.example.com:
          ansible_host: 192.168.1.11
      vars:
        ansible_user: ubuntu
        ansible_ssh_private_key_file: ~/.ssh/prod_key

    dbservers:
      hosts:
        db-01.example.com:
          ansible_host: 192.168.1.20
          ansible_port: 2222

    production:
      children:
        webservers:
        dbservers:

```

### 主机变量说明

| 变量                               | 说明                                |
| -------------------------------- | --------------------------------- |
| ansible\_host                    | 实际连接的 IP 或域名（区别于 inventory 中的别名）  |
| ansible\_port                    | SSH 端口，默认 22                      |
| ansible\_user                    | SSH 用户名                           |
| ansible\_ssh\_private\_key\_file | SSH 私钥路径                          |
| ansible\_password                | SSH 密码（不推荐，建议用密钥）                 |
| ansible\_become                  | 是否启用提权，等同于 become: true           |
| ansible\_become\_user            | 提权目标用户，默认 root                    |
| ansible\_python\_interpreter     | 目标机器 Python 路径，如 /usr/bin/python3 |
| ansible\_connection              | 连接类型，默认 ssh，可设为 local             |

### 动态 Inventory

动态 Inventory 通过可执行脚本或插件从外部系统（AWS、GCP、CMDB）动态获取主机列表。

```bash
# 使用动态 inventory 脚本
ansible-playbook -i inventory_script.py site.yml

# 使用 inventory 插件（aws_ec2）
ansible-inventory -i aws_ec2.yml --list

```

`aws_ec2.yml` 示例：

```yaml
plugin: amazon.aws.aws_ec2
regions:
  - cn-north-1
filters:
  instance-state-name: running
  tag:Environment: production
keyed_groups:
  - key: tags.Role
    prefix: role

```

---

## Playbook 语法

### 完整 Playbook 结构

```yaml
---
# site.yml
- name: 配置 Web 服务器
  hosts: webservers
  become: true
  gather_facts: true

  vars:
    app_port: 8000
    app_user: www-data

  vars_files:
    - vars/secrets.yml

  pre_tasks:
    - name: 更新 apt 缓存
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

  roles:
    - common
    - nginx

  tasks:
    - name: 确保应用目录存在
      ansible.builtin.file:
        path: /opt/app
        state: directory
        owner: "{{ app_user }}"
        mode: "0755"

    - name: 部署应用配置
      ansible.builtin.template:
        src: app.conf.j2
        dest: /etc/app/app.conf
      notify: restart app

  post_tasks:
    - name: 验证服务启动
      ansible.builtin.uri:
        url: "http://localhost:{{ app_port }}/health"
        status_code: 200

  handlers:
    - name: restart app
      ansible.builtin.service:
        name: myapp
        state: restarted

```

### Play 字段

| 字段                    | 类型       | 说明                                   |
| --------------------- | -------- | ------------------------------------ |
| name                  | str      | Play 名称，显示在执行输出中                     |
| hosts                 | str/list | 目标主机或主机组，支持通配符（webservers:dbservers） |
| become                | bool     | 是否提权（sudo），默认 false                  |
| become\_user          | str      | 提权目标用户，默认 root                       |
| gather\_facts         | bool     | 是否收集主机信息（facts），默认 true，不需要时关闭可加速    |
| vars                  | dict     | Play 级别变量                            |
| vars\_files           | list     | 加载外部变量文件                             |
| tasks                 | list     | Task 列表                              |
| pre\_tasks            | list     | 在 roles 之前执行的 Task                   |
| post\_tasks           | list     | 在 roles 之后执行的 Task                   |
| roles                 | list     | 引用的 Role 列表                          |
| handlers              | list     | Handler 列表                           |
| serial                | int/str  | 滚动更新批次大小（如 2 或 "20%"）                |
| any\_errors\_fatal    | bool     | 任意主机失败时立即中止整个 Play                   |
| max\_fail\_percentage | int      | 允许失败的主机比例上限                          |
| tags                  | list     | 为整个 Play 打标签                         |

### Task 字段

```yaml
tasks:
  - name: 安装 Python 依赖
    ansible.builtin.pip:
      requirements: /opt/app/requirements.txt
      virtualenv: /opt/app/venv
    # 条件执行
    when: ansible_os_family == "Debian"
    # 注册返回值
    register: pip_result
    # 触发 Handler
    notify: restart app
    # 循环
    loop:
      - package1
      - package2
    # 标签（ansible-playbook --tags deploy 时执行）
    tags:
      - deploy
      - python
    # 忽略错误
    ignore_errors: true
    # 超时（秒）
    timeout: 60
    # 失败重试
    retries: 3
    delay: 5
    until: pip_result.rc == 0

```

Task 字段说明：

| 字段             | 说明                                 |
| -------------- | ---------------------------------- |
| name           | Task 描述，显示在执行输出中，建议清晰描述操作目的        |
| module         | 要调用的模块（即 Task 中除元字段外的键）            |
| when           | 条件表达式，为 true 时执行，支持 Jinja2         |
| register       | 将模块返回值存储到变量中                       |
| notify         | 任务有变更（changed）时通知 Handler          |
| loop           | 循环执行，每次迭代 item 变量为当前元素             |
| loop\_control  | 控制循环行为（loop\_var、label、pause）      |
| tags           | 标签，支持 \--tags / \--skip-tags 选择性执行 |
| ignore\_errors | 忽略错误继续执行                           |
| become         | 此 Task 级别的提权设置（覆盖 Play 级别）         |
| delegate\_to   | 委托给指定主机执行                          |
| run\_once      | 整个 Play 中只执行一次（用于需要唯一操作的场景）        |
| retries        | 失败重试次数，配合 until 和 delay 使用         |

### when 条件语法

```yaml
tasks:
  # 比较操作系统
  - name: 安装 apt 包（仅 Debian/Ubuntu）
    ansible.builtin.apt:
      name: nginx
    when: ansible_os_family == "Debian"

  # 检查变量
  - name: 仅在生产环境执行
    ansible.builtin.command: /opt/scripts/deploy.sh
    when: env == "production"

  # 多条件（and）
  - name: 满足多个条件
    ansible.builtin.debug:
      msg: "条件满足"
    when:
      - ansible_memtotal_mb > 2048
      - ansible_processor_vcpus >= 2

  # or 条件
  - name: 满足任一条件
    ansible.builtin.debug:
      msg: "OS 匹配"
    when: ansible_distribution == "Ubuntu" or ansible_distribution == "Debian"

  # 检查变量是否定义
  - name: 仅在变量已定义时执行
    ansible.builtin.debug:
      msg: "{{ my_var }}"
    when: my_var is defined

  # 检查命令返回值
  - name: 检查文件是否存在
    ansible.builtin.stat:
      path: /opt/app/.installed
    register: installed_flag

  - name: 仅在未安装时执行安装
    ansible.builtin.command: /opt/scripts/install.sh
    when: not installed_flag.stat.exists

```

### loop 和 with\_items

```yaml
# 现代写法（loop，Ansible 2.5+）
- name: 安装多个软件包
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - git
    - python3
    - python3-pip

# 遍历字典列表
- name: 创建多个用户
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
    shell: /bin/bash
  loop:
    - { name: alice, groups: sudo }
    - { name: bob, groups: www-data }

# loop_control：自定义循环变量名和标签
- name: 部署多个应用
  ansible.builtin.template:
    src: "{{ item.src }}"
    dest: "{{ item.dest }}"
  loop: "{{ app_configs }}"
  loop_control:
    loop_var: item          # 默认就是 item，可以改名避免嵌套循环冲突
    label: "{{ item.dest }}" # 简化输出中每次迭代的显示

# 传统写法（with_items，仍然有效但不推荐）
- name: 安装软件包（旧写法）
  ansible.builtin.apt:
    name: "{{ item }}"
  with_items:
    - nginx
    - git

```

### register 保存结果

```yaml
- name: 检查 Nginx 运行状态
  ansible.builtin.command: systemctl is-active nginx
  register: nginx_status
  ignore_errors: true

- name: 打印 Nginx 状态
  ansible.builtin.debug:
    msg: "Nginx 状态：{{ nginx_status.stdout }}"

- name: 仅在 Nginx 未运行时启动
  ansible.builtin.service:
    name: nginx
    state: started
  when: nginx_status.rc != 0

# 常用 register 返回字段
# result.stdout        - 标准输出（字符串）
# result.stderr        - 标准错误（字符串）
# result.rc            - 返回码（int）
# result.stdout_lines  - 标准输出按行分割的列表
# result.changed       - 是否有变更（bool）
# result.failed        - 是否失败（bool）

```

---

## 常用模块

### ansible.builtin.command / shell

```yaml
# command：不经过 shell，更安全，不支持管道、重定向、通配符
- name: 执行脚本
  ansible.builtin.command:
    cmd: /opt/scripts/migrate.sh
    chdir: /opt/app         # 执行前切换目录
    creates: /opt/app/.migrated  # 文件存在时跳过（幂等性保障）

# shell：经过 /bin/sh，支持管道、重定向、环境变量展开
- name: 统计日志行数
  ansible.builtin.shell:
    cmd: "grep ERROR /var/log/app.log | wc -l"
  register: error_count

```

| 参数         | 说明                         |
| ---------- | -------------------------- |
| cmd        | 要执行的命令                     |
| chdir      | 执行前切换到指定目录                 |
| creates    | 指定文件存在时跳过此 Task（幂等性）       |
| removes    | 指定文件不存在时跳过此 Task           |
| stdin      | 向命令的标准输入传递内容               |
| executable | shell 模块可指定解释器，如 /bin/bash |

### ansible.builtin.copy

```yaml
- name: 复制配置文件
  ansible.builtin.copy:
    src: files/nginx.conf         # 控制节点上的路径（相对于 playbook 或 role）
    dest: /etc/nginx/nginx.conf   # 目标路径
    owner: root
    group: root
    mode: "0644"
    backup: true                  # 覆盖前备份原文件
  notify: reload nginx

- name: 直接写入内容
  ansible.builtin.copy:
    content: |
      [Unit]
      Description=My App
    dest: /etc/systemd/system/myapp.service
    mode: "0644"

```

| 参数      | 说明                     |
| ------- | ---------------------- |
| src     | 控制节点上的源文件路径            |
| dest    | 目标机器上的目标路径             |
| content | 直接指定文件内容（与 src 互斥）     |
| owner   | 文件所有者                  |
| group   | 文件所属组                  |
| mode    | 文件权限，建议用字符串格式如 "0644"  |
| backup  | 覆盖前是否备份                |
| force   | 目标文件已存在时是否强制覆盖，默认 true |

### ansible.builtin.template

基于 Jinja2 模板引擎渲染文件，支持变量、条件、循环。

```yaml
- name: 渲染 Nginx 配置
  ansible.builtin.template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/sites-available/myapp.conf
    owner: root
    mode: "0644"
  notify: reload nginx

```

模板文件示例（`templates/nginx.conf.j2`）：

```nginx
# 由 Ansible 自动生成，请勿手动修改
server {
    listen {{ http_port }};
    server_name {{ ansible_hostname }};

    location / {
        proxy_pass http://127.0.0.1:{{ app_port }};
        proxy_set_header Host $host;
    }

    {% if ssl_enabled %}
    listen 443 ssl;
    ssl_certificate {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
    {% endif %}
}

```

| 参数                      | 说明                   |
| ----------------------- | -------------------- |
| src                     | 控制节点上的 Jinja2 模板文件路径 |
| dest                    | 目标机器上的目标路径           |
| owner                   | 文件所有者                |
| mode                    | 文件权限                 |
| trim\_blocks            | 移除块标签后的换行，默认 true    |
| variable\_start\_string | 变量标签起始符，默认 {{        |

### ansible.builtin.file

```yaml
# 创建目录
- name: 创建应用数据目录
  ansible.builtin.file:
    path: /opt/app/data
    state: directory
    owner: app_user
    group: app_user
    mode: "0755"
    recurse: true    # 递归设置权限

# 创建符号链接
- name: 创建当前版本链接
  ansible.builtin.file:
    src: /opt/app/releases/v1.2.3
    dest: /opt/app/current
    state: link

# 删除文件或目录
- name: 清理旧版本
  ansible.builtin.file:
    path: /opt/app/releases/v1.1.0
    state: absent

```

| state 值   | 说明                 |
| --------- | ------------------ |
| file      | 确保是普通文件（不创建，只修改属性） |
| directory | 确保目录存在，不存在则创建      |
| link      | 确保符号链接存在           |
| hard      | 确保硬链接存在            |
| absent    | 确保路径不存在（删除文件或目录）   |
| touch     | 创建空文件或更新时间戳        |

### ansible.builtin.apt / yum / dnf

```yaml
# apt（Debian/Ubuntu）
- name: 安装 Nginx
  ansible.builtin.apt:
    name:
      - nginx
      - python3-pip
    state: present
    update_cache: true
    cache_valid_time: 3600

# 安装指定版本
- name: 安装指定版本的 Python
  ansible.builtin.apt:
    name: python3.11=3.11.0-1
    state: present

# yum（CentOS/RHEL 7）
- name: 安装依赖
  ansible.builtin.yum:
    name: "@Development Tools"
    state: present

# dnf（Fedora/CentOS 8+）
- name: 安装软件包
  ansible.builtin.dnf:
    name: python3
    state: latest

```

`apt` 模块 `state` 字段：

| 值         | 说明        |
| --------- | --------- |
| present   | 确保安装，版本不限 |
| latest    | 确保安装最新版本  |
| absent    | 确保卸载      |
| build-dep | 安装包的构建依赖  |

### ansible.builtin.service

```yaml
- name: 启动并设置 Nginx 开机自启
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

- name: 重启应用（用于 Handler）
  ansible.builtin.service:
    name: myapp
    state: restarted

```

| 参数      | 说明                                       |
| ------- | ---------------------------------------- |
| name    | 服务名称                                     |
| state   | started / stopped / restarted / reloaded |
| enabled | 是否开机自启（true/false）                       |

### ansible.builtin.user / group

```yaml
- name: 创建应用运行组
  ansible.builtin.group:
    name: appgroup
    gid: 1500
    system: true

- name: 创建应用运行用户
  ansible.builtin.user:
    name: appuser
    uid: 1500
    group: appgroup
    groups:
      - sudo
    shell: /bin/bash
    home: /opt/app
    create_home: true
    system: false
    password: "{{ vault_app_password | password_hash('sha512') }}"

```

| 参数        | 说明                      |
| --------- | ----------------------- |
| name      | 用户/组名称                  |
| state     | present（创建）或 absent（删除） |
| uid / gid | 指定 UID/GID              |
| group     | 主组                      |
| groups    | 附加组列表                   |
| shell     | 登录 Shell                |
| home      | 家目录路径                   |
| system    | 是否为系统用户                 |
| password  | 加密后的密码哈希                |

### ansible.builtin.git

```yaml
- name: 克隆应用代码
  ansible.builtin.git:
    repo: "https://github.com/org/myapp.git"
    dest: /opt/app/releases/{{ app_version }}
    version: "{{ app_version }}"    # tag、branch 或 commit hash
    depth: 1                         # 浅克隆，加速
    force: false                     # 是否丢弃本地修改
  register: git_result
  notify: restart app

```

| 参数              | 说明                           |
| --------------- | ---------------------------- |
| repo            | 仓库 URL                       |
| dest            | 本地目标路径                       |
| version         | 分支、tag 或 commit hash，默认 HEAD |
| depth           | 浅克隆深度                        |
| key\_file       | SSH 私钥路径（私有仓库）               |
| accept\_hostkey | 自动接受未知 SSH 主机密钥              |
| update          | 目标目录已存在时是否更新，默认 true         |

### community.docker.docker\_container

```yaml
- name: 启动 Redis 容器
  community.docker.docker_container:
    name: redis
    image: redis:7-alpine
    state: started
    restart_policy: unless-stopped
    ports:
      - "127.0.0.1:6379:6379"
    volumes:
      - /opt/redis/data:/data
    env:
      REDIS_PASSWORD: "{{ vault_redis_password }}"
    command: "redis-server --requirepass {{ vault_redis_password }}"
    networks:
      - name: app-network

```

| 参数              | 说明                                   |
| --------------- | ------------------------------------ |
| name            | 容器名称                                 |
| image           | 镜像名称和标签                              |
| state           | started / stopped / absent / present |
| restart\_policy | 重启策略                                 |
| ports           | 端口映射列表                               |
| volumes         | 卷挂载列表                                |
| env             | 环境变量字典                               |
| networks        | 网络列表                                 |
| pull            | 每次运行时是否拉取最新镜像，默认 missing             |

---

## 变量系统

### 变量优先级（从低到高）

优先级越高，值越能覆盖低优先级的同名变量：

| 优先级 | 来源                                           |
| --- | -------------------------------------------- |
| 1   | role 默认值（roles/role\_name/defaults/main.yml） |
| 2   | inventory 文件变量                               |
| 3   | inventory group\_vars/all                    |
| 4   | inventory group\_vars/<group\_name>          |
| 5   | inventory host\_vars/<host\_name>            |
| 6   | playbook group\_vars/all                     |
| 7   | playbook group\_vars/<group\_name>           |
| 8   | playbook host\_vars/<host\_name>             |
| 9   | 主机 facts（由 gather\_facts 收集）                 |
| 10  | Play 级别 vars                                 |
| 11  | Play 级别 vars\_files                          |
| 12  | Play 级别 vars\_prompt                         |
| 13  | Task 级别 vars                                 |
| 14  | role 变量（roles/role\_name/vars/main.yml）      |
| 15  | block 级别 vars                                |
| 16  | include\_vars 加载的变量                          |
| 17  | set\_fact / register                         |
| 18  | role 参数 / include\_role 参数                   |
| 19  | include\_tasks 参数                            |
| 20  | 命令行 \-e / \--extra-vars（最高）                  |

### group\_vars 和 host\_vars 目录

```
inventory/
    hosts.yml
    group_vars/
        all.yml               # 所有主机共享变量
        all/
            main.yml
            vault.yml         # ansible-vault 加密的敏感变量
        webservers.yml        # 仅 webservers 组的变量
        dbservers/
            main.yml
            vault.yml
    host_vars/
        web-01.example.com.yml    # 仅 web-01 的变量

```

`group_vars/all.yml` 示例：

```yaml
# 所有主机的公共变量
timezone: Asia/Shanghai
ntp_server: ntp.aliyun.com
log_level: info

```

### vars\_files 加载

```yaml
- name: 部署应用
  hosts: webservers
  vars_files:
    - vars/common.yml
    - vars/{{ env }}.yml        # 动态加载（根据 env 变量选择文件）
    - "vars/secrets.yml"

```

### ansible-vault 加密敏感变量

```bash
# 加密整个文件
ansible-vault encrypt vars/secrets.yml

# 解密文件
ansible-vault decrypt vars/secrets.yml

# 查看加密文件内容
ansible-vault view vars/secrets.yml

# 编辑加密文件
ansible-vault edit vars/secrets.yml

# 加密单个字符串（嵌入普通 YAML 文件）
ansible-vault encrypt_string 'my_secret_password' --name 'db_password'

# 执行 Playbook 时提供密码
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass

```

加密文件内容示例（`vars/secrets.yml`）：

```yaml
$ANSIBLE_VAULT;1.1;AES256
63303831303566336465396561396531303466306365326665313565353065323...

```

---

## Roles

### Role 目录结构

```
roles/
  myapp/
    tasks/
      main.yml          # 主 Task 文件，其他文件可通过 include 引入
      install.yml
      configure.yml
    handlers/
      main.yml          # Handler 定义
    templates/
      nginx.conf.j2     # Jinja2 模板文件
      app.conf.j2
    files/
      app.service       # 静态文件（copy 模块使用）
    vars/
      main.yml          # Role 变量（高优先级，一般不被覆盖）
    defaults/
      main.yml          # Role 默认变量（最低优先级，允许使用者覆盖）
    meta/
      main.yml          # Role 元数据（依赖声明）
    README.md

```

`defaults/main.yml` 示例：

```yaml
# 使用者可以在 group_vars 或 playbook 中覆盖这些值
app_port: 8000
app_workers: 4
app_log_level: info

```

`meta/main.yml` 示例：

```yaml
galaxy_info:
  author: yourname
  description: Deploy FastAPI app with Supervisor
  min_ansible_version: "2.10"

dependencies:
  - role: common
  - role: nginx
    vars:
      nginx_listen_port: 80

```

### 创建 Role

```bash
# 使用 ansible-galaxy 初始化 Role 目录结构
ansible-galaxy init roles/myapp

# 初始化后生成完整目录结构

```

### requirements.yml 依赖管理

```yaml
# requirements.yml
---
roles:
  # 从 Ansible Galaxy 安装
  - name: geerlingguy.nginx
    version: "3.2.0"

  # 从 Git 仓库安装
  - name: internal-common
    src: git+https://github.com/org/ansible-common.git
    version: main
    scm: git

collections:
  - name: community.docker
    version: ">=3.0.0"
  - name: amazon.aws
    version: "6.0.0"

```

```bash
# 安装 requirements.yml 中的依赖
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install -r requirements.yml

```

---

## 实战场景

### 批量部署 Python 应用（FastAPI + Supervisor）

```yaml
# playbooks/deploy_fastapi.yml
---
- name: 部署 FastAPI 应用
  hosts: webservers
  become: true
  vars:
    app_name: myapi
    app_version: "{{ version | default('main') }}"
    app_dir: "/opt/{{ app_name }}"
    app_user: appuser
    app_port: 8000
    app_workers: 4

  tasks:
    - name: 确保应用用户存在
      ansible.builtin.user:
        name: "{{ app_user }}"
        system: true
        shell: /bin/false
        home: "{{ app_dir }}"
        create_home: false

    - name: 创建应用目录结构
      ansible.builtin.file:
        path: "{{ item }}"
        state: directory
        owner: "{{ app_user }}"
        mode: "0755"
      loop:
        - "{{ app_dir }}"
        - "{{ app_dir }}/releases"
        - "{{ app_dir }}/shared/logs"
        - "{{ app_dir }}/shared/config"

    - name: 拉取最新代码
      ansible.builtin.git:
        repo: "https://github.com/org/{{ app_name }}.git"
        dest: "{{ app_dir }}/releases/{{ app_version }}"
        version: "{{ app_version }}"
        depth: 1
      register: git_result

    - name: 创建虚拟环境并安装依赖
      ansible.builtin.pip:
        requirements: "{{ app_dir }}/releases/{{ app_version }}/requirements.txt"
        virtualenv: "{{ app_dir }}/venv"
        virtualenv_python: python3

    - name: 更新 current 符号链接
      ansible.builtin.file:
        src: "{{ app_dir }}/releases/{{ app_version }}"
        dest: "{{ app_dir }}/current"
        state: link
      notify: restart app

    - name: 部署 Supervisor 配置
      ansible.builtin.template:
        src: templates/supervisor.conf.j2
        dest: "/etc/supervisor/conf.d/{{ app_name }}.conf"
        mode: "0644"
      notify: reload supervisor

    - name: 确保 Supervisor 运行
      ansible.builtin.service:
        name: supervisor
        state: started
        enabled: true

  handlers:
    - name: reload supervisor
      ansible.builtin.command: supervisorctl reread
      notify: update supervisor

    - name: update supervisor
      ansible.builtin.command: supervisorctl update

    - name: restart app
      ansible.builtin.supervisorctl:
        name: "{{ app_name }}"
        state: restarted

```

Supervisor 配置模板（`templates/supervisor.conf.j2`）：

```ini
[program:{{ app_name }}]
command={{ app_dir }}/venv/bin/uvicorn main:app --host 0.0.0.0 --port {{ app_port }} --workers {{ app_workers }}
directory={{ app_dir }}/current
user={{ app_user }}
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile={{ app_dir }}/shared/logs/app.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
environment=APP_ENV="{{ env }}"

```

### 配置 Nginx + SSL

```yaml
# tasks/nginx_ssl.yml
- name: 安装 Certbot
  ansible.builtin.apt:
    name:
      - certbot
      - python3-certbot-nginx
    state: present

- name: 申请 Let's Encrypt 证书
  ansible.builtin.command:
    cmd: >
      certbot certonly --nginx
      -d {{ domain }}
      --non-interactive
      --agree-tos
      --email {{ admin_email }}
    creates: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"
  notify: reload nginx

- name: 部署 Nginx 站点配置
  ansible.builtin.template:
    src: templates/nginx_ssl.conf.j2
    dest: "/etc/nginx/sites-available/{{ domain }}"
    mode: "0644"
  notify: reload nginx

- name: 启用站点
  ansible.builtin.file:
    src: "/etc/nginx/sites-available/{{ domain }}"
    dest: "/etc/nginx/sites-enabled/{{ domain }}"
    state: link
  notify: reload nginx

- name: 配置证书自动续期 cron
  ansible.builtin.cron:
    name: "certbot renew"
    hour: "3"
    minute: "30"
    weekday: "1"
    job: "certbot renew --quiet && systemctl reload nginx"

```

---

## 踩坑与注意事项

### command vs shell 的区别

`command` 模块不通过 Shell 解释器执行，因此：

| 特性       | command      | shell         |
| -------- | ------------ | ------------- |
| 管道符 \|   | 不支持          | 支持            |
| 重定向 > >> | 不支持          | 支持            |
| 变量展开 $   | 仅 Ansible 变量 | 支持 Shell 环境变量 |
| 通配符 \* ? | 不支持          | 支持            |
| 安全性      | 更高           | 较低（注意注入）      |

**原则**：优先用 `command`，仅在需要管道或重定向时才用 `shell`。

### 幂等性设计

Ansible 的核心价值是幂等性，即多次运行结果相同。设计 Task 时注意：

```yaml
# 错误：每次都会执行，无法判断是否已完成
- name: 初始化数据库
  ansible.builtin.command: python manage.py migrate

# 正确：通过 creates 标志或 register + when 实现幂等
- name: 检查迁移标志
  ansible.builtin.stat:
    path: /opt/app/.migrated
  register: migrated

- name: 初始化数据库（仅首次）
  ansible.builtin.command:
    cmd: python manage.py migrate
    chdir: /opt/app
  when: not migrated.stat.exists

- name: 创建迁移完成标志
  ansible.builtin.file:
    path: /opt/app/.migrated
    state: touch
  when: not migrated.stat.exists

```

避免以下反幂等模式：

- 不加 `creates` 的 `command` / `shell`
- 用 `lineinfile` 但没有正确设置 `regexp`（会重复追加）
- `template` 中使用每次都变化的值（如时间戳）

### become 提权失败排查

```bash
# 测试 sudo 权限
ansible webservers -m command -a "whoami" --become -v

# 常见原因 1：目标用户没有 sudo 权限
# 解决：在 /etc/sudoers 中添加
echo "ansible_user ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/ansible

# 常见原因 2：sudo 需要密码但未提供
# 解决：执行时添加 --ask-become-pass 或设置 NOPASSWD
ansible-playbook site.yml --ask-become-pass

# 常见原因 3：become_method 不对（某些系统用 su 而非 sudo）
- name: 提权执行
  ansible.builtin.command: whoami
  become: true
  become_method: sudo    # 或 su、doas、pbrun 等
  become_user: root

```

### 调试常用命令

```bash
# 检查 Inventory 解析结果
ansible-inventory -i inventory/ --list

# 测试主机连通性
ansible all -m ping -i inventory/

# 列出所有 hosts 和 facts
ansible webservers -m setup | grep ansible_distribution

# 空运行（不真正执行，--check）
ansible-playbook site.yml --check

# 显示差异（--diff，配合 template/copy 使用）
ansible-playbook site.yml --check --diff

# 仅执行特定 Tag
ansible-playbook site.yml --tags "deploy,nginx"

# 跳过特定 Tag
ansible-playbook site.yml --skip-tags "slow_tasks"

# 从特定 Task 开始执行
ansible-playbook site.yml --start-at-task="部署应用配置"

# 逐步确认执行
ansible-playbook site.yml --step

# 增加详细输出
ansible-playbook site.yml -v    # -vv -vvv 逐级增加

```

---

## 最佳实践

### 目录结构：遵循 Ansible 推荐布局

```
project/
├── ansible.cfg              # 项目级配置（inventory 路径、roles 路径等）
├── inventory/
│   ├── production/
│   │   ├── hosts            # 生产环境主机
│   │   └── group_vars/
│   │       ├── all.yml      # 所有主机共用变量
│   │       └── webservers.yml
│   └── staging/
│       └── hosts
├── roles/
│   ├── common/              # 基础配置（NTP、用户、防火墙）
│   ├── nginx/               # Nginx 角色
│   └── app/                 # 应用部署角色
├── site.yml                 # 主 Playbook（入口）
├── webservers.yml           # 仅针对 webservers 的 Playbook
└── requirements.yml         # Ansible Galaxy 依赖声明

```

### Ansible Vault：加密敏感变量

不要在 Git 中存储明文密码、API Key 等敏感信息：

```bash
# 加密整个文件
ansible-vault encrypt vars/secrets.yml

# 编辑加密文件
ansible-vault edit vars/secrets.yml

# 加密单个字符串（嵌入到普通 YAML 中）
ansible-vault encrypt_string 'mysecretpassword' --name 'db_password'

# 执行时提供密码文件
ansible-playbook site.yml --vault-password-file ~/.vault_pass

```

在 `vars/secrets.yml` 中：

```yaml
db_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  66386439...（加密后的内容）

```

### 幂等性设计原则

- 优先使用声明式模块（`file`、`template`、`apt`、`service`）而非 `command`/`shell`
- 使用 `command`/`shell` 时配合 `creates`、`removes` 或 `changed_when` 实现幂等
- 避免 `command: echo "something"` 这类永远 changed 的写法

```yaml
# 错误：每次都 changed，破坏幂等性
- name: 初始化数据库
  ansible.builtin.command: python manage.py migrate

# 正确：通过返回码判断是否真正有变更
- name: 初始化数据库
  ansible.builtin.command: python manage.py migrate
  register: migrate_result
  changed_when: "'No migrations to apply' not in migrate_result.stdout"

```

### 滚动更新：serial + 健康检查

```yaml
- name: 滚动更新 Web 服务器（每次 20%）
  hosts: webservers
  serial: "20%"          # 或 serial: 2（绝对数量）
  max_fail_percentage: 0 # 任意主机失败立即终止

  tasks:
    - name: 从负载均衡摘除节点
      community.general.haproxy:
        state: disabled
        host: "{{ inventory_hostname }}"
        socket: /var/run/haproxy/admin.sock

    - name: 部署新版本
      ansible.builtin.git:
        repo: "https://github.com/org/app.git"
        dest: /opt/app
        version: "{{ app_version }}"

    - name: 健康检查
      ansible.builtin.uri:
        url: "http://localhost:8000/health"
        status_code: 200
      retries: 5
      delay: 10

    - name: 重新加入负载均衡
      community.general.haproxy:
        state: enabled
        host: "{{ inventory_hostname }}"
        socket: /var/run/haproxy/admin.sock

```

### 性能优化

| 优化项              | 配置                                                                  | 效果           |
| ---------------- | ------------------------------------------------------------------- | ------------ |
| 关闭 gather\_facts | gather\_facts: false（不需要 facts 时）                                   | 每台主机节省 1–3 秒 |
| 增加并发数            | ansible.cfg: forks = 20（默认 5）                                       | 并发执行更多主机     |
| SSH 连接复用         | ssh\_args = -o ControlMaster=auto -o ControlPersist=60s             | 减少 SSH 握手    |
| 使用 Mitogen       | strategy\_plugins = /path/mitogen/ansible\_mitogen/plugins/strategy | 整体提速 3–7 倍   |
| Pipelining       | pipelining = True（需目标 sudoers 配置 requiretty 已禁用）                    | 减少 SSH 往返    |

---

## 常见陷阱

### 陷阱：任务幂等性破坏——每次都触发 changed

**现象：** Playbook 每次运行相同任务都报 `changed`，重复执行会触发不必要的 handler（如重启服务）。  
**原因：** 使用 `shell`/`command` 模块执行的命令无状态感知，Ansible 无法判断是否需要执行，默认标记为 `changed`。  
**解决：** 优先用内置模块（`copy`、`template`、`lineinfile`、`service`），它们原生支持幂等性。必须用 `shell` 时，配合 `creates` 或 `changed_when: false`：

```yaml
- name: 初始化数据库（仅首次）
  shell: init-db.sh
  args:
    creates: /var/lib/db/.initialized  # 文件存在则跳过

```

### 陷阱：`become: true` 与 SSH 转发冲突

**现象：** 使用 `become: true` 提权后，任务中的 `git` 或 `ssh` 操作报权限错误，无法访问私钥。  
**原因：** `become` 切换用户后，SSH agent forwarding 的 socket 路径属于原用户，新用户无读取权限。  
**解决：** 避免在 `become` 后执行需要 SSH 认证的操作；改用 deploy key 或在目标机器上配置 HTTPS token 认证；或用 `become_flags: -E` 保留环境变量（需 sudo 配置允许）。

### 陷阱：变量优先级混乱导致意外覆盖

**现象：** 在 `group_vars` 中定义的变量被某个任务中的 `set_fact` 覆盖，后续任务使用了错误值。  
**原因：** Ansible 变量有 22 个优先级层级，`set_fact` 的优先级高于 `group_vars`，容易在调试时留下副作用。  
**解决：** 了解优先级顺序（extra vars > set\_fact > role vars > group\_vars），生产 Playbook 避免用 `set_fact` 修改全局变量，改用 `register` 保存局部结果。

---

## 参见

- [Kubernetes基础](https://blog.vercanti.com/kubernetes-ji-chu/) — Ansible 与 Kubernetes 结合部署应用
- [Docker Compose完全指南](https://blog.vercanti.com/docker-compose-wan-quan-zhi-nan/) — 用 Ansible 管理 Docker Compose 应用
- [Prometheus与Grafana监控](https://blog.vercanti.com/prometheus-yu-grafana-jian-kong/) — 用 Ansible 自动化部署监控栈
- [Git进阶指南](https://blog.vercanti.com/git-jin-jie-zhi-nan/) — Ansible Playbook 的版本控制工作流