Compliance Automation with Azure Policy
Automate compliance enforcement and governance with Azure Policy integrated into Azure DevOps CI/CD pipelines
Compliance Automation with Azure Policy
Azure Policy is the enforcement layer that turns governance from a suggestion into a guarantee. If you are deploying infrastructure to Azure through CI/CD pipelines without policy guardrails, you are one misconfigured resource away from a compliance violation, a security breach, or a very expensive conversation with your auditor. This article covers how to define, assign, evaluate, and remediate Azure Policy at scale, with a heavy emphasis on integrating it into Azure DevOps pipelines using Node.js.
Prerequisites
- An Azure subscription with Owner or Resource Policy Contributor role
- Azure DevOps organization with a project and service connection to Azure
- Node.js 16+ installed locally
- Azure CLI installed and authenticated (
az login) - Basic familiarity with ARM templates or Bicep
@azure/arm-policy,@azure/arm-policyinsights, and@azure/identitynpm packages
Install the required packages:
npm install @azure/arm-policy @azure/arm-policyinsights @azure/identity
Azure Policy Fundamentals for DevOps Teams
Azure Policy evaluates resources against rules you define. Every rule has a condition and an effect. The condition describes what to look for. The effect describes what happens when the condition matches. The effects you will use most often are:
- Deny — blocks resource creation or modification that violates the rule
- Audit — allows the resource but flags it as non-compliant
- DeployIfNotExists — deploys a remediation resource if the target is missing a required configuration
- Modify — adds, updates, or removes tags or properties during creation or update
- Disabled — turns the policy off without deleting the assignment
Policy evaluation happens at several points: during resource creation via ARM, during periodic compliance scans (roughly every 24 hours), and on-demand when you trigger an evaluation. For CI/CD pipelines, the critical one is the deny effect during resource creation. If a pipeline tries to deploy a storage account without encryption enabled and a deny policy is in place, the deployment fails immediately.
The evaluation engine runs in Azure's control plane. It does not inspect data plane operations. If you need to enforce rules about what happens inside a VM or container, you need Guest Configuration policies, which we will cover later.
Built-In Policies for Azure DevOps Resources
Azure ships with over 4,000 built-in policy definitions. Before writing custom policies, check if a built-in one already covers your requirement. Here are the ones I use most frequently for DevOps-related governance:
| Policy Name | Effect | Purpose |
|---|---|---|
| Allowed locations | Deny | Restrict resource deployment to approved regions |
| Require a tag and its value on resources | Deny | Enforce tagging standards (environment, cost-center, team) |
| Storage accounts should use private link | Audit | Flag storage without private endpoints |
| Kubernetes cluster should not allow privileged containers | Deny | Block privileged pods in AKS |
| SQL servers should have auditing enabled | AuditIfNotExists | Flag SQL servers without audit logs |
| Key vaults should have soft delete enabled | Deny | Prevent permanent key loss |
List built-in policies with the Azure CLI:
az policy definition list --query "[?policyType=='BuiltIn'] | [0:5].{name:name, displayName:displayName}" -o table
Output:
Name DisplayName
------------------------------------ ----------------------------------------
06a78e20-9358-41c9-923c-fb736d382a4d Allowed locations
e765b5de-1225-4ba3-bd56-1ac6695af988 Allowed locations for resource groups
2a0e14a6-b0a6-4fab-991a-187a4f81c498 Audit resource location matches resource group
Custom Policy Definitions
When built-in policies do not cover your requirement, you write a custom definition. A policy definition is a JSON document with a policy rule, parameters, and metadata. Here is a custom policy that denies the creation of public IP addresses:
{
"mode": "All",
"policyRule": {
"if": {
"field": "type",
"equals": "Microsoft.Network/publicIPAddresses"
},
"then": {
"effect": "[parameters('effect')]"
}
},
"parameters": {
"effect": {
"type": "String",
"metadata": {
"displayName": "Effect",
"description": "Deny or Audit public IP creation"
},
"allowedValues": [
"Deny",
"Audit",
"Disabled"
],
"defaultValue": "Deny"
}
}
}
A more realistic custom policy checks that all storage accounts enforce HTTPS-only traffic and use a minimum TLS version:
{
"mode": "All",
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Storage/storageAccounts"
},
{
"anyOf": [
{
"field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"notEquals": true
},
{
"field": "Microsoft.Storage/storageAccounts/minimumTlsVersion",
"notEquals": "TLS1_2"
}
]
}
]
},
"then": {
"effect": "Deny"
}
}
}
Create the custom definition with Azure CLI:
az policy definition create \
--name "deny-storage-insecure-tls" \
--display-name "Deny storage accounts without TLS 1.2" \
--description "Ensures all storage accounts require HTTPS and TLS 1.2 minimum" \
--rules ./policy-rules/storage-tls.json \
--mode All
Policy Assignments and Scopes
A policy definition does nothing until you assign it to a scope. Scopes follow the Azure hierarchy: management group, subscription, resource group. Assignments inherit downward. A policy assigned at the subscription level applies to every resource group in that subscription.
az policy assignment create \
--name "enforce-storage-tls" \
--display-name "Enforce TLS 1.2 on Storage Accounts" \
--policy "deny-storage-insecure-tls" \
--scope "/subscriptions/YOUR_SUBSCRIPTION_ID" \
--params '{"effect": {"value": "Deny"}}'
For DevOps teams managing multiple environments, I recommend assigning policies at the management group level with different parameters per environment. Production gets deny effects. Development and staging get audit effects. This gives developers visibility into violations without blocking their work.
Initiative Definitions (Policy Sets)
An initiative groups multiple policy definitions into a single assignable unit. This is how you manage compliance at scale. Instead of assigning 30 individual policies, you assign one initiative.
{
"policyDefinitions": [
{
"policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/404c3081-a854-4457-ae30-26a93ef643f9",
"parameters": {
"effect": {
"value": "Audit"
}
}
},
{
"policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/34c877ad-507e-4c82-993e-3452a6e0ad3c",
"parameters": {
"effect": {
"value": "Deny"
}
}
},
{
"policyDefinitionReferenceId": "customStorageTls",
"policyDefinitionId": "/subscriptions/YOUR_SUB/providers/Microsoft.Authorization/policyDefinitions/deny-storage-insecure-tls",
"parameters": {}
}
]
}
Create and assign the initiative:
az policy set-definition create \
--name "security-baseline" \
--display-name "Security Baseline Initiative" \
--definitions ./initiatives/security-baseline.json
az policy assignment create \
--name "security-baseline-assignment" \
--display-name "Security Baseline" \
--policy-set-definition "security-baseline" \
--scope "/subscriptions/YOUR_SUBSCRIPTION_ID"
Compliance Evaluation and Remediation
Azure Policy evaluates compliance automatically, but the cycle can take up to 24 hours. For CI/CD scenarios, you need on-demand evaluation. Trigger it with:
az policy state trigger-scan --resource-group "my-resource-group"
This is an asynchronous operation. The scan runs in the background and can take several minutes depending on resource count. To check compliance state after the scan:
az policy state summarize \
--filter "complianceState eq 'NonCompliant'" \
--query "value[].{policy:policyAssignmentName, nonCompliant:results.nonCompliantResources}" \
-o table
For policies with DeployIfNotExists or Modify effects, non-compliant resources can be automatically remediated. Create a remediation task:
az policy remediation create \
--name "remediate-storage-tls" \
--policy-assignment "enforce-storage-tls" \
--resource-group "my-resource-group"
The remediation task creates a managed identity and deploys the corrective template to each non-compliant resource. Monitor progress with:
az policy remediation show \
--name "remediate-storage-tls" \
--resource-group "my-resource-group" \
--query "{status:provisioningState, succeeded:deploymentStatus.successfulDeployments, failed:deploymentStatus.failedDeployments}"
Integrating Policy Checks into CI/CD Pipelines
This is where policy becomes a real DevOps tool rather than a checkbox exercise. The pattern I use is a pre-deployment gate that evaluates the target resource group's compliance state and blocks the deployment if violations exist.
Here is an Azure DevOps pipeline YAML stage that runs a compliance check before deploying:
stages:
- stage: ComplianceGate
displayName: 'Policy Compliance Check'
jobs:
- job: EvaluateCompliance
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
displayName: 'Trigger Policy Evaluation'
inputs:
azureSubscription: 'my-azure-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az policy state trigger-scan \
--resource-group $(resourceGroup) \
--no-wait
- task: AzureCLI@2
displayName: 'Wait and Check Compliance'
inputs:
azureSubscription: 'my-azure-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
sleep 120
NON_COMPLIANT=$(az policy state summarize \
--resource-group $(resourceGroup) \
--query "value[0].results.nonCompliantResources" -o tsv)
echo "Non-compliant resources: $NON_COMPLIANT"
if [ "$NON_COMPLIANT" -gt 0 ]; then
echo "##vso[task.logissue type=error]Found $NON_COMPLIANT non-compliant resources"
echo "##vso[task.complete result=Failed;]Compliance check failed"
exit 1
fi
echo "All resources compliant. Proceeding with deployment."
- stage: Deploy
displayName: 'Deploy Infrastructure'
dependsOn: ComplianceGate
condition: succeeded()
jobs:
- deployment: DeployInfra
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureResourceManagerTemplateDeployment@3
inputs:
azureResourceManagerConnection: 'my-azure-service-connection'
resourceGroupName: $(resourceGroup)
templateLocation: 'linkedArtifact'
csmFile: '$(Pipeline.Workspace)/templates/main.bicep'
For a more sophisticated approach, use the Node.js SDK to do a what-if deployment analysis combined with policy evaluation. This catches violations before any resources are actually created.
Node.js SDK for Policy Management
The @azure/arm-policy and @azure/arm-policyinsights packages give you full programmatic control over policy definitions, assignments, and compliance state. Here is how to use them:
var PolicyClient = require("@azure/arm-policy").PolicyClient;
var PolicyInsightsClient = require("@azure/arm-policyinsights").PolicyInsightsClient;
var DefaultAzureCredential = require("@azure/identity").DefaultAzureCredential;
var subscriptionId = process.env.AZURE_SUBSCRIPTION_ID;
var credential = new DefaultAzureCredential();
var policyClient = new PolicyClient(credential, subscriptionId);
var insightsClient = new PolicyInsightsClient(credential, subscriptionId);
// List all policy assignments for a subscription
function listAssignments() {
return policyClient.policyAssignments.list().then(function(assignments) {
assignments.forEach(function(assignment) {
console.log(assignment.displayName + " -> " + assignment.policyDefinitionId);
});
});
}
// Create a policy assignment
function createAssignment(name, policyDefinitionId, scope, parameters) {
var assignment = {
policyDefinitionId: policyDefinitionId,
displayName: name,
parameters: parameters || {},
enforcementMode: "Default"
};
return policyClient.policyAssignments.create(scope, name, assignment);
}
// Get compliance summary
function getComplianceSummary(resourceGroupName) {
var scope = "/subscriptions/" + subscriptionId;
if (resourceGroupName) {
scope += "/resourceGroups/" + resourceGroupName;
}
return insightsClient.policyStates.summarizeForSubscription(
"latest",
{ queryOptions: { filter: "complianceState eq 'NonCompliant'" } }
).then(function(result) {
var summary = result.value[0];
console.log("Total resources: " + summary.results.totalResources);
console.log("Non-compliant: " + summary.results.nonCompliantResources);
console.log("Non-compliant policies: " + summary.results.nonCompliantPolicies);
if (summary.policyAssignments) {
summary.policyAssignments.forEach(function(pa) {
console.log(" " + pa.policyAssignmentId + ": " +
pa.results.nonCompliantResources + " non-compliant");
});
}
return summary;
});
}
// Trigger on-demand evaluation
function triggerEvaluation(resourceGroupName) {
var scope = "/subscriptions/" + subscriptionId +
"/resourceGroups/" + resourceGroupName;
return insightsClient.policyStates.beginTriggerResourceGroupEvaluation(
subscriptionId,
resourceGroupName
).then(function(poller) {
console.log("Evaluation triggered. Waiting for completion...");
return poller.pollUntilDone();
}).then(function() {
console.log("Evaluation complete.");
});
}
Policy Exemptions and Waivers
Not every non-compliant resource is a problem. Sometimes you have a legitimate reason to deviate from a policy. Policy exemptions let you document and approve deviations without disabling the policy globally.
There are two exemption categories:
- Waiver — the resource is acknowledged as non-compliant, but the team accepts the risk
- Mitigated — the compliance requirement is met through an alternative control
function createExemption(resourceGroupName, exemptionName, assignmentId, reason) {
var scope = "/subscriptions/" + subscriptionId +
"/resourceGroups/" + resourceGroupName;
var exemption = {
policyAssignmentId: assignmentId,
exemptionCategory: "Waiver",
displayName: exemptionName,
description: reason,
expiresOn: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days
};
return policyClient.policyExemptions.createOrUpdate(
scope,
exemptionName,
exemption
).then(function(result) {
console.log("Exemption created: " + result.name);
console.log("Expires: " + result.expiresOn);
return result;
});
}
Always set an expiration date on exemptions. Permanent exemptions are a governance anti-pattern. Review and renew them quarterly.
Compliance Dashboards and Reporting
Azure Policy compliance data is available through the Policy Insights API. For teams that need to generate compliance reports for auditors or leadership, here is a Node.js script that produces a structured compliance report:
function generateComplianceReport() {
return insightsClient.policyStates.listQueryResultsForSubscription(
"latest",
{ queryOptions: { filter: "complianceState eq 'NonCompliant'" } }
).then(function(results) {
var report = {
generatedAt: new Date().toISOString(),
subscriptionId: subscriptionId,
violations: []
};
var items = [];
return collectPages(results, items);
}).then(function(items) {
var report = {
generatedAt: new Date().toISOString(),
subscriptionId: subscriptionId,
totalViolations: items.length,
violations: items.map(function(state) {
return {
resourceId: state.resourceId,
resourceType: state.resourceType,
policyAssignment: state.policyAssignmentName,
policyDefinition: state.policyDefinitionName,
complianceState: state.complianceState,
timestamp: state.timestamp
};
})
};
return report;
});
}
function collectPages(iterator, items) {
return iterator.next().then(function(result) {
if (result.done) {
return items;
}
items.push(result.value);
return collectPages(iterator, items);
});
}
// Generate and save report
generateComplianceReport().then(function(report) {
var fs = require("fs");
var filename = "compliance-report-" +
new Date().toISOString().split("T")[0] + ".json";
fs.writeFileSync(filename, JSON.stringify(report, null, 2));
console.log("Report saved to " + filename);
console.log("Total violations: " + report.totalViolations);
}).catch(function(err) {
console.error("Failed to generate report:", err.message);
process.exit(1);
});
Guest Configuration Policies
Standard Azure Policy operates at the control plane level. Guest Configuration extends policy evaluation inside virtual machines. It uses Azure Automanage Machine Configuration to audit or enforce settings inside Windows and Linux VMs.
Common guest configuration scenarios:
- Ensure specific Windows features are enabled or disabled
- Verify that certain services are running
- Check that password policies meet complexity requirements
- Audit installed applications against an approved list
Guest configuration requires the Azure Connected Machine agent (for Arc-enabled servers) or the Guest Configuration extension for Azure VMs. Assign a guest configuration policy like any other:
az policy assignment create \
--name "audit-password-policy" \
--display-name "Audit VM Password Policy" \
--policy "/providers/Microsoft.Authorization/policyDefinitions/ea53dbee-c6c9-4f0e-9f9e-de0039b78023" \
--scope "/subscriptions/YOUR_SUBSCRIPTION_ID" \
--mi-system-assigned \
--location "eastus"
The --mi-system-assigned flag is critical. Guest configuration policies use a managed identity to communicate with the VM. Without it, the evaluation silently fails.
Regulatory Compliance Built-In Initiatives
Azure provides built-in initiatives that map directly to regulatory frameworks. These are invaluable if you need to demonstrate compliance with standards like:
- CIS Microsoft Azure Foundations Benchmark — 200+ controls for Azure hardening
- NIST SP 800-53 Rev. 5 — Federal information systems security controls
- PCI DSS v4.0 — Payment card industry requirements
- HIPAA HITRUST — Healthcare data protection
- SOC 2 Type II — Service organization controls
- ISO 27001:2013 — Information security management
Assign a regulatory initiative:
az policy assignment create \
--name "cis-benchmark" \
--display-name "CIS Azure Foundations Benchmark" \
--policy-set-definition "06f19060-9e68-4070-92ca-f15cc126059e" \
--scope "/subscriptions/YOUR_SUBSCRIPTION_ID" \
--mi-system-assigned \
--location "eastus"
These initiatives start with audit effects by default. Do not switch them to deny without careful planning. A CIS benchmark with deny effects will break deployments across your entire subscription because many controls are extremely strict.
Complete Working Example
This Node.js application ties everything together. It manages policy assignments, evaluates compliance, and can be called from an Azure DevOps pipeline as a pre-deployment gate.
// compliance-gate.js
var PolicyClient = require("@azure/arm-policy").PolicyClient;
var PolicyInsightsClient = require("@azure/arm-policyinsights").PolicyInsightsClient;
var DefaultAzureCredential = require("@azure/identity").DefaultAzureCredential;
var subscriptionId = process.env.AZURE_SUBSCRIPTION_ID;
var resourceGroup = process.env.TARGET_RESOURCE_GROUP;
var maxNonCompliant = parseInt(process.env.MAX_NON_COMPLIANT || "0", 10);
var triggerScan = process.env.TRIGGER_SCAN === "true";
if (!subscriptionId || !resourceGroup) {
console.error("AZURE_SUBSCRIPTION_ID and TARGET_RESOURCE_GROUP are required");
process.exit(1);
}
var credential = new DefaultAzureCredential();
var policyClient = new PolicyClient(credential, subscriptionId);
var insightsClient = new PolicyInsightsClient(credential, subscriptionId);
function run() {
console.log("=== Azure Policy Compliance Gate ===");
console.log("Subscription: " + subscriptionId);
console.log("Resource Group: " + resourceGroup);
console.log("Max allowed non-compliant: " + maxNonCompliant);
console.log("");
var pipeline = Promise.resolve();
if (triggerScan) {
pipeline = pipeline.then(function() {
return triggerEvaluation();
});
}
return pipeline
.then(function() {
return getDetailedCompliance();
})
.then(function(result) {
return evaluateGate(result);
})
.catch(function(err) {
console.error("Compliance gate error: " + err.message);
if (err.statusCode) {
console.error("Status code: " + err.statusCode);
}
process.exit(2);
});
}
function triggerEvaluation() {
console.log("Triggering on-demand policy evaluation...");
return insightsClient.policyStates
.beginTriggerResourceGroupEvaluation(subscriptionId, resourceGroup)
.then(function(poller) {
return poller.pollUntilDone();
})
.then(function() {
console.log("Evaluation complete.\n");
});
}
function getDetailedCompliance() {
var scope = "/subscriptions/" + subscriptionId +
"/resourceGroups/" + resourceGroup;
return insightsClient.policyStates.summarizeForResourceGroup(
"latest",
subscriptionId,
resourceGroup
).then(function(summary) {
var data = summary.value[0];
var result = {
totalResources: data.results.totalResources,
compliantResources: data.results.totalResources - data.results.nonCompliantResources,
nonCompliantResources: data.results.nonCompliantResources,
nonCompliantPolicies: data.results.nonCompliantPolicies,
assignments: []
};
if (data.policyAssignments) {
data.policyAssignments.forEach(function(pa) {
if (pa.results.nonCompliantResources > 0) {
result.assignments.push({
name: pa.policyAssignmentId.split("/").pop(),
nonCompliantResources: pa.results.nonCompliantResources,
definitions: (pa.policyDefinitions || [])
.filter(function(pd) { return pd.results.nonCompliantResources > 0; })
.map(function(pd) {
return {
name: pd.policyDefinitionId.split("/").pop(),
nonCompliant: pd.results.nonCompliantResources,
effect: pd.effect
};
})
});
}
});
}
return result;
});
}
function evaluateGate(compliance) {
console.log("=== Compliance Summary ===");
console.log("Total resources: " + compliance.totalResources);
console.log("Compliant: " + compliance.compliantResources);
console.log("Non-compliant: " + compliance.nonCompliantResources);
console.log("Violating policies: " + compliance.nonCompliantPolicies);
console.log("");
if (compliance.assignments.length > 0) {
console.log("=== Non-Compliant Policy Assignments ===");
compliance.assignments.forEach(function(assignment) {
console.log("\n Assignment: " + assignment.name);
console.log(" Non-compliant resources: " + assignment.nonCompliantResources);
assignment.definitions.forEach(function(def) {
console.log(" - " + def.name + " (" + def.effect + "): " +
def.nonCompliant + " resource(s)");
});
});
console.log("");
}
var compliancePercent = compliance.totalResources > 0
? ((compliance.compliantResources / compliance.totalResources) * 100).toFixed(1)
: "100.0";
console.log("Compliance rate: " + compliancePercent + "%");
if (compliance.nonCompliantResources > maxNonCompliant) {
console.error("\nGATE FAILED: " + compliance.nonCompliantResources +
" non-compliant resources exceed threshold of " + maxNonCompliant);
// Output Azure DevOps variable for downstream tasks
console.log("##vso[task.setvariable variable=compliancePassed]false");
console.log("##vso[task.logissue type=error]Policy compliance gate failed with " +
compliance.nonCompliantResources + " violations");
process.exit(1);
}
console.log("\nGATE PASSED: All resources within compliance threshold.");
console.log("##vso[task.setvariable variable=compliancePassed]true");
return compliance;
}
run();
Use this in an Azure DevOps pipeline:
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
- script: |
npm install @azure/arm-policy @azure/arm-policyinsights @azure/identity
displayName: 'Install Dependencies'
- task: AzureCLI@2
displayName: 'Run Compliance Gate'
inputs:
azureSubscription: 'my-azure-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv)
export TARGET_RESOURCE_GROUP="production-rg"
export MAX_NON_COMPLIANT="0"
export TRIGGER_SCAN="true"
node compliance-gate.js
Sample output when the gate fails:
=== Azure Policy Compliance Gate ===
Subscription: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Resource Group: production-rg
Max allowed non-compliant: 0
Triggering on-demand policy evaluation...
Evaluation complete.
=== Compliance Summary ===
Total resources: 24
Compliant: 21
Non-compliant: 3
Violating policies: 2
=== Non-Compliant Policy Assignments ===
Assignment: security-baseline-assignment
Non-compliant resources: 3
- deny-storage-insecure-tls (Deny): 1 resource(s)
- require-resource-tags (Deny): 2 resource(s)
Compliance rate: 87.5%
GATE FAILED: 3 non-compliant resources exceed threshold of 0
##vso[task.setvariable variable=compliancePassed]false
##vso[task.logissue type=error]Policy compliance gate failed with 3 violations
Common Issues and Troubleshooting
1. Policy Assignment Has No Effect
Error: Deployment succeeded but non-compliant resources were created
This happens when the policy assignment's enforcement mode is set to DoNotEnforce. This mode is useful for testing but does not actually block anything. Check the assignment:
az policy assignment show --name "my-assignment" --query "enforcementMode"
If it returns "DoNotEnforce", update it:
az policy assignment update --name "my-assignment" --enforcement-mode Default
Also check that the policy effect is Deny, not Audit. An audit effect logs violations but does not prevent deployment.
2. Remediation Task Fails with Identity Error
Error: The policy assignment does not have a managed identity.
Remediation requires a managed identity to be associated with the policy assignment.
DeployIfNotExists and Modify policies require a managed identity on the assignment. The identity needs appropriate RBAC roles on the target scope. Fix it by recreating the assignment with an identity:
az policy assignment create \
--name "deploy-diagnostics" \
--policy "deploy-diagnostic-settings" \
--scope "/subscriptions/YOUR_SUB" \
--mi-system-assigned \
--location "eastus" \
--role "Contributor" \
--identity-scope "/subscriptions/YOUR_SUB"
3. Compliance Evaluation Returns Stale Data
Non-compliant count shows 5 but I just fixed those resources
Azure Policy evaluation runs on a roughly 24-hour cycle. After fixing resources, you must trigger an on-demand scan and wait for it to complete:
az policy state trigger-scan --resource-group "my-rg"
The scan is asynchronous. For a resource group with 100+ resources, it can take 5-15 minutes. Do not query compliance state immediately after triggering. In CI/CD pipelines, add a sleep or polling loop.
4. Custom Policy Definition Fails Validation
Error: (InvalidPolicyRule) The policy rule is invalid. Failed to parse the policy rule:
'The property 'filed' is not supported. Supported properties include 'field'.'
This is a typo in the policy rule JSON. The policy engine validates property names strictly. Common mistakes include:
filedinstead offieldequalsinstead ofEquals(case matters for some operators, butequalsis actually correct — check the specific operator)- Using
valuewhen you needfield(they have different evaluation behaviors) - Missing the
allOforanyOfwrapper when combining conditions
Validate your policy definition before deploying:
az policy definition create \
--name "test-policy" \
--rules ./my-policy.json \
--mode All \
--validate
5. Policy Blocks Terraform/Bicep Deployment with Cryptic Error
Error: creating/updating Resource: Status=403 Code="RequestDisallowedByPolicy"
Message="Resource 'storageaccount01' was disallowed by policy.
Policy identifiers: '[{"policyAssignment":{"name":"enforce-naming"}]'"
The resource failed a deny policy. The error message includes the policy assignment name but not which specific rule it violated. To debug:
az policy assignment show --name "enforce-naming" --query "policyDefinitionId" -o tsv
az policy definition show --name $(az policy assignment show --name "enforce-naming" --query "policyDefinitionId" -o tsv | xargs basename)
Read the policy rule to understand what condition triggered the denial, then fix your infrastructure code to comply.
Best Practices
Start with audit, graduate to deny. Deploy policies in audit mode first. Review the compliance dashboard for two weeks. Only switch to deny after you have addressed existing violations and confirmed the policy does not break legitimate workflows.
Use initiatives, not individual assignments. Group related policies into initiatives. This reduces assignment sprawl, simplifies exemption management, and makes it easier to apply consistent governance across environments.
Automate compliance gates in pipelines. Do not rely on periodic compliance scans alone. Add pre-deployment compliance checks to every production pipeline. Catching violations before deployment is far cheaper than remediating them afterward.
Set expiration dates on all exemptions. An exemption without an expiry is technical debt that never gets paid. Set 90-day maximums and require re-approval for renewals.
Version control your policy definitions. Store policy JSON files in a Git repository alongside your infrastructure code. Use CI/CD to deploy policy definitions so that changes go through code review and approval workflows.
Use naming conventions for policy resources. Prefix policy definitions with the team or domain (
security-,network-,cost-). Use consistent naming for assignments across subscriptions so you can query and report on them programmatically.Test policies in isolated subscriptions first. A misconfigured deny policy in production can block all deployments. Always test in a sandbox subscription before rolling out to production scopes.
Monitor policy evaluation performance. Large numbers of policies or complex conditions can slow down resource deployments. If ARM deployments are taking longer than expected, check the number of policy evaluations using Activity Log entries with operation name
Microsoft.Authorization/policies/audit/action.