Tutorial

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:

What's missing: are the configs in staging identical to what's about to land in prod?

This matters because:

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:

SeverityWhat It MeansCI Behavior
BREAKINGCritical key changed, added, or removed — database.*, auth.*, secret.*, password.*, token.*, endpoint.*, api_key.*Exit 1 — block deploy
WARNINGOptional key added or removedLog warning, don't block
INFONon-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:


What ConfigDrift Checks That Terraform Doesn't

Drift SourceTerraform Detects?ConfigDrift Detects?
Infrastructure resource changedYesNo
Application YAML config differs between envsNoYes
.env file missing a key in prodNoYes
TOML config has different database endpointNoYes
Secret removed from one environmentNoYes (BREAKING)
Feature flag added to staging onlyNoYes (WARNING)
Log level different across envsNoYes (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:

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.