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-1for 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:
- Discovers the Current state (minimal normalized snapshot)
- Plans a list of Actions required to converge
- Executes those Actions via a Provider
- 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: boolis the lifecycle switchpresent=True=> ensure distribution existspresent=False=> ensure distribution is deleted (destroy)
DistributionCurrent(observed state)PlanandAction(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 eventsapply()executes and waitsexecute()is the primary entry pointdestroy(desired)is a convenience wrapper that preserves aliases
Discovery Strategy#
Discovery is currently alias-driven:
get_current()attemptsprovider.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:<name>) 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_DEPLOYEDaction (planned for create/update and optionally wait-only)- Manager-level wait for
current.status != Deployed
Destroy requires a stricter sequence:
- Disable distribution
- Wait until:
- status is
Deployed - config shows
Enabled=False
- status is
- 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.donemanager.apply.start/manager.apply.progress/manager.apply.donemanager.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.