# Design Notes for dxaws-cloudfront This document is the architecture/design reference for **dxaws-cloudfront**. The goal is to keep the **module contract stable** while allowing the internals (planner comparisons, provider implementations, config shapes) to evolve. --- ## Purpose and Scope **dxaws-cloudfront** converges a single CloudFront distribution to a desired state. MVP scope: - One CloudFront distribution - One S3 origin - Origin Access Control (OAC) - Optional aliases (CNAMEs) - Optional ACM certificate (must be in `us-east-1` for CloudFront) - Default cache behavior (MVP subset) - Optional SPA-friendly custom error responses - Deterministic create/update/wait/destroy lifecycle Non-goals (for now): - Multiple origins / origin groups - Multiple cache behaviors - Cache policies / origin request policies - Logging / realtime logs - WAF / Shield configuration - Lambda@Edge / CloudFront Functions --- ## How this fits into dxaws dxaws modules are **primitive** building blocks that expose stable, testable interfaces. Higher-level orchestration (e.g., `dxaws-website`, Step Functions, runbooks) composes primitives but should not reach into their internals. This module is intended to be used by: - `dxaws-website` (CloudFront distribution fronting an S3 website origin) - DNS modules (Route53 alias record creation using distribution domain + hosted zone id) - Runbooks / automation flows (idempotent apply + explicit outputs) --- ## Declarative Convergence This module follows a **declarative convergence** model. Callers provide a **Desired** state. The module: 1. **Discovers** the Current state (minimal normalized snapshot) 2. **Plans** a list of Actions required to converge 3. **Executes** those Actions via a Provider 4. **Waits** when necessary for eventual consistency Key properties: - **Idempotent**: repeat runs converge to the same result - **Deterministic**: planning is pure and does not call AWS - **Provider-isolated**: AWS API details live in providers - **Stable contracts**: models + provider protocol change cautiously --- ## Module Layers ### Models (stable contract) `models.py` defines the stable data model: - `DistributionDesired` (target state) - `present: bool` is the lifecycle switch - `present=True` => ensure distribution exists - `present=False` => ensure distribution is deleted (destroy) - `DistributionCurrent` (observed state) - `Plan` and `Action` (what will be done) - `DistributionResult` (outputs) **Rule:** Keep these models small, explicit, and backwards-compatible. ### Planner (pure) `planner.py` converts: - `(desired, current, oac_id)` → `Plan(actions=[...])` Planner rules: - No AWS calls - Deterministic config synthesis - Compare using summarized fields only (avoid AWS response drift) Destroy planning (present=False): - If absent already → noop - Else → DISABLE → WAIT_DEPLOYED → DELETE ### Executor (boring) `executor.py` is a thin action dispatcher: - Executes actions in order - Calls provider methods - Avoids any planning decisions ### Provider Providers implement the AWS (or mock) behavior behind a stable protocol. `providers/base.py` defines the provider contract. `providers/aws.py` implements it using boto3/botocore. Provider rules: - AWS-only logic lives here - Handle strict AWS request shapes (CloudFront is strict) - Handle eventual consistency where required - notably disable/delete sequencing ### Manager `manager.py` is the orchestrator: - `get_current()` discovery (by alias) - `plan()` emits plan events - `apply()` executes and waits - `execute()` is the primary entry point - `destroy(desired)` is a convenience wrapper that preserves aliases --- ## Discovery Strategy Discovery is currently **alias-driven**: - `get_current()` attempts `provider.find_distribution_by_alias(aliases[0])` - If found, pulls config + etag and normalizes into `DistributionCurrent` Implications: - Callers should provide aliases for deterministic discovery. - In future, we can optionally support tag/comment-based discovery. --- ## Naming and Idempotency ### Stable comment We use a stable comment (`dxaws-cloudfront:`) to support deterministic config synthesis and optional discovery. ### OAC name length CloudFront enforces strict resource name limits. We use `truncate_with_hash()` to generate stable, bounded OAC names. --- ## Waiting and Eventual Consistency CloudFront is eventually consistent. We model waiting explicitly: - `WAIT_DEPLOYED` action (planned for create/update and optionally wait-only) - Manager-level wait for `current.status != Deployed` Destroy requires a stricter sequence: 1. Disable distribution 2. Wait until: - status is `Deployed` - config shows `Enabled=False` 3. Delete distribution The AWS provider is responsible for waiting until CloudFront reflects the **disabled** state before proceeding. --- ## Outputs `DistributionResult` includes: - distribution id - distribution domain name - hosted zone id (constant) for Route53 alias record creation --- ## Observability (o11y) Manager emits structured events via `dxaws_core.o11y.O11y`: - `manager.plan.start` / `manager.plan.done` - `manager.apply.start` / `manager.apply.progress` / `manager.apply.done` - `manager.execute.done` (outcome: noop/applied/failed) These events are designed to be stable enough for: - CLI output shaping - runbook audit trails - eventual EventBridge integration --- ## Tests We aim for three layers: - **Unit tests**: planner comparisons, config shape synthesis - **Integration tests**: manager + stub provider behavior - **Acceptance tests**: real AWS (slow) for lifecycle correctness Acceptance tests intentionally reuse persistent resources (e.g., origin bucket) so repeated runs are cheap and predictable. --- ## Extension Guidelines When extending this module: - Prefer adding new optional fields to `DistributionDesired` - Keep planner comparisons summarized and stable - Extend the provider protocol only when necessary, and add tests - Maintain backwards compatibility whenever possible If a change would break existing callers, introduce a new optional field or a new action type instead of changing semantics.