Block Deployments on Config Drift: CI/CD Gating That Catches What Terraform Misses
Your infrastructure code is version-controlled. Your config files aren't. Here's how to gate deployments on environment parity — so a missing database.endpoint in staging doesn't become a 3 AM incident in production.
Here's a pattern I've seen at every company I've worked with: the Terraform plan looks clean, the CI pipeline is green, and the deploy goes through. Then production breaks because someone changed a database endpoint in staging but forgot to update prod — or a new secret was added to .env.dev that never made it to .env.prod.
Config drift between environments is a silent killer. Your IaC tools manage infrastructure resources. They don't manage the config files that sit on top — the YAML, JSON, TOML, and .env files that hold your application-level settings, connection strings, and feature flags.
ConfigDrift closes that gap. It compares config files across environments, flags critical differences with severity levels, and gives you a machine-readable exit code to gate your CI/CD pipeline.
Installation
pip install git+https://github.com/Coding-Dev-Tools/configdrift.git
Or via Homebrew (macOS/Linux):
brew tap Coding-Dev-Tools/tap
brew install configdrift
Or via Scoop (Windows):
scoop bucket add Coding-Dev-Tools https://github.com/Coding-Dev-Tools/scoop-bucket
scoop install configdrift
The Core Problem: What Your CI Pipeline Isn't Checking
A typical CI/CD pipeline runs these checks before deploy:
- Unit and integration tests pass
- Terraform plan has no destructive changes
- Docker image builds and scans clean
- Code review approved
What's missing: are the configs in staging identical to what's about to land in prod?
This matters because:
- A
database.endpointpointing to the wrong host doesn't fail tests — it fails in production at 3 AM - A missing
auth.secretin one environment causes 500s that no unit test catches - Feature flags that exist in dev but not in prod silently change behavior
- An extra
api_keyin staging that was never added to prod means the new feature deploys broken
These are all config drift problems, and they're all preventable.
Workflow 1: Fail the Build on Breaking Drift
The simplest CI gate. If ConfigDrift finds any critical key that's different between environments, the build fails.
# Compare two config files
configdrift check .env.staging .env.prod --output silent
With --output silent, ConfigDrift prints nothing and exits with code 1 if it finds any breaking drift — changes to critical keys like database.*, auth.*, api_key.*, secret.*, password.*, token.*, or endpoint.*.
No breaking drift? Exit code 0. The build proceeds.
This is your first line of defense. Add it to any CI pipeline:
# GitHub Actions
- name: Check config parity
run: |
pip install git+https://github.com/Coding-Dev-Tools/configdrift.git
configdrift check config/staging.yaml config/prod.yaml --output silent
If database.endpoint in staging differs from prod, the step fails. The deploy is blocked. The on-call engineer stays asleep.
Workflow 2: Three-Environment Scan with Severity Levels
Most teams have more than two environments. ConfigDrift's scan command compares entire directories of config files against a baseline.
# Scan dev, staging, and prod directories against dev as baseline
configdrift scan ./config/dev ./config/staging ./config/prod --baseline dev
Output looks like this:
Config Drift: dev → staging
┌──────────────────────────┬──────────┬──────────────────────┬──────────────────────┬───────────┐
│ Key │ Change │ Old Value │ New Value │ Severity │
├──────────────────────────┼──────────┼──────────────────────┼──────────────────────┼───────────┤
│ database.endpoint │ ~ changed│ db-dev.internal:5432 │ db-staging.internal │ BREAKING │
│ auth.token_expiry │ ~ changed│ 3600 │ 7200 │ BREAKING │
│ logging.level │ ~ changed│ debug │ info │ INFO │
│ feature.new_dashboard │ + added │ │ true │ WARNING │
└──────────────────────────┴──────────┴──────────────────────┴──────────────────────┴───────────┘
Config Drift: dev → prod
┌──────────────────────────┬──────────┬──────────────────────┬──────────────────────┬───────────┐
│ Key │ Change │ Old Value │ New Value │ Severity │
├──────────────────────────┼──────────┼──────────────────────┼──────────────────────┼───────────┤
│ database.endpoint │ ~ changed│ db-dev.internal:5432 │ db-prod.internal │ BREAKING │
│ secret.stripe_key │ - removed│ sk_test_*** │ │ BREAKING │
│ rate_limit.max_requests │ ~ changed│ 1000 │ 5000 │ INFO │
└──────────────────────────┴──────────┴──────────────────────┴──────────────────────┴───────────┘
Notice the severity levels:
| Severity | What It Means | CI Behavior |
|---|---|---|
| BREAKING | Critical key changed, added, or removed — database.*, auth.*, secret.*, password.*, token.*, endpoint.*, api_key.* | Exit 1 — block deploy |
| WARNING | Optional key added or removed | Log warning, don't block |
| INFO | Non-critical value changed (log levels, feature flags, etc.) | Informational only |
This triage is what makes ConfigDrift usable as a CI gate. If every difference failed the build, you'd disable the check within a week. By focusing on breaking severity, you only block on changes that actually cause incidents.
Workflow 3: JSON Output for Custom Pipeline Logic
Need more control than a simple pass/fail? Use --output json to get machine-readable drift data:
configdrift check staging.yaml prod.yaml --output json
Returns structured JSON you can pipe into jq, a Python script, or any CI tool:
{
"prod": {
"changes": [
{
"key": "database.endpoint",
"change_type": "changed",
"old_value": "db-staging.internal:5432",
"new_value": "db-prod.internal:5432",
"severity": "breaking",
"env": "prod"
},
{
"key": "logging.level",
"change_type": "changed",
"old_value": "debug",
"new_value": "info",
"severity": "info",
"env": "prod"
}
],
"has_breaking": true
}
}
Use this in a CI script to implement custom logic:
# Only block if more than 2 breaking changes
BREAKING=$(configdrift check staging.yaml prod.yaml --output json \
| python3 -c "import sys,json; d=json.load(sys.stdin); \
print(sum(1 for r in d.values() for c in r['changes'] if c['severity']=='breaking'))")
if [ "$BREAKING" -gt 2 ]; then
echo "FATAL: $BREAKING breaking config drifts detected"
exit 1
fi
Full GitHub Actions Workflow
Here's a production-ready CI workflow that gates deploys on config parity:
# .github/workflows/config-parity.yml
name: Config Parity Check
on:
pull_request:
paths:
- 'config/**'
- '.env.*'
- 'infra/**'
jobs:
config-parity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ConfigDrift
run: pip install git+https://github.com/Coding-Dev-Tools/configdrift.git
- name: Check staging → prod parity
run: |
configdrift check config/staging.yaml config/prod.yaml --output silent
- name: Full environment scan (non-blocking)
if: always()
run: |
configdrift scan ./config/dev ./config/staging ./config/prod --baseline dev
- name: Upload drift report
if: always()
run: |
configdrift check config/staging.yaml config/prod.yaml --output json \
> config-drift-report.json
- name: Archive drift report
if: always()
uses: actions/upload-artifact@v4
with:
name: config-drift-report
path: config-drift-report.json
This workflow:
- Blocks the PR if any breaking config drift exists between staging and prod
- Always runs the full scan for visibility (even if the check fails)
- Archives a JSON drift report as a CI artifact for post-incident review
What ConfigDrift Checks That Terraform Doesn't
| Drift Source | Terraform Detects? | ConfigDrift Detects? |
|---|---|---|
| Infrastructure resource changed | Yes | No |
| Application YAML config differs between envs | No | Yes |
| .env file missing a key in prod | No | Yes |
| TOML config has different database endpoint | No | Yes |
| Secret removed from one environment | No | Yes (BREAKING) |
| Feature flag added to staging only | No | Yes (WARNING) |
| Log level different across envs | No | Yes (INFO) |
Terraform and ConfigDrift are complementary. Terraform ensures your infrastructure resources are correct. ConfigDrift ensures the config files on top of those resources are consistent. You need both.
Supported Config Formats
ConfigDrift reads all common config file formats out of the box:
- YAML (
.yaml,.yml) — nested structures are flattened to dot-notation keys - JSON (
.json) — nested objects flattened like YAML - TOML (
.toml) — Python 3.10+ withtomlifallback - .env (
.env) — standardKEY=VALUEformat with quote stripping
Nested structures are automatically flattened to dot-notation keys. A YAML file like:
database:
endpoint: db.internal:5432
pool_size: 10
auth:
token_expiry: 3600
Is compared as:
database.endpoint = db.internal:5432
database.pool_size = 10
auth.token_expiry = 3600
So database.endpoint automatically gets BREAKING severity because it matches the critical key prefix.
Config File for Complex Projects
For projects with many environments and directory layouts, use a config file:
# Generate a template
configdrift init .
# Edit .configdrift.yaml
environments:
dev:
- ./config/dev
staging:
- ./config/staging
- ./secrets/staging
prod:
- ./config/prod
- ./secrets/prod
Then run the scan with:
configdrift scan --config .configdrift.yaml --baseline dev
Getting Started
Stop deploying with config drift
Install ConfigDrift and add a CI gate in 2 minutes. Your first check is one command.
pip install git+https://github.com/Coding-Dev-Tools/configdrift.git
cd your-project
configdrift check .env.staging .env.prod --output silent
View on GitHub →
ConfigDrift is part of the DevForge developer tool ecosystem — 11 CLI tools built by autonomous AI for autonomous developers.