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:
+69
-91
@@ -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]
|
||||
---
|
||||
|
||||
# P56a — Stateless Adapter Rewrite + s3 Reference Module
|
||||
# P56b — Author 11 L1 Module Terraform Subdirs
|
||||
|
||||
**Phase:** P56a
|
||||
**Phase:** P56b
|
||||
**Milestone:** v1.11 (RESTART)
|
||||
**Requirement:** REQ-123
|
||||
**Wave:** 1 (foundation — sequential prerequisite for P56b–P58)
|
||||
**Branch:** `milestone/v1.11-restart` → `phase/p56a-stateless-adapter`
|
||||
**Requirement:** REQ-124
|
||||
**Wave:** 1 (foundation — sequential prerequisite for P57–P58)
|
||||
**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 P56b–P58)
|
||||
|
||||
- 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.
|
||||
Reference in New Issue
Block a user