ADR-0002 Phase D, erp-repo layer. iac/main.tf iterates `envs = ["prod",
"sandbox"]` so the app_roles module + the admin-bootstrap resources are
materialised per environment:
- module.app_roles["sandbox"] → auth/kubernetes/role/erp-sandbox +
postgres/creds/erp-sandbox (dynamic role GRANTs erp_sandbox_role — the
snake-case owner role created in factory#17 — and REVOKEs on DATABASE
erp-sandbox; token_policies ["default","erp-sandbox"] = the policy from
tools#3).
- random_password.admin_initial_password["sandbox"], random_uuid.dolibarr_id
["sandbox"], and vault_kv_secret_v2.dolibarr_admin_setup["sandbox"]
(kvv2/erp-sandbox/config) → the sandbox Dolibarr's own admin password +
encryption id.
State migration via `moved` blocks: the pre-existing single-env resources are
re-keyed into the for_each map under "prod" so introducing for_each does NOT
destroy+recreate them. Critical for random_uuid.dolibarr_id (prevent_destroy =
true — prod encryption id + paid-module binding): a wrong/absent moved would
HARD-FAIL the apply rather than lose it. The module's internal moved
(role -> role[0]) chains with the module re-key. Verified the exact compound
(old-module state → new module count+moved + for_each, one apply) with two
standalone tofu plans: both show "X moved", sandbox created, 0 destroyed.
env=prod renders byte-identical to the single-env baseline (module elision
rule), so the prod erp Vault auth role, dynamic creds, admin secret + KV are
unchanged.
D3 of Phase D. D1 = factory#17 (DB+role, merged). D2 = tools#3 (Vault
policies, merged). D4 (ArgoCD Application) is next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>