Azure Policy at Scale: Governance as Code

Azure Policy is the enforcement engine of your governance model. It is also the fastest way to make every engineering team hate the platform team if you roll it out carelessly. The craft is in writing policies that encode real standards, deploying them through code, and introducing enforcement gradually enough that compliance improves without work stopping.

Effects and When to Use Them

Five effects cover almost everything. Audit marks non compliant resources and costs nothing, which makes it the right starting point for every new policy. Deny blocks creation and is where mature standards end up. Modify fixes properties during creation, ideal for adding tags or flipping a boolean like disabling public access. DeployIfNotExists creates companion resources after the fact, the workhorse for diagnostic settings and private DNS wiring. And Disabled exists so you can park a policy without deleting the assignment.

Custom Definitions in Terraform

The built in library covers a lot, but every organization ends up with custom rules. Here is a real one: deny storage accounts that allow shared key access, since Entra based auth should be the only path.

resource "azurerm_policy_definition" "deny_shared_key" {
  name                = "deny-storage-shared-key"
  policy_type         = "Custom"
  mode                = "Indexed"
  display_name        = "Storage accounts must not allow shared key access"
  management_group_id = azurerm_management_group.root.id

  policy_rule = jsonencode({
    if = {
      allOf = [
        {
          field  = "type"
          equals = "Microsoft.Storage/storageAccounts"
        },
        {
          field     = "Microsoft.Storage/storageAccounts/allowSharedKeyAccess"
          notEquals = false
        }
      ]
    }
    then = {
      effect = "[parameters('effect')]"
    }
  })

  parameters = jsonencode({
    effect = {
      type          = "String"
      allowedValues = ["Audit", "Deny", "Disabled"]
      defaultValue  = "Audit"
    }
  })
}

Always parameterize the effect. It lets the same definition run as Audit in sandbox and Deny in production without duplicating code.

Initiatives and Remediation

Never assign policies individually at scale, group them into initiatives per domain: a security baseline, a monitoring baseline, a tagging standard. One assignment per scope, one compliance score per domain. For DeployIfNotExists initiatives, the assignment needs a managed identity with the roles required to create the remediation resources, and existing resources only get fixed when you create a remediation task.

resource "azurerm_management_group_policy_assignment" "monitoring" {
  name                 = "monitoring-baseline"
  management_group_id  = azurerm_management_group.landing_zones.id
  policy_definition_id = azurerm_policy_set_definition.monitoring.id
  location             = "westeurope"

  identity {
    type = "SystemAssigned"
  }

  parameters = jsonencode({
    logAnalytics = { value = azurerm_log_analytics_workspace.central.id }
  })
}

resource "azurerm_role_assignment" "policy_contributor" {
  scope                = azurerm_management_group.landing_zones.id
  role_definition_name = "Monitoring Contributor"
  principal_id         = azurerm_management_group_policy_assignment.monitoring.identity[0].principal_id
}

Rolling Out Deny Without Breaking Everyone

The sequence that works: assign as Audit everywhere, publish the compliance dashboard, give teams a dated warning with the exact remediation steps, flip to Deny in lower environments first, then production two weeks later. Use exemptions, not unassignment, for legitimate exceptions, and give every exemption an expiry date so they get rereviewed instead of living forever. Track the compliance percentage per initiative per team over time. When a number is visible and owned, it improves; when policy failures are surprises inside a pipeline run, you get resentment and shadow IT.

One last habit: treat policy changes like application changes. Pull requests, review from both platform and an affected workload team, and a changelog teams can subscribe to. Governance as code only earns trust if it behaves like code.

Cheers
Osama

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.