Setting up drift detection in CI always took the same 15 minutes of boilerplate. Install Python. Install tfdrift. Wire up secrets. Figure out how to upload SARIF. Handle exit codes so the job doesn't fail before the artifact uploads. Add a second scan run because SARIF and JSON can't come from the same command.
Every repo. Every time.
I got tired of it, so I published tfdrift as a GitHub Actions Marketplace action. Now it's one uses: line.
What Changed
Before:
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install tfdrift
- run: tfdrift scan --format sarif --output results.sarif
continue-on-error: true
- run: tfdrift scan --format json --output report.json --quiet
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
After:
- uses: sudarshan8417/tfdrift@v1
with:
sarif-output: results/tfdrift.sarif
That's it. Python setup, install, scan, PR annotations, step summary, and SARIF output are all handled.
How It Works
The action runs tfdrift scan under the hood. Because it runs inside GitHub Actions, the existing --gha path fires automatically: every drifted resource becomes an inline PR annotation and the job summary gets a drift table.
Here's a full scheduled scan with Code Scanning upload and Slack notifications:
# .github/workflows/drift-check.yml
name: Terraform Drift Check
on:
schedule:
- cron: '0 */6 * * *'
pull_request:
workflow_dispatch:
jobs:
drift:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # for Code Scanning upload
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Detect drift
id: drift
uses: sudarshan8417/tfdrift@v1
with:
path: .
min-severity: low
fail-on: high
sarif-output: results/tfdrift.sarif
json-output: results/drift-report.json
slack-webhook: ${{ secrets.SLACK_WEBHOOK }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results/tfdrift.sarif
Six hours later your Security tab has entries like this:
● aws_security_group.app — ingress rules modified out-of-band [critical]
● aws_instance.web — instance_type drifted from t3.medium to t3.large [high]
Drift surfacing as native GitHub security alerts means it lives in the same place as your Dependabot findings and CodeQL results: one dashboard, one triage workflow.
The SARIF + JSON Problem (and How We Solved It)
Originally, if you wanted both a SARIF file for Code Scanning and a JSON file for artifact download, you had to run the scan twice. That meant running terraform plan twice, which is expensive for large workspaces.
v0.5.4 adds --sarif-output as a secondary output flag. One scan run, two files:
tfdrift scan \
--format json --output drift-report.json \
--sarif-output results/tfdrift.sarif
The action uses this internally so you never pay the cost of two plan runs.
PR Gate: Block Merges on Critical Drift
The second use case is a PR gate. If someone merges a PR while critical drift is active, your next apply might conflict or fail unexpectedly. This blocks it:
# .github/workflows/drift-gate.yml
name: Drift Gate
on:
pull_request:
paths:
- '**.tf'
- '**.tfvars'
jobs:
drift-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- uses: sudarshan8417/tfdrift@v1
with:
min-severity: medium # ignore tag noise in PRs
fail-on: high # block only on high+ severity
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
The job exits 1 on high or critical drift and appears as a failing required check on the PR.
Action Outputs for Conditional Steps
The action exposes two outputs, drift-count and has-drift, so you can branch on them in downstream steps:
- name: Detect drift
id: drift
uses: sudarshan8417/tfdrift@v1
with:
fail-on: critical # only fail the job on critical
- name: Do something if drift was detected
if: steps.drift.outputs.has-drift == 'true'
run: |
echo "Drifted resources: ${{ steps.drift.outputs.drift-count }}"
# call your ticketing API here
This lets you fail the job at one severity threshold while still triggering notifications or ticket creation at a lower one, without running a second scan.
Full Inputs Reference
| Input | Default | Description |
|---|---|---|
path |
. |
Root directory to scan |
min-severity |
info |
Minimum severity to report |
fail-on |
(any drift) | Severity threshold to exit 1 |
sarif-output |
— | Path for SARIF file |
json-output |
— | Path for JSON report |
exclude-resource |
— | fnmatch pattern to skip noisy resources |
binary |
terraform |
Path to terraform or tofu |
workers |
4 |
Parallel scan workers |
tfdrift-version |
latest | Pin a specific PyPI version |
slack-webhook |
— | Slack Incoming Webhook URL |
opsgenie-key |
— | OpsGenie API key |
Getting Started
Install tfdrift locally to test before wiring up CI:
pip install tfdrift
tfdrift scan --path ./infra
Then drop the action into any repo that uses Terraform. The only hard requirement is that terraform (or tofu) is on PATH before the action runs. hashicorp/setup-terraform@v3 handles that.
Full docs and examples: github.com/sudarshan8417/tfdrift
What's Next
-
Auto-PR: when drift is detected, automatically open a GitHub PR with the AI-generated
.tfremediation file - Jira / Linear integration: create tickets directly from drift alerts
-
tfdrift serve: HTTP API mode for embedding drift detection in internal portals and Backstage plugins
If you're using tfdrift or have feedback, open an issue or drop a star on GitHub. Both go a long way for an open-source project.
Top comments (0)