docs(P56b): create phase plan — author 11 L1 module terraform subdirs

PLAN stage. P56b authors the remaining 11 L1 module terraform subdirs
(vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds,
kms-key, uptime) with the full versions/variables/locals/main/outputs
split. Defaults move from the adapter into locals.tf.

7 single-resource modules (simpler): kms-key, ecr, ecs-cluster, iam-role,
rds, waf, uptime.
4 multi-resource modules (full split with intra-refs): vpc, ecs-service,
alb, cloudfront.

Success gate: all 12 terraform/ subdirs validate standalone, all 12
registry entries have terraform_dir, the 6 P56b-skipped tests unblock.

---ci---
project: acdl
phase: P56b
milestone: v1.11
status: plan
---/ci---
This commit is contained in:
Jon Chery
2026-07-28 15:15:38 +00:00
parent 027a845b4d
commit 8218734957
+69 -91
View File
@@ -1,122 +1,100 @@
---
phase: P56a
name: stateless-adapter-rewrite
phase: P56b
name: l1-module-terraform-authoring
milestone: v1.11
requirements: [REQ-123]
requirements: [REQ-124]
wave: 1
depends_on: [P56a]
---
# P56aStateless Adapter Rewrite + s3 Reference Module
# P56bAuthor 11 L1 Module Terraform Subdirs
**Phase:** P56a
**Phase:** P56b
**Milestone:** v1.11 (RESTART)
**Requirement:** REQ-123
**Wave:** 1 (foundation — sequential prerequisite for P56bP58)
**Branch:** `milestone/v1.11-restart``phase/p56a-stateless-adapter`
**Requirement:** REQ-124
**Wave:** 1 (foundation — sequential prerequisite for P57P58)
**Branch:** `milestone/v1.11-restart``phase/p56b-l1-terraform`
## Goal
Rewrite `adapters/terraform/adapter.py` from a 918-line monolith (3 constant
tables + 39 type-specific branches) to a ~80-line stateless assembler. Author
`modules/l1/s3/terraform/` as the reference module proving the assembly path
end-to-end. Extend the registry. Rewrite STANDARDS.md §8. Rewrite
`tests/test_adapter.py` to assert assembly, not HCL string matching.
Author the remaining 11 L1 module terraform subdirs (vpc, ecs-cluster,
ecs-service, iam-role, alb, ecr, cloudfront, waf, rds, kms-key, uptime)
with the full versions/variables/locals/main/outputs split. Defaults
currently hardcoded in the adapter move into locals.tf. Each module's
interface.json stays engine-agnostic. Add terraform_dir to each registry
entry.
This phase proves the stateless adapter design with ONE module (s3) before
P56b authors the remaining 11. If the assembly path works for s3, it works
for all L1s (the adapter is generic; only the module terraform differs).
## Module Classification
## Vertical Slice — Single Wave (no parallelization; one cohesive change)
**Single-resource modules** (simpler; may inline locals in main.tf):
- `kms-key` — aws_kms_key + alias
- `ecr` — aws_ecr_repository
- `ecs-cluster` — aws_ecs_cluster
- `iam-role` — aws_iam_role (+ inline policy defaults)
- `rds` — aws_db_instance
- `waf` — aws_wafv2_web_acl
- `uptime` — aws_ecs_service (uptime-kuma)
### Task 1 — s3 reference terraform module (data-engineer)
**Multi-resource modules** (full 5-file split with locals.tf):
- `vpc` — aws_vpc + aws_subnet + aws_route_table + igw (intra-refs)
- `ecs-service` — aws_ecs_task_definition + aws_ecs_service (intra-refs)
- `alb` — aws_lb + aws_lb_target_group + aws_lb_listener (intra-refs)
- `cloudfront` — aws_cloudfront_distribution + aws_cloudfront_origin_access_control (intra-refs)
Author `modules/l1/s3/terraform/`:
- `versions.tf``terraform { required_version = ">= 1.9, < 1.10"; required_providers { aws = { source = "hashicorp/aws", version = "~> 5.0" } } }`
- `variables.tf` — one `variable {}` per s3 interface.json input: `bucket_name` (string, required), `region` (string, required), `kms_key_arn` (string, optional, default null). Plus `tags` (map(string), optional, default {}) for tag merge.
- `locals.tf` — heavy interpolation:
- `sse_algorithm` — "aws:kms" if `var.kms_key_arn != null`, else "AES256" (SSE-S3). This is the default the adapter currently hardcodes (adapter.py:225-228, 289-297).
- `tags` — merge `var.tags` with `acdl:owner`/`acdl:environment` defaults.
- `main.tf``resource "aws_s3_bucket" "this" { bucket = var.bucket_name; tags = local.tags }` + `resource "aws_s3_bucket_versioning" "this" { bucket = aws_s3_bucket.this.id; versioning { enabled = true } }` + `resource "aws_s3_bucket_server_side_encryption_configuration" "this" { bucket = aws_s3_bucket.this.id; rule { apply_server_side_encryption_by_default { sse_algorithm = local.sse_algorithm; kms_master_key_id = var.kms_key_arn } } }` (conditional on kms_key_arn).
- `outputs.tf``output "bucket_arn" { value = aws_s3_bucket.this.arn }`, `output "bucket_name" { value = aws_s3_bucket.this.id }`, `output "bucket_regional_domain_name" { value = aws_s3_bucket.this.bucket_regional_domain_name }`.
## Tasks (single wave — all modules are independent once the adapter is proven)
**Must-have:** `terraform init + validate` passes standalone in `modules/l1/s3/terraform/`.
### Task 1 — Simple single-resource modules (data-engineer)
### Task 2 — Registry extension (backend-engineer)
Author terraform/ for: kms-key, ecr, ecs-cluster, rds, waf, uptime.
Each gets versions.tf + variables.tf + main.tf (with inlined locals or a
locals.tf) + outputs.tf. Defaults from the adapter move here.
Update `modules/registry.json`: add `"terraform_dir": "modules/l1/s3/terraform"` to the s3 1.0.0 entry. This is the field the adapter reads to find the module source path.
Key defaults to move:
- `kms-key`: enable_key_rotation=true, deletion_window_in_days=30 default
- `ecr`: encryption_configuration from kms_key_arn, image_scanning=true
- `ecs-cluster`: name default
- `rds`: storage_encrypted=true default, multi_az default, kms_key_arn
- `waf`: scope=cloudfront, default_action=allow, visibility_config, rules rendering
- `uptime`: Fargate compat, container_definitions from inputs
**Must-have:** `registry.json` validates as JSON; the s3 entry has `terraform_dir`.
### Task 2 — iam-role module (data-engineer)
### Task 3 — Stateless adapter rewrite (backend-engineer)
Author terraform/ for iam-role. The adapter previously hardcoded:
- assume_role_policy fallback (ECS task trust) — move to locals.tf
- inline ECR/logs policy when no managed_policies — move to locals.tf
- role_name default
Rewrite `adapters/terraform/adapter.py` (918 → ~80 lines):
- Delete `TYPE_MAP`, `INPUT_MAP`, `OUTPUT_MAP` (lines 26-94).
- Delete all 39 type-specific branches in `_emit_resource` + `_emit_igw`, `_container_definitions`, `_resource_block`, `_emit_output`.
- New `adapt(stack_instance, out_dir)`:
1. `os.makedirs(out_dir, exist_ok=True)`.
2. Read `registry.json` → build a `module_name → terraform_dir` map.
3. For each resource in `stack_instance["resources"]`:
- Look up the module name from the resource's `module` field (e.g. `s3@1.0.0``s3`).
- Get `terraform_dir` from the registry.
- Emit a `module "<rid>" { source = "<terraform_dir>" ... }` block, passing each input as a module argument. For `ref:` values, emit `module "<ref_rid>".<output>` interpolations. Skip `region` (provider-level, not a module arg).
4. Emit root `output {}` blocks wiring module outputs to stack outputs.
5. Emit `providers.tf` (aws provider, region from first resource) + `terraform.tf` (required_version + required_providers + S3 backend, state key `spike/{stack_name}/terraform.tfstate` — env-aware key fix is P58, not this phase).
- The adapter owns NO resource shape, NO nested blocks, NO defaults, NO type-specific logic. It only assembles module instantiations and wires refs.
### Task 3 — Multi-resource modules with intra-refs (data-engineer)
**Must-have:**
- `grep -n "TYPE_MAP\|INPUT_MAP\|OUTPUT_MAP\|rtype ==" adapters/terraform/adapter.py` returns nothing.
- `wc -l adapters/terraform/adapter.py` < 100.
- `python3 adapters/terraform/adapter.py modules/l1/s3/instance.json /tmp/p56a-test/` emits a root `main.tf` containing `module "s3" { source = "modules/l1/s3/terraform" ... }`.
- `terraform init + validate` passes in the emitted output dir (with the s3 module present).
Author terraform/ for: vpc, ecs-service, alb, cloudfront.
Each has multiple resources with intra-refs (one resource references
another's output within the same module). The locals.tf centralizes:
- vpc: cidr_block default, subnet cidr derivation, route table defaults, IGW
- ecs-service: container_definitions jsonencode, Fargate compat, execution_role, network_config
- alb: listener default_action, target group vpc_id, port defaults
- cloudfront: origin block, default_cache_behavior, OAC defaults
### Task 4 — STANDARDS.md §8 rewrite (backend-engineer)
### Task 4 — Registry extension (backend-engineer)
Rewrite `modules/STANDARDS.md` §8 "Adapter Extension Pattern" (lines 448-504):
- Delete the "three tables" + "specialized emit branch" documentation (it described the drift).
- New §8: "Stateless Assembler Pattern". The adapter is a ~80-line assembler reading `terraform_dir` from the registry. Adding a new L1 = author the `terraform/` subdir (versions/variables/locals/main/outputs.tf) + register in `registry.json`. No adapter code changes. Document the file-split standard (versions/variables/locals/main/outputs) + the heavy-locals.tf convention for default interpolation.
Add terraform_dir to all 11 remaining registry entries.
**Must-have:** STANDARDS.md §8 documents the stateless assembler + per-module terraform dir standard.
### Task 5 — Validate all modules standalone (data-engineer)
### Task 5 — test_adapter.py rewrite (backend-engineer)
Rewrite `tests/test_adapter.py` (667 lines → focused assembly assertions):
- Delete the HCL string-matching tests (TestTypeMap, TestInputMap, etc. — they test the deleted constants).
- New tests assert the adapter ASSEMBLES module instantiations:
- `test_adapt_emits_module_block` — given the s3 instance, the root main.tf contains `module "s3" { source = "modules/l1/s3/terraform" ... }`.
- `test_adapt_passes_inputs` — the module block passes `bucket_name` and `region` as arguments.
- `test_adapt_wires_refs` — given a 2-resource stack with a `ref:` input, the root main.tf wires `module "<rid>".<output>` as the argument value.
- `test_adapt_emits_outputs` — root outputs wire module outputs to stack outputs.
- `test_adapt_emits_providers_and_terraform_tf` — providers.tf + terraform.tf present with correct region + backend.
- `test_no_type_specific_logic``grep` assertion: adapter.py contains no `TYPE_MAP`/`INPUT_MAP`/`OUTPUT_MAP`/`rtype ==`.
- `test_adapter_under_100_lines``wc -l` assertion.
- Keep the TestRegistry class (registry still has 14 entries; now with `terraform_dir`).
**Must-have:** `pytest tests/test_adapter.py` passes (offline).
Run `terraform init + validate` in each of the 12 terraform/ subdirs.
Fix any validation errors.
### Task 6 — Offline regression (backend-engineer)
Run the full offline test suite to confirm no regression from the adapter rewrite:
- `pytest tests/ -m "not slow"` — all offline tests pass (the adapter change is the largest; other tests that import the adapter must still work).
- `bash scripts/run_primitive_plan.sh --check-only s3` — the plan-only pipeline still works against the new module-assembled output.
**Must-have:** offline pytest suite green + `run_primitive_plan.sh --check-only s3` exits 0.
- `pytest tests/ -m "not slow"` — all offline tests pass; the 6 P56b-skipped
tests should now unblock (static-assets contract resolves cloudfront/waf/s3).
- `bash scripts/run_primitive_plan.sh --check-only <module>` for each of the
12 modules — each exits 0.
## Success Criteria (phase gate)
1. `grep -n "TYPE_MAP\|INPUT_MAP\|OUTPUT_MAP\|rtype ==" adapters/terraform/adapter.py` returns nothing.
2. `wc -l adapters/terraform/adapter.py` < 100.
3. `modules/l1/s3/terraform/` passes `terraform init + validate` standalone.
4. Adapter, given the s3 instance, emits a root `main.tf` that `terraform init + validate` accepts.
5. `pytest tests/test_adapter.py` passes (offline).
6. Full offline pytest suite green (no regression).
7. `run_primitive_plan.sh --check-only s3` exits 0.
8. STANDARDS.md §8 documents the stateless assembler + per-module terraform dir standard.
9. `registry.json` s3 entry has `terraform_dir`.
## Out of Scope (deferred to P56bP58)
- Authoring the remaining 11 L1 module terraform subdirs (P56b).
- The `--apply`/`--destroy` shell lifecycle modes (P57).
- The single platform VPC + env-aware state key fix (P58).
- The modules-lifecycle pipeline (P59).
- Live AWS apply/modify/destroy (P60).
1. All 12 L1 modules have a terraform/ subdir with the required files.
2. All 12 terraform/ subdirs pass `terraform init + validate` standalone.
3. All 12 registry entries have a terraform_dir field.
4. No defaults remain in the adapter (grep for hardcoded CIDR, assume_role_policy, etc.).
5. `pytest tests/ -m "not slow"` — 455+ passed, 0 skipped (the 6 P56b-skipped tests unblock).
6. `run_primitive_plan.sh --check-only <module>` exits 0 for all 12 modules.