Azure Policy as code: the deployment pattern that does not cause outages

Azure Policy is the strongest governance engine of the three big clouds and the easiest to misuse. Here is how we write, test and roll out policy so a Deny effect never surprises a deployment.

Azure Policy can do something the other clouds cannot do as cleanly: evaluate a resource at creation time and refuse it, or deploy a missing configuration automatically. That is a genuine advantage over detecting the misconfiguration after the fact and filing a ticket.

It is also the mechanism most likely to break a Friday deployment, because a Deny assigned at the root management group applies to everything immediately, including the pipeline of a team that has never heard of you.

The effects, and what each is for

  • Audit — records non-compliance, changes nothing. Where every policy starts.
  • Deny — blocks creation or update. For the small set of things that must never exist: public blob access, public SQL endpoints, resources outside permitted regions, untagged production resources.
  • DeployIfNotExists — creates a missing sub-resource. The most useful effect in practice: diagnostic settings to the central workspace, Defender plans on a new subscription, backup on a production VM.
  • Modify — adds or changes properties, typically tags inherited from the resource group.
  • AuditIfNotExists — audits absence of a related resource. Use it as the audit-mode twin of a DeployIfNotExists while you evaluate.

A common mistake is reaching for Deny when the intent is a default. If you want every storage account to have TLS 1.2 minimum, Modify sets it and nobody is blocked. Deny blocks the deployment and someone has to change their template. Both are legitimate; they differ in who does the work.

The rollout sequence

1. Write the policy in Terraform or Bicep, in a repository with review. Policy definitions created in the portal are invisible to your change process and will drift.

2. Assign at management group, in Audit mode, at the scope where the rule genuinely belongs. A rule about production belongs on the production branch, not on the root with thirty exclusions.

3. Wait two weeks and read the compliance data. The compliance percentage tells you how big the change is. Anything below 90 percent compliance means flipping to Deny will block real work; anything near 100 percent means the rule is already how your teams behave and enforcement is free.

4. Remediate the existing non-compliant resources. For DeployIfNotExists and Modify, a remediation task handles it in bulk with a managed identity. Do this before enforcement, so that the Deny applies to a compliant estate.

5. Flip to Deny, one policy at a time, and tell the affected teams first. With a documented exemption path — Azure Policy exemptions are a first-class object with an expiry date and a reason, which is much better than a permanent exclusion nobody remembers granting.

6. Watch for a fortnight, then move on to the next.

Initiatives, not loose policies

Group related policies into an initiative (policy set) and assign the initiative. Thirty individually assigned policies produce a compliance view nobody can read and thirty things to move when the scope changes. Start from the built-in initiatives — the Azure Security Benchmark, CIS, ISO 27001 — and add your own custom definitions alongside rather than rebuilding.

Use parameters properly. One initiative with an allowedRegions parameter assigned differently per branch beats four near-identical initiatives.

Testing

Policy has a real testing story and almost nobody uses it. az policy state trigger-scan forces an evaluation instead of waiting for the periodic cycle. A what-if deployment against a policy assignment in a test subscription tells you whether a template would be denied. And a dedicated test subscription inside your production management group — running your actual deployment templates — is the highest-value test environment for policy work, because it catches the case where your production policy blocks your production pipeline.

Exemptions with expiry, not exclusions

Scope exclusions are permanent and invisible. Exemptions carry a reason, an expiry date and show in compliance reporting as an explicit decision. Use exemptions, set an expiry, and review them quarterly. An estate with twenty expired exemptions is telling you something; an estate with twenty scope exclusions is telling you nothing.

This is the machinery underneath the Azure landing zone, and it is what makes the difference between a governance model on a slide and one in the resource provider. It is also what stops Defender recommendations from coming back after you remediate them. We deploy it as part of a security engagement.

What to do this week

Pick the one rule you most wish were true across your estate — probably "no storage account allows public blob access" — and assign it in Audit mode at your production management group. In two weeks the compliance number will tell you whether enforcing it is a click or a project.

ConsultorIA

Want this done on your cloud?

A ten-day read-only assessment is free, and Skyline lets you see your estate on a map before you write to us.

Related articles