September 18, 2025josh-pollara

Implementing Terraform drift detection with GitHub Actions

What you'll learn: How to automate drift detection with GitHub actions, so you can be notified of any changes to your infrastructure that could cause Terraform drift and take preventative measures before drift occurs. Set up notifications that help you focus on what matters and learn which tools you can use.

Introduction to Terraform drift detection

Infrastructure drift occurs when your live infrastructure no longer matches what your infrastructure code defines. Manual changes through cloud consoles, auto-scaling adjustments, and updates from other automation tools all create discrepancies between your defined state and actual resources. These changes accumulate over time, creating a gap between what your code says should exist and what actually exists in your cloud accounts.

Most teams discover drift only when something breaks. Without systematic drift detection, these surprises become increasingly common as the infrastructure scales and more teams interact with cloud resources. Catching drift early prevents these disruptions and maintains a reliable infrastructure footprint.

This guide shows you how to build automated drift detection using GitHub Actions. You'll implement scheduled workflows that continuously monitor your infrastructure, catch unauthorized changes early, and alert your team before drift causes problems.

The approach uses Terraform's native planning capabilities combined with GitHub's workflow automation to create a drift detection system that scales with your infrastructure.

⚡ ⚡ ⚡

What is Terraform drift and drift detection?

Terraform manages infrastructure through three states:

  • The configuration in your .tf files
  • The recorded state in terraform.tfstate
  • The actual state of resources in your cloud provider

Drift happens when actual resources diverge from what Terraform expects based on its state file.

This divergence occurs constantly in production environments. For example, when:

  • Engineers adjust load balancer health checks while troubleshooting
  • Auto-scaling groups modify instance counts based on traffic
  • Cloud providers update managed services with patches
  • Security teams apply firewall rules through the console during incidents

Each change creates a gap between Terraform's recorded state and reality.

Manual changes cause the most instances of production drift, such as when an engineer increases an RDS instance size to handle unexpected load, someone modifies security group rules for debugging, or a quick fix during an incident becomes permanent when no one updates the Terraform code.

External automation adds another change vector, with monitoring systems scaling resources, backup tools modifying retention policies, and security scanners adjusting permissions - all operating independently of Terraform's desired state.

These undocumented changes break deployments, hide security vulnerabilities, and force engineers to spend hours reconciling config with live infrastructure.

Terraform provides built-in capabilities to detect these discrepancies before they cause problems. The terraform plan command serves as a drift detector by comparing actual infrastructure against your defined configuration.

When automated through CI/CD pipelines, terraform plan can be used as a monitoring tool to catch drift. The command's exit codes distinguish between different states (no changes, errors, or drift detected), while JSON output enables parsing specific changes for alerts and reporting.

These features can be used to build automated drift detection that we'll show you how to implement with GitHub Actions.

⚡ ⚡ ⚡

How to set up a scheduled GitHub Action

GitHub Actions schedule workflows using cron syntax, allowing automated runs at specific intervals. For drift detection, daily checks ensure that unauthorised changes are caught within 24 hours, while avoiding alert fatigue from too-frequent notifications. Running checks at consistent times, such as early morning, provides fresh reports to review at the start of each workday.

Start by creating the workflow file in your repository at .github/workflows/drift-detection.yml. This location tells GitHub to recognize and execute the workflow according to the defined schedule:

name: Terraform Drift Detection

on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM UTC
  workflow_dispatch:      # Manual triggering for testing

permissions:
  id-token: write    # AWS OIDC authentication
  contents: read     # Repository checkout
  issues: write      # GitHub issue creation

jobs:
  detect-drift: # Job name - steps will be added below
    runs-on: ubuntu-latest
    timeout-minutes: 30

Multi-environment configuration

Most organizations run separate dev, staging, and production environments, each with its own Terraform state file and AWS account. The GitHub Actions matrix strategy enables you to check all environments in parallel rather than sequentially:

Configure the matrix to define your environments and their specific settings:

jobs:
  detect-drift:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging, prod]
        include:
          - environment: dev
            aws_account: '111111111111'
            working_dir: 'environments/dev'
          - environment: staging
            aws_account: '222222222222'
            working_dir: 'environments/staging'
          - environment: prod
            aws_account: '333333333333'
            working_dir: 'environments/prod'
    defaults:
      run:
        working-directory: ${{ matrix.working_dir }}

This configuration runs three parallel jobs, one for each environment. Each job operates in its own directory and authenticates to the appropriate AWS account.

Secure authentication with OIDC

OpenID Connect eliminates long-term AWS credentials in GitHub. Configure IAM roles that trust GitHub's OIDC provider to establish the connection:

steps:
  - name: Checkout
    uses: actions/checkout@v5

  - name: Configure AWS Credentials
    uses: aws-actions/configure-aws-credentials@main
    with:
      role-to-assume: arn:aws:iam::${{ matrix.aws_account }}:role/github-actions-terraform
      aws-region: us-east-1
      role-session-name: drift-detection-${{ matrix.environment }}

The IAM role needs S3 access for state files and read-only permissions for the resources managed in Terraform. Restrict the trust policy to your specific GitHub repository and workflow.

Running Terraform plan with drift detection

Execute terraform plan with flags that enable automated drift detection:

- name: Setup Terraform
  uses: hashicorp/setup-terraform@v3
  with:
    terraform_version: 1.6.0
    terraform_wrapper: false  # Clean output for parsing

- name: Initialize Terraform
  run: |
    terraform init \
      -backend-config="key=${{ matrix.environment }}/terraform.tfstate" \
      -input=false

- name: Detect Drift
  id: plan
  run: |
    set +e  # Continue on non-zero exit codes
    terraform plan \
      -detailed-exitcode \
      -no-color \
      -input=false \
      -out=tfplan \
      > plan_output.txt 2>&1

    PLAN_EXIT_CODE=$?
    echo "exitcode=$PLAN_EXIT_CODE" >> $GITHUB_OUTPUT

    if [ $PLAN_EXIT_CODE -eq 0 ]; then
      echo "✅ No drift detected in ${{ matrix.environment }}"
      echo "drift_detected=false" >> $GITHUB_OUTPUT
    elif [ $PLAN_EXIT_CODE -eq 2 ]; then
      echo "⚠️ Drift detected in ${{ matrix.environment }}"
      echo "drift_detected=true" >> $GITHUB_OUTPUT
    else
      echo "❌ Error running plan for ${{ matrix.environment }}"
      cat plan_output.txt
      exit 1
    fi

Exit code 2 specifically indicates that drift exists. By capturing output to a file, you can parse the code for specific changes and affected resources.

Parsing plan output to get actionable information

Extract details about drift to make notifications useful:


- name: Analyze Drift
  if: steps.plan.outputs.drift_detected == 'true'
  id: analysis
  run: |
    # Count change types
    CREATES=$(grep -c "will be created" plan_output.txt || echo "0")
    UPDATES=$(grep -c "will be updated" plan_output.txt || echo "0")
    DELETES=$(grep -c "will be destroyed" plan_output.txt || echo "0")
    REPLACES=$(grep -c "must be replaced" plan_output.txt || echo "0")

    echo "creates=$CREATES" >> $GITHUB_OUTPUT
    echo "updates=$UPDATES" >> $GITHUB_OUTPUT
    echo "deletes=$DELETES" >> $GITHUB_OUTPUT
    echo "replaces=$REPLACES" >> $GITHUB_OUTPUT

    # Extract changed resource addresses
    grep -E "^ # (.+) (will be|must be)" plan_output.txt | \
      sed 's/ # //' | \
      cut -d' ' -f1 > changed_resources.txt

    # Flag security-sensitive resources
    if grep -qE "aws_security_group|aws_iam|aws_s3_bucket_public" changed_resources.txt; then
      echo "security_sensitive=true" >> $GITHUB_OUTPUT
    else
      echo "security_sensitive=false" >> $GITHUB_OUTPUT
    fi

Deletions and replacements indicate more serious drift than updates, while security-related resources should probably get immediate attention.

⚡ ⚡ ⚡

How to configure the GitHub Action

After detecting drift, the findings should be made available for review by ops/engineering. Configuration involves setting up notifications, storing results for analysis, and filtering out expected changes to reduce noise.

Using GitHub Issues for drift tracking

GitHub Issues provide persistent tracking of drift over time. Rather than losing alerts in chat channels or email, issues create an audit trail and can be assigned to team members for resolution. The following configuration creates issues when drift is detected and updates existing issues if drift persists:

- name: Create Drift Issue
  if: steps.plan.outputs.drift_detected == 'true'
  uses: actions/github-script@v8
  with:
    script: |
      const fs = require('fs');
      const planOutput = fs.readFileSync('plan_output.txt', 'utf8');
      const environment = '${{ matrix.environment }}';

      // Check for existing open drift issues
      const { data: issues } = await github.rest.issues.listForRepo({
        owner: context.repo.owner,
        repo: context.repo.repo,
        state: 'open',
        labels: [`drift:${environment}`]
      });

      const title = `Drift detected in ${environment} environment`;
      const body = `
      ## Drift Detection Report
      **Environment:** ${environment}
      **Detection Time:** ${new Date().toISOString()}
      **Workflow Run:** [${context.runId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})

      ### Summary
      - Resources to create: ${{ steps.analysis.outputs.creates }}
      - Resources to update: ${{ steps.analysis.outputs.updates }}
      - Resources to delete: ${{ steps.analysis.outputs.deletes }}
      - Resources to replace: ${{ steps.analysis.outputs.replaces }}

      ### Plan Output
      \`\`\`
      ${planOutput.substring(0, 50000)}
      \`\`\`
      `;

      if (issues.length > 0) {
        // Add comment to existing issue
        await github.rest.issues.createComment({
          owner: context.repo.owner,
          repo: context.repo.repo,
          issue_number: issues[0].number,
          body: `New drift detected at ${new Date().toISOString()}\n\n${body}`
        });
      } else {
        // Create new issue
        await github.rest.issues.create({
          owner: context.repo.owner,
          repo: context.repo.repo,
          title: title,
          body: body,
          labels: [`drift:${environment}`, 'infrastructure']
        });
      }

The script prevents duplicate issues by checking for existing open drift reports. Additional drift adds comments to the existing issue rather than creating new ones.

Slack notifications

For immediate visibility, Slack notifications alert the team when drift occurs. Different severity levels help prioritize responses, with production environments and security-sensitive resources triggering urgent alerts:

- name: Send Slack Alert
  if: steps.plan.outputs.drift_detected == 'true'
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
  run: |
    # Determine urgency based on environment and security impact
    if [ "${{ matrix.environment }}" = "prod" ] || [ "${{ steps.analysis.outputs.security_sensitive }}" = "true" ]; then
      ALERT_PREFIX="🚨 URGENT: "
    else
      ALERT_PREFIX="⚠️ "
    fi

    curl -X POST $SLACK_WEBHOOK_URL \
      -H 'Content-Type: application/json' \
      -d @- <<EOF
    {
      "text": "${ALERT_PREFIX}Terraform drift detected in ${{ matrix.environment }}",
      "attachments": [{
        "color": "warning",
        "fields": [
          {"title": "Environment", "value": "${{ matrix.environment }}", "short": true},
          {"title": "Changes", "value": "${{ steps.analysis.outputs.creates }} creates, ${{ steps.analysis.outputs.updates }} updates, ${{ steps.analysis.outputs.deletes }} deletes", "short": true}
        ],
        "actions": [{
          "type": "button",
          "text": "View Details",
          "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
        }]
      }]
    }
    EOF

Production environments and security-sensitive changes trigger urgent notifications with different prefixes.

Artifact storage

Terraform plans contain detailed information that's valuable for debugging and analysis. Preserving this output allows teams to review exactly what changed, even after the workflow completes:

- name: Upload Drift Artifacts
  if: steps.plan.outputs.drift_detected == 'true'
  uses: actions/upload-artifact@v4
  with:
    name: drift-${{ matrix.environment }}-${{ github.run_number }}
    path: |
      plan_output.txt
      tfplan
      changed_resources.txt
    retention-days: 30

- name: Generate Summary
  if: always()
  run: |
    echo "## Drift Detection Summary - ${{ matrix.environment }}" >> $GITHUB_STEP_SUMMARY
    if [ "${{ steps.plan.outputs.drift_detected }}" = "true" ]; then
      echo "⚠️ **Drift Detected**" >> $GITHUB_STEP_SUMMARY
      echo "- Creates: ${{ steps.analysis.outputs.creates }}" >> $GITHUB_STEP_SUMMARY
      echo "- Updates: ${{ steps.analysis.outputs.updates }}" >> $GITHUB_STEP_SUMMARY
      echo "- Deletes: ${{ steps.analysis.outputs.deletes }}" >> $GITHUB_STEP_SUMMARY
    else
      echo "✅ **No Drift Detected**" >> $GITHUB_STEP_SUMMARY
    fi

Artifacts preserve the full Terraform plan for 30 days. The GitHub step summary provides immediate visibility in the workflow run page.

Filtering expected changes

Some drift patterns are expected and shouldn't trigger alerts. Add optional filtering to reduce noise:

- name: Filter Expected Drift
  if: steps.plan.outputs.drift_detected == 'true'
  id: filter
  run: |
    # Define patterns for expected drift (customize for your infrastructure)
    EXPECTED_PATTERNS="aws_autoscaling_group.*desired_capacity|aws_instance.*ami|random_password.*result"

    # Check if all changes match expected patterns
    if grep -vE "$EXPECTED_PATTERNS" changed_resources.txt > /dev/null; then
      echo "skip_notification=false" >> $GITHUB_OUTPUT
    else
      echo "skip_notification=true" >> $GITHUB_OUTPUT
    fi

Customize expected patterns based on your infrastructure's normal behavior. Auto-scaling adjustments and AMI updates commonly create harmless drift.

⚡ ⚡ ⚡

Terraform drift detection tools

Alternative approaches

While this guide focuses on GitHub Actions, several tools address drift detection with different approaches.

Terraform Enterprise provides native drift detection. The platform displays drift status in a centralized dashboard and sends notifications through email, Slack, or webhooks. The main limitation is cost: per-resource pricing becomes expensive for large infrastructures, often making GitHub Actions more economical even accounting for development time.

Open-source alternatives include several actively maintained projects. Terragrunt offers drift detection in the Gruntwork Platform. Meanwhile, cloud-concierge goes beyond detection to generate Terraform code for unmanaged resources and estimate cost impacts.

The GitHub Actions marketplace offers pre-built actions like dflook/terraform-check that handle Terraform installation, backend configuration, and output formatting. These save initial setup time but often make assumptions about directory structure and notification preferences that don't match every organization's needs.

Why build your own?

Custom drift detection provides control and flexibility that third-party tools might not offer. You can define detection logic that handles organization-specific edge cases, such as ignoring certain drift types, implementing custom severity calculations, or integrating with internal systems. Your infrastructure data stays within GitHub's environment rather than requiring third-party access to state files and cloud credentials.

Most importantly, you can adapt the system to your exact workflow rather than conforming to a tool's opinions about how drift detection should work.

⚡ ⚡ ⚡

Conclusion

Terraform drift detection through GitHub Actions solves a fundamental infrastructure management problem without requiring specialized tools or additional costs. The implementation handles the complete detection lifecycle: scheduled workflows check infrastructure nightly, parse Terraform output to identify changes, and route notifications based on severity and environment.

The approach scales with your infrastructure. The same matrix configuration pattern works for three AWS accounts or thirty. Adjustments to filtering logic accommodate your specific drift patterns - auto-scaling changes that should be ignored, security resources that demand immediate attention, and production environments that trigger different response procedures than development.

Building your own drift detection requires understanding Terraform's exit codes, parsing its plan output, and orchestrating GitHub Actions workflows. This investment pays off through complete control over detection logic, integration with your existing tools, and infrastructure data that never leaves your GitHub environment. You decide which drift matters, how to notify teams, and when to trigger automated responses.

For teams ready to implement more comprehensive Terraform automation, Terrateam extends beyond drift detection to provide plan and apply automation, cost estimation, and policy enforcement integrated directly with GitHub workflows. Explore these capabilities by signing up to use Terrateam.