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.