Skip to main content

dxaws-cloudfront / design

·748 words·4 mins

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:<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_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.