Azure Governance

Policy Definitions vs Initiatives: When to Bundle Controls into a Set

You wrote one Azure Policy that denies storage accounts with public network access, and it works. Then security hands you a list: enforce HTTPS-only, require TLS 1.2, deny public blob access, audit accounts without a private endpoint, require a CostCenter tag, and turn on diagnostic logging — for storage alone. You could assign six policies, one at a time, to every subscription, and then do it again for the eight subscriptions that get onboarded next quarter, and again every time the security list grows. Or you could put all six in a box, label the box “Storage Baseline,” and assign the box once. That box is an initiative — Azure’s UI now calls it a policy set — and choosing when to use one is the single most important governance-at-scale decision after picking the right effect.

A policy definition is one rule: an if condition over a resource, plus a single effect (Deny, Audit, Modify, DeployIfNotExists) that fires when it matches. An initiative is a named collection of policy definitions you assign and track as a unit. That is the entire conceptual difference — an initiative contains nothing a definition doesn’t; it is a wrapper. But the wrapper changes everything about how you operate at scale: one assignment instead of forty, one compliance percentage instead of a spreadsheet of forty, one line to add a control to a hundred subscriptions, and one way to map your rules to a real framework like CIS, PCI-DSS or ISO 27001 so an auditor sees a single “92% compliant against PCI” number instead of being handed a pile of unconnected policies.

This article is about the decision, not the syntax. By the end you’ll know when a rule should stay standalone versus belong in a set, how the one shared effect parameter trick lets the same initiative enforce in prod while only auditing in dev, how compliance rolls up, and the Audit-first rollout that keeps a fifty-control initiative from breaking every pipeline on a Friday. You’ll leave able to look at any pile of controls and say “bundle these, leave that one alone, and here is why.”

What problem this solves

The pain is not writing policies — it is operating them past a handful. Picture governance done one-rule-at-a-time: 60 standalone assignments across a management group, twelve subscriptions and dozens of resource groups. Now answer the auditor’s question: “Are you compliant with PCI-DSS?” There is no single answer — 60 separate percentages, no grouping, no mapping to the 200-odd PCI controls, no way to tell which rules even relate to PCI. You build a spreadsheet by hand, stale the moment a new subscription appears.

Then the change requests start. Security adds one control — “require infrastructure encryption on storage.” Standalone, that means a new assignment on the management group, on every subscription that needs it, and remembered for every future subscription. Miss one and you have a silent gap; multiply by the dozen new controls a year and the toil is constant, the drift inevitable.

What breaks without initiatives is therefore not a deployment; it is governance economics. Standalone policies scale as O(rules × scopes) assignments to maintain. Initiatives collapse that to O(scopes): assign one set per scope, and adding the 61st control is a one-line edit that applies everywhere the set is already assigned. Who hits this: every platform team past the toy stage, anyone chasing a certification, and anyone who has found a subscription that “somehow” never got the encryption policy. The fix: stop treating policies as loose rules and start treating baselines — coherent groups of rules — as the unit you assign.

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand what a single policy does: a policy definition has an if/then rule and an effect, you assign it to a scope (management group, subscription, or resource group), and assignments inherit downward. If “Deny vs Audit vs Modify vs DeployIfNotExists” is fuzzy, read Azure Policy Effects Decoded first — initiatives are containers for those effects. Be comfortable running az in Cloud Shell, and know the Azure resource hierarchy, because where you assign an initiative is half of getting it right.

This sits in the Governance track — the bridge between “I can write a rule” and “I can govern a tenant.” Initiatives are the backbone of Defender for Cloud (its regulatory-compliance dashboard is driven by built-in initiatives) and of landing zone design, where a small library of standard sets is assigned at the management-group tier so every subscription inherits the same baseline. For the full estate-wide picture, the companion is Azure Policy governance at scale; this article is the focused “definitions vs sets” decision underneath it. Ownership usually splits cleanly: the platform/security team authors definitions and sets, the cloud team owns the scopes they assign to, and security/audit reads the rolled-up compliance.

Core concepts

Four mental models make every later decision obvious.

A definition is an atom; an initiative is a molecule. A policy definition is the smallest unit that does anything — one condition, one effect — and you can assign it directly. An initiative (the portal increasingly says policy set definition) is purely a grouping construct with no rule logic of its own; it denies or audits only through its members. So an initiative is never “more powerful” — it has exactly the power of the policies inside it. What it adds is leverage: you handle the molecule instead of every atom.

An initiative references definitions; it does not copy them. Adding a policy stores a reference — the definition’s ID plus a local policyDefinitionReferenceId (unique within the set) and any pinned parameter values; the rule still lives in the standalone definition. Add a definition to an initiative and every scope where it’s already assigned gets the new rule on the next evaluation — no new assignment, no scope editing. That single property is why initiatives win at scale.

One assignment, many rules, one compliance number. Assign a standalone policy and you get one result; assign an initiative and you get a roll-up across its members. This is the feature auditors care about — “we are 94% compliant against the Azure Security Benchmark” is a sentence an initiative can produce and forty loose policies cannot. (How the roll-up aggregates is its own section below.)

Parameters flow from the assignment, through the initiative, into each definition. The powerful move is a single initiative-level parameter — most commonly effect — passed down to many members at once. Set it on the assignment (Audit in dev, Deny in prod) and every member that honours it switches together. Parameters are how one initiative serves many environments without being forked.

Definition vs initiative: the same power, different leverage

The most useful thing to internalise: an initiative is not a different kind of rule — it is a different operating unit. Hold the two side by side (the Glossary at the end defines every term for lookup):

Dimension Standalone definition Initiative (policy set)
What it contains One rule + one effect References to many definitions
Enforcement power Exactly its own effect Exactly the sum of its members
Assignments to manage One per rule per scope One per scope (regardless of rule count)
Adding a rule to N scopes New assignment on each scope One edit to the set definition
Compliance view One percentage One roll-up + per-member drill-down
Maps to a framework No Yes (group + metadata + built-ins)
Parameter sharing Per assignment One set-level knob to many members
Best for A one-off rule with no siblings A baseline — a coherent group

In one sentence: an initiative buys fewer assignments, one number, and framework mapping, at the cost of one extra authoring layer. Everything below is about when that trade is worth taking.

When to bundle and when to leave it standalone

Here is the decision, as a table you can actually use. Run a rule (or a group of rules) down it and the answer falls out:

If… Then… Why
The rule is a one-off with no siblings Leave it standalone A set of one is pure overhead
You have 3+ rules covering one theme (storage, tags, network) Bundle into a themed initiative One assignment, one baseline
You’re chasing a named framework (CIS, PCI, ISO) Use the built-in initiative Pre-mapped; don’t reinvent it
You assign the same group to many subscriptions Bundle and assign at the management group Inherit once, not per-sub
Rules must switch Audit→Deny together per environment Bundle with a shared effect param Flip the whole baseline at once
You need “X% compliant against <standard>” Bundle Only a set produces a roll-up number
The rules have wildly different lifecycles/owners Keep separate (or split sets) Don’t couple unrelated change cadences
You’re still prototyping a single rule Standalone first, bundle later Promote into a set once it’s stable

Two judgement calls hide inside that table. Bundle by coherence, not count — “everything that hardens storage” is a good set; “the ten policies I wrote this sprint” is not. Mind lifecycle coupling: editing any member redeploys the whole set, so don’t mix a stable audited rule with one you tweak weekly. The sweet spot is a handful of baselines (Security, Tagging, Network), each a clear theme, each assigned high.

The one-effect-parameter trick — why it makes sets reusable

This technique turns an initiative from “a folder of policies” into a reusable governance product. A typical member parameterises its own effect — “Storage accounts should disable public network access” accepts Audit, Deny, or Disabled. Hard-code each member’s effect inside the set and you bake a posture in: it’s forever a Deny set, and you need a second set to merely audit the same rules in dev — which is how teams end up with “Storage-Audit” and “Storage-Deny” initiatives that drift apart.

The better pattern: declare one effect parameter on the initiative and wire several members to read from it. Now a single value on the assignment drives the whole baseline — the dev MG gets effect=Audit, the same set on prod gets effect=Deny. One definition, two postures, zero forking; add a control and both inherit it at their own level.

It need not be all-or-nothing — real baselines mix a shared knob with per-member overrides (the Authoring section shows this in code: soft rules read the shared param, security-critical ones pin Deny):

Pattern How When to use
One shared effect for all Every member references [parameters('effect')] Homogeneous baseline; flip together
Shared default + per-member override Most read the shared param; a few pin their own A couple of rules must always Deny
Grouped effects Two params (e.g. auditEffect, denyEffect) Two tiers of severity in one set
No shared param Each member’s effect fixed in the set The posture truly never varies

How initiative compliance rolls up

Compliance is where the payoff and the surprises both live. When an initiative is assigned, the policy engine evaluates each member independently against every in-scope resource, then aggregates: a resource is compliant for the set only when it passes every applicable member — one failing member makes it non-compliant for the whole set — and the set’s overall percentage is the share of resource-evaluations that pass across all members. So one chatty Audit rule flagging 300 resources drags the percentage down even when every other rule is green. The blade lets you drill from the set into each member to see which rule is responsible; always drill before reacting to the headline figure.

Two things routinely confuse people:

Surprise What’s really happening What to do
Set shows low % right after assignment Members include Audit rules flagging existing drift Drill into members; fix or exempt the noisy ones
Number looks “stale” / didn’t move after a fix Compliance is evaluated on a scan (~24 h) or on change Trigger an on-demand scan; don’t trust the cached tile
NotStarted / blank state Evaluation hasn’t run yet for new resources/assignment Wait for the scan or trigger one
A member shows Compliant with 0 resources No in-scope resource matches that rule’s type Expected; it’s not an error

The states you will see on the dashboard, and what each means for a set:

State Meaning for the initiative Typical cause
Compliant Every applicable member passed for this resource Resource meets the whole baseline
Non-compliant At least one member failed One rule flagged it; drill to find which
Conflicting Two members give opposing results Overlapping rules; reconcile the set
Exempt An exemption suppresses the result Documented, time-boxed waiver
NotStarted Evaluation pending New assignment/resource; scan not yet run

Built-in initiatives — don’t hand-build a framework

Before you author anything, look at what Microsoft already ships: a large catalog of built-in initiatives that map your resources to recognised standards, each maintained as the standard evolves — so you inherit the mapping work instead of doing it by hand. The ones you’ll reach for:

Built-in initiative Maps to Typical use
Microsoft Cloud Security Benchmark (MCSB) Microsoft’s own baseline (Defender default) Default security posture; assigned by Defender for Cloud
CIS Microsoft Azure Foundations Benchmark CIS hardening guide A widely-recognised security baseline
PCI-DSS Payment-card standard Cardholder-data workloads
NIST SP 800-53 / 800-171 US federal controls Gov / regulated workloads
ISO/IEC 27001 International infosec standard Enterprise certification
Azure Security Benchmark (legacy) Predecessor to MCSB Older estates; migrate to MCSB

Two truths about these. First, most built-in regulatory initiatives are Audit-heavy by design — they tell you where you stand against a standard, they don’t block deployments (enforcing a whole standard with Deny everywhere would break a real estate instantly). You layer your own Deny rules on top for the controls you truly want enforced. Second, they drive Defender for Cloud’s regulatory-compliance dashboard, control by control. Treat built-ins as your measurement layer and custom initiatives as your enforcement layer. Find them from the CLI:

# List built-in initiatives (policy SET definitions); filter to a standard you care about
az policy set-definition list \
  --query "[?policyType=='BuiltIn' && contains(displayName,'CIS')].{name:name, display:displayName}" \
  -o table

Assigning a built-in is a one-liner — point an assignment at the set and supply any required parameters (often a Log Analytics workspace for the audit-if-not-exists members):

az policy assignment create \
  --name "cis-baseline-mg" \
  --policy-set-definition "<built-in-initiative-name-from-list>" \
  --scope "/providers/Microsoft.Management/managementGroups/<mg-id>" \
  --display-name "CIS Azure Foundations - audit"

Authoring a custom initiative — az CLI and Bicep

When no built-in fits — your own tagging standard, your storage baseline — you author a custom policy set definition: a JSON array of policy references (each pointing at a definition by ID, with a policyDefinitionReferenceId and optional pinned parameters), plus a parameters block for set-level knobs like effect.

First, the definitions file the set references — an array of { policyDefinitionId, policyDefinitionReferenceId, parameters }, each reference id unique within the set:

[
  {
    "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/<deny-public-storage-id>",
    "policyDefinitionReferenceId": "denyPublicStorage",
    "parameters": { "effect": { "value": "Deny" } }
  },
  {
    "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/<require-https-id>",
    "policyDefinitionReferenceId": "requireHttpsOnly",
    "parameters": { "effect": { "value": "[parameters('effect')]" } }
  },
  {
    "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/<min-tls-id>",
    "policyDefinitionReferenceId": "minTls12",
    "parameters": { "effect": { "value": "[parameters('effect')]" } }
  }
]

Note the trick in action: requireHttpsOnly and minTls12 read the shared [parameters('effect')], while denyPublicStorage is pinned to Deny. Create the set, exposing the shared effect parameter, scoped to a management group so any child can use it:

az policy set-definition create \
  --name "storage-baseline" \
  --display-name "Storage Hardening Baseline" \
  --description "Bundled storage controls: public access, HTTPS, TLS 1.2" \
  --definitions @storage-baseline.definitions.json \
  --params '{ "effect": { "type": "String", "allowedValues": ["Audit","Deny","Disabled"], "defaultValue": "Audit" } }' \
  --management-group "<mg-id>"

Then assign it — Audit for the dev management group, Deny for prod — from the same set definition:

# Dev: measure only
az policy assignment create --name "storage-baseline-dev" \
  --policy-set-definition "storage-baseline" \
  --scope "/providers/Microsoft.Management/managementGroups/mg-dev" \
  --params '{ "effect": { "value": "Audit" } }'

# Prod: enforce
az policy assignment create --name "storage-baseline-prod" \
  --policy-set-definition "storage-baseline" \
  --scope "/providers/Microsoft.Management/managementGroups/mg-prod" \
  --params '{ "effect": { "value": "Deny" } }'

The same set in Bicep, which is how you should actually ship it (policy as code, reviewed, version-controlled):

// A custom initiative (policy SET) with one shared effect knob plus a pinned member.
resource storageBaseline 'Microsoft.Authorization/policySetDefinitions@2023-04-01' = {
  name: 'storage-baseline'
  properties: {
    displayName: 'Storage Hardening Baseline'
    policyType: 'Custom'
    description: 'Bundled storage controls assigned as one unit'
    parameters: {
      effect: {
        type: 'String'
        allowedValues: [ 'Audit', 'Deny', 'Disabled' ]
        defaultValue: 'Audit'
        metadata: { displayName: 'Effect for soft rules' }
      }
    }
    policyDefinitions: [
      {
        policyDefinitionReferenceId: 'denyPublicStorage'
        policyDefinitionId: denyPublicStorageId   // pinned hard control
        parameters: { effect: { value: 'Deny' } }
      }
      {
        policyDefinitionReferenceId: 'requireHttpsOnly'
        policyDefinitionId: requireHttpsId
        parameters: { effect: { value: '[parameters(\'effect\')]' } }
      }
      {
        policyDefinitionReferenceId: 'minTls12'
        policyDefinitionId: minTlsId
        parameters: { effect: { value: '[parameters(\'effect\')]' } }
      }
    ]
  }
}

To assign that set as Bicep, point a Microsoft.Authorization/policyAssignments resource at storageBaseline.id and pass parameters: { effect: { value: { value: 'Deny' } } } (note the nested value — an easy gotcha) plus enforcementMode: 'Default' ('DoNotEnforce' to stage).

A note that saves an hour: if any member uses Modify or DeployIfNotExists, the assignment needs a managed identity and the right RBAC roles — exactly as a standalone DINE/Modify assignment would, and bundling doesn’t remove that. Add identity: { type: 'SystemAssigned' } and a location to the assignment, then grant the union of every member’s roleDefinitionIds, or remediation fails Forbidden.

Scope, inheritance and rollout for a whole set

Because an initiative is the unit you assign, where you assign it matters more than for a single policy — one assignment now carries dozens of rules. The rule of thumb: assign baselines as high as they are universally true. A control every workload must obey belongs at a top management group; one specific to a business unit belongs on that unit’s MG; a one-off belongs on a subscription. Assign high and inheritance does the rest — every child subscription gets the whole baseline with no per-subscription work.

Scope Assign a set here when… Watch out for
Tenant root MG The baseline is truly universal Needs elevated rights; blast radius is everything
Mid-level MG (e.g. mg-prod) A whole environment/BU shares the baseline The natural home for most baselines
Subscription One subscription needs an extra set Drift risk if you copy it per-sub
Resource group A short-lived or exceptional case Easy to forget on teardown

Rolling out a large set safely is the same Audit-first discipline, with one extra lever. Use enforcement mode DoNotEnforce to assign the whole set without firing any effects — every member evaluates and reports, nothing is blocked — so you see the full blast radius of a fifty-control set before a single deny fires. Once the dashboard is clean (or exemptions cover the legitimate exceptions), flip the assignment to Deny and enforcement mode Default. Never lead a big set with Deny in Default mode.

Architecture at a glance

Follow one storage account’s create request left to right. A platform engineer authors several standalone policy definitions — deny public storage, require HTTPS, require TLS 1.2 — then bundles the references into one initiative (policy set) exposing a shared effect parameter and makes exactly one assignment at the prod management group, which inheritance carries down to every child subscription. Now a developer’s pipeline sends a create to Azure Resource Manager (ARM). ARM consults the policy engine, which gathers every member of every assigned initiative inherited from above and evaluates them together — the initiative is invisible here; ARM simply sees its constituent rules.

If the bundled Deny member matches (public access on), ARM rejects the request with RequestDisallowedByPolicy; the Audit-level members instead let it through and record the result. The outcome of all members rolls up into the compliance store as a single initiative percentage on the Defender for Cloud / Policy compliance dashboard — one “92% against this baseline” number, drillable to the rule that failed. Trace it from atoms on the left, through the molecule bundled and assigned once, to the ARM gate, to the rolled-up number on the right — and you see why the initiative is a wrapper that changes operations, not enforcement.

Left-to-right Azure Policy initiative architecture: standalone policy definitions (deny public storage, require HTTPS, require TLS 1.2) bundled into one initiative with a shared effect parameter, assigned once at the prod management group and inherited down to child subscriptions; a developer create request reaches Azure Resource Manager, whose policy engine evaluates every bundled member and denies the request when the Deny member matches; results roll up into a compliance dashboard as one initiative percentage. Numbered badges mark the bundling step, the single MG assignment, the ARM deny gate, the shared effect knob, and the compliance roll-up.

Real-world scenario

Meridian Logistics runs about 25 subscriptions under three management groups — mg-platform, mg-prod, mg-nonprod. Over a year the platform team accumulated 38 standalone policy assignments, and three things had quietly gone wrong: a subscription onboarded in Q2 had somehow never received the “deny public storage” policy (unnoticed for two months); a SOC 2 audit asked “what’s your compliance against CIS?” and the team spent two days hand-correlating 38 loose policies; and every new control meant an engineer assigning it to the right subscriptions and inevitably missing one. The rules were fine. The operating model was the problem.

They consolidated in three moves. First, they grouped their custom rules into three themed initiatives — a Storage Hardening Baseline, a Tagging Baseline (CostCenter, Environment, Owner), and a Network Baseline — each with one shared effect parameter. They resisted a single giant “Meridian Standard” set so the storage and tagging numbers stayed separately readable and the baselines kept their different owners and cadences.

Second, they assigned each baseline once per management group: mg-nonprod got all three with effect=Audit, mg-prod with effect=Deny. Twenty-five subscriptions, six assignments total, down from 38 — and the “missed a subscription” gap became structurally impossible, because new subscriptions inherit the baseline the instant they join the MG. The first prod rollout ran in DoNotEnforce for a week; the dashboard surfaced 22 non-compliant resources; they fixed 19, exempted 3, then flipped to Default. Zero broken pipelines.

Third, for the CIS audit question they assigned the built-in CIS Microsoft Azure Foundations Benchmark at mg-platform in Audit mode — now Defender for Cloud shows a live “CIS: 89% compliant” number the auditor reads directly. One snag: their first custom set with a Modify tagging rule failed remediation Forbidden — bundling hadn’t removed the need for a managed identity and a Tag Contributor role at MG scope. They added both, reran remediation, and thousands of resources got their CostCenter. The lesson: bundle by theme, assign high, stage before you enforce, and remember a set inherits its members’ identity needs.

Advantages and disadvantages

Bundling is a trade, not a free win. The two-column view:

Advantages Disadvantages
Standalone definition Simplest possible unit; independent lifecycle; trivial to reason about; no extra authoring layer O(rules × scopes) assignments; no roll-up number; no framework mapping; drift-prone at scale
Initiative (policy set) One assignment per scope; one compliance number; framework mapping; shared effect knob; add a rule once, applies everywhere Extra authoring layer; couples members’ change cadence; referenceId collisions; inherits all members’ identity/RBAC needs; a too-big set’s number is meaningless

When each matters: the crossover is roughly “three related rules you’ll assign to more than one scope.” Below it, a standalone rule is simpler; above it, the assignment-count and roll-up wins compound fast — and you should still prefer several focused initiatives over one monolith.

Hands-on lab

This lab is free — definitions, set definitions, assignments and evaluation cost nothing. You’ll author a two-rule initiative with a shared effect parameter, assign it as Audit to a resource group, see a noncompliant resource flagged, then flip the set to Deny and watch the next deployment get blocked. (Real life assigns at a management group; a resource group keeps the lab self-contained.)

1. Set up a sandbox resource group.

az group create --name rg-initiative-lab --location eastus

2. Find two built-in storage definitions whose effect is parameterised. “Storage accounts should disable public network access” and “Secure transfer to storage accounts should be enabled” both expose an effect parameter.

az policy definition list \
  --query "[?policyType=='BuiltIn' && (contains(displayName,'public network access') || contains(displayName,'Secure transfer'))].{name:name, display:displayName}" \
  -o table

3. Build the initiative’s definitions file. Capture the two name values from step 2 into the IDs below; both members read the shared [parameters('effect')].

cat > initiative-defs.json <<'JSON'
[
  {
    "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/<public-access-def-name>",
    "policyDefinitionReferenceId": "denyPublicStorage",
    "parameters": { "effect": { "value": "[parameters('effect')]" } }
  },
  {
    "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/<secure-transfer-def-name>",
    "policyDefinitionReferenceId": "requireHttps",
    "parameters": { "effect": { "value": "[parameters('effect')]" } }
  }
]
JSON

4. Create the policy set definition with a shared effect parameter.

az policy set-definition create \
  --name "lab-storage-baseline" \
  --display-name "Lab Storage Baseline" \
  --definitions @initiative-defs.json \
  --params '{ "effect": { "type": "String", "allowedValues": ["Audit","Deny","Disabled"], "defaultValue": "Audit" } }'

5. Assign the set as Audit at the resource group.

az policy assignment create \
  --name "lab-baseline-audit" \
  --policy-set-definition "lab-storage-baseline" \
  --scope "$(az group show -n rg-initiative-lab --query id -o tsv)" \
  --params '{ "effect": { "value": "Audit" } }'

6. Create a deliberately noncompliant storage account. It should succeed (Audit never blocks).

az storage account create \
  --name "stinitlab$RANDOM" \
  --resource-group rg-initiative-lab \
  --sku Standard_LRS \
  --public-network-access Enabled

7. Trigger an on-demand scan and read the initiative compliance. Don’t trust the cached tile.

az policy state trigger-scan --resource-group rg-initiative-lab
# after it completes — note the set name appears in the results:
az policy state list \
  --resource-group rg-initiative-lab \
  --query "[].{res:resourceId, set:policySetDefinitionName, state:complianceState}" -o table

You should see the storage account as NonCompliant under your lab-storage-baseline set — flagged by the bundle, not by a loose rule.

8. Flip the whole baseline to Deny. One parameter change drives both members at once.

az policy assignment update \
  --name "lab-baseline-audit" \
  --scope "$(az group show -n rg-initiative-lab --query id -o tsv)" \
  --params '{ "effect": { "value": "Deny" } }'

9. Try another public storage account. Now it should fail with RequestDisallowedByPolicy, naming the initiative.

az storage account create \
  --name "stinitlab$RANDOM" \
  --resource-group rg-initiative-lab \
  --sku Standard_LRS \
  --public-network-access Enabled
# Expect: (RequestDisallowedByPolicy) ... disallowed by policy ... lab-storage-baseline

10. Tear down.

az policy assignment delete --name "lab-baseline-audit" \
  --scope "$(az group show -n rg-initiative-lab --query id -o tsv)"
az policy set-definition delete --name "lab-storage-baseline"
az group delete --name rg-initiative-lab --yes --no-wait

That is the whole operating model in ten steps: author, bundle with a shared knob, assign once, watch Audit flag and Deny block.

Common mistakes & troubleshooting

The failures here are almost all about the wrapper — the references, the shared parameter, the identity — not the rules inside. Match your symptom:

# Symptom Root cause How to confirm Fix
1 Set fails to create / save Duplicate policyDefinitionReferenceId Two members share a reference id in the JSON Make every policyDefinitionReferenceId unique
2 Flipping the assignment’s effect does nothing Members hard-pinned their own effect, not the shared param Inspect members: do they read [parameters('effect')]? Wire members to the shared effect parameter
3 Remediation tasks fail Forbidden A Modify/DINE member needs an identity + RBAC the assignment lacks Remediation detail → Forbidden; assignment has no identity Add system-assigned identity + grant members’ roleDefinitionIds
4 Initiative compliance looks terrible immediately Audit members flag pre-existing drift Drill set → member view; find the noisy rule Fix/exempt those resources; stage with DoNotEnforce
5 Number “stale” after a fix Compliance is scan-based (~24 h) or change-driven Tile timestamp is old az policy state trigger-scan
6 A new subscription isn’t governed Set assigned per-subscription, not at the MG Assignment scope is a sub, not the MG Assign the set at the management group
7 Big set broke pipelines on rollout Assigned Deny in Default mode with no Audit phase Pipelines fail RequestDisallowedByPolicy tenant-wide Re-stage: DoNotEnforce/Audit → fix → enforce
8 Member parameter “missing value” error A member needs a param the set neither pins nor exposes Assignment error names the parameter Pin it in the reference or surface it on the set
9 Can’t assign the built-in regulatory set as Deny Most regulatory built-ins are Audit-only by design Members are auditIfNotExists, not deny Use it to measure; add your own Deny rules to enforce
10 Conflicting compliance state Two members give opposing verdicts on a resource State shows Conflicting Reconcile overlapping rules; remove the duplicate

The meta-lesson: when “the initiative is broken,” check the three wrapper things first — reference ids unique, effect parameter wired, identity present — before you ever suspect the rules themselves.

Best practices

Security notes

Initiatives are a governance control, so the security considerations are about the wrapper and its reach. First, least privilege on the assignment identity: an initiative with Modify/DeployIfNotExists members runs remediation as a managed identity, and you must grant it exactly the union of its members’ roleDefinitionIds at the narrowest scope that works — no more. Bundling tempts a broad role “to be safe”; resist it, because one over-privileged remediation identity at a management group is a real blast radius. Use a managed identity and RBAC, never a service-principal secret.

Second, who can author and assign is RBAC — Resource Policy Contributor (or Owner) at the scope. Keep set authoring with the platform/security team so coherent baselines aren’t undercut by ad-hoc local sets. Third, don’t mistake Audit for enforcement: a regulatory built-in shows green/red but blocks nothing — security-critical controls (public data endpoints, unencrypted disks, weak TLS) belong in a Deny member you actually enforce. Finally, protect the baseline from accidental removal: a deleted assignment silently un-governs everything below it, so pair policy with resource locks and reviewed IaC. (For the remediation identity, Key Vault and managed identities are the right pattern.)

Cost & sizing

The reassuring headline: Azure Policy — definitions, initiatives, assignments and compliance evaluation — is free. No charge per policy, per initiative, per assignment, or per evaluation; no SKU, no per-scan fee. “Sizing” an initiative is about operational scale, not money.

What carries a cost is what your policies cause. A DeployIfNotExists member that pushes a diagnostic setting to a Log Analytics workspace drives ingestion and retention charges there — a single DINE rule in a widely-assigned initiative can meaningfully move your Monitor bill, and one that enables a paid feature (a backup, a Defender plan) bills for it across every resource it touches. Right-size the consequences, not the policy.

Cost driver Free? What actually bills Control
Policy/initiative definitions Free Nothing
Assignments & evaluation Free Nothing
Compliance scans Free Nothing
DINE → diagnostic settings Indirect Log Analytics ingestion + retention Scope the workspace; set retention; filter logs
DINE → enables a paid feature Indirect That feature (backup, Defender plan) Assign only where you want the feature on
Modify → tags/properties Free Nothing
Remediation tasks Free Whatever the remediation deploys Same as DINE consequences

Operational sizing: you’ll hit “too broad to reason about” long before any platform limit on members — keep sets focused. And because evaluation is free, there’s no cost reason to under-assign. The only bill an initiative produces is for the resources its remediation creates — watch that, not the policy.

Interview & exam questions

These map to AZ-104 (assign policies and initiatives), AZ-305 (governance design), and AZ-500 (regulatory compliance, Defender for Cloud).

  1. Difference between a policy definition and an initiative? A definition is a single rule — one if condition, one effect. An initiative (policy set) is a named collection of definitions assigned and tracked as one unit; it adds no enforcement power, only operational leverage (one assignment, one compliance number, framework mapping).

  2. Why bundle instead of assigning separately? Fewer assignments (one per scope, not one per rule per scope), a single rolled-up percentage, framework mapping, and a shared effect parameter so the whole baseline flips Audit→Deny at once. Adding a control becomes one edit, not N assignments.

  3. How does compliance roll up? Each member is evaluated independently; a resource is compliant for the set only if it passes every applicable member, and the percentage aggregates across all members and resources. One noisy Audit member drags the headline down — always drill into the member view.

  4. What is policyDefinitionReferenceId? A member’s local name inside the set, unique within it. The set won’t save if two share one, and you read these ids in compliance drill-downs — keep them unique and descriptive.

  5. Audit in dev, Deny in prod without forking? Declare a shared effect parameter, wire members to [parameters('effect')], and supply the value per assignment — Audit for dev, Deny for prod. One definition, two postures.

  6. When should a rule stay standalone? When it’s a genuine one-off, its lifecycle/owner differs from everything else, or it’s still being prototyped. A set of one is pure overhead.

  7. Extra wiring if a member is DINE/Modify? The assignment needs a managed identity and the union of those members’ roleDefinitionIds at an appropriate scope — exactly as a standalone DINE/Modify assignment would. Bundling doesn’t remove it.

  8. How do built-in regulatory initiatives relate to enforcement? They’re mostly Audit/auditIfNotExists by design — they measure against a standard and feed Defender for Cloud’s dashboard. To enforce a control, add your own Deny rule alongside.

  9. How do you safely roll out a fifty-control initiative? Assign with DoNotEnforce (or effect=Audit) so members report without firing, review the blast radius, fix or exempt the gaps, then flip to Default/Deny. Never lead with Deny in Default mode.

  10. At what scope should a baseline go, and why? As high as it’s universally true — typically a management group — so inheritance carries it to every child subscription, including ones onboarded later. Per-subscription assignment re-introduces the drift initiatives remove.

  11. Why might compliance look wrong right after assignment? Either Audit members are flagging real pre-existing drift (drill in), or the result is stale because evaluation runs on a ~24-hour scan or on change — trigger an on-demand scan.

  12. The risk of one giant “everything” initiative? Its compliance number becomes meaningless, members with different cadences get coupled, and it’s hard to reason about. Prefer a few focused, themed baselines.

Quick check

  1. In one sentence, how much enforcement power does an initiative add beyond the policies inside it?
  2. You need one set to merely audit in dev but deny in prod. What feature makes that possible without copying the set?
  3. A resource passes four of five members of an initiative. What is its compliance state for the set, and why?
  4. You added a 61st control to a baseline already assigned at five management groups. How many new assignments must you create?
  5. Your initiative includes a Modify member and remediation fails Forbidden. What did you forget?

Answers

  1. None — an initiative has exactly the power of its members; it adds leverage (one assignment, one number, framework mapping), not new enforcement.
  2. A shared effect parameter read by members via [parameters('effect')]; supply Audit on the dev assignment, Deny on prod.
  3. Non-compliant — a resource is compliant for the set only if it passes every applicable member; one failure makes it non-compliant (drill in to find which).
  4. Zero — editing the set definition applies the control everywhere the set is already assigned; that’s the core scale win.
  5. A managed identity on the assignment plus the member’s RBAC role(s) (the union of members’ roleDefinitionIds) at an appropriate scope — bundling doesn’t remove the identity requirement.

Glossary

Next steps

AzureAzure PolicyInitiativesPolicy SetGovernanceComplianceManagement GroupsRegulatory Compliance
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading