3 Commits

Author SHA1 Message Date
CIAgent 11e342a9f7 docs(P03): complete l1-primitives-registry phase
---ci---
project: nova-platform
phase: 3
milestone: v1.0
status: complete
---/ci---

P3 complete: 13 L1 primitives + registry + module docs.
68 tests pass. REQ-10,11,13,34 covered.
2026-08-24 17:41:43 +00:00
CIAgent 14be01d971 docs(P02): complete terraform-adapter-engine-boundary phase
---ci---
project: nova-platform
phase: 2
milestone: v1.0
status: complete
---/ci---

P2 complete: adapter (loads registry D-037), engine-boundary test (D-034).
58 tests pass. REQ-07,08,09,25,26 + REQ-10 (registry) covered.
2026-08-24 17:33:22 +00:00
CIAgent 97691fd752 docs(P01): complete contract-surface-schemas-resolver phase
---ci---
project: nova-platform
phase: 1
milestone: v1.0
status: complete
phase_role: execution
---/ci---

P1 complete: contract schema, stack schema, environment schema,
resolver, env_check, 10 sample contracts, 36 tests. REQ-01..06,
23..28 covered.
2026-08-24 17:31:00 +00:00
125 changed files with 3226 additions and 13 deletions
+14 -13
View File
@@ -1,19 +1,20 @@
{
"phase": 0,
"stage": "grill",
"phase": 3,
"stage": "verify",
"milestone": "v1.0",
"phase_role": "pre_execution",
"phase_role": "execution",
"attempts": 0,
"updated_at": "2026-08-20T18:30:00Z",
"updated_at": "2026-08-20T19:45:00Z",
"project": "nova-platform",
"milestone_branch": "milestone/v1.0-nova-platform",
"phase_branch": "phase/00-pre-execution",
"specify_validated": true,
"requirements_count": 38,
"clarify_decisions": "D-011..D-038",
"clarify_escalated": ["D-017", "D-025", "D-030"],
"grill_verdict": "PROCEED (conditions resolved)",
"grill_confidence": 0.82,
"grill_conditions": ["C-1 D-037", "C-2 D-038", "C-3 docs-grep", "C-4 reorder", "C-5 concurrency-note", "C-6 deviation-claim-fix"],
"mvp_ux_check": "PASS"
"phase_branch": "phase/03-l1-primitives-registry",
"phase_0_ship": {"tag": "v0.1.0", "local_only": true},
"phase_1_ship": {"tag": "v0.1.1", "local_only": true},
"phase_2_ship": {"tag": "v0.1.2", "local_only": true},
"phase_3_verify": {
"tests_pass": true,
"tests_count": 68,
"reqs_covered": ["REQ-10", "REQ-11", "REQ-13", "REQ-34"]
},
"next_phase": "phase/04-l2-patterns-bootstrap-platform"
}
View File
+3
View File
@@ -0,0 +1,3 @@
from adapters.terraform.adapter import adapt
__all__ = ["adapt"]
+102
View File
@@ -0,0 +1,102 @@
"""Nova Platform — Terraform Adapter.
The ONLY engine-specific code in the platform (per REQ-09, verified by
tests/test_engine_boundary.py). Loads modules/registry.json internally
to map module -> terraform_dir (per D-037/C-1 grill fix — the resolver
does NOT put a `source` field in the stack; the adapter resolves it
here, inside the engine boundary).
Stateless assembler: no `terraform` CLI invocation, no state files, no
plan files. Emits Terraform HCL: one `module "x" { source = ...; <inputs> }`
block per stack resource.
"""
import json
import os
from pathlib import Path
def _load_registry(repo_root):
"""Load modules/registry.json -> {module_name: terraform_dir}."""
registry_path = os.path.join(str(repo_root), "modules", "registry.json")
with open(registry_path) as fh:
registry = json.load(fh)
return {name: list(versions.values())[0].get("terraform_dir")
for name, versions in registry.items()
if list(versions.values())[0].get("terraform_dir")}
def _tf_value(value):
"""Render a Python value as an HCL expression."""
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, list):
return "[" + ", ".join(_tf_value(v) for v in value) + "]"
if isinstance(value, dict):
return "{ " + ", ".join(f"{k} = {_tf_value(v)}" for k, v in value.items()) + " }"
return json.dumps(str(value))
def _emit_module_block(resource, terraform_dirs, repo_root):
"""Emit one `module "x" { source = ...; <inputs> }` block."""
module_name = resource["module"]
rid = module_name.replace("-", "_")
tf_dir = terraform_dirs.get(module_name)
if tf_dir is None:
raise ValueError(f"module '{module_name}' has no terraform_dir in registry")
source = os.path.join(str(repo_root), tf_dir)
lines = [f'module "{rid}" {{', f' source = "{source}"']
for key, val in resource.get("inputs", {}).items():
if key == "region":
continue
lines.append(f" {key} = {_tf_value(val)}")
lines.append("}")
return "\n".join(lines)
def adapt(stack, repo_root):
"""Compile a flat stack dict to Terraform HCL.
Args:
stack: a flat stack dict conforming to schemas/stack.schema.json
(NO `source` field per D-037 — the adapter resolves
module -> terraform_dir via the registry).
repo_root: Path to the repo root (the adapter loads
modules/registry.json from here).
Returns:
A string of Terraform HCL with one `module "x" {}` block per
stack resource.
"""
terraform_dirs = _load_registry(repo_root)
blocks = []
for resource in stack.get("resources", []):
blocks.append(_emit_module_block(resource, terraform_dirs, repo_root))
return "\n\n".join(blocks) + "\n"
def main(argv=None):
import sys
argv = argv or sys.argv[1:]
if len(argv) < 1:
print("usage: adapter.py <stack.json> [out.tf]", file=sys.stderr)
return 2
stack_path = argv[0]
out_path = argv[1] if len(argv) > 1 else None
repo_root = Path(__file__).resolve().parent.parent.parent
with open(stack_path) as fh:
stack = json.load(fh)
hcl = adapt(stack, repo_root)
if out_path:
with open(out_path, "w") as fh:
fh.write(hcl)
else:
print(hcl)
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
+9
View File
@@ -0,0 +1,9 @@
id: msvc
name: Microservice
environment: dev
infrastructure:
- module: microservice
version: "1.0.0"
inputs:
service_name: "${env.environment}-${contract.id}-svc"
desired_count: 2
+9
View File
@@ -0,0 +1,9 @@
id: msvc
name: Microservice
environment: dr
infrastructure:
- module: microservice
version: "1.0.0"
inputs:
service_name: "${env.environment}-${contract.id}-svc"
desired_count: 2
+9
View File
@@ -0,0 +1,9 @@
id: msvc
name: Microservice
environment: prod
infrastructure:
- module: microservice
version: "1.0.0"
inputs:
service_name: "${env.environment}-${contract.id}-svc"
desired_count: 2
+9
View File
@@ -0,0 +1,9 @@
id: msvc
name: Microservice
environment: qa
infrastructure:
- module: microservice
version: "1.0.0"
inputs:
service_name: "${env.environment}-${contract.id}-svc"
desired_count: 2
+9
View File
@@ -0,0 +1,9 @@
id: msvc
name: Microservice
environment: dev
infrastructure:
- module: microservice
version: "1.0.0"
inputs:
service_name: "${env.environment}-${contract.id}-svc"
desired_count: 2
+9
View File
@@ -0,0 +1,9 @@
id: stsi
name: Static Assets Site
environment: dev
infrastructure:
- module: static-assets
version: "1.0.0"
inputs:
bucket_name: "${env.environment}-${contract.id}-assets"
index_document: index.html
+9
View File
@@ -0,0 +1,9 @@
id: stsi
name: Static Assets Site
environment: dr
infrastructure:
- module: static-assets
version: "1.0.0"
inputs:
bucket_name: "${env.environment}-${contract.id}-assets"
index_document: index.html
+9
View File
@@ -0,0 +1,9 @@
id: stsi
name: Static Assets Site
environment: prod
infrastructure:
- module: static-assets
version: "1.0.0"
inputs:
bucket_name: "${env.environment}-${contract.id}-assets"
index_document: index.html
+9
View File
@@ -0,0 +1,9 @@
id: stsi
name: Static Assets Site
environment: qa
infrastructure:
- module: static-assets
version: "1.0.0"
inputs:
bucket_name: "${env.environment}-${contract.id}-assets"
index_document: index.html
+9
View File
@@ -0,0 +1,9 @@
id: stsi
name: Static Assets Site
environment: dev
infrastructure:
- module: static-assets
version: "1.0.0"
inputs:
bucket_name: "${env.environment}-${contract.id}-assets"
index_document: index.html
View File
+180
View File
@@ -0,0 +1,180 @@
"""Nova Platform — Contract Resolver.
Resolves a validated consumer contract to a Stack instance (a flat dict
conforming to schemas/stack.schema.json).
Flow:
1. Validate the contract dict against schemas/contract.schema.json.
2. Load the environment via core.environment_check.check().
3. Build an interpolation context {'env': env, 'contract': contract}.
4. For each infrastructure entry: look up the module + version in the
registry, interpolate ${env.*} / ${contract.*} tokens in inputs,
and emit a flat stack resource {module, version, inputs}.
5. Return the stack dict.
Engine-agnostic: no aws_*, no Terraform terms, no module paths. The stack
carries NO 'source' field (D-037/C-1 grill fix) — the adapter loads the
registry to map module -> terraform_dir. L2 is opaque (D-012): a single
stack resource, no children/wires expansion.
"""
import json
import re
from pathlib import Path
import jsonschema
import yaml
_TOKEN_RE = re.compile(r"\$\{([a-zA-Z_][a-zA-Z0-9_.]*)\}")
class ModuleNotFoundError(KeyError):
"""Raised when a contract references a module not in the registry."""
class VersionNotFoundError(KeyError):
"""Raised when a contract references a version not in the registry."""
def _lookup_dotted(context, dotted):
parts = dotted.split(".")
cur = context
for part in parts:
if isinstance(cur, dict) and part in cur:
cur = cur[part]
else:
raise KeyError(dotted)
return cur
def _expand_vars(value, context):
if isinstance(value, str):
def _replace(match):
token = match.group(1)
try:
resolved = _lookup_dotted(context, token)
except KeyError:
raise ValueError(f"unresolved interpolation token: ${{{token}}}")
if isinstance(resolved, (dict, list)):
return json.dumps(resolved)
return str(resolved)
return _TOKEN_RE.sub(_replace, value)
if isinstance(value, dict):
return {k: _expand_vars(v, context) for k, v in value.items()}
if isinstance(value, list):
return [_expand_vars(v, context) for v in value]
return value
def _latest_version(registry, module_name):
versions = registry[module_name]
non_deprecated = [(v, e) for v, e in versions.items()
if not e.get("deprecated", False)]
if not non_deprecated:
non_deprecated = list(versions.items())
non_deprecated.sort(key=lambda x: [int(p) for p in x[0].split(".")],
reverse=True)
return non_deprecated[0][0]
def _load_schema(path):
with open(path) as fh:
return json.load(fh)
def resolve(contract, registry, modules_dir, environments_dir=None,
repo_root=None):
"""Resolve a validated contract dict to a flat Stack dict.
Args:
contract: validated contract dict (must conform to
schemas/contract.schema.json).
registry: modules/registry.json loaded as a dict.
modules_dir: Path to the modules/ directory (unused for L2-opaque
resolution but kept per D-011 for future interface.json reads).
environments_dir: Path to core/environments/. If None, derived from
repo_root / 'core' / 'environments'.
repo_root: Path to the repo root. If None, derived from modules_dir
parent's parent (modules_dir is <root>/modules).
Returns:
A flat stack dict conforming to schemas/stack.schema.json:
{contract_id, contract_name, environment, resources: [{module,
version, inputs}]}.
Raises:
ModuleNotFoundError: contract references an unknown module.
VersionNotFoundError: contract references an unknown version.
jsonschema.ValidationError: contract does not conform to schema.
ValueError: unresolved interpolation token.
"""
if repo_root is None:
repo_root = Path(modules_dir).parent.parent
if environments_dir is None:
environments_dir = Path(repo_root) / "core" / "environments"
contract_schema_path = Path(repo_root) / "schemas" / "contract.schema.json"
contract_schema = _load_schema(contract_schema_path)
jsonschema.validate(contract, contract_schema)
from core import environment_check
env = environment_check.check(contract["environment"], environments_dir)
# Expose 'environment' as an alias for the env's 'name' field so
# ${env.environment} resolves (the env JSON uses 'name', but contracts
# reference the environment by ${env.environment}).
env["environment"] = env.get("name", contract["environment"])
context = {"env": env, "contract": contract}
resources = []
for item in contract["infrastructure"]:
module_name = item["module"]
if module_name not in registry:
raise ModuleNotFoundError(module_name)
version = item.get("version")
if version is None:
version = _latest_version(registry, module_name)
elif version not in registry[module_name]:
raise VersionNotFoundError(f"{module_name}@{version}")
inputs = _expand_vars(item.get("inputs", {}), context)
resources.append({
"module": module_name,
"version": version,
"inputs": inputs,
})
return {
"contract_id": contract["id"],
"contract_name": contract["name"],
"environment": contract["environment"],
"resources": resources,
}
def main(argv=None):
import sys
argv = argv or sys.argv[1:]
if len(argv) < 2:
print("usage: contract_resolver.py <contract.yaml> [out.json]",
file=sys.stderr)
return 2
contract_path = argv[0]
out_path = argv[1] if len(argv) > 1 else None
repo_root = Path(__file__).resolve().parent.parent
with open(contract_path) as fh:
contract = yaml.safe_load(fh)
with open(repo_root / "modules" / "registry.json") as fh:
registry = json.load(fh)
stack = resolve(contract, registry, repo_root / "modules",
repo_root=repo_root)
if out_path:
with open(out_path, "w") as fh:
json.dump(stack, fh, indent=2)
else:
print(json.dumps(stack, indent=2))
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
+37
View File
@@ -0,0 +1,37 @@
"""Nova Platform — Environment Check.
Loads and validates a platform-managed environment JSON file.
Simplified per D-019: check(env_name, environments_dir) -> dict, raises
EnvironmentNotFoundError on missing env. Drops the reference's
(ok, message) tuple, _onboarding_message, and main() CLI.
"""
import json
from pathlib import Path
class EnvironmentNotFoundError(FileNotFoundError):
"""Raised when a named environment has no JSON file."""
def check(env_name, environments_dir):
"""Load and return the environment dict for env_name.
Args:
env_name: environment name (dev, qa, prod, dr).
environments_dir: Path to the core/environments/ directory.
Returns:
The parsed environment dict.
Raises:
EnvironmentNotFoundError: no <env_name>.json in environments_dir.
"""
env_path = Path(environments_dir) / f"{env_name}.json"
if not env_path.exists():
raise EnvironmentNotFoundError(
f"environment '{env_name}' not found at {env_path}")
with open(env_path) as fh:
env = json.load(fh)
return env
+14
View File
@@ -0,0 +1,14 @@
{
"name": "dev",
"description": "Sample dev environment for offline/local testing. account_id placeholder (000000000000) for offline mode.",
"account_id": "000000000000",
"region": "us-east-1",
"state_backend": {
"bucket": "nova-tfstate-dev-us-east-1",
"lock_table": "nova-tfstate-locks"
},
"network": {
"vpc_cidr": "10.0.0.0/16",
"azs": ["us-east-1a", "us-east-1b"]
}
}
+36
View File
@@ -0,0 +1,36 @@
# Module Catalog
Every module's full documentation lives next to its code under
`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md` (per
D-028). This page is the index: it lists the available modules and
links to their per-module docs.
## L1 primitives (13)
| Module | Stack type | Multi-resource? | Docs |
|-----------------|-------------------------------|-----------------|-----------------------------------------------|
| `s3` | `aws:s3:bucket` | no | [modules/l1/s3/README.md](../../modules/l1/s3/README.md) |
| `vpc` | `aws:ec2:vpc` | yes | [modules/l1/vpc/README.md](../../modules/l1/vpc/README.md) |
| `ecs-cluster` | `aws:ecs:cluster` | no | [modules/l1/ecs-cluster/README.md](../../modules/l1/ecs-cluster/README.md) |
| `ecs-service` | `aws:ecs:service` | yes | [modules/l1/ecs-service/README.md](../../modules/l1/ecs-service/README.md) |
| `iam-role` | `aws:iam:role` | no | [modules/l1/iam-role/README.md](../../modules/l1/iam-role/README.md) |
| `alb` | `aws:alb` | yes | [modules/l1/alb/README.md](../../modules/l1/alb/README.md) |
| `ecr` | `aws:ecr:repository` | no | [modules/l1/ecr/README.md](../../modules/l1/ecr/README.md) |
| `cloudfront` | `aws:cloudfront:distribution` | no | [modules/l1/cloudfront/README.md](../../modules/l1/cloudfront/README.md) |
| `waf` | `aws:waf:web_acl` | no | [modules/l1/waf/README.md](../../modules/l1/waf/README.md) |
| `rds` | `aws:rds:instance` | no | [modules/l1/rds/README.md](../../modules/l1/rds/README.md) |
| `kms-key` | `aws:kms:key` | no | [modules/l1/kms-key/README.md](../../modules/l1/kms-key/README.md) |
| `dynamodb` | `aws:dynamodb:table` | no | [modules/l1/dynamodb/README.md](../../modules/l1/dynamodb/README.md) |
| `uptime` | `aws:uptime:monitor` | no | [modules/l1/uptime/README.md](../../modules/l1/uptime/README.md) |
## L2 compositions (2)
| Module | Composes | Docs |
|------------------|---------------------------------------------|---------------------------------------------------|
| `microservice` | vpc + ecs-cluster + ecs-service + alb + ecr | [modules/l2/microservice/README.md](../../modules/l2/microservice/README.md) |
| `static-assets` | s3 + cloudfront | [modules/l2/static-assets/README.md](../../modules/l2/static-assets/README.md) |
## See also
- [modules/README.md](../../modules/README.md) — L1/L2 distinction, registry format, how to add a module.
- [modules/README-TEMPLATE.md](../../modules/README-TEMPLATE.md) — per-module doc template.
+96
View File
@@ -0,0 +1,96 @@
# Module: `<name>`
> Copy this template into `modules/l1/<name>/README.md` or
> `modules/l2/<name>/README.md` and fill in the placeholders. Sections
> marked **DROP** are intentionally omitted from nova modules
> (D-029): do **not** add `NFRs` or `Compliance` sections.
## Overview
One-paragraph description of what this module provisions, the stack
type(s) it exposes, and when to reach for it. Mention whether it is L1
(single primitive) or L2 (composition of L1s), and whether it is
multi-resource.
- **Stack type:** `aws:<service>:<resource>`
- **Kind:** `l1` (or `l2`)
- **Version:** `1.0.0`
## Resources
List the concrete cloud resources the Terraform adapter creates. For L1
single-resource modules this is one row; for multi-resource L1s mirror
the `resources[]` array in `interface.json`.
| Stack type | Terraform resource | Notes |
|-------------------------|------------------------------------|----------------------------------|
| `aws:s3:bucket` | `aws_s3_bucket` | The bucket itself |
| `aws:s3:bucket` | `aws_s3_bucket_versioning` | Versioning sibling |
| `aws:s3:bucket` | `aws_s3_bucket_server_side_encryption_configuration` | SSE config sibling |
For L2 modules, list the L1 modules composed via `module` blocks in
`terraform/main.tf` instead.
## Inputs
Mirror `interface.json``inputs`. Mark required inputs with **yes**.
| Name | Type | Required | Default | Description |
|----------------|---------|----------|---------------|-----------------------------------|
| `bucket_name` | string | yes | — | Globally-unique S3 bucket name |
| `region` | string | yes | — | AWS region |
| `kms_key_arn` | string | no | `null` | CMK ARN for SSE-KMS |
| `enabled` | boolean | no | `true` | Feature flag |
| `tags` | map | no | `{}` | Tags merged with module defaults |
## Outputs
Mirror `interface.json``outputs`.
| Name | Type | Description |
|---------------------------------|--------|----------------------------------------------|
| `bucket_arn` | arn | The S3 bucket ARN |
| `bucket_name` | string | The bucket name |
| `bucket_regional_domain_name` | string | The bucket regional domain name |
## Usage
```hcl
module "bucket" {
source = "modules/l1/s3/terraform"
bucket_name = "nova-prod-assets"
region = "us-east-1"
tags = {
"nova:owner" = "team-platform"
"nova:environment" = "prod"
}
}
```
Or as a flat-stack contract entry:
```json
{
"module": "s3",
"version": "1.0.0",
"inputs": {
"bucket_name": "nova-prod-assets",
"region": "us-east-1"
}
}
```
## Versioning
This module follows the registry semver contract: bump the patch/minor
version in `interface.json` and `modules/registry.json` for any
input/output/behavior change. Breaking changes (renamed inputs,
removed outputs, changed defaults) require a major bump and a new
registry entry; the previous version is marked `deprecated: true` and
remains selectable by pinned contracts. See `modules/README.md` for
the registry format and the resolver's version-selection rules.
<!-- DROP: NFRs — out of scope for nova v1 (D-029) -->
<!-- DROP: Compliance / attestation — out of scope for nova v1 (D-029) -->
+127
View File
@@ -0,0 +1,127 @@
# 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": { "<name>": { "type": "...", "required": true, "description": "..." } },
"outputs": { "<name>": { "type": "...", "description": "..." } },
"resources": [ { "type": "aws:ec2:vpc", "inputs": [...], "outputs": [...] } ]
}
```
- `type` is **stack-typed**`aws:<service>:<resource>` (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/<name>/` (or `modules/l2/<name>/`).
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/<name>/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 + alb + ecr | Container microservice with public ALB |
| `static-assets` | s3 + cloudfront | Static site fronted by CloudFront |
+3
View File
@@ -0,0 +1,3 @@
# L1: alb
Application Load Balancer primitive (multi-resource: LB + target group + listener; stack type `aws:alb`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+68
View File
@@ -0,0 +1,68 @@
{
"name": "alb",
"version": "1.0.0",
"kind": "l1",
"type": "aws:alb",
"description": "Application Load Balancer primitive (multi-resource: LB + target group + listener). Engine-agnostic stack types aws:alb + aws:alb:targetgroup + aws:alb:listener; the Terraform adapter translates to aws_lb/aws_lb_target_group/aws_lb_listener.",
"inputs": {
"lb_name": {
"type": "string",
"description": "Name of the load balancer.",
"required": true
},
"subnet_ids": {
"type": "list",
"description": "List of subnet ids the LB is deployed into.",
"required": true
},
"target_group_port": {
"type": "integer",
"default": 80,
"description": "Port the target group forwards to."
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"lb_arn": {
"type": "arn",
"description": "The load balancer ARN."
},
"dns_name": {
"type": "string",
"description": "The load balancer DNS name."
},
"target_group_arn": {
"type": "arn",
"description": "The target group ARN."
}
},
"resources": [
{
"type": "aws:alb",
"description": "The Application Load Balancer.",
"inputs": ["lb_name", "subnet_ids"],
"outputs": ["lb_arn", "dns_name"]
},
{
"type": "aws:alb:targetgroup",
"description": "Target group on the LB port.",
"inputs": ["lb_name", "target_group_port"],
"outputs": ["target_group_arn"]
},
{
"type": "aws:alb:listener",
"description": "Listener forwarding to the target group.",
"inputs": ["target_group_port", "target_group_arn"],
"outputs": []
}
]
}
+16
View File
@@ -0,0 +1,16 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
# Target group requires a vpc_id. The L1 interface does not expose it as
# an input by design (kept minimal per D-014); the caller is expected to
# supply subnets in a single VPC. When a vpc_id input is added later, this
# local can be removed. For now, null forces the caller to set it via a
# provider-level default or an extension.
vpc_id = null
}
+36
View File
@@ -0,0 +1,36 @@
resource "aws_lb" "this" {
count = var.enabled ? 1 : 0
name = var.lb_name
load_balancer_type = "application"
subnets = var.subnet_ids
tags = local.tags
}
resource "aws_lb_target_group" "this" {
count = var.enabled ? 1 : 0
name_prefix = "${var.lb_name}-"
port = var.target_group_port
protocol = "HTTP"
target_type = "ip"
vpc_id = local.vpc_id
lifecycle {
create_before_destroy = true
}
tags = local.tags
}
resource "aws_lb_listener" "this" {
count = var.enabled ? 1 : 0
load_balancer_arn = aws_lb.this[0].id
port = var.target_group_port
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.this[0].arn
}
depends_on = [aws_lb_target_group.this]
}
+14
View File
@@ -0,0 +1,14 @@
output "lb_arn" {
value = var.enabled ? aws_lb.this[0].arn : null
description = "The load balancer ARN."
}
output "dns_name" {
value = var.enabled ? aws_lb.this[0].dns_name : null
description = "The load balancer DNS name."
}
output "target_group_arn" {
value = var.enabled ? aws_lb_target_group.this[0].arn : null
description = "The target group ARN."
}
+27
View File
@@ -0,0 +1,27 @@
variable "lb_name" {
type = string
description = "Name of the load balancer."
}
variable "subnet_ids" {
type = list(string)
description = "List of subnet ids the LB is deployed into."
}
variable "target_group_port" {
type = number
description = "Port the target group forwards to."
default = 80
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: cloudfront
CloudFront distribution primitive (stack type `aws:cloudfront:distribution`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+39
View File
@@ -0,0 +1,39 @@
{
"name": "cloudfront",
"version": "1.0.0",
"kind": "l1",
"type": "aws:cloudfront:distribution",
"description": "CloudFront distribution primitive (engine-agnostic stack type aws:cloudfront:distribution; the Terraform adapter translates to aws_cloudfront_distribution).",
"inputs": {
"distribution_name": {
"type": "string",
"description": "Name (comment) of the CloudFront distribution.",
"required": true
},
"origin_domain": {
"type": "string",
"description": "Domain name of the origin (e.g. an S3 bucket regional domain or ALB DNS).",
"required": true
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"distribution_arn": {
"type": "arn",
"description": "The CloudFront distribution ARN."
},
"domain_name": {
"type": "string",
"description": "The CloudFront distribution domain name."
}
}
}
+11
View File
@@ -0,0 +1,11 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
origin_id = "${var.distribution_name}-origin"
}
+41
View File
@@ -0,0 +1,41 @@
resource "aws_cloudfront_distribution" "this" {
count = var.enabled ? 1 : 0
comment = var.distribution_name
enabled = true
price_class = "PriceClass_100"
tags = local.tags
origin {
domain_name = var.origin_domain
origin_id = local.origin_id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
@@ -0,0 +1,9 @@
output "distribution_arn" {
value = var.enabled ? aws_cloudfront_distribution.this[0].arn : null
description = "The CloudFront distribution ARN."
}
output "domain_name" {
value = var.enabled ? aws_cloudfront_distribution.this[0].domain_name : null
description = "The CloudFront distribution domain name."
}
@@ -0,0 +1,21 @@
variable "distribution_name" {
type = string
description = "Name (comment) of the CloudFront distribution."
}
variable "origin_domain" {
type = string
description = "Domain name of the origin (e.g. an S3 bucket regional domain or ALB DNS)."
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: dynamodb
DynamoDB table primitive (stack type `aws:dynamodb:table`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+44
View File
@@ -0,0 +1,44 @@
{
"name": "dynamodb",
"version": "1.0.0",
"kind": "l1",
"type": "aws:dynamodb:table",
"description": "DynamoDB table primitive (engine-agnostic stack type aws:dynamodb:table; the Terraform adapter translates to aws_dynamodb_table).",
"inputs": {
"table_name": {
"type": "string",
"description": "Name of the DynamoDB table.",
"required": true
},
"hash_key": {
"type": "string",
"description": "Name of the partition (hash) key.",
"required": true
},
"billing_mode": {
"type": "string",
"default": "PAY_PER_REQUEST",
"description": "Billing mode: PAY_PER_REQUEST or PROVISIONED."
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"table_arn": {
"type": "arn",
"description": "The DynamoDB table ARN."
},
"table_name": {
"type": "string",
"description": "The DynamoDB table name (echoes the input)."
}
}
}
+9
View File
@@ -0,0 +1,9 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+12
View File
@@ -0,0 +1,12 @@
resource "aws_dynamodb_table" "this" {
count = var.enabled ? 1 : 0
name = var.table_name
billing_mode = var.billing_mode
hash_key = var.hash_key
tags = local.tags
attribute {
name = var.hash_key
type = "S"
}
}
+9
View File
@@ -0,0 +1,9 @@
output "table_arn" {
value = var.enabled ? aws_dynamodb_table.this[0].arn : null
description = "The DynamoDB table ARN."
}
output "table_name" {
value = var.enabled ? aws_dynamodb_table.this[0].name : null
description = "The DynamoDB table name (echoes the input)."
}
@@ -0,0 +1,27 @@
variable "table_name" {
type = string
description = "Name of the DynamoDB table."
}
variable "hash_key" {
type = string
description = "Name of the partition (hash) key."
}
variable "billing_mode" {
type = string
description = "Billing mode: PAY_PER_REQUEST or PROVISIONED."
default = "PAY_PER_REQUEST"
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: ecr
ECR repository primitive (stack type `aws:ecr:repository`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+34
View File
@@ -0,0 +1,34 @@
{
"name": "ecr",
"version": "1.0.0",
"kind": "l1",
"type": "aws:ecr:repository",
"description": "ECR repository primitive (engine-agnostic stack type aws:ecr:repository; the Terraform adapter translates to aws_ecr_repository).",
"inputs": {
"repository_name": {
"type": "string",
"description": "Name of the ECR repository.",
"required": true
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"repository_url": {
"type": "string",
"description": "The ECR repository URL."
},
"repository_arn": {
"type": "arn",
"description": "The ECR repository ARN."
}
}
}
+9
View File
@@ -0,0 +1,9 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+10
View File
@@ -0,0 +1,10 @@
resource "aws_ecr_repository" "this" {
count = var.enabled ? 1 : 0
name = var.repository_name
image_tag_mutability = "MUTABLE"
tags = local.tags
image_scanning_configuration {
scan_on_push = true
}
}
+9
View File
@@ -0,0 +1,9 @@
output "repository_url" {
value = var.enabled ? aws_ecr_repository.this[0].repository_url : null
description = "The ECR repository URL."
}
output "repository_arn" {
value = var.enabled ? aws_ecr_repository.this[0].arn : null
description = "The ECR repository ARN."
}
+16
View File
@@ -0,0 +1,16 @@
variable "repository_name" {
type = string
description = "Name of the ECR repository."
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: ecs-cluster
ECS cluster primitive (stack type `aws:ecs:cluster`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+34
View File
@@ -0,0 +1,34 @@
{
"name": "ecs-cluster",
"version": "1.0.0",
"kind": "l1",
"type": "aws:ecs:cluster",
"description": "ECS cluster primitive (engine-agnostic stack type aws:ecs:cluster; the Terraform adapter translates to aws_ecs_cluster).",
"inputs": {
"cluster_name": {
"type": "string",
"description": "Name of the ECS cluster.",
"required": true
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"cluster_arn": {
"type": "arn",
"description": "The ECS cluster ARN."
},
"cluster_name": {
"type": "string",
"description": "The ECS cluster name (echoes the input)."
}
}
}
@@ -0,0 +1,9 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+5
View File
@@ -0,0 +1,5 @@
resource "aws_ecs_cluster" "this" {
count = var.enabled ? 1 : 0
name = var.cluster_name
tags = local.tags
}
@@ -0,0 +1,9 @@
output "cluster_arn" {
value = var.enabled ? aws_ecs_cluster.this[0].arn : null
description = "The ECS cluster ARN."
}
output "cluster_name" {
value = var.enabled ? aws_ecs_cluster.this[0].name : null
description = "The ECS cluster name (echoes the input)."
}
@@ -0,0 +1,16 @@
variable "cluster_name" {
type = string
description = "Name of the ECS cluster."
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: ecs-service
ECS service primitive (multi-resource: task definition + service; stack type `aws:ecs:service`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+63
View File
@@ -0,0 +1,63 @@
{
"name": "ecs-service",
"version": "1.0.0",
"kind": "l1",
"type": "aws:ecs:service",
"description": "ECS service primitive (multi-resource: task definition + service). Engine-agnostic stack types aws:ecs:taskdef + aws:ecs:service; the Terraform adapter translates to aws_ecs_task_definition/aws_ecs_service.",
"inputs": {
"service_name": {
"type": "string",
"description": "Name of the ECS service (also used as the task definition family).",
"required": true
},
"cluster_arn": {
"type": "arn",
"description": "ARN of the ECS cluster the service runs in.",
"required": true
},
"task_definition": {
"type": "string",
"description": "Task definition ARN or family:revision to run. If supplied as a path/string JSON, the module creates an aws_ecs_task_definition.",
"required": true
},
"desired_count": {
"type": "integer",
"default": 1,
"description": "Number of tasks to run."
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"service_arn": {
"type": "arn",
"description": "The ECS service ARN."
},
"service_name": {
"type": "string",
"description": "The ECS service name (echoes the input)."
}
},
"resources": [
{
"type": "aws:ecs:taskdef",
"description": "The ECS task definition (registered from task_definition input).",
"inputs": ["service_name", "task_definition"],
"outputs": []
},
{
"type": "aws:ecs:service",
"description": "The ECS service running the task definition on the cluster.",
"inputs": ["service_name", "cluster_arn", "desired_count"],
"outputs": ["service_arn", "service_name"]
}
]
}
@@ -0,0 +1,9 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+15
View File
@@ -0,0 +1,15 @@
resource "aws_ecs_task_definition" "this" {
count = var.enabled ? 1 : 0
family = var.service_name
container_definitions = var.task_definition
tags = local.tags
}
resource "aws_ecs_service" "this" {
count = var.enabled ? 1 : 0
name = var.service_name
cluster = var.cluster_arn
task_definition = aws_ecs_task_definition.this[0].arn
desired_count = var.desired_count
tags = local.tags
}
@@ -0,0 +1,9 @@
output "service_arn" {
value = var.enabled ? aws_ecs_service.this[0].id : null
description = "The ECS service ARN."
}
output "service_name" {
value = var.enabled ? aws_ecs_service.this[0].name : null
description = "The ECS service name (echoes the input)."
}
@@ -0,0 +1,32 @@
variable "service_name" {
type = string
description = "Name of the ECS service (also used as the task definition family)."
}
variable "cluster_arn" {
type = string
description = "ARN of the ECS cluster the service runs in."
}
variable "task_definition" {
type = string
description = "Task definition JSON string (container definitions). The module registers an aws_ecs_task_definition with family = service_name."
}
variable "desired_count" {
type = number
description = "Number of tasks to run."
default = 1
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: iam-role
IAM role primitive (stack type `aws:iam:role`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+39
View File
@@ -0,0 +1,39 @@
{
"name": "iam-role",
"version": "1.0.0",
"kind": "l1",
"type": "aws:iam:role",
"description": "IAM role primitive (engine-agnostic stack type aws:iam:role; the Terraform adapter translates to aws_iam_role).",
"inputs": {
"role_name": {
"type": "string",
"description": "Name of the IAM role.",
"required": true
},
"policy_document": {
"type": "string",
"description": "Assume-role policy document JSON string.",
"required": true
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"role_arn": {
"type": "arn",
"description": "The IAM role ARN."
},
"role_name": {
"type": "string",
"description": "The IAM role name (echoes the input)."
}
}
}
+9
View File
@@ -0,0 +1,9 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+6
View File
@@ -0,0 +1,6 @@
resource "aws_iam_role" "this" {
count = var.enabled ? 1 : 0
name = var.role_name
assume_role_policy = var.policy_document
tags = local.tags
}
+9
View File
@@ -0,0 +1,9 @@
output "role_arn" {
value = var.enabled ? aws_iam_role.this[0].arn : null
description = "The IAM role ARN."
}
output "role_name" {
value = var.enabled ? aws_iam_role.this[0].name : null
description = "The IAM role name (echoes the input)."
}
@@ -0,0 +1,21 @@
variable "role_name" {
type = string
description = "Name of the IAM role."
}
variable "policy_document" {
type = string
description = "Assume-role policy document JSON string."
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: kms-key
KMS customer master key primitive (stack type `aws:kms:key`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+34
View File
@@ -0,0 +1,34 @@
{
"name": "kms-key",
"version": "1.0.0",
"kind": "l1",
"type": "aws:kms:key",
"description": "KMS customer master key primitive (engine-agnostic stack type aws:kms:key; the Terraform adapter translates to aws_kms_key).",
"inputs": {
"key_name": {
"type": "string",
"description": "Name (alias) of the KMS key.",
"required": true
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"key_arn": {
"type": "arn",
"description": "The KMS key ARN."
},
"key_id": {
"type": "string",
"description": "The KMS key id."
}
}
}
+9
View File
@@ -0,0 +1,9 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+12
View File
@@ -0,0 +1,12 @@
resource "aws_kms_key" "this" {
count = var.enabled ? 1 : 0
description = "KMS key managed by nova L1 kms-key primitive."
deletion_window_in_days = 30
tags = local.tags
}
resource "aws_kms_alias" "this" {
count = var.enabled ? 1 : 0
name = "alias/${var.key_name}"
target_key_id = aws_kms_key.this[0].key_id
}
+9
View File
@@ -0,0 +1,9 @@
output "key_arn" {
value = var.enabled ? aws_kms_key.this[0].arn : null
description = "The KMS key ARN."
}
output "key_id" {
value = var.enabled ? aws_kms_key.this[0].key_id : null
description = "The KMS key id."
}
+16
View File
@@ -0,0 +1,16 @@
variable "key_name" {
type = string
description = "Name (alias) of the KMS key."
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: rds
RDS DB instance primitive (stack type `aws:rds:instance`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+44
View File
@@ -0,0 +1,44 @@
{
"name": "rds",
"version": "1.0.0",
"kind": "l1",
"type": "aws:rds:instance",
"description": "RDS DB instance primitive (engine-agnostic stack type aws:rds:instance; the Terraform adapter translates to aws_db_instance).",
"inputs": {
"instance_name": {
"type": "string",
"description": "Name (identifier) of the RDS DB instance.",
"required": true
},
"instance_class": {
"type": "string",
"default": "db.t3.micro",
"description": "DB instance class."
},
"allocated_storage": {
"type": "integer",
"default": 20,
"description": "Allocated storage in GiB."
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"instance_endpoint": {
"type": "string",
"description": "The RDS DB instance endpoint (host:port)."
},
"instance_arn": {
"type": "arn",
"description": "The RDS DB instance ARN."
}
}
}
+9
View File
@@ -0,0 +1,9 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+12
View File
@@ -0,0 +1,12 @@
resource "aws_db_instance" "this" {
count = var.enabled ? 1 : 0
identifier = var.instance_name
instance_class = var.instance_class
allocated_storage = var.allocated_storage
engine = "postgres"
engine_version = "14"
username = "nova"
password = "changeme-rotate-me"
skip_final_snapshot = true
tags = local.tags
}
+9
View File
@@ -0,0 +1,9 @@
output "instance_endpoint" {
value = var.enabled ? aws_db_instance.this[0].endpoint : null
description = "The RDS DB instance endpoint (host:port)."
}
output "instance_arn" {
value = var.enabled ? aws_db_instance.this[0].arn : null
description = "The RDS DB instance ARN."
}
+28
View File
@@ -0,0 +1,28 @@
variable "instance_name" {
type = string
description = "Name (identifier) of the RDS DB instance."
}
variable "instance_class" {
type = string
description = "DB instance class."
default = "db.t3.micro"
}
variable "allocated_storage" {
type = number
description = "Allocated storage in GiB."
default = 20
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: s3
S3 bucket primitive (stack type `aws:s3:bucket`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+48
View File
@@ -0,0 +1,48 @@
{
"name": "s3",
"version": "1.0.0",
"kind": "l1",
"type": "aws:s3:bucket",
"description": "S3 bucket primitive (engine-agnostic stack type aws:s3:bucket; the Terraform adapter translates to aws_s3_bucket).",
"inputs": {
"bucket_name": {
"type": "string",
"description": "Globally-unique S3 bucket name.",
"required": true
},
"region": {
"type": "string",
"description": "AWS region the bucket is created in (provider-level; not a resource arg).",
"required": true
},
"kms_key_arn": {
"type": "string",
"description": "ARN of the CMK for SSE-KMS; if absent, uses managed key (SSE-S3).",
"required": false
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"bucket_arn": {
"type": "arn",
"description": "The S3 bucket ARN."
},
"bucket_name": {
"type": "string",
"description": "The bucket name (echoes the input)."
},
"bucket_regional_domain_name": {
"type": "string",
"description": "The bucket regional domain name (e.g. nova-bucket.s3.us-east-1.amazonaws.com)."
}
}
}
+13
View File
@@ -0,0 +1,13 @@
locals {
# SSE algorithm: KMS when a CMK ARN is supplied, else AES256 (SSE-S3).
sse_algorithm = var.kms_key_arn != null ? "aws:kms" : "AES256"
# Tags: merge caller-supplied tags with the module defaults.
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+26
View File
@@ -0,0 +1,26 @@
resource "aws_s3_bucket" "this" {
count = var.enabled ? 1 : 0
bucket = var.bucket_name
tags = local.tags
}
resource "aws_s3_bucket_versioning" "this" {
count = var.enabled ? 1 : 0
bucket = aws_s3_bucket.this[0].id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
count = var.enabled ? 1 : 0
bucket = aws_s3_bucket.this[0].id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = local.sse_algorithm
kms_master_key_id = var.kms_key_arn
}
}
}
+14
View File
@@ -0,0 +1,14 @@
output "bucket_arn" {
value = var.enabled ? aws_s3_bucket.this[0].arn : null
description = "The S3 bucket ARN."
}
output "bucket_name" {
value = var.enabled ? aws_s3_bucket.this[0].id : null
description = "The bucket name (echoes the input)."
}
output "bucket_regional_domain_name" {
value = var.enabled ? aws_s3_bucket.this[0].bucket_regional_domain_name : null
description = "The bucket regional domain name (e.g. nova-bucket.s3.us-east-1.amazonaws.com)."
}
+28
View File
@@ -0,0 +1,28 @@
variable "bucket_name" {
type = string
description = "Globally-unique S3 bucket name."
}
variable "region" {
type = string
description = "AWS region the bucket is created in (provider-level; not a resource arg)."
default = null
}
variable "kms_key_arn" {
type = string
description = "ARN of the CMK for SSE-KMS; if absent, uses managed key (SSE-S3)."
default = null
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: uptime
Uptime monitor primitive (simplified stand-in: a CloudWatch alarm; stack type `aws:uptime:monitor`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+35
View File
@@ -0,0 +1,35 @@
{
"name": "uptime",
"version": "1.0.0",
"kind": "l1",
"type": "aws:uptime:monitor",
"description": "Uptime monitor primitive (simplified stand-in: a CloudWatch alarm watching the target resource). Engine-agnostic stack type aws:uptime:monitor; the Terraform adapter translates to aws_cloudwatch_metric_alarm.",
"inputs": {
"monitor_name": {
"type": "string",
"description": "Name of the uptime monitor (CloudWatch alarm).",
"required": true
},
"target_arn": {
"type": "arn",
"description": "ARN of the target resource being monitored.",
"required": true
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"monitor_arn": {
"type": "arn",
"description": "The CloudWatch alarm ARN (stand-in for the monitor ARN)."
}
}
}
+9
View File
@@ -0,0 +1,9 @@
locals {
tags = merge(
{
"nova:owner" = "nova"
"nova:environment" = "dev"
},
var.tags,
)
}
+21
View File
@@ -0,0 +1,21 @@
# Uptime monitor stand-in: a CloudWatch metric alarm referencing the
# target resource ARN via dimensions. A future revision may swap this
# for a Route 53 health check or CloudWatch composite alarm.
resource "aws_cloudwatch_metric_alarm" "this" {
count = var.enabled ? 1 : 0
alarm_name = var.monitor_name
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "RequestCount"
namespace = "AWS/ApplicationELB"
period = 60
statistic = "Sum"
threshold = 1
alarm_description = "Uptime monitor (CloudWatch alarm stand-in) for target ${var.target_arn}."
dimensions = {
LoadBalancer = var.target_arn
}
tags = local.tags
}
+4
View File
@@ -0,0 +1,4 @@
output "monitor_arn" {
value = var.enabled ? aws_cloudwatch_metric_alarm.this[0].arn : null
description = "The CloudWatch alarm ARN (stand-in for the monitor ARN)."
}
+21
View File
@@ -0,0 +1,21 @@
variable "monitor_name" {
type = string
description = "Name of the uptime monitor (CloudWatch alarm)."
}
variable "target_arn" {
type = string
description = "ARN of the target resource being monitored."
}
variable "tags" {
type = map(string)
description = "Additional tags to merge with the module defaults."
default = {}
}
variable "enabled" {
type = bool
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
default = true
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# L1: vpc
VPC primitive (multi-resource: VPC + subnets + route table + IGW; stack type `aws:ec2:vpc`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
+69
View File
@@ -0,0 +1,69 @@
{
"name": "vpc",
"version": "1.0.0",
"kind": "l1",
"type": "aws:ec2:vpc",
"description": "VPC primitive (multi-resource: VPC + subnets + route table + internet gateway). Engine-agnostic stack types aws:ec2:vpc + aws:ec2:subnet + aws:ec2:routetable + aws:ec2:igw; the Terraform adapter translates to aws_vpc/aws_subnet/aws_route_table/aws_internet_gateway.",
"inputs": {
"cidr": {
"type": "string",
"description": "VPC CIDR block, e.g. 10.0.0.0/16.",
"required": true
},
"azs": {
"type": "list",
"description": "List of availability zones, e.g. [\"us-east-1a\", \"us-east-1b\"]. One subnet is created per AZ.",
"required": true
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
},
"tags": {
"type": "map",
"default": {},
"description": "Additional tags to merge with the module defaults."
}
},
"outputs": {
"vpc_id": {
"type": "string",
"description": "The VPC id."
},
"subnet_ids": {
"type": "list",
"description": "List of subnet ids (one per AZ)."
},
"igw_id": {
"type": "string",
"description": "The internet gateway id."
}
},
"resources": [
{
"type": "aws:ec2:vpc",
"description": "The VPC itself.",
"inputs": ["cidr"],
"outputs": ["vpc_id"]
},
{
"type": "aws:ec2:subnet",
"description": "One subnet per availability zone (azs).",
"inputs": ["cidr", "az", "vpc_id"],
"outputs": ["subnet_ids"]
},
{
"type": "aws:ec2:routetable",
"description": "Route table bound to the VPC with a default route via the IGW.",
"inputs": ["vpc_id"],
"outputs": []
},
{
"type": "aws:ec2:igw",
"description": "Internet gateway attached to the VPC.",
"inputs": ["vpc_id"],
"outputs": ["igw_id"]
}
]
}

Some files were not shown because too many files have changed in this diff Show More