# Nova Modules Nova ships a two-tier module library. Modules are **engine-agnostic**: their contract is declared in `interface.json` (stack types like `aws:s3:bucket`), and an adapter translates the contract to a concrete IaC engine (Terraform today; Pulumi/CDK possible later). All L1 modules in this repo ship a Terraform adapter under `terraform/`. ## L1 vs L2 | Tier | What it is | Composes | Examples | |------|----------------------------------------------------------------------------|---------------------|-----------------------------------| | L1 | A single primitive resource (or tightly-coupled resource group) on a cloud | One stack resource | `s3`, `vpc`, `ecs-cluster`, `alb` | | L2 | A composition of L1s expressing an architectural pattern | Multiple L1 modules | `microservice`, `static-assets` | - **L1** = one entry in the flat stack. Even multi-resource L1s (e.g. `vpc`, `ecs-service`, `alb`) emit a single stack entry; their `interface.json` lists the child resources in a `resources[]` array for documentation, but the resolver does **not** expand them (D-012). - **L2** = also one opaque entry in the flat stack. The L2's `terraform/main.tf` composes L1 modules internally via `module` blocks (D-012). The L2 exposes its own L2-level `inputs`/`outputs`; children and wiring live in terraform, not in the interface. ## Registry format `modules/registry.json` maps `module_name -> version -> entry`: ```json { "s3": { "1.0.0": { "interface": "modules/l1/s3/interface.json", "terraform_dir": "modules/l1/s3/terraform", "published_at": "2026-08-20T00:00:00Z", "deprecated": false, "kind": "l1" } } } ``` - `interface` — path to the `interface.json` declaring the contract. - `terraform_dir` — path to the adapter's Terraform module directory (the flat stack's `source` field). - `kind` — `"l1"` or `"l2"`. - `deprecated` — when `true`, the resolver warns and selects the latest non-deprecated version unless the caller pins a version. ## interface.json shape (D-014) ```json { "name": "s3", "version": "1.0.0", "kind": "l1", "type": "aws:s3:bucket", "description": "...", "inputs": { "": { "type": "...", "required": true, "description": "..." } }, "outputs": { "": { "type": "...", "description": "..." } }, "resources": [ { "type": "aws:ec2:vpc", "inputs": [...], "outputs": [...] } ] } ``` - `type` is **stack-typed** — `aws::` (e.g. `aws:s3:bucket`), **not** the Terraform resource name (`aws_s3_bucket`). The adapter performs the translation. - `resources[]` is present only on multi-resource L1s (`vpc`, `ecs-service`, `alb`); it documents the child stack types but does not drive resolution. - **Dropped** per D-014: `nfrs` (confidence signal, out of scope) and `intra_refs` (wire engine, eliminated by D-012). Do not re-add them. ## Conventions shared by all L1 Terraform adapters - `terraform/versions.tf` pins `required_version = ">= 1.9, < 1.10"` and `aws ~> 5.0`. - Every resource is guarded by `count = var.enabled ? 1 : 0`; the `enabled` input defaults to `true`. - `locals.tf` merges module-default tags with caller-supplied `var.tags`: ```hcl tags = merge({ "nova:owner" = "nova", "nova:environment" = "dev" }, var.tags) ``` - Every `interface.json` input has a matching `variable` block; every output has a matching `output` block. Outputs return `null` (or `[]`) when `enabled = false`. ## How to add a module 1. Pick the tier. New primitive → L1. New pattern composing existing L1s → L2. 2. Create `modules/l1//` (or `modules/l2//`). 3. Author `interface.json` (L1) or `interface.json` + L2 terraform that composes L1s via `module` blocks. Use `modules/README-TEMPLATE.md` as the per-module doc template. 4. Author `terraform/{main,variables,outputs,versions,locals}.tf` following the conventions above. 5. Add an entry to `modules/registry.json` and a row to the catalog at `docs/modules/index.md`. 6. Verify: `python3 -c "import json; json.load(open('modules/l1//interface.json'))"` and `terraform validate` inside `terraform/`. ## L1 primitives (13) | Module | Stack type | Multi-resource? | Description | |-----------------|-------------------------------|-----------------|----------------------------------------------------------| | `s3` | `aws:s3:bucket` | no | S3 bucket with versioning + SSE-KMS | | `vpc` | `aws:ec2:vpc` | yes | VPC + subnets + route table + IGW | | `ecs-cluster` | `aws:ecs:cluster` | no | ECS cluster | | `ecs-service` | `aws:ecs:service` | yes | ECS task definition + service | | `iam-role` | `aws:iam:role` | no | IAM role with assume-role policy | | `alb` | `aws:alb` | yes | ALB + target group + listener | | `ecr` | `aws:ecr:repository` | no | ECR repository with scan-on-push | | `cloudfront` | `aws:cloudfront:distribution` | no | CloudFront distribution with a single origin | | `waf` | `aws:waf:web_acl` | no | WAFv2 web ACL (regional, default allow) | | `rds` | `aws:rds:instance` | no | RDS Postgres DB instance | | `kms-key` | `aws:kms:key` | no | KMS CMK with alias | | `dynamodb` | `aws:dynamodb:table` | no | DynamoDB table (PAY_PER_REQUEST default) | | `uptime` | `aws:uptime:monitor` | no | Uptime monitor (CloudWatch alarm stand-in) | ## L2 compositions (2) | Module | Composes | Description | |------------------|-------------------------------------------|----------------------------------------------| | `microservice` | vpc + ecs-cluster + ecs-service + iam-role + ecr + alb | Container microservice with public ALB (D-038) | | `static-assets` | s3 + cloudfront + kms-key | Static site fronted by CloudFront (D-038) |