Implementing Terraform drift detection with GitHub Actions
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.