Blocking Deploys on Config Drift with a Parity Gate

You want a pipeline step that refuses to deploy when a candidate’s configuration has drifted from production — a missing key or an undeclared extra one — before it causes a “worked in preview, broke in prod” incident. This is the complete, copy-paste config-drift gate that implements the key-diff check from environment parity validation gates.

When to use this pattern

  • You promote config through stages and have been bitten by a key present in one environment but not another.
  • You want an explicit, auditable allow-list for the keys that are supposed to differ.
  • You need the check to block merges, not just warn.

Prerequisites

Complete working example

#!/usr/bin/env bash
# scripts/config-drift-gate.sh — fails the pipeline on config-key drift vs production
set -euo pipefail

# Keys that are ALLOWED to differ between environments (explicit, auditable).
# Extend this list deliberately; every entry is a documented exception.
ALLOWED_OVERRIDES='^(APP_ENV|DATABASE_URL|REDIS_URL|API_BASE_URL|SENTRY_DSN|PREVIEW_URL)$'

# 1. Production baseline key set (read-only).
prod_keys() {
  kubectl get configmap app-config -n production -o json \
    | jq -r '.data | keys[]'
}

# 2. Candidate key set from the rendered preview/staging config JSON.
candidate_keys() {
  jq -r '.data | keys[]' candidate-config.json
}

# 3. Strip allow-listed keys, sort, and diff the remainder.
strip() { grep -Ev "$ALLOWED_OVERRIDES" | sort; }

PROD=$(prod_keys | strip)
CAND=$(candidate_keys | strip)

if diff <(echo "$PROD") <(echo "$CAND") >/dev/null; then
  echo "✅ Config parity OK — non-override key sets match production."
  exit 0
fi

echo "❌ CONFIG DRIFT — candidate diverges from production (< prod, > candidate):"
diff <(echo "$PROD") <(echo "$CAND") || true
echo
echo "Resolve by declaring the key in both environments, or add it to ALLOWED_OVERRIDES if it is a legitimate stage-specific key."
exit 1
# .github/workflows/config-drift-gate.yml
name: Config Drift Gate
on:
  pull_request:
    types: [opened, synchronize]
jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Render candidate config
        run: ./scripts/render-config.sh > candidate-config.json
      - name: Configure kubectl (read-only prod access)
        run: echo "${{ secrets.KUBE_CONFIG_RO }}" | base64 -d > "$HOME/.kube/config" && mkdir -p "$HOME/.kube"
      - name: Run config-drift gate
        run: bash scripts/config-drift-gate.sh    # non-zero exit blocks the merge

Step-by-step walkthrough

The allow-list. ALLOWED_OVERRIDES is an explicit regex of keys that are meant to differ between environments — the database URL, the app env name, the preview URL. Every entry is a documented, auditable exception. Keeping it explicit means the gate stays strict about everything else.

Reading both key sets. prod_keys pulls the production ConfigMap’s keys with read-only credentials; candidate_keys reads the rendered candidate. Both are reduced to sorted key lists — values are never compared, because values are supposed to differ.

The diff. After stripping allow-listed keys and sorting, diff of the two sets is empty only when they match exactly. A < line means a key exists in production but not the candidate (a missing required key); a > line means the candidate has an undeclared extra key. Either fails the job.

Blocking the merge. Because the script exits non-zero on drift and the workflow runs on pull requests, registering the drift job as a required status check in branch protection prevents a divergent config from merging at all — the gate is preventive, not advisory.

Verification

# Passing case: identical non-override keys → exit 0
bash scripts/config-drift-gate.sh; echo "exit=$?"   # → exit=0

# Failing case: inject an undeclared key into the candidate and re-run
jq '.data.UNDECLARED_KEY = "x"' candidate-config.json > tmp && mv tmp candidate-config.json
bash scripts/config-drift-gate.sh; echo "exit=$?"   # → prints "> UNDECLARED_KEY", exit=1

Expected: the clean candidate passes; the injected key produces a > diff line and a non-zero exit that blocks the deploy.

Common pitfalls

  • Comparing values instead of keys. Value comparison fails on every legitimate difference (different DB URLs) and trains people to bypass the gate. Diff keys only, as the script does.
  • An implicit or ever-growing allow-list. If every failure is “fixed” by adding the key to ALLOWED_OVERRIDES, the gate erodes to nothing. Treat each addition as a reviewed exception with a reason, not a reflex.
  • Running after deploy. A drift gate that runs post-deploy can only roll back a running environment. Run it pre-provision and as a required check so drift never starts, per the parent guide.

← Back to Environment Parity Validation Gates