Azure Landing Zone Governance: Policy at Scale
Every Landing Zone project I have been part of follows the same arc. The first month is all architecture -- management groups, subscriptions, networking. The second month is deployment automation and CI/CD. By month three, 40 engineers are provisioning resources and nobody remembers the architecture decisions from month one. By month six, someone has deployed a storage account with public blob access in production, three VMs have public IPs for "temporary debugging," and the cost dashboard shows $12,000/month in resources nobody can identify.
Governance is the part everyone skips because it feels like bureaucracy. It is not. It is the automation layer that enforces your architecture decisions after you stop paying attention.
Effort: 2-3 days for policy framework, ongoing 2hrs/week for compliance review Cost: $0 (Azure Policy is free; Defender for Cloud CSPM adds ~$15/server/month if desired) Prerequisites: Completed Landing Zone foundation (Part 1) and networking (Part 2), Management Group hierarchy, Global Administrator or Policy Contributor role
What Changed in 2025
The governance toolchain got a significant overhaul this year.
Azure Blueprints retirement was announced. Microsoft formally deprecated Blueprints and announced end-of-life for 2026. The replacement is Azure Policy combined with Bicep or Terraform for deployment -- exactly what this post covers. If you are still running Blueprints, the migration path is to export your blueprint artifacts as ARM/Bicep templates and convert policy assignments to standalone Azure Policy resources.
Azure Policy exemptions went GA. Before this, handling policy exceptions was miserable. You either had exclusion scopes (which applied to everything in a resource group) or you disabled the policy entirely. Now you can create time-bound exemptions on individual resources with a justification field that shows up in compliance reports. This matters for production environments where you occasionally need to break the rules with an audit trail.
Azure Verified Modules (AVM) replaced the CAF enterprise-scale module. The old terraform-azurerm-caf-enterprise-scale module is in extended support and will be archived in August 2026. AVM modules are smaller, composable, and maintained by Microsoft engineering teams rather than the CAF community. If you are starting a new Landing Zone, use AVM. If you are maintaining an existing one, plan the migration.
Regulatory Compliance dashboard improvements. The built-in compliance dashboard now supports custom regulatory frameworks alongside the standard ones (PCI DSS, HIPAA, ISO 27001). You can map your internal security baseline to policy definitions and track compliance percentage per management group.
Why This Matters
Without policy guardrails, entropy wins. Here is a real timeline from a client engagement with approximately 200 Azure resources across three subscriptions:
Month 1-2: Everything is deployed via Bicep pipelines. Architecture is clean. Month 3: A developer creates a storage account via the portal "just to test something." Public blob access is enabled. Month 4: Three more portal-provisioned resources appear. No tags. No naming convention. Month 6: Cost alert fires at $18,000/month -- $6,000 over budget. Investigation reveals 14 resources with no owner tag, two ExpressRoute circuits nobody remembers creating, and a VM running a Docker container that "someone needed for a demo."
The fix was not a policy document. The fix was Azure Policy enforcing:
- Deny public IP addresses on VMs unless explicitly exempted
- Deny storage accounts without HTTPS-only enforcement
- Audit resources without required tags (CostCenter, Owner, Environment)
- DeployIfNotExists to auto-enable diagnostic settings on every new resource
After policy enforcement, the next compliance scan showed the non-compliant resource count drop from 47 to 3 (all with documented exemptions).
Implementation: Policy Framework with Bicep
The framework has four components: custom policy definitions, a policy initiative (grouping related policies), assignment at Management Group scope, and a compliance dashboard query.
Custom Policy Definition: Deny Public IP on VMs
// modules/governance/policy-deny-public-ip.bicep
// Custom policy: deny public IP attachment to VM network interfaces
targetScope = 'managementGroup'
param managementGroupId string
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {
name: 'policy-deny-public-ip-vm'
properties: {
displayName: 'Deny public IP addresses on virtual machines'
description: 'Prevents attaching public IP addresses to VM network interfaces. Exemptions available for jump boxes and bastion alternatives.'
policyType: 'Custom'
mode: 'All'
metadata: {
category: 'Network'
version: '1.0.0'
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Network/networkInterfaces'
}
{
field: 'Microsoft.Network/networkInterfaces/ipConfigurations[*].publicIpAddress.id'
notEquals: ''
}
]
}
then: {
effect: 'Deny'
}
}
}
}
output policyDefinitionId string = policyDefinition.idPolicy Initiative: Landing Zone Security Baseline
// modules/governance/initiative-security-baseline.bicep
// Group related security policies into a single assignable initiative
targetScope = 'managementGroup'
param denyPublicIpPolicyId string
param managementGroupId string
resource initiative 'Microsoft.Authorization/policySetDefinitions@2023-04-01' = {
name: 'initiative-landing-zone-security-baseline'
properties: {
displayName: 'Landing Zone Security Baseline'
description: 'Core security policies for all Landing Zone subscriptions. Covers network isolation, encryption, and tagging.'
policyType: 'Custom'
metadata: {
category: 'Security'
version: '1.0.0'
}
policyDefinitions: [
{
policyDefinitionId: denyPublicIpPolicyId
policyDefinitionReferenceId: 'denyPublicIpOnVm'
parameters: {}
}
{
// Built-in: Storage accounts should use HTTPS
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/404c3081-a854-4457-ae30-26a93ef643f9'
policyDefinitionReferenceId: 'storageHttpsOnly'
parameters: {}
}
{
// Built-in: Require a tag on resources
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/871b6d14-10aa-478d-b466-ef391e9b22ef'
policyDefinitionReferenceId: 'requireOwnerTag'
parameters: {
tagName: {
value: 'Owner'
}
}
}
{
// Built-in: Require a tag on resources
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/871b6d14-10aa-478d-b466-ef391e9b22ef'
policyDefinitionReferenceId: 'requireCostCenterTag'
parameters: {
tagName: {
value: 'CostCenter'
}
}
}
]
}
}
output initiativeId string = initiative.idAssignment at Management Group Scope
// modules/governance/assign-initiative.bicep
// Assign the security baseline initiative to the Landing Zones management group
targetScope = 'managementGroup'
param initiativeId string
param managementGroupName string
resource assignment 'Microsoft.Authorization/policyAssignments@2023-04-01' = {
name: 'assign-lz-security-baseline'
properties: {
displayName: 'Landing Zone Security Baseline Assignment'
description: 'Enforces core security policies across all Landing Zone subscriptions'
policyDefinitionId: initiativeId
enforcementMode: 'Default'
nonComplianceMessages: [
{
message: 'This resource violates the Landing Zone security baseline. Contact the platform team for exemptions: platform-team@contoso.com'
}
{
message: 'Public IP addresses are not allowed on virtual machines. Use Azure Bastion or a jump box with an exemption.'
policyDefinitionReferenceId: 'denyPublicIpOnVm'
}
]
}
}
output assignmentId string = assignment.idThe nonComplianceMessages array is something most people skip. When a developer's deployment gets denied by policy, they see a generic "RequestDisallowedByPolicy" error. With custom messages, they see exactly which policy blocked them and who to contact. This single addition cut our platform team's support tickets by roughly 40%.
Compliance Dashboard Query (Azure Resource Graph)
Once policies are assigned, you need visibility. This Azure Resource Graph query gives you a compliance summary per management group:
PolicyResources
| where type == 'microsoft.policyinsights/policystates'
| where properties.complianceState != 'Compliant'
| extend
resourceId = tostring(properties.resourceId),
policyName = tostring(properties.policyDefinitionName),
complianceState = tostring(properties.complianceState),
timestamp = todatetime(properties.timestamp)
| summarize
nonCompliantCount = count(),
latestEvaluation = max(timestamp)
by policyName
| order by nonCompliantCount descRun this in Azure Resource Graph Explorer or schedule it via Azure Monitor workbooks to get a weekly compliance email.
Real-World Results
After rolling out this policy framework across a client's Landing Zone with three management groups (Platform, Landing Zones, Sandbox), here is the compliance summary after 30 days:
$ az policy state summarize \
--management-group "mg-landing-zones" \
--query 'results.{total:resourceDetails[0].count, compliant:resourceDetails[1].count, nonCompliant:resourceDetails[2].count}' \
--output table
Total Compliant NonCompliant
------- ----------- --------------
342 339 3
Those three non-compliant resources were all covered by time-bound exemptions: a staging VM with a public IP for a vendor integration test (exemption expires in 14 days), a storage account that needed HTTP for legacy application compatibility (migration ticket filed), and a resource group missing the CostCenter tag pending finance team's cost allocation decision.
The most impactful policy was the tagging requirement. Before enforcement, only 60% of resources had Owner and CostCenter tags. After one month of "Audit" mode followed by switching to "Deny," compliance hit 99.1%. The remaining 0.9% had documented exemptions. When the CFO asked "who owns the $4,200 VM in West Europe," we had the answer in 30 seconds instead of 3 days.
A real remediation event that demonstrated the value of DeployIfNotExists: a developer created a Key Vault via the portal without enabling diagnostic logging. The policy auto-remediated within 15 minutes, creating a diagnostic setting that forwarded Key Vault audit logs to the central Log Analytics workspace. The developer never even knew -- which is exactly the point.
Key Takeaways
- Start with "Audit" mode, switch to "Deny" after 30 days. Deploying new policies in Deny mode immediately will break existing deployments and make you unpopular. Audit first, communicate the timeline, then enforce.
- Use policy initiatives, not individual assignments. Assigning 20 policies individually at the management group level creates an unmanageable list. Group them into 3-5 initiatives by domain (security, networking, tagging, cost).
- Add custom non-compliance messages to every assignment. The default "RequestDisallowedByPolicy" error tells developers nothing. A message with the policy name, rationale, and contact information reduces friction dramatically.
- Migrate off Azure Blueprints now. With the 2026 retirement announced, waiting increases the migration effort. Export blueprint artifacts and convert to Bicep + Policy assignments.
- Azure Policy is free. There is no cost barrier to governance. The only investment is the time to define and test your policies.
This wraps up the Azure Landing Zone Enterprise Series. With the foundation from Part 1, hub-spoke networking from Part 2, and this governance layer, you have an enterprise-grade Landing Zone running on three pillars: structure, connectivity, and compliance. If your organization needs help designing or implementing any of these layers, check out our cloud architecture consulting services.
About the author

Martin Rylko
Senior Cloud Architect & DevOps Engineer
14+ years in IT – from on-premises datacenters and Hyper-V clustering to cloud infrastructure on Microsoft Azure. I specialize in Landing Zones, IaC automation, Kubernetes and security compliance.
Frequently Asked Questions
Should I start with Audit or Deny mode when rolling out Azure Policies?▾
When should I write custom policies vs using Azure built-in policies?▾
How do I handle policy exemptions without losing governance control?▾
What is the difference between Azure Policy initiatives and individual policy assignments?▾
You might also like
NIS2 Azure Compliance: Checklist for Architects
NIS2 Azure compliance checklist with concrete steps: Azure Policy governance, Defender for Cloud CSPM, centralized logging, and Zero Trust identity.
ReadMicrosoft Defender for Cloud: CSPM Setup Guide
Configure Microsoft Defender for Cloud CSPM for Azure Landing Zones. Secure Score optimization, attack path analysis, regulatory compliance dashboards, and real cost breakdown.
ReadAzure Landing Zone Networking: Hub-Spoke with Firewall
Deploy hub-spoke network topology for Azure Landing Zone with Azure Firewall, Private DNS Zones, and VNet peering automation using Bicep modules.
Read