diff --git a/skills/terraform-yaml-render-pattern/SKILL.md b/skills/terraform-yaml-render-pattern/SKILL.md new file mode 100644 index 00000000..11797748 --- /dev/null +++ b/skills/terraform-yaml-render-pattern/SKILL.md @@ -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// + 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 --graph` 正确解析。 +- 渲染产物已被 `.gitignore` 忽略;机密未入库。 +- HCL 内无 `for_each`/`count`/`dynamic`/`templatefile` 控制结构。 diff --git a/terraform-hcl-standard/AGENTS.md b/terraform-hcl-standard/AGENTS.md new file mode 100644 index 00000000..bf1ec486 --- /dev/null +++ b/terraform-hcl-standard/AGENTS.md @@ -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/`。 +- 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 --graph` 正确解析。 +- 确认渲染产物已被 `.gitignore` 忽略、机密未入库。 diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/.gitignore b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/.gitignore new file mode 100644 index 00000000..b82392d3 --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/.gitignore @@ -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__/ diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/README.md b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/README.md new file mode 100644 index 00000000..cab7a69d --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/README.md @@ -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`。 diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/cloud-init.yaml b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/cloud-init.yaml new file mode 100644 index 00000000..1bdc6138 --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/cloud-init.yaml @@ -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 diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/generate.py b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/generate.py new file mode 100755 index 00000000..13d47ae9 --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/generate.py @@ -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() diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/hosts.yaml b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/hosts.yaml new file mode 100644 index 00000000..dd96287b --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/hosts.yaml @@ -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 diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/provider.tf b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/provider.tf new file mode 100644 index 00000000..f12f5aa4 --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/provider.tf @@ -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 +} diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/provision.sh b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/provision.sh new file mode 100755 index 00000000..c94ad223 --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/provision.sh @@ -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 }" + ( cd "${PLAYBOOKS_DIR}" && ansible-playbook -i "${DYN_INV}" "${pb}" ) + ;; + *) + echo "未知动作: ${action} (支持: none|ping|playbook)" >&2 + exit 1 + ;; +esac diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/templates/hosts.tf.j2 b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/templates/hosts.tf.j2 new file mode 100644 index 00000000..df98e18c --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/templates/hosts.tf.j2 @@ -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 -%} + } +} diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/templates/inventory.ini.j2 b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/templates/inventory.ini.j2 new file mode 100644 index 00000000..d6f1f65a --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/templates/inventory.ini.j2 @@ -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' diff --git a/terraform-hcl-standard/vultr-vps/envs/ai-workspace/variables.tf b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/variables.tf new file mode 100644 index 00000000..3d7a30e4 --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/envs/ai-workspace/variables.tf @@ -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" +} diff --git a/terraform-hcl-standard/vultr-vps/modules/compute/main.tf b/terraform-hcl-standard/vultr-vps/modules/compute/main.tf index 0d453dac..548661f6 100644 --- a/terraform-hcl-standard/vultr-vps/modules/compute/main.tf +++ b/terraform-hcl-standard/vultr-vps/modules/compute/main.tf @@ -60,7 +60,7 @@ resource "vultr_instance" "this" { plan = var.plan os_id = var.os_id enable_ipv6 = var.enable_ipv6 - backups = var.backups + backups = var.backups ? "enabled" : "disabled" tags = var.tags vpc_ids = var.vpc_id == null ? [] : [var.vpc_id] ssh_key_ids = var.ssh_key_ids diff --git a/terraform-hcl-standard/vultr-vps/modules/compute/versions.tf b/terraform-hcl-standard/vultr-vps/modules/compute/versions.tf new file mode 100644 index 00000000..763f0cb4 --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/modules/compute/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + vultr = { + source = "vultr/vultr" + version = "~> 2.19" + } + } +} diff --git a/terraform-hcl-standard/vultr-vps/modules/iam/versions.tf b/terraform-hcl-standard/vultr-vps/modules/iam/versions.tf new file mode 100644 index 00000000..763f0cb4 --- /dev/null +++ b/terraform-hcl-standard/vultr-vps/modules/iam/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + vultr = { + source = "vultr/vultr" + version = "~> 2.19" + } + } +}