# Terraform / Terragrunt — IaC Patterns > Canonical IaC boilerplate for André Bassi's projects (reference: crava/infra). OpenTofu + Terragrunt Stacks, versioned module repo, S3+DynamoDB state, YAML manifest wrapper as the standard configuration interface, Taskfile as the only entrypoint. Point an LLM here and it builds/extends infra exactly in this pattern. ## Hard Rules 1. Terragrunt drives everything. Raw `terraform apply` never runs by hand — only via Taskfile tasks. 2. Modules live in a SEPARATE versioned repo, referenced by git tag: `source = "${modules_base}/?ref="`. Never path-reference local modules from live infra. 3. One state per component per env: S3 key `{env}/{component}/terraform.tfstate`, DynamoDB lock table, `encrypt = true`. 4. backend.tf and provider.tf are GENERATED by terragrunt (`generate` blocks) — never hand-written in units. 5. `default_tags` on every provider: `Project`, `Environment`, `ManagedBy = "Terragrunt"`. 6. Environments are isolated dirs: `env/{dev,qas,prd}/`, each with `env.hcl` (environment, region, account_id, module_ref). 7. Production tasks have `prompt:` confirmation. Destroy tasks ALWAYS have prompt. 8. YAML wrapper is THE pattern: every component is configured through a CRD-style YAML manifest (`manifests/{env}/.yaml`, with apiVersion/kind/metadata/spec), decoded with `yamldecode(file(...))`. HCL units stay generic; humans and LLMs edit YAML, never unit inputs. New env = new set of YAMLs. 9. Lint/validate before apply: `terragrunt fmt`, tflint (.tflint.hcl), checkov for security. 10. Engine: OpenTofu (`TG_TF_PATH: tofu`) + `TF_PLUGIN_CACHE_DIR` for provider cache. ## Repo Layout (live infra) infra/ ├── root.hcl # root config: locals (project, modules_base), remote_state ├── _units/ # parametrized unit templates (Terragrunt Stacks) │ ├── vpc/terragrunt.hcl │ ├── ecr/terragrunt.hcl # param: service_name │ ├── rds/terragrunt.hcl # param: service_name, db_name, is_production │ ├── alb/terragrunt.hcl │ ├── ecs-service/terragrunt.hcl # deps resolved dynamically │ ├── s3-frontend/ cloudfront/ ses/ sqs/ oidc/ ... ├── env/ │ ├── terragrunt.stack.hcl # orchestrator (all envs) │ ├── dev/ │ │ ├── env.hcl # environment=dev, aws_region, account_id, module_ref │ │ ├── terragrunt.stack.hcl # maps units -> instances │ │ ├── vpc/terragrunt.hcl # unit instantiation │ │ └── /terragrunt.hcl (one dir per component) │ ├── qas/ (same shape) │ └── prd/ (same shape) ├── manifests/ │ ├── _base/ # template YAMLs for new envs │ └── {dev,qas,prd}/.yaml ├── scripts/ # operational scripts (health, diagnose, deploy-frontend, rds-start...) ├── Taskfile.yaml └── .tflint.hcl ## Unit terragrunt.hcl Skeleton include "root" { path = find_in_parent_folders("root.hcl") expose = true } locals { env = read_terragrunt_config(find_in_parent_folders("env.hcl")) ref = local.env.locals.module_ref } generate "backend" { path = "backend.tf" if_exists = "overwrite_terragrunt" contents = <.com.br/v1 kind: RDSPostgres metadata: name: rds-api spec: db_name: "app_api" instance_class: "db.t4g.micro" skip_final_snapshot: true # dev only deletion_protection: false # dev only — prd: true # unit reads: locals { manifest = yamldecode(file("${get_terragrunt_dir()}/../../..../manifests/${env}/${component}.yaml")) } inputs = merge(local.manifest.spec, { /* computed deps */ }) - `_base/` holds template manifests; new env = copy `_base/` + adjust. - prd manifests flip safety: `deletion_protection: true`, `skip_final_snapshot: false`, bigger instance classes. ## Module Repo Conventions (separate repo, e.g. tinnova/terraform-modules) - One folder per module: `vpc/`, `ecr/`, `rds/postgres/`, `alb/`, `ecs/cluster/`, `ecs/service/`, `s3-website/`, `cloudfront/`, `ses/`, `sqs/`, `iam/`, `bitbucket-oidc/`... - Files per module: `main.tf`, `variables.tf`, `outputs.tf`, `versions.tf` (provider version pinning). - Every variable typed + described; outputs for everything a dependent unit needs. - Terraform native tests per module; Taskfile: `task test`, `task test:vpc`, `task lint`. - Release = git tag (semver `0.0.x`). Live infra bumps `module_ref` in `env.hcl` per env — dev first, then qas, then prd. ## Naming & Tagging - Resources: `--[-suffix]` (e.g. crava-api-dev-alb, crava-rds-api-qas). - Region code suffix when multi-region: `use1`. - Tags via provider default_tags only — never per-resource tag blocks for the standard trio. ## Taskfile Vocabulary (IaC repo) plan / apply # single component: task apply -- env/dev/vpc ({{.CLI_ARGS}}) dev:plan dev:apply dev:destroy # run --all per env (dir: env/dev) qas:* prd:* # prd:apply and *:destroy carry prompt: validate fmt lint security # tflint + checkov clean # caches (.terragrunt-cache, plugin cache) health:domains diagnose costs # operational scripts vpn:up vpn:down vpn:status # access path to private resources # run --all pattern: dev:apply: dir: env/dev cmds: - terragrunt run --all --no-stack-generate --queue-exclude-dir '.terragrunt-stack' --non-interactive apply -- -auto-approve prd:apply: dir: env/prd prompt: "PRODUCAO: confirma apply em prd?" cmds: [ ... ] ## Workflow 1. New component: create module in modules repo (+tests) -> tag release. 2. Create `_units//terragrunt.hcl` template. 3. Create `manifests/_base/.yaml` + per-env copies. 4. Instantiate `env/dev//terragrunt.hcl` -> `task plan -- env/dev/` -> apply. 5. Promote: qas -> prd (bump module_ref per env; prd behind prompt). 6. Never edit state by hand; import/move via terragrunt commands logged with tee. ## Credentials & Access - AWS auth via `AWS_PROFILE` (SSO) or `pass show //aws-*` exports. Never plaintext. - Private resources (RDS in private subnets) reached via Cloudflare WARP/tunnel or VPN — tasks `vpn:up -- `.