Azure Spring Clean 2026 : Policy-as-Code Made Simple: Modern Azure Governance with Bicep (Part 1)

Azure Spring Clean 2026 : Policy-as-Code Made Simple: Modern Azure Governance with Bicep (Part 1)

This is Part 1 of a four-part series for Azure Spring Clean “Policy-as-Code Made Simple: Modern Azure Governance with Bicep”.

Introduction

I have been working with Azure Policy across multiple projects and environments over the past few years, and the one thing that consistently slows teams down is authoring and maintaining policy definitions in raw JSON. The structure is verbose, the nesting gets deep quickly, and a single misplaced bracket can break an entire deployment.

This series shows some of the recent additions to bicep that help with managing reusable JSON blocks.

The Problem with Raw JSON

Azure Policy definitions are JSON documents at their core. A simple “deny resources without tags” policy looks something like this:

{
  "mode": "Indexed",
  "parameters": {
    "tagName": {
      "type": "String",
      "metadata": {
        "displayName": "Tag Name",
        "description": "Name of the tag"
      }
    }
  },
  "policyRule": {
    "if": {
      "field": "[concat('tags[', parameters('tagName'), ']')]",
      "exists": "false"
    },
    "then": {
      "effect": "deny"
    }
  }
}

This is a straightforward policy, and it already has enough nesting and bracket-matching to trip people up, making it hard to manage and maintain. Every definition needs its own JSON file, which leads to many files with subtle differences and makes reviews difficult.

Bicep for Policy Definitions

Bicep is a domain specific language that generates ARM Templates and deploys to Azure. This can be used for policy definitions and assignments, it brings the IDE and Agentic tools, and produces a more readable structure. The same tag-enforcement policy in Bicep:

targetScope = 'subscription'

param tagName string

resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {
  name: 'require-tag-on-resources'
  properties: {
    displayName: 'Require a tag on resources'
    policyType: 'Custom'
    mode: 'Indexed'
    metadata: {
      version: '1.0.0'
      category: 'Tags'
    }
    parameters: {
      tagName: {
        type: 'String'
        metadata: {
          displayName: 'Tag Name'
          description: 'Name of the tag'
        }
      }
    }
    policyRule: {
      if: {
        field: '[concat(\'tags[\', parameters(\'tagName\'), \']\')]'
        exists: 'false'
      }
      then: {
        effect: 'deny'
      }
    }
  }
}

The structure is immediately cleaner. There are no closing braces to count, the property types are validated at compile time, and VS Code gives auto-completion for the resource API schema. The targetScope = 'subscription' declaration makes it explicit where this definition lives.

The catch is that the policyRule block still contains ARM template expressions (the concat and parameters calls). These are evaluated at runtime by the Azure Policy engine, not at deployment time by Bicep. This is an important distinction. Bicep compiles to ARM JSON, but the policy rule expressions are pass-through strings that Azure Policy evaluates later during compliance scans.

Deploying Policy Definitions

Policy definitions and assignments typically target subscription or management group scope. Bicep handles this with the targetScope declaration and the corresponding deployment command:

# Deploy a policy definition at subscription scope
az deployment sub create \
  --location uksouth \
  --template-file policy-definition.bicep

For management group deployments, the Bicep file needs targetScope = 'managementGroup' instead. This is a common pattern for organisations that manage policies centrally deploying the definitions to a central Management Group and assigning them across multiple subscriptions or lower management groups.

What This Buys You

Moving policy authoring to Bicep is not just about syntax. The practical benefits I have found across projects include:

  • Compile-time validation catches structural errors before deployment
  • Single source of truth for policy definitions, assignments, and initiatives in one repository
  • Parameterisation without file duplication, making multi-environment governance manageable
  • Git history that is actually readable, because Bicep diffs are cleaner than JSON diffs

The gotcha is that you still need to understand how ARM template expressions work inside the policyRule block. Bicep does not abstract those away. The policy engine evaluates those expressions at compliance scan time, so they follow ARM function syntax regardless of the authoring language.

What’s Next

Bicep 0.40.2 graduated multi-line string interpolation to GA. In Part 2 I dig into how this and other Bicep features make policy authoring simpler, and in Part 3 I cover multi-line string interpolation in depth.

Further Resources