From 8a99be1194ed0fba588ebd5ec49ea82180009fc9 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 18 Apr 2026 21:10:05 +0000 Subject: [PATCH] [infra] Document HCL import {} block convention [ci skip] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Wave 8 of the state-drift consolidation plan — adopt the HCL `import {}` block pattern (Terraform 1.5+) as the canonical way to bring live cluster / Vault / Cloudflare resources under TF management. Historically the repo has used `terraform import` on the CLI for adoptions. That path has three real problems: 1. **Not reviewable** — it's an out-of-band state mutation that leaves no trace in git beyond the subsequent `resource {}` block. A reviewer sees only the new resource, not the adoption intent. 2. **Not plan-safe** — if the resource address or ID is wrong, the CLI path commits the mistake to state before anyone can catch it. 3. **Not idempotent** — a failed apply mid-import leaves state in a confusing half-adopted shape. `import {}` blocks fix all three: the adoption intent is in the PR diff, `scripts/tg plan` shows the import as its own plan line (mistyped IDs fail before apply), and re-applying after a partial failure just retries the import step. Canonicalizing the pattern before Wave 5 (Calico + kured adoption) lands so the reviewer of those imports has the rule in front of them. ## This change - `AGENTS.md`: new "Adopting Existing Resources — Use `import {}` Blocks, Not the CLI" section sitting right after Execution. Includes the canonical 5-step workflow (write resource → add import stanza → plan to zero → apply → drop stanza), the reasoning, and a per-provider ID format table (helm_release, kubernetes_manifest, kubernetes__v1, authentik_provider_proxy, cloudflare_record). - `.claude/CLAUDE.md`: one-line cross-reference at the end of the Terraform State two-tier section pointing back to AGENTS.md. Keeps CLAUDE.md's quick-reference density intact while making sure the rule is reachable from the Claude-instructions path. ## What is NOT in this change - Any actual imports — this is a pure docs landing. Wave 5 will demonstrate the pattern on kured + Calico. - Replacing the handful of existing `terraform import`-style adoptions in the repo history — `import {}` blocks are delete-after-apply, so retro-documenting them is not useful. Closes: code-[wave8-task] Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/CLAUDE.md | 1 + AGENTS.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 37f81406..88e4f11c 100755 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -48,6 +48,7 @@ Violations cause state drift, which causes future applies to break or silently r - **Tier 0 details**: Decrypt priority: Vault Transit (primary) → age key fallback. Encrypt: both Vault Transit + age recipients. Scripts: `scripts/state-sync {encrypt|decrypt|commit} [stack]`. - **Adding operator**: Generate age key (`age-keygen`), add pubkey to `.sops.yaml`, run `sops updatekeys` on Tier 0 `.enc` files. For Tier 1, only Vault access is needed. - **Migration script**: `scripts/migrate-state-to-pg` (one-shot, idempotent) migrates Tier 1 stacks from local to PG. +- **Adopting existing resources**: use HCL `import {}` blocks (TF 1.5+), not `terraform import` CLI. Commit stanza → plan-to-zero → apply → delete stanza. Canonical reason: reviewable in PR, plan-safe, idempotent, tier-agnostic. Full rules + per-provider ID formats in `AGENTS.md` → "Adopting Existing Resources". ## Secrets Management — Vault KV - **Vault is the sole source of truth** for secrets. diff --git a/AGENTS.md b/AGENTS.md index a726c628..cab67b24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,49 @@ - **Health check**: `bash scripts/cluster_healthcheck.sh --quiet` - **Plan all**: `cd stacks && terragrunt run --all --non-interactive -- plan` +## Adopting Existing Resources — Use `import {}` Blocks, Not the CLI + +When bringing a live cluster/Vault/Cloudflare resource under Terraform management, use an HCL `import {}` block (Terraform 1.5+). Do **NOT** use `terraform import` on the CLI for anything landing in this repo — the CLI path leaves no audit trail and makes multi-operator adoption fragile. + +**Canonical workflow:** + +1. Write the `resource` block that matches the live object. +2. In the same stack, add an `import {}` stanza naming the target and the provider-specific ID: + ```hcl + import { + to = helm_release.kured + id = "kured/kured" # Helm ID format: / + } + + resource "helm_release" "kured" { + name = "kured" + namespace = "kured" + repository = "https://kubereboot.github.io/charts/" + chart = "kured" + version = "5.7.0" + # ... values matching the live release + } + ``` +3. `scripts/tg plan` — every change it proposes is real divergence between HCL and live state. Iterate on values until the plan is **0 changes**. +4. `scripts/tg apply` — the import runs alongside whatever zero-change apply you have. If your plan is 0 changes, this commits only the state-ownership transfer. +5. After the apply lands cleanly, **delete the `import {}` block** in a follow-up commit. The resource is now fully TF-owned and the stanza would be a no-op that clutters diffs. + +**Why `import {}` and not `terraform import`:** + +- Reviewable in PRs before any state mutation. The CLI path is an out-of-band action nobody sees. +- Plan-safe: the `import` plan step shows the exact object being adopted. Mistyped IDs or the wrong resource address are caught before apply, not after. +- Survives state backend changes (Tier 0 SOPS vs Tier 1 PG) transparently — both work identically from the operator's perspective because both use `scripts/tg`. +- Re-runnable: if the apply fails partway through, the `import {}` block is idempotent. The CLI path's state mutation is not. + +**Finding the provider-specific ID:** each provider has its own convention. +| Resource | ID format | Example | +|---|---|---| +| `helm_release` | `/` | `kured/kured` | +| `kubernetes_manifest` | `{"apiVersion":"...","kind":"...","metadata":{"namespace":"...","name":"..."}}` | (pass as HCL object literal) | +| `kubernetes__v1` | `/` for namespaced, `` for cluster-scoped | `kube-system/coredns` | +| `authentik_provider_proxy` | provider UUID | `0eecac07-97c7-443c-...` | +| `cloudflare_record` | `/` | `abc123/def456` | + ## Secrets Management (SOPS) - **`config.tfvars`** — plaintext config (hostnames, IPs, DNS records, public keys) - **`secrets.sops.json`** — SOPS-encrypted secrets (passwords, tokens, SSH keys, API keys)