Skip to content
CAMPUX Cloud Bootcamp
Field notes · Governance
Custom Azure Policy definition

How to write a custom Azure Policy definition

By Captain O8 min read

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')]" }
    }
  }
}

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:

EffectWhat it doesUse when
AuditFlags non-compliant resources; changes nothingTesting, or you only need visibility
DenyBlocks creation/update of non-compliant resourcesEnforcing a rule going forward
ModifyAdds/changes properties (e.g. tags)Auto-fixing config on write
DeployIfNotExistsDeploys a remediating resource when one is missinge.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.

Audit-then-Deny is the whole discipline

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.

Further reading — the Microsoft docs
Your next class · free
You've read the idea. Class 1 — What is Cloud? is where you build it, hands-on — no account needed.Start Class 1 →
Captain O
Founder & instructor · CAMPUX Cloud Engineering Bootcamp
Related: Azure governance guardrails · Azure Policy vs RBAC · What is infrastructure as code? · 100 Days of Azure →