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 mergeStep-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=1Expected: 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.
Related
- Environment Parity Validation Gates — the parent guide covering config, runtime, and schema parity.
- Synchronizing Environment Variables Across Stages — the canonical config source this gate validates.
- Secrets Injection for Preview Environments — supplies values whose keys the gate checks.
- Preview Environments & Environment Parity — the section overview.
← Back to Environment Parity Validation Gates