Prevent Azure Policy compliance failures from blocking deployments by tracking expiring exemptions, remediating non-compliant resources, and automating compliance monitoring.

Azure Policy compliance failures are one of the most common deployment blockers in governed Azure environments. A deployment that worked last week suddenly fails because a policy exemption expired overnight, or a resource that passed validation at creation time is now flagged as non-compliant after a policy assignment update. These failures are particularly disruptive because they surface at deployment time rather than during planning, forcing engineering teams to scramble for exemptions or remediation while release timelines slip.
The root cause is almost always the same: no one is actively tracking exemption expiry dates, no one is monitoring the compliance state of existing resources against evolving policy assignments, and no one has automated the remediation workflow. Policy compliance is treated as a gate rather than a continuous process, which means problems accumulate silently until they block something visible.
Based on what we typically see, the most effective approach combines three practices: proactive exemption lifecycle tracking so you know what is expiring before it blocks a deployment, systematic remediation of non-compliant resources so your compliance posture improves over time rather than degrading, and automated monitoring that scales across subscriptions and tenants. Spotto's governance platform continuously identifies expiring exemptions and non-compliant resources, surfacing targeted remediation steps before they become deployment blockers.
Key Takeaways at a Glance:
Azure Policy exemptions have an optional expiry date. When that date passes, the exemption is automatically removed and the underlying policy effect re-activates. If the effect is Deny, any new deployment that violates the policy will fail immediately. If the effect is Modify or DeployIfNotExists, the resource will be flagged as non-compliant and may be auto-remediated in ways the team did not anticipate.
Operational Pain:
Business Impact:
Financial Consequence:
Engineering Trade-off:
Non-compliant resources are those that exist in your environment but do not meet the conditions specified by an active policy assignment. These resources were either created before the policy was assigned, created with an exemption that has since expired, or drifted out of compliance due to configuration changes.
Operational Pain:
Business Impact:
Financial Consequence:
Engineering Trade-off:
Real-world example: An MSP managing 30+ tenants discovered that 47 policy exemptions across 12 subscriptions were set to expire within the next 30 days. Without proactive monitoring, each expiry would have triggered a deployment failure, an emergency triage call, and a rushed exemption renewal -- multiplied across tenants.
Azure Policy exemptions are scoped to a specific policy assignment and a specific resource or scope (management group, subscription, or resource group). Each exemption has a category (Waiver or Mitigated), an optional expiry date, and a description. When an exemption expires, it is not deleted -- it remains as an expired object, but the policy effect re-activates immediately for the exempted scope.
The most effective way to track exemptions across subscriptions is Azure Resource Graph. The following KQL query returns all exemptions with their expiry status, allowing you to identify which ones are approaching expiry and which have already expired.
Resource Graph KQL -- Exemption Expiry Query:
// Query all policy exemptions with expiry status
policyresources
| where type == "microsoft.authorization/policyexemptions"
| extend expiresOn = todatetime(properties.expiresOn)
| extend isExpired = iif(isnotempty(expiresOn) and expiresOn < now(), true, false)
| extend daysUntilExpiry = iif(isnotempty(expiresOn), datetime_diff('day', expiresOn, now()), int(null))
| extend category = tostring(properties.exemptionCategory)
| extend policyAssignmentId = tostring(properties.policyAssignmentId)
| extend displayName = tostring(properties.displayName)
| extend description = tostring(properties.description)
| project
name,
displayName,
subscriptionId,
resourceGroup,
category,
expiresOn,
isExpired,
daysUntilExpiry,
policyAssignmentId,
description
| order by daysUntilExpiry ascNote: This query runs across all subscriptions visible to the authenticated identity. For MSPs using Azure Lighthouse, it will include delegated subscriptions. Filter by subscriptionId to scope results to specific customers.
Once you have identified non-compliant resources, the remediation approach depends on the policy effect. Resources violating Deny policies cannot be created in the first place (the compliance issue is that existing resources pre-date the policy). Resources violating Modify or DeployIfNotExists policies can be remediated through policy remediation tasks. Resources violating Audit policies require manual remediation.
Review non-compliant resources for a policy assignment:
# List non-compliant resources for a specific policy assignment
az policy state list \
--filter "complianceState eq 'NonCompliant' and policyAssignmentName eq '<assignment-name>'" \
--query "[].{Resource:resourceId, Policy:policyDefinitionName, State:complianceState}" \
-o table
# Summarize non-compliance by policy definition
az policy state summarize \
--filter "complianceState eq 'NonCompliant'" \
--query "value[].{Policy:policyAssignments[0].policyDefinitions[0].policyDefinitionId, \
NonCompliant:policyAssignments[0].policyDefinitions[0].results.nonCompliantResources}" \
-o tableTrigger a remediation task for Modify or DeployIfNotExists policies:
# Create a remediation task for a specific policy assignment
az policy remediation create \
--name "remediate-<assignment-name>-$(date +%Y%m%d)" \
--policy-assignment "<assignment-id>" \
--resource-group "<rg-name>"
# Check remediation task status
az policy remediation show \
--name "remediate-<assignment-name>-$(date +%Y%m%d)" \
--resource-group "<rg-name>" \
--query "{Status:provisioningState, Deployed:deploymentStatus.totalDeployments, \
Succeeded:deploymentStatus.successfulDeployments, \
Failed:deploymentStatus.failedDeployments}" \
-o tableCreate an exemption for a resource that cannot be remediated immediately:
# Create a time-bounded exemption (Waiver category)
az policy exemption create \
--name "exempt-<resource-name>-<date>" \
--policy-assignment "<assignment-id>" \
--exemption-category "Waiver" \
--scope "<resource-id>" \
--expires-on "2026-05-01T00:00:00Z" \
--description "Temporary exemption pending remediation. Ticket: INC-12345"
# Create a permanent exemption (Mitigated category)
az policy exemption create \
--name "exempt-<resource-name>-mitigated" \
--policy-assignment "<assignment-id>" \
--exemption-category "Mitigated" \
--scope "<resource-id>" \
--description "Compensating control in place: NSG restricts access. Approved by: Security Team"
Trigger an on-demand compliance scan:
# Trigger a compliance evaluation scan at subscription scope
az policy state trigger-scan
# Trigger a compliance scan at resource group scope
az policy state trigger-scan --resource-group "<rg-name>"
# Note: Scans are asynchronous. Full subscription scans can take
# 30-60 minutes depending on resource count. Resource group scans
# are faster but still asynchronous.Failure mode warning: Remediation tasks for Modify and DeployIfNotExists policies will change resource configurations. Always review the policy definition's then block to understand exactly what will be modified before triggering remediation. Test on non-production resources first. A Modify policy that adds a tag is low risk; a DeployIfNotExists policy that deploys a diagnostic settings resource may have cost implications.
When rolling out new policy assignments or changing existing ones, the DoNotEnforce enforcement mode allows you to evaluate the compliance impact without actually blocking deployments. This is the safest way to stage policy changes in production environments.
In DoNotEnforce mode, the policy evaluates resources and reports compliance state, but the effect (Deny, Modify, etc.) is not applied. This gives you a compliance report showing which resources would be affected, without any operational impact.
Create a policy assignment in DoNotEnforce mode:
# Create a policy assignment in DoNotEnforce mode
az policy assignment create \
--name "audit-storage-encryption" \
--display-name "Audit: Storage accounts should use encryption" \
--policy "<policy-definition-id>" \
--scope "/subscriptions/<subscription-id>" \
--enforcement-mode "DoNotEnforce"
# After reviewing compliance results, switch to Default (enforce) mode
az policy assignment update \
--name "audit-storage-encryption" \
--enforcement-mode "Default"
# To revert back to DoNotEnforce if issues are discovered
az policy assignment update \
--name "audit-storage-encryption" \
--enforcement-mode "DoNotEnforce"Best practice: Use DoNotEnforce for at least 7 days before switching to Default enforcement. This gives you one full business cycle to observe which resources would be affected and to pre-create exemptions or remediate non-compliant resources before enforcement begins.
| Category | Intent | Typical Use | Expiry Recommendation |
|---|---|---|---|
| Waiver | Temporary exception -- the resource does not meet the policy, and remediation is planned | Migration in progress, pending remediation, temporary architecture | Always set an expiry date (30-90 days) |
| Mitigated | Permanent exception -- the resource does not meet the policy, but a compensating control exists | Alternative security control, architectural constraint, vendor limitation | No expiry, but review annually |
| Effect | Blocks Deployment | Auto-Remediation | Remediation Approach |
|---|---|---|---|
| Deny | Yes -- prevents resource creation or modification | No | Modify the resource to comply, or create an exemption |
| Modify | No -- allows creation, then modifies | Yes -- via remediation task | Trigger a remediation task to apply the modification to existing resources |
| DeployIfNotExists | No -- allows creation, then deploys companion resource | Yes -- via remediation task | Trigger a remediation task to deploy the missing companion resource |
| Audit | No -- only reports compliance state | No | Manual remediation or exemption; no auto-remediation available |
| Disabled | No -- policy is inactive | No | No action needed; used to temporarily deactivate a policy |
| Recommendation | Cost Avoidance | Effort | Risk Reduction |
|---|---|---|---|
| Track and renew expiring exemptions proactively | Avoids emergency triage costs (2-4 hours per incident) | Low | High -- prevents deployment failures |
| Remediate non-compliant resources systematically | Avoids audit remediation costs and potential penalties | Medium | High -- closes security gaps |
| Use DoNotEnforce staging for policy rollouts | Avoids production disruption from untested policy changes | Low | High -- safe change management |
| Automate compliance monitoring across tenants | Replaces manual audit cycles (days per quarter per tenant) | Medium | High -- continuous visibility |
When an exemption expires, the underlying policy effect re-activates immediately for the exempted scope. If the policy effect is Deny, any new deployment that violates the policy condition will be blocked. If the effect is Modify or DeployIfNotExists, the resources will be flagged as non-compliant and will be remediated on the next evaluation cycle if a remediation task is active. The expired exemption object remains in Azure but has no effect. This is why proactive tracking is critical -- the re-activation happens silently.
Use Azure Resource Graph with the KQL query provided in the Exemption Lifecycle section above. Resource Graph queries span all subscriptions visible to the authenticated identity, including delegated subscriptions for MSPs using Azure Lighthouse. Filter by daysUntilExpiry to focus on exemptions expiring within 30, 14, or 7 days.
A Waiver exemption indicates a temporary exception where the resource does not meet the policy and remediation is planned. It should always have an expiry date. A Mitigated exemption indicates that the resource does not meet the policy, but a compensating control is in place (for example, network-level restrictions instead of resource-level encryption). Mitigated exemptions typically do not have an expiry date but should be reviewed annually to confirm the compensating control is still effective.
DoNotEnforce mode keeps the policy assignment active and evaluates compliance, but does not apply the effect. Resources are flagged as non-compliant in the compliance view, giving you visibility into what would be affected. Disabled mode completely deactivates the policy -- no evaluation occurs and no compliance data is generated. Use DoNotEnforce when you want to observe impact before enforcing; use Disabled when you want to completely turn off a policy.
Yes, but only for policies with Modify or DeployIfNotExists effects. These policies support remediation tasks that apply the policy's intended configuration to existing resources. Policies with Audit or Deny effects do not support auto-remediation -- Audit only reports non-compliance, and Deny only blocks future deployments. For Audit-only policies, remediation must be performed manually or through separate automation.
Azure evaluates policy compliance in three scenarios: on resource creation or update (real-time for Deny effects), on a standard compliance evaluation cycle (approximately every 24 hours for all resources), and on-demand when you trigger a scan using az policy state trigger-scan. The 24-hour cycle means the compliance view in the portal can be stale. For accurate, up-to-date compliance data, trigger an on-demand scan before making decisions.
To create, update, or delete policy exemptions, you need the Microsoft.Authorization/policyExemptions/write permission. This is included in the Resource Policy Contributor built-in role. To read exemptions and compliance data, you need Microsoft.Authorization/policyExemptions/read and Microsoft.PolicyInsights/policyStates/read. For cross-subscription queries via Resource Graph, you need Reader access on each subscription.
Use the Mitigated exemption category. Document the compensating control in the exemption description, reference the approval ticket or decision record, and set no expiry date. Include the exemption in your annual governance review to confirm the compensating control remains effective and the resource still requires the exception. This is preferable to removing the policy assignment entirely, as it preserves the policy for all other resources.
Azure does not provide built-in notifications for exemption expiry. You need to implement this through automation -- either a scheduled Azure Automation runbook, a Logic App, or a monitoring platform like Spotto. The PowerShell runbook in the Automation section of this article queries Resource Graph for exemptions expiring within a configurable window and sends alerts. Spotto provides this monitoring continuously without requiring custom automation.
During migrations, use a combination of DoNotEnforce mode and Waiver exemptions. Assign new policies in DoNotEnforce mode to understand the compliance impact without blocking the migration. For specific resources that you know will be non-compliant temporarily, create Waiver exemptions with expiry dates aligned to the migration completion timeline. After migration, switch policies to Default enforcement and let the Waiver exemptions expire naturally as resources are remediated.
Azure imposes a limit of 5,000 policy exemptions per subscription. While this is a generous limit, organizations that rely heavily on exemptions rather than remediation can approach it over time. If you find yourself creating large numbers of exemptions, it often indicates that the policy definition needs to be refined to better match your environment, or that a systematic remediation effort is needed.
Spotto continuously monitors your Azure Policy compliance posture across all subscriptions and tenants. It tracks exemption expiry dates and alerts you before they lapse, identifies non-compliant resources and prioritizes them by impact, and provides remediation guidance specific to each policy violation. Rather than running quarterly compliance audits or maintaining custom automation, Spotto gives you a real-time compliance dashboard with actionable insights -- especially valuable for MSPs managing multiple customer environments.
Before
47 expiring exemptions
Across 12 subscriptions, expiring within 30 days, no tracking
After
Zero surprise failures
Automated alerts at 30, 14, and 7 days before expiry
Actions taken: Deployed Resource Graph query as a scheduled runbook across all tenants via Azure Lighthouse. Created a centralized dashboard showing all exemptions by tenant, expiry date, and category. Established a weekly review cadence for exemptions expiring within 30 days. Renewed valid exemptions proactively and remediated resources where exemptions were no longer needed. Eliminated emergency exemption requests entirely.
Before
82% compliance
340 non-compliant resources across 8 subscriptions
After
96% compliance
48 remaining exemptions, all documented with compensating controls
Actions taken: Ran compliance summary across all subscriptions to identify the scope of non-compliance. Categorized non-compliant resources by policy effect: 180 were Modify-remediable, 95 were DeployIfNotExists-remediable, and 65 required manual action. Triggered remediation tasks for the 275 auto-remediable resources over two weeks. Created Mitigated exemptions for 35 resources with compensating controls and Waiver exemptions for 13 resources pending architectural changes. Manual remediation completed for the remaining 52 resources.
Before
12 audit findings
Previous quarter: 12 findings related to policy non-compliance
After
Zero findings
Next quarter: zero policy-related audit findings
Actions taken: Implemented continuous compliance monitoring with weekly automated scans. Established a remediation SLA: Deny-effect violations within 48 hours, Modify/DINE within 7 days, Audit-only within 30 days. Created a compliance report that maps each policy assignment to the relevant audit control (SOC 2 CC6.1, CIS 4.3, etc.). Pre-audit compliance review reduced from 3 days of manual work to a 30-minute dashboard review. All exemptions documented with justification and compensating control evidence.
Before
Same policies as production
Dev teams constantly blocked by Deny policies, filing exemptions daily
After
Tiered governance
DoNotEnforce for dev, Default for staging and production
Actions taken: Duplicated production policy assignments for dev/test subscriptions with DoNotEnforce mode. Developers can deploy freely while compliance data is still collected. Non-compliance in dev/test is reported but not blocked. Staging subscriptions use Default enforcement to catch issues before production. Eliminated 90% of exemption requests from dev teams while maintaining full compliance visibility. Weekly compliance report highlights dev resources that would fail in production.
Before
Region-specific policy conflicts
Data residency policies blocking deployments in new regions
After
Mitigated exemptions with controls
Documented compensating controls for each region-specific exception
Actions taken: Identified 8 policy assignments with region-restriction conditions that conflicted with multi-region expansion plans. Created Mitigated exemptions for specific resource types in new regions, documented with compensating controls (encryption at rest, customer-managed keys, network isolation). Established a policy review process for each new region onboarding. Used DoNotEnforce mode to evaluate compliance impact in new regions before creating exemptions. All exemptions tied to architectural decision records and reviewed quarterly.
Microsoft.Authorization/policyExemptions/* permissions)then block and test remediation on non-production resources first.Every compliance change should have a clear rollback path:
az policy assignment update --enforcement-mode DoNotEnforce. This immediately stops the policy from blocking deployments while preserving compliance reporting.az policy exemption delete. Exemption changes take effect immediately.The following PowerShell runbook can be deployed as an Azure Automation scheduled task to continuously monitor policy compliance across your subscriptions. It queries for expiring exemptions and non-compliant resources, then sends alerts to your configured notification channel.
# Azure Automation Runbook: Policy Compliance Monitoring
# Schedule: Daily (recommended) or Weekly
# Prerequisites: Az.ResourceGraph, Az.Accounts, Az.PolicyInsights modules
# Managed Identity with Reader access across target subscriptions
param(
[int]$ExpiryWarningDays = 30,
[string]$NotificationWebhookUrl = "",
[string]$NotificationEmail = "",
[string[]]$SubscriptionIds = @()
)
# Authenticate using managed identity
Connect-AzAccount -Identity
# If no subscriptions specified, get all accessible subscriptions
if ($SubscriptionIds.Count -eq 0) {
$SubscriptionIds = (Get-AzSubscription | Where-Object { $_.State -eq 'Enabled' }).Id
}
Write-Output "=== Policy Compliance Monitoring Report ==="
Write-Output "Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC')"
Write-Output "Subscriptions monitored: $($SubscriptionIds.Count)"
Write-Output ""
# -------------------------------------------------------
# Section 1: Expiring Exemptions
# -------------------------------------------------------
Write-Output "--- Section 1: Expiring Policy Exemptions ---"
$exemptionQuery = @"
policyresources
| where type == "microsoft.authorization/policyexemptions"
| extend expiresOn = todatetime(properties.expiresOn)
| extend isExpired = iif(isnotempty(expiresOn) and expiresOn < now(), true, false)
| extend daysUntilExpiry = iif(isnotempty(expiresOn),
datetime_diff('day', expiresOn, now()), int(null))
| extend category = tostring(properties.exemptionCategory)
| extend displayName = tostring(properties.displayName)
| extend description = tostring(properties.description)
| extend policyAssignmentId = tostring(properties.policyAssignmentId)
| where isnotempty(expiresOn)
| where daysUntilExpiry <= $ExpiryWarningDays
| project
name,
displayName,
subscriptionId,
resourceGroup,
category,
expiresOn,
isExpired,
daysUntilExpiry,
policyAssignmentId,
description
| order by daysUntilExpiry asc
"@
$expiringExemptions = Search-AzGraph -Query $exemptionQuery \
-Subscription $SubscriptionIds
$expiredCount = ($expiringExemptions | Where-Object { $_.isExpired -eq $true }).Count
$warningCount = ($expiringExemptions | Where-Object { $_.isExpired -eq $false }).Count
Write-Output "Expired exemptions: $expiredCount"
Write-Output "Expiring within $ExpiryWarningDays days: $warningCount"
Write-Output ""
foreach ($exemption in $expiringExemptions) {
$status = if ($exemption.isExpired) { "[EXPIRED]" } else { "[WARNING]" }
Write-Output "$status $($exemption.displayName)"
Write-Output " Subscription: $($exemption.subscriptionId)"
Write-Output " Category: $($exemption.category)"
Write-Output " Expires: $($exemption.expiresOn) (Days: $($exemption.daysUntilExpiry))"
Write-Output " Policy: $($exemption.policyAssignmentId)"
Write-Output " Description: $($exemption.description)"
Write-Output ""
}
# -------------------------------------------------------
# Section 2: Non-Compliant Resource Summary
# -------------------------------------------------------
Write-Output "--- Section 2: Non-Compliant Resources Summary ---"
$complianceSummary = @()
foreach ($subId in $SubscriptionIds) {
Set-AzContext -SubscriptionId $subId -ErrorAction SilentlyContinue | Out-Null
$summary = Get-AzPolicyStateSummary -ErrorAction SilentlyContinue
if ($summary) {
$nonCompliantResources = $summary.Results.NonCompliantResources
$nonCompliantPolicies = $summary.Results.NonCompliantPolicies
if ($nonCompliantResources -gt 0) {
$complianceSummary += [PSCustomObject]@{
SubscriptionId = $subId
NonCompliantResources = $nonCompliantResources
NonCompliantPolicies = $nonCompliantPolicies
}
Write-Output "Subscription: $subId"
Write-Output " Non-compliant resources: $nonCompliantResources"
Write-Output " Non-compliant policies: $nonCompliantPolicies"
Write-Output ""
}
}
}
$totalNonCompliant = ($complianceSummary |
Measure-Object -Property NonCompliantResources -Sum).Sum
Write-Output "Total non-compliant resources across all subscriptions: $totalNonCompliant"
Write-Output ""
# -------------------------------------------------------
# Section 3: Send Notifications
# -------------------------------------------------------
if (($expiredCount -gt 0 -or $warningCount -gt 0 -or $totalNonCompliant -gt 0) \
-and $NotificationWebhookUrl -ne "") {
$payload = @{
title = "Policy Compliance Alert"
text = @"
**Policy Compliance Monitoring Report**
- Expired exemptions: $expiredCount
- Exemptions expiring within $($ExpiryWarningDays) days: $warningCount
- Total non-compliant resources: $totalNonCompliant
Review the full report in the Azure Automation job output.
"@
} | ConvertTo-Json -Depth 3
try {
Invoke-RestMethod -Uri $NotificationWebhookUrl \
-Method Post \
-ContentType "application/json" \
-Body $payload
Write-Output "Notification sent to webhook successfully."
} catch {
Write-Warning "Failed to send webhook notification: $($_.Exception.Message)"
}
}
# -------------------------------------------------------
# Section 4: Report Summary
# -------------------------------------------------------
Write-Output "=== Report Complete ==="
Write-Output "Subscriptions scanned: $($SubscriptionIds.Count)"
Write-Output "Expired exemptions: $expiredCount"
Write-Output "Expiring exemptions (within $ExpiryWarningDays days): $warningCount"
Write-Output "Total non-compliant resources: $totalNonCompliant"
Write-Output ""
Write-Output "Next steps:"
if ($expiredCount -gt 0) {
Write-Output " - URGENT: Review and renew or remove $expiredCount expired exemptions"
}
if ($warningCount -gt 0) {
Write-Output " - Review $warningCount exemptions expiring soon and plan remediation"
}
if ($totalNonCompliant -gt 0) {
Write-Output " - Remediate $totalNonCompliant non-compliant resources"
}Failure mode warning: The runbook requires the managed identity to have Reader access on all target subscriptions and Microsoft.PolicyInsights/policyStates/read permission. If the managed identity lacks permissions on a subscription, that subscription will be silently skipped. Verify access across all subscriptions before relying on the report for audit compliance.
The challenges outlined in this article — expiring policy exemptions, non-compliant resources blocking deployments, missing compliance automation — are the same ones our customers work through across their Azure environments. The difference is they use Spotto to surface these opportunities automatically, rather than running the analysis by hand.
Spotto continuously monitors your Azure Policy compliance posture, flagging expiring exemptions and non-compliant resources before they block deployments or trigger audit findings — giving you advance warning and clear remediation steps across every subscription.
Free TrialDisclaimer: This article is provided for informational purposes only and does not constitute professional advice. Azure pricing, features, and service behaviour may have changed since publication. Always validate recommendations against your specific environment and consult Microsoft's official documentation before making changes to production infrastructure. Spotto is not liable for any actions taken based on the content of this article.