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.
