Skip to main content

Infrastructure Drift Detection

Detect when your cloud infrastructure diverges from your IaC definitions using Terraform plan, driftctl, and CloudQuery. Set up automated drift alerts before manual changes cause incidents.

✓ Terraform plan analysis ✓ driftctl unmanaged resources ✓ CloudQuery SQL queries ✓ CI drift alerts

Method 1 — Terraform Plan as Drift Detector

# Run terraform plan in CI and parse for unexpected changes
cd terraform/

# Refresh state to pull latest cloud state, then plan
terraform init -reconfigure
terraform refresh  # or use -refresh=true in plan

# Generate machine-readable plan output
terraform plan -refresh=true -out=tfplan.binary -detailed-exitcode
EXIT_CODE=$?
# Exit code: 0 = no changes, 1 = error, 2 = changes detected (drift!)

terraform show -json tfplan.binary > tfplan.json

# Parse for changes using jq
jq '[.resource_changes[] | select(.change.actions | contains(["update"]) or contains(["delete"]))] |
  map({resource: .address, actions: .change.actions, before: .change.before, after: .change.after})' \
  tfplan.json

# Count drift by type
jq '[.resource_changes[] | .change.actions[]] |
  group_by(.) | map({action: .[0], count: length})' tfplan.json

# Exit 1 in CI if drift detected
if [ $EXIT_CODE -eq 2 ]; then
  echo "DRIFT DETECTED: $(jq '.resource_changes | length' tfplan.json) resources changed"
  exit 1
fi

Method 2 — driftctl for Unmanaged Resources

# Install driftctl
curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_linux_amd64 \
  -o /usr/local/bin/driftctl && chmod +x /usr/local/bin/driftctl

# Scan AWS resources against Terraform state
export AWS_PROFILE=production
driftctl scan \
  --from tfstate+s3://my-terraform-state/prod/terraform.tfstate \
  --output json://drift-report.json

# View coverage and drift summary
driftctl scan \
  --from tfstate+s3://my-terraform-state/prod/terraform.tfstate \
  --output console

# Example output:
# Found 14 resource(s) not covered by IaC: (unmanaged)
#   - aws_s3_bucket.manual-uploads-2024
#   - aws_security_group.sg-0abc1234
#   - aws_iam_role.manual-role
# Coverage: 87.3% (127 managed / 14 unmanaged)

# Scan multiple state files (multi-env)
driftctl scan \
  --from tfstate+s3://state-bucket/prod/terraform.tfstate \
  --from tfstate+s3://state-bucket/staging/terraform.tfstate

# Ignore known unmanaged resources in .driftignore
cat > .driftignore <<EOF
# Intentionally unmanaged — created by EKS node group
aws_instance.*
aws_volume_attachment.*
# Managed by external team
aws_iam_role.external-team-role
EOF

Method 3 — CloudQuery SQL for Drift Analysis

# Install CloudQuery
curl -L https://github.com/cloudquery/cloudquery/releases/latest/download/cloudquery_linux_amd64 \
  -o /usr/local/bin/cloudquery && chmod +x /usr/local/bin/cloudquery

# cloudquery.yml — sync AWS to PostgreSQL
kind: source
spec:
  name: aws
  registry: cloudquery
  path: cloudquery/aws
  version: "v23.0.0"
  tables: ["aws_ec2_instances", "aws_ec2_security_groups",
           "aws_iam_roles", "aws_s3_buckets"]
  destinations: ["postgresql"]
---
kind: destination
spec:
  name: postgresql
  registry: cloudquery
  path: cloudquery/postgresql
  version: "v7.0.0"
  spec:
    connection_string: "postgresql://user:pass@localhost:5432/cloudquery"

# Run sync
cloudquery sync cloudquery.yml

# SQL: Find EC2 instances without required tags (drift from policy)
SELECT
  instance_id,
  instance_type,
  state_name,
  tags
FROM aws_ec2_instances
WHERE
  state_name = 'running'
  AND (tags ->> 'Environment' IS NULL
       OR tags ->> 'Team' IS NULL
       OR tags ->> 'Project' IS NULL);

-- Security groups with 0.0.0.0/0 inbound rules (unexpected open ports)
SELECT
  sg.group_id,
  sg.group_name,
  perm.from_port,
  perm.to_port,
  perm.ip_protocol
FROM aws_ec2_security_groups sg,
  jsonb_array_elements(sg.ip_permissions) AS perm,
  jsonb_array_elements(perm -> 'IpRanges') AS ip_range
WHERE ip_range ->> 'CidrIp' = '0.0.0.0/0'
  AND perm ->> 'IpProtocol' != '-1'
ORDER BY perm ->> 'FromPort';

CI Integration — GitHub Actions Drift Alert

# .github/workflows/drift-detection.yml
name: Infrastructure Drift Detection

on:
  schedule:
    - cron: '0 8 * * *'   # Daily at 8 AM UTC
  workflow_dispatch:

jobs:
  drift-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Terraform Init
        run: terraform init
        working-directory: terraform/

      - name: Detect Drift
        id: drift
        run: |
          terraform plan -refresh=true -detailed-exitcode \
            -out=tfplan.binary 2>&1 | tee plan_output.txt
          echo "exit_code=$?" >> $GITHUB_OUTPUT
        working-directory: terraform/
        continue-on-error: true

      - name: Alert on Drift
        if: steps.drift.outputs.exit_code == '2'
        uses: slackapi/[email protected]
        with:
          payload: |
            {
              "text": ":rotating_light: *Infrastructure Drift Detected*\nTerraform plan shows unexpected changes in production.\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DRIFT_WEBHOOK }}

Drift Severity Classification

Severity Resource Type Response
CriticalIAM policies, security groups, KMS keysImmediate alert + auto-remediation
HighInstance types, RDS configs, network ACLsAlert within 1 hour
MediumAuto-scaling groups, load balancer rulesDaily report
LowTags, descriptions, metadataWeekly batch review

Frequently Asked Questions

What is infrastructure drift and why is it dangerous?▼

Infrastructure drift occurs when the actual state of your cloud infrastructure diverges from the state defined in your infrastructure-as-code (Terraform, Pulumi, etc.). It happens when someone manually changes a resource in the cloud console, when a resource is modified by an automated process outside IaC, or when resources are created outside Terraform. Drift is dangerous for several reasons: your IaC no longer accurately represents what's running (so terraform apply can destroy manual changes), security configurations may have been altered manually (opening ports, changing IAM policies), and incident response becomes harder because the 'ground truth' is unclear. Regular drift detection is an essential operational discipline.

How does driftctl differ from terraform plan for drift detection?▼

terraform plan detects drift only for resources Terraform already manages (those in the state file). driftctl detects both: managed resources that have drifted (differ from state) AND unmanaged resources — cloud resources that exist but have no corresponding Terraform state at all. Unmanaged resources are often the more important finding: shadow infrastructure, forgotten resources running up bills, or security risks. driftctl also provides a coverage score showing what percentage of your cloud infrastructure is under IaC management. terraform plan is a prerequisite for every apply; driftctl is a periodic audit tool that answers 'what's out there that we don't control?'

How should I handle drift when it's detected?▼

Drift handling depends on type. For managed resource drift (Terraform knows about it but actual state differs): determine if the manual change was intentional. If yes, update your Terraform code to match and run apply (or use terraform import if needed). If no, run terraform apply to restore the desired state. For unmanaged resources: decide to either import into Terraform (terraform import) so it's managed going forward, or use driftctl's ignore file (.driftignore) to explicitly acknowledge it. Never just delete unmanaged resources without investigation — they may be critical. Build a drift triage process: high-severity (security group changes, IAM policy changes) trigger immediate alerts; low-severity (tag changes) are batched for weekly review.

Can I detect drift in Kubernetes resources with these tools?▼

Yes, but different tools are better suited. CloudQuery can query Kubernetes resources alongside cloud resources and detect configuration drift via SQL comparisons. For Kubernetes-specific drift, consider: (1) Flux and ArgoCD both detect drift natively — they show 'OutOfSync' status when the cluster state differs from Git. (2) config-sync (Google) does the same. (3) kubectl diff can show differences between running state and your local manifests. For Helm-managed resources, helm diff (a Helm plugin) shows what a helm upgrade would change. GitOps tools solve the Kubernetes drift problem fundamentally by continuously reconciling.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →