How to write a custom Azure Policy definition
The built-in policies cover the common rules. When your rule is specific to your org, you write your own — and it's less code than you'd think: an if that matches a resource and a then that decides what happens.
New to cloud? CAMPUX is a free, build-first course. Start here →
A custom Azure Policy definition is JSON with three moving parts: a mode, optional parameters, and a policyRule that pairs an if condition (matching resource properties with fields and operators) to a then effect (Audit, Deny, DeployIfNotExists, Modify). You assign it at a management group, subscription, or resource-group scope, test it with the Audit effect first, then switch to Deny to enforce. Once you've read one definition, they all read the same way.
Check the built-ins before you write anything — Azure ships hundreds, and there's a good chance a policy for "allowed locations," "require a tag," or "HTTPS-only storage" already exists; assign it and move on. You write a custom definition when the rule is yours: an internal naming convention, a tag taxonomy, a configuration constraint no built-in expresses. Here's the anatomy, then a real one.
The anatomy of a definition
Every definition has the same skeleton. The two parts that matter are if (what to match) and then (what to do):
{
"properties": {
"displayName": "Deny storage accounts without HTTPS-only",
"mode": "all",
"parameters": {
"effect": {
"type": "String",
"allowedValues": ["Audit", "Deny", "Disabled"],
"defaultValue": "Audit"
}
},
"policyRule": {
"if": { /* match a resource */ },
"then": { "effect": "[parameters('effect')]" }
}
}
}
- mode —
allfor every resource type, orindexedto only evaluate resource types that support tags and location (useindexedfor tag/location policies). - parameters — make the rule reusable. Parameterising the effect (default
Audit) is the single best habit: you can assign in audit mode, then flip toDenywithout editing the definition. - policyRule — the
if/thenthat does the work.
Writing the if condition
The if matches resource properties using fields and operators, combined with allOf (AND) and anyOf (OR). For our example — storage accounts that aren't HTTPS-only — match the type and the property:
"if": { "allOf": [ { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, { "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "notEquals": "true" } ] }
Common operators are equals/notEquals, exists, in/notIn, and like (wildcards, handy for naming conventions). An allOf with two clauses reads exactly like it looks: "is a storage account and its HTTPS-only flag isn't true."
Choosing the then effect
The effect decides what happens to a matched resource:
| Effect | What it does | Use when |
|---|---|---|
| Audit | Flags non-compliant resources; changes nothing | Testing, or you only need visibility |
| Deny | Blocks creation/update of non-compliant resources | Enforcing a rule going forward |
| Modify | Adds/changes properties (e.g. tags) | Auto-fixing config on write |
| DeployIfNotExists | Deploys a remediating resource when one is missing | e.g. enforce diagnostic settings |
Because we parameterised the effect, this one definition can audit or deny depending on how it's assigned — no second version.
Assign, test, then enforce
A definition does nothing until it's assigned to a scope. Create the definition, then assign it at a management group, subscription, or resource group:
# create the definition from a JSON file az policy definition create \ --name "deny-storage-no-https" \ --display-name "Deny storage accounts without HTTPS-only" \ --rules "@rule.json" --params "@params.json" --mode All # assign it in AUDIT first, scoped to a subscription az policy assignment create \ --name "deny-storage-no-https" \ --policy "deny-storage-no-https" \ --scope "/subscriptions/<sub>" \ --params '{ "effect": { "value": "Audit" } }'
Let the compliance scan run — it's not instant, a full evaluation can take up to ~30 minutes — and read the compliance results to see what would be affected. When it looks right, update the assignment's effect parameter to Deny and it starts enforcing on new and updated resources.
Never assign a new Deny straight to production. Assign it as Audit, read the compliance report, fix or exempt the surprises, then switch to Deny. The parameterised effect exists precisely so this is a config change, not a code change.
Remediating what already exists
Deny only stops new non-compliance — it won't touch resources already deployed. To fix existing ones, use a Modify or DeployIfNotExists policy and run a remediation task. Those effects need a managed identity on the assignment with a role that can make the change (for example, to add a tag or deploy a diagnostic setting). The remediation task walks the existing non-compliant resources and applies the fix — the same identity-first, keyless pattern the rest of a well-run estate uses.
Grouping policies into initiatives
Once you have several definitions, bundle related ones into an initiative (policy set) and assign the initiative instead of a dozen policies. It's how you ship a coherent baseline — "our storage baseline," "our tagging baseline" — as one assignable unit, and it's the building block landing zones use to enforce a whole governance posture at once.
A policy is just an if and a then. The skill is assigning it in audit, reading the truth, and only then turning it to deny.
Questions people also ask
When should I write a custom Azure Policy definition?
Only when no built-in covers your need. Azure ships hundreds of built-ins for common controls — check and assign one first. Write custom when the rule is org-specific: a naming convention, an internal tag scheme, or a constraint the built-ins don't express.
What is the structure of a policy definition?
JSON with a mode (usually all or indexed), optional parameters for reuse, and a policyRule holding an if condition and a then effect. The if matches resource properties with fields and operators; the then declares what happens when it's true.
What are the Azure Policy effects?
Audit (flag only), Deny (block create/update), Modify (add/change properties like tags), DeployIfNotExists (deploy a remediating resource), AuditIfNotExists, and Disabled. Deny prevents future drift; Modify and DeployIfNotExists can remediate existing resources.
How do I test a policy without breaking anything?
Assign it with the Audit effect (parameterise the effect so you can). Audit reports non-compliance without blocking, so you see real impact across the scope; then switch to Deny. Evaluation isn't instant — a full scan can take ~30 minutes.
How do I remediate existing resources?
Deny only affects new/updated resources; to fix existing ones use a Modify or DeployIfNotExists policy and run a remediation task. Those effects need a managed identity on the assignment with the right role to make the change.