[{"content":"\u003cp\u003eMost Azure environments fail because nobody built a foundation first. One team creates a resource group called \u003ccode\u003etest-rg\u003c/code\u003e that becomes production 18 months later. Another ships without tags and cannot attribute 40% of the monthly bill. These aren\u0026rsquo;t isolated mistakes — they are the predictable outcome of deploying workloads before designing the platform that hosts them.\u003c/p\u003e\n\u003cp\u003eAn Azure Landing Zone establishes a governed, scalable platform \u003cem\u003ebefore\u003c/em\u003e workloads arrive. This guide is the pillar of a 10-part series. It covers the architecture and the decisions required to build an enterprise-grade foundation from scratch.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eImplement the eight CAF design areas.\u003c/li\u003e\n\u003cli\u003eUnderstand the separation between platform and application landing zones.\u003c/li\u003e\n\u003cli\u003eChoose between Terraform AVM, Bicep AVM, and the ALZ Accelerator.\u003c/li\u003e\n\u003cli\u003eDeploy a production scaffold using modern IaC patterns.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"what-is-an-azure-landing-zone\"\u003eWhat Is an Azure Landing Zone?\u003c/h2\u003e\n\u003cp\u003eA landing zone is not a product — it is a set of design decisions, encoded in IaC and policy, applied before workloads arrive. It ensures every team inherits the same baseline: hub networking with centralized egress, identity constraints, mandatory tagging with budget alerts, and centralized logging.\u003c/p\u003e\n\u003cp\u003eThe separation is explicit:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePlatform Landing Zones\u003c/strong\u003e: Shared services (Hub VNet, Firewall, Bastion, DNS, Logging) managed by the platform team.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eApplication Landing Zones\u003c/strong\u003e: Spoke subscriptions where product teams deploy application resources.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre class=\"mermaid\"\u003e\n  graph TD\n    subgraph Platform_Landing_Zone [Platform Landing Zone]\n        subgraph Shared_Services [Shared Services]\n            Networking[Connectivity: Hub VNet, FW, Bastion]\n            Identity[Identity: Entra ID, PIM]\n            Management[Management: Log Analytics, Sentinel]\n        end\n    end\n\n    subgraph Application_Landing_Zones [Application Landing Zones]\n        Spoke_A[Spoke A: HR Application]\n        Spoke_B[Spoke B: Payroll System]\n        Spoke_C[Spoke C: Customer API]\n    end\n\n    Networking --- Peering_A[VNet Peering] --- Spoke_A\n    Networking --- Peering_B[VNet Peering] --- Spoke_B\n    Networking --- Peering_C[VNet Peering] --- Spoke_C\n\n    Spoke_A -.-\u0026gt; Management\n    Spoke_B -.-\u0026gt; Management\n    Spoke_C -.-\u0026gt; Management\n\n    classDef platform fill:#f9f,stroke:#333,stroke-width:2px;\n    classDef app fill:#bbf,stroke:#333,stroke-width:2px;\n    class Networking,Identity,Management platform;\n    class Spoke_A,Spoke_B,Spoke_C app;\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eNotes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe \u003cstrong\u003ePlatform Landing Zone\u003c/strong\u003e provides shared infrastructure that is inherited by all applications.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVNet Peering\u003c/strong\u003e establishes the private network path for traffic.\u003c/li\u003e\n\u003cli\u003eDashed lines represent \u003cstrong\u003eTelemetry/Logging\u003c/strong\u003e paths back to the central management hub.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"the-eight-caf-design-areas\"\u003eThe Eight CAF Design Areas\u003c/h2\u003e\n\u003cp\u003eEach area addresses a specific failure mode. Skipping one creates a technical debt that is expensive to remediate.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eBilling and Entra Tenant\u003c/strong\u003e: Determining APIs for subscription vending.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIdentity and Access\u003c/strong\u003e: Implementing OIDC secret-less pipelines and PIM.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eManagement Group Organization\u003c/strong\u003e: Designing the hierarchy for policy inheritance.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNetwork Topology\u003c/strong\u003e: Hub-and-spoke with forced tunneling and IPAM.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecurity\u003c/strong\u003e: Defender for Cloud and centralized Sentinel.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGovernance\u003c/strong\u003e: Enforcing standards via Azure Policy (Audit/Deny/DINE).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eManagement and Monitoring\u003c/strong\u003e: Centralized Log Analytics and Workbooks.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePlatform Automation\u003c/strong\u003e: CI/CD pipelines and automated subscription vending.\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"iac-tooling-terraform-avm-vs-bicep-avm\"\u003eIaC Tooling: Terraform AVM vs. Bicep AVM\u003c/h2\u003e\n\u003cp\u003eModern landing zones utilize \u003cstrong\u003eAzure Verified Modules (AVM)\u003c/strong\u003e. These are Microsoft-maintained modules that follow Azure\u0026rsquo;s recommended practices.\u003c/p\u003e\n\u003ch3 id=\"terraform-avm\"\u003eTerraform AVM\u003c/h3\u003e\n\u003cp\u003eBest for teams already using Terraform or multi-cloud environments. Requires an Azure Storage state backend.\n\u003cem\u003eLegacy Note: The \u003ccode\u003eterraform-azurerm-caf-enterprise-scale\u003c/code\u003e module is archived as of August 2026.\u003c/em\u003e\u003c/p\u003e\n\u003ch3 id=\"bicep-avm\"\u003eBicep AVM\u003c/h3\u003e\n\u003cp\u003eNative ARM integration with no state file to manage. Uses \u003cstrong\u003eDeployment Stacks\u003c/strong\u003e (GA since May 2024) for lifecycle management.\n\u003cem\u003eLegacy Note: ALZ-Bicep was deprecated in February 2026.\u003c/em\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"hands-on-deploying-the-scaffold\"\u003eHands-On: Deploying the Scaffold\u003c/h2\u003e\n\u003cp\u003eUse the ALZ Accelerator to generate your initial repository structure.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStep 1: Install the ALZ Module\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInstall-Module -Name ALZ -Force -Scope CurrentUser\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 2026 release is 7.0.1+; review breaking changes if upgrading from 6.x\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eStep 2: Run the Bootstrap\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eNew-ALZEnvironment -Path \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;C:\\Source\\MyALZ\u0026#34;\u003c/span\u003e -DeploymentStrategy \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;GitHubActions\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eStep 3: Deploy the Hierarchy (Terraform)\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;management_groups\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  source  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Azure/avm-ptn-alz/azurerm\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;0.19.0\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  management_group_name \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;contoso\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  enable_telemetry      \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHierarchy First\u003c/strong\u003e: Design your management group tree before writing any networking or application code.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIP Planning\u003c/strong\u003e: Plan address space for three to five years. Resizing VNets after peering is established triggers outages.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOIDC Only\u003c/strong\u003e: Never use client secrets in pipelines. Use OIDC federated identity for all Azure connections.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePolicy at Scale\u003c/strong\u003e: Assign policies at the management group level, not the subscription level.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"series-roadmap\"\u003eSeries Roadmap\u003c/h2\u003e\n\u003cp\u003eThis guide is the introduction to a 10-part series on building a production-ready Azure foundation. Follow the articles in order to build your environment layer-by-layer:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eOrder\u003c/th\u003e\n          \u003cth\u003eArticle\u003c/th\u003e\n          \u003cth\u003eFocus\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../azure-management-group-design/\"\u003eManagement Group Hierarchy\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eCore structure and subscription organization.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../azure-hub-spoke-networking/\"\u003eHub-and-Spoke Networking\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eCentralized egress, Firewall, and Private DNS.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 3\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../azure-identity-access-architecture/\"\u003eIdentity and Access Architecture\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eRBAC, PIM, and secret-less OIDC pipelines.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 4\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../azure-policy-governance-scale/\"\u003eGovernance at Scale\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eAutomated enforcement via Azure Policy.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 5\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../azure-subscription-vending-automation/\"\u003eSubscription Vending\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eProgrammatic workload onboarding.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 6\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../azure-monitor-centralized-logging/\"\u003eCentralized Monitoring\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eLog Analytics, DINE policies, and Workbooks.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 7\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../azure-security-baseline-guide/\"\u003eSecurity Baseline\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eDefender for Cloud and Microsoft Sentinel.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 8\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../landing-zone-cicd-pipeline/\"\u003eCI/CD for Azure Landing Zones\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eAutomating AVM with GitHub Actions.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 9\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../landing-zone-cost-governance/\"\u003eAzure Cost Governance\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eTagging enforcement and forecasted budgets.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003ePost 10\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"../landing-zone-day-2-ops/\"\u003eDay-2 Operations\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eDrift remediation and AVM migration.\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/\"\u003eMicrosoft Cloud Adoption Framework — Landing Zone Overview\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://azure.github.io/Azure-Landing-Zones/accelerator/\"\u003eAzure Landing Zones Accelerator\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://azure.github.io/Azure-Verified-Modules/\"\u003eAzure Verified Modules (AVM)\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eStart with \u003ca href=\"../azure-management-group-design/\"\u003ePost 1: Design Your Azure Management Group and Subscription Hierarchy\u003c/a\u003e — the foundation for every subsequent article in this series.\u003c/p\u003e\n","description":"Build a production-grade Azure Landing Zone from scratch. Covers all 8 CAF design areas with Terraform and Bicep AVM code examples.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/azure-landing-zone-guide/","title":"Enterprise Azure Landing Zone: The Complete Guide"},{"content":"\u003cp\u003eThe management group hierarchy is the first IaC file you commit to a landing zone repository. It is also the decision that is hardest to change later. Moving subscriptions between management groups after workloads are deployed triggers policy re-evaluation and potential compliance violations across every resource in those subscriptions. Get it right the first time.\u003c/p\u003e\n\u003cp\u003eThe placement of a subscription in the hierarchy determines which policies it inherits, which RBAC assignments cascade down to it, and how costs roll up for reporting. A hierarchy designed without this context ends up as a flat list of subscriptions under the Tenant Root — which is structurally identical to having no hierarchy at all.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eImplement the standard CAF management group hierarchy.\u003c/li\u003e\n\u003cli\u003eStructure subscriptions for production, non-production, and sandbox environments.\u003c/li\u003e\n\u003cli\u003eEnforce naming conventions as IaC.\u003c/li\u003e\n\u003cli\u003eDeploy the hierarchy using Terraform AVM and Bicep AVM.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 1 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e. Every subsequent article builds on the hierarchy deployed here.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"the-caf-management-group-hierarchy\"\u003eThe CAF Management Group Hierarchy\u003c/h2\u003e\n\u003cp\u003eThe Cloud Adoption Framework (CAF) defines a standard hierarchy built around governance and connectivity requirements, not org chart reporting lines. The structure under the Tenant Root:\u003c/p\u003e\n\u003cpre class=\"mermaid\"\u003e\n  graph TD\n    Root[Tenant Root Group] --\u0026gt; Platform[Platform MG]\n    Root --\u0026gt; LZ[Landing Zones MG]\n    Root --\u0026gt; Sandbox[Sandbox MG]\n    Root --\u0026gt; Decomm[Decommissioned MG]\n\n    Platform --\u0026gt; Connectivity[Connectivity Sub]\n    Platform --\u0026gt; Identity[Identity Sub]\n    Platform --\u0026gt; Management[Management Sub]\n\n    LZ --\u0026gt; Corp[Corp MG - Hybrid]\n    LZ --\u0026gt; Online[Online MG - Public]\n\n    Corp --\u0026gt; Spoke_HR[HR Workload Sub]\n    Online --\u0026gt; Spoke_Web[Web App Sub]\n\n    style Root fill:#f96,stroke:#333,stroke-width:4px\n    style Platform fill:#dfd,stroke:#333\n    style LZ fill:#dfd,stroke:#333\n    style Sandbox fill:#eee,stroke:#333\n    style Decomm fill:#eee,stroke:#333\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eNotes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eTenant Root Group\u003c/strong\u003e is the apex for all global policies.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePlatform MG\u003c/strong\u003e isolates shared services from workload traffic.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLanding Zones MG\u003c/strong\u003e is subdivided by network requirements (Corp vs. Online).\u003c/li\u003e\n\u003cli\u003ePolicies assigned at the \u003cstrong\u003eLanding Zones\u003c/strong\u003e level automatically cascade to all workload subscriptions.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"platform-tier--shared-services\"\u003ePlatform Tier — Shared Services\u003c/h3\u003e\n\u003cp\u003eThe Platform management group holds three subscriptions owned by the platform team. No workloads deploy here.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eManagement\u003c/strong\u003e: Hosts centralized observability (Log Analytics).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConnectivity\u003c/strong\u003e: Hosts the hub networking stack (Azure Firewall, Bastion, DNS).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIdentity\u003c/strong\u003e: Hosts hybrid identity components (AD DS) if required.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"landing-zones-tier--workload-isolation\"\u003eLanding Zones Tier — Workload Isolation\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCorp\u003c/strong\u003e: Subscriptions for workloads requiring hybrid connectivity to on-premises networks.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOnline\u003c/strong\u003e: Internet-facing workloads with no on-premises dependency.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"sandbox-and-decommissioned-tiers\"\u003eSandbox and Decommissioned Tiers\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSandbox\u003c/strong\u003e: A space for experimentation with mandatory budget alerts but fewer restrictive policies.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDecommissioned\u003c/strong\u003e: A parking area for subscriptions being retired. A \u003ccode\u003eDeny\u003c/code\u003e policy on all write operations ensures no new resources are created during the archival process.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"subscription-design-and-naming\"\u003eSubscription Design and Naming\u003c/h2\u003e\n\u003cp\u003eSubscriptions are the primary blast-radius boundary in Azure. One production workload per subscription is the recommended pattern for enterprises.\u003c/p\u003e\n\u003ch3 id=\"naming-conventions\"\u003eNaming Conventions\u003c/h3\u003e\n\u003cp\u003eManagement group IDs are immutable. Use descriptive IDs that reflect the tier\u0026rsquo;s purpose.\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eResource\u003c/th\u003e\n          \u003cth\u003ePattern\u003c/th\u003e\n          \u003cth\u003eExample\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eManagement group ID\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e\u0026lt;org\u0026gt;-\u0026lt;tier\u0026gt;[-\u0026lt;subtier\u0026gt;]\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003econtoso-landingzones-corp\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eSubscription name\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e\u0026lt;org\u0026gt;-\u0026lt;workload\u0026gt;-\u0026lt;env\u0026gt;\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003econtoso-payroll-prod\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"deploying-the-hierarchy-with-terraform-avm\"\u003eDeploying the Hierarchy with Terraform AVM\u003c/h2\u003e\n\u003cp\u003eThe AVM pattern module \u003ccode\u003eAzure/avm-ptn-alz/azurerm\u003c/code\u003e deploys the full hierarchy. Note that modern versions require the \u003ccode\u003ealz\u003c/code\u003e provider.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003eproviders.tf\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eterraform\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003erequired_providers\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    azurerm \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      source  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;hashicorp/azurerm\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;~\u0026gt; 4.0\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    alz \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      source  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azure/alz\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;~\u0026gt; 0.20\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eprovider\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;alz\u0026#34;\u003c/span\u003e {\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e  # The alz provider handles policy and management group logic\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e\u003ccode\u003emain.tf\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;alz\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  source  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Azure/avm-ptn-alz/azurerm\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;0.19.0\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  management_group_name \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eorg_prefix\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  enable_telemetry      \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Subscription placement\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azurerm_management_group_subscription_association\u0026#34; \u0026#34;connectivity\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  management_group_id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/providers/Microsoft.Management/managementGroups/${var.org_prefix}-platform-connectivity\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  subscription_id     \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/subscriptions/${var.subscription_ids.connectivity}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  depends_on \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003ealz\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"deploying-the-hierarchy-with-bicep-avm\"\u003eDeploying the Hierarchy with Bicep AVM\u003c/h2\u003e\n\u003cp\u003eBicep uses Deployment Stacks for lifecycle management. Use \u003ccode\u003edetachAll\u003c/code\u003e for the initial deployment to prevent accidental deletion of existing resources.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003emain.bicep\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bicep\" data-lang=\"bicep\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003etargetScope\u003c/span\u003e = \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;tenant\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e alz \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;br/public:avm/ptn/alz/alz:0.1.0\u0026#39;\u003c/span\u003e = {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;alzHierarchyDeploy\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  params: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    managementGroupName: orgPrefix\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    subscriptionPlacement: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      management: subscriptionIds.management\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      connectivity: subscriptionIds.connectivity\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eDeployment Command\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eaz stack tenant create \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --name \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;alz-hierarchy\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --template-file main.bicep \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --parameters parameters.json \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --action-on-unmanage detachAll \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --location eastus\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAvoid Environment-based Management Groups\u003c/strong\u003e: Do not create \u003ccode\u003eProduction\u003c/code\u003e and \u003ccode\u003eDevelopment\u003c/code\u003e MGs. Use subscriptions for environments so that a single policy (like \u0026ldquo;Require Tags\u0026rdquo;) assigned at the \u003ccode\u003eCorp\u003c/code\u003e MG covers both.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLimit Depth\u003c/strong\u003e: Keep the hierarchy to four or five levels maximum. Deeper structures make policy troubleshooting difficult.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTest Policy Inheritance\u003c/strong\u003e: Create a test resource group without required tags immediately after deployment to confirm the \u003ccode\u003eDeny\u003c/code\u003e policy triggers.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"troubleshooting\"\u003eTroubleshooting\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;Management group will be destroyed and recreated\u0026rdquo;\u003c/strong\u003e\nThe \u003ccode\u003ename\u003c/code\u003e (ID) field changed. IDs are immutable. If a rename is necessary, you must manually move subscriptions and policies to a new group before deleting the old one. Use \u003ccode\u003eterraform state list\u003c/code\u003e to identify the exact resource addresses before refactoring.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;Forbidden error during deployment\u0026rdquo;\u003c/strong\u003e\nThe deploying identity needs \u003ccode\u003eManagement Group Contributor\u003c/code\u003e at the Tenant Root scope (\u003ccode\u003e/\u003c/code\u003e). Standing \u003ccode\u003eGlobal Administrator\u003c/code\u003e should be removed after the initial bootstrap in favor of PIM.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/resource-org-management-groups\"\u003eCAF Resource Organization — Management Groups\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://registry.terraform.io/modules/Azure/avm-ptn-alz/azurerm\"\u003eAVM — ALZ Terraform Module\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#management-group-limits\"\u003eAzure Subscription and Management Group Service Limits\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWith the hierarchy deployed, move to \u003ca href=\"../azure-hub-spoke-networking/\"\u003ePost 2: Hub-and-Spoke Networking\u003c/a\u003e. The Connectivity subscription created here becomes the home for the hub VNet and Azure Firewall.\u003c/p\u003e\n","description":"Design and deploy a production Azure management group hierarchy with Terraform and Bicep AVM. Covers CAF topology, subscription strategy, and naming.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/azure-management-group-design/","title":"Design Your Azure Management Group and Subscription Hierarchy"},{"content":"\u003cp\u003eRunning out of IP address space in a cloud network is not like running out of disk space. There is no \u0026ldquo;add more\u0026rdquo; without a rebuild. Reconfiguring VNet address ranges after workloads are deployed means re-deploying VMs, re-creating private endpoints, and coordinating outages across teams. The right time to plan your network is before the first spoke VNet exists.\u003c/p\u003e\n\u003cp\u003eHub-and-spoke is the standard topology for Azure landing zones. Production hub networks carry Azure Firewall with forced tunnel routing, Azure Bastion for secure VM access, and Private DNS Zones for centralized name resolution.\u003c/p\u003e\n\u003cp\u003eAs of March 31, 2026, Azure retired default outbound internet access for VMs in new VNets. New VNets are private-by-default — a VM with no public IP and no explicit egress path has no internet connectivity. A centralized hub with Azure Firewall is now a mandatory requirement for internet egress, not just a security recommendation.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIdentify the mandatory components of a production hub VNet.\u003c/li\u003e\n\u003cli\u003ePlan an IP address space that supports three to five years of spoke growth.\u003c/li\u003e\n\u003cli\u003eDeploy the hub network using Terraform AVM and Bicep AVM.\u003c/li\u003e\n\u003cli\u003eConfigure UDRs to route all spoke traffic through Azure Firewall.\u003c/li\u003e\n\u003cli\u003eCentralize Private DNS resolution for private endpoints.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 2 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e. It builds on the Connectivity subscription provisioned in \u003ca href=\"../azure-management-group-design/\"\u003ePost 1\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"hub-and-spoke-topology\"\u003eHub-and-Spoke Topology\u003c/h2\u003e\n\u003cpre class=\"mermaid\"\u003e\n  graph LR\n    subgraph Hub_VNet [Hub VNet - 10.0.0.0/16]\n        direction TB\n        FW[Azure Firewall Subnet]\n        Bastion[Azure Bastion Subnet]\n        GW[Gateway Subnet]\n        DNS[Private DNS Resolver]\n    end\n\n    subgraph Spoke_A [Spoke VNet A - 10.10.0.0/22]\n        Workload_A[Workload Subnet]\n    end\n\n    subgraph Spoke_B [Spoke VNet B - 10.10.4.0/22]\n        Workload_B[Workload Subnet]\n    end\n\n    Workload_A -- Peering \u0026amp; UDR --\u0026gt; FW\n    Workload_B -- Peering \u0026amp; UDR --\u0026gt; FW\n    FW -- Egress --\u0026gt; Internet((Internet))\n    GW -- Hybrid --\u0026gt; OnPrem((On-Premises))\n\n    style Hub_VNet fill:#e1f5fe,stroke:#01579b\n    style Spoke_A fill:#f1f8e9,stroke:#33691e\n    style Spoke_B fill:#f1f8e9,stroke:#33691e\n    style FW fill:#f44336,color:#fff\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eNotes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eUDR (User Defined Routes)\u003c/strong\u003e on spoke subnets force all \u003ccode\u003e0.0.0.0/0\u003c/code\u003e traffic to the Hub\u0026rsquo;s Firewall private IP.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVNet Peering\u003c/strong\u003e is non-transitive by default; spokes cannot talk to each other unless routed through the Firewall.\u003c/li\u003e\n\u003cli\u003eThe \u003cstrong\u003eGateway Subnet\u003c/strong\u003e handles Site-to-Site VPN or ExpressRoute traffic.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"the-hub-vnet\"\u003eThe Hub VNet\u003c/h3\u003e\n\u003cp\u003eThe hub VNet hosts shared network services that every spoke consumes. Workload teams do not deploy resources here; they peer their spoke VNets to the hub to inherit its security and connectivity.\u003c/p\u003e\n\u003cp\u003eThe hub VNet requires four subnets with names enforced by Azure:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eSubnet Name\u003c/th\u003e\n          \u003cth\u003eMinimum Size\u003c/th\u003e\n          \u003cth\u003ePurpose\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eAzureFirewallSubnet\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e/26\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eAzure Firewall.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eAzureFirewallManagementSubnet\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e/26\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eMandatory for forced tunneling and \u003cstrong\u003eall\u003c/strong\u003e Basic SKU firewalls.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eAzureBastionSubnet\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e/26\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eAzure Bastion.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eGatewaySubnet\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e/27\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eVPN or ExpressRoute Gateway.\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003eScale Tip:\u003c/strong\u003e Adding a subnet to a VNet with active peerings requires removing and re-creating those peerings. Plan and reserve your hub subnets before establishing any connections.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"ip-address-space-planning\"\u003eIP Address Space Planning\u003c/h2\u003e\n\u003cp\u003eAzure VNet address spaces cannot overlap within a peering relationship. Allocate a large block exclusively for Azure and subdivide by purpose.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA Practical Model:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHub (East US)\u003c/strong\u003e: \u003ccode\u003e10.0.0.0/16\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCorp Spokes\u003c/strong\u003e: \u003ccode\u003e10.10.0.0/16\u003c/code\u003e – \u003ccode\u003e10.49.0.0/16\u003c/code\u003e (Workloads needing on-prem access)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOnline Spokes\u003c/strong\u003e: \u003ccode\u003e10.50.0.0/16\u003c/code\u003e – \u003ccode\u003e10.79.0.0/16\u003c/code\u003e (Internet-facing workloads)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEach spoke VNet should receive a \u003ccode\u003e/22\u003c/code\u003e (1,024 addresses) by default. This handles most enterprise application requirements without fragmentation.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"azure-firewall-and-routing\"\u003eAzure Firewall and Routing\u003c/h2\u003e\n\u003ch3 id=\"forced-tunneling-and-udrs\"\u003eForced Tunneling and UDRs\u003c/h3\u003e\n\u003cp\u003eForced tunneling ensures all internet-bound traffic from spokes flows through Azure Firewall for inspection. This requires a User Defined Route (UDR) on spoke subnets with \u003ccode\u003e0.0.0.0/0\u003c/code\u003e pointing to the Firewall\u0026rsquo;s private IP.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAvoiding Asymmetric Routing:\u003c/strong\u003e When an on-premises host connects to a spoke VM, the return traffic might hit the spoke UDR and attempt to exit through the Firewall. The Firewall will drop this traffic because it didn\u0026rsquo;t see the original inbound request. Fix this by adding a UDR to the hub\u0026rsquo;s \u003ccode\u003eGatewaySubnet\u003c/code\u003e that routes traffic \u003cstrong\u003edestined for the spoke address ranges\u003c/strong\u003e through the Firewall.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"azure-bastion\"\u003eAzure Bastion\u003c/h2\u003e\n\u003cp\u003eAzure Bastion replaces the traditional jump server anti-pattern. It provides RDP and SSH directly through the portal or \u003ccode\u003eaz\u003c/code\u003e CLI without exposing ports 22 or 3389 to the internet.\u003c/p\u003e\n\u003cp\u003eUse the \u003cstrong\u003eStandard SKU\u003c/strong\u003e for hub deployments. Standard Bastion supports peering-based access, allowing it to reach VMs in any peered spoke VNet. The Developer SKU is restricted to its own VNet and is unsuitable for hub-and-spoke designs.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"private-dns-zones\"\u003ePrivate DNS Zones\u003c/h2\u003e\n\u003cp\u003eWhen you use private endpoints, you must resolve the service\u0026rsquo;s public name (e.g., \u003ccode\u003emystorage.blob.core.windows.net\u003c/code\u003e) to its private IP.\u003c/p\u003e\n\u003cp\u003eDeploy the full set of Private DNS Zones (Blob, Key Vault, SQL, etc.) in the Connectivity subscription from day one. Link these zones to the hub VNet. For spokes to resolve these records, either link the zones to every spoke VNet or deploy an \u003cstrong\u003eAzure DNS Private Resolver\u003c/strong\u003e in the hub to forward queries.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"deploying-the-hub-with-terraform-avm\"\u003eDeploying the Hub with Terraform AVM\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003emain.tf\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;hub_vnet\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  source  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Azure/avm-res-network-virtualnetwork/azurerm\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;~\u0026gt; 0.7\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name                \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;vnet-hub-connectivity-001\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  address_space       \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;10.0.0.0/16\u0026#34;\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  subnets \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    AzureFirewallSubnet \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e { address_prefixes \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;10.0.0.0/26\u0026#34;\u003c/span\u003e] }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    AzureBastionSubnet  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e { address_prefixes \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;10.0.1.0/26\u0026#34;\u003c/span\u003e] }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;firewall\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  source  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Azure/avm-res-network-azurefirewall/azurerm\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;~\u0026gt; 0.3\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name       \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;fw-hub-001\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  sku_tier   \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Standard\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  firewall_policy_id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003efirewall_policy\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eresource_id\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e  # ... IP configurations\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eStandard SKUs Only\u003c/strong\u003e: Use Standard SKU for both Bastion and Firewall. Basic SKUs lack the peering and scaling features required for production hub-and-spoke environments.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEnable DNS Proxy\u003c/strong\u003e: Turn on DNS Proxy in the Firewall Policy. This allows spokes to use the Firewall as their DNS server, enabling FQDN-based filtering for all traffic.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGlobal Private DNS\u003c/strong\u003e: Deploy the \u003ccode\u003eAzure/avm-ptn-network-private-link-private-dns-zones\u003c/code\u003e module to create all ~60 required DNS zones at once. Reactive zone creation is an operational burden.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"troubleshooting\"\u003eTroubleshooting\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;Spoke VM cannot reach the internet\u0026rdquo;\u003c/strong\u003e\nVerify the spoke subnet is associated with a UDR. Since March 31, 2026, there is no default outbound access. Check the Firewall logs:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eaz monitor log-analytics query \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --workspace \u0026lt;workspace-id\u0026gt; \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --analytics-query \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;AZFWApplicationRule | where Action == \u0026#39;Deny\u0026#39; | take 10\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e\u0026ldquo;Private endpoint resolves to a public IP\u0026rdquo;\u003c/strong\u003e\nThe Private DNS Zone is likely not linked to the hub VNet, or the spoke VNet is not using the hub for DNS. Confirm the VNet link exists in the Connectivity subscription.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/network-topology-and-connectivity\"\u003eCAF Network Topology and Connectivity\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/bastion/bastion-faq#skus\"\u003eAzure Bastion SKU Comparison\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://registry.terraform.io/browse/modules?provider=azurerm\"\u003eAzure Verified Modules — Networking\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWith networking deployed, move to \u003ca href=\"../azure-identity-access-architecture/\"\u003ePost 3: Identity and Access Architecture\u003c/a\u003e. We will configure the OIDC identities and PIM settings that govern access to this hub.\u003c/p\u003e\n","description":"Build a production hub-and-spoke network for Azure landing zones. Covers Azure Firewall, Bastion, Private DNS Zones, and VNet peering with Terraform and Bicep.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/azure-hub-spoke-networking/","title":"Azure Landing Zone Hub-and-Spoke: Firewall, Bastion, DNS"},{"content":"\u003cp\u003eThe most common Azure security finding in enterprise environments is not a misconfigured firewall or an exposed storage account. It is a service principal with Owner rights that was created two years ago, whose secret has never rotated, and whose owner left the company six months ago. Nobody knows what it does. Nobody is willing to delete it.\u003c/p\u003e\n\u003cp\u003eIdentity is the control plane of Azure. Every action — whether taken by a human, a CI/CD pipeline, or a workload — is authorized through either Entra ID roles or Azure RBAC. Organizations that treat identity as an afterthought end up with over-privileged accounts, stale service principals, standing access to production, and no auditability. A landing zone designed without a deliberate identity model accumulates these problems as quickly as new workloads arrive.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDistinguish between Entra ID roles and Azure RBAC to close dangerous governance gaps.\u003c/li\u003e\n\u003cli\u003eApply RBAC at the management group level to ensure every workload subscription inherits a consistent access baseline.\u003c/li\u003e\n\u003cli\u003eConfigure PIM to eliminate standing privileged access for human operators.\u003c/li\u003e\n\u003cli\u003eReplace service principal secrets in CI/CD pipelines with OIDC federated credentials.\u003c/li\u003e\n\u003cli\u003eDeploy role assignments and federated credentials as IaC using Terraform and Bicep.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 3 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e. It builds on the management group hierarchy from \u003ca href=\"../azure-management-group-design/\"\u003ePost 1\u003c/a\u003e and provides the OIDC identities used by the CI/CD pipeline in \u003ca href=\"../landing-zone-cicd-pipeline/\"\u003ePost 8\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"two-identity-systems-one-azure-environment\"\u003eTwo Identity Systems, One Azure Environment\u003c/h2\u003e\n\u003ch3 id=\"entra-id-roles--tenant-wide-administrative-access\"\u003eEntra ID Roles — Tenant-Wide Administrative Access\u003c/h3\u003e\n\u003cp\u003eEntra ID roles (formerly Azure AD roles) control administrative privileges to tenant-level services: user and group management, application registration, and PIM administration. These roles operate entirely within the Entra ID plane — they govern who can manage identities, not who can access Azure resources.\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eRole\u003c/th\u003e\n          \u003cth\u003eWhat It Controls\u003c/th\u003e\n          \u003cth\u003eNeeded By\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eGlobal Administrator\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eFull tenant admin\u003c/td\u003e\n          \u003ctd\u003eBreak-glass accounts only\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003ePrivileged Role Administrator\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eManages Entra ID roles and PIM settings\u003c/td\u003e\n          \u003ctd\u003ePlatform team (via PIM)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eSecurity Administrator\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eManages Defender XDR, Sentinel, CA policies\u003c/td\u003e\n          \u003ctd\u003eSecurity team\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eApplication Administrator\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eCreates and manages app registrations\u003c/td\u003e\n          \u003ctd\u003ePlatform CI/CD pipeline\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ccode\u003eDirectory Readers\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eReads directory objects (users, groups)\u003c/td\u003e\n          \u003ctd\u003eMost automation identities\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003eThe Elevation Boundary:\u003c/strong\u003e A Global Administrator cannot read a VM\u0026rsquo;s disk or view a subscription\u0026rsquo;s resources unless they also have an Azure RBAC role on that subscription. An Owner on a subscription cannot escalate themselves to Global Administrator. These are separate authorization systems.\u003c/p\u003e\n\u003ch3 id=\"azure-rbac--resource-level-authorization\"\u003eAzure RBAC — Resource-Level Authorization\u003c/h3\u003e\n\u003cp\u003eAzure RBAC controls who can perform specific actions on Azure resources. The scope hierarchy mirrors the management group hierarchy: Tenant Root → Management Group → Subscription → Resource Group → Resource.\u003c/p\u003e\n\u003cp\u003eThree built-in roles form the foundation of landing zone RBAC designs:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eOwner\u003c/code\u003e\u003c/strong\u003e: Full resource control plus the ability to assign roles to others.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eContributor\u003c/code\u003e\u003c/strong\u003e: Full resource control, but cannot manage role assignments.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eReader\u003c/code\u003e\u003c/strong\u003e: Read-only access.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eRole assignments are additive. A principal\u0026rsquo;s effective permissions are the union of all role assignments across all applicable scopes.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScale Tip:\u003c/strong\u003e Azure allows 500 role assignments per management group. Assigning at the management group level uses 1 of that 500 quota but covers all subscriptions beneath it without consuming per-subscription quota. For 50 subscriptions, this is a 50:1 efficiency gain over subscription-level assignments.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"rbac-design-at-management-group-scope\"\u003eRBAC Design at Management Group Scope\u003c/h2\u003e\n\u003ch3 id=\"rbac-conditions--constraining-owner-permissions\"\u003eRBAC Conditions — Constraining Owner Permissions\u003c/h3\u003e\n\u003cp\u003eThe platform CI/CD pipeline requires \u003ccode\u003eOwner\u003c/code\u003e at the management group level to create role assignments for policy remediation identities. However, an unrestricted \u003ccode\u003eOwner\u003c/code\u003e assignment is a security risk.\u003c/p\u003e\n\u003cp\u003eRBAC conditions (GA since 2023) constrain \u003ccode\u003eOwner\u003c/code\u003e assignments with attribute-based logic. An 8 KB size limit applies to the condition expression; if you approach this limit, consider using Custom Security Attributes for more complex logic.\u003c/p\u003e\n\u003cp\u003eThe following condition allows the pipeline to assign \u003ccode\u003eContributor\u003c/code\u003e but blocks it from assigning \u003ccode\u003eOwner\u003c/code\u003e or \u003ccode\u003eUser Access Administrator\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Terraform: azurerm_role_assignment with RBAC condition\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azurerm_role_assignment\u0026#34; \u0026#34;platform_cicd_owner\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  scope                \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/providers/Microsoft.Management/managementGroups/${var.platform_mg_id}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  role_definition_name \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Owner\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  principal_id         \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eazurerm_user_assigned_identity\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eplatform_cicd\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eprincipal_id\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  condition_version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;2.0\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e  # Use ForAllOfAnyValues:GuidNotEquals to ensure ALL requested roles \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e  # are checked against the blocked list.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  condition \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e\u0026lt;\u0026lt;-\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eEOT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    (\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      (\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e!\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eActionMatches\u003c/span\u003e{\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eMicrosoft\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eAuthorization\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eroleAssignments\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003ewrite\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e\u0026#39;\u003c/span\u003e}))\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#66d9ef\"\u003eOR\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      (\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e@\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eRequest\u003c/span\u003e[\u003cspan style=\"color:#66d9ef\"\u003eMicrosoft\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eAuthorization\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eroleAssignments\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e:\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eRoleDefinitionId\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          \u003cspan style=\"color:#66d9ef\"\u003eForAllOfAnyValues\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e:\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eGuidNotEquals\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#ae81ff\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003ee\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003eaf\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e657\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003ea8ff\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e443\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003ec-\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003ea75c\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003efe\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003ec\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003ebcb\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e635\u003c/span\u003e,\u003cspan style=\"color:#75715e\"\u003e # Owner\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#ae81ff\"\u003e18\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003ed\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e7\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003ed\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e88\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003ed-\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003ed4f5\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003eb\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e35\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e97\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003eb\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e4\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003ec3f4b4b9b9b6\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e  # User Access Administrator\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      )\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    )\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003eEOT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  skip_service_principal_aad_check \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"privileged-identity-management\"\u003ePrivileged Identity Management\u003c/h2\u003e\n\u003ch3 id=\"pim-for-groups--scaling-jit-access\"\u003ePIM for Groups — Scaling JIT Access\u003c/h3\u003e\n\u003cp\u003ePIM for Groups (GA since 2023) ensures that users hold an \u003cstrong\u003eeligible\u003c/strong\u003e assignment rather than a \u003cstrong\u003estanding\u003c/strong\u003e one. Users are made eligible members of an Entra security group, and the group holds the role assignment.\u003c/p\u003e\n\u003cp\u003eThis separates access management (group membership) from role management (group assignments). Adding a new engineer means adding them to the \u003ccode\u003esg-platform-engineers\u003c/code\u003e group as an eligible member.\u003c/p\u003e\n\u003ch3 id=\"automating-pim-role-settings\"\u003eAutomating PIM Role Settings\u003c/h3\u003e\n\u003cp\u003ePIM role settings (activation duration, MFA, approval) must be configured per-role per-scope. Since these are not fully supported in the \u003ccode\u003eazurerm\u003c/code\u003e provider yet, use the Microsoft Graph API via a two-step \u003ccode\u003eaz rest\u003c/code\u003e process:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 1. Fetch the specific policy ID for the \u0026#39;Owner\u0026#39; role at this scope\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ePOLICY_ID\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e$(\u003c/span\u003eaz rest --method GET --url \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://management.azure.com/providers/Microsoft.Management/managementGroups/\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e${\u003c/span\u003eMG_ID\u003cspan style=\"color:#e6db74\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e/providers/Microsoft.Authorization/roleManagementPolicies?api-version=2020-10-01\u0026#34;\u003c/span\u003e --query \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;value[?roleDefinitionId==\u0026#39;/providers/Microsoft.Authorization/roleDefinitions/\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e${\u003c/span\u003eOWNER_ROLE_ID\u003cspan style=\"color:#e6db74\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;].id\u0026#34;\u003c/span\u003e -o tsv\u003cspan style=\"color:#66d9ef\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 2. PATCH the policy to require MFA and 2-hour max duration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eaz rest --method PATCH --url \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://management.azure.com/\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e${\u003c/span\u003ePOLICY_ID\u003cspan style=\"color:#e6db74\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e?api-version=2020-10-01\u0026#34;\u003c/span\u003e --body \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;{\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    \u0026#34;properties\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      \u0026#34;rules\u0026#34;: [\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e          \u0026#34;id\u0026#34;: \u0026#34;Expiration_Admin_Assignment\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e          \u0026#34;ruleType\u0026#34;: \u0026#34;RoleManagementPolicyExpirationRule\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e          \u0026#34;isExpirationRequired\u0026#34;: true,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e          \u0026#34;maximumDuration\u0026#34;: \u0026#34;PT2H\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e          \u0026#34;id\u0026#34;: \u0026#34;Enablement_Admin_Assignment\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e          \u0026#34;ruleType\u0026#34;: \u0026#34;RoleManagementPolicyEnablementRule\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e          \u0026#34;enabledRules\u0026#34;: [\u0026#34;MultiFactorAuthentication\u0026#34;, \u0026#34;Justification\u0026#34;]\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      ]\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e  }\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"pipelines-without-secrets-workload-identity-with-oidc\"\u003ePipelines Without Secrets: Workload Identity with OIDC\u003c/h2\u003e\n\u003cp\u003eWorkload Identity Federation (OIDC) replaces static client secrets with short-lived tokens.\u003c/p\u003e\n\u003cpre class=\"mermaid\"\u003e\n  sequenceDiagram\n    autonumber\n    participant GH as GitHub Actions\n    participant Entra as Entra ID\n    participant Azure as Azure Resource Manager\n\n    GH-\u0026gt;\u0026gt;GH: Generate OIDC ID Token (JWT)\n    GH-\u0026gt;\u0026gt;Entra: Request Access Token (JWT + Aud/Sub)\n    Note over Entra: Validate Federated Identity\u0026lt;br/\u0026gt;(Repo/Env match Subject)\n    Entra--\u0026gt;\u0026gt;GH: Issue Short-lived Access Token\n    GH-\u0026gt;\u0026gt;Azure: Deploy IaC (Terraform/Bicep)\n    Note right of Azure: Authorization via RBAC Roles\n    Azure--\u0026gt;\u0026gt;GH: Deployment Success\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eNotes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNo Client Secrets\u003c/strong\u003e are stored in GitHub; the trust is based on the repository identity.\u003c/li\u003e\n\u003cli\u003eThe \u003cstrong\u003eSubject (sub)\u003c/strong\u003e claim must match the Federated Credential configuration in Entra ID.\u003c/li\u003e\n\u003cli\u003eTokens are short-lived (usually 1 hour), significantly reducing the risk of credential leakage.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"terraform--oidc-federated-credential\"\u003eTerraform — OIDC Federated Credential\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Federated credential for GitHub Actions (environment: production)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azuread_application_federated_identity_credential\u0026#34; \u0026#34;github_prod\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  application_id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eazuread_application\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003elanding_zone_cicd\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eid\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  display_name   \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;github-production\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  issuer         \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://token.actions.githubusercontent.com\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  subject        \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;repo:contoso/landing-zone:environment:production\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  audiences      \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;api://AzureADTokenExchange\u0026#34;\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEliminate Standing Access\u003c/strong\u003e: Make every Entra ID privileged role PIM-eligible. The 60-second activation overhead is a small price for a full audit trail.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUse User-Assigned Managed Identities\u003c/strong\u003e: These survive resource recreation and can be pre-assigned roles in IaC.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAudit via Access Reviews\u003c/strong\u003e: Schedule quarterly PIM reviews. If an eligible assignment isn\u0026rsquo;t activated for six months, revoke it.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNever Use Secrets\u003c/strong\u003e: OIDC has been the standard for GitHub Actions since 2022. Delete existing secrets and migrate to federated credentials.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"troubleshooting\"\u003eTroubleshooting\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;Role assignment fails — \u0026lsquo;PrincipalNotFound\u0026rsquo; error\u0026rdquo;\u003c/strong\u003e\nThe service principal was recently created and hasn\u0026rsquo;t replicated. In Bicep, set \u003ccode\u003eprincipalType: 'ServicePrincipal'\u003c/code\u003e explicitly to skip the AAD lookup. In Terraform, use \u003ccode\u003eskip_service_principal_aad_check = true\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;OIDC login fails — AADSTS70021\u0026rdquo;\u003c/strong\u003e\nThe \u003ccode\u003esub\u003c/code\u003e claim in the JWT does not match the federated credential. Debug by printing the token claims in your workflow:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eTOKEN\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e$(\u003c/span\u003ecurl -s -H \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Authorization: bearer \u003c/span\u003e$ACTIONS_ID_TOKEN_REQUEST_TOKEN\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e$ACTIONS_ID_TOKEN_REQUEST_URL\u003cspan style=\"color:#e6db74\"\u003e\u0026amp;audience=api://AzureADTokenExchange\u0026#34;\u003c/span\u003e | jq -r \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;.value\u0026#39;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho $TOKEN | cut -d\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;.\u0026#39;\u003c/span\u003e -f2 | base64 -d | jq .sub\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/role-based-access-control/overview\"\u003eAzure RBAC Documentation\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation\"\u003eEntra ID Workload Identity Federation\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://techcommunity.microsoft.com/t5/microsoft-entra-azure-ad-blog/microsoft-entra-privileged-identity-management-pim-for-groups-is/ba-p/3942008\"\u003ePIM for Groups GA Announcement\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/rest/api/authorization/role-management-policies\"\u003eAzure REST API: Role Management Policies\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eMove to \u003ca href=\"../azure-policy-governance-scale/\"\u003ePost 4: Governance at Scale with Azure Policy\u003c/a\u003e. The patterns established here are prerequisites for enforcing compliance and automating remediation across your subscriptions.\u003c/p\u003e\n","description":"Design the identity layer of an Azure landing zone. Covers Entra ID vs Azure RBAC, MG-scoped role assignments, PIM, and OIDC for pipelines.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/azure-identity-access-architecture/","title":"Azure Landing Zone Identity: Entra ID, RBAC, and PIM"},{"content":"\u003cp\u003eA policy assigned at the wrong scope is benign. A policy with a typo in the condition silently fails to enforce anything. A \u003ccode\u003eDeployIfNotExists\u003c/code\u003e (DINE) policy without the right managed identity permissions creates remediation tasks that queue forever without executing. Azure Policy is the most capable governance tool in Azure — and the one most likely to produce a false sense of security when misconfigured.\u003c/p\u003e\n\u003cp\u003eOrganizations often assign a policy and assume the environment is compliant. However, without moving from \u003ccode\u003eAudit\u003c/code\u003e to \u003ccode\u003eDeny\u003c/code\u003e and automating remediation, gaps remain open. This guide covers the full lifecycle of Policy-as-Code.\u003c/p\u003e\n\u003cp\u003eBy the end, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eImplement the three-resource model: Definitions, Initiatives, and Assignments.\u003c/li\u003e\n\u003cli\u003eWrite custom policy definitions in JSON and deploy them via Terraform and Bicep.\u003c/li\u003e\n\u003cli\u003eAssign regulatory benchmarks (CIS, NIST, MCSB) at the management group level.\u003c/li\u003e\n\u003cli\u003eConfigure DINE policies to automate resource configuration.\u003c/li\u003e\n\u003cli\u003eManage exceptions through time-bound exemptions.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 4 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"the-three-resource-model\"\u003eThe Three-Resource Model\u003c/h2\u003e\n\u003cp\u003eAzure Policy separates its governance model into three distinct resources:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003ePolicy Definition\u003c/strong\u003e: The rule itself — a JSON document specifying the \u003ccode\u003eif\u003c/code\u003e condition and \u003ccode\u003ethen\u003c/code\u003e effect.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePolicy Initiative\u003c/strong\u003e: A group of definitions (e.g., the CIS Benchmark). Use these to simplify management at scale.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePolicy Assignment\u003c/strong\u003e: The application of a definition or initiative to a scope (Management Group or Subscription). This triggers enforcement.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"exclusions-vs-exemptions\"\u003eExclusions vs. Exemptions\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eExclusions\u003c/strong\u003e: structural carve-outs (e.g., excluding the Sandbox MG from production \u003ccode\u003eDeny\u003c/code\u003e rules). No evaluation occurs.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eExemptions\u003c/strong\u003e: Precise, time-bound waivers for specific resources. The resource shows as \u0026ldquo;Exempt\u0026rdquo; in compliance reports.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre class=\"mermaid\"\u003e\n  graph TD\n    subgraph Governance_Resources [Policy-as-Code Objects]\n        Def[Policy Definition: \u0026#39;Enforce Tags\u0026#39;]\n        Init[Policy Initiative: \u0026#39;CIS Benchmark v2.0\u0026#39;]\n        Def --\u0026gt; Init\n    end\n\n    subgraph Scope_Hierarchy [Azure Management Scopes]\n        MG_Root[Tenant Root]\n        MG_LZ[Landing Zones MG]\n        Sub_A[Workload Sub A]\n        Sub_B[Workload Sub B]\n    end\n\n    Init -- Assignment --\u0026gt; MG_LZ\n    Def -- Assignment --\u0026gt; Sub_A\n\n    MG_LZ --\u0026gt; Sub_A\n    MG_LZ --\u0026gt; Sub_B\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eNotes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDefinitions\u003c/strong\u003e are the individual rules; \u003cstrong\u003eInitiatives\u003c/strong\u003e aggregate them for easier management.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAssignments\u003c/strong\u003e link the policy to a scope (Management Group, Subscription, or Resource Group).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInheritance\u003c/strong\u003e ensures that a policy assigned at a high level (e.g., Landing Zones MG) covers all current and future resources beneath it.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"policy-effects-and-enforcement\"\u003ePolicy Effects and Enforcement\u003c/h2\u003e\n\u003ch3 id=\"audit-and-auditifnotexists\"\u003eAudit and AuditIfNotExists\u003c/h3\u003e\n\u003cp\u003eUse \u003ccode\u003eAudit\u003c/code\u003e to measure a gap before enforcing it. The standard progression is: assign as \u003ccode\u003eAudit\u003c/code\u003e, remediate existing resources, then convert to \u003ccode\u003eDeny\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"deny\"\u003eDeny\u003c/h3\u003e\n\u003cp\u003eBlocks non-compliant resource creation. ARM rejects the request before it is written, providing a \u003ccode\u003eRequestDisallowedByPolicy\u003c/code\u003e error.\u003c/p\u003e\n\u003ch3 id=\"deployifnotexists-dine\"\u003eDeployIfNotExists (DINE)\u003c/h3\u003e\n\u003cp\u003eThe most powerful effect. If a related resource (like a diagnostic setting) is missing, the policy\u0026rsquo;s managed identity deploys it automatically via an ARM template.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDINE Example: Key Vault Diagnostic Settings\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;if\u0026#34;\u003c/span\u003e: { \u003cspan style=\"color:#f92672\"\u003e\u0026#34;field\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#f92672\"\u003e\u0026#34;equals\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Microsoft.KeyVault/vaults\u0026#34;\u003c/span\u003e },\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;then\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026#34;effect\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;DeployIfNotExists\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026#34;details\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Microsoft.Insights/diagnosticSettings\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026#34;roleDefinitionIds\u0026#34;\u003c/span\u003e: [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/providers/Microsoft.Authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293\u0026#34;\u003c/span\u003e], \u003cspan style=\"color:#75715e\"\u003e// Log Analytics Contributor\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003e\u0026#34;deployment\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026#34;properties\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          \u003cspan style=\"color:#f92672\"\u003e\u0026#34;mode\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;incremental\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          \u003cspan style=\"color:#f92672\"\u003e\u0026#34;template\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026#34;resources\u0026#34;\u003c/span\u003e: [\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e              {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#f92672\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Microsoft.KeyVault/vaults/providers/diagnosticSettings\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#f92672\"\u003e\u0026#34;apiVersion\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;2021-05-01-preview\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#75715e\"\u003e// Extension resources use \u0026#39;parentName/providers/resourceName\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#f92672\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;[concat(parameters(\u0026#39;kvName\u0026#39;), \u0026#39;/microsoft.insights/platform-diag-settings\u0026#39;)]\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#f92672\"\u003e\u0026#34;properties\u0026#34;\u003c/span\u003e: { \u003cspan style=\"color:#f92672\"\u003e\u0026#34;workspaceId\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;[parameters(\u0026#39;workspaceId\u0026#39;)]\u0026#34;\u003c/span\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e              }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"regulatory-compliance-benchmarks\"\u003eRegulatory Compliance Benchmarks\u003c/h2\u003e\n\u003cp\u003eAssign these at the \u003ccode\u003eLanding Zones\u003c/code\u003e management group to establish a baseline:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eInitiative\u003c/th\u003e\n          \u003cth\u003eID (built-in)\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eMicrosoft Cloud Security Benchmark (MCSB)\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e1f3afdf9-d0c9-4c3d-847f-89da613e70a8\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eCIS Azure Foundations Benchmark v2.0.0\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e06f19060-9e68-4070-92ca-f15cc126059e\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eNIST SP 800-53 Rev 5\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e1f9634a6-3fde-4bd2-82f3-30b5702d8d90\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"deploying-via-terraform\"\u003eDeploying via Terraform\u003c/h2\u003e\n\u003cp\u003eUse \u003ccode\u003ejsonencode()\u003c/code\u003e or \u003ccode\u003efile()\u003c/code\u003e to manage your JSON definitions.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azurerm_management_group_policy_assignment\u0026#34; \u0026#34;cis_benchmark\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name                 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;cis-foundations-lz\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  display_name         \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;CIS Benchmark v2.0.0\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  policy_definition_id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/providers/Microsoft.Authorization/policySetDefinitions/06f19060-9e68-4070-92ca-f15cc126059e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  management_group_id  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003elanding_zones_mg_id\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azurerm_role_assignment\u0026#34; \u0026#34;dine_remediation\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  scope                            \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003elanding_zones_mg_id\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  role_definition_name             \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Log Analytics Contributor\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  principal_id                     \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eazurerm_management_group_policy_assignment\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003ekv_diags\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eidentity\u003c/span\u003e[\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e].\u003cspan style=\"color:#66d9ef\"\u003eprincipal_id\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  skip_service_principal_aad_check \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e//\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003ePrevents\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003ereplication\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003erace\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003econditions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eParameterize the Effect\u003c/strong\u003e: Always parameterize the \u003ccode\u003eeffect\u003c/code\u003e field in your definitions. This allows the same rule to be \u003ccode\u003eAudit\u003c/code\u003e in Sandbox and \u003ccode\u003eDeny\u003c/code\u003e in Prod.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMode: All vs Indexed\u003c/strong\u003e: Use \u003ccode\u003emode: 'All'\u003c/code\u003e for networking policies. \u003ccode\u003eIndexed\u003c/code\u003e skips subnets and other child resources.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTime-bound Exemptions\u003c/strong\u003e: Never create an exemption without an expiry date.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWait for Evaluation\u003c/strong\u003e: Allow 15–30 minutes for the policy engine to evaluate resources before triggering remediation tasks.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eLimits to track (2026):\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e500 policy definitions per scope.\u003c/li\u003e\n\u003cli\u003e200 policy assignments per scope.\u003c/li\u003e\n\u003cli\u003e1,000 exemptions per scope.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"troubleshooting\"\u003eTroubleshooting\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;DINE remediation task is stuck at \u0026lsquo;Running\u0026rsquo;\u0026rdquo;\u003c/strong\u003e\nCheck the deployment history in the Policy blade. This usually indicates the managed identity lacks RBAC on the target subscription.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;Terraform replace on every plan\u0026rdquo;\u003c/strong\u003e\nTerraform compares JSON strings literally. Use \u003ccode\u003ejq --sort-keys\u003c/code\u003e in a pre-commit hook to normalize your policy JSON files.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/governance/policy/overview\"\u003eAzure Policy Documentation\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/governance/policy/samples/built-in-regulatory-compliance\"\u003eRegulatory Compliance initiatives\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#policy-limits\"\u003eAzure Policy Service Limits\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNext, proceed to \u003ca href=\"../azure-subscription-vending-automation/\"\u003ePost 5: Subscription Vending\u003c/a\u003e. We will integrate these policies into the automated workload onboarding process.\u003c/p\u003e\n","description":"Deploy Azure Policy definitions and assignments as code using Terraform and Bicep. Covers compliance benchmarks, DINE effects, and exemptions.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/azure-policy-governance-scale/","title":"Azure Policy as Code: Governance with Terraform and Bicep"},{"content":"\u003cp\u003eA workload team submits a ticket for a new Azure subscription. Three weeks later, a subscription exists — manually created, placed in the wrong management group, and named \u003ccode\u003esubscription-1\u003c/code\u003e because no naming convention was enforced. The team deploys anyway, into an environment with no tags, no diagnostic settings, and no budget alert. This is the default outcome when subscription creation is manual.\u003c/p\u003e\n\u003cp\u003eAs a landing zone scales, the cost of manual onboarding compounds. Every manual entry is a governance gap. Subscription vending replaces this with a PR-based workflow: a workload team submits a YAML file; the pipeline creates the subscription, places it in the hierarchy, deploys the spoke network, and configures the monitoring baseline — consistently, in under 10 minutes.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIdentify which Azure billing account types support programmatic creation.\u003c/li\u003e\n\u003cli\u003eBuild a Terraform vending module that provisions a complete application landing zone.\u003c/li\u003e\n\u003cli\u003eImplement the Bicep \u003ccode\u003esub-vending\u003c/code\u003e AVM pattern module.\u003c/li\u003e\n\u003cli\u003eDesign a PR approval workflow with automated validation.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 5 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"subscription-creation-prerequisites\"\u003eSubscription Creation Prerequisites\u003c/h2\u003e\n\u003ch3 id=\"billing-account-compatibility\"\u003eBilling Account Compatibility\u003c/h3\u003e\n\u003cp\u003eProgrammatic creation requires specific billing roles:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEnterprise Agreement (EA)\u003c/strong\u003e: Requires \u003ccode\u003eAccount Owner\u003c/code\u003e on the EA enrollment account.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMicrosoft Customer Agreement (MCA)\u003c/strong\u003e: Requires \u003ccode\u003eBilling Profile Contributor\u003c/code\u003e or \u003ccode\u003eInvoice Section Owner\u003c/code\u003e. Note that MCA accounts default to a 5-subscription limit and 1 creation per 24 hours. Open a support ticket to increase these quotas early.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePay-As-You-Go\u003c/strong\u003e: Does not support programmatic creation.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eTip:\u003c/strong\u003e Find your MCA billing IDs via CLI:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eaz billing profile list --account-name \u0026lt;account-name\u0026gt; --query \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;[].{Name:name, Id:id}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"what-every-vended-subscription-receives\"\u003eWhat Every Vended Subscription Receives\u003c/h2\u003e\n\u003cp\u003eThe vending module is a contract. Every subscription receives:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eHierarchy Placement\u003c/strong\u003e: Correct MG tier (Corp, Online, Sandbox).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMandatory Tags\u003c/strong\u003e: \u003ccode\u003eOwner\u003c/code\u003e, \u003ccode\u003eCostCenter\u003c/code\u003e, \u003ccode\u003eEnvironment\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSpoke Networking\u003c/strong\u003e: VNet with consistent naming and CIDR allocation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHub Connectivity\u003c/strong\u003e: Bidirectional peering with gateway transit.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEgress Control\u003c/strong\u003e: UDR routing all \u003ccode\u003e0.0.0.0/0\u003c/code\u003e traffic through the hub Firewall.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBaseline RBAC\u003c/strong\u003e: \u003ccode\u003eContributor\u003c/code\u003e for the workload team.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMonitoring\u003c/strong\u003e: Activity logs forwarded to the platform Log Analytics Workspace.\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"terraform-vending-module\"\u003eTerraform Vending Module\u003c/h2\u003e\n\u003ch3 id=\"subscription-and-propagation\"\u003eSubscription and Propagation\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azurerm_subscription\u0026#34; \u0026#34;workload\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  subscription_name \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003elocal\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003esubscription_display_name\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  alias             \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003elocal\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003esubscription_display_name\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e # Idempotent anchor\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  billing_scope_id  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003ebilling_scope\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  workload          \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e var.environment \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;sandbox\u0026#34; ? \u0026#34;DevTest\u0026#34; : \u0026#34;Production\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Wait for Entra directory replication (90s)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;time_sleep\u0026#34; \u0026#34;wait_for_propagation\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  create_duration \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;90s\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  depends_on      \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#66d9ef\"\u003eazurerm_subscription\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eworkload\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"spoke-networking\"\u003eSpoke Networking\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;spoke_vnet\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  source  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Azure/avm-res-network-virtualnetwork/azurerm\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;~\u0026gt; 0.7\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  providers \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e { azurerm \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eazurerm\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eworkload\u003c/span\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name                \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;vnet-${var.workload_name}-001\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  address_space       \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003evnet_address_prefix\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  subnets \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    workload \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      name                    \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;snet-workload-001\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      address_prefixes        \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#66d9ef\"\u003ecidrsubnet\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003evnet_address_prefix\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e, \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e)]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      route_table_resource_id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003espoke_udr\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eresource_id\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e # Correct attribute\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"bicep-vending-module\"\u003eBicep Vending Module\u003c/h2\u003e\n\u003cp\u003eUse the official AVM pattern module \u003ccode\u003ebr/public:avm/ptn/lz/sub-vending\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bicep\" data-lang=\"bicep\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e subVend \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;br/public:avm/ptn/lz/sub-vending:0.3.0\u0026#39;\u003c/span\u003e = {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;subVend-\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e${\u003c/span\u003eworkloadName\u003cspan style=\"color:#e6db74\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  params: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    subscriptionAliasEnabled: \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    subscriptionDisplayName: subscriptionDisplayName\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    subscriptionAliasName: subscriptionDisplayName\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    subscriptionBillingScope: billingScope\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    subscriptionManagementGroupId: targetManagementGroupId\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    virtualNetworkEnabled: \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    virtualNetworkAddressSpace: [vnetAddressPrefix]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    hubNetworkResourceId: hubVnetResourceId\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"pr-approval-workflow\"\u003ePR Approval Workflow\u003c/h2\u003e\n\u003cp\u003eThe workflow uses GitHub Actions to validate requests before they reach a platform engineer.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eValidation Stage:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eSchema Check\u003c/strong\u003e: Validates the YAML request.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCIDR Overlap\u003c/strong\u003e: Checks \u003ccode\u003eipam.yaml\u003c/code\u003e to ensure no address space conflicts.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePlan Preview\u003c/strong\u003e: Posts the \u003ccode\u003eterraform plan\u003c/code\u003e output as a PR comment.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003eApply Stage:\u003c/strong\u003e\nExecutes the deployment only after the PR is merged to \u003ccode\u003emain\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFixing GHA Path Parsing:\u003c/strong\u003e\nEnsure your GitHub Script correctly parses multiline file lists:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// Correct regex splitting for multiline git diff output\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efiles\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;${{ steps.changed.outputs.files }}\u0026#39;\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003esplit\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e/\\s+/\u003c/span\u003e).\u003cspan style=\"color:#a6e22e\"\u003efilter\u003c/span\u003e(Boolean);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre class=\"mermaid\"\u003e\n  graph TD\n    User[Workload Team] -- Submit YAML --\u0026gt; PR[Pull Request]\n    subgraph CI_Validation [Automation Pipeline]\n        PR -- Trigger --\u0026gt; Valid[Schema \u0026amp; CIDR Validation]\n        Valid -- Plan --\u0026gt; Preview[Terraform Plan / What-If]\n    end\n    Preview -- Comment --\u0026gt; Approver[Platform Engineer]\n    Approver -- Merge --\u0026gt; Deploy[Deployment Phase]\n    subgraph Deployment [Azure Infrastructure]\n        Deploy --\u0026gt; Sub[Create Subscription]\n        Sub --\u0026gt; Net[VNet \u0026amp; Peering]\n        Net --\u0026gt; Gov[Apply Policy \u0026amp; RBAC]\n    end\n    Gov --\u0026gt; Notify[Notify Team: Ready]\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eNotes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePR-based self-service\u003c/strong\u003e allows teams to request resources without manual ticketing delays.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eValidation\u003c/strong\u003e steps prevent IP address overlaps and configuration errors before deployment.\u003c/li\u003e\n\u003cli\u003eThe final \u003cstrong\u003eNotification\u003c/strong\u003e provides the workload team with the credentials/access to their new, compliant environment.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eIsolate State\u003c/strong\u003e: Use one Terraform state file per subscription to isolate the blast radius of failures.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMandatory Budgets\u003c/strong\u003e: Make budget alerts a required field in the request schema.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLog Category Support\u003c/strong\u003e: When configuring diagnostic settings, ensure the requested categories (like \u003ccode\u003eAdministrative\u003c/code\u003e) are supported at the subscription level to avoid deployment warnings.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIPAM Source of Truth\u003c/strong\u003e: Update your \u003ccode\u003eipam.yaml\u003c/code\u003e in the same pipeline run that provisions the subscription to prevent race conditions.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/design-area/subscription-vending\"\u003eCAF Subscription Vending\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/Azure/bicep-lz-vending\"\u003eBicep LZ Vending Module (GitHub)\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits\"\u003eAzure Subscription Limits\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNext, move to \u003ca href=\"../azure-monitor-centralized-logging/\"\u003ePost 6: Centralized Monitoring\u003c/a\u003e. We will deploy the Log Analytics Workspace that receives the activity logs from these vended subscriptions.\u003c/p\u003e\n","description":"Automate Azure subscription provisioning with a PR-based vending workflow. Deploy spoke networking, RBAC, and monitoring baselines using Terraform and Bicep.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/azure-subscription-vending-automation/","title":"Azure Subscription Vending: Automated Workload Onboarding"},{"content":"\u003cp\u003eIn a distributed cloud environment, what you cannot see will eventually break your environment. A networking issue traced to a misconfigured firewall rule that has been dropping traffic for three weeks. A security breach that went undetected because no diagnostic settings were configured on an accessed Key Vault. A cost spike caught only when the invoice arrived.\u003c/p\u003e\n\u003cp\u003eAs an organization scales, logs scatter across hundreds of resources. Without a centralized strategy, troubleshooting becomes a manual search across disconnected data sources. This guide implements a single-pane-of-glass observability platform.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eImplement a Management subscription as the telemetry hub.\u003c/li\u003e\n\u003cli\u003eDeploy a production Log Analytics Workspace (LAW) with optimized retention tiers.\u003c/li\u003e\n\u003cli\u003eAutomate log collection using \u003ccode\u003eDeployIfNotExists\u003c/code\u003e (DINE) Azure Policy.\u003c/li\u003e\n\u003cli\u003eReduce ingestion costs using Data Collection Rules (DCR).\u003c/li\u003e\n\u003cli\u003eBuild interactive dashboards with Azure Monitor Workbooks.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 6 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"centralized-logging-architecture\"\u003eCentralized Logging Architecture\u003c/h2\u003e\n\u003cpre class=\"mermaid\"\u003e\n  graph TD\n    subgraph Platform_Mgmt [Management Subscription]\n        LAW[Central Log Analytics Workspace]\n        Sentinel[Microsoft Sentinel SIEM]\n        Workbooks[Azure Monitor Workbooks]\n        Sentinel --- LAW\n        Workbooks --- LAW\n    end\n\n    subgraph Spoke_A [Spoke Subscription A]\n        VM_A[VMs] -- Diag Settings --\u0026gt; LAW\n        SQL_A[SQL DB] -- Diag Settings --\u0026gt; LAW\n    end\n\n    subgraph Spoke_B [Spoke Subscription B]\n        KV_B[Key Vault] -- Diag Settings --\u0026gt; LAW\n        VNet_B[VNet Flow Logs] -- Diag Settings --\u0026gt; LAW\n    end\n\n    subgraph Connectivity [Connectivity Subscription]\n        FW[Azure Firewall] -- Diag Settings --\u0026gt; LAW\n    end\n\n    style LAW fill:#e3f2fd,stroke:#1565c0,stroke-width:4px\n    style Sentinel fill:#fff9c4,stroke:#fbc02d\n    style Platform_Mgmt fill:#f5f5f5,stroke:#9e9e9e\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eNotes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDiagnostic Settings\u003c/strong\u003e are the mechanism used to ship logs from resources to the central workspace.\u003c/li\u003e\n\u003cli\u003eThe \u003cstrong\u003eManagement Subscription\u003c/strong\u003e acts as the telemetry hub for the entire landing zone.\u003c/li\u003e\n\u003cli\u003eCentralization allows for \u003cstrong\u003ecross-resource correlation\u003c/strong\u003e and simplified compliance reporting.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"the-management-subscription-hub\"\u003eThe Management Subscription Hub\u003c/h3\u003e\n\u003cp\u003eCentralizing telemetry in the Management subscription provides:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eCross-subscription Querying\u003c/strong\u003e: Correlate events from Firewall, Key Vault, and VMs in a single KQL query.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSimplified Compliance\u003c/strong\u003e: Manage retention policies (e.g., PCI-DSS 12-month requirements) in one place.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRBAC Isolation\u003c/strong\u003e: Grant SOC teams access to the platform logs without exposing workload-specific application data.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"log-analytics-table-plans-2026\"\u003eLog Analytics Table Plans (2026)\u003c/h3\u003e\n\u003cp\u003eAzure Monitor now uses a table-plan-based model to optimize costs:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003ePlan\u003c/th\u003e\n          \u003cth\u003ePrice (est.)\u003c/th\u003e\n          \u003cth\u003eBest For\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eAnalytics\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e$2.30/GB\u003c/td\u003e\n          \u003ctd\u003eSecurity logs, audit events, firewall rules (Full KQL).\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eBasic\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e$0.50/GB\u003c/td\u003e\n          \u003ctd\u003eVerbose application traces and debug logs.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eAuxiliary\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e$0.05/GB\u003c/td\u003e\n          \u003ctd\u003eLong-term archiving (Search jobs only).\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003ePro Tip\u003c/strong\u003e: Set a daily ingestion cap (minimum 0.01 GB) and an alert at 80% of that cap to prevent runaway costs from misconfigured resources.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"deploying-the-stack-with-terraform-avm\"\u003eDeploying the Stack with Terraform AVM\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003emain.tf\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;log_analytics_workspace\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  source  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Azure/avm-res-operationalinsights-workspace/azurerm\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  version \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;0.5.1\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name                \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;law-platform-central-001\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  retention_in_days   \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e90\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  daily_quota_gb      \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e50\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  sku                 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;PerGB2018\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e # Analytics Tier\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# DCR to filter expensive east-west firewall traffic\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azurerm_monitor_data_collection_rule\u0026#34; \u0026#34;firewall\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name     \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;dcr-firewall-filtered\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  location \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003elocation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003edata_flow\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    streams      \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Microsoft-CommonSecurityLog\u0026#34;\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    destinations \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;platform-law\u0026#34;\u003c/span\u003e]\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e    # Drop internal-to-internal allows; keep internet egress and all denials\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    transform_kql \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;source | where not(SourceIP matches regex @\u0026#39;^10\\\\.\u0026#39; and DestinationIP matches regex @\u0026#39;^10\\\\.\u0026#39;)\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cem\u003eNote: Starting in late 2025, Microsoft introduced transformation charges for data filtered over 50%. Verify current regional thresholds on the Azure Monitor pricing page.\u003c/em\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"automating-collection-via-azure-policy\"\u003eAutomating Collection via Azure Policy\u003c/h2\u003e\n\u003cp\u003eManual configuration does not scale. Use a \u003ccode\u003eDeployIfNotExists\u003c/code\u003e (DINE) policy initiative at the \u003ccode\u003eLanding Zones\u003c/code\u003e management group to ensure every new resource automatically forwards logs to your central LAW.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCritical Step\u003c/strong\u003e: Grant the policy assignment\u0026rsquo;s managed identity the \u003cstrong\u003eLog Analytics Contributor\u003c/strong\u003e role at the management group scope. Without this, remediation tasks will fail.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"visualizing-platform-health\"\u003eVisualizing Platform Health\u003c/h2\u003e\n\u003cp\u003eUse \u003cstrong\u003eAzure Monitor Workbooks\u003c/strong\u003e to turn raw data into insights.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRBAC Change Detection (KQL)\u003c/strong\u003e:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode class=\"language-kusto\" data-lang=\"kusto\"\u003eAzureActivity\n| where TimeGenerated \u0026gt; ago(24h)\n| where OperationNameValue == \u0026#34;MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE\u0026#34;\n| where ActivityStatusValue == \u0026#34;Success\u0026#34;\n| extend\n    // Pro Tip: In modern workspaces, use \u0026#39;Properties_d\u0026#39; directly if available\n    AssignedRole = tostring(parse_json(tostring(parse_json(Properties).responseBody)).properties.roleDefinitionId),\n    AssignedTo   = tostring(parse_json(tostring(parse_json(Properties).responseBody)).properties.principalId),\n    CallerIP     = CallerIpAddress\n| project TimeGenerated, Caller, AssignedRole, AssignedTo, CallerIP\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAnalytics for Security\u003c/strong\u003e: Never put security logs (Firewall, Key Vault, Activity Log) in the Basic tier. Basic logs do not support the \u003ccode\u003ejoin\u003c/code\u003e operator, which is required for incident investigation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFilter at Ingestion\u003c/strong\u003e: Use DCR transforms to drop noisy \u003ccode\u003eAZFWFlowTrace\u003c/code\u003e or internal \u003ccode\u003eAllow\u003c/code\u003e logs before they reach the workspace.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCommitment Tiers\u003c/strong\u003e: If you ingest more than 100 GB/day, switch to a commitment tier to save ~15%.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"troubleshooting\"\u003eTroubleshooting\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;Logs are missing after remediation\u0026rdquo;\u003c/strong\u003e\nVerify the managed identity permissions. Run:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eaz role assignment list --assignee \u0026lt;policy-identity-id\u0026gt; --scope \u0026lt;mg-id\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e\u0026ldquo;KQL Error: join operation requires analytics logs\u0026rdquo;\u003c/strong\u003e\nYou are trying to join a table in the \u003cstrong\u003eBasic\u003c/strong\u003e plan. Move the table to \u003cstrong\u003eAnalytics\u003c/strong\u003e if full KQL capability is required.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/azure-monitor/best-practices\"\u003eAzure Monitor Best Practices\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/azure-monitor/logs/table-plans\"\u003eLog Analytics Table Plans\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/data-collection-rule-transformations\"\u003eAzure Monitor Ingestion-time Transformations\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNext, move to \u003ca href=\"../azure-security-baseline-guide/\"\u003ePost 7: Security Baseline\u003c/a\u003e. Sentinel and Defender for Cloud will use the workspace deployed here to provide centralized threat detection.\u003c/p\u003e\n","description":"Design a centralized logging architecture for your Azure Landing Zone using Log Analytics, automated Diagnostic Settings via Policy, and Monitor Workbooks.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/azure-monitor-centralized-logging/","title":"Azure Centralized Monitoring: Log Analytics and Workbooks"},{"content":"\u003cp\u003eDeploying your landing zone from a local terminal is a single point of failure. When an engineer\u0026rsquo;s laptop holds the Terraform state, or when \u0026ldquo;just a quick change\u0026rdquo; bypasses review, you no longer have a governed foundation. You have an undocumented configuration that cannot be reliably reconstructed.\u003c/p\u003e\n\u003cp\u003eEnterprise landing zones require version-controlled pipelines. Every change to your management group hierarchy, hub networking, or identity model must be previewed and gated before reaching production. A CI/CD pipeline is the enforcement mechanism that makes your cloud governance model real.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eImplement a PR-driven \u0026ldquo;plan-first\u0026rdquo; workflow.\u003c/li\u003e\n\u003cli\u003eConfigure OIDC-based, secret-less authentication to Azure.\u003c/li\u003e\n\u003cli\u003eAutomate security scanning (Checkov) and WAF compliance (PSRule).\u003c/li\u003e\n\u003cli\u003eDesign sequential deployment layers with environment approval gates.\u003c/li\u003e\n\u003cli\u003eSecure state management for Terraform and Bicep.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 8 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"repository-structure-and-strategy\"\u003eRepository Structure and Strategy\u003c/h2\u003e\n\u003ch3 id=\"organizing-iac-by-layer\"\u003eOrganizing IaC by Layer\u003c/h3\u003e\n\u003cp\u003eA mono-repo simplifies dependency tracking and centralizes branch protection. Organize your platform code by deployment layer:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003elanding-zone/\n├── .github/workflows/\n│   ├── validate.yml          # Pull request validation\n│   └── deploy.yml            # Main branch deployment\n├── live/                     # Environment configurations\n│   ├── layer-0-management/   # Hierarchy and Policies\n│   ├── layer-1-connectivity/ # Hub Networking\n│   └── layer-2-identity/     # Entra ID and PIM\n└── modules/                  # Reusable AVM wrappers\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eKeep application-specific code in separate workload repositories. The platform pipeline deploys the foundation; workload pipelines deploy into it.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"authentication-oidc-with-federated-credentials\"\u003eAuthentication: OIDC with Federated Credentials\u003c/h2\u003e\n\u003cp\u003eGitHub Actions connects to Azure using \u003cstrong\u003eOpenID Connect (OIDC)\u003c/strong\u003e. This eliminates the need for long-lived client secrets. GitHub issues a short-lived token for each run, which Entra ID exchanges for an Azure access token.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFederated Credential (2026)\u003c/strong\u003e:\nIn 2026, Azure supports \u003cstrong\u003eFlexible Federation\u003c/strong\u003e (wildcards). A single credential can cover all environments in a repo using \u003ccode\u003erepo:org/name:*\u003c/code\u003e.\n\u003cem\u003eNote: Wildcard syntax may require the \u003ccode\u003eclaimsMatchingExpression\u003c/code\u003e property in your App Registration.\u003c/em\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"the-pull-request-workflow\"\u003eThe Pull Request Workflow\u003c/h2\u003e\n\u003ch3 id=\"validation-and-scanning\"\u003eValidation and Scanning\u003c/h3\u003e\n\u003cp\u003eEvery PR triggers \u003ccode\u003evalidate.yml\u003c/code\u003e. The pipeline enforces standards, allowing human review to focus on architecture.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eKey Steps\u003c/strong\u003e:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eTerraform fmt\u003c/strong\u003e: Enforces consistent code style.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCheckov\u003c/strong\u003e: Scans for security misconfigurations (e.g., unencrypted storage).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePSRule for Azure\u003c/strong\u003e: Validates WAF compliance before deployment.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"plan-previews\"\u003ePlan Previews\u003c/h3\u003e\n\u003cp\u003eThe pipeline runs \u003ccode\u003eterraform plan\u003c/code\u003e or \u003ccode\u003eaz deployment mg what-if\u003c/code\u003e and posts the diff as a collapsible PR comment. Reviewers see the exact resource delta without leaving GitHub.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e- \u003cspan style=\"color:#f92672\"\u003ename\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ePost Plan to PR\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003euses\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eactions/github-script@v8\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 2026 Stable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003ewith\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003escript\u003c/span\u003e: |\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      const planOutput = fs.readFileSync(\u0026#39;plan.txt\u0026#39;, \u0026#39;utf8\u0026#39;);\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      const body = `## Terraform Plan\\n\u0026lt;details\u0026gt;\u0026lt;summary\u0026gt;Show Plan\u0026lt;/summary\u0026gt;\\n\\n\\`\\`\\`\\n${planOutput}\\n\\`\\`\\`\\n\u0026lt;/details\u0026gt;`;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      await github.rest.issues.createComment({ ...context.issue, body });\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cpre class=\"mermaid\"\u003e\n  graph TD\n    subgraph GitHub_Repo [GitHub Repository]\n        PR[Pull Request] -- Merge --\u0026gt; Main[Main Branch]\n    end\n\n    subgraph CI_Pipeline [CI/CD Workflow]\n        direction TB\n        L0[Layer 0: Management Group Hierarchy]\n        L1[Layer 1: Connectivity - Hub VNet/Firewall]\n        L2[Layer 2: Identity - PIM/RBAC]\n        L3[Layer 3: Governance - Azure Policy]\n    end\n\n    Main --\u0026gt; L0\n    L0 -- Success --\u0026gt; Gate1{Gate: Hub-Prod Approval}\n    Gate1 -- Approved --\u0026gt; L1\n    L1 -- Success --\u0026gt; Gate2{Gate: Identity-Prod Approval}\n    Gate2 -- Approved --\u0026gt; L2\n    L2 -- Success --\u0026gt; L3\n\n    style L0 fill:#e1f5fe,stroke:#01579b\n    style L1 fill:#e1f5fe,stroke:#01579b\n    style L2 fill:#e1f5fe,stroke:#01579b\n    style Gate1 fill:#fff9c4,stroke:#fbc02d\n    style Gate2 fill:#fff9c4,stroke:#fbc02d\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eNotes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eLayered Deployment\u003c/strong\u003e prevents dependency cycles and ensures a logical build order.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eManual Gates\u003c/strong\u003e (Environment approvals) provide human oversight before changes are applied to production infrastructure.\u003c/li\u003e\n\u003cli\u003eThe \u003cstrong\u003eSequential Flow\u003c/strong\u003e ensures that the management group hierarchy (Layer 0) is established before attempting to deploy networking or identity resources into those groups.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"deployment-execution\"\u003eDeployment Execution\u003c/h2\u003e\n\u003ch3 id=\"sequential-layers-and-gates\"\u003eSequential Layers and Gates\u003c/h3\u003e\n\u003cp\u003eLanding zone layers have hard dependencies. You cannot deploy networking before the management group hierarchy exists. Use \u003ccode\u003eenvironment\u003c/code\u003e gates to enforce human approval for production layers.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003edeploy-layer-1\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eneeds\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003edeploy-layer-0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eenvironment\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ehub-prod\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# Approval gate\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003esteps\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    - \u003cspan style=\"color:#f92672\"\u003euses\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eazure/login@v3\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 2026 Stable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003ewith\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003eclient-id\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${{ vars.AZURE_CLIENT_ID }}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e# ... OIDC login\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"state-security\"\u003eState Security\u003c/h3\u003e\n\u003cp\u003eStore Terraform state in an Azure Storage Account with:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSoft Delete\u003c/strong\u003e: Minimum 7-day retention.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVersioning\u003c/strong\u003e: Recovery from state corruption.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRBAC-only Access\u003c/strong\u003e: Disable Shared Key access to force the pipeline\u0026rsquo;s identity to use RBAC.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePin Action Versions\u003c/strong\u003e: Use \u003ccode\u003eazure/login@v3\u003c/code\u003e, not \u003ccode\u003e@latest\u003c/code\u003e, to prevent silent pipeline breakages.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConcurrency Groups\u003c/strong\u003e: Use GitHub\u0026rsquo;s \u003ccode\u003econcurrency\u003c/code\u003e feature to prevent parallel runs from corrupting the same state file.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSelf-hosted Runners\u003c/strong\u003e: Consider self-hosted runners if your state storage account requires Private Link connectivity for enhanced security.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure\"\u003eConfiguring OIDC in Azure (GitHub)\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://azure.github.io/Azure-Verified-Modules/\"\u003eAzure Verified Modules\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://azure.github.io/PSRule.Rules.Azure/\"\u003ePSRule for Azure\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNext, move to \u003ca href=\"../landing-zone-cost-governance/\"\u003ePost 9: Azure Cost Governance: Tagging, Budgets, and FinOps\u003c/a\u003e. We will establish financial guardrails and automated cost reporting for your landing zone.\u003c/p\u003e\n","description":"Build a production-grade CI/CD pipeline for your Azure Landing Zone. Automate AVM module deployment using GitHub Actions with Terraform plans and Bicep previews.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/landing-zone-cicd-pipeline/","title":"CI/CD for Azure Landing Zones: GitHub Actions \u0026 AVM"},{"content":"\u003cp\u003e\u0026ldquo;Unlimited scalability\u0026rdquo; is a selling point for developers and a liability for finance teams — unless the platform enforces guardrails before spend happens. Most organizations discover this mismatch via a surprise bill at month-end, followed by a scramble to identify who created the expensive resources.\u003c/p\u003e\n\u003cp\u003eIn an enterprise landing zone, cost governance cannot be reactive. If a resource lacks a required tag, ARM should reject the deployment. If a subscription approaches its budget ceiling, the platform team should know before it crosses. This guide implements the automated controls required to prevent cloud sprawl.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eImplement Deny policies to block untagged resource creation.\u003c/li\u003e\n\u003cli\u003eAutomate tag inheritance from Resource Groups using Modify policies.\u003c/li\u003e\n\u003cli\u003eDeploy subscription-level budgets with forecasted alerts as code.\u003c/li\u003e\n\u003cli\u003eConfigure FOCUS v1.3 cost exports for cross-cloud reporting.\u003c/li\u003e\n\u003cli\u003eQuery cost anomalies using KQL.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 9 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"tagging-enforcement-the-foundation-of-accountability\"\u003eTagging Enforcement: The Foundation of Accountability\u003c/h2\u003e\n\u003cp\u003eEvery FinOps capability — chargeback, showback, and cost allocation — depends on tags. A resource without a \u003ccode\u003eCostCenter\u003c/code\u003e tag cannot be mapped to a business unit.\u003c/p\u003e\n\u003ch3 id=\"deny-policies-for-untagged-resources\"\u003eDeny Policies for Untagged Resources\u003c/h3\u003e\n\u003cp\u003eA Deny policy evaluates the request before the resource is created. The \u003ccode\u003emode: 'Indexed'\u003c/code\u003e setting is critical; it ensures the policy only evaluates resource types that support tags, avoiding errors on internal Azure resources like managed disks or NICs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBicep: CostCenter Enforcement\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bicep\" data-lang=\"bicep\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e requireCostCenter \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;Microsoft.Authorization/policyDefinitions@2021-06-01\u0026#39;\u003c/span\u003e = {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;require-costcenter-tag\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  properties: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    displayName: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;Require CostCenter tag\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    mode: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;Indexed\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    policyRule: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e: { field: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;tags[\\\u0026#39;CostCenter\\\u0026#39;]\u0026#39;\u003c/span\u003e, exists: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;false\u0026#39;\u003c/span\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      then: { effect: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;Deny\u0026#39;\u003c/span\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"tag-inheritance-via-modify-policy\"\u003eTag Inheritance via Modify Policy\u003c/h3\u003e\n\u003cp\u003eUse a \u003ccode\u003eModify\u003c/code\u003e policy to automatically copy tags from a Resource Group to its child resources. This requires granting the policy identity the \u003cstrong\u003eTag Contributor\u003c/strong\u003e role (\u003ccode\u003e4a9aeed2-a923-413d-95c2-739962ca585d\u003c/code\u003e) at the target scope.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"automated-budgeting-and-alerts\"\u003eAutomated Budgeting and Alerts\u003c/h2\u003e\n\u003cp\u003eEvery vended subscription should receive a default budget at creation. Configure the \u003cstrong\u003e80% Forecasted\u003c/strong\u003e alert as your primary operational trigger. Forecasted alerts fire when the projected month-end cost will exceed the threshold, giving you days to act rather than hours.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTerraform: Monthly Budget\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eresource\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;azurerm_consumption_budget_subscription\u0026#34; \u0026#34;default\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name            \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;${var.subscription_name}-budget\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  subscription_id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003esubscription_id\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  amount          \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1000\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  time_grain      \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Monthly\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003etime_period\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    start_date \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eformatdate\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;YYYY-MM-01\u0026#39;T\u0026#39;00:00:00Z\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#66d9ef\"\u003etimestamp\u003c/span\u003e())\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003enotification\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    threshold      \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e80\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    operator       \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;GreaterThan\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    threshold_type \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Forecasted\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    contact_emails \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [\u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eadmin_email\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cem\u003eNote: Cost Management data has an 8–24 hour lag. Alerts evaluate against the previous day\u0026rsquo;s billing data.\u003c/em\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"finops-automation-and-visibility\"\u003eFinOps Automation and Visibility\u003c/h2\u003e\n\u003ch3 id=\"cost-anomaly-detection-kql\"\u003eCost Anomaly Detection (KQL)\u003c/h3\u003e\n\u003cp\u003eIdentify Resource Groups with the largest day-over-day cost increase.\n\u003cem\u003eRequirement: Enable the Azure Cost Management connector for Log Analytics.\u003c/em\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode class=\"language-kusto\" data-lang=\"kusto\"\u003eUsageDetails\n| where TimeGenerated \u0026gt; ago(7d)\n| summarize DailyCost = sum(PreTaxCost) by bin(TimeGenerated, 1d), ResourceGroup\n| serialize\n| extend PrevDailyCost = prev(DailyCost)\n| extend CostChange = DailyCost - PrevDailyCost\n| where CostChange \u0026gt; 100 // Flag $100+/day increases\n| project Date = TimeGenerated, ResourceGroup, DeltaUSD = round(CostChange, 2)\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"focus-v13-exports\"\u003eFOCUS v1.3 Exports\u003c/h3\u003e\n\u003cp\u003eExport your cost data in the \u003cstrong\u003eFOCUS\u003c/strong\u003e (FinOps Open Cost \u0026amp; Usage Specification) format for normalized cloud reporting. These exports must be configured at the \u003cstrong\u003eBilling Account\u003c/strong\u003e scope.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eaz costmanagement export create \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --name \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;platform-focus-export\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --scope \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/providers/Microsoft.Billing/billingAccounts/12345\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --type FocusCost \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --dataset-configuration version\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;1.3\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --recurrence \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Daily\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eLimit Mandatory Tags\u003c/strong\u003e: Enforce no more than 3–5 tags. Tag fatigue leads to teams entering \u0026ldquo;junk\u0026rdquo; data to bypass the policy.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eForecast Over Actual\u003c/strong\u003e: Always prioritize forecasted alerts for operations. Actual alerts at 100% only tell you that the budget has already been spent.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGrant Reader Access\u003c/strong\u003e: Grant your finance team the \u003ccode\u003eCost Management Reader\u003c/code\u003e role at the management group scope. This allows full visibility without infrastructure permissions.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://focus.finops.org/\"\u003eFinOps Open Cost \u0026amp; Usage Specification (FOCUS)\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/governance/policy/samples/built-in-policies#tags\"\u003eAzure Policy Tag Samples\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/rest/api/consumption/budgets\"\u003eAzure Budget API Reference\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNext, proceed to \u003ca href=\"../landing-zone-day-2-ops/\"\u003ePost 10: Day-2 Operations\u003c/a\u003e. This final article covers maintaining and evolving your landing zone after initial deployment.\u003c/p\u003e\n","description":"Master Azure cost governance by automating tagging enforcement, budget alerts, and anomaly detection. Build a FinOps-ready landing zone using Terraform and Bicep.","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/landing-zone-cost-governance/","title":"Azure Cost Governance: Tagging, Budgets, and FinOps"},{"content":"\u003cp\u003eDeploying your landing zone was the easy part. Now you must operate it.\u003c/p\u003e\n\u003cp\u003eThe most common failure in platform engineering is treating the landing zone as a finished project rather than an ongoing product. Azure releases new services, requirements change, and teams drift from standards. Without an operational strategy that anticipates these pressures, your foundation becomes a collection of special cases and undocumented changes.\u003c/p\u003e\n\u003cp\u003eBy the end of this guide, you will:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAutomate drift detection with scheduled GitHub Actions.\u003c/li\u003e\n\u003cli\u003eRemediate non-compliance via Policy without manual intervention.\u003c/li\u003e\n\u003cli\u003eMigrate from legacy modules to Azure Verified Modules (AVM) safely.\u003c/li\u003e\n\u003cli\u003eImplement quarterly identity and networking reviews.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is Post 10 in the \u003ca href=\"/series/azure-platform-engineering-build-an-enterprise-landing-zone-from-scratch/\"\u003eAzure Platform Engineering series\u003c/a\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"managing-configuration-drift\"\u003eManaging Configuration Drift\u003c/h2\u003e\n\u003cpre class=\"mermaid\"\u003e\n  graph TD\n    subgraph Continuous_Cycle [Day-2 Operations Lifecycle]\n        Deploy[Initial Deployment] --\u0026gt; Audit[Automated Drift Audit]\n        Audit --\u0026gt; Detect{Drift Detected?}\n        Detect -- Yes --\u0026gt; Remediate[Policy Remediation / IaC Apply]\n        Detect -- No --\u0026gt; Evolve[Review \u0026amp; Evolve]\n        Remediate --\u0026gt; Audit\n        Evolve --\u0026gt; Update[Migrate to AVM / New Services]\n        Update --\u0026gt; Audit\n    end\n\n    subgraph Tools [Operations Stack]\n        GHA[GitHub Actions: Scheduled Scans]\n        AzPolicy[Azure Policy: Compliance Store]\n        AzPIM[Entra ID: Access Reviews]\n    end\n\n    Audit --- GHA\n    Detect --- AzPolicy\n    Evolve --- AzPIM\n\n    style Continuous_Cycle fill:#f5f5f5,stroke:#9e9e9e\n    style Remediate fill:#e8f5e9,stroke:#2e7d32\n    style Detect fill:#fff9c4,stroke:#fbc02d\n\u003c/pre\u003e\n\n\u003cp\u003e\u003cstrong\u003eVisual Notes:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eContinuous Auditing\u003c/strong\u003e ensures that manual \u0026ldquo;out-of-band\u0026rdquo; changes are detected and documented.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePolicy Remediation\u003c/strong\u003e allows the platform to self-heal without manual engineering effort.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePeriodic Evolution\u003c/strong\u003e (Review/Evolve) incorporates new Azure features and module updates (like AVM) into the established foundation.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"detecting-state-and-policy-drift\"\u003eDetecting State and Policy Drift\u003c/h3\u003e\n\u003cp\u003eDrift takes two forms: IaC state drift (the gap between code and reality) and Policy drift (compliance violations).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTerraform Refresh-Only\u003c/strong\u003e:\nDetect manual portal changes without modifying resources:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eterraform plan --refresh-only -no-color 2\u0026gt;\u0026amp;\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e | tee drift-output.txt\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003ePowerShell Policy Audit\u003c/strong\u003e:\nIdentify non-compliant resources across your hierarchy:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$nonCompliant = Get-AzPolicyState -ManagementGroupName \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;mg-intermediate\u0026#34;\u003c/span\u003e -Filter \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;complianceState eq \u0026#39;NonCompliant\u0026#39;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$nonCompliant | Group-Object -Property policyDefinitionName | Select-Object Name, Count\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"scheduling-scans-in-github-actions\"\u003eScheduling Scans in GitHub Actions\u003c/h3\u003e\n\u003cp\u003eRun drift scans weekly and automatically open a GitHub Issue when deviations are found. This ensures drift is triaged during sprint planning rather than accumulating indefinitely.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"the-migration-path-moving-to-avm\"\u003eThe Migration Path: Moving to AVM\u003c/h2\u003e\n\u003ch3 id=\"safe-refactoring-with-moved-blocks\"\u003eSafe Refactoring with \u003ccode\u003emoved\u003c/code\u003e Blocks\u003c/h3\u003e\n\u003cp\u003eThe legacy CAF Terraform module is archived as of \u003cstrong\u003eAugust 2026\u003c/strong\u003e. Migrating to AVM modules is a requirement for continued support. Use the \u003ccode\u003emoved\u003c/code\u003e block to remap resource addresses without destruction:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emoved\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  from \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eenterprise_scale\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003eazurerm_management_group\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003elevel_1\u003c/span\u003e[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;mg-platform\u0026#34;\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  to   \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003emanagement_groups_avm\u003c/span\u003e[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;mg-platform\u0026#34;\u003c/span\u003e].\u003cspan style=\"color:#66d9ef\"\u003eazurerm_management_group\u003c/span\u003e.\u003cspan style=\"color:#66d9ef\"\u003ethis\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"bicep-avm-subnets\"\u003eBicep AVM subnets\u003c/h3\u003e\n\u003cp\u003eWhen moving to Bicep AVM, ensure your template matches the new schema:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bicep\" data-lang=\"bicep\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003emodule\u003c/span\u003e hubVnet \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;br/public:avm/res/network/virtual-network:0.10.0\u0026#39;\u003c/span\u003e = {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  name: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;hub-vnet-avm\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  params: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    name: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;conn-hub-vnet\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// AVM uses an array of objects for subnets\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    subnets: [\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      { name: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;AzureFirewallSubnet\u0026#39;\u003c/span\u003e, addressPrefix: \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;10.0.0.0/26\u0026#39;\u003c/span\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003chr\u003e\n\u003ch2 id=\"governance-and-identity-lifecycle\"\u003eGovernance and Identity Lifecycle\u003c/h2\u003e\n\u003ch3 id=\"quarterly-rbac-and-pim-reviews\"\u003eQuarterly RBAC and PIM Reviews\u003c/h3\u003e\n\u003cp\u003eLanding zones accumulate orphaned role assignments for deleted service principals. Run a quarterly audit to find and remove these entries.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAutomated Access Reviews\u003c/strong\u003e:\nConfigure Entra ID to automatically revoke access if not explicitly approved by a lead:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eaz rest --method POST \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --uri \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --body \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;{\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    \\\u0026#34;displayName\\\u0026#34;: \\\u0026#34;Quarterly Platform Role Review\\\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    \\\u0026#34;scope\\\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      \\\u0026#34;query\\\u0026#34;: \\\u0026#34;/subscriptions?\\$filter=startsWith(displayName, \u0026#39;lz-\u0026#39;)\\\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      \\\u0026#34;queryType\\\u0026#34;: \\\u0026#34;MicrosoftGraph\\\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    \\\u0026#34;settings\\\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      \\\u0026#34;defaultDecision\\\u0026#34;: \\\u0026#34;Deny\\\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e      \\\u0026#34;autoApplyDecisionsEnabled\\\u0026#34;: true\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e  }\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"networking-evolution\"\u003eNetworking Evolution\u003c/h3\u003e\n\u003cp\u003eUpgrading Azure Firewall from Standard to Premium is a \u003cstrong\u003ezero-downtime\u003c/strong\u003e operation using the \u0026ldquo;Easy SKU change\u0026rdquo; method. Premium is required for TLS inspection and IDPS (Intrusion Detection and Prevention).\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"best-practices\"\u003eBest Practices\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAudit Before Deny\u003c/strong\u003e: Set new policies to \u003ccode\u003eAudit\u003c/code\u003e for 7 days before switching to \u003ccode\u003eDeny\u003c/code\u003e to avoid blocking active workloads.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBatch Remediation\u003c/strong\u003e: When fixing thousands of resources, batch your remediation tasks by resource group to avoid ARM API throttling.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCanary Subscriptions\u003c/strong\u003e: Test every AVM module upgrade in a \u003ccode\u003eSandbox\u003c/code\u003e subscription before applying it to the production management groups.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/governance-and-operations\"\u003eCAF Governance and Operations\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://developer.hashicorp.com/terraform/language/modules/develop/refactoring\"\u003eTerraform Refactoring Documentation\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://learn.microsoft.com/en-us/azure/governance/policy/how-to/remediate-resources\"\u003eAzure Policy Remediation\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eYou have completed the core series. Use the Day-2 Ops patterns established here to ensure your landing zone remains a reliable foundation for your application teams.\u003c/p\u003e\n","description":"Operate and evolve your Azure Landing Zone. Covers automated drift remediation, RBAC reviews, and migrating to Azure Verified Modules (AVM).","image":"images/featured.jpg","permalink":"https://larryjameshenry.com/posts/landing-zone-day-2-ops/","title":"Azure Landing Zone Day-2 Ops: Maintenance and Evolution"},{"content":"","description":"My gallery :earth_asia:","image":null,"permalink":"https://larryjameshenry.com/gallery/","title":"Image Gallery"},{"content":"\u003cp\u003eI am a Senior DevOps Engineer and Azure Solutions Architect specializing in Platform Engineering and PowerShell automation. My mission is to help organizations build governed, scalable, and production-ready Azure environments using modern Infrastructure as Code (IaC) patterns like Terraform and Bicep.\u003c/p\u003e\n\u003cp\u003eWith over a decade of experience in the Microsoft ecosystem, I focus on the \u0026ldquo;Day 0\u0026rdquo; to \u0026ldquo;Day 2\u0026rdquo; lifecycle of cloud infrastructure, ensuring that security, governance, and cost-optimization are baked into every deployment.\u003c/p\u003e\n\u003ch3 id=\"core-technical-expertise\"\u003eCore Technical Expertise:\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAzure Platform Engineering (CAF/ALZ)\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePowerShell \u0026amp; Automation\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTerraform \u0026amp; Bicep (AVM)\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDevOps CI/CD (GitHub Actions/ADOs)\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCloud Governance \u0026amp; Policy\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","description":"Senior DevOps Engineer and Azure Solutions Architect specializing in Platform Engineering and PowerShell automation.","image":"/images/larryjameshenry.png","permalink":"https://larryjameshenry.com/about/","title":"About Larry James Henry"}]