iac_modules is the Infrastructure-as-Code repository of the AI Workspace platform. It declares
cloud resources as YAML, renders explicit Terraform HCL from that YAML with Python + Jinja2, applies
it per environment, and emits a CMDB (cmdb.json) that the Ansible layer consumes. Terraform here
owns compute / network / storage / identity only — software configuration belongs to the playbooks
repo on the other side of the CMDB contract.
config/resources/<env>/<group>.yaml declaration — the only hand-edited entry point
│ scripts/generate.py render loops run in Python + Jinja2, never inside HCL
▼
generated_hosts.tf one explicit module/resource block per host
terraform.auto.tfvars.json ──▶ variables.tf the YAML `global` section
│ terraform apply
▼
output "cmdb_runtime" runtime-only facts (ip / instance_id / resolved os_id)
│ scripts/generate.py inventory merges static YAML fields + runtime outputs
▼
cmdb.json + inventory.ini the IaC ↔ Ansible contract
Binding rules that follow from it — full text in
terraform-hcl-standard/AGENTS.md and
skills/:
- No control structures in HCL. No
for_each/count/dynamic, notemplatefile()with%{ for }/%{ if }. Multiplicity is expanded at render time into uniquely named blocks. - No topology in HCL. Regions, plans, instance counts, domains and host vars live in YAML.
- No embedded scripts in HCL. No
local-execprovisioners, nonull_resourceshell/python. - One state namespace per environment.
sit/uat/prodnever share a backend key. - Secrets never travel through files. API keys arrive as
TF_VAR_*env vars; generated credentials are pushed to Vault for Ansible to read back — never written to YAML, tfvars or handed forward on disk. - Rendered artifacts are not committed.
generated_hosts.tf,terraform.auto.tfvars.json,cmdb.json,inventory.iniare gitignored; a run directory tracks onlyREADME.md+.gitignore.
Every provider under terraform-hcl-standard/ follows the same three-layer split — declaration,
reusable templates, composition logic — leaving run directories as pure Terraform workdirs.
| Layer | Path | Role | Tracked |
|---|---|---|---|
| Declaration | <provider>/config/resources/<env>/*.yaml |
Resource topology per environment and per resource group | ✅ |
| Declaration | <provider>/config/accounts/ |
Placeholder for account / bootstrap facts (state bucket, lock table, roles) — the actual YAML is sourced from the external GitOps repo via BOOTSTRAP_CONFIG_PATH / TF_VAR_config_root |
.gitkeep only |
| Templates | <provider>/templates/ |
Shared provider.tf, variables.tf, backend.tf, cloud-init.yaml and their .j2 renderers |
✅ |
| Composition | <provider>/scripts/generate.py, provision.sh |
render + inventory subcommands, one-shot provisioning |
✅ |
| Modules | <provider>/modules/<resource>/ |
Reusable resource modules consumed by the rendered blocks | ✅ |
| Root modules | <provider>/component/ (AWS), <provider>/instance/ (GCP) |
Per-resource Terraform roots with a Makefile each |
✅ |
| Run directories | <provider>/envs/<name>/ |
Terraform workdir — receives the rendered artifacts and holds the state | README + .gitignore only |
| Bootstrap | <provider>/bootstrap/{state,lock,identity} |
Remote state bucket, lock table, deploy role/user — applied before anything else | ✅ |
| Provider | Path | Status | Modules | What exists today |
|---|---|---|---|---|
| AWS | terraform-hcl-standard/aws-cloud/ |
Production | 14 | Terragrunt bootstrap (state / lock / identity), component/ roots, sit + uat + prod resource declarations, render + inventory scripts |
| Vultr | terraform-hcl-standard/vultr-vps/ |
Production — reference implementation of the render pattern | 7 | Four run directories (ai-workspace, dev, platform-ops-toolkit, site-migration-toolkit), sit + uat + prod declarations, provision.sh |
| Alibaba Cloud | terraform-hcl-standard/ali-cloud/ |
Partial | 9 | Modules + bootstrap + envs/dev; config/resources is still empty |
| GCP | terraform-hcl-standard/gcp-cloud/ |
Skeleton | 14 | One main.tf per module plus instance/ roots; no resource declarations |
| Azure | terraform-hcl-standard/azure-cloud/ |
Skeleton | 14 | One main.tf per module; no run directories, no resource declarations |
utils/ at the terraform-hcl-standard/ root holds the shared Python renderer
(renderer.py, config_loader.py, render_provider_backend.py).
| Domain | AWS | Alibaba Cloud | Vultr | GCP / Azure (skeleton) |
|---|---|---|---|---|
| Identity & governance | iam, landingzone |
ram |
iam |
iam, landingzone |
| Compute | ec2, keypair, ami_lookup |
ecs |
compute, resize-instance |
ec2, keypair, ami_lookup |
| Network | vpc, sg, alb, nlb |
vpc, alb, nlb |
vpc |
vpc, sg, alb, nlb |
| Data & storage | rds, redis, msk, s3 |
rds, redis, oss |
data_store, storage |
rds, redis, msk, s3 |
| Teardown | bootstrap-destroy |
bootstrap-destroy |
bootstrap-destroy |
bootstrap-destroy |
The GCP and Azure trees declare genuine
google_*/azurerm_*resources but inherited the AWS directory names (ec2,s3,msk,ami_lookup). Naming is not normalized yet.
Three environments, each with its own YAML directory, its own run directory and its own state:
sit (integration), uat (pre-production), prod.
Resource groups are the unit of declaration inside an environment — one YAML per group, one Terraform run per group:
| Group | Ansible group / tags | Carries |
|---|---|---|
web-saas |
web_saas, database |
Console / accounts / PostgreSQL SaaS hosts, including the public service domains Caddy terminates TLS for |
ai-workspace |
ai_workspace, database |
The AI workspace application nodes |
infra-platform |
infra_platform, database |
Shared platform services |
agent-proxy |
agent_proxy |
Agent egress / proxy nodes |
action-runner |
action_runner |
Self-hosted CI runners |
all-in-one (sit only) |
mixed | The whole stack collapsed onto a single SIT host |
uat and prod are deliberately separate declarations and separate states — the CMDB produced by a
run is the deploy source of truth for that environment, and a run must never be pointed at another
environment's host variables.
| Path | Contents |
|---|---|
vpn-overlay/ |
Cross-site L2/L3 overlay: wireguard/, xray/, vxlan/, gretap/, config/sites.yaml, topology diagrams |
skills/ |
Binding specs — IAC-Spec (IaC red lines), terraform-yaml-render-pattern (the render pattern), release-branch-policy |
scripts/ |
Repo-level helpers — dynamic_inventory.py, WireGuard key generation, gitleaks auto-fix, and workflows/ helpers for Flux, Ansible, kubeconfig and xconfig |
.github/actions/ |
Pipeline definitions — per-cloud landing-zone baselines, infrastructure resources, monitoring exporter/server, SSL certificate renewal |
.github/workflows/ |
validate-release-pr.yml — the only workflow that runs in this repository |
example/ |
Reference samples: Pulumi (Python) and plain Terraform for AWS / Azure / GCP |
docs/ |
Bilingual documentation set — start at docs/README.md |
These modules are built to be driven by pipelines and by
generate.py; runningterraformby hand inside a provider directory will not work, because run directories only exist after a render.
- Terraform >= 1.5.0
- Terragrunt >= 0.67.14 (AWS bootstrap only)
- Python 3 with
PyYAMLandJinja2(requirements.txtinstalls these plus the Pulumi example deps) - Provider credentials — AWS via the standard credential chain / OIDC, Vultr via
TF_VAR_vultr_api_key
Account facts (bucket, lock table, role names) come from the external GitOps repo, not from this one:
git clone https://github.com/cloud-neutral-workshop/gitops.git ../gitops
cd terraform-hcl-standard/aws-cloud/bootstrap
BOOTSTRAP_CONFIG_PATH=../../../../gitops/config/accounts/bootstrap.yaml make bootstrap-init bootstrap-applyCreates the S3 state bucket (versioned + encrypted), the DynamoDB lock table and the deploy
role/user. See aws-cloud/README.md for credential
modes and teardown.
export TF_VAR_vultr_api_key=...
cd terraform-hcl-standard/vultr-vps/scripts
RESOURCES=../config/resources/uat/web-saas.yaml \
WORKDIR=../envs/ai-workspace \
./provision.shprovision.sh runs the four steps in order — generate.py render → terraform init && apply →
generate.py inventory → optional Ansible. To drive the steps individually:
python3 generate.py render --resources ../config/resources/uat/web-saas.yaml --workdir ../envs/ai-workspace
terraform -chdir=../envs/ai-workspace init && terraform -chdir=../envs/ai-workspace apply
python3 generate.py inventory --resources ../config/resources/uat/web-saas.yaml --workdir ../envs/ai-workspaceThe playbooks repo consumes cmdb.json through its dynamic inventory
(playbooks/inventory/terraform_cmdb.py) — it never reads tfstate. Re-run generate.py inventory
after every infrastructure change.
ansible web_saas -i ../../../../playbooks/inventory/terraform_cmdb.py -m pingterraform-hcl-standard/AGENTS.md— binding Terraform constraintsskills/IAC-Spec/SKILL.md— IaC red lines and the IaC ↔ config-as-code boundaryskills/terraform-yaml-render-pattern/SKILL.md— the render pattern with its pre-commit checklistskills/release-branch-policy/SKILL.md— branch and release policy
Pre-commit self-check: terraform fmt clean, terraform validate passing, generate.py render
producing valid HCL, ansible-inventory --graph parsing the generated inventory, no rendered
artifacts or secrets staged.
- GCP and Azure are single-file skeletons with AWS-inherited module names — not deployable as-is.
ali-cloud/config/resources/is empty; onlyenvs/devexists.- The default
--resourcespaths ingenerate.pystill point at the pre-splitconfig/resources/ai-workspace-hosts.yaml; always pass--resources/RESOURCESexplicitly. - Documentation consolidation is tracked in
docs/DOC_COVERAGE.md.