Skip to content

aws-cf-reverse-proxy: stable hash-keyed iteration to eliminate ordered_cache_behavior shuffle diffs#67

Merged
sam-at-luther merged 1 commit into
mainfrom
sam/cf-stable-hash-keyed-iteration
May 7, 2026
Merged

aws-cf-reverse-proxy: stable hash-keyed iteration to eliminate ordered_cache_behavior shuffle diffs#67
sam-at-luther merged 1 commit into
mainfrom
sam/cf-stable-hash-keyed-iteration

Conversation

@sam-at-luther
Copy link
Copy Markdown
Member

Summary

CloudFront's ordered_cache_behavior and origin blocks are TypeList in the AWS provider schema. Terraform iterates the for_each map in sorted-key order, and the resulting list is diffed by index. When local.origin_configs was keyed by the raw path_pattern, inserting a new path that sorts earlier than an existing key shifted every later entry down by one and produced cosmetic ~ diffs across every behavior — even though the end-state was byte-identical.

This switches local.origin_configs and local.grpc_origin_configs to be keyed by a stable hash-prefix of the path: ${substr(sha256(path), 0, 8)}-${path}. The path is appended after the hash purely so generated keys remain human-readable in plan output. path_pattern is now carried as a value field so the dynamic blocks can read it from .value.path_pattern instead of .key.

Public API is unchanged. var.origin_routes and var.grpc_routes keep their existing map(string) schemas. The output origin_configs is re-keyed by path_pattern so the externally-observable shape is preserved for downstream consumers.

The diff-shuffling problem (before)

Consumer origin_routes:

{
  "/v1/*"                        = "https://app.platform-test..."
  "/insideout-a2a/*"             = "https://app.platform-test..."
  "/.well-known/agent.json"      = "https://app.platform-test..."
  "/.well-known/agent-card.json" = "https://app.platform-test..."  # added
}

/.well-known/agent-card.json sorts BEFORE /.well-known/agent.json because - (0x2d) < . (0x2e). With raw-path keys, terraform iterates in sorted key order and the new entry lands at index 1 (right after /*), pushing every later entry down by 1 — producing ~ modifications for every existing behavior. Real-world example: luthersystems/ui-infrastructure#240.

After (this PR)

Iteration order is now by hash prefix:

[
  "154f2671-/.health",                          # newly added
  "6df8e0a2-/insideout-a2a/*",
  "72042627-/v1/*",
  "766e0153-/*",
  "e610b271-/.well-known/agent-card.json",
  "fc4ec54a-/.well-known/agent.json",
]

The new entry lands at its hash position; existing entries keep theirs.

Verified plan diff (simulated)

I simulated the post-refactor locals against terraform_data resources whose input is the iterated list-of-objects (the same internal representation a dynamic TypeList block produces inside a resource). After applying baseline origin_routes and re-planning with one new entry (/.health):

# terraform_data.ordered_cache_behaviors will be updated in-place
~ resource "terraform_data" "ordered_cache_behaviors" {
    ~ input  = [
        + {
            + origin_domain = "app.platform-test.luthersystemsapp.com"
            + origin_id     = "origin-.health"
            + path_pattern  = "/.health"
          },
          {
              origin_domain = "app.platform-test.luthersystemsapp.com"
              origin_id     = "origin-insideout-a2a"
              path_pattern  = "/insideout-a2a/*"
          },
      ]
}

Single + block for the new entry; siblings render as unchanged. Same shape for the corresponding origin block in all_origin_configs.

One-time first-apply shuffle on bump

Heads up to anyone reviewing the first plan after bumping to this version: the for_each map keys for every existing entry change from raw-path (/v1/*) to hash-prefixed (72042627-/v1/*). Terraform will render this as what looks like a wholesale rewrite of every ordered_cache_behavior and origin block in your distribution.

The end-state is byte-identical. No CloudFront resource is destroyed or recreated — aws_cloudfront_distribution is a single resource; the nested blocks are attributes that get updated in-place. After this one cosmetic plan, every subsequent route addition produces a clean single-add diff (verified above).

If you want extra confidence, you can apply, then immediately re-plan — the second plan should be empty.

Test plan

  • terraform fmt -check -recursive aws-cf-reverse-proxy/ clean
  • terraform validate from aws-cf-reverse-proxy/tests/test1/ passes
  • Simulated baseline + add-one-route plan shows single + (no ~ shuffles); see verified plan diff above
  • Output origin_configs shape unchanged from a consumer's perspective (re-keyed by path_pattern, which equals the old raw-path key)
  • CI green (Terraform CI: format-check, validate-modules)

Review notes

  • /review findings (manual): no P0/P1 blockers introduced by this PR. Notes:
    • 8-char hex hash prefix gives 2^32 distinct values; for N routes per consumer (< 100), birthday-bound collision probability is < 10^-6. If a collision did occur, terraform would error loudly (duplicate map key), not corrupt silently.
    • sha256() is deterministic across terraform versions and platforms.
    • Filter if v.path_pattern != "/*" is semantically equivalent to the old if k != "/*" (path_pattern carries what the old raw key did).
    • /* remains in all_origin_configs (used by the origin dynamic block) and is excluded only from ordered_cache_behavior, matching prior behavior.
  • qa-professor: no test files changed, skipped per skill.

Refs

Release

Additive change with no API break — minor bump. Tag as v55.19.0 post-merge.

…d_cache_behavior shuffle diffs

CloudFront's ordered_cache_behavior and origin blocks are TypeList in the
provider schema; terraform iterates the for_each map in sorted-key order
and the resulting list is diffed by index. When local.origin_configs was
keyed by raw path, inserting a new path that sorted earlier than an
existing key (e.g. "/.well-known/agent-card.json" sorts before
"/.well-known/agent.json" because "-" < ".") shifted every later entry
down by one index and produced cosmetic ~ diffs across every behavior.

Switch local.origin_configs and local.grpc_origin_configs to be keyed by
"<sha256(path)[:8]>-<path>" — a stable hash-prefix that spreads entries
over the keyspace so insertions don't predictably shift siblings. The
path is appended after the hash purely so generated keys remain
human-readable in plan output. path_pattern is now carried in the value
so the dynamic blocks can stop reading it from .key.

Public API is unchanged: var.origin_routes and var.grpc_routes keep
their existing schemas. The output "origin_configs" is re-keyed by
path_pattern so its observable shape stays the same for downstream
consumers — only internal iteration order changes.

The "__grpc__" prefix from #64 was no longer needed because gRPC keys
now carry an unambiguous "grpc-" hash-prefixed namespace; the merge into
all_origin_configs simplifies to a plain merge().

First apply after consumers bump to this version produces a one-time
index shuffle as existing keys migrate from raw-path to hash-prefixed
layout. End-state is byte-identical, but reviewers will see what looks
like a major rewrite of every existing cache behavior in their first
plan. Subsequent additions produce clean single-add diffs.

Refs luthersystems/ui-infrastructure#240
@sam-at-luther sam-at-luther merged commit 0063774 into main May 7, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant