From 350867137703281aaeedd8091b60fe53bb2d90db Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Wed, 22 Jul 2026 13:54:40 +0000 Subject: [PATCH] refactor(modules): remove thin-composition layer; rewrite all module READMEs The L2 thin-composition layer (composition.json + contract_resolver.py + contract schema + sample contracts) has been removed completely. The implementation was unsatisfactory and is deferred for a later redesign. - Delete: composition.json x2, contract_resolver.py, contracts/ x2, contract.schema.json - Patch: run_platform.sh now loads a pre-existing IR instance instead of resolving a contract (the downstream adapter/checkov/confidence/outbox pipeline is unchanged) - Prune: L2 entries removed from registry.json (L1 entries unchanged) - Rewrite: all 7 L1 module READMEs in plain language (no jargon), each with Resources/Inputs/Outputs/Usage/Compliance-extension-points/Versioning sections derived from interface.json - Add: 2 L2 placeholder READMEs noting the composition is under redesign - Add: modules-ir/README.md catalog index + README-TEMPLATE.md ---ci--- project: acdl phase: 17 milestone: v1.3 status: execute ---/ci--- --- acdl_platform/contract_resolver.py | 256 ------------------ contracts/microservice.yaml | 13 - contracts/spike.yaml | 5 - modules-ir/README-TEMPLATE.md | 48 ++++ modules-ir/README.md | 51 ++++ modules-ir/l1/l1-alb/README.md | 99 ++++--- modules-ir/l1/l1-ecr/README.md | 66 +++-- modules-ir/l1/l1-ecs-cluster/README.md | 64 +++-- modules-ir/l1/l1-ecs-service/README.md | 105 ++++--- modules-ir/l1/l1-iam-role/README.md | 76 ++++-- modules-ir/l1/l1-s3/README.md | 75 +++-- modules-ir/l1/l1-vpc/README.md | 91 ++++--- modules-ir/l2/l2-microservice/README.md | 110 +++----- .../l2/l2-microservice/composition.json | 55 ---- modules-ir/l2/l2-static-asset/README.md | 66 +++-- .../l2/l2-static-asset/composition.json | 17 -- modules-ir/registry.json | 14 - schemas/contract.schema.json | 92 ------- scripts/run_platform.sh | 29 +- 19 files changed, 565 insertions(+), 767 deletions(-) delete mode 100644 acdl_platform/contract_resolver.py delete mode 100644 contracts/microservice.yaml delete mode 100644 contracts/spike.yaml create mode 100644 modules-ir/README-TEMPLATE.md create mode 100644 modules-ir/README.md delete mode 100644 modules-ir/l2/l2-microservice/composition.json delete mode 100644 modules-ir/l2/l2-static-asset/composition.json delete mode 100644 schemas/contract.schema.json diff --git a/acdl_platform/contract_resolver.py b/acdl_platform/contract_resolver.py deleted file mode 100644 index f59e9ae..0000000 --- a/acdl_platform/contract_resolver.py +++ /dev/null @@ -1,256 +0,0 @@ -"""ACDL Contract Resolver — resolve a contract to a Target Stack IR instance. - -ARCHITECTURE.md §12.8: the contract declares intent in IR-typed terms; -the resolver resolves the contract to a target stack (list of L1 -instances + inputs + relationships); the adapter compiles the target -stack to a plan. - -Steps: - 1. Load the contract (YAML -> dict). - 2. Validate the contract against schemas/contract.schema.json. - 3. Look up the L2 in modules-ir/registry.json. - 4. Load the L2's composition.json (the thin-composition tree). - 5. Map the contract's inputs through the composition's wires to the - child L1s' inputs. Two wire kinds: - - passthrough: {target, input} (or an array of the same) -> the - concrete contract value. - - child->child: {target, input, source:"child:."} -> - a "ref:." string (value known at apply - time only). - A wire value may be a single object or an array of objects (for - contract inputs that fan out to multiple children); both forms are - iterated. - 6. Emit an IR instance {version, stack:{name, kind:l2, depth}, - resources:[], relationships:[...]}. - Multi-resource L1s (interface.json has a `resources` array) expand - into one IR resource per entry, id `-` where - type_suffix is the last IR-type segment with underscores stripped; - single-resource L1s keep the child id verbatim. - 7. Validate the IR instance against schemas/ir.schema.json. - -CLI: contract_resolver.py -""" - -import json -import os -import sys - -import yaml -import jsonschema - - -REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -def _load_json(path): - with open(path, "r") as fh: - return json.load(fh) - - -def _iter_wire_targets(wire_value): - """Yield each target-spec from a wire value (single object or array).""" - if isinstance(wire_value, list): - for spec in wire_value: - yield spec - elif isinstance(wire_value, dict): - yield wire_value - - -def _type_suffix(ir_type): - """Last segment of an IR type, underscores stripped (e.g. aws:ec2:vpc -> vpc, - aws:elbv2:targetgroup -> targetgroup, aws:ecs:task_definition -> taskdefinition).""" - return ir_type.rsplit(":", 1)[-1].replace("_", "") - - -def _resolve_child_ref(source, child_id, l1_iface, child_ir_ids): - """Resolve a "child:." source to "ref:.". - - The ir_resource_id is the producing child's sub-resource that - declares the output. For single-resource L1s that is the child id; - for multi-resource L1s the L1's `resources` array is scanned for - which sub-resource declares the output (exact match, then a - singular->plural fallback so e.g. `subnet_ids` matches a per-resource - `subnet_id`). The ref's output name is the per-resource output name - when matched that way, else the source output name verbatim. - """ - prefix = "child:" - if not source.startswith(prefix): - raise ValueError(f"unsupported wire source {source!r}") - body = source[len(prefix):] - src_child_id, src_output = body.split(".", 1) - if src_child_id != child_id: - # Cross-child reference: look up the producing child's first IR - # resource id (the child->child wiring table is keyed by child id - # by the caller; this branch is unused for v1.2's wires but kept - # for completeness). - ir_resource_id = child_ir_ids.get(src_child_id, src_child_id) - return f"ref:{ir_resource_id}.{src_output}" - # Same-child reference: find the producing sub-resource. - resources = l1_iface.get("resources") - if not resources: - return f"ref:{child_id}.{src_output}" - for idx, sub in enumerate(resources): - sub_outputs = sub.get("outputs", []) - if src_output in sub_outputs: - ir_id = child_ir_ids[child_id][idx] - return f"ref:{ir_id}.{src_output}" - # Singular->plural fallback (subnet_ids -> subnet_id). - singular = src_output[:-1] if src_output.endswith("s") else src_output - for idx, sub in enumerate(resources): - sub_outputs = sub.get("outputs", []) - if singular in sub_outputs: - ir_id = child_ir_ids[child_id][idx] - return f"ref:{ir_id}.{singular}" - # No per-resource match: point at the first sub-resource, keep the - # source output name verbatim. - ir_id = child_ir_ids[child_id][0] - return f"ref:{ir_id}.{src_output}" - - -def resolve(contract_path, repo_root=None): - """Resolve a contract YAML to an IR instance dict.""" - rr = repo_root or REPO_ROOT - - # 1. Load the contract YAML. - with open(contract_path, "r") as fh: - contract = yaml.safe_load(fh) - - # 2. Validate the contract against the contract schema. - contract_schema = _load_json(os.path.join(rr, "schemas/contract.schema.json")) - jsonschema.validate(contract, contract_schema) - - # 3. Look up the L2 in the registry. - stack_name = contract["stack"] - registry = _load_json(os.path.join(rr, "modules-ir/registry.json")) - if stack_name not in registry: - raise ValueError(f"stack {stack_name!r} not in registry") - versions = registry[stack_name] - # Pick the highest 1.x.x (spike: just take the first non-deprecated). - entry = next(v for v in versions.values() if not v.get("deprecated", False)) - - # 4. Load the L2's composition.json. - composition_key = entry.get("composition") or entry.get("interface") - composition = _load_json(os.path.join(rr, composition_key)) - - # 5. Map the contract's inputs through the wires to the child L1s' inputs. - wires = composition.get("wires", {}) - contract_inputs = contract.get("inputs", {}) - children = composition.get("children", []) - - # Pre-load every child's L1 interface + compute IR resource ids. - child_ifaces = {} - child_ir_ids = {} - for child in children: - child_id = child["id"] - child_module = child["module"] - l1_name, l1_version = child_module.split("@", 1) - l1_entry = registry.get(l1_name, {}).get(l1_version) - if not l1_entry: - raise ValueError(f"L1 {child_module!r} not in registry") - l1_iface = _load_json(os.path.join(rr, l1_entry["interface"])) - child_ifaces[child_id] = l1_iface - sub_resources = l1_iface.get("resources") - if sub_resources: - child_ir_ids[child_id] = [ - f"{child_id}-{_type_suffix(sub['type'])}" for sub in sub_resources - ] - else: - child_ir_ids[child_id] = [child_id] - - # Build each child's mapped inputs (concrete values + ref strings). - child_inputs_map = {child["id"]: {} for child in children} - for wire_name, wire_value in wires.items(): - for spec in _iter_wire_targets(wire_value): - target = spec.get("target") - if target not in child_inputs_map: - continue - input_name = spec["input"] - source = spec.get("source") - if source: - # Child->child reference: emit a ref string. - src_child_id = source[len("child:"):].split(".", 1)[0] - child_inputs_map[target][input_name] = _resolve_child_ref( - source, src_child_id, child_ifaces[src_child_id], child_ir_ids - ) - else: - # Contract->child passthrough. - if wire_name in contract_inputs: - child_inputs_map[target][input_name] = contract_inputs[wire_name] - - # 6. Emit the IR instance. - resources = [] - relationships = [] - for child in children: - child_id = child["id"] - child_module = child["module"] - l1_iface = child_ifaces[child_id] - l1_outputs = l1_iface.get("outputs", {}) - child_inputs = child_inputs_map[child_id] - sub_resources = l1_iface.get("resources") - ir_ids = child_ir_ids[child_id] - if sub_resources: - for idx, sub in enumerate(sub_resources): - ir_id = ir_ids[idx] - sub_in_names = sub.get("inputs", []) - sub_out_names = sub.get("outputs", []) - sub_inputs = { - n: child_inputs[n] for n in sub_in_names if n in child_inputs - } - sub_outputs = { - n: l1_outputs[n] for n in sub_out_names if n in l1_outputs - } - resources.append({ - "id": ir_id, - "type": sub["type"], - "module": child_module, - "inputs": sub_inputs, - "outputs": sub_outputs, - }) - relationships.append({"from": "root", "to": ir_id, "kind": "parent"}) - # Resolve intra-L1 refs (refs between sub-resources of the same L1). - intra_refs = l1_iface.get("intra_refs", []) - for iref in intra_refs: - from_type, from_input = iref["from"].split(".", 1) - to_type, to_output = iref["to"].split(".", 1) - from_ir_id = next((ir_ids[i] for i, s in enumerate(sub_resources) if s["type"] == from_type), None) - to_ir_id = next((ir_ids[i] for i, s in enumerate(sub_resources) if s["type"] == to_type), None) - if from_ir_id and to_ir_id: - for r in resources: - if r["id"] == from_ir_id: - r["inputs"][from_input] = f"ref:{to_ir_id}.{to_output}" - else: - resources.append({ - "id": child_id, - "type": l1_iface["type"], - "module": child_module, - "inputs": child_inputs, - "outputs": l1_outputs, - }) - relationships.append({"from": "root", "to": child_id, "kind": "parent"}) - - ir_instance = { - "version": "1.0.0", - "stack": { - "name": composition["name"], - "kind": composition["kind"], - "depth": composition["depth"], - }, - "resources": resources, - "relationships": relationships, - } - - # 7. Validate the IR instance against the IR schema. - ir_schema = _load_json(os.path.join(rr, "schemas/ir.schema.json")) - jsonschema.validate(ir_instance, ir_schema) - - return ir_instance - - -if __name__ == "__main__": - if len(sys.argv) != 3: - print("usage: contract_resolver.py ", file=sys.stderr) - sys.exit(2) - ir = resolve(sys.argv[1]) - with open(sys.argv[2], "w") as fh: - json.dump(ir, fh, indent=2) - print(f"resolver: emitted IR to {sys.argv[2]}", file=sys.stderr) \ No newline at end of file diff --git a/contracts/microservice.yaml b/contracts/microservice.yaml deleted file mode 100644 index 160cee9..0000000 --- a/contracts/microservice.yaml +++ /dev/null @@ -1,13 +0,0 @@ -stack: l2-microservice -environment: dev -inputs: - name: acdl-microservice - cidr: "10.0.0.0/16" - azs: "us-east-1a,us-east-1b" - image: "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest" - port: 8080 - cpu: 256 - memory: 512 - role_name: acdl-microservice-exec - assume_role_policy: '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}' - managed_policies: "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" \ No newline at end of file diff --git a/contracts/spike.yaml b/contracts/spike.yaml deleted file mode 100644 index ade2281..0000000 --- a/contracts/spike.yaml +++ /dev/null @@ -1,5 +0,0 @@ -stack: l2-static-asset -environment: dev -inputs: - bucket_name: acdl-spike-bucket - region: us-east-1 \ No newline at end of file diff --git a/modules-ir/README-TEMPLATE.md b/modules-ir/README-TEMPLATE.md new file mode 100644 index 0000000..fe37c86 --- /dev/null +++ b/modules-ir/README-TEMPLATE.md @@ -0,0 +1,48 @@ +# <module-name> — <plain-language description> + +> **Module kind:** L1 primitive | **Version:** 1.0.0 + +One or two sentences describing what this module provisions, in plain +language. No jargon. A reader should know after this paragraph whether +this module is what they need. + +## Resources + +Terraform resources this module creates: + +| Resource | Type | Purpose | +|----------|------|---------| +| `<name>` | `aws_<type>` | what it does | + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `<name>` | string | yes | — | description | + +## Outputs + +| Name | Type | Description | +|------|------|-------------| +| `<name>` | string | description | + +## Usage + +``` +# A concrete snippet showing how to reference this module or what a +# consumer writes to use it. +``` + +## Compliance extension points + +Resources this module could be extended with for the future compliance +milestone (GDPR, SOX, SOC2, HIPAA, DORA). Not implemented yet — listed +so the redesign can plan for them. + +- **<area>** — <what could be added, e.g. KMS key for encryption> + +## Versioning + +`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR +bumps require a new registry entry (immutable publication); old entries +enter a 12-month deprecation window. \ No newline at end of file diff --git a/modules-ir/README.md b/modules-ir/README.md new file mode 100644 index 0000000..9b3e5ad --- /dev/null +++ b/modules-ir/README.md @@ -0,0 +1,51 @@ +# ACDL Modules + +Reusable building blocks for cloud infrastructure. Each module is +self-documented with a `README.md` following the +[template](README-TEMPLATE.md). + +## How the modules work + +There are two kinds of module: + +- **L1 primitives** — a single cloud resource or a small group of + related resources (e.g. a VPC with subnets and routing). Each L1 has + an `interface.json` declaring its inputs and outputs, and a `README.md` + in plain language. +- **L2 compositions** — a composition that references multiple L1s to + deploy a complete stack (e.g. an ECS Fargate microservice). **The L2 + composition layer is being redesigned.** The previous implementation + has been removed; a new mechanism will be designed in a later phase. + +The Terraform adapter (`adapters/terraform/adapter.py`) compiles a +module instance to Terraform. Each module's README documents which +Terraform resources it creates. + +## L1 primitives + +| Module | What it creates | README | +|--------|----------------|--------| +| `l1-s3` | `aws_s3_bucket` — a single S3 bucket | [README](l1/l1-s3/README.md) | +| `l1-vpc` | `aws_vpc` + `aws_subnet` + `aws_route_table` + `aws_internet_gateway` — VPC with subnets and routing | [README](l1/l1-vpc/README.md) | +| `l1-ecs-cluster` | `aws_ecs_cluster` — ECS Fargate cluster | [README](l1/l1-ecs-cluster/README.md) | +| `l1-ecs-service` | `aws_ecs_task_definition` + `aws_ecs_service` — Fargate service with task definition | [README](l1/l1-ecs-service/README.md) | +| `l1-iam-role` | `aws_iam_role` — IAM role with assume-role policy | [README](l1/l1-iam-role/README.md) | +| `l1-alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [README](l1/l1-alb/README.md) | +| `l1-ecr` | `aws_ecr_repository` — ECR container image repository | [README](l1/l1-ecr/README.md) | + +## L2 compositions + +| Module | What it references | README | +|--------|--------------------|--------| +| `l2-microservice` | 6 L1s (vpc, cluster, ecr, iam-role, alb, ecs-service) — **under redesign** | [README](l2/l2-microservice/README.md) | +| `l2-static-asset` | 1 L1 (s3) — **under redesign** | [README](l2/l2-static-asset/README.md) | + +## Registry + +Module versions are tracked in `registry.json`. Only L1 entries are +active; L2 entries have been pruned pending the composition redesign. + +## Template + +New modules should use [README-TEMPLATE.md](README-TEMPLATE.md) as +their starting point. \ No newline at end of file diff --git a/modules-ir/l1/l1-alb/README.md b/modules-ir/l1/l1-alb/README.md index 3b154ba..ae821f8 100644 --- a/modules-ir/l1/l1-alb/README.md +++ b/modules-ir/l1/l1-alb/README.md @@ -1,55 +1,70 @@ -# l1-alb — Application Load Balancer primitive (multi-resource L1) +# l1-alb — Application Load Balancer (load balancer + target group + listener) -An L1 module for an Application Load Balancer (load balancer + target -group + listener). Substrate-agnostic (the IR types are -`aws:elbv2:loadbalancer`, `aws:elbv2:listener`, `aws:elbv2:targetgroup`, -not Terraform resource types). This is a multi-resource L1: the -interface declares the group's inputs/outputs plus a `resources` array -listing the IR types it emits. The IR instance (Phase 14/15) will have -multiple `resources` entries all with `module: "l1-alb@1.0.0"`. +> **Module kind:** L1 primitive | **Version:** 1.0.0 -## Interface (the IR-typed contract) +An Application Load Balancer with a target group and a listener. This is +a multi-resource module: it creates a load balancer, a target group, and +a listener that forwards traffic to the target group. The target group +is what `l1-ecs-service` registers its tasks with. -See `interface.json`: inputs `name` (string), `subnets` (string, -comma-separated, ref to l1-vpc), `security_group` (string), `port` -(number, default 80), `protocol` (string, default "HTTP"), `region` -(string); outputs `lb_arn` (arn) + `listener_arn` (arn) + -`target_group_arn` (arn); no NFRs. +## Resources -The `resources` array lists the emitted IR types: +| Resource | Type | Purpose | +|----------|------|---------| +| load_balancer | `aws_lb` | Application load balancer in the VPC subnets | +| target_group | `aws_lb_target_group` | Target group for the ECS service tasks | +| listener | `aws_lb_listener` | Listener forwarding the LB port to the target group | -- `aws:elbv2:loadbalancer` — application load balancer in the VPC - subnets. -- `aws:elbv2:targetgroup` — target group for the ECS service tasks. -- `aws:elbv2:listener` — listener forwarding the LB port to the target - group. +## Inputs -## IR → Terraform mapping (performed by the adapter) +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `name` | string | yes | — | Name tag for the load balancer and child resources | +| `subnets` | string | yes | — | Comma-separated subnet ids (from `l1-vpc`) | +| `security_group` | string | yes | — | Security group id for the load balancer | +| `port` | number | no | 80 | Listener port | +| `protocol` | string | no | `HTTP` | Listener protocol | +| `region` | string | yes | — | AWS region the load balancer is created in | -The Terraform adapter (`adapters/terraform/adapter.py`) translates each -emitted IR resource to Terraform: +## Outputs -| IR | Terraform | -|----|-----------| -| `resource.type = aws:elbv2:loadbalancer` | `resource "aws_lb" "" { ... }` | -| `resource.inputs.name` | `name = ` arg | -| `resource.inputs.subnets` | `subnets = []` arg (comma-split) | -| `resource.inputs.security_group` | `security_groups = []` arg (comma-split) | -| `resource.outputs.lb_arn` | `output "lb_arn" { value = aws_lb..id }` | -| `resource.type = aws:elbv2:targetgroup` | `resource "aws_lb_target_group" "" { ... }` | -| `resource.inputs.port` | `port = ` arg | -| `resource.inputs.protocol` | `protocol = ` arg | -| `resource.outputs.target_group_arn` | `output "target_group_arn" { value = aws_lb_target_group..arn }` | -| `resource.type = aws:elbv2:listener` | `resource "aws_lb_listener" "" { ... }` | -| `resource.inputs.lb_arn` | `load_balancer_arn = ` arg (identity) | -| `resource.inputs.port` | `port = ` arg | -| `resource.inputs.protocol` | `protocol = ` arg | -| `resource.outputs.listener_arn` | `output "listener_arn" { value = aws_lb_listener..id }` | +| Name | Type | Description | +|------|------|-------------| +| `lb_arn` | arn | The load balancer ARN | +| `listener_arn` | arn | The listener ARN | +| `target_group_arn` | arn | The target group ARN | -The adapter is a thin layer (ARCHITECTURE.md §12.2); it does not own L1 -content — it only translates. +## Usage -## Versioning (W3.D) +```json +{ + "id": "alb", + "type": "aws:elbv2:loadbalancer", + "module": "l1-alb@1.0.0", + "inputs": { + "name": "acdl-microservice", + "subnets": "ref:vpc.subnet_ids", + "security_group": "ref:roles.role_arn", + "port": 8080, + "protocol": "HTTP", + "region": "us-east-1" + } +} +``` + +The `target_group_arn` output is referenced by `l1-ecs-service` as its +`lb_target_group_arn` input to wire the service to the ALB. + +## Compliance extension points + +- **TLS / HTTPS listener** — add `aws_acm_certificate` + `ssl_policy` + `certificate_arn` for encryption in transit (SOC2 CC6.1, PCI-DSS 4.1, HIPAA §164.312(e)(1), GDPR Art.32). +- **Access logs** — add `access_logs { bucket = ..., prefix = ... }` to the load balancer (SOX, SOC2 CC7.2, DORA ICT audit trail). +- **Security group rules** — add ingress/egress rules restricting traffic to known sources (SOC2 CC6.6, PCI-DSS 1.2). +- **Health check** — add a `health_check` block to the target group (SOC2 CC7.3 monitoring, DORA operational resilience). +- **WAF** — add `aws_wafv2_web_acl_association` for application-layer protection (SOC2 CC7.6, PCI-DSS 6.5, DORA ICT risk). +- **Deregistration delay** — add `deregistration_delay` for graceful draining (SOC2 CC9.1 resilience). + +## Versioning `1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps require a new registry entry (immutable publication); old entries enter diff --git a/modules-ir/l1/l1-ecr/README.md b/modules-ir/l1/l1-ecr/README.md index 8ab812f..ca805b4 100644 --- a/modules-ir/l1/l1-ecr/README.md +++ b/modules-ir/l1/l1-ecr/README.md @@ -1,31 +1,57 @@ -# l1-ecr — ECR repository primitive +# l1-ecr — ECR repository -An L1 module for an ECR repository that hosts the ECS task image. -Single-purpose, substrate-agnostic (the IR type is -`aws:ecr:repository`, not a Terraform resource type). +> **Module kind:** L1 primitive | **Version:** 1.0.0 -## Interface (the IR-typed contract) +A single ECR repository that hosts the container image for the ECS +task. The simplest container-registry module — one resource, two +inputs, two outputs. -See `interface.json`: inputs `name` + `region` (strings), outputs -`repository_url` (string) + `repository_arn` (arn), no NFRs. +## Resources -## IR → Terraform mapping (performed by the adapter) +| Resource | Type | Purpose | +|----------|------|---------| +| repository | `aws_ecr_repository` | The ECR repository | -The Terraform adapter (`adapters/terraform/adapter.py`) translates this -L1's IR shape to Terraform: +## Inputs -| IR | Terraform | -|----|-----------| -| `resource.type = aws:ecr:repository` | `resource "aws_ecr_repository" "" { ... }` | -| `resource.inputs.name` | `name = ` arg | -| `resource.inputs.region` | `provider "aws" { region = }` | -| `resource.outputs.repository_url` | `output "repository_url" { value = aws_ecr_repository..repository_url }` | -| `resource.outputs.repository_arn` | `output "repository_arn" { value = aws_ecr_repository..arn }` | +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `name` | string | yes | — | The ECR repository name | +| `region` | string | yes | — | AWS region the repository is created in | -The adapter is a thin layer (ARCHITECTURE.md §12.2); it does not own L1 -content — it only translates. +## Outputs -## Versioning (W3.D) +| Name | Type | Description | +|------|------|-------------| +| `repository_url` | string | The ECR repository URL | +| `repository_arn` | arn | The ECR repository ARN | + +## Usage + +```json +{ + "id": "ecr", + "type": "aws:ecr:repository", + "module": "l1-ecr@1.0.0", + "inputs": { + "name": "acdl-microservice", + "region": "us-east-1" + } +} +``` + +The `repository_url` output is used to build the `image` input for +`l1-ecs-service` (e.g. `:latest`). + +## Compliance extension points + +- **Image scanning** — add `image_scanning_configuration { scan_on_push = true }` for vulnerability scanning (SOC2 CC7.6, DORA ICT risk testing, HIPAA security monitoring). +- **Encryption** — add `encryption_configuration { encryption_type = "KMS", kms_key = ... }` with a customer-managed key (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv), GDPR Art.32). +- **Image tag immutability** — add `image_tag_mutability = "IMMUTABLE"` to prevent tag overwriting (SOX §802, SOC2 CC6.1 integrity, DORA audit integrity). +- **Lifecycle policy** — add `aws_ecr_lifecycle_policy` to enforce image retention / cleanup (GDPR Art.5(2) data minimization, SOC2 CC5.2). +- **Access policy** — add a repository policy restricting pull/push to known roles (SOC2 CC6.1, HIPAA §164.308(a)(4)). + +## Versioning `1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps require a new registry entry (immutable publication); old entries enter diff --git a/modules-ir/l1/l1-ecs-cluster/README.md b/modules-ir/l1/l1-ecs-cluster/README.md index e82da13..834d95a 100644 --- a/modules-ir/l1/l1-ecs-cluster/README.md +++ b/modules-ir/l1/l1-ecs-cluster/README.md @@ -1,31 +1,55 @@ -# l1-ecs-cluster — ECS Fargate cluster primitive +# l1-ecs-cluster — ECS Fargate cluster -An L1 module for an ECS Fargate cluster. Single-purpose, -substrate-agnostic (the IR type is `aws:ecs:cluster`, not a Terraform -resource type). +> **Module kind:** L1 primitive | **Version:** 1.0.0 -## Interface (the IR-typed contract) +An ECS Fargate cluster. The simplest ECS module — one resource, two +inputs, two outputs. The cluster is the container orchestration +boundary that `l1-ecs-service` references for task placement. -See `interface.json`: inputs `name` + `region` (strings), outputs -`cluster_arn` (arn) + `cluster_id` (string), no NFRs. +## Resources -## IR → Terraform mapping (performed by the adapter) +| Resource | Type | Purpose | +|----------|------|---------| +| cluster | `aws_ecs_cluster` | The ECS Fargate cluster | -The Terraform adapter (`adapters/terraform/adapter.py`) translates this -L1's IR shape to Terraform: +## Inputs -| IR | Terraform | -|----|-----------| -| `resource.type = aws:ecs:cluster` | `resource "aws_ecs_cluster" "" { ... }` | -| `resource.inputs.name` | `name = ` arg | -| `resource.inputs.region` | `provider "aws" { region = }` | -| `resource.outputs.cluster_arn` | `output "cluster_arn" { value = aws_ecs_cluster..arn }` | -| `resource.outputs.cluster_id` | `output "cluster_id" { value = aws_ecs_cluster..id }` | +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `name` | string | yes | — | The ECS cluster name | +| `region` | string | yes | — | AWS region the cluster is created in | -The adapter is a thin layer (ARCHITECTURE.md §12.2); it does not own L1 -content — it only translates. +## Outputs -## Versioning (W3.D) +| Name | Type | Description | +|------|------|-------------| +| `cluster_arn` | arn | The ECS cluster ARN | +| `cluster_id` | string | The ECS cluster id (name) | + +## Usage + +```json +{ + "id": "cluster", + "type": "aws:ecs:cluster", + "module": "l1-ecs-cluster@1.0.0", + "inputs": { + "name": "acdl-microservice", + "region": "us-east-1" + } +} +``` + +The `cluster_arn` output is referenced by `l1-ecs-service` as its +`cluster_arn` input. + +## Compliance extension points + +- **Container Insights** — add `configuration { container_insights = "enabled" }` for observability (SOC2 CC7.3, DORA ICT risk monitoring). +- **CloudWatch Logs** — add a log group with retention policy for cluster-level audit logs (SOX, SOC2 CC7.2, HIPAA §164.312(b)). +- **Encryption** — add `settings { name = "containerInsights", value = "enabled" }` and KMS-based encryption for container data (HIPAA §164.312(a)(2)(iv), GDPR Art.32). + +## Versioning `1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps require a new registry entry (immutable publication); old entries enter diff --git a/modules-ir/l1/l1-ecs-service/README.md b/modules-ir/l1/l1-ecs-service/README.md index 134e321..d25a941 100644 --- a/modules-ir/l1/l1-ecs-service/README.md +++ b/modules-ir/l1/l1-ecs-service/README.md @@ -1,54 +1,77 @@ -# l1-ecs-service — ECS Fargate service primitive (multi-resource L1) +# l1-ecs-service — ECS Fargate service (task definition + service) -An L1 module for an ECS Fargate service (task definition + service). -Substrate-agnostic (the IR types are `aws:ecs:task_definition` and -`aws:ecs:service`, not Terraform resource types). This is a -multi-resource L1: the interface declares the group's inputs/outputs -plus a `resources` array listing the IR types it emits. The IR instance -(Phase 14/15) will have multiple `resources` entries all with -`module: "l1-ecs-service@1.0.0"`. +> **Module kind:** L1 primitive | **Version:** 1.0.0 -## Interface (the IR-typed contract) +An ECS Fargate service with its task definition. Runs a container image +on Fargate, optionally behind an ALB target group. This is a +multi-resource module: it creates a task definition and a service that +runs it. -See `interface.json`: inputs `image` (string, ECR image URL), `port` -(number), `cpu` (number, default 256), `memory` (number, default 512), -`env` (optional JSON map string), `cluster_arn` (arn, ref to -l1-ecs-cluster), `subnets` (string, ref to l1-vpc), `security_group` -(string), `lb_target_group_arn` (arn, optional, ref to l1-alb), `region` -(string); outputs `service_arn` (arn) + `task_def_arn` (arn); no NFRs. +## Resources -The `resources` array lists the emitted IR types: +| Resource | Type | Purpose | +|----------|------|---------| +| task_definition | `aws_ecs_task_definition` | Fargate task definition with container image, CPU, memory, port, env | +| service | `aws_ecs_service` | Fargate service running the task definition in a cluster + subnets | -- `aws:ecs:task_definition` — Fargate task definition. The adapter - jsonencodes `image`/`port`/`env` into `container_definitions`. -- `aws:ecs:service` — Fargate service running the task definition in the - cluster + subnets (+ optional ALB target group wiring). +## Inputs -## IR → Terraform mapping (performed by the adapter) +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `image` | string | yes | — | ECR image URL for the task container | +| `port` | number | yes | — | Container port the service listens on | +| `cpu` | number | no | 256 | Task CPU units (Fargate) | +| `memory` | number | no | 512 | Task memory in MiB (Fargate) | +| `env` | string | no | — | Environment variables as a JSON map string | +| `cluster_arn` | arn | yes | — | ECS cluster ARN (from `l1-ecs-cluster`) | +| `subnets` | string | yes | — | Comma-separated subnet ids (from `l1-vpc`) | +| `security_group` | string | yes | — | Security group id for the service ENIs | +| `lb_target_group_arn` | arn | no | — | Optional ALB target group ARN (from `l1-alb`) | +| `region` | string | yes | — | AWS region the service is created in | -The Terraform adapter (`adapters/terraform/adapter.py`) translates each -emitted IR resource to Terraform: +## Outputs -| IR | Terraform | -|----|-----------| -| `resource.type = aws:ecs:task_definition` | `resource "aws_ecs_task_definition" "" { ... }` | -| `resource.inputs.image` + `port` + `env` | `container_definitions = jsonencode(...)` (adapter-built) | -| `resource.inputs.cpu` | `cpu = ` arg | -| `resource.inputs.memory` | `memory = ` arg | -| `resource.outputs.task_def_arn` | `output "task_def_arn" { value = aws_ecs_task_definition..arn }` | -| `resource.type = aws:ecs:service` | `resource "aws_ecs_service" "" { ... }` | -| `resource.inputs.cluster_arn` | `cluster = ` arg (identity) | -| `resource.inputs.subnets` | `network_configuration { subnets = [...] }` (emit as-is) | -| `resource.inputs.security_group` | `network_configuration { security_groups = [...] }` (emit as-is) | -| `resource.inputs.lb_target_group_arn` | `load_balancer { target_group_arn = }` (emit as-is) | -| `resource.outputs.service_arn` | `output "service_arn" { value = aws_ecs_service..id }` | +| Name | Type | Description | +|------|------|-------------| +| `service_arn` | arn | The ECS service ARN | +| `task_def_arn` | arn | The ECS task definition ARN | -The adapter is a thin layer (ARCHITECTURE.md §12.2); it does not own L1 -content — it only translates. The `container_definitions` JSON is built -by the adapter from the IR `image`/`port`/`env` inputs (the one -transformation the adapter owns for ECS task definitions). +## Usage -## Versioning (W3.D) +```json +{ + "id": "service", + "type": "aws:ecs:task_definition", + "module": "l1-ecs-service@1.0.0", + "inputs": { + "image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest", + "port": 8080, + "cpu": 256, + "memory": 512, + "cluster_arn": "ref:cluster.cluster_arn", + "subnets": "ref:vpc.subnet_ids", + "security_group": "ref:roles.role_arn", + "region": "us-east-1" + } +} +``` + +The `image`, `port`, and `env` inputs are compiled into a +`container_definitions` JSON block by the adapter. The service is +placed in the cluster with the given subnets and security group, and +optionally wired to the ALB target group if `lb_target_group_arn` is +provided. + +## Compliance extension points + +- **CloudWatch Logs** — add `logConfiguration` to the container definition with a log group + retention policy (SOX, SOC2 CC7.2, HIPAA §164.312(b), DORA ICT incident logging). +- **Task execution role separation** — add a separate `aws_iam_role` for execution vs. the task role (SOC2 CC6.3 segregation of duties at runtime). +- **Secrets injection** — add `secrets` block referencing AWS Secrets Manager / SSM Parameter Store with KMS encryption (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv)). +- **Execute command** — add `enable_execute_command` with KMS encryption for session audit (SOC2 CC7.2). +- **Deployment circuit breaker** — add `deployment_circuit_breaker` block for resilience (SOC2 CC9.1, DORA operational resilience). +- **Health check** — add a `health_check` block to the target group (currently missing despite the contract schema having a healthcheck field). + +## Versioning `1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps require a new registry entry (immutable publication); old entries enter diff --git a/modules-ir/l1/l1-iam-role/README.md b/modules-ir/l1/l1-iam-role/README.md index 626e1e2..6489e3c 100644 --- a/modules-ir/l1/l1-iam-role/README.md +++ b/modules-ir/l1/l1-iam-role/README.md @@ -1,35 +1,63 @@ -# l1-iam-role — IAM role primitive +# l1-iam-role — IAM role -An L1 module for an IAM role (used as the ECS task execution role). -Single-purpose, substrate-agnostic (the IR type is `aws:iam:role`, not a -Terraform resource type). +> **Module kind:** L1 primitive | **Version:** 1.0.0 -## Interface (the IR-typed contract) +A single IAM role with an assume-role policy and optional managed +policy attachments. Used as the ECS task execution role. -See `interface.json`: inputs `role_name` (string), `assume_role_policy` -(JSON string), `managed_policies` (optional comma-separated ARNs), -`region` (string); outputs `role_arn` (arn) + `role_id` (string), no -NFRs. +## Resources -## IR → Terraform mapping (performed by the adapter) +| Resource | Type | Purpose | +|----------|------|---------| +| role | `aws_iam_role` | The IAM role with assume-role policy | -The Terraform adapter (`adapters/terraform/adapter.py`) translates this -L1's IR shape to Terraform: +## Inputs -| IR | Terraform | -|----|-----------| -| `resource.type = aws:iam:role` | `resource "aws_iam_role" "" { ... }` | -| `resource.inputs.role_name` | `name = ` arg | -| `resource.inputs.assume_role_policy` | `assume_role_policy = ` arg (JSON string) | -| `resource.inputs.managed_policies` | `managed_policy_arns = []` arg (comma-split) | -| `resource.inputs.region` | `provider "aws" { region = }` | -| `resource.outputs.role_arn` | `output "role_arn" { value = aws_iam_role..arn }` | -| `resource.outputs.role_id` | `output "role_id" { value = aws_iam_role..id }` | +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `role_name` | string | yes | — | The IAM role name | +| `assume_role_policy` | string | yes | — | Assume-role policy document (JSON string) | +| `managed_policies` | string | no | — | Comma-separated list of managed policy ARNs to attach | +| `region` | string | yes | — | AWS region the role is created in | -The adapter is a thin layer (ARCHITECTURE.md §12.2); it does not own L1 -content — it only translates. +## Outputs -## Versioning (W3.D) +| Name | Type | Description | +|------|------|-------------| +| `role_arn` | arn | The IAM role ARN | +| `role_id` | string | The IAM role id | + +## Usage + +```json +{ + "id": "roles", + "type": "aws:iam:role", + "module": "l1-iam-role@1.0.0", + "inputs": { + "role_name": "acdl-microservice-exec", + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ecs-tasks.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}", + "managed_policies": "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", + "region": "us-east-1" + } +} +``` + +The `assume_role_policy` is a JSON string — the adapter jsonencodes it +into the Terraform `assume_role_policy` argument. The +`managed_policies` input is a comma-separated list of ARNs, emitted as +`managed_policy_arns = [...]`. + +## Compliance extension points + +- **Permissions boundary** — add `permissions_boundary` to enforce least-privilege guardrails (SOC2 CC6.1, SOX ITGC, DORA ICT access control). +- **Inline policy** — add `aws_iam_role_policy` for fine-grained least-privilege instead of broad managed policies (SOC2 CC6.1, HIPAA §164.308(a)(4)). +- **MFA conditions** — add `condition` blocks requiring MFA for assume-role (SOC2 CC6.1, HIPAA §164.312(d)). +- **Source IP / region conditions** — add `aws:SourceIp` / `aws:RequestedRegion` conditions for data residency enforcement (GDPR Art.44-49, DORA ICT third-party risk). +- **Access Analyzer** — add `aws_accessanalyzer_analyzer` to verify least-privilege (SOC2 CC6.1, GDPR Art.32). +- **Role separation** — add a separate task role vs. execution role (SOC2 CC6.3 segregation of duties). + +## Versioning `1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps require a new registry entry (immutable publication); old entries enter diff --git a/modules-ir/l1/l1-s3/README.md b/modules-ir/l1/l1-s3/README.md index 23b3cb8..7a4711c 100644 --- a/modules-ir/l1/l1-s3/README.md +++ b/modules-ir/l1/l1-s3/README.md @@ -1,39 +1,62 @@ -# l1-s3 — S3 bucket primitive +# l1-s3 — S3 bucket -The first real L1 module for the v1.1 spike. Single-purpose, -substrate-agnostic (the IR type is `aws:s3:bucket`, not a Terraform -resource type). +> **Module kind:** L1 primitive | **Version:** 1.0.0 -## Interface (the IR-typed contract) +A single S3 bucket for object storage. The simplest module — one +resource, two inputs, two outputs. Versioning is enabled by default. -See `interface.json`: inputs `bucket_name` + `region` (strings), outputs -`bucket_arn` (arn) + `bucket_name` (string), NFR `versioning` (bool, -default true). +## Resources -## IR → Terraform mapping (performed by the adapter) +| Resource | Type | Purpose | +|----------|------|---------| +| bucket | `aws_s3_bucket` | The S3 bucket itself | -The Terraform adapter (`adapters/terraform/adapter.py`) translates this -L1's IR shape to Terraform: +## Inputs -| IR | Terraform | -|----|-----------| -| `resource.type = aws:s3:bucket` | `resource "aws_s3_bucket" "" { ... }` | -| `resource.inputs.bucket_name` | `bucket = ` arg | -| `resource.inputs.region` | `provider "aws" { region = }` | -| `resource.outputs.bucket_arn` | `output "bucket_arn" { value = aws_s3_bucket..arn }` | -| `resource.outputs.bucket_name` | `output "bucket_name" { value = aws_s3_bucket..id }` | +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `bucket_name` | string | yes | — | Globally-unique S3 bucket name | +| `region` | string | yes | — | AWS region the bucket is created in | -The adapter is a thin layer (ARCHITECTURE.md §12.2); it does not own L1 -content — it only translates. +## Outputs -## Spike instance +| Name | Type | Description | +|------|------|-------------| +| `bucket_arn` | arn | The S3 bucket ARN | +| `bucket_name` | string | The bucket name (echoes the input) | -`spike_instance.json` is a concrete stack instance (with values -`bucket_name=acdl-spike-bucket`, `region=us-east-1`) that validates -against `schemas/ir.schema.json`. The adapter consumes this instance -(not the interface contract) to emit Terraform. +## NFRs -## Versioning (W3.D) +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `versioning` | boolean | true | Enable S3 versioning | + +## Usage + +```json +{ + "id": "s3", + "type": "aws:s3:bucket", + "module": "l1-s3@1.0.0", + "inputs": { + "bucket_name": "acdl-spike-bucket", + "region": "us-east-1" + } +} +``` + +A concrete instance is at `spike_instance.json` (used by the platform +pipeline as the regression baseline). + +## Compliance extension points + +- **Encryption at rest** — add `aws_s3_bucket_server_side_encryption_configuration` with a customer-managed KMS key (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv), GDPR Art.32). +- **Object Lock** — add `aws_s3_bucket_object_lock_configuration` in compliance mode with 7-year retention for immutable evidence (SOX §802, DORA audit trail). +- **Access logging** — add `aws_s3_bucket_logging` to a target logging bucket (SOC2 CC7.2). +- **Public access block** — add `aws_s3_bucket_public_access_block` to prevent data exfiltration (SOC2 CC6.1, GDPR Art.32). +- **Lifecycle policy** — add `aws_s3_bucket_lifecycle_configuration` for retention enforcement (GDPR Art.5(2), HIPAA §164.530(j)). + +## Versioning `1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps require a new registry entry (immutable publication); old entries enter diff --git a/modules-ir/l1/l1-vpc/README.md b/modules-ir/l1/l1-vpc/README.md index 0e13e78..e561d18 100644 --- a/modules-ir/l1/l1-vpc/README.md +++ b/modules-ir/l1/l1-vpc/README.md @@ -1,51 +1,66 @@ -# l1-vpc — VPC primitive (multi-resource L1) +# l1-vpc — VPC with subnets and routing -An L1 module for a VPC with subnets and a route table. Substrate-agnostic -(the IR types are `aws:ec2:vpc`, `aws:ec2:subnet`, `aws:ec2:routetable`, -not Terraform resource types). This is a multi-resource L1: the -interface declares the group's inputs/outputs plus a `resources` array -listing the IR types it emits. The IR instance (Phase 14/15) will have -multiple `resources` entries all with `module: "l1-vpc@1.0.0"`. +> **Module kind:** L1 primitive | **Version:** 1.0.0 -## Interface (the IR-typed contract) +A VPC with one subnet per availability zone and a route table with a +default route through an internet gateway. The networking foundation +that other modules (ALB, ECS service) reference for subnet ids. -See `interface.json`: inputs `cidr` (string, e.g. "10.0.0.0/16"), `azs` -(string, comma-separated, e.g. "us-east-1a,us-east-1b"), `name` (string, -used for tagging), `region` (string); outputs `vpc_id` (string), -`subnet_ids` (string, comma-separated), `igw_id` (string); no NFRs. +## Resources -The `resources` array lists the emitted IR types: +| Resource | Type | Purpose | +|----------|------|---------| +| vpc | `aws_vpc` | The VPC itself | +| subnet | `aws_subnet` | One subnet per availability zone | +| route_table | `aws_route_table` | Route table with default route 0.0.0.0/0 | +| internet_gateway | `aws_internet_gateway` | IGW for public internet access | +| route_table_association | `aws_route_table_association` | Binds subnet to route table | -- `aws:ec2:vpc` — the VPC itself (cidr → cidr_block, name → tag). -- `aws:ec2:subnet` — one subnet per availability zone (`azs` split on - comma); inputs include the parent VPC id. -- `aws:ec2:routetable` — route table bound to the VPC with an internet - gateway + default route (0.0.0.0/0 → igw). +## Inputs -## IR → Terraform mapping (performed by the adapter) +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `cidr` | string | yes | — | VPC CIDR block, e.g. `10.0.0.0/16` | +| `azs` | string | yes | — | Comma-separated availability zones, e.g. `us-east-1a,us-east-1b` | +| `name` | string | yes | — | Name tag for the VPC and child resources | +| `region` | string | yes | — | AWS region the VPC is created in | -The Terraform adapter (`adapters/terraform/adapter.py`) translates each -emitted IR resource to Terraform: +## Outputs -| IR | Terraform | -|----|-----------| -| `resource.type = aws:ec2:vpc` | `resource "aws_vpc" "" { ... }` | -| `resource.inputs.cidr` | `cidr_block = ` arg | -| `resource.inputs.name` | `tags = { Name = }` (emit as-is) | -| `resource.outputs.vpc_id` | `output "vpc_id" { value = aws_vpc..id }` | -| `resource.type = aws:ec2:subnet` | `resource "aws_subnet" "" { ... }` | -| `resource.inputs.cidr` | `cidr_block = ` arg | -| `resource.inputs.az` | `availability_zone = ` arg | -| `resource.outputs.subnet_id` | `output "subnet_id" { value = aws_subnet..id }` | -| `resource.type = aws:ec2:routetable` | `resource "aws_route_table" "" { ... }` | -| `resource.inputs.vpc_id` | `vpc_id = ` arg | +| Name | Type | Description | +|------|------|-------------| +| `vpc_id` | string | The VPC id | +| `subnet_ids` | string | Comma-separated subnet ids | -The internet gateway + default route are emitted as part of the route -table resource's IR (the `igw_id` output is wired via the route table's -inputs). The adapter is a thin layer (ARCHITECTURE.md §12.2); it does -not own L1 content — it only translates. +## Usage -## Versioning (W3.D) +```json +{ + "id": "vpc", + "type": "aws:ec2:vpc", + "module": "l1-vpc@1.0.0", + "inputs": { + "cidr": "10.0.0.0/16", + "azs": "us-east-1a,us-east-1b", + "name": "acdl-microservice", + "region": "us-east-1" + } +} +``` + +The `azs` input is split on comma; one subnet is created per zone. The +route table gets a default route `0.0.0.0/0` → internet gateway. Other +modules reference `subnet_ids` for their network placement. + +## Compliance extension points + +- **VPC Flow Logs** — add `aws_flow_log` + CloudWatch Logs group / S3 destination (SOX ITGC, SOC2 CC7.2, HIPAA §164.312(b), DORA ICT risk logging). +- **Private subnets + NAT gateway** — add private subnets with a NAT gateway so ECS tasks don't need public IPs (SOC2 CC6.6, PCI-DSS 1.3, HIPAA network isolation). +- **VPC endpoints** — add S3, ECR, KMS, DynamoDB, CloudWatch interface/gateway endpoints to keep traffic off the public internet (SOC2 CC6.7, GDPR Art.32(1)(a), DORA ICT third-party risk). +- **Security groups** — add `aws_security_group` as a first-class sub-resource (currently missing; needed for all regulated deployments) (SOC2 CC6.6, PCI-DSS 1.2). +- **Network ACLs** — add `aws_network_acl` for subnet-level segmentation (PCI-DSS 1.3). + +## Versioning `1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps require a new registry entry (immutable publication); old entries enter diff --git a/modules-ir/l2/l2-microservice/README.md b/modules-ir/l2/l2-microservice/README.md index be30784..c519b55 100644 --- a/modules-ir/l2/l2-microservice/README.md +++ b/modules-ir/l2/l2-microservice/README.md @@ -1,85 +1,57 @@ -# l2-microservice — thin-composition (ECS Fargate microservice) +# l2-microservice — ECS Fargate microservice (composition being redesigned) -The v1.2 L2. A thin-composition that references 6 L1s (depth 1): -`l1-vpc`, `l1-ecs-cluster`, `l1-ecr`, `l1-iam-role`, `l1-alb`, -`l1-ecs-service`. The contract's inputs (`name`, `cidr`, `azs`, -`image`, `port`, `cpu`, `memory`, `env`, `protocol`, `region`, -`role_name`, `assume_role_policy`, `managed_policies`) map to the -children's inputs through two wire kinds. +> **Module kind:** L2 composition | **Version:** TBD | **Status:** Under redesign -## Composition (the IR-typed thin-composition tree) +A composition that references multiple L1 primitives to deploy an ECS +Fargate microservice end-to-end (VPC, cluster, ECR, IAM role, ALB, +ECS service). -See `composition.json`: `kind=l2`, `depth=1`, six children. +**The composition layer is being redesigned.** The previous +thin-composition implementation (a `composition.json` with children + +wires) has been removed. A new composition mechanism will be designed +in a later phase. -### Children +## Resources -| child id | L1 module | IR type(s) | -|----------|-----------|------------| -| `vpc` | `l1-vpc@1.0.0` | `aws:ec2:vpc`, `aws:ec2:subnet`, `aws:ec2:routetable` | -| `cluster` | `l1-ecs-cluster@1.0.0` | `aws:ecs:cluster` | -| `ecr` | `l1-ecr@1.0.0` | `aws:ecr:repository` | -| `roles` | `l1-iam-role@1.0.0` | `aws:iam:role` | -| `alb` | `l1-alb@1.0.0` | `aws:elbv2:loadbalancer`, `aws:elbv2:listener`, `aws:elbv2:targetgroup` | -| `service` | `l1-ecs-service@1.0.0` | `aws:ecs:task_definition`, `aws:ecs:service` | +TBD — the composition will reference these L1 primitives: -Multi-resource L1s (`vpc`, `alb`, `service`) declare a `resources` -array in their `interface.json`; the resolver expands each child into -one IR resource per `resources` entry (id scheme `-` -where `type_suffix` is the last segment of the IR type with underscores -stripped — e.g. `vpc-vpc`, `vpc-subnet`, `vpc-routetable`, -`alb-loadbalancer`, `alb-targetgroup`, `alb-listener`, -`service-taskdefinition`, `service-service`. The hyphen separator keeps -the id valid against `schemas/ir.schema.json`'s -`^[a-z][a-z0-9-]*$` resource id pattern). Single-resource L1s keep the -child id verbatim (`cluster`, `ecr`, `roles`). +| L1 module | Purpose | README | +|-----------|---------|--------| +| `l1-vpc` | VPC, subnets, routing | [README](../l1/l1-vpc/README.md) | +| `l1-ecs-cluster` | ECS Fargate cluster | [README](../l1/l1-ecs-cluster/README.md) | +| `l1-ecr` | ECR image repository | [README](../l1/l1-ecr/README.md) | +| `l1-iam-role` | IAM task execution role | [README](../l1/l1-iam-role/README.md) | +| `l1-alb` | Application Load Balancer | [README](../l1/l1-alb/README.md) | +| `l1-ecs-service` | ECS task definition + service | [README](../l1/l1-ecs-service/README.md) | -### Wire kinds +## Inputs -1. **Contract→child passthrough** — wire name = contract input name; - target = child id, input = child's input name. For contract inputs - that fan out to multiple children (`name`, `port`, `region`), the - wire value is an array of `{target, input}` objects; otherwise a - single object. Resolves to the concrete contract value. +TBD — will be defined when the composition mechanism is redesigned. -2. **Child→child references** — wire with `source: "child:."`. - The value is only known at apply time, so the resolver emits the IR - input as the string `ref:.` (the IR resource - id of the *producing* child's first resource — for single-resource - L1s that is the child id, for multi-resource L1s it is - `-` of the first resource in the `resources` - array that declares the output). The adapter translates `ref:` to a - Terraform interpolation. +## Outputs -Wires used by this composition: +TBD — will be defined when the composition mechanism is redesigned. -- Passthrough: `name` (→vpc/cluster/ecr/alb), `cidr` (→vpc), `azs` - (→vpc), `image` (→service), `port` (→service/alb), `cpu` (→service), - `memory` (→service), `env` (→service), `protocol` (→alb), `region` - (→all 6), `role_name` (→roles), `assume_role_policy` (→roles), - `managed_policies` (→roles). -- Child→child: `cluster_arn` (cluster→service), `subnet_ids` - (vpc→service/alb `subnets`), `target_group_arn` (alb→service - `lb_target_group_arn`), `role_arn` (roles→service/alb - `security_group`). +## Usage -## IR → Terraform mapping (D-P10-1) +TBD — the composition mechanism is being redesigned. Until then, use +the L1 primitives directly. See each L1 module's README for usage +examples. -The Terraform adapter consumes the *resolved IR instance* (which has -`kind=l2` + all 6 L1s expanded into one IR resource per entry in each -L1's `resources` array, with `ref:` strings on the consumer inputs). -For a depth-1 thin-composition, the L2 root module **IS** the union of -the L1 resources — no separate `module "l1_x" { source = "..." }` -blocks. The existing adapter `TYPE_MAP` + `INPUT_MAP` + `OUTPUT_MAP` -tables handle every IR type. `ref:.` inputs are translated -to `${..}` (attribute mapped through `OUTPUT_MAP` -for the referenced resource's type). The `relationships` array records -the parent composition tree; ordering is implicit in the resource list. +## Compliance extension points -v1.3+ may emit real `module "l1_x" { source = "..." }` blocks once L1s -are published Terraform modules rather than inline resources. +The composition will need to wire compliance resources across L1s +when the compliance milestone (GDPR, SOX, SOC2, HIPAA, DORA) lands: -## Versioning (W3.D) +- **KMS key** — shared encryption key referenced by S3, ECR, CloudWatch Logs, and Secrets Manager. +- **CloudTrail** — management-plane audit trail for the entire stack. +- **VPC Flow Logs** — network audit trail. +- **Security groups** — proper network segmentation between ALB, service, and data tiers. +- **Private subnets** — ECS tasks in private subnets with NAT egress. -`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps -require a new registry entry (immutable publication); old entries enter -a 12-month deprecation window. \ No newline at end of file +See each L1 module's README for per-module compliance extension points. + +## Versioning + +Versioning will be defined when the composition mechanism is +redesigned. \ No newline at end of file diff --git a/modules-ir/l2/l2-microservice/composition.json b/modules-ir/l2/l2-microservice/composition.json deleted file mode 100644 index de21d2d..0000000 --- a/modules-ir/l2/l2-microservice/composition.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "l2-microservice", - "version": "1.0.0", - "kind": "l2", - "depth": 1, - "description": "Thin-composition: an ECS Fargate microservice. References 6 L1s (vpc, cluster, ecr, roles, alb, service).", - "children": [ - {"id": "vpc", "module": "l1-vpc@1.0.0"}, - {"id": "cluster", "module": "l1-ecs-cluster@1.0.0"}, - {"id": "ecr", "module": "l1-ecr@1.0.0"}, - {"id": "roles", "module": "l1-iam-role@1.0.0"}, - {"id": "alb", "module": "l1-alb@1.0.0"}, - {"id": "service", "module": "l1-ecs-service@1.0.0"} - ], - "wires": { - "name": [ - {"target": "vpc", "input": "name"}, - {"target": "cluster", "input": "name"}, - {"target": "ecr", "input": "name"}, - {"target": "alb", "input": "name"} - ], - "cidr": {"target": "vpc", "input": "cidr"}, - "azs": {"target": "vpc", "input": "azs"}, - "image": {"target": "service", "input": "image"}, - "port": [ - {"target": "service", "input": "port"}, - {"target": "alb", "input": "port"} - ], - "cpu": {"target": "service", "input": "cpu"}, - "memory": {"target": "service", "input": "memory"}, - "env": {"target": "service", "input": "env"}, - "protocol": {"target": "alb", "input": "protocol"}, - "region": [ - {"target": "vpc", "input": "region"}, - {"target": "cluster", "input": "region"}, - {"target": "ecr", "input": "region"}, - {"target": "roles", "input": "region"}, - {"target": "alb", "input": "region"}, - {"target": "service", "input": "region"} - ], - "role_name": {"target": "roles", "input": "role_name"}, - "assume_role_policy": {"target": "roles", "input": "assume_role_policy"}, - "managed_policies": {"target": "roles", "input": "managed_policies"}, - "cluster_arn": {"target": "service", "input": "cluster_arn", "source": "child:cluster.cluster_arn"}, - "subnet_ids": [ - {"target": "service", "input": "subnets", "source": "child:vpc.subnet_ids"}, - {"target": "alb", "input": "subnets", "source": "child:vpc.subnet_ids"} - ], - "target_group_arn": {"target": "service", "input": "lb_target_group_arn", "source": "child:alb.target_group_arn"}, - "role_arn": [ - {"target": "service", "input": "security_group", "source": "child:roles.role_arn"}, - {"target": "alb", "input": "security_group", "source": "child:roles.role_arn"} - ] - } -} \ No newline at end of file diff --git a/modules-ir/l2/l2-static-asset/README.md b/modules-ir/l2/l2-static-asset/README.md index f13dc00..cef0129 100644 --- a/modules-ir/l2/l2-static-asset/README.md +++ b/modules-ir/l2/l2-static-asset/README.md @@ -1,31 +1,51 @@ -# l2-static-asset — thin-composition (S3 static asset) +# l2-static-asset — S3 static asset (composition being redesigned) -The v1.1 spike's L2. A thin-composition that references `l1-s3` only -(depth 1). The contract's inputs (`bucket_name`, `region`) map 1:1 -through the wires to the L1's inputs. +> **Module kind:** L2 composition | **Version:** TBD | **Status:** Under redesign -## Composition (the IR-typed thin-composition tree) +A composition that references the `l1-s3` primitive to deploy a single +S3 bucket for static asset hosting. -See `composition.json`: `kind=l2`, `depth=1`, one child `l1-s3@1.0.0`, -wires `{bucket_name → s3.inputs.bucket_name, region → s3.inputs.region}` -(passthrough). +**The composition layer is being redesigned.** The previous +thin-composition implementation (a `composition.json` with children + +wires) has been removed. A new composition mechanism will be designed +in a later phase. -## IR → Terraform mapping (D-P10-1) +## Resources -The Terraform adapter consumes the *resolved IR instance* (which has -`kind=l2` + the L1 resource `s3` in its `resources` array). For a -depth-1 thin-composition, the L2 root module **IS** the L1's resource — -no separate `module "l1_s3" { source = "..." }` block. The existing -adapter `TYPE_MAP` + resource emission handle both l1 and l2 instances -(the resources array is the same shape). The `relationships` array is -ignored at the Terraform level for the spike (composition ordering is -implicit in the single resource). +TBD — the composition will reference this L1 primitive: -v1.2 may emit a real `module "l1_s3" { source = "..." }` block when L1s -become published Terraform modules rather than inline resources. +| L1 module | Purpose | README | +|-----------|---------|--------| +| `l1-s3` | S3 bucket | [README](../l1/l1-s3/README.md) | -## Versioning (W3.D) +## Inputs -`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps -require a new registry entry (immutable publication); old entries enter -a 12-month deprecation window. \ No newline at end of file +TBD — will be defined when the composition mechanism is redesigned. + +## Outputs + +TBD — will be defined when the composition mechanism is redesigned. + +## Usage + +TBD — the composition mechanism is being redesigned. Until then, use +`l1-s3` directly. See the [l1-s3 README](../l1/l1-s3/README.md) for a +usage example. + +## Compliance extension points + +The composition will need to wire compliance resources when the +compliance milestone (GDPR, SOX, SOC2, HIPAA, DORA) lands: + +- **KMS key** — shared encryption key for S3 SSE. +- **S3 access logs** — access logging to a separate audit bucket. +- **Object Lock** — 7-year immutable retention for evidence. +- **Public access block** — prevent data exfiltration. + +See the [l1-s3 README](../l1/l1-s3/README.md) for per-module compliance +extension points. + +## Versioning + +Versioning will be defined when the composition mechanism is +redesigned. \ No newline at end of file diff --git a/modules-ir/l2/l2-static-asset/composition.json b/modules-ir/l2/l2-static-asset/composition.json deleted file mode 100644 index e24f092..0000000 --- a/modules-ir/l2/l2-static-asset/composition.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "l2-static-asset", - "version": "1.0.0", - "kind": "l2", - "depth": 1, - "description": "Thin-composition: a single S3 bucket for static asset hosting. References l1-s3 only (depth 1).", - "children": [ - { - "id": "s3", - "module": "l1-s3@1.0.0" - } - ], - "wires": { - "bucket_name": {"target": "s3", "input": "bucket_name"}, - "region": {"target": "s3", "input": "region"} - } -} \ No newline at end of file diff --git a/modules-ir/registry.json b/modules-ir/registry.json index 0f16ac1..43a0c82 100644 --- a/modules-ir/registry.json +++ b/modules-ir/registry.json @@ -47,19 +47,5 @@ "published_at": "2026-07-21T21:30:00Z", "deprecated": false } - }, - "l2-static-asset": { - "1.0.0": { - "composition": "modules-ir/l2/l2-static-asset/composition.json", - "published_at": "2026-07-21T19:30:00Z", - "deprecated": false - } - }, - "l2-microservice": { - "1.0.0": { - "composition": "modules-ir/l2/l2-microservice/composition.json", - "published_at": "2026-07-21T22:00:00Z", - "deprecated": false - } } } \ No newline at end of file diff --git a/schemas/contract.schema.json b/schemas/contract.schema.json deleted file mode 100644 index ef89f34..0000000 --- a/schemas/contract.schema.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://acdl.cloudinit.dev/schemas/contract.schema.json", - "title": "ACDL Contract", - "description": "Consumer-declared intent. The central pipeline resolves a contract to a Target Stack IR (schemas/ir.schema.json), the Terraform adapter compiles the IR to a plan. Strict fail-fast at schema stage with reason codes from a published vocabulary.", - "$comment": "Per-env mandatory inputs per W3.E (PROJECT.md). dev requires stack+environment; qa adds validation.e2eSuite + validation.loadTest; prod adds runbook+dashboard+oncall; dr adds drDrillRef. inputs always optional. profile: agentic fields optional everywhere (naturalLanguageIntent required when profile is agentic). W2.A (tag for dev/qa, SHA for prod) is a workflow-reference concern, not a schema field; the platform CLI resolves tag->SHA for prod-bound workflows.", - "type": "object", - "required": ["stack", "environment"], - "properties": { - "stack": { - "type": "string", - "pattern": "^l2-[a-z][a-z0-9-]*$", - "description": "L2 thin-composition reference (resolved by the pipeline to a Target Stack IR)." - }, - "environment": { - "type": "string", - "enum": ["dev", "qa", "prod", "dr"], - "description": "Target environment. Staging does not exist (Path A locked, ARCHITECTURE.md §5)." - }, - "inputs": { - "type": "object", - "description": "L2-level parameter map. Free-form in v1, typed per-L1 in v1.2 (W3.E).", - "additionalProperties": {"type": ["string", "number", "boolean", "object"]} - }, - "healthcheck": { - "type": "object", - "description": "Healthcheck config for the service.", - "properties": { - "path": {"type": "string"}, - "interval": {"type": "number"}, - "timeout": {"type": "number"}, - "healthy_threshold": {"type": "number"} - } - }, - "validation": { - "type": "object", - "description": "Validation evidence required in qa (W3.E).", - "properties": { - "e2eSuite": {"type": "string", "description": "Reference to the contract-declared e2e suite (last 24h, pass rate >= 99%)."}, - "loadTest": {"type": "string", "description": "Reference to the load test report (last 7d, p99 < declared NFR)."} - } - }, - "runbook": {"type": "string", "description": "Runbook reference, mandatory in prod (W3.E)."}, - "dashboard": {"type": "string", "description": "Dashboard reference, mandatory in prod (W3.E)."}, - "oncall": {"type": "string", "description": "On-call rotation reference, mandatory in prod (W3.E)."}, - "drDrillRef": {"type": "string", "description": "DR drill report reference (last 180d), mandatory in dr (W3.E)."}, - "profile": { - "type": "string", - "enum": ["developer", "agentic"], - "default": "developer", - "description": "Consumer surface. 'agentic' unlocks L3B fields (ARCHITECTURE.md §5)." - }, - "naturalLanguageIntent": { - "type": "string", - "description": "L3B: the original natural-language prompt. Required when profile is agentic (W3.E)." - }, - "confidenceAtSubmission": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "L3B: the agent's self-reported confidence at submission time." - }, - "agentTrace": { - "type": "string", - "description": "L3B: reference to the agent's execution trace." - }, - "supersedes": { - "type": "string", - "format": "uuid", - "description": "Prior contractId this re-submission replaces (after rejection — ARCHITECTURE.md §10.6)." - } - }, - "allOf": [ - { - "if": {"properties": {"environment": {"const": "qa"}}}, - "then": {"required": ["validation"], - "properties": {"validation": {"required": ["e2eSuite", "loadTest"]}}} - }, - { - "if": {"properties": {"environment": {"const": "prod"}}}, - "then": {"required": ["runbook", "dashboard", "oncall"]} - }, - { - "if": {"properties": {"environment": {"const": "dr"}}}, - "then": {"required": ["drDrillRef"]} - }, - { - "if": {"required": ["profile"], "properties": {"profile": {"const": "agentic"}}}, - "then": {"required": ["naturalLanguageIntent"]} - } - ] -} \ No newline at end of file diff --git a/scripts/run_platform.sh b/scripts/run_platform.sh index 59386ed..c2b7ac3 100755 --- a/scripts/run_platform.sh +++ b/scripts/run_platform.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash -# scripts/run_platform.sh - the ACDL platform pipeline (consolidated from -# the v1.1 spike scripts run_spike_e2e.sh + run_spike_plan.sh per D-048). +# scripts/run_platform.sh - the ACDL platform pipeline. # -# Default: full end-to-end pipeline (contract resolution -> IR -> terraform -# plan (real AWS) -> Checkov -> PolicyCheckResult -> confidence signal -> -# evidence event to DynamoDB outbox). -# --plan-only: contract resolution + adapter + terraform init/validate/plan -# (steps 1-4), then exit. +# Default: full end-to-end pipeline (load pre-existing IR instance -> +# adapter -> terraform plan (real AWS) -> Checkov -> PolicyCheckResult -> +# confidence signal -> evidence event to DynamoDB outbox). +# --plan-only: load IR + adapter + terraform init/validate/plan (steps +# 1-4), then exit. +# +# NOTE: contract resolution (contract_resolver.py) was removed when the +# thin-composition layer was taken out. The pipeline now starts from a +# pre-existing IR instance (modules-ir/l1/l1-s3/spike_instance.json). A +# new contract-resolution mechanism will be designed in a later phase. # # Uses the rotated spike key (D-039/D-047) from gitignored .env.secrets. # Plan-only (no apply); -lock=false per D-P09-1. @@ -33,14 +37,15 @@ export AWS_ACCESS_KEY_ID="$ACDL_AWS_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="$ACDL_AWS_SECRET_ACCESS_KEY" export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" -CONTRACT="contracts/spike.yaml" CONTRACT_ID="11111111-1111-1111-1111-111111111111" # spike fixed UUID WORK="/tmp/spike_e2e" rm -rf "$WORK"; mkdir -p "$WORK" -echo "=== Step 1+2: resolve contract -> IR (validates contract schema + IR schema) ===" -python3 acdl_platform/contract_resolver.py "$CONTRACT" "$WORK/spike_ir.json" || fail "contract resolution failed" -python3 -c "import json; d=json.load(open('$WORK/spike_ir.json')); print(f\"IR: {d['stack']['name']} {d['stack']['kind']} {len(d['resources'])} resource(s)\")" +echo "=== Step 1+2: load pre-existing IR instance (contract resolution deferred) ===" +IR_INSTANCE="modules-ir/l1/l1-s3/spike_instance.json" +[ -f "$IR_INSTANCE" ] || fail "IR instance $IR_INSTANCE missing (contract resolution is deferred; load a pre-existing IR)" +python3 -c "import json; d=json.load(open('$IR_INSTANCE')); print(f\"IR: {d['stack']['name']} {d['stack']['kind']} {len(d['resources'])} resource(s)\")" +cp "$IR_INSTANCE" "$WORK/spike_ir.json" echo "=== Step 3: adapter compiles IR -> terraform/spike/*.tf (regenerate) ===" python3 adapters/terraform/adapter.py "$WORK/spike_ir.json" terraform/spike || fail "adapter failed" @@ -112,5 +117,5 @@ echo "outbox: $(python3 -c "import json; d=json.load(open('$WORK/outbox_item.jso echo "" echo "=== PLATFORM E2E OK ===" -echo "contract=$CONTRACT -> IR -> terraform plan -> Checkov -> confidence ($BAND) -> outbox" +echo "IR instance -> terraform plan -> Checkov -> confidence ($BAND) -> outbox" exit 0 \ No newline at end of file