docs(operations): end-to-end IaC<->Ansible dynamic-inventory workflow

Documents the YAML->generate.py->terraform->cmdb.json->ansible flow, the FQDN
inventory_hostname contract, the two execution models, the Vault-OIDC pipeline,
the non-empty/fail-fast checks, and the key fixes that make it work end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Haitao Pan 2026-06-24 20:51:54 +08:00
parent c2cd3035a4
commit d3356a0ef0

View File

@ -0,0 +1,122 @@
# IaC ↔ Ansible 动态 inventory 联动工作流
端到端梳理:**YAML 资源声明 → Python+Jinja2 渲染显式 HCL → Terraform 起机 →
CMDB/inventory 生成 → Ansible 部署**。配套 Vault(OIDC)取密钥、Cloudflare DNS 同步。
- IaC 侧:`ai-workspace-infra/iac_modules/terraform-hcl-standard/vultr-vps`
- Ansible 侧:`ai-workspace-infra/playbooks`
- 编排/流水线:`xworkspace-console/.github/workflows/deploy-ai-workspace-iac.yaml`
- 相关文档:[vault-github-actions.md](vault-github-actions.md) · [iac-ansible-deploy-verification.md](iac-ansible-deploy-verification.md) · IaC 渲染约束见 `iac_modules/terraform-hcl-standard/AGENTS.md``skills/terraform-yaml-render-pattern/SKILL.md`
## 1. 数据流
```
config/resources/ai-workspace-hosts.yaml # 唯一人工入口global / ssh_keys / hosts
│ generate.py render YAML → 显式 HCL无 for_each/count/dynamic
workdir/{provider.tf, variables.tf, cloud-init.yaml, generated_hosts.tf,
terraform.auto.tfvars.json}
│ terraform apply (创建 VPS + 公网 IP
terraform output cmdb_runtime 只输出运行时才确定的事实ip / instance_id / os_id
│ generate.py inventory (运行时事实 + YAML 静态字段 合并)
workdir/{cmdb.json, inventory.ini} # 数据契约gitignore不入库
│ │
│ terraform_cmdb.py │ inventory.ini
▼ (动态 inventory) ▼ (静态 inventory)
Ansible 部署(两种执行模型,见 §4
```
> 渲染产物(`generated_hosts.tf` / `terraform.auto.tfvars.json` / `cmdb.json` /
> `inventory.ini` 等)均 `.gitignore`IaC 变更后重跑 `generate.py inventory` 再生成。
## 2. 数据契约 cmdb.json
`generate.py inventory` 产出,贯穿三个流水线 job也是动态 inventory 的唯一来源
(不直接耦合 tfstate。**以 `service_domains` 首个 FQDN 为键**(动态取自 YAML
无 service_domains 时回退 `name`)——即 `inventory_hostname` 是真实 FQDN绝不硬编码短名/127.0.0.1。
```jsonc
{
"xworkmate-bridge-debian-13.svc.plus": { // key = inventory_hostname (FQDN)
"name": "ai-debian13", // 资源声明里的短名terraform 模块名)
"fqdn": "xworkmate-bridge-debian-13.svc.plus",
"ip": "x.x.x.x", // 运行时事实terraform output
"instance_id": "...", "os_id": 2625,
"os_name": "Debian 13 x64 (trixie)", "plan": "vc2-4c-8gb", "region": "nrt",
"ansible_user": "root",
"groups": ["ai_workspace", "debian"],
"host_vars": { "service_domains": "...", "role": "primary", ... }
}
}
```
## 3. generate.py 两个子命令
| 子命令 | 输入 | 输出 | 职责 |
| --- | --- | --- | --- |
| `render` | `config/resources/*.yaml` | `generated_hosts.tf` + `terraform.auto.tfvars.json` + 拷入共享 `.tf` | YAML→显式 HCLJinja2 展开命名唯一块global 段进 tfvars |
| `inventory` | `terraform output cmdb_runtime` + YAML | `cmdb.json` + `inventory.ini` | 合并「运行时事实 + 静态字段」;以 FQDN 为键 |
参数化:`--resources` / `--workdir`(共享一份脚本,不在每个 env 各放一份)。
## 4. 两种执行模型(关键)
**all-in-one playbook 设计为「在目标主机本地执行」**(见 [iac-ansible-deploy-verification.md](iac-ansible-deploy-verification.md) §3
| 模型 | 调用 | localhost | 适用 |
| --- | --- | --- | --- |
| **本地 / pull** | `curl\|bash``ansible-playbook -i <FQDN>, -c local` | = 目标主机本身 | self-host、流水线 deploy jobssh 到主机后本地跑) |
| **远程 controller** | `ansible-playbook -i <inventory> root@host`(经 SSH | = controller(runner/mac) | 批量管控;但 `roles/agent_skills` 历史上 `delegate_to: localhost` 会错位 |
- `roles/agent_skills` 已重构为**全程在目标主机执行**`git clone` 取源、`copy` 合并、无 `delegate_to: localhost`),两种模型行为一致。
- 流水线 deploy job 采用**本地/pull**runner 仅 ssh 到主机执行官方引导,主机内部 `-c local`(自动离线包加速),规避 controller≠主机 的错位,且与用户 self-host 同一路径。
- on-host 的 `inventory_hostname``.sh``XWORKMATE_BRIDGE_DOMAIN`(流水线注入 = CMDB service_domains或主机 FQDN绝不硬编码 127.0.0.1。
## 5. 流水线deploy-ai-workspace-iac.yaml
| job | 作用 | 开关 |
| --- | --- | --- |
| `provision` | terraform 起机 + 渲染 cmdb.json + 动态生成部署矩阵 | `terraform_action=apply` |
| `deploy`matrix/主机) | 按 cmdb.json 取 IPssh 到主机本地跑 `curl\|bash` 引导 | `run_deploy` |
| `dns` | 依据 inventory 的 service_domains/IP 同步 Cloudflare A 记录 | `run_dns` |
- **密钥**:不用 GitHub Secrets**Vault OIDC(JWT)**`kv/data/CICD`VULTR/SSH/Cloudflare/INFRA token`kv/data/openclaw`LLM keys读取详见 [vault-github-actions.md](vault-github-actions.md)。
- **公网暴露**Linux VPS 默认仅 `XWORKMATE_BRIDGE_PUBLIC_ACCESS=true`bridge 进 `/etc/caddy/conf.d/`console(17000) 等本地only。macOS 全内网、`caddy_enabled=false` 不装 caddy。
## 6. 非空传递检查与 fail-fast默认要求非空
| 边界 | 检查 | 失败行为 |
| --- | --- | --- |
| terraform → cmdb.json | `generate.py inventory` 校验每台 `ip`/`instance_id` 非空 | `sys.exit` + 列出缺失主机 |
| Vault → 流水线 job | 每 job `Validate required secrets` 步骤校验必需 `steps.vault.outputs.*` 非空 | `::error::` 命名缺失键/路径 + `exit 1` |
| 域名 → hostname/caddy | bridge 角色断言 `xworkmate_bridge_domain` 为非空 FQDNcaddy 暴露时) | `assert` fail_msg 指引设置 env/service_domains |
| bridge 二进制 / auth token | bridge 角色既有 `assert` | fail_msg |
## 7. 一次部署(操作)
```bash
# 流水线GitHub Actions → "Deploy AI Workspace (IaC + Ansible + Cloudflare)" → Run workflow
# inputs: terraform_action=apply, run_deploy=true, run_dns=true
# 本地手动IaC 起机 + 出 inventory
cd iac_modules/terraform-hcl-standard/vultr-vps
export TF_VAR_vultr_api_key=... # 机密走环境变量,禁止入 YAML/tfvars
python3 scripts/generate.py render
terraform -chdir=envs/ai-workspace init && terraform -chdir=envs/ai-workspace apply
python3 scripts/generate.py inventory # 缺 ip 会 fail-fast
# 部署:在每台主机上跑 curl|bash 引导(本地模型),或用 inventory.ini 驱动
```
## 8. 已应用的关键修复(端到端可用前提)
- **console 伺服**:预编译 runtime 只发 `dashboard/dist`,改用 `python3 -m http.server` 本地伺服Linux/macOS 统一,不起第二个 caddy
- **api 路径**manifest `apiBinary: bin/xworkspace-api`,修正 `api_dir``bin/`
- **agent_skills**:重构为主机本地 `git clone` + `copy`,去 `delegate_to: localhost`
- **apt lock_timeout**`module_defaults.apt.lock_timeout`(模板值)经 `ansible.builtin.package` 间接派发到 apt 时不渲染 → Debian 上相关预装任务改用 `ansible.builtin.apt`bridge/litellm/fail2ban
- **py3.13**`ANSIBLE_PIPELINING=False` + `PYTHONWARNINGS=ignore`
- **inventory_hostname = service_domains FQDN**(动态、非硬编码)。