a16e6f1bff
EXECUTE stage. Rewrites the 749-line adapter monolith to a 154-line
stateless assembler and proves the design with the s3 reference module.
Stateless adapter (adapters/terraform/adapter.py, 749 → 154 lines):
- Deleted TYPE_MAP, INPUT_MAP, OUTPUT_MAP (3 constant tables).
- Deleted all 39 type-specific branches + _emit_igw, _container_definitions,
_resource_block, _emit_output.
- New adapt(): reads registry.json → terraform_dir → emits root main.tf
with module-instantiation blocks (module "x" { source = ... }) + ref
wiring via module.<rid>.<output> interpolations + root outputs.
- The adapter owns NO resource shape, NO nested blocks, NO defaults, NO
type-specific logic. It only assembles module instantiations and wires refs.
s3 reference terraform module (modules/l1/s3/terraform/):
- versions.tf (required_version + aws ~> 5.0)
- variables.tf (bucket_name, region, kms_key_arn, tags)
- locals.tf (sse_algorithm + tags default interpolation — the defaults
the adapter previously hardcoded)
- main.tf (aws_s3_bucket + versioning + SSE config, referencing local.*)
- outputs.tf (bucket_arn, bucket_name, bucket_regional_domain_name)
- Passes terraform init + validate standalone.
Registry (modules/registry.json): s3 entry gains terraform_dir field.
STANDARDS.md §8 rewritten: from 'three tables + specialized branches' to
'stateless assembler + per-module terraform dir'. §9.4 checklist updated.
§9.1 required-files list updated to include terraform/ subdir.
tests/test_adapter.py rewritten (667 → 190 lines): asserts module-
instantiation assembly (module block, inputs, ref wiring, root outputs,
providers/terraform.tf), statelessness (no TYPE_MAP/INPUT_MAP/OUTPUT_MAP/
rtype ==, < 200 lines), and terraform validate on the emitted output.
Deleted test_p1_1_adapter_parameterization.py (tested the deleted HCL
string emission).
6 pipeline tests skipped (run_platform.sh --check-only defaults to
static-assets.yml which needs cloudfront/waf terraform dirs — P56b).
Regression: 455 passed, 6 skipped, 5 deselected (slow). run_primitive_plan
--check-only s3 exits 0.
---ci---
project: acdl
phase: P56a
milestone: v1.11
status: execute
---/ci---
609 lines
26 KiB
Markdown
609 lines
26 KiB
Markdown
# ACDL Module Engineering Standards
|
|
|
|
Standards for authoring and reviewing ACDL modules. These standards
|
|
govern the two module tiers — **L1 primitives** (single cloud resource
|
|
or small group of related resources) and **L2 modules** (compositions
|
|
that reference L1 primitives to deploy a complete stack) — and the
|
|
engine adapter that compiles them to Terraform. They are written for
|
|
**platform engineers** and **AI agents** that author or review new
|
|
modules against the existing corpus (12 L1 primitives and 2 L2 modules
|
|
shipped in v1.8).
|
|
|
|
A module that fails any section below is not ready to publish.
|
|
|
|
## 1. Overview
|
|
|
|
These standards codify the conventions already established by the
|
|
shipped modules (`s3`, `vpc`, `ecs-cluster`, `ecs-service`, `iam-role`,
|
|
`alb`, `ecr`, `cloudfront`, `waf`, `rds`, `kms-key`, `uptime`; the L2
|
|
modules `static-assets` and `microservice`). They exist so that:
|
|
|
|
- platform engineers can review a new module against a fixed checklist;
|
|
- AI agents authoring modules produce code that passes review without
|
|
iteration; and
|
|
- the engine adapter (`adapters/terraform/adapter.py`) can compile a
|
|
module instance with no module-specific code in the adapter beyond the
|
|
three tables in §8.
|
|
|
|
When this document and an existing module disagree, the existing module
|
|
is the authority for v1.x. A change to this document is a MINOR version
|
|
bump of the standards; a change that breaks shipped modules is a MAJOR
|
|
bump and requires a migration plan.
|
|
|
|
## 2. L1 Primitive Standards
|
|
|
|
An L1 primitive is a single cloud resource or a small group of related
|
|
resources (e.g. a VPC with subnets and a route table). It is declared by
|
|
an `interface.json` and realized by the engine adapter; it does not
|
|
own Terraform code.
|
|
|
|
### 2.1 Required files
|
|
|
|
Every L1 primitive MUST contain, at minimum:
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `interface.json` | Angine-agnostic declaration: inputs, outputs, NFRs, optional multi-resource graph. |
|
|
| `instance.json` | A concrete instance used as the adapter regression baseline. |
|
|
| `README.md` | Plain-language documentation following `README-TEMPLATE.md` (see §7). |
|
|
| `examples/simple.yml` | A minimal contract that uses the primitive with required inputs only. |
|
|
| `examples/complex.yml` | A contract that exercises optional inputs, NFRs, and (if applicable) the multi-resource graph. |
|
|
|
|
Directory layout:
|
|
|
|
```
|
|
modules/l1/<name>/
|
|
interface.json
|
|
instance.json
|
|
README.md
|
|
examples/
|
|
simple.yml
|
|
complex.yml
|
|
```
|
|
|
|
### 2.2 interface.json schema
|
|
|
|
`interface.json` MUST be a JSON object with the following required
|
|
fields:
|
|
|
|
| Field | Type | Constraint |
|
|
|-------|------|------------|
|
|
| `name` | string | `^[a-z][a-z0-9-]*$`; MUST match the module folder name. |
|
|
| `version` | string | Semver (`^\d+\d+\.\d+$`); MUST match the registry entry semver. |
|
|
| `kind` | string | Literal `"l1"`. |
|
|
| `type` | string | Stack type in `aws:<service>:<kind>` format (see §2.7). |
|
|
| `description` | string | One or two sentences in plain language; no Terraform jargon. |
|
|
| `inputs` | object | Keyed by input name; each value is an input declaration (§2.3). MAY be empty. |
|
|
| `outputs` | object | Keyed by output name; each value is an output declaration (§2.4). MAY be empty. |
|
|
| `nfrs` | object | Keyed by NFR name; each value is an NFR declaration (§2.5). MUST include `deletion_protection` and `encryption_enabled`. |
|
|
|
|
Optional fields for multi-resource primitives:
|
|
|
|
| Field | Type | Constraint |
|
|
|-------|------|------------|
|
|
| `resources` | array | One entry per distinct cloud resource; see §2.6. |
|
|
| `intra_refs` | array | Internal wiring between resources; see §2.6. |
|
|
|
|
A primitive that creates a single resource (e.g. `s3`, `iam-role`,
|
|
`rds`, `kms-key`) omits `resources` and `intra_refs`; its `type` field
|
|
is the single resource's stack type. A primitive that creates a small
|
|
group of related resources (e.g. `vpc`, `alb`, `cloudfront`) declares
|
|
`resources[]` with one entry per resource and `intra_refs[]` for the
|
|
internal wiring; its `type` field is the *primary* resource's stack
|
|
type.
|
|
|
|
### 2.3 Input declaration
|
|
|
|
Each entry in `inputs` is an object:
|
|
|
|
| Field | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| `type` | string | yes | One of: `string`, `number`, `boolean`, `array`, `object`. |
|
|
| `description` | string | yes | Plain language; no Terraform jargon. |
|
|
| `required` | boolean | yes | `true` if the consumer MUST supply this input. |
|
|
| `default` | (any) | no | Present only when `required` is `false`. MUST match the declared `type`. |
|
|
| `enum` | array | no | Allowed values for `string`/`number` inputs (e.g. RDS `engine`). |
|
|
|
|
`region` is a required `string` input on every primitive that creates a
|
|
regional resource. Global resources (e.g. CloudFront) still declare
|
|
`region` because the provider region is used for child resources (the
|
|
OAC in the `cloudfront` case).
|
|
|
|
Every primitive that holds at-rest data MUST declare an optional
|
|
`kms_key_arn` input (`string`, `required: false`); see §4.
|
|
|
|
### 2.4 Output declaration
|
|
|
|
Each entry in `outputs` is an object:
|
|
|
|
| Field | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| `type` | string | yes | `arn` for ARN outputs; `string` for all others. |
|
|
| `description` | string | yes | Plain language. |
|
|
|
|
Use `arn` (not `string`) for any output that returns an AWS ARN — the
|
|
adapter and policy engine key off the `arn` type to apply ARN-scoped
|
|
rules.
|
|
|
|
### 2.5 NFR declaration
|
|
|
|
Each entry in `nfrs` is an object:
|
|
|
|
| Field | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| `type` | string | yes | One of: `string`, `number`, `boolean`. |
|
|
| `description` | string | yes | Plain language. |
|
|
| `default` | (any) | yes | MUST match the declared `type`. NFRs always have a default. |
|
|
|
|
Mandatory NFRs on every L1:
|
|
|
|
| NFR | Type | Default | Notes |
|
|
|-----|------|---------|-------|
|
|
| `deletion_protection` | boolean | `true` | See §5. |
|
|
| `encryption_enabled` | boolean | `true` | See §4. |
|
|
|
|
A primitive for which an NFR does not conceptually apply (e.g. an IAM
|
|
role has no at-rest data) still declares it with `default: true` and a
|
|
description noting the non-applicability, so the standards check and the
|
|
adapter emit logic stay uniform. The shipped `iam-role` primitive is the
|
|
reference for this case.
|
|
|
|
Additional NFRs are encouraged where they carry operational meaning
|
|
(e.g. `s3.versioning`, `rds.backup_retention_period`,
|
|
`kms-key.enable_rotation`, `vpc.flow_logs_encrypted`). Name them in
|
|
lowercase snake_case.
|
|
|
|
### 2.6 Multi-resource pattern
|
|
|
|
A primitive that creates more than one cloud resource (e.g. `vpc`
|
|
creates `aws_vpc` + `aws_subnet` + `aws_route_table`; `alb` creates
|
|
`aws_lb` + `aws_lb_target_group` + `aws_lb_listener`; `cloudfront`
|
|
creates `aws_cloudfront_distribution` +
|
|
`aws_cloudfront_origin_access_control`) declares a `resources` array.
|
|
|
|
Each `resources[]` entry:
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| `type` | string | The resource's stack type (`aws:<service>:<kind>`). |
|
|
| `description` | string | Plain language. |
|
|
| `inputs` | array | Names (strings) of inputs from the top-level `inputs` object that this resource consumes. |
|
|
| `outputs` | array | Names (strings) of outputs from the top-level `outputs` object that this resource produces. |
|
|
|
|
The top-level `inputs`/`outputs` objects remain the single source of
|
|
truth; `resources[].inputs` and `resources[].outputs` are arrays of
|
|
*names* referencing those objects, not re-declarations.
|
|
|
|
`intra_refs[]` wires outputs of one resource to inputs of another
|
|
within the same primitive. Each entry:
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| `from` | string | `<resource-type>.<output-name>` — the producing side. |
|
|
| `to` | string | `<resource-type>.<input-name>` — the consuming side. |
|
|
|
|
Reference: `cloudfront/interface.json` declares an intra-ref from
|
|
`aws:cloudfront:distribution.oac_id` to
|
|
`aws:cloudfront:originaccesscontrol.oac_id`; `vpc/interface.json`
|
|
declares intra-refs from the subnet and route table to the VPC's
|
|
`vpc_id`.
|
|
|
|
### 2.7 Naming and stack types
|
|
|
|
- Module folder names and `interface.json` `name` values MUST match
|
|
`^[a-z][a-z0-9-]*$` (lowercase, hyphenated, leading letter). Examples:
|
|
`s3`, `ecs-cluster`, `kms-key`, `iam-role`, `uptime`.
|
|
- Input and output names are lowercase snake_case.
|
|
- Stack types follow `aws:<service>:<kind>`:
|
|
- `aws:s3:bucket`
|
|
- `aws:ec2:vpc`, `aws:ec2:subnet`, `aws:ec2:routetable`
|
|
- `aws:ecs:cluster`, `aws:ecs:task_definition`, `aws:ecs:service`,
|
|
`aws:ecs:uptime-service`
|
|
- `aws:iam:role`
|
|
- `aws:elbv2:loadbalancer`, `aws:elbv2:listener`,
|
|
`aws:elbv2:targetgroup`
|
|
- `aws:ecr:repository`
|
|
- `aws:cloudfront:distribution`, `aws:cloudfront:originaccesscontrol`
|
|
- `aws:wafv2:webacl`
|
|
- `aws:rds:instance`
|
|
- `aws:kms:key`, `aws:kms:alias`
|
|
- The engine adapter's `TYPE_MAP` is the registry of stack types the
|
|
adapter can compile (see §8). A new stack type requires a `TYPE_MAP`
|
|
entry before the primitive can be deployed.
|
|
|
|
## 3. L2 Module Standards
|
|
|
|
An L2 module is a composition that references one or more L1 primitives
|
|
to deploy a complete stack (e.g. an ECS Fargate microservice, a static
|
|
asset site behind CloudFront + WAF). It is declared by a
|
|
`composition.json`; it does not own Terraform code and does not have an
|
|
`instance.json`.
|
|
|
|
### 3.1 Required files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `composition.json` | The composition tree: children, wires, outputs, optional features. |
|
|
| `README.md` | Plain-language documentation following `README-TEMPLATE.md` (see §7). |
|
|
| `examples/simple.yml` | A minimal contract that uses the module with required inputs only. |
|
|
| `examples/complex.yml` | A contract that exercises optional inputs and feature flags. |
|
|
|
|
Directory layout:
|
|
|
|
```
|
|
modules/l2/<name>/
|
|
composition.json
|
|
README.md
|
|
examples/
|
|
simple.yml
|
|
complex.yml
|
|
```
|
|
|
|
There is no `instance.json` for an L2 module — the L2 is deployed by
|
|
resolving the composition tree to L1 instances at compile time, not by
|
|
loading a pre-baked instance.
|
|
|
|
### 3.2 composition.json schema
|
|
|
|
`composition.json` MUST be a JSON object with the following fields:
|
|
|
|
| Field | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| `name` | string | yes | `^[a-z][a-z0-9-]*$`; matches the module folder name. |
|
|
| `version` | string | yes | Semver; matches the registry entry. |
|
|
| `kind` | string | yes | Literal `"l2"`. |
|
|
| `depth` | integer | yes | Literal `1` in v1 (see §3.5). |
|
|
| `description` | string | yes | Plain language. |
|
|
| `children` | array | yes | One entry per referenced L1 module (§3.3). |
|
|
| `wires` | array | yes | Wires from contract inputs / child outputs to child inputs / stack outputs (§3.4). |
|
|
| `outputs` | array | yes | Wires from child outputs to stack outputs (§3.4). |
|
|
| `features` | object | no | Feature flags propagated to children by the resolver (§3.6). |
|
|
|
|
### 3.3 Children
|
|
|
|
Each `children[]` entry:
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| `id` | string | The child id, unique within the composition. `^[a-z][a-z0-9-]*$`. The id is the local name used in wires (e.g. `vpc`, `cluster`, `kms`). |
|
|
| `module` | string | `<name>@<semver>` referencing a registered L1 module. |
|
|
|
|
Children MUST reference L1 modules registered in `registry.json` (see
|
|
§6). The referenced semver MUST exist in the registry. An L2 MUST NOT
|
|
reference another L2 (no L3 in v1; see §3.5).
|
|
|
|
Reference: `microservice/composition.json` declares seven children
|
|
(`vpc`, `cluster`, `ecr`, `roles`, `alb`, `service`, `kms`), each
|
|
referencing an L1 at `@1.0.0`.
|
|
|
|
### 3.4 Wire format
|
|
|
|
A wire is a JSON object `{"from": "<source>", "to": "<target>"}` with an
|
|
optional `default` field for contract-input wires.
|
|
|
|
Sources (the `from` side):
|
|
|
|
| Source form | Meaning |
|
|
|-------------|---------|
|
|
| `contract.inputs.<name>` | A value supplied by the consumer's contract YAML. |
|
|
| `<childId>.outputs.<name>` | An output produced by a child L1 module. |
|
|
|
|
Targets (the `to` side):
|
|
|
|
| Target form | Meaning |
|
|
|-------------|---------|
|
|
| `<childId>.inputs.<name>` | An input on a child L1 module. |
|
|
| `stack.outputs.<name>` | A value the L2 exposes as a stack output. |
|
|
|
|
Wires that source from `contract.inputs.<name>` MAY carry a `default`
|
|
value used when the consumer omits the input. Reference:
|
|
`microservice/composition.json` wires `contract.inputs.bucket_name` to
|
|
`vpc.inputs.cidr` with `default: "10.0.0.0/16"` (a historical quirk
|
|
preserved for regression).
|
|
|
|
The `outputs[]` array uses the same wire shape but its `to` is always
|
|
`stack.outputs.<name>` and its `from` is always
|
|
`<childId>.outputs.<name>`.
|
|
|
|
### 3.5 Maximum depth
|
|
|
|
`depth` is `1` for every L2 in v1. The composition tree is strictly L2
|
|
→ L1: an L2 may reference only L1 primitives, never another L2. There
|
|
is no L3 in v1. The stack schema permits `depth` up to 5 for forward
|
|
compatibility, but the v1 resolver and adapter only handle depth 1.
|
|
|
|
### 3.6 Feature flags
|
|
|
|
An L2 MAY declare a `features` object. Two flags are defined in v1:
|
|
|
|
| Flag | Type | Default | Effect |
|
|
|------|------|---------|--------|
|
|
| `deletion_protection` | boolean | `true` | When `true`, the resolver propagates `deletion_protection: true` to every child's NFRs. When `false`, children are deployed with `deletion_protection: false` (used by decommission; see §5). |
|
|
| `uptime_enabled` | boolean | `true` | When `true`, the uptime monitoring L1 is deployed after the L2 module in a separate terraform state. When `false`, the uptime deployment is skipped. |
|
|
|
|
Feature flags are propagated to children by the resolver; the L2
|
|
`composition.json` does not need to wire them explicitly as inputs. The
|
|
resolver reads `features` and injects the corresponding NFR/input on
|
|
each child.
|
|
|
|
## 4. Encryption by Default
|
|
|
|
Encryption is mandatory and on by default across the platform.
|
|
|
|
1. Every L1 MUST declare an `encryption_enabled` NFR (boolean, default
|
|
`true`) in `interface.json`. See §2.5.
|
|
2. Every L1 that holds at-rest data (S3, RDS, ECR, ECS task
|
|
definition env, VPC flow logs, CloudWatch log groups) MUST declare an
|
|
optional `kms_key_arn` input (`string`, `required: false`). When
|
|
supplied, the adapter wires it to the resource's KMS encryption
|
|
argument.
|
|
3. L2 modules MUST wire a per-stack customer-managed KMS key to all
|
|
children that accept `kms_key_arn`. The KMS key is a `kms-key` child
|
|
of the L2 — one key per L2 deployment, no shared keys. Reference:
|
|
both `static-assets` and `microservice` declare a `kms` child
|
|
(`kms-key@1.0.0`) and wire `kms.outputs.kms_key_arn` to every child
|
|
that accepts a CMK.
|
|
4. For a standalone L1 deployment (an L1 used outside an L2), if the
|
|
consumer does not supply `kms_key_arn`, the adapter falls back to the
|
|
AWS-managed default key for that service and emits a warning to
|
|
stderr. The primitive is still encrypted; only the key manager
|
|
differs.
|
|
5. The `kms-key` primitive enables key rotation by default
|
|
(`enable_rotation` NFR, default `true`), and the adapter emits
|
|
`enable_key_rotation = true` on the `aws_kms_key` resource.
|
|
|
|
A primitive that does not hold at-rest data (e.g. `iam-role`,
|
|
`ecs-cluster`, `alb`) still declares `encryption_enabled` for standards
|
|
uniformity (see §2.5) but does not declare `kms_key_arn`.
|
|
|
|
## 5. Deletion Protection by Default
|
|
|
|
Deletion protection is mandatory and on by default to prevent
|
|
accidental teardown of production infrastructure.
|
|
|
|
1. Every L1 MUST declare a `deletion_protection` NFR (boolean, default
|
|
`true`) in `interface.json`. See §2.5.
|
|
2. When `deletion_protection` is `true`, the engine adapter emits a
|
|
`lifecycle { prevent_destroy = true }` block on the corresponding
|
|
Terraform resource. A `terraform destroy` against a protected
|
|
resource fails with an error naming the resource.
|
|
3. L2 modules expose `features.deletion_protection` (default `true`).
|
|
The resolver propagates the flag to every child's NFRs (see §3.6).
|
|
4. **Decommission mode.** To tear down a stack that was deployed with
|
|
deletion protection, the consumer sets `inputs.deletion_protection:
|
|
false` on the contract (or `features.deletion_protection: false` on
|
|
an L2) and re-applies. The decommission transform
|
|
(`decommission_transform`) zeroes capacity counts (e.g. ECS desired
|
|
count to 0, RDS allocated storage to the minimum) so that the
|
|
subsequent `destroy` applies against a quiesced stack. The transform
|
|
is applied by the resolver before the adapter emits resources.
|
|
|
|
## 6. Registry
|
|
|
|
Every module — L1 and L2 — MUST be registered in
|
|
`modules/registry.json` at its semver. The registry is the source of
|
|
truth for what is published; the adapter and resolver refuse to compile
|
|
a module that is not registered.
|
|
|
|
Registry entry shape:
|
|
|
|
```json
|
|
{
|
|
"<module-name>": {
|
|
"<semver>": {
|
|
"interface": "modules/<l1|l2>/<module-name>/<interface.json|composition.json>",
|
|
"published_at": "<ISO 8601 timestamp>",
|
|
"deprecated": false
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- `interface` is the path (relative to the repo root) to the module's
|
|
interface file — `interface.json` for an L1, `composition.json` for
|
|
an L2.
|
|
- `published_at` is an ISO 8601 timestamp. Use a full
|
|
`YYYY-MM-DDTHH:MM:SSZ` form; do not omit the seconds or the timezone
|
|
designator.
|
|
- `deprecated` is `false` for a live module. A MAJOR version bump does
|
|
not delete the old entry; it flips `deprecated` to `true` and starts a
|
|
12-month deprecation window (see §7 Versioning).
|
|
|
|
A new semver of an existing module is a new key under the module's
|
|
object; old semvers are retained. The registry is append-only for
|
|
published semvers — a published semver is never edited or deleted.
|
|
|
|
## 7. README Standards
|
|
|
|
Every module README MUST follow the structure of
|
|
`modules/README-TEMPLATE.md`. Required sections, in order:
|
|
|
|
1. `# <name> — <plain-language description>` — title with the module
|
|
name and a one-line description.
|
|
2. `## Overview` — one or two sentences in plain language.
|
|
3. `## Resources` — a table of the Terraform resources the module
|
|
creates (L1) or the primitives it references (L2).
|
|
4. `## Inputs` — a table: `| Name | Type | Required | Default | Description |`.
|
|
5. `## Outputs` — a table: `| Name | Type | Description |`.
|
|
6. `## NFRs` — a table: `| Name | Type | Default | Description |`.
|
|
`deletion_protection` and `encryption_enabled` are mandatory NFRs
|
|
for every L1; they MUST appear in this table.
|
|
7. `## Usage` — a concrete snippet showing how a consumer references
|
|
the module in a contract.
|
|
8. `## Compliance extension points` — resources or behaviors that could
|
|
be added for the future compliance milestone (GDPR, SOX, SOC2,
|
|
DORA). Not implemented yet; listed so the redesign can plan for them.
|
|
9. `## Examples` — links to `examples/simple.yml` and
|
|
`examples/complex.yml` with a one-line description of each.
|
|
10. `## Versioning` — the module's semver policy: interface MAJOR,
|
|
behavior MINOR, lifecycle PATCH. MAJOR bumps require a new
|
|
`registry.json` entry (immutable publication); old entries enter a
|
|
12-month deprecation window.
|
|
|
|
An L2 README's `## Resources` section lists the referenced L1 children
|
|
rather than Terraform resources, and its `## Inputs`/`## Outputs`
|
|
sections reflect the contract inputs and stack outputs of the
|
|
composition.
|
|
|
|
## 8. Stateless Assembler Pattern
|
|
|
|
The Terraform adapter (`adapters/terraform/adapter.py`) is a **stateless
|
|
assembler** (~80 lines). It owns no module content — no resource shape, no
|
|
nested HCL blocks, no defaults, no type-specific logic. It reads the
|
|
registry to find each L1 module's `terraform/` dir, then emits a root
|
|
`main.tf` that instantiates each resource as a
|
|
`module "<rid>" { source = ... }` block with resolved inputs and wired refs.
|
|
|
|
Engine-specific knowledge (resource type, arg names, nested blocks,
|
|
defaults, NFRs) lives in the per-module `terraform/` subdir, NOT in the
|
|
adapter. `interface.json` stays engine-agnostic (the contract); the
|
|
`terraform/` dir is the engine binding. A future Azure adapter would add
|
|
an `azure/` subdir per module without touching `interface.json`.
|
|
|
|
### 8.1 Per-module terraform dir
|
|
|
|
Each L1 module ships a `terraform/` subdir:
|
|
|
|
```
|
|
modules/l1/<name>/terraform/
|
|
├── versions.tf # required_version + required_providers (aws ~> 5.0)
|
|
├── variables.tf # one variable {} per interface.json input
|
|
├── locals.tf # HEAVY: centralizes var-vs-default interpolation
|
|
├── main.tf # resource {} blocks referencing locals (not vars directly)
|
|
└── outputs.tf # one output {} per interface.json output
|
|
```
|
|
|
|
**`locals.tf` is the key file.** Every default that was previously
|
|
hardcoded in the adapter (CIDR blocks, assume_role_policy JSON, ECR/logs
|
|
inline policy, Fargate requires_compatibilities, assign_public_ip,
|
|
listener/target ports) moves here as a `locals` block that interpolates
|
|
the variable against its sensible default:
|
|
|
|
```hcl
|
|
locals {
|
|
cidr_block = var.cidr != null ? var.cidr : "10.0.0.0/16"
|
|
assume_role_policy = var.assume_role_policy != null ? var.assume_role_policy : jsonencode({ ... })
|
|
}
|
|
```
|
|
|
|
`main.tf` stays clean — pure resource blocks referencing `local.*`, never
|
|
interpolating vars directly. Trivial single-resource modules (e.g.
|
|
`kms-key`, `ecr`) may inline locals in `main.tf`; multi-resource modules
|
|
get the full 5-file split.
|
|
|
|
### 8.2 How the adapter assembles
|
|
|
|
Given a resolved stack instance, the adapter:
|
|
|
|
1. Reads `modules/registry.json` → builds a `module_name → terraform_dir` map.
|
|
2. For each resource, extracts the module name from the resource's `module`
|
|
field (e.g. `s3@1.0.0` → `s3`), looks up `terraform_dir`, and emits a
|
|
`module "<rid>" { source = "<absolute terraform_dir>" ... }` block.
|
|
3. Passes each input (except `region`, which is provider-level) as a module
|
|
argument. For `ref:<rid>.<output>` values, emits
|
|
`module.<rid>.<output>` interpolations (terraform-native module outputs).
|
|
4. Emits root `output {}` blocks wiring module outputs to stack outputs.
|
|
5. Emits `providers.tf` (aws provider, region from the first resource) +
|
|
`terraform.tf` (required_version + required_providers + S3 backend).
|
|
|
|
The adapter owns NO resource shape, NO nested blocks, NO defaults, NO
|
|
type-specific logic. It only assembles module instantiations and wires refs.
|
|
|
|
### 8.3 Adding a new L1
|
|
|
|
When a new L1 primitive is added:
|
|
|
|
1. Author the `terraform/` subdir (`versions.tf`/`variables.tf`/`locals.tf`/
|
|
`main.tf`/`outputs.tf`) with the resource shape, nested blocks, and
|
|
defaults. Defaults go in `locals.tf` (heavy interpolation of vars against
|
|
sensible defaults).
|
|
2. Add a `terraform_dir` field to the module's `registry.json` entry.
|
|
3. Author `interface.json` (engine-agnostic), `instance.json` (regression
|
|
baseline), `README.md`, and `examples/{simple,complex}.yml`.
|
|
|
|
**No adapter code changes.** The adapter is generic; it assembles any
|
|
module that has a `terraform_dir` in the registry.
|
|
|
|
## 9. Code Review Checklist
|
|
|
|
Use this checklist when reviewing a new module (L1 or L2). Every box
|
|
must be checked before the module is registered and published.
|
|
|
|
### 9.1 Files and structure
|
|
|
|
- [ ] All required files present:
|
|
- L1: `interface.json`, `instance.json`, `README.md`,
|
|
`examples/simple.yml`, `examples/complex.yml`,
|
|
`terraform/` (versions.tf, variables.tf, locals.tf, main.tf, outputs.tf).
|
|
- L2: `composition.json`, `README.md`, `examples/simple.yml`,
|
|
`examples/complex.yml` (no `instance.json`, no `terraform/`).
|
|
- [ ] `interface.json` (L1) / `composition.json` (L2) validates against
|
|
`schemas/stack.schema.json`.
|
|
- [ ] `examples/simple.yml` and `examples/complex.yml` validate
|
|
against `schemas/contract.schema.json`.
|
|
- [ ] Module registered in `modules/registry.json` at its semver with a
|
|
full ISO 8601 `published_at` and `deprecated: false`.
|
|
|
|
### 9.2 Interface (L1)
|
|
|
|
- [ ] `name` matches the folder name and `^[a-z][a-z0-9-]*$`.
|
|
- [ ] `version` is semver and matches the registry entry.
|
|
- [ ] `kind` is `"l1"`.
|
|
- [ ] `type` follows `aws:<service>:<kind>`.
|
|
- [ ] Every input has `type`, `description`, `required`; optional inputs
|
|
carry a `default` of the correct type; `enum` present where the value
|
|
set is constrained.
|
|
- [ ] Every output has `type` (`arn` for ARNs, `string` otherwise) and
|
|
`description`.
|
|
- [ ] `nfrs` includes `deletion_protection` (boolean, default `true`)
|
|
and `encryption_enabled` (boolean, default `true`).
|
|
- [ ] `kms_key_arn` input present if the primitive holds at-rest data.
|
|
- [ ] Multi-resource primitives declare `resources[]` (with `inputs`/
|
|
`outputs` as arrays of names) and `intra_refs[]` with `{from, to}`.
|
|
|
|
### 9.3 Composition (L2)
|
|
|
|
- [ ] `kind` is `"l2"` and `depth` is `1`.
|
|
- [ ] Every `children[]` entry is `{id, module}` with `module` in
|
|
`<name>@<semver>` form referencing a registered L1.
|
|
- [ ] No child references an L2 (no L3 in v1).
|
|
- [ ] `wires[]` use the `contract.inputs.<name>` /
|
|
`<childId>.outputs.<name>` → `<childId>.inputs.<name>` /
|
|
`stack.outputs.<name>` forms.
|
|
- [ ] `outputs[]` use `<childId>.outputs.<name>` →
|
|
`stack.outputs.<name>`.
|
|
- [ ] A `kms` child (`kms-key@<semver>`) is present and its
|
|
`kms_key_arn` output is wired to every child that accepts a CMK.
|
|
- [ ] `features` (if present) only uses defined flags
|
|
(`deletion_protection`, `uptime_enabled`).
|
|
|
|
### 9.4 Adapter (stateless assembler)
|
|
|
|
- [ ] The new primitive's `terraform/` subdir exists with
|
|
`versions.tf`/`variables.tf`/`locals.tf`/`main.tf`/`outputs.tf` and
|
|
passes `terraform init + validate` standalone.
|
|
- [ ] `registry.json` has a `terraform_dir` field for the new primitive.
|
|
- [ ] No adapter code changes are needed (the adapter is generic; it
|
|
assembles any module with a `terraform_dir` in the registry).
|
|
- [ ] The new primitive's `instance.json` round-trips through the
|
|
adapter without error (regression baseline — the adapter emits a root
|
|
`main.tf` with a `module "<rid>" { source = ... }` block).
|
|
|
|
### 9.5 README and docs
|
|
|
|
- [ ] README follows `README-TEMPLATE.md` with all required sections in
|
|
order (§7).
|
|
- [ ] `## NFRs` table lists `deletion_protection` and
|
|
`encryption_enabled` for an L1.
|
|
- [ ] `## Compliance extension points` lists at least one plausible
|
|
future extension.
|
|
|
|
### 9.6 Tests
|
|
|
|
- [ ] A test is added for the new primitive covering adapter emission
|
|
(the Terraform output for `instance.json` matches the expected
|
|
fixture) and interface validation (`interface.json` validates against
|
|
`stack.schema.json`).
|
|
- [ ] For an L2, a test is added that the composition resolves to the
|
|
expected set of L1 instances and that the adapter emits a root module
|
|
calling the L1 modules. |