feat(vultr-vps): ai-workspace IaC env with YAML+Jinja2 render and Ansible linkage
- envs/ai-workspace: hosts.yaml -> generate.py renders explicit Terraform module/resource blocks via Jinja2 (no for_each/count); terraform runtime output merged with YAML -> cmdb.json + inventory.ini for Ansible. - modules/compute: backups bool -> "enabled"/"disabled" (vultr provider 2.19+); add required_providers to compute & iam modules. - skills/terraform-yaml-render-pattern + terraform-hcl-standard/AGENTS.md: binding spec for the render pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4755198a9d
commit
de7fe511d7
75
skills/terraform-yaml-render-pattern/SKILL.md
Normal file
75
skills/terraform-yaml-render-pattern/SKILL.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Skill: terraform-yaml-render-pattern
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
约束性规范:在 `iac_modules/terraform-hcl-standard/**` 下编排可批量创建的资源
|
||||||
|
(典型如多主机 VPS)时,**必须**采用「YAML 描述 → Python+Jinja2 渲染显式 HCL 块
|
||||||
|
→ Terraform apply → Python 合并生成 CMDB/Inventory」的范式,**不得**在 HCL 内做循环。
|
||||||
|
|
||||||
|
这是 binding skill:与本文件冲突的写法一律以本文件为准。
|
||||||
|
配套约束见 `iac_modules/terraform-hcl-standard/AGENTS.md`。
|
||||||
|
|
||||||
|
参考实现(基准,新 env 照此结构落地):`terraform-hcl-standard/vultr-vps/envs/ai-workspace/`
|
||||||
|
|
||||||
|
## Pattern(强制数据流)
|
||||||
|
|
||||||
|
```
|
||||||
|
hosts.yaml (唯一人工入口:资源描述 / CMDB 源)
|
||||||
|
│ generate.py render —— 循环在 Python+Jinja2 侧完成
|
||||||
|
├─▶ generated_hosts.tf 每实例/每 key 一个独立显式 module/resource/data 块
|
||||||
|
└─▶ terraform.auto.tfvars.json ──▶ variables.tf (global 段 -> 变量)
|
||||||
|
│
|
||||||
|
▼ terraform apply
|
||||||
|
output "cmdb_runtime" (仅运行时事实:ip / instance_id / 解析后的 os_id)
|
||||||
|
│ generate.py inventory —— 合并 YAML 静态字段 + 运行时输出
|
||||||
|
├─▶ cmdb.json (IaC ↔ Ansible 契约)
|
||||||
|
└─▶ inventory.ini
|
||||||
|
│
|
||||||
|
▼ Ansible 动态 inventory: playbooks/inventory/terraform_cmdb.py (只读 cmdb.json)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
### MUST NOT
|
||||||
|
- 不在 env 的 `.tf` 中使用 `for_each` / `count` / `dynamic`。
|
||||||
|
- 不用 `templatefile()` + `%{ for }` / `%{ if }` 等 HCL 模板控制结构做渲染。
|
||||||
|
|
||||||
|
### MUST
|
||||||
|
- 资源信息由 env 内 `hosts.yaml` 描述;多份资源由 Jinja2 展开为**多个命名唯一的显式块**。
|
||||||
|
- YAML 全局段经 `terraform.auto.tfvars.json` 传给 `variables.tf`;逐实例字段由 Jinja2 进 `.tf`。
|
||||||
|
- 机密走环境变量(如 `TF_VAR_vultr_api_key`),**禁止**写入 YAML/tfvars;公钥可入 YAML。
|
||||||
|
- 每个 env 提供 `generate.py`,至少含 `render` 与 `inventory` 两个子命令(职责见上图)。
|
||||||
|
- Terraform 只输出运行时才确定的事实;静态字段(os_name/plan/groups/host_vars…)由 Python 合并。
|
||||||
|
- 渲染产物(`generated_hosts.tf`、`terraform.auto.tfvars.json`、`cmdb.json`、`inventory.ini`)
|
||||||
|
加入 `.gitignore`,不入库。
|
||||||
|
- `inventory.ini` 中含空格的 host_var 值加引号(`key="a b c"`)。
|
||||||
|
- Ansible 动态 inventory 只消费 `cmdb.json`,不直接耦合 tfstate;IaC 变更后重跑 `generate.py inventory`。
|
||||||
|
|
||||||
|
### SHOULD
|
||||||
|
- 复用 `modules/compute` 等既有模块,不在 env 内重写 provider 资源。
|
||||||
|
- 每个用到 provider 的子模块声明 `required_providers`(含正确 `source`)。
|
||||||
|
- OS 用 `data "vultr_os"` 按 `os_name` 解析 `os_id`,避免硬编码漂移 ID;解析不到时允许直接给 `os_id`。
|
||||||
|
|
||||||
|
## Reference Layout(新 env 必备文件)
|
||||||
|
|
||||||
|
```
|
||||||
|
envs/<name>/
|
||||||
|
hosts.yaml # 唯一人工入口:global / ssh_keys / hosts
|
||||||
|
generate.py # render + inventory 两个子命令
|
||||||
|
templates/
|
||||||
|
hosts.tf.j2 # 渲染逐实例 module/data + 资源块
|
||||||
|
inventory.ini.j2 # 渲染静态 inventory
|
||||||
|
variables.tf # 全局变量声明(值来自 tfvars.json)
|
||||||
|
provider.tf
|
||||||
|
provision.sh # 一键 render -> apply -> inventory -> (可选) ansible
|
||||||
|
.gitignore # 忽略渲染产物与 .terraform/tfstate
|
||||||
|
# 渲染产物(不入库):generated_hosts.tf / terraform.auto.tfvars.json / cmdb.json / inventory.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operator Checklist(提交前自检)
|
||||||
|
|
||||||
|
- `terraform fmt` 无 diff;`terraform validate` 通过。
|
||||||
|
- `python3 generate.py render` 产出合法 `.tf`(`validate` 通过)。
|
||||||
|
- 生成的 `inventory.ini` 能被 `ansible-inventory -i <file> --graph` 正确解析。
|
||||||
|
- 渲染产物已被 `.gitignore` 忽略;机密未入库。
|
||||||
|
- HCL 内无 `for_each`/`count`/`dynamic`/`templatefile` 控制结构。
|
||||||
87
terraform-hcl-standard/AGENTS.md
Normal file
87
terraform-hcl-standard/AGENTS.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# AGENTS.md —— Terraform 模块约束规范(强制)
|
||||||
|
|
||||||
|
适用范围:`iac_modules/terraform-hcl-standard/**`。本文件为**约束性规范**,
|
||||||
|
在本目录下创建/修改 Terraform env 时 **必须遵守**。冲突时以本文件为准。
|
||||||
|
|
||||||
|
## 0. 核心范式(MUST)
|
||||||
|
|
||||||
|
新建可批量编排的 env(如多主机 VPS)时,**必须**采用如下数据流,
|
||||||
|
**不得**在 HCL 内做循环:
|
||||||
|
|
||||||
|
```
|
||||||
|
YAML 描述资源
|
||||||
|
│ (Python + Jinja2 渲染,循环在此完成)
|
||||||
|
▼
|
||||||
|
显式 Terraform module/resource 块 ── 每个实例/资源一个独立块,不用 for_each
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
terraform apply
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Python 合并「YAML 静态信息」+「Terraform 运行时输出」
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
CMDB (cmdb.json) + Ansible inventory (inventory.ini / 动态 inventory)
|
||||||
|
```
|
||||||
|
|
||||||
|
**参考实现(基准,新 env 照此结构落地):**
|
||||||
|
`vultr-vps/envs/ai-workspace/`
|
||||||
|
|
||||||
|
**配套 binding skill(完整规则与自检清单):**
|
||||||
|
`../skills/terraform-yaml-render-pattern/SKILL.md`
|
||||||
|
|
||||||
|
## 1. HCL 控制结构禁令(MUST NOT)
|
||||||
|
|
||||||
|
- ❌ 禁止在 env 的 `.tf` 中使用 `for_each`、`count`、`dynamic` 块。
|
||||||
|
- ❌ 禁止用 `templatefile()` + `%{ for }`/`%{ if }` 等 HCL 模板控制结构做渲染。
|
||||||
|
- ✅ 允许纯函数:`concat`、`merge`、`coalesce`、`jsonencode` 等(非控制结构)。
|
||||||
|
- ✅ 资源的“多份”由 Python+Jinja2 在生成阶段展开为**多个显式块**,
|
||||||
|
每台主机 / 每个 key 对应一个命名唯一的 `module`/`resource`/`data` 块。
|
||||||
|
|
||||||
|
> 理由:把循环与条件收敛到 Python/Jinja2 单一处,HCL 保持“扁平、可审计、
|
||||||
|
> diff 友好”,资源命名稳定,避免 for_each key 漂移导致的重建。
|
||||||
|
|
||||||
|
## 2. 资源描述与变量传递(MUST)
|
||||||
|
|
||||||
|
- 资源信息**必须**由 env 目录下的 `hosts.yaml`(或等价 `*.yaml`)描述,作为唯一人工入口。
|
||||||
|
- YAML 的全局段经渲染写入 `terraform.auto.tfvars.json`,**传给 `variables.tf`**;
|
||||||
|
逐实例字段由 Jinja2 展开进生成的 `.tf`。
|
||||||
|
- 机密(API Key、私钥等)**禁止**写入 YAML / tfvars,**必须**走环境变量
|
||||||
|
(如 `TF_VAR_vultr_api_key`)。公钥可入 YAML。
|
||||||
|
|
||||||
|
## 3. 渲染器约定(MUST)
|
||||||
|
|
||||||
|
每个采用本范式的 env **必须**提供 `generate.py`,至少含两个子命令:
|
||||||
|
|
||||||
|
- `render`:`hosts.yaml` → `generated_hosts.tf` + `terraform.auto.tfvars.json`
|
||||||
|
- `inventory`:`terraform output`(运行时事实)+ `hosts.yaml`(静态字段)
|
||||||
|
→ `cmdb.json` + `inventory.ini`
|
||||||
|
|
||||||
|
约定:
|
||||||
|
- Jinja2 模板放 `templates/*.j2`;标识符经 `tf_id` 过滤器净化为合法 HCL 名。
|
||||||
|
- Terraform 仅输出 **运行时才确定** 的事实(如 `cmdb_runtime`:ip / instance_id /
|
||||||
|
解析后的 os_id)。静态字段(os_name/plan/groups/host_vars 等)由 Python 从 YAML 合并。
|
||||||
|
- `inventory.ini` 中含空格的 host_var 值**必须**加引号(`key="a b c"`)。
|
||||||
|
- 渲染产物(`generated_hosts.tf`、`terraform.auto.tfvars.json`、`cmdb.json`、
|
||||||
|
`inventory.ini`)为派生物,**必须**加入 `.gitignore`,不入库。
|
||||||
|
|
||||||
|
## 4. 模块复用与 OS 解析(SHOULD)
|
||||||
|
|
||||||
|
- 实例**应**复用 `modules/compute` 等既有模块,不在 env 内重写 provider 资源。
|
||||||
|
- 每个使用 provider 的子模块**必须**声明 `required_providers`(含正确 `source`),
|
||||||
|
否则 Terraform 误判为 `hashicorp/<name>`。
|
||||||
|
- OS **应**用 `data "vultr_os"` 按 `os_name` 解析 `os_id`,避免硬编码漂移 ID;
|
||||||
|
解析不到时允许在 YAML 直接给 `os_id`。
|
||||||
|
|
||||||
|
## 5. IaC ↔ Ansible 联动(MUST)
|
||||||
|
|
||||||
|
- CMDB(`cmdb.json`)是 IaC 与 Ansible 的契约。Ansible 侧的动态 inventory
|
||||||
|
(`playbooks/inventory/terraform_cmdb.py`)**只**消费 `cmdb.json`,不直接耦合 tfstate。
|
||||||
|
- IaC 变更后**必须**重跑 `generate.py inventory` 刷新 CMDB/inventory,保持二者一致。
|
||||||
|
|
||||||
|
## 6. 提交前自检(MUST)
|
||||||
|
|
||||||
|
- `terraform fmt` 无 diff;`terraform validate` 通过。
|
||||||
|
- `python3 generate.py render` 能产出合法 `.tf`(`validate` 通过)。
|
||||||
|
- 生成的 `inventory.ini` 能被 `ansible-inventory -i <file> --graph` 正确解析。
|
||||||
|
- 确认渲染产物已被 `.gitignore` 忽略、机密未入库。
|
||||||
13
terraform-hcl-standard/vultr-vps/envs/ai-workspace/.gitignore
vendored
Normal file
13
terraform-hcl-standard/vultr-vps/envs/ai-workspace/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# generate.py 渲染产物(由 hosts.yaml 派生,不入库)
|
||||||
|
generated_hosts.tf
|
||||||
|
terraform.auto.tfvars.json
|
||||||
|
cmdb.json
|
||||||
|
inventory.ini
|
||||||
|
|
||||||
|
# terraform 运行时
|
||||||
|
.terraform/
|
||||||
|
.terraform.lock.hcl
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.*
|
||||||
|
terraform.tfvars
|
||||||
|
__pycache__/
|
||||||
70
terraform-hcl-standard/vultr-vps/envs/ai-workspace/README.md
Normal file
70
terraform-hcl-standard/vultr-vps/envs/ai-workspace/README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# ai-workspace env —— YAML 描述资源 + Python/Jinja2 渲染 + Ansible 动态 inventory
|
||||||
|
|
||||||
|
在 `vultr-vps` 模块基础上,做到 **IaC 创建主机 ↔ Ansible inventory** 的联动,且满足约束:
|
||||||
|
|
||||||
|
- **不使用 HCL 控制结构**(无 `for_each`/`count`/`dynamic`/`templatefile` 循环)。
|
||||||
|
- **用 Python + Jinja2 渲染 YAML** 生成显式的 Terraform `module`/`resource`/`data` 块。
|
||||||
|
- `hosts.yaml` 描述资源信息,全局段经 `terraform.auto.tfvars.json` 传给 `variables.tf`。
|
||||||
|
|
||||||
|
默认创建两台机器(均 **4 核 8G / 公网 IP / 不开备份**):`ai-debian13`(Debian 13)、`ai-ubuntu2604`(Ubuntu 26.04)。
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
hosts.yaml ──generate.py render──▶ generated_hosts.tf (逐主机显式 module 块,无 for_each)
|
||||||
|
│ └ terraform.auto.tfvars.json ──▶ variables.tf
|
||||||
|
│
|
||||||
|
└──────────────▶ terraform apply ──▶ output cmdb_runtime (ip/instance_id/os_id)
|
||||||
|
│
|
||||||
|
hosts.yaml(静态字段) + cmdb_runtime ──generate.py inventory──▶ cmdb.json + inventory.ini
|
||||||
|
│
|
||||||
|
playbooks/inventory/terraform_cmdb.py (Ansible 动态 inventory 读 cmdb.json)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 循环逻辑全部在 Python/Jinja2 侧:每台主机 / 每个 SSH key 都被展开成
|
||||||
|
> `generated_hosts.tf` 里独立的显式块,HCL 内不出现任何控制结构。
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
| 文件 | 角色 |
|
||||||
|
|------|------|
|
||||||
|
| `hosts.yaml` | **唯一人工入口**:global / ssh_keys / hosts 资源描述 (CMDB 源) |
|
||||||
|
| `generate.py` | `render`:YAML→tf+tfvars;`inventory`:tf 输出+YAML→cmdb.json+inventory.ini |
|
||||||
|
| `templates/hosts.tf.j2` | 渲染逐主机 module/data 块与 `vultr_ssh_key` 资源 |
|
||||||
|
| `templates/inventory.ini.j2` | 渲染静态 inventory.ini |
|
||||||
|
| `variables.tf` | 全局变量声明(值来自 tfvars.json) |
|
||||||
|
| `provider.tf` / `cloud-init.yaml` | Provider 与云初始化 |
|
||||||
|
| `provision.sh` | 一键:render → apply → inventory →(可选)跑 Ansible |
|
||||||
|
| `../../../../../playbooks/inventory/terraform_cmdb.py` | Ansible 动态 inventory 脚本 |
|
||||||
|
|
||||||
|
> `generated_hosts.tf` / `terraform.auto.tfvars.json` / `cmdb.json` / `inventory.ini`
|
||||||
|
> 均为渲染产物,已在 `.gitignore` 中忽略。
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export TF_VAR_vultr_api_key=xxxxxxxx
|
||||||
|
# 编辑 hosts.yaml(填入真实 ssh 公钥 / 调整主机),然后:
|
||||||
|
./provision.sh # 渲染 + 创建 + 生成 inventory
|
||||||
|
|
||||||
|
# 用动态 inventory 驱动 Ansible
|
||||||
|
cd ../../../../../playbooks
|
||||||
|
ansible ai_workspace -i inventory/terraform_cmdb.py -m ping
|
||||||
|
ansible-playbook -i inventory/terraform_cmdb.py setup-ai-workspace-all-in-one.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
分步执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 generate.py render # YAML -> generated_hosts.tf + terraform.auto.tfvars.json
|
||||||
|
terraform init && terraform apply # 建机
|
||||||
|
python3 generate.py inventory # -> cmdb.json + inventory.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调整主机
|
||||||
|
|
||||||
|
改 `hosts.yaml` 即可增删机器或改套餐/区域/分组/host_vars,重跑 `generate.py render`。
|
||||||
|
`groups` 决定主机进入哪些 Ansible 组;`host_vars` 会进入 inventory 行与动态 inventory 的 `hostvars`。
|
||||||
|
|
||||||
|
> OS 通过 `os_name` 自动解析 `os_id`(避免硬编码漂移的镜像 ID)。名称查不到时用
|
||||||
|
> `vultr-cli os list` 核对,或在该主机直接写 `os_id`。
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
#cloud-config
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: true
|
||||||
|
runcmd:
|
||||||
|
- echo "Provisioned by Terraform ai-workspace env" > /etc/motd
|
||||||
|
- systemctl enable --now ssh || systemctl enable --now sshd || true
|
||||||
180
terraform-hcl-standard/vultr-vps/envs/ai-workspace/generate.py
Executable file
180
terraform-hcl-standard/vultr-vps/envs/ai-workspace/generate.py
Executable file
@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""hosts.yaml -> Terraform 资源 / Ansible inventory 渲染器。
|
||||||
|
|
||||||
|
设计要点(满足约束):
|
||||||
|
- 不在 HCL 里使用 for_each/count 等控制结构;用 Python + Jinja2 把 YAML
|
||||||
|
展开成 generated_hosts.tf 中逐个的显式 module/resource/data 块。
|
||||||
|
- hosts.yaml 的 global 段渲染成 terraform.auto.tfvars.json,传给 variables.tf。
|
||||||
|
- apply 后用 terraform 运行时输出 + YAML 静态字段合并出 cmdb.json,
|
||||||
|
再渲染 inventory.ini;二者供 Ansible(含动态 inventory 脚本)消费。
|
||||||
|
|
||||||
|
子命令:
|
||||||
|
render hosts.yaml -> generated_hosts.tf + terraform.auto.tfvars.json
|
||||||
|
inventory terraform output(cmdb_runtime) + hosts.yaml -> cmdb.json + inventory.ini
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
HOSTS_YAML = os.path.join(HERE, "hosts.yaml")
|
||||||
|
TEMPLATE_DIR = os.path.join(HERE, "templates")
|
||||||
|
GENERATED_TF = os.path.join(HERE, "generated_hosts.tf")
|
||||||
|
TFVARS_JSON = os.path.join(HERE, "terraform.auto.tfvars.json")
|
||||||
|
CMDB_JSON = os.path.join(HERE, "cmdb.json")
|
||||||
|
INVENTORY_INI = os.path.join(HERE, "inventory.ini")
|
||||||
|
|
||||||
|
|
||||||
|
def _tf_id(value):
|
||||||
|
"""把任意名字转成合法的 Terraform 标识符。"""
|
||||||
|
return re.sub(r"[^0-9a-zA-Z_]", "_", str(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _env():
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(TEMPLATE_DIR),
|
||||||
|
trim_blocks=True,
|
||||||
|
lstrip_blocks=False,
|
||||||
|
keep_trailing_newline=True,
|
||||||
|
)
|
||||||
|
env.filters["tf_id"] = _tf_id
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def load_yaml():
|
||||||
|
with open(HOSTS_YAML, encoding="utf-8") as fh:
|
||||||
|
return yaml.safe_load(fh) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_render(_args):
|
||||||
|
data = load_yaml()
|
||||||
|
glob = data.get("global", {}) or {}
|
||||||
|
ssh_keys = data.get("ssh_keys", []) or []
|
||||||
|
hosts = data.get("hosts", []) or []
|
||||||
|
|
||||||
|
env = _env()
|
||||||
|
rendered = env.get_template("hosts.tf.j2").render(
|
||||||
|
ssh_keys=ssh_keys,
|
||||||
|
hosts=hosts,
|
||||||
|
true=True,
|
||||||
|
false=False,
|
||||||
|
)
|
||||||
|
with open(GENERATED_TF, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(rendered)
|
||||||
|
|
||||||
|
tfvars = {
|
||||||
|
"region": glob.get("region", "nrt"),
|
||||||
|
"name_prefix": glob.get("name_prefix", "ai-workspace"),
|
||||||
|
"user_data_file": glob.get("user_data_file", "cloud-init.yaml"),
|
||||||
|
}
|
||||||
|
with open(TFVARS_JSON, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(tfvars, fh, indent=2, ensure_ascii=False)
|
||||||
|
fh.write("\n")
|
||||||
|
|
||||||
|
print(f" wrote {os.path.relpath(GENERATED_TF, HERE)}")
|
||||||
|
print(f" wrote {os.path.relpath(TFVARS_JSON, HERE)}")
|
||||||
|
print(" next: terraform init && terraform apply")
|
||||||
|
|
||||||
|
|
||||||
|
def _terraform_output(name):
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["terraform", f"-chdir={HERE}", "output", "-json", name],
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
return json.loads(out)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_inventory(_args):
|
||||||
|
data = load_yaml()
|
||||||
|
glob = data.get("global", {}) or {}
|
||||||
|
hosts = data.get("hosts", []) or []
|
||||||
|
default_region = glob.get("region", "nrt")
|
||||||
|
|
||||||
|
try:
|
||||||
|
runtime = _terraform_output("cmdb_runtime")
|
||||||
|
except (OSError, subprocess.CalledProcessError) as exc:
|
||||||
|
msg = getattr(exc, "stderr", b"") or b""
|
||||||
|
sys.exit(
|
||||||
|
"无法读取 terraform 输出 cmdb_runtime(请先在本目录 terraform apply)。\n"
|
||||||
|
+ msg.decode(errors="replace")
|
||||||
|
)
|
||||||
|
|
||||||
|
cmdb = {}
|
||||||
|
groups = {}
|
||||||
|
for host in hosts:
|
||||||
|
name = host["name"]
|
||||||
|
rt = runtime.get(name, {})
|
||||||
|
host_vars = dict(host.get("host_vars", {}) or {})
|
||||||
|
host_vars.setdefault("os_name", host.get("os_name", ""))
|
||||||
|
host_vars.setdefault("plan", host.get("plan", "vc2-4c-8gb"))
|
||||||
|
host_vars.setdefault("region", host.get("region") or default_region)
|
||||||
|
|
||||||
|
cmdb[name] = {
|
||||||
|
"name": name,
|
||||||
|
"ip": rt.get("ip"),
|
||||||
|
"instance_id": rt.get("instance_id"),
|
||||||
|
"os_id": rt.get("os_id"),
|
||||||
|
"os_name": host.get("os_name", ""),
|
||||||
|
"plan": host.get("plan", "vc2-4c-8gb"),
|
||||||
|
"region": host.get("region") or default_region,
|
||||||
|
"ansible_user": host.get("ansible_user", "root"),
|
||||||
|
"groups": host.get("groups", []) or [],
|
||||||
|
"tags": host.get("tags", []) or [],
|
||||||
|
"host_vars": host_vars,
|
||||||
|
}
|
||||||
|
for group in cmdb[name]["groups"] or ["ungrouped"]:
|
||||||
|
groups.setdefault(group, []).append(name)
|
||||||
|
|
||||||
|
with open(CMDB_JSON, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(cmdb, fh, indent=2, ensure_ascii=False)
|
||||||
|
fh.write("\n")
|
||||||
|
|
||||||
|
# 每台主机整行在 Python 侧拼好(含带引号的 host_vars),模板里只做表达式
|
||||||
|
# 输出,避免 Jinja2 trim_blocks 把行尾 block 标签后的换行吃掉。
|
||||||
|
lines = {}
|
||||||
|
for name, host in cmdb.items():
|
||||||
|
parts = [
|
||||||
|
name,
|
||||||
|
f"ansible_host={host['ip']}",
|
||||||
|
f"ansible_user={host['ansible_user']}",
|
||||||
|
]
|
||||||
|
for k, v in host["host_vars"].items():
|
||||||
|
parts.append(f'{k}="{v}"')
|
||||||
|
lines[name] = " ".join(parts)
|
||||||
|
|
||||||
|
env = _env()
|
||||||
|
rendered = env.get_template("inventory.ini.j2").render(
|
||||||
|
cmdb=cmdb,
|
||||||
|
lines=lines,
|
||||||
|
groups={g: sorted(m) for g, m in sorted(groups.items())},
|
||||||
|
)
|
||||||
|
with open(INVENTORY_INI, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(rendered)
|
||||||
|
|
||||||
|
print(f" wrote {os.path.relpath(CMDB_JSON, HERE)}")
|
||||||
|
print(f" wrote {os.path.relpath(INVENTORY_INI, HERE)}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
sub.add_parser("render", help="YAML -> generated_hosts.tf + tfvars").set_defaults(
|
||||||
|
func=cmd_render
|
||||||
|
)
|
||||||
|
sub.add_parser(
|
||||||
|
"inventory", help="terraform output + YAML -> cmdb.json + inventory.ini"
|
||||||
|
).set_defaults(func=cmd_inventory)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# ai-workspace 资源描述 (CMDB 源数据)
|
||||||
|
#
|
||||||
|
# 这是唯一的人工维护入口。generate.py 读取本文件:
|
||||||
|
# - global 段 -> terraform.auto.tfvars.json (传给 variables.tf)
|
||||||
|
# - ssh_keys -> generated_hosts.tf 里的显式 vultr_ssh_key 资源块
|
||||||
|
# - hosts -> generated_hosts.tf 里逐主机的显式 module/data 块 (无 for_each)
|
||||||
|
#
|
||||||
|
# 改完 YAML 后执行: python3 generate.py render
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
global:
|
||||||
|
region: nrt # 默认区域:东京。可选 ewr/sgp/fra ...
|
||||||
|
name_prefix: ai-workspace
|
||||||
|
user_data_file: cloud-init.yaml
|
||||||
|
|
||||||
|
# 注入实例的登录公钥(公钥非敏感,可入库;私钥/API Key 不要写这里)
|
||||||
|
ssh_keys:
|
||||||
|
- name: ai-workspace-admin
|
||||||
|
public: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO37O6S1Kf0QRV3/hQ7OVGZetEEcP7AevwXrRi8HRpHg ai-workspace-vultr"
|
||||||
|
|
||||||
|
# 主机清单。map key 风格用 name 字段;每台机器渲染成一个独立 module 块。
|
||||||
|
hosts:
|
||||||
|
- name: ai-debian13
|
||||||
|
os_name: "Debian 13 x64 (trixie)" # Vultr 实际镜像名(含 trixie);也可写 os_id: 2625
|
||||||
|
plan: vc2-4c-8gb # 4 核 8G
|
||||||
|
backups: false # 不开备份
|
||||||
|
enable_ipv6: true # 公网 IPv4 默认分配,这里附带开 IPv6
|
||||||
|
ansible_user: root
|
||||||
|
groups: [ai_workspace, debian]
|
||||||
|
tags: [debian13]
|
||||||
|
host_vars:
|
||||||
|
role: primary
|
||||||
|
# 逗号分隔的服务域名;cloudflare_dns 角色据此为本机 IP 建 A 记录
|
||||||
|
service_domains: ai-debian13.svc.plus
|
||||||
|
|
||||||
|
- name: ai-ubuntu2604
|
||||||
|
os_name: "Ubuntu 26.04 LTS x64"
|
||||||
|
plan: vc2-4c-8gb
|
||||||
|
backups: false
|
||||||
|
enable_ipv6: true
|
||||||
|
ansible_user: root
|
||||||
|
groups: [ai_workspace, ubuntu]
|
||||||
|
tags: [ubuntu2604]
|
||||||
|
host_vars:
|
||||||
|
role: secondary
|
||||||
|
service_domains: ai-ubuntu2604.svc.plus
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
vultr = {
|
||||||
|
source = "vultr/vultr"
|
||||||
|
version = "~> 2.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "vultr" {
|
||||||
|
api_key = var.vultr_api_key
|
||||||
|
rate_limit = 700
|
||||||
|
}
|
||||||
46
terraform-hcl-standard/vultr-vps/envs/ai-workspace/provision.sh
Executable file
46
terraform-hcl-standard/vultr-vps/envs/ai-workspace/provision.sh
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 一键联动:YAML -> 渲染 TF -> apply -> 生成 CMDB/inventory ->(可选)跑 Ansible
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# export TF_VAR_vultr_api_key=xxxx
|
||||||
|
# ./provision.sh # 渲染+创建+生成 inventory
|
||||||
|
# ./provision.sh ping # 之后对 ai_workspace 组跑 ping
|
||||||
|
# ./provision.sh playbook setup-ai-workspace-all-in-one.yml
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ENV_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${ENV_DIR}/../../../../.." && pwd)"
|
||||||
|
PLAYBOOKS_DIR="${REPO_ROOT}/playbooks"
|
||||||
|
DYN_INV="${PLAYBOOKS_DIR}/inventory/terraform_cmdb.py"
|
||||||
|
|
||||||
|
echo "==> [1/4] generate.py render (hosts.yaml -> generated_hosts.tf + tfvars)"
|
||||||
|
python3 "${ENV_DIR}/generate.py" render
|
||||||
|
|
||||||
|
echo "==> [2/4] terraform init & apply"
|
||||||
|
terraform -chdir="${ENV_DIR}" init -input=false >/dev/null
|
||||||
|
terraform -chdir="${ENV_DIR}" apply -auto-approve -input=false
|
||||||
|
|
||||||
|
echo "==> [3/4] generate.py inventory (terraform output + YAML -> cmdb.json + inventory.ini)"
|
||||||
|
python3 "${ENV_DIR}/generate.py" inventory
|
||||||
|
|
||||||
|
echo "==> [4/4] 完成。生成文件:"
|
||||||
|
echo " ${ENV_DIR}/cmdb.json"
|
||||||
|
echo " ${ENV_DIR}/inventory.ini"
|
||||||
|
|
||||||
|
action="${1:-none}"
|
||||||
|
case "${action}" in
|
||||||
|
none)
|
||||||
|
echo "可手动执行: ansible ai_workspace -i ${DYN_INV} -m ping"
|
||||||
|
;;
|
||||||
|
ping)
|
||||||
|
( cd "${PLAYBOOKS_DIR}" && ansible ai_workspace -i "${DYN_INV}" -m ping )
|
||||||
|
;;
|
||||||
|
playbook)
|
||||||
|
pb="${2:?用法: provision.sh playbook <playbook.yml>}"
|
||||||
|
( cd "${PLAYBOOKS_DIR}" && ansible-playbook -i "${DYN_INV}" "${pb}" )
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "未知动作: ${action} (支持: none|ping|playbook)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# 由 generate.py 从 hosts.yaml 渲染生成 —— 请勿手工编辑
|
||||||
|
# 重新生成: python3 generate.py render
|
||||||
|
#
|
||||||
|
# 约束: 不使用 for_each/count/dynamic 等 HCL 控制结构。
|
||||||
|
# 每台主机、每个 SSH key 都渲染成独立的显式 module/resource/data 块。
|
||||||
|
# (循环在 Python+Jinja2 侧完成,而非 HCL 侧)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
{% for key in ssh_keys -%}
|
||||||
|
resource "vultr_ssh_key" "{{ key.name | tf_id }}" {
|
||||||
|
name = "{{ key.name }}"
|
||||||
|
ssh_key = "{{ key.public }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endfor -%}
|
||||||
|
{% for host in hosts -%}
|
||||||
|
{%- set hid = host.name | tf_id -%}
|
||||||
|
{%- if host.os_id is not defined or host.os_id is none %}
|
||||||
|
data "vultr_os" "{{ hid }}" {
|
||||||
|
filter {
|
||||||
|
name = "name"
|
||||||
|
values = ["{{ host.os_name }}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
module "compute_{{ hid }}" {
|
||||||
|
source = "../../modules/compute"
|
||||||
|
|
||||||
|
label = "${var.name_prefix}-{{ host.name }}"
|
||||||
|
region = {% if host.region %}"{{ host.region }}"{% else %}var.region{% endif %}
|
||||||
|
|
||||||
|
plan = "{{ host.plan | default('vc2-4c-8gb') }}"
|
||||||
|
os_id = {% if host.os_id %}{{ host.os_id }}{% else %}data.vultr_os.{{ hid }}.id{% endif %}
|
||||||
|
|
||||||
|
enable_ipv6 = {{ 'true' if host.get('enable_ipv6', true) else 'false' }}
|
||||||
|
backups = {{ 'true' if host.get('backups', false) else 'false' }}
|
||||||
|
tags = concat([var.name_prefix], {{ host.get('tags', []) | tojson }})
|
||||||
|
ssh_key_ids = [{% for k in ssh_keys %}vultr_ssh_key.{{ k.name | tf_id }}.id{{ ", " if not loop.last }}{% endfor %}]
|
||||||
|
user_data = file(var.user_data_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endfor -%}
|
||||||
|
# 运行时事实:仅暴露 apply 后才确定的动态值。静态字段(os_name/plan/groups/
|
||||||
|
# host_vars 等)由 generate.py 从 hosts.yaml 合并,最终一起写入 cmdb.json。
|
||||||
|
output "cmdb_runtime" {
|
||||||
|
description = "name -> {ip, instance_id, os_id}"
|
||||||
|
value = {
|
||||||
|
{% for host in hosts -%}
|
||||||
|
{%- set hid = host.name | tf_id %}
|
||||||
|
"{{ host.name }}" = {
|
||||||
|
ip = module.compute_{{ hid }}.main_ip
|
||||||
|
instance_id = module.compute_{{ hid }}.instance_id
|
||||||
|
os_id = {% if host.os_id %}{{ host.os_id }}{% else %}data.vultr_os.{{ hid }}.id{% endif %}
|
||||||
|
|
||||||
|
}
|
||||||
|
{% endfor -%}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "hosts_summary" {
|
||||||
|
description = "name -> public_ip"
|
||||||
|
value = {
|
||||||
|
{% for host in hosts -%}
|
||||||
|
{%- set hid = host.name | tf_id %}
|
||||||
|
"{{ host.name }}" = module.compute_{{ hid }}.main_ip
|
||||||
|
{% endfor -%}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# 由 generate.py 从 cmdb.json 渲染生成 —— 请勿手工编辑
|
||||||
|
# 重新生成: python3 generate.py inventory
|
||||||
|
# =============================================================================
|
||||||
|
{% for group, members in groups.items() %}
|
||||||
|
[{{ group }}]
|
||||||
|
{% for name in members %}
|
||||||
|
{{ lines[name] }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
[all:vars]
|
||||||
|
ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
# 全局变量。值由 generate.py 从 hosts.yaml 的 global 段渲染进
|
||||||
|
# terraform.auto.tfvars.json 后传入(即 “YAML 描述资源信息 -> variables.tf”)。
|
||||||
|
# 逐主机的资源不走变量,而是由 generate.py 用 Jinja2 渲染成 generated_hosts.tf
|
||||||
|
# 里的显式 module/resource 块(不使用 for_each/count 等 HCL 控制结构)。
|
||||||
|
|
||||||
|
variable "vultr_api_key" {
|
||||||
|
description = "Vultr API Key,请通过环境变量 TF_VAR_vultr_api_key 提供(勿写入 YAML/tfvars)"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region" {
|
||||||
|
description = "默认部署区域,主机未单独指定 region 时使用"
|
||||||
|
type = string
|
||||||
|
default = "nrt"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name_prefix" {
|
||||||
|
description = "实例 label 前缀"
|
||||||
|
type = string
|
||||||
|
default = "ai-workspace"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "user_data_file" {
|
||||||
|
description = "cloud-init 脚本路径"
|
||||||
|
type = string
|
||||||
|
default = "cloud-init.yaml"
|
||||||
|
}
|
||||||
@ -60,7 +60,7 @@ resource "vultr_instance" "this" {
|
|||||||
plan = var.plan
|
plan = var.plan
|
||||||
os_id = var.os_id
|
os_id = var.os_id
|
||||||
enable_ipv6 = var.enable_ipv6
|
enable_ipv6 = var.enable_ipv6
|
||||||
backups = var.backups
|
backups = var.backups ? "enabled" : "disabled"
|
||||||
tags = var.tags
|
tags = var.tags
|
||||||
vpc_ids = var.vpc_id == null ? [] : [var.vpc_id]
|
vpc_ids = var.vpc_id == null ? [] : [var.vpc_id]
|
||||||
ssh_key_ids = var.ssh_key_ids
|
ssh_key_ids = var.ssh_key_ids
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
vultr = {
|
||||||
|
source = "vultr/vultr"
|
||||||
|
version = "~> 2.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
terraform-hcl-standard/vultr-vps/modules/iam/versions.tf
Normal file
8
terraform-hcl-standard/vultr-vps/modules/iam/versions.tf
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
vultr = {
|
||||||
|
source = "vultr/vultr"
|
||||||
|
version = "~> 2.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user