Detecting Infrastructure Drift๏
Audience: Operators who want to know if live infrastructure has diverged from what the Terraform code declares โ before running a full deploy.
What is drift?๏
Drift is any difference between the current state of your infrastructure (recorded in the Terraform state file) and what your Terraform configuration says it should be.
Drift happens when:
Someone edits a resource directly in the cloud console.
An upstream provider makes an automatic change (e.g. a managed certificate renewal that adds unexpected tags).
A previous pipeline failed mid-apply, leaving resources in a partially-updated state.
strata deploy drift surfaces these differences without making any changes.
Quick start๏
strata deploy drift run -f deploy/deploy-prd.yaml
When drift is found, the output looks like:
๐ Checking drift for 2 stage(s) (threshold: info)โฆ
Stage: infrastructure
ADDRESS ACTION SEVERITY CHANGED ATTRIBUTES
---------------------------------------------------- -------- ---------- ------------------------------
azurerm_network_security_rule.allow_http [~] ๐ด critical priority, access
azurerm_virtual_machine.web01 [~] ๐ high os_disk_size_gb
azurerm_app_service.api [~] ๐ก medium app_settings
Summary: 3 change(s) โ ๐ด 1 critical ๐ 1 high ๐ก 1 medium ๐ต 0 low โช 0 info
Exit code 3 is returned when drift is found at or above the configured severity threshold. Exit code 0 means no drift (or all drift is below the threshold).
How it works๏
For each deployment stage:
terraform initโ ensure providers are up to date.terraform plan -detailed-exitcodeโ compare state to configuration.
Exit 0 = no changes, exit 2 = changes present.terraform show -jsonโ decode the plan to get structured resource changes.Classify each changed resource by severity using built-in + workspace rules.
Persist history at
.strata/drift/{deployment}.drift.json.
No resources are modified. No plan file is saved to the build directory.
Severity levels๏
Level |
What it means |
|---|---|
๐ด critical |
Security-sensitive: network security rules, IAM roles, key vault policies |
๐ high |
Core infrastructure: VM sizes, Kubernetes clusters, virtual networks |
๐ก medium |
Configuration: app settings, monitoring rules, kubernetes workloads |
๐ต low |
Cosmetic: tags, descriptions |
โช info |
Data sources, null resources, bookkeeping resources |
Tag changes are always low regardless of resource type โ they match the attribute-level
tags rule, which takes priority over any resource-type rule.
Command reference๏
strata deploy drift is a subgroup with three subcommands:
strata deploy drift run # detect drift (was: strata deploy drift)
strata deploy drift acknowledge # suppress a known-drifted address
strata deploy drift history # view run history and acknowledged addresses
drift run flags๏
strata deploy drift run -f <FILE> [OPTIONS]
Flag |
Default |
Description |
|---|---|---|
|
required |
Path to the deployment YAML file |
|
all stages |
Run drift detection on one stage only |
|
|
Minimum severity for exit code 3 (critical/high/medium/low/info) |
|
off |
Acknowledge all detected drift as baseline; always exits 0 |
|
console |
|
|
off |
Show terraform output lines |
Using severity thresholds๏
By default, any drift (including tag changes) triggers exit code 3. Use
--severity to ignore low-signal changes in CI pipelines:
# Fail only on security-sensitive or core-infrastructure changes
strata deploy drift run -f deploy/deploy-prd.yaml --severity high
# Fail on anything medium or above (ignore tags, data sources)
strata deploy drift run -f deploy/deploy-prd.yaml --severity medium
Exit code 0 is returned when all detected drift is below the threshold. The drift is still reported in the output โ the threshold only controls the exit code.
Checking one stage๏
strata deploy drift run -f deploy/deploy-prd.yaml --stage infrastructure
Useful when a specific stage is suspected to have drift and you donโt want to run
terraform init for every stage.
Setting a baseline๏
After a known-good deploy, reset the drift history so age tracking starts from now:
strata deploy drift run -f deploy/deploy-prd.yaml --baseline
This runs drift detection, shows the report, acknowledges every detected entry, clears
the run history, and records a baseline_at timestamp. Future checks will treat those
resources as having first_detected from the next run they appear in.
--baseline always exits 0, regardless of what drift was found.
Acknowledging expected drift๏
Some resources drift intentionally โ auto-scalers change replica counts, policy agents add tags. Rather than resetting the entire baseline, suppress individual addresses:
# Suppress one address
strata deploy drift acknowledge -f deploy/deploy-prd.yaml \
--address "azurerm_autoscale_setting.web" \
--reason "auto-scaler managed โ expected drift"
# Re-enable reporting for an address
strata deploy drift acknowledge -f deploy/deploy-prd.yaml \
--address "azurerm_autoscale_setting.web" \
--remove
Acknowledged addresses are excluded from the report and do not affect the exit code. They remain suppressed until explicitly removed or until the baseline is reset.
drift acknowledge flags๏
Flag |
Required |
Description |
|---|---|---|
|
yes |
Deployment YAML to identify the deployment |
|
yes |
Terraform resource address (e.g. |
|
no |
Explanation stored in history for audit purposes |
|
no |
Remove a previously added acknowledgement |
Viewing drift history๏
strata deploy drift history -f deploy/deploy-prd.yaml
strata deploy drift history -f deploy/deploy-prd.yaml --last 5
Shows:
A table of recent drift-check runs with their timestamps and drifted resource counts.
A list of currently acknowledged (suppressed) addresses.
drift history flags๏
Flag |
Default |
Description |
|---|---|---|
|
yes |
Deployment YAML to identify the deployment |
|
|
Number of most-recent runs to display |
|
console |
|
JSON output for CI๏
strata deploy drift run -f deploy/deploy-prd.yaml --output json
Output shape:
{
"deployment": "my-deployment",
"checked_at": "2026-07-06T09:00:00Z",
"stages_checked": ["infrastructure", "services"],
"has_drift": true,
"max_severity": "critical",
"summary": {
"critical": 1, "high": 0, "medium": 1, "low": 0, "info": 0, "total": 2
},
"entries": [
{
"address": "azurerm_network_security_rule.allow_http",
"resource_type": "azurerm_network_security_rule",
"action": "update",
"severity": "critical",
"stage": "infrastructure",
"changed_attributes": ["priority", "access"],
"before": { "priority": 100, "access": "Allow" },
"after": { "priority": 200, "access": "Deny" },
"first_detected": "2026-07-05T08:00:00Z",
"consecutive_checks": 2
}
]
}
Sensitive values (flagged by Terraform) are replaced with "(sensitive)" so they
are never written to history or output.
Drift history๏
Every run appends to .strata/drift/{deployment}.drift.json. This file tracks:
first_detectedโ timestamp of the first run that found this resource drifting.last_detectedโ timestamp of the most recent run that found it drifting.consecutive_checksโ how many consecutive runs found it in a drifted state.
consecutive_checks is reset to 0 when a resource stops drifting (e.g. after a deploy
or a manual fix). It is never overwritten: if a resource was first detected 30 runs
ago, that timestamp is preserved.
The history file is automatically added to .gitignore.
Customising severity rules๏
To override severity for specific resource types in your workspace, create
.strata/drift_rules.yaml. Workspace rules take priority over the built-in rules:
# .strata/drift_rules.yaml
rules:
# Our team considers tag drift worth tracking as medium (not low)
- attribute: "tags"
severity: medium
# We manage this resource manually โ treat any drift as info only
- resource_type: "azurerm_management_lock"
severity: info
defaults:
severity: medium
Rules are matched in this order:
Attribute-level rules (most specific โ any resource with this attribute).
Resource-type rules (exact or glob, e.g.
azurerm_vm*).defaults.severity(catch-all).
Workspace rules are prepended to the built-in rule list, so they are evaluated first. You cannot remove built-in rules; you can only add higher-priority rules that match before them.
Using drift in CI๏
Add a drift check step before every deploy to catch out-of-band changes:
# .github/workflows/deploy.yml (example)
- name: Check for drift
run: strata deploy drift run -f deploy/deploy-prd.yaml --severity high
continue-on-error: true # report drift but don't block the deploy
- name: Deploy
run: strata deploy run -f deploy/deploy-prd.yaml --force
Or fail the pipeline on critical drift:
- name: Fail on critical drift
run: strata deploy drift run -f deploy/deploy-prd.yaml --severity critical
Nightly scheduled drift check๏
A reusable GitHub Actions workflow is provided at
.github/workflows/drift-check.yml. Use it directly:
# .github/workflows/nightly.yml
jobs:
drift:
uses: ./.github/workflows/drift-check.yml
with:
deployment: deploy/production.yaml
severity: high
secrets: inherit
The workflow:
Runs nightly at 02:00 UTC.
Posts a Slack notification when critical/high drift is found (optional โ set
SLACK_DRIFT_WEBHOOKsecret).Uploads
drift-report.jsonas an artifact (kept 90 days).Exits 3 when drift at or above
severityis detected.
Exit codes๏
Code |
Meaning |
|---|---|
|
No drift found, or all drift is below |
|
Execution error (terraform failure, auth problem, missing build) |
|
Drift found at or above |
Prerequisites๏
Build artifacts must exist (
strata build runmust have run first).Terraform must be on
PATH(strata tools statusto verify).Authentication to the Terraform backend must be configured (same credentials required by
terraform planโ e.g.az loginfor Azure,AWS_*env vars for AWS).