Detect and remediate unauthorized changes to infrastructure. Covers drift detection tools, reconciliation strategies, policy enforcement, and the patterns that ensure your actual infrastructure matches your declared state.
Infrastructure drift occurs when a system's actual state diverges from its intended state—configuration, software, network, or hardware—over time. It matters most in environments where infrastructure is declared (infrastructure as code), but not continuously enforced: after deployments, upgrades, manual changes, or incident responses, the live system slowly departs from its original blueprint. Drift detection is the practice of identifying, reporting, and tracking these discrepancies, enabling teams to maintain consistency and reliability across their environments.
The first step is defining a canonical representation of the desired infrastructure state. This is typically a set of configuration files, versioned in Git, that describe the full environment: servers, services, network rules, security policies, and software versions.
version: 1.0
services:
- name: web
image: nginx:1.25
ports:
- 80:80
environment:
- NODE_ENV=production
- LOG_LEVEL=info
volumes:
- /var/www/html:/usr/share/nginx/html
restart: always
- name: database
image: postgres:15
ports:
- 5432:5432
environment:
- POSTGRES_DB=app
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=securepass
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
driver: local
options:
type: ext4
device: /dev/sdb
This file serves as the source of truth. It may be generated by a tool like Terraform, Pulumi, or a custom script, but it is not assumed to be *executed*—only *declared*. The intended state is frozen at a Git commit, and the detection process compares this to what is running.
The actual state is collected from the live system. This can be done via agents, periodic snapshots, or ad-hoc queries. The process must gather enough detail to compare against the intended state.
# Collect actual state from a single host
$ sudo systemctl status nginx
$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
$ curl -s http://localhost:8080/health
$ cat /etc/nginx/nginx.conf
$ ip route show
$ lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
For Kubernetes clusters, actual state is extracted using kubectl:
$ kubectl get pods,deployments,services,configmaps,secrets -A -o yaml
$ kubectl get nodes -o json
$ kubectl describe node worker-01
These outputs are combined into a single structured report. The result is a JSON object containing all relevant runtime data, including:
The comparison is the heart of drift detection. It aligns the intended state (from infrastructure.yaml) with the actual state (from the live system), then identifies differences.
A common approach is to define a set of checks in a configuration file that specifies how to match and compare each component.
# drift-checks.yaml
checks:
- type: container
name: nginx
expected:
image: nginx:1.25
ports:
- 80:80
environment:
- NODE_ENV=production
- LOG_LEVEL=info
volumes:
- /var/www/html:/usr/share/nginx/html
restart: always
actual:
source: docker ps
key: name
fields:
- image
- ports
- environment
- volumes
- restart
- type: file
name: nginx.conf
path: /etc/nginx/nginx.conf
expected:
content: |
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
}
actual:
source: file
path: /etc/nginx/nginx.conf
- type: network
name: web-service-port
expected:
port: 80
protocol: tcp
listening: true
actual:
source: netstat
command: netstat -tuln
parser: grep "LISTEN.*:80"
fields:
- port
- protocol
- listening
The comparison engine reads both the intended and actual state, applies the checks, and outputs a report.
{
"drift": [
{
"check": "nginx",
"type": "container",
"status": "drifted",
"details": [
{
"field": "image",
"expected": "nginx:1.25",
"actual": "nginx:1.24",
"changed": true
},
{
"field": "environment",
"expected": [
"NODE_ENV=production",
"LOG_LEVEL=info"
],
"actual": [
"NODE_ENV=production",
"LOG_LEVEL=info",
"DEBUG=true"
],
"extra": [ "DEBUG=true" ],
"missing": []
},
{
"field": "volumes",
"expected": [ "/var/www/html:/usr/share/nginx/html" ],
"actual": [ "/var/www/html:/usr/share/nginx/html", "/data:/data" ],
"extra": [ "/data:/data" ],
"missing": []
}
]
},
{
"check": "nginx.conf",
"type": "file",
"status": "identical",
"details": []
}
]
}
Once drift is detected, the system must surface it to operators and developers. This involves generating reports, storing results, and triggering alerts.
A standard format is a structured JSON report stored in a database or file system.
{
"timestamp": "2024-04-15T12:34:56Z",
"target": "production",
"environment": "prod",
"checks": [
{
"name": "nginx",
"status": "drifted",
"diffs": [
{
"field": "image",
"expected": "nginx:1.25",
"actual": "nginx:1.24"
},
{
"field": "environment",
"expected": [ "NODE_ENV=production", "LOG_LEVEL=info" ],
"actual": [ "NODE_ENV=production", "LOG_LEVEL=info", "DEBUG=true" ]
}
],
"metadata": {
"host": "web-01.prod.example.com",
"source": "docker ps"
}
}
],
"summary": {
"total_checks": 12,
"drifted_checks": 5,
"new_drifts": 3,
"resolved_drifts": 2
}
}
This report can be used to populate a dashboard, trigger a notification, or feed into a change management system.
Alerts are configured based on thresholds and conditions:
drifted_checks > 0field: image AND expected: nginx:1.25 AND actual: nginx:1.24new_drifts > 0Alerts are sent via email, Slack, or a webhook:
{
"title": "Infrastructure Drift Detected",
"text": "5 checks are drifting in production.\n\n- nginx: image mismatch (1.25 → 1.24)\n- database: missing LOG_LEVEL=debug\n- nginx.conf: extra DEBUG=true in environment",
"channel": "#infrastructure",
"attachments": [
{
"title": "Drift Report",
"text": "Full report available at https://drift.example.com/reports/2024-04-15T12:34:56Z",
"fields": [
{ "title": "Total Checks", "value": "12" },
{ "title": "Drifted", "value": "5" },
{ "title": "New", "value": "3" },
{ "title": "Resolved", "value": "2" }
],
"color": "#FF6B35"
}
]
}
Drift detection is not a one-off operation. It must be automated and scheduled.
A cron job runs the full detection pipeline every 15 minutes:
# Run drift detection every 15 minutes
*/15 * * * * /opt/drift/detect.sh --target=production --output=/var/reports/drift-$(date +\%Y-\%m-\%d-\%H-\%M).json
The script orchestrates the entire workflow:
#!/bin/bash
# detect.sh
set -euo pipefail
TARGET="${1:-}"
OUTPUT="${2:-/tmp/drift-report.json}"
# 1. Pull latest infrastructure.yaml
git pull origin main
# 2. Collect actual state from all nodes in target
mkdir -p /tmp/actual
for host in $(cat /etc/hosts | grep -v '^#' | awk '{print $2}' | sort -u); do
ssh "$host" 'systemctl status nginx; docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"; ip route show' > /tmp/actual/"$host".out
done
# 3. Run comparison
python3 /opt/drift/compare.py \
--intended infrastructure.yaml \
--actual /tmp/actual \
--checks drift-checks.yaml \
--output "$OUTPUT"
# 4. Upload to central store
curl -X POST \
-H "Content-Type: application/json" \
-d "@$OUTPUT" \
https://api.infrastructure.example.com/v1/reports
This pipeline can be triggered manually, on demand, or in response to events (e.g., after a deployment).
Drift detection often fails silently. A check may be defined but not executed due to misconfiguration.
source field: If a check expects source: docker ps, but the actual state collection step does not include docker ps, the check returns null or [], and the comparison assumes the field is missing even when it’s present but not collected.parser: json, but the actual output is YAML. The engine fails to parse and returns null for the field, causing drift even when data is correct.key: name, but the actual output includes Name (capitalized), name, or container_name. Without case-insensitive matching, the check fails to link expected and actual containers.expected vs actual: A configuration key expected.image defines the intended image, but the actual state may be pulled from docker inspect. If the actual state includes the image ID, but expected.image is only the tag, the comparison fails even when the image is correct.fields vs include: fields defines which fields to compare; include defines which data to extract. Confusing them leads to comparing fields that were never extracted.parser options: A parser may support json, yaml, regex, csv, or text. But without specifying delimiter: comma, a csv parser interprets a,b,c as a single field instead of three.infrastructure.yaml but forgets to commit and push. The next drift detection uses an old version, leading to false positives.To avoid double-counting and inconsistent state, drift detection should be idempotent: running it multiple times produces the same result.
This requires:
timestamp field, enabling accurate tracking.Drift detection integrates naturally into CI/CD:
# .github/workflows/drift.yml
name: Drift Detection
on:
push:
branches: [ main ]
jobs:
detect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
./detect.sh --target=staging
curl -X POST https://api.infrastructure.example.com/v1/reports \
-H "Authorization: Bearer $DRIFT_API_KEY" \
-d @report.json
- run: |
# Optional: Send summary to Slack
curl -X POST https://hooks.slack.com/services/... \
-H "Content-Type: application/json" \
-d '{
"text": "Drift detection completed in staging.",
"attachments": [
{
"title": "Summary",
"fields": [
{ "title": "Drifted", "value": "3" },
{ "title": "Total", "value": "10" }
]
}
]
}'
This ensures that every code change triggers a drift detection, validating that the new code does not introduce configuration drift.
Infrastructure drift detection is the process of identifying and reporting the difference between a system's intended and actual state. It relies on a clear definition of the intended state, collection of actual state from live systems, and a structured comparison process. The output is a report that enables alerting, tracking, and remediation. The workflow is repeatable, automated, and robust when attention is paid to edge cases: silent failures, configuration confusion, and idempotency. When properly implemented, drift detection becomes a foundational practice for maintaining consistency across infrastructure.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy