Configuration Drift Detection Guide
EXPERIMENTAL ONLY: Drift detection and reconciliation behavior is not production-grade. Results may be incomplete, and automated accept/revert operations should be treated as test-only. Do not use this workflow as your production compliance control.
Overviewâ
Configuration drift occurs when Azure resources are modified outside the Infrastructure-as-Code (IaC) workflow. This can happen through:
- Manual Portal Changes - Developers or operators modify resources via Azure Portal
- Azure Policy Remediations - Compliance policies automatically fix non-compliant resources
- Automated Tooling - Third-party tools or scripts make changes
- Emergency Hotfixes - Production incidents requiring immediate changes
Git-Ape's drift detection system helps you:
- Detect configuration differences between Azure and your IaC
- Analyze the severity and impact of changes
- Reconcile by accepting drift (update IaC) or reverting drift (redeploy)
- Audit all drift actions with complete logging
Quick Startâ
Check for Driftâ
Agent Workflow:
User: @git-ape check for drift on deploy-20260218-143022
Agent: Running drift detection...
â ī¸ Configuration Drift Detected
- Critical: 1 (security settings changed)
- Warning: 2 (tags and config modified)
What would you like to do?
A. Accept Drift (update IaC)
B. Revert Drift (redeploy original state)
C. Review Details
D. Ignore (mark as known)
Command Line:
# Check specific deployment
.github/skills/azure-drift-detector/scripts/detect-drift.sh \
--deployment-id deploy-20260218-143022
# Check all deployments
.github/skills/azure-drift-detector/scripts/drift-check-all.sh
Accept Drift (Update IaC to Match Azure)â
When to use:
- Azure Policy enforced a security improvement
- Manual change was intentional and should be kept
- Emergency hotfix should become permanent
Agent Workflow:
User: A (Accept Drift)
Agent: Accepting drift...
â Backed up original requirements.json
â Updated IaC with current Azure state
â Logged acceptance to audit trail
â Created git commit (if --auto-commit)
Your IaC now matches Azure.
Command Line:
.github/skills/azure-drift-detector/scripts/accept-drift.sh \
--deployment-id deploy-20260218-143022 \
--reason "Azure Policy compliance requirement" \
--auto-commit
Revert Drift (Restore IaC State)â
When to use:
- Unauthorized change detected
- Security settings were weakened
- Change violated compliance policy
Agent Workflow:
User: B (Revert Drift)
Agent: â ī¸ This will redeploy resources to restore original state.
Type 'confirm revert' to proceed:
User: confirm revert
Agent: Reverting drift...
đ Deployment Started (revert)
â httpsOnly: false â true (restored)
â minimumTlsVersion: 1.0 â 1.2 (restored)
â
Drift reverted successfully
Command Line:
.github/skills/azure-drift-detector/scripts/revert-drift.sh \
--deployment-id deploy-20260218-143022 \
--confirm
Drift Severity Levelsâ
đ´ Critical Driftâ
Security-impacting changes that introduce vulnerabilities
Examples:
httpsOnly: true â false- Allows unencrypted trafficminimumTlsVersion: "1.2" â "1.0"- Weakens encryptionpublicNetworkAccess: Disabled â Enabled- Exposes resource publicly- Managed identity disabled
- Diagnostic logging disabled
Recommended Action: Revert immediately unless change was intentional and approved.
đĄ Warning Driftâ
Configuration or compliance changes that don't directly impact security
Examples:
- Tag changes (
Environment: dev â prod) - Runtime version changes (
python@3.11 â python@3.10) - Scaling configuration (
instanceCount: 2 â 4) - Non-critical app settings
Recommended Action: Review change reason, accept if intentional, revert if unauthorized.
âšī¸ Info Driftâ
Cosmetic or Azure-managed properties
Examples:
- Last modified timestamp
- Azure-generated resource IDs
- Auto-scaling metrics
- System-managed tags
Recommended Action: Usually safe to accept or ignore.
Drift Detection Workflowâ
Step 1: Identify Target Deploymentâ
List available deployments:
.github/scripts/deployment-manager.sh list
Output:
Recent Deployments:
ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ID Status Resource Type Region
ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
deploy-20260218-143022 success Function App eastus
deploy-20260217-100000 success Web App + SQL westus2
deploy-20260215-093022 success Storage Account eastus
Step 2: Run Drift Detectionâ
Detect changes between Azure and stored state:
.github/skills/azure-drift-detector/scripts/detect-drift.sh \
--deployment-id deploy-20260218-143022 \
--output-format markdown \
--verbose
What happens:
- Loads deployment metadata from
.azure/deployments/deploy-20260218-143022/ - Queries current Azure state via
az resource showfor each resource - Compares properties by resource type:
- Function Apps:
httpsOnly,FUNCTIONS_WORKER_RUNTIME, identity - Storage:
minimumTlsVersion,supportsHttpsTrafficOnly - All resources: tags
- Function Apps:
- Classifies severity: Critical, Warning, Info
- Generates report
Output files:
.azure/deployments/deploy-20260218-143022/drift-analysis/
âââ drift-details.json # Machine-readable drift data
âââ drift-report.md # Human-readable markdown report
âââ current-func-*.json # Current state snapshots
âââ current-storage-*.json
âââ drift-log.jsonl # Audit log (created on accept/revert)
Exit codes:
0- No drift detected1- Warning-level drift found2- Critical drift found
Step 3: Review Drift Reportâ
Markdown Report Example:
# Drift Detection Report
**Deployment ID:** deploy-20260218-143022
**Analyzed:** 2026-02-18 14:30:00 UTC
**Resources Checked:** 3
## Summary
- đ´ Critical Drift: 1
- đĄ Warning Drift: 2
- âšī¸ Info Drift: 0
- **Total Drifts:** 3
## Drift Details
### Resource: func-api-dev-eastus (Microsoft.Web/sites)
đ´ **Critical Drift**
- **Property:** `properties.httpsOnly`
- **Expected:** `false`
- **Current:** `true`
- **Reason:** Azure Policy "Require HTTPS" enforced this change
- **Recommendation:** Accept (security improvement)
đĄ **Warning Drift**
- **Property:** `tags.CostCenter`
- **Expected:** `""`
- **Current:** `"12345"`
- **Reason:** Manually added via Azure Portal
- **Recommendation:** Accept if required for billing
### Resource: stfuncapidev8k3m (Microsoft.Storage/storageAccounts)
đĄ **Warning Drift**
- **Property:** `properties.minimumTlsVersion`
- **Expected:** `TLS1_0`
- **Current:** `TLS1_2`
- **Reason:** Security team policy
- **Recommendation:** Accept (security improvement)
JSON Format:
{
"deploymentId": "deploy-20260218-143022",
"analyzedAt": "2026-02-18T14:30:00Z",
"summary": {
"totalDrifts": 3,
"criticalDrift": 1,
"warningDrift": 2,
"infoDrift": 0
},
"drifts": [
{
"resourceId": "/subscriptions/.../func-api-dev-eastus",
"resourceType": "Microsoft.Web/sites",
"drifts": [
{
"property": "properties.httpsOnly",
"expected": false,
"current": true,
"severity": "Critical"
}
]
}
]
}
Step 4: Choose Reconciliation Optionâ
Option A: Accept Drift (Update IaC)
Update your requirements.json to match current Azure state:
.github/skills/azure-drift-detector/scripts/accept-drift.sh \
--deployment-id deploy-20260218-143022 \
--reason "Azure Policy compliance" \
--auto-commit
What happens:
- Backs up original
requirements.jsonto timestamped directory - Loads current Azure state from drift analysis JSON
- Updates
requirements.jsonwith Azure values:{"resources": [{"httpsOnly": true, // Updated from false"minimumTlsVersion": "1.2", // Updated from 1.0"tags": {"CostCenter": "12345" // Added}}]} - Updates
metadata.jsonwith drift acceptance record - Appends to
drift-log.jsonlfor audit trail - Creates git commit (if
--auto-commit)
Option B: Revert Drift (Redeploy)
Restore original configuration by redeploying:
.github/skills/azure-drift-detector/scripts/revert-drift.sh \
--deployment-id deploy-20260218-143022 \
--confirm
What happens:
- Shows drift summary and asks for confirmation
- Loads original
template.jsonandparameters.json - Creates new deployment with suffix
-revert - Deploys using Azure CLI in Incremental mode:
az deployment group create \--name deploy-20260218-164500-revert \--resource-group rg-api-dev-eastus \--template-file template.json \--mode Incremental
- Logs revert action to
drift-log.jsonl
Option C: Selective Reconciliation
Use the agent for interactive property-by-property reconciliation:
User: @git-ape review drift for deploy-20260218-143022
Agent: Found 3 drifts. Let's review each one:
Drift 1/3: httpsOnly (Critical)
- Current Azure: true
- Your IaC: false
This is a security improvement. Accept this change?
A. Yes (keep Azure value)
B. No (revert to IaC)
User: A
Agent: Accepted. Next...
Drift 2/3: tags.CostCenter (Warning)
- Current Azure: "12345"
- Your IaC: (not set)
Accept this tag?
A. Yes
B. No
User: B
[Continues for all drifts, then applies mixed reconciliation]
Option D: Mark as Known Drift
Flag drift as expected so it won't alert on future scans:
# Not yet implemented - future enhancement
.github/skills/azure-drift-detector/scripts/ignore-drift.sh \
--deployment-id deploy-20260218-143022 \
--property "properties.httpsOnly"
Automated Monitoringâ
GitHub Actions Workflowâ
Create .github/workflows/drift-detection.yml:
name: Azure Drift Detection
on:
schedule:
# Run every 6 hours
- cron: '0 */6 * * *'
# Allow manual trigger
workflow_dispatch:
jobs:
detect-drift:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Install Azure CLI
run: |
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- name: Check All Deployments for Drift
id: drift-check
run: |
.github/skills/azure-drift-detector/scripts/drift-check-all.sh \
--format json > drift-report.json
# Set outputs for subsequent steps
CRITICAL=$(jq -r '.summary.totalCriticalDrift' drift-report.json)
WARNING=$(jq -r '.summary.totalWarningDrift' drift-report.json)
echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
echo "warning=$WARNING" >> $GITHUB_OUTPUT
- name: Upload Drift Report
uses: actions/upload-artifact@v3
with:
name: drift-report-${{ github.run_id }}
path: drift-report.json
- name: Notify on Critical Drift
if: steps.drift-check.outputs.critical > 0
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
payload: |
{
"text": "đ´ Critical Azure Configuration Drift Detected",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Critical Drift Detected*\nâĸ Critical Drifts: ${{ steps.drift-check.outputs.critical }}\nâĸ Warning Drifts: ${{ steps.drift-check.outputs.warning }}\n\n<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
}
}
]
}
- name: Create Issue on Critical Drift
if: steps.drift-check.outputs.critical > 0
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('drift-report.json'));
const criticalDeployments = report.deployments
.filter(d => d.criticalDrift > 0)
.map(d => `- **${d.deploymentId}**: ${d.criticalDrift} critical drifts`)
.join('\n');
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'đ´ Critical Azure Configuration Drift Detected',
body: `## Critical Drift Alert\n\n${criticalDeployments}\n\n**Action Required:**\n1. Review drift report in GitHub Actions artifacts\n2. Investigate changes in Azure Portal activity logs\n3. Revert unauthorized changes or accept if intentional\n\n**Auto-generated by drift detection workflow**`,
labels: ['azure', 'drift', 'security']
});
Scheduled Cron Jobâ
For local or self-hosted runners:
# Add to crontab
0 */6 * * * cd /path/to/git-ape && .github/skills/azure-drift-detector/scripts/drift-check-all.sh --format json > /var/log/drift-$(date +\%Y\%m\%d).json
Azure Monitor Alert Ruleâ
Create alert rule for resource modifications:
az monitor activity-log alert create \
--name "Resource Configuration Changes" \
--resource-group rg-monitoring \
--condition category=Administrative and operationName=Microsoft.Resources/deployments/write \
--action-group drift-notification-group \
--description "Alert when Azure resources are modified"
Common Scenariosâ
Scenario 1: Azure Policy Complianceâ
Situation: Azure Policy enforces HTTPS-only on all web apps
Drift Detected:
đ´ Critical Drift
- httpsOnly: false â true (enforced by policy)
Resolution:
# Accept drift - policy is a good thing
.github/skills/azure-drift-detector/scripts/accept-drift.sh \
--deployment-id deploy-20260218-143022 \
--reason "Azure Policy: Require HTTPS on web apps" \
--auto-commit
Outcome: Your IaC is updated to require HTTPS. Future deployments will align with policy.
Scenario 2: Emergency Production Hotfixâ
Situation: Production outage requires immediate config change via portal
Drift Detected:
đĄ Warning Drift
- FUNCTIONS_WORKER_RUNTIME: python â dotnet
- appSettings.FeatureFlag: "false" â "true"
Resolution:
User: @git-ape what changed in production?
Agent: Found drift in deploy-20260215-093022 (production):
- Runtime changed from Python to .NET
- Feature flag enabled
- Modified by: john.doe@company.com at 03:45 AM
These look like emergency changes. Would you like to:
A. Accept temporarily (update IaC later with proper review)
B. Revert immediately (restore Python runtime)
C. Review incident logs first
User: A for now, we'll fix properly tomorrow
Agent: Accepted drift with note: "Emergency hotfix - will review in morning"
Created issue #123 to track proper fix
Scenario 3: Security Vulnerabilityâ
Situation: Storage account TLS version was downgraded
Drift Detected:
đ´ Critical Drift
- minimumTlsVersion: "TLS1_2" â "TLS1_0" (security degradation)
Resolution:
# REVERT IMMEDIATELY
.github/skills/azure-drift-detector/scripts/revert-drift.sh \
--deployment-id deploy-20260218-143022 \
--confirm
# Follow up:
# 1. Check Azure activity logs for who made change
# 2. Review RBAC permissions
# 3. Enable resource locks on production resources
Scenario 4: Tag Governanceâ
Situation: Finance team adds cost center tags via portal
Drift Detected:
đĄ Warning Drift
- tags.CostCenter: "" â "IT-12345"
- tags.Department: "" â "Engineering"
- tags.Owner: "" â "Platform Team"
Resolution:
# Accept tags - they're required for billing
.github/skills/azure-drift-detector/scripts/accept-drift.sh \
--deployment-id deploy-20260218-143022 \
--reason "Finance team added required billing tags" \
--auto-commit
# Update deployment process to include these tags by default
# Edit .github/copilot-instructions.md environment tags
Best Practicesâ
1. Run Drift Detection Regularlyâ
Frequency:
- Production: Every 6 hours (or after each deployment)
- Staging: Daily
- Development: Weekly or on-demand
2. Classify and Prioritizeâ
Critical Drift â Immediate Action
- Security settings weakened
- Public access enabled
- Encryption downgraded
Warning Drift â Review Within 24 Hours
- Configuration changes
- Tag modifications
- Non-security settings
Info Drift â Accept or Ignore
- Azure-managed properties
- Cosmetic changes
3. Maintain Audit Trailâ
All drift actions are logged to drift-log.jsonl:
{"timestamp":"2026-02-18T14:30:00Z","action":"accept","user":"john.doe","driftsAccepted":2,"reason":"Azure Policy compliance"}
{"timestamp":"2026-02-18T16:45:00Z","action":"revert","user":"jane.smith","revertDeploymentId":"deploy-20260218-164500-revert","driftsReverted":3}
Query logs:
# Show all drift acceptances
jq 'select(.action == "accept")' .azure/deployments/*/drift-analysis/drift-log.jsonl
# Count reverts by user
jq -r 'select(.action == "revert") | .user' .azure/deployments/*/drift-analysis/drift-log.jsonl | sort | uniq -c
4. Prevent Drift with Azure Locksâ
For critical production resources, enable resource locks:
az lock create \
--name "Prevent Deletion" \
--resource-group rg-webapp-prod-eastus \
--lock-type CanNotDelete \
--notes "Production resource - use IaC for changes"
Lock levels:
- CanNotDelete - Can modify but not delete
- ReadOnly - Cannot modify or delete (prevents all drift)
5. Use Azure Policy for Complianceâ
Define organizational standards with Azure Policy:
# Assign built-in policy: Require HTTPS
az policy assignment create \
--name "require-https" \
--policy "$(az policy definition list --query "[?displayName=='App Service apps should only be accessible over HTTPS'].id" -o tsv)" \
--scope "/subscriptions/{subscription-id}"
When policy remediates resources, drift detection will show:
Reason: Azure Policy "Require HTTPS" enforced this change
Recommendation: Accept (compliance requirement)
6. Document Known Driftâ
For recurring acceptable drift (e.g., auto-scaling metrics), document in deployment metadata:
{
"knownDrift": [
{
"property": "properties.instanceCount",
"reason": "Auto-scaling adjusts this value",
"acceptedBy": "platform-team",
"acceptedAt": "2026-02-15"
}
]
}
Troubleshootingâ
Drift Detection Failsâ
Error: "Could not query Azure resource"
Solutions:
# Verify Azure CLI authentication
az account show
# Check resource still exists
az resource show --ids {resource-id}
# Verify permissions (Reader role required)
az role assignment list --scope {resource-id}
False Positivesâ
Issue: Azure-managed properties show as drift
Solution: Update detect-drift.sh to exclude these properties:
# In detect-drift.sh, add to IGNORED_PROPERTIES
IGNORED_PROPERTIES=(
"properties.createdTime"
"properties.lastModifiedTime"
"systemData"
)
Accept Drift Failsâ
Error: "Could not update requirements.json"
Solutions:
# Check file permissions
chmod +w .azure/deployments/{id}/requirements.json
# Check JSON syntax
jq . .azure/deployments/{id}/requirements.json
# Restore from backup
cp .azure/deployments/{id}/backups/{timestamp}/requirements.json \
.azure/deployments/{id}/requirements.json
Revert Deployment Failsâ
Error: "Deployment failed with InvalidTemplate"
Solutions:
# Validate template
az deployment group validate \
--resource-group {rg} \
--template-file .azure/deployments/{id}/template.json
# Check error log
cat .azure/deployments/{id}-revert/error.log
# Try manual deployment
az deployment group create \
--resource-group {rg} \
--template-file .azure/deployments/{id}/template.json \
--mode Incremental \
--debug
Advanced Usageâ
Custom Drift Checkersâ
Add custom property comparisons for specific resource types:
# In detect-drift.sh, add custom checker
check_custom_properties() {
local RESOURCE_TYPE="$1"
case "$RESOURCE_TYPE" in
"Microsoft.Web/sites")
# Your custom logic
check_app_settings
check_connection_strings
;;
esac
}
Integration with External Toolsâ
Terraform:
# Export drift as Terraform-compatible format
jq -r '.drifts[] | .drifts[] |
"resource \"" + .resourceType + "\" \"" + .resourceName + "\" {\n " +
.property + " = " + .current + "\n}"' \
drift-details.json > drift.tf
Ansible:
# Generate Ansible playbook to accept drift
jq -r '.drifts[] |
"- name: Update " + .resourceName + "\n azure_rm_webapp:\n " +
.property + ": " + .current' \
drift-details.json > accept-drift.yml
Referenceâ
Script Optionsâ
detect-drift.sh
--deployment-id <id> Required: Deployment to check
--output-format <fmt> json | markdown | github (default: markdown)
--include-known-drift Include previously accepted drift
--verbose Show detailed progress
accept-drift.sh
--deployment-id <id> Required: Deployment to accept drift for
--reason <text> Reason for acceptance (audit requirement)
--auto-commit Create git commit automatically
revert-drift.sh
--deployment-id <id> Required: Deployment to revert
--confirm Skip confirmation prompt
--dry-run Show what would be reverted
drift-check-all.sh
--format <fmt> summary | detailed | json (default: summary)
--only-critical Only report critical drift
--include-known-drift Include accepted drift
--verbose Show detailed progress
Exit Codesâ
| Code | Meaning |
|---|---|
| 0 | No drift detected / successful operation |
| 1 | Warning-level drift found |
| 2 | Critical drift found |
| 3+ | Error during execution |