---
url: /courses/cloud-platform-build-management/17-ansible-observability/index.md
---
# 第十七次课：Ansible 自动化与可观测性

## 进入本课环境

* `[Windows PowerShell] wsl ~`：打开本机默认的 WSL2 Ubuntu；Inventory、Playbook 和 Ansible 命令都保存在这里。
* `[WSL] ssh root@你的ECS公网IP`：先确认 WSL 能正常连接自己的 ECS，再退出 SSH 会话并由 Ansible 发起后续连接；如果登录用户不是 `root`，同步修改 Inventory 中的远程用户。

到目前为止，我们常常登录服务器后逐条执行命令。只有一台机器时还能勉强记住步骤；机器增多、任务重复或需要审计时，“我上次大概这样做的”就很危险。Ansible 把目标主机放进 Inventory，把期望步骤写进 Playbook，再用 changed、failed、HTTP 和业务标记判断结果。

::: tip 关键提醒
自动化不是把一串 shell 命令换成 YAML。可维护的自动化要有明确目标、可重复执行、可解释的变更结果、失败门禁、健康证据和只清理自身资源的边界。
:::

学完这一课，做到四件事：

1. 区分控制节点、被管理节点、Inventory、模块和 Playbook。
2. 从 WSL 或演示控制端通过 SSH 运行 `ansible.builtin.ping`。
3. 阅读一次首跑与一次重复执行的 recap，解释 `changed=0` 的意义和限制。
4. 用 `uri`、`assert` 和容器状态组合服务健康检查。
5. 处理“HTTP 200 但发布内容漂移”的故障，并用同一 Playbook 恢复期望状态。

## 17.1 先分清控制端和目标主机

Inventory 与 Playbook 保存在控制端，模块通过 SSH 到目标主机执行。开始前先确认这两个位置，避免把本地路径、远端目录和运行结果混在一起。

实训包包含 `ansible.cfg`、`inventory.ini`、`site.yml`、`health.yml`、`fault.yml`、`cleanup.yml`、`verify.sh` 等文件（从智慧职教课程资源区下载 `lab-17-starter.zip`）：

```bash
[WSL] mkdir -p ~/cloud-course/lab-17
[WSL] cd ~/cloud-course/lab-17
[WSL] cp -r /path/to/lab-17-starter/* .
[WSL] ssh root@你的ECS公网IP hostname
```

最后一条只确认 SSH 连通性——`hostname` 应返回你的 ECS 主机名。

同一份 Playbook 会连续执行两次。第一次建立期望状态，第二次用于观察是否还有多余变更；随后用一次可恢复的页面漂移检查健康门禁和恢复路径。

## 17.2 控制端与被管理端各自做什么

本课角色架构包含控制节点与被管理节点：

| 角色 | 本课环境 | 核心职责 | 绝不应该暴露或保存的内容 |
|---|---|---|---|
| 控制端 | 个人 WSL；演示环境使用本地 Linux/macOS 控制节点 | 保存 Inventory 清单与 Playbook，发起 SSH 连接并汇总执行结果 | 绝不能把密钥与真实 IP 写入代码包 |
| 被管理端 | 云主机 B，登录目标 `root@SYG680400` | 运行 Python 探针与模块，创建隔离目录与部署业务容器 | 被管理端不需要安装 Ansible 控制程序 |

演示机不是 Windows WSL 环境，因此本课截图中统一标记为 `[CTRL]`；学生在 WSL 里敲相同命令时，路径换成各自的本地目录即可。两种控制节点都是通过 SSH 管理 ECS，但请注意不能把演示机的路径或主机名直接抄进个人报告。

```bash
[WSL] python3 --version
```

Ansible 的核心逻辑在控制端运行。本课基线版本要求 Python 3.10 或更高版本。Ubuntu 22.04 自带的 Python 3.10 可以直接使用；如果显示 3.9 或更早版本，先停止安装并请教师统一处理控制端环境，不要自行替换系统 Python。

```bash
[WSL] python3 -m venv "$HOME/.venvs/cloud-course-ansible"
```

新建独立的 Python 虚拟环境，隔离全局环境。

```bash
[WSL] "$HOME/.venvs/cloud-course-ansible/bin/python" -m pip install \
  "ansible-core==2.17.14"
```

安装经课程验证过的 Ansible Core 指定版本。这个版本兼容 Python 3.10，适合常见的 Ubuntu 22.04 WSL 环境。较新的 Ansible Core 版本可能同时提高 Python 最低版本；在真实的生产环境里，应当把 Python、Ansible 和目标系统的兼容关系一起验证后再升级。

```bash
[WSL] source "$HOME/.venvs/cloud-course-ansible/bin/activate"
```

激活虚拟环境。命令行提示符前会多出环境名称。

## 17.3 Inventory 不是服务器密码表

本课定义的 Inventory 主机清单格式：

```ini
[course_hosts]
cloud-b ansible_host=B ansible_user=root ansible_python_interpreter=/usr/bin/python3

[course_hosts:vars]
ansible_ssh_common_args='-o BatchMode=yes'
```

关键配置项说明：

| 字段 | 属性含义 | 本课设定 |
|---|---|---|
| `course_hosts` | 逻辑主机组 | 本课限定为包含单个目标机器 |
| `cloud-b` | Inventory 内部识别的服务名 | 仅作标识，不是 IP 地址 |
| `ansible_host=B` | 引用本地 SSH 配置文件里的 Config 别名 | 真实 IP 和私钥依然由 `.ssh/config` 统一托管 |
| `ansible_user=root` | SSH 登录用户名 | 演示环境账号 |
| `ansible_python_interpreter` | 远端 ECS 的 Python 解释器路径 | `/usr/bin/python3` |
| `BatchMode=yes` | 禁用交互式密码弹窗询问 | 一旦认证失败直接退出并抛错 |

Inventory 切忌直接写入明文密码、私钥或验证码。学生在个人 ECS 操作时，建议使用普通用户搭配 `sudo` 权限。

```bash
[WSL] ansible-inventory --graph
```

检查 Inventory 能否解析，预期 `course_hosts` 下只有 `cloud-b`。

```bash
[WSL] ansible course_hosts -m ansible.builtin.ping -o
```

Ansible 的 ping 模块通过 SSH 登录远端，验证远端 Python 是否可用，并返回 `pong`；它不是网络层 ICMP ping。

2026-07-28 的真实控制端到云主机 B 结果：

{{guided-demo:lesson-17-automation-chain}}

## 17.4 Playbook 把期望状态写清楚

`site.yml` 只管理本课资源：

```yaml
- name: Deploy the isolated lesson 17 health site
  hosts: course_hosts
  gather_facts: false
```

`hosts` 限制目标为课程组。`gather_facts: false` 跳过本课不需要的全量事实采集，减少输出和等待；需要操作系统事实时再按需开启。

创建目录使用 `file` 模块：

```yaml
- name: Ensure the course directories exist
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    mode: "0755"
```

目录已经存在且权限符合时，模块返回 ok，不重复创建目录。

发布页面使用 `copy` 模块：

```yaml
- name: Publish the expected lesson page
  ansible.builtin.copy:
    dest: /opt/xpk-course-demos/lesson-17/site/index.html
    mode: "0644"
    content: |
      ...
      <p>release=lesson17-v1</p>
      <p>health=lesson17-health-ok</p>
```

copy 会比较内容。目标缺失或漂移时写入并返回 changed；内容一致时返回 ok。

容器先 inspect，再按条件创建：

```yaml
- name: Start the isolated course container
  ansible.builtin.command:
    argv:
      - docker
      - run
      - --detach
      - --name
      - cloud-course-lab-17-web
      - --publish
      - 127.0.0.1:18017:80
      - nginx:1.27-alpine
  when: lesson17_container_inspect.rc != 0
```

容器存在时跳过创建，避免第二次运行出现名称冲突。入口只绑定远端回环地址，不新增公网安全组。

::: warning 安全边界
不要把 `docker rm -f $(docker ps -aq)`、`pkill`、全局防火墙修改或未知服务重启放进 Playbook。自动化会放大命令的速度，也会放大错误范围。
:::

## 17.5 首跑和第二次执行要放在一起看

```bash
[WSL] ansible-playbook --syntax-check site.yml
```

检查 YAML 和任务结构能否解析。语法通过只过了第一关，SSH、权限、镜像、端口和业务内容是否都可用还得接着验证。

```bash
[WSL] ansible-playbook site.yml
```

第一次运行真实结果：

* 创建课程目录；
* 发布页面；
* 创建 `cloud-course-lab-17-web`；
* 检查容器边界；
* 请求 `127.0.0.1:18017`；
* recap 为 `ok=7 changed=3 failed=0`。

```bash
[WSL] ansible-playbook site.yml
```

不修改清单，立即执行第二次。真实结果中目录和页面为 ok，创建容器任务为 skipping，HTTP 继续通过，recap 为 `ok=6 changed=0 failed=0 skipped=1`。

::: tip 关键提醒
`changed=0` 是一次有边界的证据：它只说明这份 Playbook 在当时环境和相同输入下没有报告变更。若任务错误地写了 `changed_when: false`，数字好看也不意味着真的幂等。
:::

## 17.6 可观测性从“现在是否符合预期”开始

完整可观测性还包括指标、日志、追踪和告警。本课先做最小健康链：

| 层次 | 证据 | 能发现什么 | 不能发现什么 |
|---|---|---|---|
| SSH/Python | ping pong | 控制端能运行远端模块 | HTTP 与业务内容 |
| 容器 | inspect status/image | 容器运行与镜像 | 页面是否正确 |
| HTTP | `status=200` | Web 入口响应 | 版本和内容是否正确 |
| 发布标记 | `release=lesson17-v1` | 当前页面版本 | 所有业务功能 |
| 健康标记 | `lesson17-health-ok` | 课程健康契约 | 长期性能和外部依赖 |
| Play recap | failed/changed/unreachable | 自动化任务结果 | 用户体验趋势 |

`health.yml` 先读取容器，再用 `uri` 请求完整路径，最后用 `assert` 检查发布和健康标记。

```bash
[WSL] ansible-playbook health.yml
```

健康时返回 `lesson17 health check passed`，recap 为 `failed=0 changed=0`。

{{chat-lab:lesson-17-health-evidence}}

## 17.7 HTTP 200 也可能是故障

`fault.yml` 只把课程页面替换为漂移版本：

```bash
[WSL] ansible-playbook fault.yml
```

页面仍由 Nginx 返回 200，但内容变成 `release=unknown-drift`，缺少健康标记。

```bash
[WSL] ansible-playbook health.yml
```

真实检查中 `uri` 任务为 ok，随后 assert 失败：

```txt
HTTP 200 returned, but the expected lesson17 marker or release is missing
```

这说明网络、容器和 HTTP 仍通，但发布内容不符合契约。

```bash
[WSL] ansible-playbook site.yml
```

重新应用期望状态。copy 发现内容漂移并恢复页面，recap 为 `changed=1 failed=0`；容器任务继续 skipping，没有重建服务。

```bash
[WSL] ansible-playbook health.yml
```

同一路径再次检查，容器 running、HTTP 200、release 与健康标记全部通过。

四步记录保留了故障注入、健康检查失败、重新应用期望状态和同路复测。Playbook 输出中的 `failed=1` 属于受控任务结果；卡片没有原始 Shell 退出码，因此不会额外猜测 exit code。

`Request the complete local service path` 为 `ok`，说明 HTTP 请求已经返回；紧接着的断言失败，说明响应内容缺少课程发布标记或健康标记。两行放在一起，才能看出这是“200 但内容不对”，而不是 SSH 或 Web 连接失败。

页面任务为 `changed`，容器任务为 `skipping`，recap 只有一次变更。这说明本次恢复修改了漂移文件，没有重建正常容器；它只描述 Playbook 管理范围内的变化。

最后一张图沿原来的 `health.yml` 路径复测。`running`、`http_status=200`、`release=lesson17-v1` 和 `lesson17-health-ok` 分别覆盖容器、协议、版本与内容契约；这仍不是公网路径或长期监控结论。

## 17.8 changed、failed 和 unreachable 怎样读

| 结果 | 含义 | 下一步 |
|---|---|---|
| `ok` | 任务成功且未报告变更 | 继续核对业务证据 |
| `changed` | 任务成功并报告状态改变 | 确认改变符合计划 |
| `failed` | 已连接目标，但模块或断言失败 | 保留任务和错误详情 |
| `unreachable` | SSH、认证、路由或目标不可达 | 回到控制端到远端链路 |
| `skipped` | 条件不满足或显式跳过 | 检查条件是否符合预期 |
| `rescued` | block 失败后进入 rescue | 仍需确认最终状态 |
| `ignored` | 失败被忽略 | 警惕把真实故障掩盖 |

不要给关键健康任务加 `ignore_errors: true` 来让 recap 变绿。失败门禁的价值就在于阻止错误结果被批准。

## 17.9 验收、清理和提交

```bash
[WSL] bash verify.sh
```

脚本检查课程文件、Inventory、Playbook 语法、Ansible ping 和 `health.yml`。远端不可达或健康断言失败时返回非 0。

```bash
[WSL] ansible-playbook cleanup.yml
```

只删除 `cloud-course-lab-17-web` 和 `/opt/xpk-course-demos/lesson-17`。

```bash
[WSL] ansible course_hosts -m ansible.builtin.shell \
  -a "docker ps -a --filter name=^/cloud-course-lab-17-web$; ss -lnt 'sport = :18017'"
```

人工检查容器不存在，18017 没有监听。不要把这条 shell 模块扩展成全局清理。

完整步骤见[实训 17：Ansible 幂等部署与健康检查](./lab-17.md)。

提交文件名：

```txt
班级_学号_姓名_第17次课_Ansible与可观测性报告.docx
```

只提交一个 Word 到智慧职教“第17次课”。不提交 SSH 私钥、实际 Inventory、完整地址、Ansible 临时目录、完整日志或其他业务数据。

{{reflection-checkpoint:lesson-17-ansible-ready}}

## 17.10 第十七次课小测

{{assessment:lesson-17-check}}

## 17.11 小结

* Inventory 定义目标和分组，不是存密码和私钥的地方。
* `ansible.builtin.ping` 验证 SSH 与远端 Python，不等于 ICMP 或业务健康。
* 相同输入下第二次 `changed=0` 是幂等证据，但仍受任务写法和环境边界限制。
* 容器 running、HTTP 200、发布版本和健康标记属于不同观察层。
* 内容漂移可以在 HTTP 仍返回 200 时被 assert 发现。
* 恢复时重新应用期望状态，只修改漂移文件，不重建正常容器。
* cleanup 只处理本课资源，并复核已有 8000、18443 业务端口。

下一次课将把 Compose、备份恢复、自动健康检查和故障自动修复流程整合为课程综合项目。

进入综合项目前，整理一份最小交接清单：

* 哪些文件声明期望状态，哪些文件只在运行时生成；
* 哪条命令能从空目录部署，哪条命令能重复验收；
* 健康检查分别覆盖容器、HTTP 状态、发布版本和内容标记中的哪几层；
* 备份放在哪里、怎样校验、怎样只在隔离目录验证恢复；
* 故障注入和清理只允许作用于哪个项目名、目录与端口。

第 18 次课不会再单独讲这些工具，而是检查它们能否被另一位同学按交接说明复现。

## 17.12 资料来源

以下资料在 2026-07-28 核对：

* [PyPI：ansible-core 2.17.14（要求 Python 3.10 或更高版本）](https://pypi.org/project/ansible-core/2.17.14/)
* [Ansible：How to build your inventory](https://docs.ansible.com/projects/ansible/latest/inventory_guide/intro_inventory.html)
* [Ansible：Introduction to ad hoc commands](https://docs.ansible.com/projects/ansible/latest/command_guide/intro_adhoc.html)
* [Ansible：Playbooks](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_intro.html)
* [Ansible：ansible.builtin.ping](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/ping_module.html)
* [Ansible：ansible.builtin.copy](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/copy_module.html)
* [Ansible：ansible.builtin.uri](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/uri_module.html)
* [Ansible：Check mode and diff mode](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_checkmode.html)
* [Ansible：Error handling in playbooks](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_error_handling.html)

{{assistant-invite:lesson-17-finish}}
