Policy as Code in Azure with Azure Policy
Azure Policy enforces compliance rules on every resource in your Azure environment — whether deployed by a pipeline, Terraform, or a developer clicking in the portal. Policy as Code means managing those rules in version-controlled files, testing them in CI, and applying them through a pipeline. This page covers how to write, test, and deploy Azure Policy definitions as part of your DevOps workflow.
Core Azure Policy concepts
Before writing policy, you need to understand the building blocks:
- Policy definition. A JSON document that describes a rule and its effect. A definition says “if a resource matches this condition, apply this effect.”
- Policy assignment. Binds a policy definition to a scope (management group, subscription, or resource group). A policy has no effect until it is assigned.
- Initiative (policy set). A named collection of policy definitions. Assign one initiative instead of many individual policies.
- Effect. What happens when a resource violates the policy. Common effects: Deny, Audit, DeployIfNotExists, Modify, AuditIfNotExists.
- Compliance state. A dashboard showing what percentage of resources in each scope are compliant.
Writing a custom policy definition
Built-in policies cover common scenarios, but you will often need custom policies for organization-specific rules. A common example is requiring a specific tag (like cost-center) on all resource groups.
{
"name": "require-cost-center-tag",
"type": "Microsoft.Authorization/policyDefinitions",
"properties": {
"displayName": "Require cost-center tag on resource groups",
"description": "Requires the 'cost-center' tag to be present on all resource groups. This enables cost allocation reporting.",
"mode": "All",
"metadata": {
"category": "Tags",
"version": "1.0.0"
},
"parameters": {
"effect": {
"type": "String",
"defaultValue": "Deny",
"allowedValues": ["Audit", "Deny"],
"metadata": {
"displayName": "Effect",
"description": "Audit to log non-compliance, Deny to block creation"
}
}
},
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Resources/resourceGroups"
},
{
"field": "tags['cost-center']",
"exists": "false"
}
]
},
"then": {
"effect": "[parameters('effect')]"
}
}
}
}
# Deploy the policy definition to a subscription
az policy definition create \
--name "require-cost-center-tag" \
--display-name "Require cost-center tag on resource groups" \
--description "Requires the cost-center tag on resource groups" \
--rules policy-definition.json \
--mode All
# Assign the policy to a resource group
az policy assignment create \
--name "require-cost-center-prod" \
--policy "require-cost-center-tag" \
--scope "/subscriptions/$(az account show --query id -o tsv)/resourceGroups/myapp-prod-rg" \
--params '{"effect": {"value": "Deny"}}'
# Check compliance status
az policy state summarize \
--resource-group myapp-prod-rg \
--query "results.nonCompliantResources"
Managing Azure Policy with Terraform
Terraform’s azurerm_policy_definition and azurerm_policy_assignment resources let you manage your entire policy estate as code, versioned in git and applied through your infrastructure pipeline.
# policy/require-tags.tf
# Load the policy rule JSON from a separate file
resource "azurerm_policy_definition" "require_cost_center" {
name = "require-cost-center-tag"
policy_type = "Custom"
mode = "All"
display_name = "Require cost-center tag on resource groups"
description = "Blocks creation of resource groups that lack a cost-center tag."
metadata = jsonencode({
category = "Tags"
version = "1.0.0"
})
parameters = jsonencode({
effect = {
type = "String"
defaultValue = "Deny"
allowedValues = ["Audit", "Deny"]
metadata = {
displayName = "Effect"
}
}
})
policy_rule = jsonencode({
if = {
allOf = [
{
field = "type"
equals = "Microsoft.Resources/resourceGroups"
},
{
field = "tags['cost-center']"
exists = "false"
}
]
}
then = {
effect = "[parameters('effect')]"
}
})
}
# Assign to a subscription
resource "azurerm_subscription_policy_assignment" "require_cost_center" {
name = "require-cost-center"
subscription_id = data.azurerm_subscription.current.id
policy_definition_id = azurerm_policy_definition.require_cost_center.id
display_name = "Require cost-center tag on all resource groups"
parameters = jsonencode({
effect = {
value = "Audit" # Start with Audit, switch to Deny once you have verified impact
}
})
}
Testing policies in a pipeline before assigning
The Azure Policy Compliance Scan pipeline task triggers an on-demand compliance evaluation and waits for results. Use this in a pipeline to detect new policy violations introduced by a deployment before they accumulate in the compliance dashboard.
# azure-pipelines.yml — policy compliance check after deployment
- stage: Compliance_Check
displayName: 'Policy Compliance Scan'
dependsOn: Deploy_Dev
jobs:
- job: PolicyScan
steps:
- task: AzurePolicyCheckGate@0
displayName: 'Run Azure Policy compliance scan'
inputs:
azureSubscription: 'My Azure Subscription'
resourceGroupName: 'myapp-dev-rg'
# Report compliance state
- task: AzureCLI@2
displayName: 'Report compliance summary'
inputs:
azureSubscription: 'My Azure Subscription'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az policy state summarize \
--resource-group myapp-dev-rg \
--query '{
totalResources: results.queryResultsUri,
nonCompliant: results.nonCompliantResources,
compliantPolicies: results.compliantPolicies
}' \
--output table
DeployIfNotExists: auto-remediation policies
The DeployIfNotExists effect automatically deploys a related resource if it is missing. A common use case is automatically enabling diagnostic settings on a resource if they were not configured during deployment. This is policy-driven remediation that runs without pipeline intervention.
{
"policyRule": {
"if": {
"field": "type",
"equals": "Microsoft.Storage/storageAccounts"
},
"then": {
"effect": "DeployIfNotExists",
"details": {
"type": "Microsoft.Insights/diagnosticSettings",
"name": "setByPolicy",
"roleDefinitionIds": [
"/providers/microsoft.authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
],
"deployment": {
"properties": {
"mode": "incremental",
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"resourceName": { "type": "string" },
"logAnalyticsWorkspaceId": { "type": "string" }
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts/providers/diagnosticSettings",
"name": "[concat(parameters('resourceName'), '/Microsoft.Insights/setByPolicy')]",
"apiVersion": "2021-05-01-preview",
"properties": {
"workspaceId": "[parameters('logAnalyticsWorkspaceId')]",
"logs": [],
"metrics": [
{
"category": "Transaction",
"enabled": true
}
]
}
}
]
}
}
}
}
}
}
}
When using DeployIfNotExists, the policy’s managed identity needs explicit role assignments to perform the remediation deployment. Always include roleDefinitionIds in the policy rule — Azure Policy will automatically create the required role assignment for the system-assigned managed identity when you assign the policy.
Common mistakes
- Assigning Deny policies without testing with Audit first. A Deny policy on a subscription can immediately break running pipelines and application deployments. Always assign new policies with the Audit effect first, review the compliance report to understand the impact, then switch to Deny once you have resolved all violations.
- Assigning policies at the wrong scope. A policy assigned at the subscription level affects all resource groups. A policy intended for production only should be scoped to the production resource group or management group. Over-broad scope causes unintended failures in dev and test environments.
- Forgetting exemptions for legitimate exceptions. Some resources genuinely cannot comply with a policy — an old legacy resource, a third-party integration requirement, or a temporary waiver. Use policy exemptions (waiver or mitigated category) rather than writing complex policy conditions to carve out exceptions globally.
Summary
- Azure Policy enforces compliance rules at the ARM level — resources that violate a Deny policy cannot be created regardless of who or what deploys them.
- Store policy definitions as JSON or HCL (Terraform) files in git and deploy them through your infrastructure pipeline for version control and auditability.
- Always start with the Audit effect on new policies to understand the blast radius before switching to Deny.
- Use the AzurePolicyCheckGate task in deployment pipelines to surface compliance violations immediately after a deployment.
- DeployIfNotExists policies auto-remediate missing configurations (diagnostic settings, monitoring agents) without requiring pipeline changes.
Frequently asked questions
What is the difference between Audit and Deny effects in Azure Policy?
Audit records non-compliant resources in the Azure Policy compliance dashboard but does not block their creation. Deny prevents non-compliant resources from being created or updated. Start with Audit to understand the impact of a policy before switching to Deny — otherwise you may block legitimate resources without warning.
Can Azure Policy block a Terraform or ARM deployment?
Yes. Azure Policy evaluates resources when they are submitted to Azure Resource Manager. If a resource violates a Deny policy, the ARM API returns a 403 error and the deployment fails. Terraform will report the deployment as failed and show the policy error message.
What is a policy initiative?
An initiative (also called a policy set definition) is a collection of policy definitions grouped together for a common compliance goal — for example, the Azure Security Benchmark initiative contains dozens of individual policies that together enforce CIS benchmark controls. You assign an initiative to a scope just like a single policy.