ADR-024: Composition Layer Pattern
- Status: Accepted
- Date: 2026-03-08
- Deciders: HITL/Manager, Product Owner, Cloud Architect
Context
The modules/web module exists to solve a problem that no single upstream module addresses:
delivering a complete, production-ready web tier that combines ALB, CloudFront, WAF, and DNS
with opinionated security defaults enforced at composition time.
Other modules (alb, cloudfront, acm, s3) follow the derived pattern (ADR-023) — they clone
upstream and add value-adds. The web module has no upstream equivalent to clone. Its value
is the orchestration and the security opinions applied at the composition boundary.
Key observation: the web module already exists and implements this pattern today. This ADR
formalizes what was built, names the pattern, and documents the constraints that govern it.
Decision
modules/web is a composition layer — an opinionated module that:
- Wires peer modules (
../alb,../cloudfront) via relativesourcereferences - Adds WAF (
waf.tf) and DNS (dns.tf) resources directly - Enforces security defaults that span the composition boundary
- Exposes a simplified variable surface that hides internal wiring complexity
Composition Wiring
modules/web/
main.tf ← module "alb" { source = "../alb" }
cloudfront.tf ← module "cdn" { source = "../cloudfront" }
waf.tf ← resource "aws_wafv2_web_acl" (direct — no upstream peer module)
dns.tf ← resource "aws_route53_record" (direct — no upstream peer module)
locals.tf ← shared tag merge, computed names
variables.tf ← simplified input surface (vpc_id, subnet_ids, domain_name, ...)
outputs.tf ← composite outputs (alb_dns_name, cdn_domain, waf_acl_arn)
Security Defaults Enforced at Composition Boundary
| Default | Location | Rationale |
|---|---|---|
drop_invalid_header_fields = true | main.tf ALB call | OWASP header injection prevention |
enable_cross_zone_load_balancing = true | main.tf ALB call | Reliability: AZ failover |
| HTTPS-only listener | ALB + CloudFront | TLS 1.3 minimum per ADR-003 / APRA CPS 234 |
| WAF association on ALB | main.tf | All web traffic protected by default |
| WAF scope REGIONAL on ALB, CLOUDFRONT on CDN | waf.tf | Correct scope required for each service |
enable_deletion_protection = true (ALB) | main.tf | Prevents accidental ALB deletion in production |
What Composition Layer Modules Must NOT Do
- Duplicate resource logic that exists in peer modules (alb, cloudfront)
- Introduce their own provider constraints beyond those inherited from peers
- Publish independently to TFC registry as a standalone module (they depend on peer module versions being available)
What Composition Layer Modules CAN Do
- Add direct resources for glue services (WAF, DNS, ACM validation records) that have no standalone peer module
- Override peer module defaults with more restrictive values
- Expose a simplified interface that consumers use instead of wiring each peer module individually
Consequences
Positive
- Operators deploy a complete, secure web tier with one module invocation
- Security defaults (WAF, HTTPS, deletion protection) cannot be omitted by composition consumers — they are hardcoded at the composition boundary
- WAF and DNS do not require separate peer modules; the composition layer handles them directly, keeping the module count bounded
- Changes to ALB or CloudFront security posture propagate automatically to all web compositions on next release
Negative
- Consumers cannot swap individual components (e.g., use a different CDN) without forking the composition layer
- The
webmodule has a non-trivial variable surface inherited from multiple peers — variable naming conflicts must be resolved explicitly - Relative
sourcereferences (../alb,../cloudfront) mean the composition layer cannot be used standalone outside this monorepo structure without path adjustment
Risks
| ID | Risk | Probability | Impact | Mitigation |
|---|---|---|---|---|
| RISK-024-001 | ALB or CloudFront peer module semver bump breaks composition layer | MEDIUM | MEDIUM | Composition layer pins peer module versions in CI; integration tests gate breakage |
| RISK-024-002 | WAF rule set becomes stale relative to OWASP Top 10 | MEDIUM | HIGH | Quarterly WAF rule review in task security:trivy scope |
| RISK-024-003 | Circular dependency introduced if peer modules reference web | LOW | BLOCKING | Architecture rule: composition layer is a leaf node — peer modules must not depend on it |
Pattern Boundaries
| Module Type | Source Strategy | Registry Publish | TFC Standalone |
|---|---|---|---|
| Derived (ADR-023) | Clone from upstream | Yes — independently | Yes |
| Composition layer | Relative peer refs + direct resources | No — depends on peers | No (monorepo only) |
Alternatives Considered
-
Clone a composition upstream (e.g., a community full-stack module): Rejected — no community module enforces the specific combination of ALB + CloudFront + WAF + Route53 with APRA CPS 234 defaults. Building on a generic community module would require more overrides than building the composition directly.
-
Separate modules for WAF and DNS: Rejected for MVP — WAF and DNS are always present when deploying a web tier in this platform. Splitting them into independent modules adds coordination overhead without business value at current scale. Revisit at Phase 3 when multi-WAF deployments are required.
-
Single monolithic web module with all resources (no peer module calls): Rejected — violates DRY; ALB and CloudFront are independently useful modules. Duplicating their resource logic inside
webwould create drift from the independently-tested peer modules.
Related ADRs
- ADR-002: Registry structure — composition layers are monorepo-only
- ADR-023: Derived module pattern (alb, cloudfront — peers consumed by this layer)
- ADR-025: Per-component versioning —
webmodule version is independent
Coordination Evidence
- Product Owner log:
tmp/terraform-aws/coordination-logs/product-owner-2026-03-08.json - Cloud Architect log:
tmp/terraform-aws/coordination-logs/cloud-architect-2026-03-08.json - Architecture decisions:
tmp/terraform-aws/architecture-decisions/ADR-024-composition-layer-2026-03-08.md