From e7fcb9535278c9b31c9f9d46ad87180fefb8019a Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Wed, 2 Sep 2026 13:44:58 +0200 Subject: [PATCH 1/4] feat: testing azure ref. arch. --- .gitignore | 5 + modules/azure/hub-network/backplane/README.md | 62 +++ modules/azure/hub-network/backplane/main.tf | 61 +++ .../azure/hub-network/backplane/outputs.tf | 23 + .../azure/hub-network/backplane/provider.tf | 7 + .../azure/hub-network/backplane/variables.tf | 41 ++ .../azure/hub-network/backplane/versions.tf | 10 + .../azure/hub-network/buildingblock/README.md | 81 ++++ .../azure/hub-network/buildingblock/logo.png | Bin 0 -> 4928 bytes .../azure/hub-network/buildingblock/main.tf | 78 ++++ .../hub-network/buildingblock/outputs.tf | 37 ++ .../hub-network/buildingblock/provider.tf | 5 + .../hub-network/buildingblock/variables.tf | 69 +++ .../hub-network/buildingblock/versions.tf | 10 + .../hub-network/meshstack_integration.tf | 285 ++++++++++++ modules/azure/meshstack_integration.tf | 214 ++++++--- .../spoke-network/meshstack_integration.tf | 5 + .../azure-landingzone/README.md | 137 ++++++ .../azure-landingzone/azure-landingzone.dot | 80 ++++ .../azure-landingzone/azure-landingzone.svg | 263 +++++++++++ .../azure-landingzone/bootstrap/README.md | 22 + .../azure-landingzone/bootstrap/main.tf | 82 ++++ .../modules/management-groups/README.md | 25 ++ .../modules/management-groups/main.tf | 49 ++ .../modules/management-groups/outputs.tf | 37 ++ .../modules/management-groups/variables.tf | 47 ++ .../modules/management-groups/versions.tf | 10 + .../azure-landingzone/bootstrap/outputs.tf | 8 + .../azure-landingzone/bootstrap/provider.tf | 10 + .../azure-landingzone/bootstrap/variables.tf | 49 ++ .../azure-landingzone/bootstrap/versions.tf | 14 + .../azure-landingzone/buildingblock/README.md | 110 +++++ .../buildingblock/SUMMARY.md.tftpl | 56 +++ .../azure-landingzone/buildingblock/logo.svg | 25 ++ .../azure-landingzone/buildingblock/main.tf | 273 +++++++++++ .../modules/es-policies/README.md | 20 + .../modules/es-policies/outputs.tf | 34 ++ .../resources.policy_assignments.tf | 84 ++++ .../resources.policy_definitions.tf | 50 +++ .../modules/es-policies/variables.tf | 20 + .../modules/es-policies/versions.tf | 10 + .../modules/management-groups/README.md | 25 ++ .../modules/management-groups/main.tf | 49 ++ .../modules/management-groups/outputs.tf | 37 ++ .../modules/management-groups/variables.tf | 47 ++ .../modules/management-groups/versions.tf | 10 + .../buildingblock/outputs.tf | 54 +++ .../allowed_locations.tmpl.json | 14 + .../deny_public_ip_nic.tmpl.json | 10 + .../allowed_locations.tmpl.json | 14 + .../allowed_locations_audit.tmpl.json | 14 + .../buildingblock/provider.tf | 11 + .../buildingblock/variables.tf | 224 +++++++++ .../buildingblock/versions.tf | 22 + .../azure-landingzone/logo.svg | 25 ++ .../meshstack_integration.tf | 425 ++++++++++++++++++ 56 files changed, 3437 insertions(+), 52 deletions(-) create mode 100644 modules/azure/hub-network/backplane/README.md create mode 100644 modules/azure/hub-network/backplane/main.tf create mode 100644 modules/azure/hub-network/backplane/outputs.tf create mode 100644 modules/azure/hub-network/backplane/provider.tf create mode 100644 modules/azure/hub-network/backplane/variables.tf create mode 100644 modules/azure/hub-network/backplane/versions.tf create mode 100644 modules/azure/hub-network/buildingblock/README.md create mode 100644 modules/azure/hub-network/buildingblock/logo.png create mode 100644 modules/azure/hub-network/buildingblock/main.tf create mode 100644 modules/azure/hub-network/buildingblock/outputs.tf create mode 100644 modules/azure/hub-network/buildingblock/provider.tf create mode 100644 modules/azure/hub-network/buildingblock/variables.tf create mode 100644 modules/azure/hub-network/buildingblock/versions.tf create mode 100644 modules/azure/hub-network/meshstack_integration.tf create mode 100644 reference-architectures/azure-landingzone/README.md create mode 100644 reference-architectures/azure-landingzone/azure-landingzone.dot create mode 100644 reference-architectures/azure-landingzone/azure-landingzone.svg create mode 100644 reference-architectures/azure-landingzone/bootstrap/README.md create mode 100644 reference-architectures/azure-landingzone/bootstrap/main.tf create mode 100644 reference-architectures/azure-landingzone/bootstrap/modules/management-groups/README.md create mode 100644 reference-architectures/azure-landingzone/bootstrap/modules/management-groups/main.tf create mode 100644 reference-architectures/azure-landingzone/bootstrap/modules/management-groups/outputs.tf create mode 100644 reference-architectures/azure-landingzone/bootstrap/modules/management-groups/variables.tf create mode 100644 reference-architectures/azure-landingzone/bootstrap/modules/management-groups/versions.tf create mode 100644 reference-architectures/azure-landingzone/bootstrap/outputs.tf create mode 100644 reference-architectures/azure-landingzone/bootstrap/provider.tf create mode 100644 reference-architectures/azure-landingzone/bootstrap/variables.tf create mode 100644 reference-architectures/azure-landingzone/bootstrap/versions.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/README.md create mode 100644 reference-architectures/azure-landingzone/buildingblock/SUMMARY.md.tftpl create mode 100644 reference-architectures/azure-landingzone/buildingblock/logo.svg create mode 100644 reference-architectures/azure-landingzone/buildingblock/main.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/es-policies/README.md create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/es-policies/outputs.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/es-policies/resources.policy_assignments.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/es-policies/resources.policy_definitions.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/es-policies/variables.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/es-policies/versions.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/management-groups/README.md create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/management-groups/main.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/management-groups/outputs.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/management-groups/variables.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/modules/management-groups/versions.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/outputs.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/policies/corp/policy_assignments/allowed_locations.tmpl.json create mode 100644 reference-architectures/azure-landingzone/buildingblock/policies/corp/policy_assignments/deny_public_ip_nic.tmpl.json create mode 100644 reference-architectures/azure-landingzone/buildingblock/policies/online/policy_assignments/allowed_locations.tmpl.json create mode 100644 reference-architectures/azure-landingzone/buildingblock/policies/sandbox/policy_assignments/allowed_locations_audit.tmpl.json create mode 100644 reference-architectures/azure-landingzone/buildingblock/provider.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/variables.tf create mode 100644 reference-architectures/azure-landingzone/buildingblock/versions.tf create mode 100644 reference-architectures/azure-landingzone/logo.svg create mode 100644 reference-architectures/azure-landingzone/meshstack_integration.tf diff --git a/.gitignore b/.gitignore index 391eecd9..e98b8dfd 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,8 @@ website/public/assets/*.json # Local terragrunt integration test files — not for committing terragrunt.hcl + +# Local dev scaffolding for reference architectures (provider blocks with secrets, local tfvars, module overrides) — not for committing +**/zz_local_dev* +**/zz_local_dev_override.tf +**/provider_tmp.tf diff --git a/modules/azure/hub-network/backplane/README.md b/modules/azure/hub-network/backplane/README.md new file mode 100644 index 00000000..44427f70 --- /dev/null +++ b/modules/azure/hub-network/backplane/README.md @@ -0,0 +1,62 @@ +# Azure Hub Network — Backplane + +Provisions the automation principal for the **Azure Hub Network** building block: a User-Assigned +Managed Identity (UAMI) federated to meshStack's workload identity federation, plus a custom role +definition and assignment at the connectivity scope that let it build and maintain the central hub. + +## What it provisions + +- **Resource group + UAMI** in the connectivity subscription (`subscription_id`), named after `name`. +- **Federated identity credentials** for the given WIF `subjects` so the building block run can + authenticate as the UAMI without any stored secret. +- **`-deploy` role definition + assignment** at `scope` (a management group or subscription — + typically the platform Connectivity scope), granting management of the hub resource group, the + hub vnet and its subnets, route tables, and the Azure Firewall with its public IPs. + +## Required permissions + +The identity applying this backplane needs, at `scope`, the ability to create custom role +definitions and role assignments (e.g. **Owner** or **User Access Administrator** + role definition +write), and **Managed Identity Contributor** in the connectivity subscription to create the UAMI. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [azurerm](#requirement\_azurerm) | >= 4.36.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azurerm_federated_identity_credential.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) | resource | +| [azurerm_resource_group.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_role_assignment.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_definition.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_user_assigned_identity.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [location](#input\_location) | Azure region for the UAMI resource group. | `string` | n/a | yes | +| [name](#input\_name) | Name for the building block identity, resource group and role definition. | `string` | n/a | yes | +| [scope](#input\_scope) | Connectivity scope where the hub network can be deployed (management group or subscription ID). The deploy role definition and assignment are applied here. | `string` | n/a | yes | +| [subscription\_id](#input\_subscription\_id) | Subscription (bare GUID) where the UAMI and its resource group are created. Typically the hub/connectivity subscription so the identity lives in a stable, platform-owned place. | `string` | n/a | yes | +| [workload\_identity\_federation](#input\_workload\_identity\_federation) | WIF issuer and subjects for federated authentication of the automation identity. |
object({
issuer = string
subjects = list(string)
})
| n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [identity](#output\_identity) | The managed identity used as the automation principal for this building block. | +| [role\_definition\_id](#output\_role\_definition\_id) | The ID of the role definition that enables deployment of the hub network to the connectivity scope. | +| [role\_definition\_name](#output\_role\_definition\_name) | The name of the role definition that enables deployment of the hub network to the connectivity scope. | +| [scope](#output\_scope) | The scope where the hub deploy role definition and role assignment are applied. | + \ No newline at end of file diff --git a/modules/azure/hub-network/backplane/main.tf b/modules/azure/hub-network/backplane/main.tf new file mode 100644 index 00000000..1f405a5e --- /dev/null +++ b/modules/azure/hub-network/backplane/main.tf @@ -0,0 +1,61 @@ +resource "azurerm_resource_group" "backplane" { + name = var.name + location = var.location +} + +resource "azurerm_user_assigned_identity" "backplane" { + name = var.name + location = var.location + resource_group_name = azurerm_resource_group.backplane.name +} + +resource "azurerm_federated_identity_credential" "backplane" { + for_each = { for i, s in var.workload_identity_federation.subjects : tostring(i) => s } + + name = "subject-${each.key}" + user_assigned_identity_id = azurerm_user_assigned_identity.backplane.id + audience = ["api://AzureADTokenExchange"] + issuer = var.workload_identity_federation.issuer + subject = each.value +} + +# +# Hub deploy role — grants the automation identity everything it needs to build and +# maintain the central hub in the connectivity scope: the hub resource group, the hub +# vnet and its subnets, the route table, and (optionally) the Azure Firewall with its +# public IPs. +# +resource "azurerm_role_definition" "backplane" { + name = "${var.name}-deploy" + description = "Enables deployment of the ${var.name} hub network building block to the connectivity scope" + scope = var.scope + + permissions { + actions = [ + # Register resource providers in Azure Resource Manager + "*/register/action", + "Microsoft.Resources/subscriptions/providers/read", + + # Hub resource group + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Resources/subscriptions/resourceGroups/write", + "Microsoft.Resources/subscriptions/resourceGroups/delete", + + # Hub virtual network + subnets + peering (spokes peer in from the outside) + "Microsoft.Network/virtualNetworks/*", + "Microsoft.Network/routeTables/*", + + # Azure Firewall + its public IPs + "Microsoft.Network/publicIPAddresses/*", + "Microsoft.Network/publicIPPrefixes/*", + "Microsoft.Network/azureFirewalls/*", + "Microsoft.Network/firewallPolicies/*", + ] + } +} + +resource "azurerm_role_assignment" "backplane" { + scope = var.scope + role_definition_id = azurerm_role_definition.backplane.role_definition_resource_id + principal_id = azurerm_user_assigned_identity.backplane.principal_id +} diff --git a/modules/azure/hub-network/backplane/outputs.tf b/modules/azure/hub-network/backplane/outputs.tf new file mode 100644 index 00000000..6d147332 --- /dev/null +++ b/modules/azure/hub-network/backplane/outputs.tf @@ -0,0 +1,23 @@ +output "identity" { + value = { + client_id = azurerm_user_assigned_identity.backplane.client_id + principal_id = azurerm_user_assigned_identity.backplane.principal_id + tenant_id = azurerm_user_assigned_identity.backplane.tenant_id + } + description = "The managed identity used as the automation principal for this building block." +} + +output "role_definition_id" { + value = azurerm_role_definition.backplane.id + description = "The ID of the role definition that enables deployment of the hub network to the connectivity scope." +} + +output "role_definition_name" { + value = azurerm_role_definition.backplane.name + description = "The name of the role definition that enables deployment of the hub network to the connectivity scope." +} + +output "scope" { + value = var.scope + description = "The scope where the hub deploy role definition and role assignment are applied." +} diff --git a/modules/azure/hub-network/backplane/provider.tf b/modules/azure/hub-network/backplane/provider.tf new file mode 100644 index 00000000..9d11da14 --- /dev/null +++ b/modules/azure/hub-network/backplane/provider.tf @@ -0,0 +1,7 @@ +provider "azurerm" { + features {} + + # The UAMI + its resource group are created in this subscription. The role + # definition/assignment are unaffected — they use their explicit `scope`. + subscription_id = var.subscription_id +} diff --git a/modules/azure/hub-network/backplane/variables.tf b/modules/azure/hub-network/backplane/variables.tf new file mode 100644 index 00000000..08cb53ff --- /dev/null +++ b/modules/azure/hub-network/backplane/variables.tf @@ -0,0 +1,41 @@ +variable "name" { + type = string + nullable = false + description = "Name for the building block identity, resource group and role definition." + validation { + condition = can(regex("^[-a-z0-9]+$", var.name)) + error_message = "Only alphanumeric lowercase characters and dashes are allowed" + } +} + +variable "scope" { + type = string + nullable = false + description = "Connectivity scope where the hub network can be deployed (management group or subscription ID). The deploy role definition and assignment are applied here." +} + +variable "subscription_id" { + type = string + nullable = false + description = "Subscription (bare GUID) where the UAMI and its resource group are created. Typically the hub/connectivity subscription so the identity lives in a stable, platform-owned place." + + validation { + condition = can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.subscription_id)) + error_message = "Must be a bare subscription GUID, not a '/subscriptions/' path." + } +} + +variable "location" { + type = string + nullable = false + description = "Azure region for the UAMI resource group." +} + +variable "workload_identity_federation" { + type = object({ + issuer = string + subjects = list(string) + }) + nullable = false + description = "WIF issuer and subjects for federated authentication of the automation identity." +} diff --git a/modules/azure/hub-network/backplane/versions.tf b/modules/azure/hub-network/backplane/versions.tf new file mode 100644 index 00000000..653c1354 --- /dev/null +++ b/modules/azure/hub-network/backplane/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.36.0" + } + } +} diff --git a/modules/azure/hub-network/buildingblock/README.md b/modules/azure/hub-network/buildingblock/README.md new file mode 100644 index 00000000..f82dc87b --- /dev/null +++ b/modules/azure/hub-network/buildingblock/README.md @@ -0,0 +1,81 @@ +--- +name: Azure Hub Network +supportedPlatforms: + - azure +description: Provisions the central hub virtual network (resource group, hub vnet, GatewaySubnet and an optional Azure Firewall) that spoke networks peer into. +--- + +This building block provisions the **central hub** of a hub-and-spoke Azure network topology in the +platform's connectivity subscription: a resource group, the hub virtual network, a `GatewaySubnet` +for a future VPN/ExpressRoute gateway, and — optionally — an Azure Firewall with a static public IP +and an egress route table whose default route points at the firewall. + +It is the counterpart to the [`spoke-network`](../../spoke-network) building block: application +teams order a spoke network into their own subscription, which peers into the hub vnet this building +block creates. + +## 🎯 When to use it + +Order this once per connectivity environment (e.g. per hub subscription) to establish the hub that +all spoke networks connect to. It is a platform-team building block, not an application-team one. + +## Shared Responsibilities + +| Responsibility | Platform Team | Application Team | +| -------------- | :-----------: | :--------------: | +| Provision and operate the hub vnet and firewall | ✅ | ❌ | +| Choose the hub address space and firewall SKU | ✅ | ❌ | +| Peer spoke networks into the hub | ✅ | ❌ | +| Order spoke networks and use the connectivity | ❌ | ✅ | + +The user-facing readme is maintained inline in the `readme` field of the +`meshstack_building_block_definition` in +[`../meshstack_integration.tf`](../meshstack_integration.tf). + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.12.0 | +| [azurerm](#requirement\_azurerm) | >= 4.36.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azurerm_firewall.hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/firewall) | resource | +| [azurerm_public_ip.firewall](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/public_ip) | resource | +| [azurerm_resource_group.hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_route_table.egress](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/route_table) | resource | +| [azurerm_subnet.firewall](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource | +| [azurerm_subnet.gateway](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource | +| [azurerm_virtual_network.hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [address\_space](#input\_address\_space) | Address space of the hub virtual network in CIDR notation, e.g. '10.0.0.0/22'. Must be large enough for the derived AzureFirewallSubnet and GatewaySubnet (a /22 gives four /24s). | `string` | n/a | yes | +| [create\_gateway\_subnet](#input\_create\_gateway\_subnet) | Create a GatewaySubnet for a future VPN/ExpressRoute gateway. | `bool` | `true` | no | +| [deploy\_firewall](#input\_deploy\_firewall) | Deploy an Azure Firewall into the hub, with an AzureFirewallSubnet, a static public IP and an egress route table with a default route pointing at the firewall. | `bool` | `false` | no | +| [firewall\_sku\_tier](#input\_firewall\_sku\_tier) | Azure Firewall SKU tier. Only Standard and Premium are supported (Basic requires a separate management subnet and IP). | `string` | `"Standard"` | no | +| [firewall\_threat\_intel\_mode](#input\_firewall\_threat\_intel\_mode) | Azure Firewall threat intelligence mode: Off, Alert or Deny. | `string` | `"Alert"` | no | +| [hub\_resource\_group\_name](#input\_hub\_resource\_group\_name) | Name of the resource group created in the connectivity subscription to host the hub vnet and firewall. | `string` | `"hub-network"` | no | +| [hub\_vnet\_name](#input\_hub\_vnet\_name) | Name of the central hub virtual network. Used as the basis for the firewall and route table resource names. | `string` | `"hub-vnet"` | no | +| [location](#input\_location) | Azure region where the hub resource group, vnet and firewall are created. | `string` | `"germanywestcentral"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [firewall\_private\_ip](#output\_firewall\_private\_ip) | Private IP of the Azure Firewall, if deployed. Spokes route egress traffic here. | +| [resource\_group\_name](#output\_resource\_group\_name) | Name of the hub resource group. | +| [summary](#output\_summary) | Markdown summary of the created hub network. | +| [vnet\_id](#output\_vnet\_id) | Azure resource ID of the hub virtual network. | +| [vnet\_name](#output\_vnet\_name) | Name of the hub virtual network. Spoke networks peer into this vnet. | + diff --git a/modules/azure/hub-network/buildingblock/logo.png b/modules/azure/hub-network/buildingblock/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..83c118d7474aa20be55646e420950f745ae88dd2 GIT binary patch literal 4928 zcmX9?c{r5a`+uHc4CA$AjR@H#DvD5p?6QWaG)m$W7m32;B)WosXHz_O&&rmn?(639NF^GCNyu<`Nep{h8U`*gPn5K^kE zSukN*N0)mva`u%@Xr*58Yn^lDx*_G;&R6Klr2Yp9c&c$SS1YJcJGexbA8_TPLHK+9 zxUL<$8yT68-{}3WIZ>BMW&o zf$ICl-G~)bWGnIOVv$}z9N2BJrFTgi?>j=d;TQ0Tq)WvCR@yg_{{aWfpnacm%&bIt zgXi)Uutx&=O!AU#*2Mo98szVH{y%WQ4{FYl;d7Z~>Z&51C>{&;g3?KyI5~DXV@nlk z#W^TmK_&Y)?^+1r{fW}R*Zx8*uC=`i-YYU9duhG^o#3U^hwa^i;;#;Weu$G>O4CKp z?SfNre7XL!P_aO+AC(Axop*5=nU5+^8hHW+j3E9F!R2kjnZl?{#VznYgPNkQOWgq; z@A3#OfNFT8)!?xdvT&{+KWuQD)Kanj>cRjwOxd!5f((HBqv8G|273)*;#3@|nOp7Rj5c@Fe175P!8c>hK6D86tO zJJcMI7zxgt92rXv{Wk8|2Z<<*=SGZr1pG#laJPU_pWTcAZ_RFC|<1}Ns+bW@q@InW8FAW_SL1EcZ22yZ-c;~xS%euuR(LxZw;COy#J@^ z18YA-Ji$~A?iQumFW@offd^n*R9sPY*JsbIYf*9SPI&?2CB;Sj!W{XZXe?GQZ1 zsKLF&hE{start?i6Ga!cvEWBQB^MR*<~t)*EP01^i&;%^N5=Cn0%u7u_xo%u>LTGZP@_@Dh58a*jy zc{-mLibp!GTRLWG$wKp$Zl1?N;@(urVebUX#^@fzdkx3% zrXyta_Yq9Gf{2qxmy2#CTIe$8Y=780L@n-XSfRDoli0j?va&U9P)ESY@~+sd7Zr!O zpByvW{DA0DgdLL2y&mo)e!Bm#*R;=^=O%Nrjk=aRqr}kU^-#syW1lFV0m**9^+&Wo z&|BLhSg4Vih=ca1rht?65tkK)W6264Yhk1#FjgGIog=T2$K>rho)dkK%Z7a#emk4XhA#k>@lfUmetXix) zs#bW44$_{35?bxZn8cJM2t5p+XVcf|+j^4i8L215ExF|8#xsr=ZaehaD>>!+$(sosjF5g{6&hW#ZI!8D=O0sr9r{Bqgf*#3*XmE~ zCGw?p?Xra`W_zCDwj|)1L-r`Ioh&oB@UwzT_CX$h9e$hVM|A1Vi(AlUh8LNh+%KB$}i z_GXFhN5+zfp5jT_fjqLU!JYJcY0IDmaFxctuy9XcMmJ8yXEQaRDKmbGl+dOALk!&k zOj5Uq-&WR$0!cJ5%j#XfJnbL zZLL+PxCxAog27?Bbsvf+`u-HYSdBv`4D~FxETph0gjb({QYU89gKH56gH4}QOuGl= z>!SKup!?(+-7HaH=l;fwLiEPa58K;o;Z#c3x3mpFJ|B}$J#AQ!VQ8RPhUIuKhwNKv z`~$kgsP{m5{HCnhHJ2CF)Ne8YS{A$|ik@>yKj;-iqSf|{$VtD$4%v%KroF&w(hz%| zVc;(rYMsKJLL9Abq+Bpy47splDH<+lG|9<)s7GA)ykm&dcV&EXxDrSbUCG9uoNV7!C!})D!$$~48}z<7k;CZ;Zo&~ zJo@U(gyx_`h`J6M-+9e(HmKiXErBfXX-}B+{>CzYh{Di{Pdw@-z^0grah%h3y3hSy z1E0PEYiLQ<_;)|RgtlwOCC%$5e1+_p*sK?nCpaR=Tw^Vs_r#rbFv3V;dy98MHqaFI z^TE@fo}cbbB2L39(D&sdm?2@0j`1j9vG}MqE#|7T5`&$%yI3mh*?dpZ_kt;L^)QjY zPvkUVgvaQIhfvv{O;G$chivk#c~}o~!0U(L?g!?MfyL|PxD(B{n~bjXjFPQ0ER8@K zs+C&Uzyo5-K*Stx%)3COZHpOE=#SLH$c8%!Bl6c5>$r4XV1hyLtsNEhH5+FTg-sRW zUw?zLrFA6x`iaw2fGoqar09e&5fKJb_m8Nr@E5HbxnwT6ibf)}Pz+^JJzE^180uS}CKur+iVBZQPo|C7;Z?89K<&m>4 z9t43h6})TPP4UB=%w_mu;N$zCofG@~^_}EZ#1D7g_=ZR$wspgad7+=PBu`i_;s$IY zxE|5%<^vyCReMiuZU(8!0~eoGUi$qUGgNPz_`JZPQ1?dWj!Jk@>y!QC{;JXpr>c}d zJ@|Y3z>ZPG@@d(O=yxDbGdh~rIJ^#G@dE(I3Msiqa)8a9ui~bXsEm>2ideLxcU7SQ z<&saIaSssyX{UsSq-lQG({fO6QdH&L3(6wa=uSYLMKQZAIE=fykmF4~A39q)cPUaZoVN)d#s-dNzdag2 zd!OChHKy6_u=ePPKXfc-8Ci-Dxc3`~EH+FQ>>}XsG<1^XE zTzl6jhq3ZJdqTjRZ_Rpe0XA49d1E9}aC(Mh3Ge2(Z4J0ia)R~SZr(l3L-6}r3{T`# zV1{v8kCksu3NdSeX!F6+UY@p0KpJ?2d%^xy4E+-M1~j9dNIe`G|2by0ZdfidT)^>e zr6{q0uGIq4w3WIWT)-dTstk%aWuD+77fLsz3@X{3Na0WtNDcA{8$Mw2w>F@iiz*&y zEEY&ipG-N3#`H?HfWe*iyaUhvnL(s2TEY$e%CWSAER_#J&}g|gRh>O6yS-KA5Pm%S zYAvvRV0ZWFm_$n$AUpT9+xw<&nISFjQza}6>Wu8*T_&barOXOWBv~}Efa3FkJ?XpO zHDXCL!qJd_g*sk?A3TX?M_;T_Jn$BGfIDo7>EM;9ES1)mR~{zrTlor3-Hdfpb<1U` zuqQW|f@iqRt(0;|2gpCdRS8dnzbL=QC7WiDGm_?a?SESJu99v7(PMDRINckfftRlC zje3e>U_4-P`SZTbA=~=d^l{tfyi;w?KSaB(Vr~I38}A^_aLNST2g)M(z{iJ|K$x9j z6XVFAPfz?$uW`ud<{JJ@dfC5!x`smr*Q7`4^D23v#3*A>kK4|xtVYv?wD%VK{O}pbCW#DRTIAw~i1GVxGp*ZyY-|OUqPCJ*(Ghg2> zC4)nios8#|7K#XA5BrivI#!+!$@8}2d5tl9-QoV4Lm5HKYnxr3#M0~=X{61W;93AzBLc#ljafqZRF0zj8_yl|Si{*%PKOG$h}$2eFg~ z#z(o{@410N@!Xtoh2@7QwK!ydI&$@pinxrKi4VkM-GbBT5%pr|oof*zO^ak*E3ZmM z^(ReNiP6RG7TxF@JEJZP0rdpU{YFPt5xcT^WcWwXet#X*n@+bld1kY=V^-0>R{$^Ino ztj5$egh$a5tmp&RNHk!P+?srt2Hr`rzdNX48`;yq_DS7xz*enk1cuXC!j)xg7dGux69>Mpohk6onBYA&$xr zJWk5jmwY5($3?zv^Kkb=d5PKhHiV{RiB(5XrDpVZoR6ozVt?8`i&_6^;~}I_^Jxc07DuTX~lKT96XHyb3RY0qNvw0g8C?P~*$rScLE zipgk@L0=pNMc5T_ox<1qGSCCT9$m;CMCD_#nbnZJNP1DRIY{A3^rK0z(%sQxExR^l ze+KzV?ygrQ!rXB-#v9Oe#8k9)2bzaV1!8BZ(YjPyS?`S~i;fB?Sp}$rOw*CHUAZ5) z+tC9<)>zFLx|kU~ep>BsO+d;NSP^1&_eg}UAA(bXeigM0OctlEn4!E-PXPn2xuX63 zV}uIq$VoVqOO_M3!Y{VTXB@UnF-LhIOMk=-81NfZ7t`F@K1!m1-W(R%u0P@poL9MB z@HiqZ&MX0LyZD)G5sexE=KQ1iw-2IoNf8jM<_oTog-oN?SalojQv;S_igJUM!iGas ztbk!P#Gxk$oR|i$gk%-`. + EOT +} + variable "meshstack" { type = object({ owning_workspace_identifier = string platform_name = optional(string, "azure") location_name = optional(string, "global") + tags = optional(map(list(string)), {}) + }) + description = "meshStack ownership and naming settings for this platform integration. `tags` are propagated to the created landing zones." +} + +variable "hub" { + type = object({ + git_ref = optional(string, "feature/azure-platfrom-ref") + bbd_draft = optional(bool, true) }) - description = "meshStack ownership and naming settings for this platform integration." + const = true + default = { + git_ref = "feature/azure-platfrom-ref" + bbd_draft = true + } + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of the meshstack-hub repo. + `bbd_draft`: If true, building block definitions sourced from this integration are kept in draft mode. + EOT +} + +output "platform" { + description = "The meshStack platform Azure subscriptions are created on. Use `uuid` as the `platform_ref` of a meshTenant." + value = { + uuid = meshstack_platform.azure.metadata.uuid + name = meshstack_platform.azure.metadata.name + } +} + +output "platform_ref" { + description = "Reference to the meshPlatform this integration creates, for compositions that create meshTenants on it." + value = { + uuid = meshstack_platform.azure.metadata.uuid + kind = "meshPlatform" + } +} + +output "landingzone_names" { + description = "meshStack landing zone names created per archetype, keyed by archetype." + value = { for key, lz in meshstack_landingzone.this : key => lz.metadata.name } +} + +output "landingzone_refs" { + description = "References to the created landing zones, keyed by archetype, for compositions that create meshTenants on them." + value = { + for key, lz in meshstack_landingzone.this : key => { + name = lz.metadata.name + kind = "meshLandingZone" + } + } } data "meshstack_integrations" "integrations" {} @@ -49,16 +129,24 @@ data "azurerm_role_definition" "reader" { name = "Reader" } -data "azurerm_billing_mca_account_scope" "subscriptions" { - billing_account_name = var.azure_billing_account_name - billing_profile_name = var.azure_billing_profile_name - invoice_section_name = var.azure_invoice_section_name +locals { + customer_agreement = var.azure_subscription_provisioning.customer_agreement + is_mca = local.customer_agreement != null } -data "azurerm_management_group" "parent" { - name = var.azure_management_group +data "azurerm_billing_mca_account_scope" "subscriptions" { + count = local.is_mca ? 1 : 0 + + billing_account_name = local.customer_agreement.billing_account_name + billing_profile_name = local.customer_agreement.billing_profile_name + invoice_section_name = local.customer_agreement.invoice_section_name } +# Scopes are built directly from the management group name (not looked up via a data source), so they +# stay known at plan time even when the management group is created in the same run (the reference +# architecture creates it). A data source read would be deferred to apply and break the meshplatform +# module's for_each over these scopes. + # Creates required resource in Azure module "azure_meshplatform" { source = "meshcloud/meshplatform/azure" @@ -66,14 +154,14 @@ module "azure_meshplatform" { replicator_enabled = true replicator_service_principal_name = "meshstack-replicator" - replicator_custom_role_scope = data.azurerm_management_group.parent.name - replicator_assignment_scopes = [data.azurerm_management_group.parent.name] + replicator_custom_role_scope = var.azure_management_group + replicator_assignment_scopes = [var.azure_management_group] - can_cancel_subscriptions_in_scopes = [data.azurerm_management_group.parent.id] + can_cancel_subscriptions_in_scopes = ["/providers/Microsoft.Management/managementGroups/${var.azure_management_group}"] metering_enabled = true metering_service_principal_name = "meshstack-metering" - metering_assignment_scopes = [data.azurerm_management_group.parent.name] + metering_assignment_scopes = [var.azure_management_group] create_passwords = false # Use only workload identity federation @@ -81,15 +169,16 @@ module "azure_meshplatform" { issuer = data.meshstack_integrations.integrations.workload_identity_federation.replicator.issuer replicator_subject = data.meshstack_integrations.integrations.workload_identity_federation.replicator.subject kraken_subject = data.meshstack_integrations.integrations.workload_identity_federation.metering.subject - mca_subject = data.meshstack_integrations.integrations.workload_identity_federation.replicator.subject + mca_subject = local.is_mca ? data.meshstack_integrations.integrations.workload_identity_federation.replicator.subject : null } - mca = { - billing_account_name = var.azure_billing_account_name - billing_profile_name = var.azure_billing_profile_name - invoice_section_name = var.azure_invoice_section_name + # Only for the customer_agreement (MCA) model — pre-provisioned needs no MCA service principal. + mca = local.is_mca ? { + billing_account_name = local.customer_agreement.billing_account_name + billing_profile_name = local.customer_agreement.billing_profile_name + invoice_section_name = local.customer_agreement.invoice_section_name service_principal_names = ["meshstack-mca"] - } + } : null } # Configure meshStack platform @@ -138,24 +227,33 @@ resource "meshstack_platform" "azure" { allow_hierarchical_management_group_assignment = false - provisioning = { - customer_agreement = { - billing_scope = data.azurerm_billing_mca_account_scope.subscriptions.id - - # This assumes the simple case where subscriptions are created in the same Entra tenant - # that meshStack is managing (source == destination). For cross-tenant setups, set these - # to the respective source and destination tenant IDs. - source_entra_tenant = module.azure_meshplatform.azure_ad_tenant_id - destination_entra_id = module.azure_meshplatform.azure_ad_tenant_id - - source_service_principal = { - client_id = module.azure_meshplatform.mca_service_principal["meshstack-mca"].Application_Client_ID - auth = {} # workload identity federation + # Exactly one of customer_agreement / pre_provisioned, selected by the provisioning model. + provisioning = merge( + { + subscription_owner_object_ids = var.azure_subscription_owner_object_ids != null ? var.azure_subscription_owner_object_ids : [data.azurerm_client_config.current.object_id] + }, + local.is_mca ? { + customer_agreement = { + billing_scope = data.azurerm_billing_mca_account_scope.subscriptions[0].id + + # This assumes the simple case where subscriptions are created in the same Entra tenant + # that meshStack is managing (source == destination). For cross-tenant setups, set these + # to the respective source and destination tenant IDs. + source_entra_tenant = module.azure_meshplatform.azure_ad_tenant_id + destination_entra_id = module.azure_meshplatform.azure_ad_tenant_id + + source_service_principal = { + client_id = module.azure_meshplatform.mca_service_principal["meshstack-mca"].Application_Client_ID + auth = {} # workload identity federation + } + subscription_creation_error_cooldown_sec = 900 + } + } : { + pre_provisioned = { + unused_subscription_name_prefix = var.azure_subscription_provisioning.pre_provisioned.unused_subscription_name_prefix } - subscription_creation_error_cooldown_sec = 900 } - subscription_owner_object_ids = var.azure_subscription_owner_object_ids != null ? var.azure_subscription_owner_object_ids : [data.azurerm_client_config.current.object_id] - } + ) azure_role_mappings = [ { @@ -217,15 +315,19 @@ resource "meshstack_platform" "azure" { } } -resource "meshstack_landingzone" "azure_default" { +# One meshStack landing zone per archetype, each pointing at an existing Azure management group. +resource "meshstack_landingzone" "this" { + for_each = var.landing_zones + metadata = { - name = "${var.meshstack.platform_name}-default" + name = "${var.meshstack.platform_name}-${each.key}" owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags } spec = { - display_name = "Azure Default" - description = "Default Azure landing zone" + display_name = each.value.display_name + description = each.value.description automate_deletion_approval = true automate_deletion_replication = true @@ -235,14 +337,22 @@ resource "meshstack_landingzone" "azure_default" { platform_properties = { azure = { - azure_management_group_id = var.azure_management_group + azure_management_group_id = each.value.management_group_id - azure_role_mappings = [] + azure_role_mappings = each.value.azure_role_mappings } } } } +# The single fixed landing zone became a per-archetype map. A deployment that used the previous +# `azure-default` landing zone keeps it by passing a `default` archetype; `name` carries no +# RequiresReplace, so this updates in place instead of recreating the landing zone. +moved { + from = meshstack_landingzone.azure_default + to = meshstack_landingzone.this["default"] +} + terraform { required_version = ">= 1.12.0" diff --git a/modules/azure/spoke-network/meshstack_integration.tf b/modules/azure/spoke-network/meshstack_integration.tf index e7fd8da9..c02bf7c4 100644 --- a/modules/azure/spoke-network/meshstack_integration.tf +++ b/modules/azure/spoke-network/meshstack_integration.tf @@ -1,3 +1,8 @@ +variable "azure_tenant_id" { + type = string + description = "Azure Entra tenant ID used for the building block's ARM authentication (ARM_TENANT_ID)." +} + variable "azure_hub_subscription_id" { type = string description = "PROVIDER TARGET: hub subscription the azurerm provider reads the hub vnet from and creates the hub-side peering in. Bare GUID (e.g. '92eae5db-...'), NOT a '/subscriptions/...' path. Same sub as azure_hub_scope in a simple setup, but different format/purpose (that one is the RBAC scope)." diff --git a/reference-architectures/azure-landingzone/README.md b/reference-architectures/azure-landingzone/README.md new file mode 100644 index 00000000..d7df5ddd --- /dev/null +++ b/reference-architectures/azure-landingzone/README.md @@ -0,0 +1,137 @@ +--- +name: Azure Landing Zone +description: > + Onboards an Azure Subscription platform into meshStack over an Enterprise-Scale management group + hierarchy — existing, or provisioned by the architecture itself. Creates one landing zone per + archetype (Corp, Online, Sandbox), registers the hub-network, spoke-network, budget-alert and + storage-account building blocks, and optionally provisions the management group hierarchy, a + central hub vnet, Enterprise-Scale policies and platform resource groups. +cloudProviders: + - azure +buildingBlocks: + - path: azure + role: Registers the Azure Subscription platform and the Corp/Online/Sandbox landing zones. + - path: azure/hub-network + role: The central hub vnet (with optional firewall) — registered, and optionally provisioned via the foundation. + - path: azure/spoke-network + role: A spoke vnet peered into the hub — best paired with the Corp landing zone. + - path: azure/budget-alert + role: Consumption budget alerts application teams can order onto a subscription. + - path: azure/storage-account + role: Self-service Azure Storage Accounts. +--- + +# Azure Landing Zone + +## Overview + +The **Azure Landing Zone** reference architecture turns an existing Azure Enterprise-Scale +management group hierarchy into a self-service-ready meshStack platform in one run using its own +Terraform code. + +The **management group hierarchy** — Corp, Online, Sandbox and Connectivity — is by default +**provisioned by the architecture** under the bootstrap scope (the parent management group given at +registration), so end users configure nothing about management groups. Set +`azure_create_management_groups = false` to instead use an existing hierarchy (passed via the +`azure_*_management_group` inputs). The MCA billing setup and the connectivity subscription are +always assumed to exist. On top of the hierarchy the architecture wires meshStack and can lay the +remaining optional `foundation` (hub network, policies, resource groups). + +Running it once **always**: + +1. Registers the **Azure Subscription** platform in meshStack, with the replicator and metering + identities scoped to the landing-zones management group. +2. Creates one **landing zone per Enterprise-Scale archetype** — Corp (internal, hub-connected), + Online (internet-facing) and Sandbox (experimentation) — each pointing at its management group, + so a subscription ordered through a landing zone lands in the matching management group. +3. Registers the **Azure Hub Network**, **Azure Spoke Network**, **Azure Budget Alert** and + **Azure Storage Account** building blocks. Each creates its own backplane — a User-Assigned + Managed Identity federated to the building block definition — so it can be ordered into landing + zone subscriptions (or, for the hub, the connectivity subscription). + +And **optionally** (via the `foundation` input): + +4. Provisions the **management group hierarchy** — Landing Zones → Corp/Online/Sandbox plus + Connectivity — under a given parent (e.g. the tenant root group). The created groups are then + used everywhere instead of the `azure_*_management_group` inputs. +5. Provisions a central **hub vnet** (with an optional Azure Firewall) in the connectivity + subscription by ordering a Hub Network instance; spoke networks then peer into it. +6. Assigns curated **Enterprise-Scale policies** to the Corp/Online/Sandbox management groups + (Corp locked down, Online region-restricted, Sandbox audit-only). +7. Creates extra platform-owned **resource groups** in the platform subscription. + +**Target audience:** + +- **Platform engineers** onboarding an existing Enterprise-Scale Azure tenant into meshStack who + want landing zones and ready-to-order building blocks without hand-wiring the platform, + landing zones and backplanes separately. +- **Application teams** who request Azure subscriptions through a landing zone and order the + registered building blocks into them. + +## Architecture Diagram + +The left cluster is the **existing Azure hierarchy** — the landing-zones management group, the Corp, +Online and Sandbox management groups beneath it, and the central hub vnet. The right cluster is +**meshStack** — the platform, its three landing zones and the building block definitions. Dotted +edges across the boundary show how each meshStack construct maps onto its Azure counterpart: each +landing zone targets a management group, the platform replicates subscriptions into them, and the +spoke-network building block peers a spoke vnet into the hub. + +![Azure Landing Zone reference architecture](azure-landingzone.svg) + +## How It Works + +The architecture is a **one-time platform onboarding building block** — a one-click experience: + +1. A platform engineer applies [`meshstack_integration.tf`](meshstack_integration.tf) once (locally + with `az login`, or via an IaC runtime). This **registers the building block definition** in + meshStack **and** provisions a privileged [`bootstrap/`](bootstrap/) identity — a User-Assigned + Managed Identity federated to this definition, granted **Owner** on the given scope and Microsoft + Graph **`Application.ReadWrite.All`**. Applying this step requires an identity that is Owner on + the scope and can grant Graph app roles (Global Admin / Privileged Role Administrator) — a + deliberately privileged, one-time bootstrap. +2. The building block is then **ordered/run in meshStack**, which executes it **as the bootstrap + identity** (workload identity federation, no stored secret). That run does the actual work: it + creates the management group hierarchy (under the bootstrap scope), registers the Azure platform + and landing zones, and provisions the optional `foundation` (hub, policies, resource groups) plus + the building block backplanes. + +See [`buildingblock/`](buildingblock/) for the composition and [`bootstrap/`](bootstrap/) for the +identity. + +Once applied, application teams request Azure subscriptions through the Corp, Online or Sandbox +landing zone; each subscription is placed in the archetype's management group. They then order the +registered building blocks: + +- **Azure Spoke Network** deploys a spoke vnet into the ordering tenant's own subscription and peers + it into the hub — pair it with the **Corp** landing zone for hub-connected workloads. +- **Azure Budget Alert** and **Azure Storage Account** currently target the platform subscription + (`azure_platform_subscription_id`); see the buildingblock inputs. + +## Getting Started + +### Prerequisites + +| Requirement | Description | +|-------------|-------------| +| Management group hierarchy | A parent management group (the bootstrap scope) under which the architecture creates Corp/Online/Sandbox/Connectivity by default; or, with `azure_create_management_groups = false`, an existing hierarchy passed via the `azure_*_management_group` inputs. | +| MCA billing | Billing account, profile and invoice section names for subscription provisioning. | +| Network hub | An existing hub vnet (subscription, resource group and vnet name) for spoke networks to peer into. | +| Azure identity (to apply the integration) | Owner on the bootstrap `scope` and the ability to grant Microsoft Graph app roles (Global Administrator / Privileged Role Administrator), on the CLI (`az login`) or as an IaC runtime identity. Used once to create the bootstrap identity + register the definition; the ordered run itself then authenticates as the bootstrap identity via WIF. | + +### Deployment Order + +Apply the architecture once per workspace. It registers the platform, the three landing zones and +the three building blocks in a single run. Application teams can then request subscriptions and +order the building blocks. + +## Shared Responsibilities + +| Responsibility | Platform Team | Application Team | +|----------------|:---:|:---:| +| Maintain the management group hierarchy, billing and network hub | ✅ | ❌ | +| Register the Azure platform and the Corp/Online/Sandbox landing zones | ✅ | ❌ | +| Register the budget-alert, storage-account and spoke-network building blocks | ✅ | ❌ | +| Request Azure subscriptions through the landing zones | ❌ | ✅ | +| Order the registered building blocks into their subscriptions | ❌ | ✅ | +| Manage workloads inside the provisioned subscriptions | ❌ | ✅ | diff --git a/reference-architectures/azure-landingzone/azure-landingzone.dot b/reference-architectures/azure-landingzone/azure-landingzone.dot new file mode 100644 index 00000000..517fb4c8 --- /dev/null +++ b/reference-architectures/azure-landingzone/azure-landingzone.dot @@ -0,0 +1,80 @@ +/* + * Azure Landing Zone reference architecture. + * Conventions: .agents/references/diagrams.md — render with: task diagrams + */ +digraph azure_landingzone { + rankdir=TB + splines=ortho + forcelabels=true + bgcolor="white" + nodesep=0.55 + ranksep=0.85 + + node [shape=box style="rounded,filled" fontname="Helvetica" fontsize=11 + fillcolor="#ffffff" color="#a2abb8" penwidth=1.1 margin="0.20,0.11"] + edge [fontname="Helvetica" fontsize=9 fontcolor="#697180" color="#8b95a3" arrowsize=0.7] + + subgraph cluster_azure { + label=" Azure — Enterprise-Scale hierarchy (existing or provisioned)" + labeljust=l + fontname="Helvetica" fontsize=12 fontcolor="#697180" + style="rounded" color="#d5dae0" + margin=18 + + MG [label="🏛️ Landing Zones\nmanagement group" fillcolor="#ffffff" color="#a2abb8"] + CORP [label="🗂️ Corp MG" fillcolor="#eef2f6" color="#93a7bb"] + ONL [label="🗂️ Online MG" fillcolor="#eef2f6" color="#93a7bb"] + SBX [label="🗂️ Sandbox MG" fillcolor="#eef2f6" color="#93a7bb"] + HUB [label="🌐 Hub VNet\nprovisioned (foundation)" fillcolor="#eef2f6" color="#93a7bb"] + SUB [label="🗂️ Subscription ×N\nper project" fillcolor="#e5f2ea" color="#85bfa0"] + + { rank=same; CORP -> ONL -> SBX [style=invis] } + + MG -> CORP + MG -> ONL + MG -> SBX + CORP -> SUB [xlabel="contains"] + SUB -> HUB [xlabel="spoke peers into hub" style=dashed constraint=false] + } + + subgraph cluster_mesh { + label=" meshStack" + labeljust=l + fontname="Helvetica" fontsize=12 fontcolor="#697180" + style="rounded" color="#d5dae0" + margin=18 + + PLAT [label="🛰️ Azure Subscription platform" fillcolor="#ecedfb" color="#9aa2e6"] + LZC [label="🛬 Corp landing zone" fillcolor="#ecedfb" color="#9aa2e6"] + LZO [label="🛬 Online landing zone" fillcolor="#ecedfb" color="#9aa2e6"] + LZS [label="🛬 Sandbox landing zone" fillcolor="#ecedfb" color="#9aa2e6"] + BBH [label="📦 hub-network BBD" fillcolor="#ecedfb" color="#9aa2e6"] + BBN [label="📦 spoke-network BBD" fillcolor="#ecedfb" color="#9aa2e6"] + BBB [label="📦 budget-alert BBD" fillcolor="#ecedfb" color="#9aa2e6"] + BBS [label="📦 storage-account BBD" fillcolor="#ecedfb" color="#9aa2e6"] + POL [label="📜 ES policies\nfoundation (optional)" fillcolor="#ecedfb" color="#9aa2e6"] + PROJ [label="🗂️ Project ×N" fillcolor="#e5f2ea" color="#85bfa0"] + + { rank=same; LZC -> LZO -> LZS [style=invis] } + { rank=same; BBH -> BBN -> BBB -> BBS [style=invis] } + + PLAT -> LZC + PLAT -> LZO + PLAT -> LZS + LZC -> PROJ [xlabel="applies to"] + } + + # Cross-boundary mapping: each meshStack construct onto its Azure counterpart. + LZC -> CORP [xlabel="targets" style=dotted constraint=false] + LZO -> ONL [style=dotted constraint=false] + LZS -> SBX [style=dotted constraint=false] + PLAT -> SUB [xlabel="replicates subscriptions" style=dotted constraint=false] + PROJ -> SUB [xlabel="tenant subscription" style=dotted constraint=false] + BBH -> HUB [xlabel="provisions hub" style=dotted constraint=false] + BBN -> HUB [xlabel="peers spoke into hub" style=dotted constraint=false] + BBB -> SUB [xlabel="provision into" style=dotted constraint=false] + BBS -> SUB [style=dotted constraint=false] + POL -> CORP [xlabel="assigns policies" style=dotted constraint=false] + POL -> ONL [style=dotted constraint=false] + POL -> SBX [style=dotted constraint=false] +} diff --git a/reference-architectures/azure-landingzone/azure-landingzone.svg b/reference-architectures/azure-landingzone/azure-landingzone.svg new file mode 100644 index 00000000..35a6539e --- /dev/null +++ b/reference-architectures/azure-landingzone/azure-landingzone.svg @@ -0,0 +1,263 @@ + + + + +azure_landingzone + + +cluster_azure + +  Azure — Enterprise-Scale hierarchy (existing or provisioned) + + +cluster_mesh + +  meshStack + + + +MG + +🏛️ Landing Zones +management group + + + +CORP + +🗂️ Corp MG + + + +MG->CORP + + + + + +ONL + +🗂️ Online MG + + + +MG->ONL + + + + + +SBX + +🗂️ Sandbox MG + + + +MG->SBX + + + + + + +SUB + +🗂️ Subscription ×N +per project + + + +CORP->SUB + + +contains + + + + +HUB + +🌐 Hub VNet +provisioned (foundation) + + + +SUB->HUB + + +spoke peers into hub + + + +PLAT + +🛰️ Azure Subscription platform + + + +PLAT->SUB + + +replicates subscriptions + + + +LZC + +🛬 Corp landing zone + + + +PLAT->LZC + + + + + +LZO + +🛬 Online landing zone + + + +PLAT->LZO + + + + + +LZS + +🛬 Sandbox landing zone + + + +PLAT->LZS + + + + + +LZC->CORP + + +targets + + + + +PROJ + +🗂️ Project ×N + + + +LZC->PROJ + + +applies to + + + +LZO->ONL + + + + + + +LZS->SBX + + + + + +BBH + +📦 hub-network BBD + + + +BBH->HUB + + +provisions hub + + + +BBN + +📦 spoke-network BBD + + + + +BBN->HUB + + +peers spoke into hub + + + +BBB + +📦 budget-alert BBD + + + + +BBB->SUB + + +provision into + + + +BBS + +📦 storage-account BBD + + + + +BBS->SUB + + + + + +POL + +📜 ES policies +foundation (optional) + + + +POL->CORP + + +assigns policies + + + +POL->ONL + + + + + +POL->SBX + + + + + +PROJ->SUB + + +tenant subscription + + + diff --git a/reference-architectures/azure-landingzone/bootstrap/README.md b/reference-architectures/azure-landingzone/bootstrap/README.md new file mode 100644 index 00000000..132927a7 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/README.md @@ -0,0 +1,22 @@ +# Azure Landing Zone — Bootstrap Identity + +Provisions the privileged identity meshStack runs the **Azure Landing Zone** reference architecture +as. It is sourced from [`../meshstack_integration.tf`](../meshstack_integration.tf) and applied once +by the platform engineer (locally with `az login`, or via an IaC runtime) together with the building +block definition registration. + +## What it provisions + +- A **User-Assigned Managed Identity** (+ its resource group) in `subscription_id`. +- **Federated identity credentials** binding the identity to the reference architecture's building + block definition (WIF subjects), so the ordered run authenticates as this identity with no secret. +- **Owner** at `scope` (Azure RBAC) — enough to create management groups beneath it, custom role + definitions and assignments, resource groups, UAMIs, and the hub vnet/firewall. +- **Microsoft Graph `Application.ReadWrite.All`** (Entra app role) — so the run can create the + meshStack platform service principals (replicator/metering/[mca]). + +## Required permissions to apply this + +The identity applying it needs to be **Owner** on `scope` (to grant Owner) and able to **grant +Microsoft Graph app roles** (e.g. Global Administrator or Privileged Role Administrator). This is a +one-time, deliberately privileged platform-bootstrap step. diff --git a/reference-architectures/azure-landingzone/bootstrap/main.tf b/reference-architectures/azure-landingzone/bootstrap/main.tf new file mode 100644 index 00000000..56885ee7 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/main.tf @@ -0,0 +1,82 @@ +# Privileged bootstrap identity for the Azure Landing Zone reference architecture. +# +# The reference architecture's building block run creates management groups, the meshStack platform +# service principals (via terraform-azure-meshplatform), management-group role assignments, the hub +# network and the per-building-block backplanes. That run therefore needs a highly privileged Azure +# identity. This module provisions a User-Assigned Managed Identity federated to the reference +# architecture's building block definition, so meshStack executes the ordered run as this identity — +# no stored secret. Apply it (via meshstack_integration.tf) with an identity that is Owner on the +# scope and can grant Microsoft Graph app roles (Global Admin / Privileged Role Administrator). + +locals { + # Accept a full resource path as-is, a bare subscription GUID as a subscription scope, or any other + # bare value as a management group name — azurerm_role_assignment.scope needs the full path. + scope = startswith(var.scope, "/") ? var.scope : ( + can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.scope)) + ? "/subscriptions/${var.scope}" + : "/providers/Microsoft.Management/managementGroups/${var.scope}" + ) +} + +data "azuread_service_principal" "msgraph" { + client_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph +} + +resource "azurerm_resource_group" "bootstrap" { + name = var.name + location = var.location +} + +resource "azurerm_user_assigned_identity" "bootstrap" { + name = var.name + location = var.location + resource_group_name = azurerm_resource_group.bootstrap.name +} + +resource "azurerm_federated_identity_credential" "bootstrap" { + for_each = { for i, s in var.workload_identity_federation.subjects : tostring(i) => s } + + name = "subject-${each.key}" + user_assigned_identity_id = azurerm_user_assigned_identity.bootstrap.id + audience = ["api://AzureADTokenExchange"] + issuer = var.workload_identity_federation.issuer + subject = each.value +} + +# Azure RBAC: Owner at the scope so the run can create management groups beneath it, custom role +# definitions and role assignments, resource groups, User-Assigned Managed Identities, and the hub +# vnet/firewall. Set the scope high enough to cover everything the architecture provisions — e.g. +# the tenant root management group. +resource "azurerm_role_assignment" "owner" { + scope = local.scope + role_definition_name = "Owner" + principal_id = azurerm_user_assigned_identity.bootstrap.principal_id +} + +# Microsoft Entra: allow the run to create the meshStack platform service principals (replicator, +# metering and — for the MCA provisioning model — mca) that the meshplatform module registers. +resource "azuread_app_role_assignment" "graph_application_readwrite" { + app_role_id = data.azuread_service_principal.msgraph.app_role_ids["Application.ReadWrite.All"] + principal_object_id = azurerm_user_assigned_identity.bootstrap.principal_id + resource_object_id = data.azuread_service_principal.msgraph.object_id +} + +# Directory.Read.All — the platform integration reads the tenant's initial domain +# (data.azuread_domains) and the meshplatform module reads directory objects. +resource "azuread_app_role_assignment" "graph_directory_read" { + app_role_id = data.azuread_service_principal.msgraph.app_role_ids["Directory.Read.All"] + principal_object_id = azurerm_user_assigned_identity.bootstrap.principal_id + resource_object_id = data.azuread_service_principal.msgraph.object_id +} + +# Enterprise-Scale management group hierarchy, created here in the bootstrap phase so the management +# groups already EXIST when the ordered building block run reaches the meshplatform module (which +# looks them up via a data source — creating and reading them in the same run is a catch-22). The +# building block then adopts them via `import` blocks and manages them going forward. Uses the same +# parent (scope) + name_prefix as the building block so the names match for the import. +module "management_groups" { + source = "./modules/management-groups" + + parent_management_group_id = var.scope + name_prefix = var.management_group_name_prefix +} diff --git a/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/README.md b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/README.md new file mode 100644 index 00000000..d4dd7291 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/README.md @@ -0,0 +1,25 @@ +# Management Groups (foundation glue) + +Creates the Enterprise-Scale management group hierarchy for the Azure Landing Zone reference +architecture, under a given parent (typically the tenant root group): + +``` + +├── Landing Zones # platform replicator/metering + building block backplane scope +│ ├── Corp +│ ├── Online +│ └── Sandbox +└── Connectivity # hosts the hub subscription; hub-network backplane scope +``` + +Management groups are created with display names only; Azure generates their names (IDs). The module +outputs each group's `name` (for meshStack platform/landing-zone references) and, where needed, its +full `scope` path (for policy assignments and RBAC). + +This is **foundation glue** for the reference architecture. The caller (the reference architecture's +`buildingblock`) drives it from the `azure_management_groups` variable, whose +`parent_management_group_id` is pre-configured STATIC to the bootstrap scope — so the hierarchy is +created under the same management group the bootstrap identity owns. + +The applying identity needs permission to create management groups (Management Group Contributor at +the parent, or Owner on it — which the bootstrap identity has). diff --git a/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/main.tf b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/main.tf new file mode 100644 index 00000000..bfcb29f1 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/main.tf @@ -0,0 +1,49 @@ +locals { + # Accept either a bare management group name (e.g. the tenant ID for the tenant root group) or a + # full resource path, and normalise to the full path azurerm expects for a parent. + parent_id = startswith(var.parent_management_group_id, "/providers/Microsoft.Management/managementGroups/") ? var.parent_management_group_id : "/providers/Microsoft.Management/managementGroups/${var.parent_management_group_id}" + + # Explicit, deterministic management group names (IDs) — NOT Azure-generated GUIDs. This keeps the + # names known at plan time, which is required because the platform module (meshplatform) does a + # for_each over the landing-zones scope; an unknown (apply-time) name breaks that for_each. + # `name_prefix` keeps them unique across the tenant (e.g. "-"). + lz_name = "${var.name_prefix}landing-zones" + corp_name = "${var.name_prefix}corp" + online_name = "${var.name_prefix}online" + sandbox_name = "${var.name_prefix}sandbox" + connectivity_name = "${var.name_prefix}connectivity" +} + +# The "Landing Zones" management group holds the archetype groups and is the scope the platform's +# replicator/metering identities and the building block backplanes operate on. +resource "azurerm_management_group" "landing_zones" { + name = local.lz_name + display_name = var.landing_zones_display_name + parent_management_group_id = local.parent_id +} + +resource "azurerm_management_group" "corp" { + name = local.corp_name + display_name = var.corp_display_name + parent_management_group_id = azurerm_management_group.landing_zones.id +} + +resource "azurerm_management_group" "online" { + name = local.online_name + display_name = var.online_display_name + parent_management_group_id = azurerm_management_group.landing_zones.id +} + +resource "azurerm_management_group" "sandbox" { + name = local.sandbox_name + display_name = var.sandbox_display_name + parent_management_group_id = azurerm_management_group.landing_zones.id +} + +# Connectivity management group — the hub subscription lives here; the hub-network backplane role is +# scoped to it. A sibling of Landing Zones under the same parent. +resource "azurerm_management_group" "connectivity" { + name = local.connectivity_name + display_name = var.connectivity_display_name + parent_management_group_id = local.parent_id +} diff --git a/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/outputs.tf b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/outputs.tf new file mode 100644 index 00000000..2a7d0915 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/outputs.tf @@ -0,0 +1,37 @@ +# meshStack and the building block backplanes reference management groups by name; policy +# assignments and RBAC scopes need the full resource path. Expose both for each group. + +output "landing_zones_name" { + value = azurerm_management_group.landing_zones.name + description = "Name (ID) of the Landing Zones management group." +} + +output "landing_zones_scope" { + value = azurerm_management_group.landing_zones.id + description = "Full resource path of the Landing Zones management group." +} + +output "corp_name" { + value = azurerm_management_group.corp.name + description = "Name (ID) of the Corp management group." +} + +output "online_name" { + value = azurerm_management_group.online.name + description = "Name (ID) of the Online management group." +} + +output "sandbox_name" { + value = azurerm_management_group.sandbox.name + description = "Name (ID) of the Sandbox management group." +} + +output "connectivity_name" { + value = azurerm_management_group.connectivity.name + description = "Name (ID) of the Connectivity management group." +} + +output "connectivity_scope" { + value = azurerm_management_group.connectivity.id + description = "Full resource path of the Connectivity management group." +} diff --git a/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/variables.tf b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/variables.tf new file mode 100644 index 00000000..611ce309 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/variables.tf @@ -0,0 +1,47 @@ +variable "parent_management_group_id" { + type = string + nullable = false + description = "Parent management group under which the hierarchy is created — a bare management group name (e.g. the tenant ID for the tenant root group) or a full '/providers/Microsoft.Management/managementGroups/' path." +} + +variable "name_prefix" { + type = string + nullable = false + default = "" + description = "Prefix for the created management group names (IDs), e.g. '-', to keep them unique across the tenant. Must be known at plan time (do not derive it from apply-time values like a random suffix)." +} + +variable "landing_zones_display_name" { + type = string + nullable = false + default = "Landing Zones" + description = "Display name of the parent management group that holds the archetype groups." +} + +variable "corp_display_name" { + type = string + nullable = false + default = "Corp" + description = "Display name of the Corp management group." +} + +variable "online_display_name" { + type = string + nullable = false + default = "Online" + description = "Display name of the Online management group." +} + +variable "sandbox_display_name" { + type = string + nullable = false + default = "Sandbox" + description = "Display name of the Sandbox management group." +} + +variable "connectivity_display_name" { + type = string + nullable = false + default = "Connectivity" + description = "Display name of the Connectivity management group that hosts the hub subscription." +} diff --git a/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/versions.tf b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/versions.tf new file mode 100644 index 00000000..c3f74a3a --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/modules/management-groups/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.12.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.36.0" + } + } +} diff --git a/reference-architectures/azure-landingzone/bootstrap/outputs.tf b/reference-architectures/azure-landingzone/bootstrap/outputs.tf new file mode 100644 index 00000000..ffe798d2 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/outputs.tf @@ -0,0 +1,8 @@ +output "identity" { + value = { + client_id = azurerm_user_assigned_identity.bootstrap.client_id + principal_id = azurerm_user_assigned_identity.bootstrap.principal_id + tenant_id = azurerm_user_assigned_identity.bootstrap.tenant_id + } + description = "The bootstrap managed identity meshStack runs the reference architecture as. Wire `client_id` into the building block definition's ARM_CLIENT_ID input." +} diff --git a/reference-architectures/azure-landingzone/bootstrap/provider.tf b/reference-architectures/azure-landingzone/bootstrap/provider.tf new file mode 100644 index 00000000..e8a223d5 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/provider.tf @@ -0,0 +1,10 @@ +provider "azurerm" { + features {} + + # The bootstrap identity + its resource group are created in this subscription. The Owner role + # assignment is unaffected — it uses its explicit `scope`. + subscription_id = var.subscription_id +} + +# azuread is configured from ambient credentials (az login / ARM_* env) via required_providers — +# no explicit provider block needed (an empty one is deprecated in OpenTofu). diff --git a/reference-architectures/azure-landingzone/bootstrap/variables.tf b/reference-architectures/azure-landingzone/bootstrap/variables.tf new file mode 100644 index 00000000..2af7074e --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/variables.tf @@ -0,0 +1,49 @@ +variable "name" { + type = string + nullable = false + default = "azure-landingzone-bootstrap" + description = "Name for the bootstrap identity and its resource group. Must match pattern ^[-a-z0-9]+$." + validation { + condition = can(regex("^[-a-z0-9]+$", var.name)) + error_message = "Only alphanumeric lowercase characters and dashes are allowed" + } +} + +variable "scope" { + type = string + nullable = false + description = "Where the bootstrap identity is granted Owner — high enough to cover everything the architecture provisions. Accepts a bare management group name (e.g. `flo-test-ref-arch` or the tenant ID), a bare subscription GUID, or a full resource path. Typically the tenant root / a top management group." +} + +variable "subscription_id" { + type = string + nullable = false + description = "Subscription (bare GUID) where the bootstrap identity and its resource group are created." + + validation { + condition = can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.subscription_id)) + error_message = "Must be a bare subscription GUID, not a '/subscriptions/' path." + } +} + +variable "location" { + type = string + nullable = false + description = "Azure region for the bootstrap identity's resource group." +} + +variable "management_group_name_prefix" { + type = string + nullable = false + default = "" + description = "Prefix for the created management group names/IDs (e.g. 'flotest-az-'), to keep them unique across the tenant. Must match the prefix the building block uses so it can import them." +} + +variable "workload_identity_federation" { + type = object({ + issuer = string + subjects = list(string) + }) + nullable = false + description = "WIF issuer and subjects for federated authentication of the bootstrap identity. The subject binds the identity to the reference architecture's building block definition." +} diff --git a/reference-architectures/azure-landingzone/bootstrap/versions.tf b/reference-architectures/azure-landingzone/bootstrap/versions.tf new file mode 100644 index 00000000..d8f6f275 --- /dev/null +++ b/reference-architectures/azure-landingzone/bootstrap/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.12.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.36" + } + azuread = { + source = "hashicorp/azuread" + version = ">= 3.0" + } + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/README.md b/reference-architectures/azure-landingzone/buildingblock/README.md new file mode 100644 index 00000000..8e371cce --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/README.md @@ -0,0 +1,110 @@ +--- +name: Azure Landing Zone +supportedPlatforms: + - azure +description: Onboards an Azure Subscription platform into meshStack on top of an existing Enterprise-Scale management group hierarchy, creates Corp/Online/Sandbox landing zones, and registers the budget-alert, storage-account and spoke-network building blocks. +--- + +This is the Terraform for the [Azure Landing Zone reference architecture](../README.md). It composes +the Azure platform integration and three Hub building blocks into a single onboarding run. + +It assumes the Azure **management group hierarchy already exists** (a parent "Landing Zones" +management group with Corp, Online and Sandbox management groups beneath it) and a central network +**hub** vnet is already in place. The run then: + +- sources [`modules/azure`](../../../modules/azure) to register the **Azure Subscription** platform + and one landing zone per archetype (Corp, Online, Sandbox), each pointing at its management group; +- sources [`modules/azure/budget-alert`](../../../modules/azure/budget-alert), + [`modules/azure/storage-account`](../../../modules/azure/storage-account) and + [`modules/azure/spoke-network`](../../../modules/azure/spoke-network), each of which creates its + own backplane (a User-Assigned Managed Identity federated to the building block definition, with a + deploy role scoped to the landing-zones management group) and registers the building block + definition. + +## Applying + +This is a one-time platform onboarding building block, ordered once in meshStack. meshStack runs it +as the privileged **bootstrap identity** provisioned by [`../bootstrap`](../bootstrap) — a UAMI +federated to the building block definition, so the run authenticates via workload identity +federation (no stored secret). The `azurerm`/`azuread` providers pick up the `ARM_*` OIDC +environment meshStack injects for that identity. (For local development you can also apply this +directly with an equivalently privileged `az login`.) + +The composed building blocks are then individually orderable by application teams; the budget-alert +and storage-account building blocks currently target the platform subscription +(`azure_platform_subscription_id`), while the spoke-network building block deploys into each +ordering tenant's own subscription. + +The user-facing readme is maintained inline in the `readme` field of the +`meshstack_building_block_definition` in +[`../meshstack_integration.tf`](../meshstack_integration.tf). + + +## Requirements + +| Name | Version | +| ---- | ------- | +| [terraform](#requirement\_terraform) | >= 1.12.0 | +| [azuread](#requirement\_azuread) | >= 3.8 | +| [azurerm](#requirement\_azurerm) | >= 4.64 | +| [meshstack](#requirement\_meshstack) | >= 0.24.0 | +| [random](#requirement\_random) | >= 3.6.0 | + +## Modules + +| Name | Source | Version | +| ---- | ------ | ------- | +| [azure\_platform](#module\_azure\_platform) | github.com/meshcloud/meshstack-hub//modules/azure | main | +| [budget\_alert](#module\_budget\_alert) | github.com/meshcloud/meshstack-hub//modules/azure/budget-alert | main | +| [es\_policies](#module\_es\_policies) | ./modules/es-policies | n/a | +| [hub\_network](#module\_hub\_network) | github.com/meshcloud/meshstack-hub//modules/azure/hub-network | main | +| [management\_groups](#module\_management\_groups) | ./modules/management-groups | n/a | +| [spoke\_network](#module\_spoke\_network) | github.com/meshcloud/meshstack-hub//modules/azure/spoke-network | main | +| [storage\_account](#module\_storage\_account) | github.com/meshcloud/meshstack-hub//modules/azure/storage-account | main | + +## Resources + +| Name | Type | +| ---- | ---- | +| [azurerm_resource_group.foundation](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [meshstack_building_block.hub](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block) | resource | +| [meshstack_location.this](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/location) | resource | +| [random_string.playground_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| ---- | ----------- | ---- | ------- | :------: | +| [azure\_backplane\_subscription\_id](#input\_azure\_backplane\_subscription\_id) | Optional bare GUID of the subscription where the spoke-network backplane identity is created. Defaults to azure\_platform\_subscription\_id. Typically the hub subscription so the automation identity lives in a stable, platform-owned place. | `string` | `null` | no | +| [azure\_connectivity\_subscription\_id](#input\_azure\_connectivity\_subscription\_id) | Bare GUID of the connectivity subscription where the Azure Hub Network backplane identity lives and, when foundation.hub is set, the hub vnet and firewall are created. | `string` | n/a | yes | +| [azure\_hub\_resource\_group\_name](#input\_azure\_hub\_resource\_group\_name) | Name of the resource group that contains an existing hub vnet spoke networks peer into. Leave null when foundation.hub creates the hub. | `string` | `null` | no | +| [azure\_hub\_scope](#input\_azure\_hub\_scope) | Full resource path where the spoke-network backplane's hub-peering role is granted: a management group or subscription path containing an existing hub vnet. Leave null when foundation.hub creates the hub. | `string` | `null` | no | +| [azure\_hub\_subscription\_id](#input\_azure\_hub\_subscription\_id) | Bare GUID of the subscription hosting an existing central hub vnet that spoke networks peer into. Leave null when foundation.hub creates the hub. | `string` | `null` | no | +| [azure\_hub\_vnet\_name](#input\_azure\_hub\_vnet\_name) | Name of an existing hub vnet spoke networks peer into. Leave null when foundation.hub creates the hub. | `string` | `null` | no | +| [azure\_location](#input\_azure\_location) | Azure region where the building block backplane resource groups and identities are created. | `string` | `"germanywestcentral"` | no | +| [azure\_management\_groups](#input\_azure\_management\_groups) | Enterprise-Scale management group hierarchy (Landing Zones → Corp/Online/Sandbox, plus
Connectivity) under `parent_management_group_id`, with names prefixed by `name_prefix`. The
bootstrap step already created these; this building block adopts them via `import` blocks and
manages them. Pre-configured STATIC by the platform team at registration (parent = bootstrap scope,
same name\_prefix as the bootstrap) — end users don't specify it. |
object({
parent_management_group_id = string
name_prefix = optional(string, "")
landing_zones_display_name = optional(string, "Landing Zones")
corp_display_name = optional(string, "Corp")
online_display_name = optional(string, "Online")
sandbox_display_name = optional(string, "Sandbox")
connectivity_display_name = optional(string, "Connectivity")
})
| n/a | yes | +| [azure\_platform\_subscription\_id](#input\_azure\_platform\_subscription\_id) | Bare GUID of a platform-owned subscription. The azurerm provider targets it, the budget-alert and storage-account backplanes are created in it, and (as written) those two building blocks deploy their resources into it. | `string` | n/a | yes | +| [azure\_subscription\_owner\_object\_ids](#input\_azure\_subscription\_owner\_object\_ids) | Optional explicit subscription owner object IDs. If null, the applying principal is used. | `list(string)` | `null` | no | +| [azure\_subscription\_provisioning](#input\_azure\_subscription\_provisioning) | Azure subscription provisioning model — set exactly one:
`pre_provisioned` (default): meshStack assigns subscriptions from a pool of existing ones whose name starts with `unused_subscription_name_prefix` (default `unused-`). No MCA service principal is created.
`customer_agreement`: meshStack creates subscriptions via the given MCA billing scope. |
object({
pre_provisioned = optional(object({
unused_subscription_name_prefix = optional(string, "unused-")
}))
customer_agreement = optional(object({
billing_account_name = string
billing_profile_name = string
invoice_section_name = string
}))
})
|
{
"pre_provisioned": {}
}
| no | +| [azure\_tenant\_id](#input\_azure\_tenant\_id) | Azure Entra tenant ID. Used as the ARM tenant for the building block backplanes. | `string` | n/a | yes | +| [foundation](#input\_foundation) | Optional Azure-side foundation this architecture provisions on top of the meshStack wiring. Leave
null to only register the platform, landing zones and building blocks. (The management group
hierarchy is configured separately via `azure_management_groups`.)
`hub`: when set, orders one Azure Hub Network instance — a hub vnet (with optional firewall) in the
connectivity subscription — that spoke networks peer into. The Azure Hub Network building block
itself is always registered.
`policies`: when true, assigns curated Enterprise-Scale policies to the Corp/Online/Sandbox
management groups.
`resource_groups`: extra platform-owned resource groups (name => { location }) created in the
platform subscription. |
object({
hub = optional(object({
address_space = optional(string, "10.0.0.0/22")
hub_vnet_name = optional(string, "hub-vnet")
hub_resource_group_name = optional(string, "hub-network")
create_gateway_subnet = optional(bool, true)
deploy_firewall = optional(bool, false)
firewall_sku_tier = optional(string, "Standard")
}))
policies = optional(bool, false)
resource_groups = optional(map(object({ location = string })), {})
})
| `null` | no | +| [hub](#input\_hub) | `git_ref`: meshstack-hub reference used to source the nested platform, budget-alert, storage-account and spoke-network integration modules. `const` so it can be interpolated into the module source at init time.
`bbd_draft`: Forwarded as-is to those nested integrations' own `hub.bbd_draft`, so their building block definition draft state tracks this architecture's own release state. |
object({
git_ref = optional(string, "main")
bbd_draft = optional(bool, true)
})
|
{
"bbd_draft": true,
"git_ref": "main"
}
| no | +| [platform\_identifier](#input\_platform\_identifier) | Identifier for the Azure platform created in meshStack (letters, digits and dashes only). Landing zone names are derived as `-`. | `string` | n/a | yes | +| [playground\_mode](#input\_playground\_mode) | Deploy a throwaway platform: the platform identifier gets a random suffix so it does not occupy a name for good across the meshStack instance. Set to false for a platform that is actually used. A playground platform and the building block definitions it registers are not meant to be published to other workspaces. | `bool` | n/a | yes | +| [tags](#input\_tags) | Tags forwarded to the nested integrations.
`landingzone` tags are applied to the created landing zones.
`building_block` tags are applied to the nested building block definitions (budget alert, storage account, spoke network). |
object({
landingzone = map(list(string))
building_block = map(list(string))
})
| n/a | yes | +| [use\_global\_location](#input\_use\_global\_location) | Use the global meshStack location instead of creating a dedicated location for this platform. | `bool` | n/a | yes | +| [workspace](#input\_workspace) | Identifier of the meshStack workspace that will own the created platform, location, landing zones and building block definitions. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +| ---- | ----------- | +| [budget\_alert\_bbd](#output\_budget\_alert\_bbd) | Reference to the Azure Budget Alert building block definition registered by this architecture. | +| [hub\_network\_bbd](#output\_hub\_network\_bbd) | Reference to the Azure Hub Network building block definition registered by this architecture, or null when foundation.hub is not set. | +| [landingzone\_names](#output\_landingzone\_names) | meshStack landing zone names created per archetype. | +| [landingzone\_refs](#output\_landingzone\_refs) | References to the created landing zones, keyed by archetype (`corp`, `online`, `sandbox`). | +| [platform\_ref](#output\_platform\_ref) | Reference to the meshPlatform this architecture creates, for compositions that create meshTenants (subscriptions) on it. | +| [spoke\_network\_bbd](#output\_spoke\_network\_bbd) | Reference to the Azure Spoke Network building block definition registered by this architecture. | +| [storage\_account\_bbd](#output\_storage\_account\_bbd) | Reference to the Azure Storage Account building block definition registered by this architecture. | +| [summary](#output\_summary) | Summary of the meshStack resources created by this reference architecture. | + diff --git a/reference-architectures/azure-landingzone/buildingblock/SUMMARY.md.tftpl b/reference-architectures/azure-landingzone/buildingblock/SUMMARY.md.tftpl new file mode 100644 index 00000000..05c35ab4 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/SUMMARY.md.tftpl @@ -0,0 +1,56 @@ +# Azure Landing Zone: **${platform_identifier}** + +%{ if playground_mode ~} +> **Playground mode.** The platform identifier carries a random suffix so this deployment does not +> occupy a name for good, and nothing here is protected against deletion. Do not publish this +> platform or the building block definitions it registered to other workspaces. Redeploy with +> `playground_mode` set to false for a platform that is actually used. + +%{ endif ~} +## Platform + +The **Azure Subscription** platform `${platform_identifier}` is registered in meshStack. Its +replicator and metering identities are scoped to the `${management_group}` management group. + +## Landing Zones + +| Archetype | Landing Zone | Management Group | +|-----------|--------------|------------------| +| Corp | `${landingzone_names["corp"]}` | `${corp_management_group}` | +| Online | `${landingzone_names["online"]}` | `${online_mgmt_group}` | +| Sandbox | `${landingzone_names["sandbox"]}` | `${sandbox_mgmt_group}` | + +Application teams request Azure subscriptions through one of these landing zones; each subscription +is placed in the archetype's management group. + +## Building Blocks + +The following building blocks are registered and available to order. Each has its own backplane +identity with a deploy role. + +- **Azure Hub Network** — the central hub vnet spoke networks peer into. +- **Azure Spoke Network** — a spoke vnet peered into the hub vnet `${hub_vnet}` + (resource group `${hub_resource_group}`). Best paired with the **Corp** landing zone. +- **Azure Budget Alert** — consumption budget alerts. +- **Azure Storage Account** — self-service storage accounts. + +Backplane identities for the budget-alert and storage-account building blocks live in subscription +`${platform_subscription}`; the spoke-network backplane identity lives in `${backplane_subscription}`. + +## Foundation + +%{ if hub_provisioned ~} +- **Hub network provisioned** — a hub vnet `${hub_vnet}` (resource group `${hub_resource_group}`) was + ordered in the connectivity subscription. Spoke networks peer into it. +%{ else ~} +- **Hub network not provisioned** — spoke networks peer into the existing hub configured via the + `azure_hub_*` inputs. Set `foundation.hub` to have this architecture provision a hub. +%{ endif ~} +%{ if policies_enabled ~} +- **Enterprise-Scale policies assigned** to the Corp, Online and Sandbox management groups. +%{ else ~} +- **Policies not assigned** — set `foundation.policies = true` to assign curated ES policies. +%{ endif ~} +%{ if length(foundation_rg_names) > 0 ~} +- **Platform resource groups created:** ${join(", ", formatlist("`%s`", foundation_rg_names))}. +%{ endif ~} diff --git a/reference-architectures/azure-landingzone/buildingblock/logo.svg b/reference-architectures/azure-landingzone/buildingblock/logo.svg new file mode 100644 index 00000000..71d042af --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/reference-architectures/azure-landingzone/buildingblock/main.tf b/reference-architectures/azure-landingzone/buildingblock/main.tf new file mode 100644 index 00000000..24b057e0 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/main.tf @@ -0,0 +1,273 @@ +locals { + # The identifier is unique across the whole meshStack instance and lands in every landing zone + # name, so a playground deployment suffixes it instead of occupying the plain name. + platform_identifier = var.playground_mode ? "${var.platform_identifier}-${random_string.playground_suffix.result}" : var.platform_identifier + + # ── Foundation ── + hub_enabled = try(var.foundation.hub, null) != null + policies_enabled = try(var.foundation.policies, false) + foundation_rgs = try(var.foundation.resource_groups, {}) + + # Management group identifiers come from the hierarchy this architecture creates under the + # bootstrap scope (azure_management_groups, pre-configured STATIC by the platform team). + lz_management_group = module.management_groups.landing_zones_name + corp_management_group = module.management_groups.corp_name + online_management_group = module.management_groups.online_name + sandbox_management_group = module.management_groups.sandbox_name + connectivity_scope = module.management_groups.connectivity_scope + + # Full resource-path scope for the building block backplanes' RBAC role assignments — the + # landing-zones management group, so one backplane per building block covers Corp, Online and + # Sandbox beneath it. + landing_zones_scope = module.management_groups.landing_zones_scope + + # The spoke-network backplane identity lives in a stable platform-owned subscription. Defaults to + # the platform subscription when no dedicated backplane subscription is given. + backplane_subscription_id = coalesce(var.azure_backplane_subscription_id, var.azure_platform_subscription_id) + + archetype_management_groups = { + corp = local.corp_management_group + online = local.online_management_group + sandbox = local.sandbox_management_group + } + + # Spoke networks peer into the hub this architecture provisions (when foundation.hub is set — it + # lives in the connectivity subscription/scope), otherwise into an existing hub supplied via the + # azure_hub_* variables. + spoke_hub = { + subscription_id = local.hub_enabled ? var.azure_connectivity_subscription_id : var.azure_hub_subscription_id + scope = local.hub_enabled ? local.connectivity_scope : var.azure_hub_scope + resource_group_name = local.hub_enabled ? var.foundation.hub.hub_resource_group_name : var.azure_hub_resource_group_name + vnet_name = local.hub_enabled ? var.foundation.hub.hub_vnet_name : var.azure_hub_vnet_name + } +} + +# ── Enterprise-Scale management group hierarchy (created under the bootstrap scope) ── +module "management_groups" { + source = "./modules/management-groups" + + # Same parent + name_prefix the bootstrap used, so the names match and the import blocks below adopt + # the already-created management groups instead of trying to create duplicates. + name_prefix = var.azure_management_groups.name_prefix + parent_management_group_id = var.azure_management_groups.parent_management_group_id + landing_zones_display_name = var.azure_management_groups.landing_zones_display_name + corp_display_name = var.azure_management_groups.corp_display_name + online_display_name = var.azure_management_groups.online_display_name + sandbox_display_name = var.azure_management_groups.sandbox_display_name + connectivity_display_name = var.azure_management_groups.connectivity_display_name +} + +# Adopt the management groups the bootstrap step already created into this building block's state, so +# meshStack manages them going forward (and the meshplatform module finds them already existing). The +# import IDs match the names the bootstrap used (same parent + name_prefix). Import blocks must live +# in the root module, but can target resources inside module.management_groups. +import { + to = module.management_groups.azurerm_management_group.landing_zones + id = "/providers/Microsoft.Management/managementGroups/${var.azure_management_groups.name_prefix}landing-zones" +} +import { + to = module.management_groups.azurerm_management_group.corp + id = "/providers/Microsoft.Management/managementGroups/${var.azure_management_groups.name_prefix}corp" +} +import { + to = module.management_groups.azurerm_management_group.online + id = "/providers/Microsoft.Management/managementGroups/${var.azure_management_groups.name_prefix}online" +} +import { + to = module.management_groups.azurerm_management_group.sandbox + id = "/providers/Microsoft.Management/managementGroups/${var.azure_management_groups.name_prefix}sandbox" +} +import { + to = module.management_groups.azurerm_management_group.connectivity + id = "/providers/Microsoft.Management/managementGroups/${var.azure_management_groups.name_prefix}connectivity" +} + +resource "random_string" "playground_suffix" { + lifecycle { + enabled = var.playground_mode + } + + length = 6 + special = false + upper = false +} + +# Dedicated meshStack location for the platform, unless the global location is used. +resource "meshstack_location" "this" { + lifecycle { + enabled = !var.use_global_location + } + + metadata = { + name = local.platform_identifier + owned_by_workspace = var.workspace + } + + spec = { + display_name = local.platform_identifier + description = "Azure location created by the Azure Landing Zone reference architecture." + } +} + +# ── Azure platform + Corp/Online/Sandbox landing zones ── +# Registers the Azure Subscription platform in meshStack and creates one landing zone per +# Enterprise-Scale archetype, each pointing at the management group this architecture created under +# the bootstrap scope (see module.management_groups / local.*_management_group). +module "azure_platform" { + source = "github.com/meshcloud/meshstack-hub//modules/azure?ref=${var.hub.git_ref}" + + azure_management_group = local.lz_management_group + azure_subscription_provisioning = var.azure_subscription_provisioning + azure_subscription_owner_object_ids = var.azure_subscription_owner_object_ids + + landing_zones = { + corp = { + management_group_id = local.corp_management_group + display_name = "Azure Corp" + description = "Corp-connected landing zone: subscriptions are placed in the Corp management group for internal, hub-connected workloads. Order the Azure Spoke Network building block inside a project for routed connectivity to the hub." + } + online = { + management_group_id = local.online_management_group + display_name = "Azure Online" + description = "Internet-facing landing zone: subscriptions are placed in the Online management group for public-facing workloads without a mandatory hub connection." + } + sandbox = { + management_group_id = local.sandbox_management_group + display_name = "Azure Sandbox" + description = "Experimentation landing zone: subscriptions are placed in the Sandbox management group with relaxed guardrails for trying things out." + } + } + + meshstack = { + owning_workspace_identifier = var.workspace + platform_name = local.platform_identifier + location_name = var.use_global_location ? "global" : meshstack_location.this.metadata.name + tags = var.tags.landingzone + } + + hub = var.hub +} + +# ── Building blocks rolled out for the platform ── +# Each integration creates its own backplane (a UAMI federated to the building block definition, +# with a deploy role scoped to the landing-zones management group) and registers the definition, so +# application teams can order these into subscriptions created through the landing zones. + +module "budget_alert" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/budget-alert?ref=${var.hub.git_ref}" + + azure_tenant_id = var.azure_tenant_id + azure_subscription_id = var.azure_platform_subscription_id + azure_scope = local.landing_zones_scope + azure_location = var.azure_location + + meshstack = { + owning_workspace_identifier = var.workspace + tags = var.tags.building_block + } + hub = var.hub +} + +module "storage_account" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/storage-account?ref=${var.hub.git_ref}" + + azure_tenant_id = var.azure_tenant_id + azure_subscription_id = var.azure_platform_subscription_id + azure_scope = local.landing_zones_scope + azure_location = var.azure_location + + meshstack = { + owning_workspace_identifier = var.workspace + tags = var.tags.building_block + } + hub = var.hub +} + +module "spoke_network" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/spoke-network?ref=${var.hub.git_ref}" + + azure_tenant_id = var.azure_tenant_id + azure_hub_subscription_id = local.spoke_hub.subscription_id + azure_scope = local.landing_zones_scope + azure_hub_scope = local.spoke_hub.scope + azure_location = var.azure_location + azure_hub_resource_group_name = local.spoke_hub.resource_group_name + azure_hub_vnet_name = local.spoke_hub.vnet_name + azure_backplane_subscription_id = local.backplane_subscription_id + + meshstack = { + owning_workspace_identifier = var.workspace + tags = var.tags.building_block + } + hub = var.hub +} + +# ── Optional foundation ── +# Provisioned only when var.foundation is set. Leaving it null keeps the architecture to the +# meshStack-side wiring (platform, landing zones and the three building blocks above). + +# Extra platform-owned resource groups (e.g. management/connectivity groups) in the platform +# subscription. +resource "azurerm_resource_group" "foundation" { + for_each = local.foundation_rgs + + name = each.key + location = each.value.location +} + +# Enterprise-Scale policy assignments on the existing Corp/Online/Sandbox management groups. +module "es_policies" { + for_each = local.policies_enabled ? local.archetype_management_groups : {} + + source = "./modules/es-policies" + + management_group_id = "/providers/Microsoft.Management/managementGroups/${each.value}" + policy_path = "${path.module}/policies/${each.key}" + location = var.azure_location + template_file_variables = { default_location = var.azure_location } +} + +# Central hub network: registers the Azure Hub Network building block (the connectivity counterpart +# to spoke-network). Always registered — it cannot be gated with `enabled` because its backplane +# carries its own provider configuration. Whether a hub vnet is actually provisioned is controlled +# by ordering an instance below (meshstack_building_block.hub), gated by var.foundation.hub. +module "hub_network" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/hub-network?ref=${var.hub.git_ref}" + + azure_tenant_id = var.azure_tenant_id + azure_connectivity_subscription_id = var.azure_connectivity_subscription_id + azure_scope = local.connectivity_scope + azure_location = var.azure_location + + meshstack = { + owning_workspace_identifier = var.workspace + tags = var.tags.building_block + } + hub = var.hub +} + +resource "meshstack_building_block" "hub" { + lifecycle { + enabled = local.hub_enabled + } + + wait_for_completion = true + depends_on = [module.hub_network] + + spec = { + building_block_definition_version_ref = { + uuid = module.hub_network.building_block_definition.version_ref.uuid + } + display_name = "Hub Network" + target_ref = { kind = "meshWorkspace", name = var.workspace } + + inputs = { + hub_resource_group_name = { value = jsonencode(try(var.foundation.hub.hub_resource_group_name, "hub-network")) } + hub_vnet_name = { value = jsonencode(try(var.foundation.hub.hub_vnet_name, "hub-vnet")) } + address_space = { value = jsonencode(try(var.foundation.hub.address_space, "10.0.0.0/22")) } + create_gateway_subnet = { value = jsonencode(try(var.foundation.hub.create_gateway_subnet, true)) } + deploy_firewall = { value = jsonencode(try(var.foundation.hub.deploy_firewall, false)) } + firewall_sku_tier = { value = jsonencode(try(var.foundation.hub.firewall_sku_tier, "Standard")) } + } + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/README.md b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/README.md new file mode 100644 index 00000000..b3a0f505 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/README.md @@ -0,0 +1,20 @@ +# Enterprise-Scale Policies (foundation glue) + +A trimmed port of collie-hub's [`kit/azure/util/azure-policies`](https://github.com/meshcloud/collie-hub/tree/main/kit/azure/util/azure-policies) +helper. It reads policy JSON files from `policy_path` and applies them to a single management group: + +- `policy_definitions/*.json` → custom policy definitions +- `policy_assignments/*.tmpl.json` → management-group policy assignments (templated) + +Policy set (initiative) support from the upstream helper is intentionally omitted here: azurerm v5 +reworked `azurerm_policy_set_definition` and the shipped libs use built-in definitions only, so no +initiatives are needed. + +`*.tmpl.json` files are expanded with `templatefile()` using `template_file_variables` (e.g. +`default_location`), so assignments can reference scopes and regions. + +This is **foundation glue** for the Azure Landing Zone reference architecture — the caller +instantiates it once per archetype (Corp/Online/Sandbox), pointing `policy_path` at the archetype's +curated policy lib under [`../../policies/`](../../policies) and `management_group_id` at the +archetype's existing management group. The shipped libs use only built-in Azure policy definitions +(allowed locations; no public IPs on NICs for Corp), so no custom definitions are required. diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/outputs.tf b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/outputs.tf new file mode 100644 index 00000000..22744703 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/outputs.tf @@ -0,0 +1,34 @@ +# we use the actual resource to generate outputs as this is what's actually deployed on Azure after all templating +# and interpolating is fully applied + +# We expose a subset of interesting data as this is mostly used to generate documentation. + +# Why jsondecode(paremeters)? +# For whatever reason azurerm really likes encoded json strings for parameters, which are a PITA to work with +# so we just decode them back to HCL + +output "policy_definitions" { + value = { for k, v in azurerm_policy_definition.enterprise_scale : + v.name => { + name = v.name, + display_name = v.display_name, + description = v.description, + parameters = jsondecode(coalesce(v.parameters, "{}")) + } + } +} + +output "policy_assignments" { + value = { for k, v in azurerm_management_group_policy_assignment.enterprise_scale : + v.name => { + id = v.id, + name = v.name, + display_name = v.display_name, + description = v.description, + identity = v.identity, + not_scopes = v.not_scopes + enforce = v.enforce, + parameters = jsondecode(coalesce(v.parameters, "{}")) + } + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/resources.policy_assignments.tf b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/resources.policy_assignments.tf new file mode 100644 index 00000000..a7ed6635 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/resources.policy_assignments.tf @@ -0,0 +1,84 @@ + +locals { + # policy assignments need some lightweight templating to make sure they can reference the right definition ids, scopes etc. + # this is why we store them as tmpl.jsons and run them trough templatefile + policy_assignment_files = fileset(var.policy_path, "policy_assignments/*.tmpl.json") + policy_assignment_objects = { for f in local.policy_assignment_files : + f => jsondecode(templatefile("${var.policy_path}/${f}", var.template_file_variables)) + } + + policy_assignments = { for f, p in local.policy_assignment_objects : + f => { + name = p.name, + display_name = try(p.properties.displayName, null) + description = try(p.properties.description, null) + + policy_definition_id = try(p.properties.policyDefinitionId, null) + + parameters = try(p.properties.parameters, null) + not_scopes = try(p.properties.notScopes, null) + enforcement_mode = try(p.properties.enforce, null) + identity = try(p.identity, null) + override = try(p.properties.override, null) + policy_non_compliance_message_enabled = try(p.properties.nonComplianceMessages, null) + } + } +} + +resource "azurerm_management_group_policy_assignment" "enterprise_scale" { + for_each = local.policy_assignments + + # Mandatory resource attributes + # The policy assignment name length must not exceed '24' characters + # note that Terraform plan is unable to validate this in the plan stage + name = each.value.name + management_group_id = var.management_group_id + + policy_definition_id = each.value.policy_definition_id + + # Optional resource attributes + description = each.value.description + display_name = each.value.display_name + + location = var.location + not_scopes = each.value.not_scopes + parameters = length(each.value.parameters) > 0 ? jsonencode(each.value.parameters) : null + enforce = each.value.enforcement_mode + + # Dynamic configuration blocks for overrides + # More details can be found here: https://learn.microsoft.com/en-gb/azure/governance/policy/concepts/assignment-structure#overrides-preview + dynamic "overrides" { + for_each = try({ for i, override in each.value.override : i => override }, {}) + content { + value = overrides.value.value + dynamic "selectors" { + for_each = try({ for i, selector in overrides.value.selectors : i => selector }, {}) + content { + in = try(selectors.in, []) + not_in = try(selectors.not_in, []) + } + } + } + } + + # Dynamic configuration blocks + # The identity block only supports a single value + # for type = "SystemAssigned" so the following logic + # ensures the block is only created when this value + # is specified in the source template + dynamic "identity" { + for_each = { + for ik, iv in try(each.value.identity, {}) : + ik => iv + if lower(iv) == "systemassigned" + } + content { + type = "SystemAssigned" + } + } + + # deploy assignments after definitions + depends_on = [ + azurerm_policy_definition.enterprise_scale + ] +} diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/resources.policy_definitions.tf b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/resources.policy_definitions.tf new file mode 100644 index 00000000..3941f00b --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/resources.policy_definitions.tf @@ -0,0 +1,50 @@ +locals { + policy_definition_files = fileset(var.policy_path, "policy_definitions/*.json") + + policy_definition_objects = { for f in local.policy_definition_files : + f => jsondecode(file("${var.policy_path}/${f}")) + } + + policy_definitions = { for f, p in local.policy_definition_objects : + f => { + #related to policies only + policy_name = p.name + policy_rule = try(p.properties.policyRule, null) + mode = try(p.properties.mode, "All") + + # used local library attributes + display_name = try(p.properties.displayName, null) + description = try(p.properties.description, null) + + metadata = try(p.properties.metadata, {}) + + parameters = try(p.properties.parameters, null) + version = try(p.properties.metadata.version, "1.0.0") + category = try(p.properties.metadata.category, "General") + } + } +} + +resource "azurerm_policy_definition" "enterprise_scale" { + for_each = local.policy_definitions + + name = each.value.policy_name + display_name = each.value.display_name + description = each.value.description + policy_type = "Custom" + mode = each.value.mode + + management_group_id = var.management_group_id + + metadata = jsonencode(each.value.metadata) + parameters = length(each.value.parameters) > 0 ? jsonencode(each.value.parameters) : null + policy_rule = jsonencode(each.value.policy_rule) + + lifecycle { + create_before_destroy = true + } + + timeouts { + read = "10m" + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/variables.tf b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/variables.tf new file mode 100644 index 00000000..be557054 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/variables.tf @@ -0,0 +1,20 @@ +variable "management_group_id" { + type = string + description = "The management group scope at which the policy will be defined. Defaults to current Subscription if omitted. Changing this forces a new resource to be created." +} + +variable "policy_path" { + type = string + description = "path of the json policies, sets or assignments" +} + +variable "location" { + type = string + description = "location for the policy assignment" +} + +variable "template_file_variables" { + type = map(string) + description = "variables for *.tmpl.json files, expanded with terraform templatefile() function" + +} \ No newline at end of file diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/versions.tf b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/versions.tf new file mode 100644 index 00000000..c3f74a3a --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/es-policies/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.12.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.36.0" + } + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/README.md b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/README.md new file mode 100644 index 00000000..d4dd7291 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/README.md @@ -0,0 +1,25 @@ +# Management Groups (foundation glue) + +Creates the Enterprise-Scale management group hierarchy for the Azure Landing Zone reference +architecture, under a given parent (typically the tenant root group): + +``` + +├── Landing Zones # platform replicator/metering + building block backplane scope +│ ├── Corp +│ ├── Online +│ └── Sandbox +└── Connectivity # hosts the hub subscription; hub-network backplane scope +``` + +Management groups are created with display names only; Azure generates their names (IDs). The module +outputs each group's `name` (for meshStack platform/landing-zone references) and, where needed, its +full `scope` path (for policy assignments and RBAC). + +This is **foundation glue** for the reference architecture. The caller (the reference architecture's +`buildingblock`) drives it from the `azure_management_groups` variable, whose +`parent_management_group_id` is pre-configured STATIC to the bootstrap scope — so the hierarchy is +created under the same management group the bootstrap identity owns. + +The applying identity needs permission to create management groups (Management Group Contributor at +the parent, or Owner on it — which the bootstrap identity has). diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/main.tf b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/main.tf new file mode 100644 index 00000000..bfcb29f1 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/main.tf @@ -0,0 +1,49 @@ +locals { + # Accept either a bare management group name (e.g. the tenant ID for the tenant root group) or a + # full resource path, and normalise to the full path azurerm expects for a parent. + parent_id = startswith(var.parent_management_group_id, "/providers/Microsoft.Management/managementGroups/") ? var.parent_management_group_id : "/providers/Microsoft.Management/managementGroups/${var.parent_management_group_id}" + + # Explicit, deterministic management group names (IDs) — NOT Azure-generated GUIDs. This keeps the + # names known at plan time, which is required because the platform module (meshplatform) does a + # for_each over the landing-zones scope; an unknown (apply-time) name breaks that for_each. + # `name_prefix` keeps them unique across the tenant (e.g. "-"). + lz_name = "${var.name_prefix}landing-zones" + corp_name = "${var.name_prefix}corp" + online_name = "${var.name_prefix}online" + sandbox_name = "${var.name_prefix}sandbox" + connectivity_name = "${var.name_prefix}connectivity" +} + +# The "Landing Zones" management group holds the archetype groups and is the scope the platform's +# replicator/metering identities and the building block backplanes operate on. +resource "azurerm_management_group" "landing_zones" { + name = local.lz_name + display_name = var.landing_zones_display_name + parent_management_group_id = local.parent_id +} + +resource "azurerm_management_group" "corp" { + name = local.corp_name + display_name = var.corp_display_name + parent_management_group_id = azurerm_management_group.landing_zones.id +} + +resource "azurerm_management_group" "online" { + name = local.online_name + display_name = var.online_display_name + parent_management_group_id = azurerm_management_group.landing_zones.id +} + +resource "azurerm_management_group" "sandbox" { + name = local.sandbox_name + display_name = var.sandbox_display_name + parent_management_group_id = azurerm_management_group.landing_zones.id +} + +# Connectivity management group — the hub subscription lives here; the hub-network backplane role is +# scoped to it. A sibling of Landing Zones under the same parent. +resource "azurerm_management_group" "connectivity" { + name = local.connectivity_name + display_name = var.connectivity_display_name + parent_management_group_id = local.parent_id +} diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/outputs.tf b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/outputs.tf new file mode 100644 index 00000000..2a7d0915 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/outputs.tf @@ -0,0 +1,37 @@ +# meshStack and the building block backplanes reference management groups by name; policy +# assignments and RBAC scopes need the full resource path. Expose both for each group. + +output "landing_zones_name" { + value = azurerm_management_group.landing_zones.name + description = "Name (ID) of the Landing Zones management group." +} + +output "landing_zones_scope" { + value = azurerm_management_group.landing_zones.id + description = "Full resource path of the Landing Zones management group." +} + +output "corp_name" { + value = azurerm_management_group.corp.name + description = "Name (ID) of the Corp management group." +} + +output "online_name" { + value = azurerm_management_group.online.name + description = "Name (ID) of the Online management group." +} + +output "sandbox_name" { + value = azurerm_management_group.sandbox.name + description = "Name (ID) of the Sandbox management group." +} + +output "connectivity_name" { + value = azurerm_management_group.connectivity.name + description = "Name (ID) of the Connectivity management group." +} + +output "connectivity_scope" { + value = azurerm_management_group.connectivity.id + description = "Full resource path of the Connectivity management group." +} diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/variables.tf b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/variables.tf new file mode 100644 index 00000000..611ce309 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/variables.tf @@ -0,0 +1,47 @@ +variable "parent_management_group_id" { + type = string + nullable = false + description = "Parent management group under which the hierarchy is created — a bare management group name (e.g. the tenant ID for the tenant root group) or a full '/providers/Microsoft.Management/managementGroups/' path." +} + +variable "name_prefix" { + type = string + nullable = false + default = "" + description = "Prefix for the created management group names (IDs), e.g. '-', to keep them unique across the tenant. Must be known at plan time (do not derive it from apply-time values like a random suffix)." +} + +variable "landing_zones_display_name" { + type = string + nullable = false + default = "Landing Zones" + description = "Display name of the parent management group that holds the archetype groups." +} + +variable "corp_display_name" { + type = string + nullable = false + default = "Corp" + description = "Display name of the Corp management group." +} + +variable "online_display_name" { + type = string + nullable = false + default = "Online" + description = "Display name of the Online management group." +} + +variable "sandbox_display_name" { + type = string + nullable = false + default = "Sandbox" + description = "Display name of the Sandbox management group." +} + +variable "connectivity_display_name" { + type = string + nullable = false + default = "Connectivity" + description = "Display name of the Connectivity management group that hosts the hub subscription." +} diff --git a/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/versions.tf b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/versions.tf new file mode 100644 index 00000000..c3f74a3a --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/modules/management-groups/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.12.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.36.0" + } + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/outputs.tf b/reference-architectures/azure-landingzone/buildingblock/outputs.tf new file mode 100644 index 00000000..b9df6a64 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/outputs.tf @@ -0,0 +1,54 @@ +output "platform_ref" { + description = "Reference to the meshPlatform this architecture creates, for compositions that create meshTenants (subscriptions) on it." + value = module.azure_platform.platform_ref +} + +output "landingzone_refs" { + description = "References to the created landing zones, keyed by archetype (`corp`, `online`, `sandbox`)." + value = module.azure_platform.landingzone_refs +} + +output "landingzone_names" { + description = "meshStack landing zone names created per archetype." + value = module.azure_platform.landingzone_names +} + +output "budget_alert_bbd" { + description = "Reference to the Azure Budget Alert building block definition registered by this architecture." + value = module.budget_alert.building_block_definition +} + +output "storage_account_bbd" { + description = "Reference to the Azure Storage Account building block definition registered by this architecture." + value = module.storage_account.building_block_definition +} + +output "spoke_network_bbd" { + description = "Reference to the Azure Spoke Network building block definition registered by this architecture." + value = module.spoke_network.building_block_definition +} + +output "hub_network_bbd" { + description = "Reference to the Azure Hub Network building block definition registered by this architecture, or null when foundation.hub is not set." + value = local.hub_enabled ? module.hub_network.building_block_definition : null +} + +output "summary" { + description = "Summary of the meshStack resources created by this reference architecture." + value = templatefile("${path.module}/SUMMARY.md.tftpl", { + platform_identifier = local.platform_identifier + playground_mode = var.playground_mode + management_group = local.lz_management_group + corp_management_group = local.corp_management_group + online_mgmt_group = local.online_management_group + sandbox_mgmt_group = local.sandbox_management_group + landingzone_names = module.azure_platform.landingzone_names + hub_vnet = local.spoke_hub.vnet_name + hub_resource_group = local.spoke_hub.resource_group_name + platform_subscription = var.azure_platform_subscription_id + backplane_subscription = local.backplane_subscription_id + hub_provisioned = local.hub_enabled + policies_enabled = local.policies_enabled + foundation_rg_names = keys(local.foundation_rgs) + }) +} diff --git a/reference-architectures/azure-landingzone/buildingblock/policies/corp/policy_assignments/allowed_locations.tmpl.json b/reference-architectures/azure-landingzone/buildingblock/policies/corp/policy_assignments/allowed_locations.tmpl.json new file mode 100644 index 00000000..9c364794 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/policies/corp/policy_assignments/allowed_locations.tmpl.json @@ -0,0 +1,14 @@ +{ + "name": "allowed-locations", + "properties": { + "displayName": "Corp: Allowed locations", + "description": "Restricts the regions Corp resources can be deployed to.", + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c", + "enforce": true, + "parameters": { + "listOfAllowedLocations": { + "value": ["${default_location}"] + } + } + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/policies/corp/policy_assignments/deny_public_ip_nic.tmpl.json b/reference-architectures/azure-landingzone/buildingblock/policies/corp/policy_assignments/deny_public_ip_nic.tmpl.json new file mode 100644 index 00000000..a6d33e9e --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/policies/corp/policy_assignments/deny_public_ip_nic.tmpl.json @@ -0,0 +1,10 @@ +{ + "name": "deny-public-ip-nic", + "properties": { + "displayName": "Corp: Network interfaces should not have public IPs", + "description": "Denies network interfaces with a public IP so Corp workloads stay behind the hub.", + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/83a86a26-fd1f-447c-b59d-e51f44264114", + "enforce": true, + "parameters": {} + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/policies/online/policy_assignments/allowed_locations.tmpl.json b/reference-architectures/azure-landingzone/buildingblock/policies/online/policy_assignments/allowed_locations.tmpl.json new file mode 100644 index 00000000..bfac9dc8 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/policies/online/policy_assignments/allowed_locations.tmpl.json @@ -0,0 +1,14 @@ +{ + "name": "allowed-locations", + "properties": { + "displayName": "Online: Allowed locations", + "description": "Restricts the regions Online resources can be deployed to. Public endpoints are allowed for internet-facing workloads.", + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c", + "enforce": true, + "parameters": { + "listOfAllowedLocations": { + "value": ["${default_location}"] + } + } + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/policies/sandbox/policy_assignments/allowed_locations_audit.tmpl.json b/reference-architectures/azure-landingzone/buildingblock/policies/sandbox/policy_assignments/allowed_locations_audit.tmpl.json new file mode 100644 index 00000000..3d6db06b --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/policies/sandbox/policy_assignments/allowed_locations_audit.tmpl.json @@ -0,0 +1,14 @@ +{ + "name": "allowed-locations-audit", + "properties": { + "displayName": "Sandbox: Allowed locations (audit)", + "description": "Audits the regions Sandbox resources are deployed to without blocking, keeping experimentation loose.", + "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c", + "enforce": false, + "parameters": { + "listOfAllowedLocations": { + "value": ["${default_location}"] + } + } + } +} diff --git a/reference-architectures/azure-landingzone/buildingblock/provider.tf b/reference-architectures/azure-landingzone/buildingblock/provider.tf new file mode 100644 index 00000000..46346cf8 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/provider.tf @@ -0,0 +1,11 @@ +# The onboarding run authenticates with ambient Azure credentials: `az login` when a platform +# engineer applies this locally, or `ARM_*` environment variables (workload identity federation) +# when an IaC runtime executes it. The identity needs Owner on the landing-zones management group +# and Entra Application Administrator, because the run creates the platform service principals, +# management-group role assignments and the building block backplane identities. +provider "azurerm" { + features {} + subscription_id = var.azure_platform_subscription_id +} + +provider "azuread" {} diff --git a/reference-architectures/azure-landingzone/buildingblock/variables.tf b/reference-architectures/azure-landingzone/buildingblock/variables.tf new file mode 100644 index 00000000..4440bb0c --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/variables.tf @@ -0,0 +1,224 @@ +variable "workspace" { + type = string + nullable = false + description = "Identifier of the meshStack workspace that will own the created platform, location, landing zones and building block definitions." +} + +variable "use_global_location" { + type = bool + nullable = false + description = "Use the global meshStack location instead of creating a dedicated location for this platform." +} + +variable "platform_identifier" { + type = string + nullable = false + description = "Identifier for the Azure platform created in meshStack (letters, digits and dashes only). Landing zone names are derived as `-`." + + validation { + condition = can(regex("^[a-zA-Z0-9-]+$", var.platform_identifier)) + error_message = "platform_identifier must only contain letters, digits, and dashes." + } +} + +variable "playground_mode" { + type = bool + nullable = false + description = "Deploy a throwaway platform: the platform identifier gets a random suffix so it does not occupy a name for good across the meshStack instance. Set to false for a platform that is actually used. A playground platform and the building block definitions it registers are not meant to be published to other workspaces." +} + +variable "tags" { + type = object({ + landingzone = map(list(string)) + building_block = map(list(string)) + }) + nullable = false + description = <<-EOT + Tags forwarded to the nested integrations. + `landingzone` tags are applied to the created landing zones. + `building_block` tags are applied to the nested building block definitions (budget alert, storage account, spoke network). + EOT +} + +# ── Azure platform (existing management group hierarchy is assumed) ── + +variable "azure_tenant_id" { + type = string + nullable = false + description = "Azure Entra tenant ID. Used as the ARM tenant for the building block backplanes." +} + +variable "azure_subscription_provisioning" { + type = object({ + pre_provisioned = optional(object({ + unused_subscription_name_prefix = optional(string, "unused-") + })) + customer_agreement = optional(object({ + billing_account_name = string + billing_profile_name = string + invoice_section_name = string + })) + }) + nullable = false + default = { pre_provisioned = {} } + description = <<-EOT + Azure subscription provisioning model — set exactly one: + `pre_provisioned` (default): meshStack assigns subscriptions from a pool of existing ones whose name starts with `unused_subscription_name_prefix` (default `unused-`). No MCA service principal is created. + `customer_agreement`: meshStack creates subscriptions via the given MCA billing scope. + EOT + + validation { + condition = (var.azure_subscription_provisioning.pre_provisioned != null) != (var.azure_subscription_provisioning.customer_agreement != null) + error_message = "Set exactly one of pre_provisioned or customer_agreement." + } +} + +variable "azure_subscription_owner_object_ids" { + type = list(string) + default = null + description = "Optional explicit subscription owner object IDs. If null, the applying principal is used." +} + +variable "azure_location" { + type = string + nullable = false + default = "germanywestcentral" + description = "Azure region where the building block backplane resource groups and identities are created." +} + +# ── Building block target/backplane placement ── + +variable "azure_platform_subscription_id" { + type = string + nullable = false + description = "Bare GUID of a platform-owned subscription. The azurerm provider targets it, the budget-alert and storage-account backplanes are created in it, and (as written) those two building blocks deploy their resources into it." + + validation { + condition = can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.azure_platform_subscription_id)) + error_message = "azure_platform_subscription_id must be a bare subscription GUID, not a '/subscriptions/' path." + } +} + +# ── Spoke network hub (existing central hub is assumed) ── + +variable "azure_connectivity_subscription_id" { + type = string + nullable = false + description = "Bare GUID of the connectivity subscription where the Azure Hub Network backplane identity lives and, when foundation.hub is set, the hub vnet and firewall are created." + + validation { + condition = can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.azure_connectivity_subscription_id)) + error_message = "azure_connectivity_subscription_id must be a bare subscription GUID, not a '/subscriptions/' path." + } +} + +# The azure_hub_* variables describe an EXISTING hub for spoke networks to peer into. Leave them +# null when you let the architecture provision the hub via var.foundation.hub — the spoke wiring then +# uses the provisioned hub's coordinates (in the connectivity subscription/scope) instead. + +variable "azure_hub_subscription_id" { + type = string + default = null + description = "Bare GUID of the subscription hosting an existing central hub vnet that spoke networks peer into. Leave null when foundation.hub creates the hub." + + validation { + condition = var.azure_hub_subscription_id == null || can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.azure_hub_subscription_id)) + error_message = "azure_hub_subscription_id must be a bare subscription GUID, not a '/subscriptions/' path." + } +} + +variable "azure_hub_scope" { + type = string + default = null + description = "Full resource path where the spoke-network backplane's hub-peering role is granted: a management group or subscription path containing an existing hub vnet. Leave null when foundation.hub creates the hub." + + validation { + condition = var.azure_hub_scope == null || can(regex("^/subscriptions/[0-9a-fA-F-]{36}$|^/providers/Microsoft\\.Management/managementGroups/.+$", var.azure_hub_scope)) + error_message = "azure_hub_scope must be a full resource path: '/subscriptions/' or '/providers/Microsoft.Management/managementGroups/'." + } +} + +variable "azure_hub_resource_group_name" { + type = string + default = null + description = "Name of the resource group that contains an existing hub vnet spoke networks peer into. Leave null when foundation.hub creates the hub." +} + +variable "azure_hub_vnet_name" { + type = string + default = null + description = "Name of an existing hub vnet spoke networks peer into. Leave null when foundation.hub creates the hub." +} + +variable "azure_management_groups" { + type = object({ + parent_management_group_id = string + name_prefix = optional(string, "") + landing_zones_display_name = optional(string, "Landing Zones") + corp_display_name = optional(string, "Corp") + online_display_name = optional(string, "Online") + sandbox_display_name = optional(string, "Sandbox") + connectivity_display_name = optional(string, "Connectivity") + }) + nullable = false + description = <<-EOT + Enterprise-Scale management group hierarchy (Landing Zones → Corp/Online/Sandbox, plus + Connectivity) under `parent_management_group_id`, with names prefixed by `name_prefix`. The + bootstrap step already created these; this building block adopts them via `import` blocks and + manages them. Pre-configured STATIC by the platform team at registration (parent = bootstrap scope, + same name_prefix as the bootstrap) — end users don't specify it. + EOT +} + +variable "foundation" { + type = object({ + hub = optional(object({ + address_space = optional(string, "10.0.0.0/22") + hub_vnet_name = optional(string, "hub-vnet") + hub_resource_group_name = optional(string, "hub-network") + create_gateway_subnet = optional(bool, true) + deploy_firewall = optional(bool, false) + firewall_sku_tier = optional(string, "Standard") + })) + policies = optional(bool, false) + resource_groups = optional(map(object({ location = string })), {}) + }) + default = null + description = <<-EOT + Optional Azure-side foundation this architecture provisions on top of the meshStack wiring. Leave + null to only register the platform, landing zones and building blocks. (The management group + hierarchy is configured separately via `azure_management_groups`.) + `hub`: when set, orders one Azure Hub Network instance — a hub vnet (with optional firewall) in the + connectivity subscription — that spoke networks peer into. The Azure Hub Network building block + itself is always registered. + `policies`: when true, assigns curated Enterprise-Scale policies to the Corp/Online/Sandbox + management groups. + `resource_groups`: extra platform-owned resource groups (name => { location }) created in the + platform subscription. + EOT +} + +variable "azure_backplane_subscription_id" { + type = string + default = null + description = "Optional bare GUID of the subscription where the spoke-network backplane identity is created. Defaults to azure_platform_subscription_id. Typically the hub subscription so the automation identity lives in a stable, platform-owned place." + + validation { + condition = var.azure_backplane_subscription_id == null || can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.azure_backplane_subscription_id)) + error_message = "azure_backplane_subscription_id must be a bare subscription GUID, not a '/subscriptions/' path." + } +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = { git_ref = "main", bbd_draft = true } + + description = <<-EOT + `git_ref`: meshstack-hub reference used to source the nested platform, budget-alert, storage-account and spoke-network integration modules. `const` so it can be interpolated into the module source at init time. + `bbd_draft`: Forwarded as-is to those nested integrations' own `hub.bbd_draft`, so their building block definition draft state tracks this architecture's own release state. + EOT +} diff --git a/reference-architectures/azure-landingzone/buildingblock/versions.tf b/reference-architectures/azure-landingzone/buildingblock/versions.tf new file mode 100644 index 00000000..600db44f --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/versions.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.12.0" # const variables require OpenTofu >= 1.12 / Terraform >= 1.15 + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.24.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.64" + } + azuread = { + source = "hashicorp/azuread" + version = ">= 3.8" + } + random = { + source = "hashicorp/random" + version = ">= 3.6.0" + } + } +} diff --git a/reference-architectures/azure-landingzone/logo.svg b/reference-architectures/azure-landingzone/logo.svg new file mode 100644 index 00000000..71d042af --- /dev/null +++ b/reference-architectures/azure-landingzone/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/reference-architectures/azure-landingzone/meshstack_integration.tf b/reference-architectures/azure-landingzone/meshstack_integration.tf new file mode 100644 index 00000000..1549b57a --- /dev/null +++ b/reference-architectures/azure-landingzone/meshstack_integration.tf @@ -0,0 +1,425 @@ +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) + description = "Shared meshStack context. Tags are optional and propagated to building block definition metadata." +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + + default = { + git_ref = "main" + bbd_draft = true + } + + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of the meshstack-hub repo. + `bbd_draft`: If true, the building block definition version is kept in draft mode. + EOT +} + +variable "playground_mode" { + type = bool + nullable = false + default = true + + description = "Deploy a throwaway platform: the platform identifier gets a random suffix so it does not occupy a name for good, and nothing is protected against deletion. Set to false for a platform that is actually used. Passed to the building block as a STATIC input, so whoever orders the architecture cannot choose. A playground platform and the building block definitions it registers are not meant to be published to other workspaces." +} + +variable "azure_bootstrap_subscription_id" { + type = string + description = "Bare GUID of the subscription where the privileged bootstrap identity (that meshStack runs this architecture as) and its resource group are created." +} + +variable "azure_bootstrap_scope" { + type = string + description = "Full resource path where the bootstrap identity is granted Owner — high enough to cover everything the architecture provisions (management groups it creates, role assignments, subscriptions). Typically the tenant root management group." +} + +variable "azure_location" { + type = string + default = "germanywestcentral" + description = "Azure region for the bootstrap identity's resource group." +} + +variable "azure_management_group_prefix" { + type = string + default = "" + description = "Prefix for the created management group names/IDs (e.g. 'flotest-az-'), to keep them unique across the tenant. The bootstrap creates the MGs with this prefix; the building block imports/manages the same names." +} + +output "building_block_definition" { + description = "BBD is consumed in building block compositions." + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } +} + +data "meshstack_integrations" "this" {} + +# Privileged identity meshStack runs the ordered building block as — federated to this very building +# block definition. Created once by whoever applies this integration (Owner on the scope + able to +# grant Microsoft Graph app roles). Its client id is wired into the ARM_CLIENT_ID input below. +module "bootstrap" { + source = "github.com/meshcloud/meshstack-hub//reference-architectures/azure-landingzone/bootstrap?ref=${var.hub.git_ref}" + + scope = var.azure_bootstrap_scope + subscription_id = var.azure_bootstrap_subscription_id + location = var.azure_location + management_group_name_prefix = var.azure_management_group_prefix + + workload_identity_federation = { + issuer = data.meshstack_integrations.this.workload_identity_federation.replicator.issuer + subjects = [ + "${trimsuffix(data.meshstack_integrations.this.workload_identity_federation.replicator.subject, ":replicator")}:workspace.${var.meshstack.owning_workspace_identifier}.buildingblockdefinition.${meshstack_building_block_definition.this.metadata.uuid}" + ] + } +} + +resource "meshstack_building_block_definition" "this" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags + } + + spec = { + display_name = "Azure Landing Zone Reference Architecture" + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/reference-architectures/azure-landingzone/buildingblock/logo.svg" + description = "Onboards an Azure Subscription platform into meshStack on top of an existing Enterprise-Scale management group hierarchy: creates Corp/Online/Sandbox landing zones and registers the budget-alert, storage-account and spoke-network building blocks." + support_url = "https://portal.azure.com" + target_type = "WORKSPACE_LEVEL" + run_transparency = true + + readme = chomp(<<-EOT + The **Azure Landing Zone** reference architecture turns an existing Azure Enterprise-Scale + management group hierarchy into a self-service-ready meshStack platform in one run. It assumes + the management groups (a parent "Landing Zones" group with Corp, Online and Sandbox beneath it) + and a central network hub already exist — it does not create them. + + Running it once: + - registers the **Azure Subscription** platform in meshStack, + - creates one landing zone per Enterprise-Scale archetype — **Corp** (internal, hub-connected), + **Online** (internet-facing) and **Sandbox** (experimentation) — each pointing at its + management group, and + - registers the **Azure Budget Alert**, **Azure Storage Account** and **Azure Spoke Network** + building blocks, each with its own backplane identity, so application teams can order them. + + ## 🎯 When to use it + + Use this building block when you have an Enterprise-Scale management group hierarchy and want to + onboard it into meshStack as a self-service Azure platform with ready-to-order building blocks, + without hand-wiring the platform, landing zones and backplanes separately. + + ## 💡 Usage + + A platform engineer runs this once for a workspace. It authenticates to Azure with the applying + identity, which needs **Owner** on the landing-zones management group and **Entra Application + Administrator** — for example running locally after `az login`, or through an IaC runtime with + workload-identity-federation `ARM_*` environment variables. + + Application teams then request Azure subscriptions through the Corp, Online or Sandbox landing + zone and order the registered building blocks into them. The spoke-network building block is + best paired with the Corp landing zone for hub-connected workloads. + + ## 🧪 Playground mode + + **Playground Mode** is fixed by whoever deployed this definition and cannot be chosen when + ordering. It defaults to `true`, which deploys a throwaway platform: the platform identifier + gets a random suffix so it does not occupy a name for good across the meshStack instance. Such a + platform and the building block definitions it registers are meant for the deploying workspace + only — do not publish them to other workspaces. Set it to `false` for a platform that is + actually used. + + ## 📊 Shared responsibility + + | Responsibility | Platform Team | Application Team | + |---|:---:|:---:| + | Provide the Azure credentials, management group IDs, billing details and hub network details | ✅ | ❌ | + | Register the Azure platform and the Corp/Online/Sandbox landing zones | ✅ | ❌ | + | Register the budget-alert, storage-account and spoke-network building blocks | ✅ | ❌ | + | Request Azure subscriptions through the landing zones | ❌ | ✅ | + | Order the registered building blocks into their subscriptions | ❌ | ✅ | + | Manage workloads inside the provisioned subscriptions | ❌ | ✅ | + EOT + ) + } + + version_spec = { + draft = var.hub.bbd_draft + deletion_mode = "DELETE" + + # Ephemeral API key permissions for the meshStack resources this building block and its nested + # platform/hub-network/budget-alert/storage-account/spoke-network integrations create. + permissions = [ + "INTEGRATION_LIST", + "BUILDINGBLOCKDEFINITION_LIST", + "BUILDINGBLOCKDEFINITION_SAVE", + "BUILDINGBLOCKDEFINITION_DELETE", + "BUILDINGBLOCK_LIST", + "BUILDINGBLOCK_SAVE", + "BUILDINGBLOCK_DELETE", + "LANDINGZONE_LIST", + "LANDINGZONE_SAVE", + "LANDINGZONE_DELETE", + "PLATFORMINSTANCE_LIST", + "PLATFORMINSTANCE_SAVE", + "PLATFORMINSTANCE_DELETE" + ] + + implementation = { + terraform = { + terraform_version = "1.12.5" + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "reference-architectures/azure-landingzone/buildingblock" + ref_name = var.hub.git_ref + use_mesh_http_backend_fallback = true + } + } + + inputs = { + # ── Azure authentication ── + # The ordered run authenticates as the bootstrap identity via workload identity federation; + # meshStack injects its OIDC token. No secret is stored. + + ARM_CLIENT_ID = { + display_name = "ARM Client ID" + description = "Client ID of the bootstrap managed identity the run authenticates as." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(module.bootstrap.identity.client_id) + } + ARM_TENANT_ID = { + display_name = "ARM Tenant ID" + description = "Azure Entra tenant ID of the bootstrap managed identity." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(module.bootstrap.identity.tenant_id) + } + ARM_USE_OIDC = { + display_name = "ARM Use OIDC" + description = "Enables OIDC-based workload identity federation for the Azure provider." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("true") + } + ARM_OIDC_TOKEN_FILE_PATH = { + display_name = "ARM OIDC Token File Path" + description = "Path to the OIDC token file meshStack mounts for workload identity federation." + type = "STRING" + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("/var/run/secrets/workload-identity/azure/token") + } + + hub = { + display_name = "Hub" + description = "HCL object with `git_ref` (meshstack-hub ref used to source the nested modules) and `bbd_draft` (forwarded to the nested definitions' draft state)." + type = "CODE" + assignment_type = "STATIC" + argument = jsonencode(jsonencode(var.hub)) + } + + # ── meshStack context ── + + workspace = { + display_name = "Workspace Identifier" + description = "Workspace that will own the created platform, location, landing zones and building block definitions." + type = "STRING" + assignment_type = "WORKSPACE_IDENTIFIER" + } + + platform_identifier = { + display_name = "Platform Identifier" + description = "Identifier for the Azure platform created in meshStack (letters, digits and dashes only)." + type = "STRING" + assignment_type = "USER_INPUT" + value_validation_regex = "^[a-zA-Z0-9-]+$" + validation_regex_error_message = "platform_identifier must only contain letters, digits, and dashes." + } + + use_global_location = { + display_name = "Use Global Location" + description = "If true, use the existing global meshStack location instead of creating a dedicated location for this platform." + type = "BOOLEAN" + assignment_type = "USER_INPUT" + default_value = jsonencode(false) + } + + tags = { + display_name = "Tags" + description = "HCL object of tag maps forwarded to the nested integrations: `landingzone` for the landing zones, `building_block` for the registered building block definitions." + type = "CODE" + assignment_type = "USER_INPUT" + updateable_by_consumer = true + + default_value = jsonencode(jsonencode({ + landingzone = {} + building_block = {} + })) + } + + playground_mode = { + display_name = "Playground Mode" + description = "Throwaway deployment: identifier gets a random suffix, nothing is protected from deletion. Do not publish such a platform to other workspaces. Set false for real use." + type = "BOOLEAN" + assignment_type = "STATIC" + argument = jsonencode(var.playground_mode) + } + + # ── Foundation — what Azure-side infra this run provisions (edit the template; remove blocks to skip) ── + + foundation = { + display_name = "Foundation" + description = "HCL object — fill in what to provision: `hub` (hub vnet), `policies` (ES policies), `resource_groups`. Remove a block to skip it. (Management groups are platform-configured, not here.)" + type = "CODE" + assignment_type = "USER_INPUT" + + # A ready-to-edit template (not null): adjust the hub CIDR, toggle policies, and delete any + # block you don't want provisioned. + default_value = jsonencode(jsonencode({ + hub = { + address_space = "10.0.0.0/22" + deploy_firewall = false + } + policies = true + resource_groups = {} + })) + } + + # ── Azure platform ── + + # STATIC — the tenant is the bootstrap identity's tenant, known at registration. + azure_tenant_id = { + display_name = "Azure Tenant ID" + description = "Azure Entra tenant ID (the bootstrap identity's tenant)." + type = "STRING" + assignment_type = "STATIC" + argument = jsonencode(module.bootstrap.identity.tenant_id) + } + + # Platform-configured (STATIC): creates the Corp/Online/Sandbox/Connectivity hierarchy under the + # bootstrap scope. End users don't configure management groups. + azure_management_groups = { + display_name = "Management Groups" + description = "Platform-configured: creates the Corp/Online/Sandbox/Connectivity management group hierarchy under the bootstrap scope." + type = "CODE" + assignment_type = "STATIC" + argument = jsonencode(jsonencode({ + parent_management_group_id = var.azure_bootstrap_scope + name_prefix = var.azure_management_group_prefix + })) + } + + azure_subscription_provisioning = { + display_name = "Subscription Provisioning" + description = "HCL object — set exactly one: `pre_provisioned` (assign from a pool named `unused-*`) or `customer_agreement` (create via MCA billing account/profile/invoice section)." + type = "CODE" + assignment_type = "USER_INPUT" + default_value = jsonencode(jsonencode({ pre_provisioned = { unused_subscription_name_prefix = "unused-" } })) + } + + azure_location = { + display_name = "Azure Location" + description = "Azure region where the building block backplane resource groups and identities are created." + type = "STRING" + assignment_type = "USER_INPUT" + default_value = jsonencode("germanywestcentral") + } + + azure_platform_subscription_id = { + display_name = "Platform Subscription ID" + description = "Bare GUID of a platform-owned subscription. Hosts the **Budget Alert** and **Storage Account** backplanes (and, as written, the resources they deploy)." + type = "STRING" + assignment_type = "USER_INPUT" + value_validation_regex = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + validation_regex_error_message = "Platform subscription ID must be a bare subscription GUID." + } + + # ── Connectivity (Azure Hub Network building block, always registered) ── + + azure_connectivity_subscription_id = { + display_name = "Connectivity Subscription ID" + description = "Bare GUID of the **connectivity** subscription. Hosts the Hub Network backplane and, when `foundation.hub` is set, the hub vnet and firewall." + type = "STRING" + assignment_type = "USER_INPUT" + value_validation_regex = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + validation_regex_error_message = "Connectivity subscription ID must be a bare subscription GUID." + } + + # ── Spoke network hub — existing hub coordinates ── + # Only needed when spokes peer into a pre-existing hub. Leave unset when `foundation.hub` + # provisions the hub (spokes then use the connectivity subscription/scope above). + + azure_hub_subscription_id = { + display_name = "Hub Subscription ID" + description = "Bare GUID of the subscription holding an **existing** hub vnet that spokes peer into. Leave unset when `foundation.hub` provisions the hub." + type = "STRING" + assignment_type = "USER_INPUT" + value_validation_regex = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + validation_regex_error_message = "Hub subscription ID must be a bare subscription GUID." + is_optional = true + } + + azure_hub_scope = { + display_name = "Hub Scope" + description = "Full path (MG or subscription) of an **existing** hub — the spoke peering RBAC scope. Leave unset when `foundation.hub` provisions the hub." + type = "STRING" + assignment_type = "USER_INPUT" + is_optional = true + } + + azure_hub_resource_group_name = { + display_name = "Hub Resource Group" + description = "Resource group holding an **existing** hub vnet that spokes peer into. Leave unset when `foundation.hub` provisions the hub (it names the RG itself)." + type = "STRING" + assignment_type = "USER_INPUT" + is_optional = true + } + + azure_hub_vnet_name = { + display_name = "Hub VNet" + description = "Name of an **existing** hub vnet that spokes peer into. Leave unset when `foundation.hub` provisions the hub." + type = "STRING" + assignment_type = "USER_INPUT" + is_optional = true + } + } + + outputs = { + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } + } + } +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.24.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.36" + } + azuread = { + source = "hashicorp/azuread" + version = ">= 3.0" + } + } +} From 4800cd2ef41dd8b916fada7611b2e229d818725f Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Fri, 4 Sep 2026 10:32:41 +0200 Subject: [PATCH 2/4] feat: testing azure ref. arch. --- modules/azure/meshstack_integration.tf | 15 ++------ .../azure-landingzone/buildingblock/README.md | 4 -- .../azure-landingzone/buildingblock/main.tf | 15 ++++---- .../buildingblock/variables.tf | 38 ------------------- .../meshstack_integration.tf | 37 ------------------ 5 files changed, 11 insertions(+), 98 deletions(-) diff --git a/modules/azure/meshstack_integration.tf b/modules/azure/meshstack_integration.tf index c934bea0..ef002cf3 100644 --- a/modules/azure/meshstack_integration.tf +++ b/modules/azure/meshstack_integration.tf @@ -37,22 +37,12 @@ variable "landing_zones" { management_group_id = string display_name = string description = optional(string, "") - azure_role_mappings = optional(list(object({ - azure_role = object({ - alias = string - id = string - }) - project_role_ref = object({ - name = string - }) - })), []) })) nullable = false description = <<-EOT Landing zones to create on the Azure platform, keyed by archetype (e.g. `corp`, `online`, `sandbox`). Each entry points a meshStack landing zone at an existing Azure management group via `management_group_id`. - `azure_role_mappings` overrides the platform-level role mappings for that landing zone; leave empty to inherit them. - The landing zone name is `-`. + Landing zones inherit the platform-level role mappings. The landing zone name is `-`. EOT } @@ -339,7 +329,8 @@ resource "meshstack_landingzone" "this" { azure = { azure_management_group_id = each.value.management_group_id - azure_role_mappings = each.value.azure_role_mappings + # Landing zones inherit the platform-level role mappings. + azure_role_mappings = [] } } } diff --git a/reference-architectures/azure-landingzone/buildingblock/README.md b/reference-architectures/azure-landingzone/buildingblock/README.md index 8e371cce..17234458 100644 --- a/reference-architectures/azure-landingzone/buildingblock/README.md +++ b/reference-architectures/azure-landingzone/buildingblock/README.md @@ -77,10 +77,6 @@ The user-facing readme is maintained inline in the `readme` field of the | ---- | ----------- | ---- | ------- | :------: | | [azure\_backplane\_subscription\_id](#input\_azure\_backplane\_subscription\_id) | Optional bare GUID of the subscription where the spoke-network backplane identity is created. Defaults to azure\_platform\_subscription\_id. Typically the hub subscription so the automation identity lives in a stable, platform-owned place. | `string` | `null` | no | | [azure\_connectivity\_subscription\_id](#input\_azure\_connectivity\_subscription\_id) | Bare GUID of the connectivity subscription where the Azure Hub Network backplane identity lives and, when foundation.hub is set, the hub vnet and firewall are created. | `string` | n/a | yes | -| [azure\_hub\_resource\_group\_name](#input\_azure\_hub\_resource\_group\_name) | Name of the resource group that contains an existing hub vnet spoke networks peer into. Leave null when foundation.hub creates the hub. | `string` | `null` | no | -| [azure\_hub\_scope](#input\_azure\_hub\_scope) | Full resource path where the spoke-network backplane's hub-peering role is granted: a management group or subscription path containing an existing hub vnet. Leave null when foundation.hub creates the hub. | `string` | `null` | no | -| [azure\_hub\_subscription\_id](#input\_azure\_hub\_subscription\_id) | Bare GUID of the subscription hosting an existing central hub vnet that spoke networks peer into. Leave null when foundation.hub creates the hub. | `string` | `null` | no | -| [azure\_hub\_vnet\_name](#input\_azure\_hub\_vnet\_name) | Name of an existing hub vnet spoke networks peer into. Leave null when foundation.hub creates the hub. | `string` | `null` | no | | [azure\_location](#input\_azure\_location) | Azure region where the building block backplane resource groups and identities are created. | `string` | `"germanywestcentral"` | no | | [azure\_management\_groups](#input\_azure\_management\_groups) | Enterprise-Scale management group hierarchy (Landing Zones → Corp/Online/Sandbox, plus
Connectivity) under `parent_management_group_id`, with names prefixed by `name_prefix`. The
bootstrap step already created these; this building block adopts them via `import` blocks and
manages them. Pre-configured STATIC by the platform team at registration (parent = bootstrap scope,
same name\_prefix as the bootstrap) — end users don't specify it. |
object({
parent_management_group_id = string
name_prefix = optional(string, "")
landing_zones_display_name = optional(string, "Landing Zones")
corp_display_name = optional(string, "Corp")
online_display_name = optional(string, "Online")
sandbox_display_name = optional(string, "Sandbox")
connectivity_display_name = optional(string, "Connectivity")
})
| n/a | yes | | [azure\_platform\_subscription\_id](#input\_azure\_platform\_subscription\_id) | Bare GUID of a platform-owned subscription. The azurerm provider targets it, the budget-alert and storage-account backplanes are created in it, and (as written) those two building blocks deploy their resources into it. | `string` | n/a | yes | diff --git a/reference-architectures/azure-landingzone/buildingblock/main.tf b/reference-architectures/azure-landingzone/buildingblock/main.tf index 24b057e0..025e07f7 100644 --- a/reference-architectures/azure-landingzone/buildingblock/main.tf +++ b/reference-architectures/azure-landingzone/buildingblock/main.tf @@ -31,14 +31,15 @@ locals { sandbox = local.sandbox_management_group } - # Spoke networks peer into the hub this architecture provisions (when foundation.hub is set — it - # lives in the connectivity subscription/scope), otherwise into an existing hub supplied via the - # azure_hub_* variables. + # Spoke networks peer into the hub in the connectivity subscription/scope (the Connectivity MG is + # always created). The hub vnet/RG names come from foundation.hub when a hub is provisioned, else + # the hub-network defaults — the platform team must provision the hub (foundation.hub) before app + # teams order spokes. spoke_hub = { - subscription_id = local.hub_enabled ? var.azure_connectivity_subscription_id : var.azure_hub_subscription_id - scope = local.hub_enabled ? local.connectivity_scope : var.azure_hub_scope - resource_group_name = local.hub_enabled ? var.foundation.hub.hub_resource_group_name : var.azure_hub_resource_group_name - vnet_name = local.hub_enabled ? var.foundation.hub.hub_vnet_name : var.azure_hub_vnet_name + subscription_id = var.azure_connectivity_subscription_id + scope = local.connectivity_scope + resource_group_name = try(var.foundation.hub.hub_resource_group_name, "hub-network") + vnet_name = try(var.foundation.hub.hub_vnet_name, "hub-vnet") } } diff --git a/reference-architectures/azure-landingzone/buildingblock/variables.tf b/reference-architectures/azure-landingzone/buildingblock/variables.tf index 4440bb0c..8b50b0e8 100644 --- a/reference-architectures/azure-landingzone/buildingblock/variables.tf +++ b/reference-architectures/azure-landingzone/buildingblock/variables.tf @@ -112,44 +112,6 @@ variable "azure_connectivity_subscription_id" { } } -# The azure_hub_* variables describe an EXISTING hub for spoke networks to peer into. Leave them -# null when you let the architecture provision the hub via var.foundation.hub — the spoke wiring then -# uses the provisioned hub's coordinates (in the connectivity subscription/scope) instead. - -variable "azure_hub_subscription_id" { - type = string - default = null - description = "Bare GUID of the subscription hosting an existing central hub vnet that spoke networks peer into. Leave null when foundation.hub creates the hub." - - validation { - condition = var.azure_hub_subscription_id == null || can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.azure_hub_subscription_id)) - error_message = "azure_hub_subscription_id must be a bare subscription GUID, not a '/subscriptions/' path." - } -} - -variable "azure_hub_scope" { - type = string - default = null - description = "Full resource path where the spoke-network backplane's hub-peering role is granted: a management group or subscription path containing an existing hub vnet. Leave null when foundation.hub creates the hub." - - validation { - condition = var.azure_hub_scope == null || can(regex("^/subscriptions/[0-9a-fA-F-]{36}$|^/providers/Microsoft\\.Management/managementGroups/.+$", var.azure_hub_scope)) - error_message = "azure_hub_scope must be a full resource path: '/subscriptions/' or '/providers/Microsoft.Management/managementGroups/'." - } -} - -variable "azure_hub_resource_group_name" { - type = string - default = null - description = "Name of the resource group that contains an existing hub vnet spoke networks peer into. Leave null when foundation.hub creates the hub." -} - -variable "azure_hub_vnet_name" { - type = string - default = null - description = "Name of an existing hub vnet spoke networks peer into. Leave null when foundation.hub creates the hub." -} - variable "azure_management_groups" { type = object({ parent_management_group_id = string diff --git a/reference-architectures/azure-landingzone/meshstack_integration.tf b/reference-architectures/azure-landingzone/meshstack_integration.tf index 1549b57a..61d6c07c 100644 --- a/reference-architectures/azure-landingzone/meshstack_integration.tf +++ b/reference-architectures/azure-landingzone/meshstack_integration.tf @@ -356,43 +356,6 @@ resource "meshstack_building_block_definition" "this" { validation_regex_error_message = "Connectivity subscription ID must be a bare subscription GUID." } - # ── Spoke network hub — existing hub coordinates ── - # Only needed when spokes peer into a pre-existing hub. Leave unset when `foundation.hub` - # provisions the hub (spokes then use the connectivity subscription/scope above). - - azure_hub_subscription_id = { - display_name = "Hub Subscription ID" - description = "Bare GUID of the subscription holding an **existing** hub vnet that spokes peer into. Leave unset when `foundation.hub` provisions the hub." - type = "STRING" - assignment_type = "USER_INPUT" - value_validation_regex = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" - validation_regex_error_message = "Hub subscription ID must be a bare subscription GUID." - is_optional = true - } - - azure_hub_scope = { - display_name = "Hub Scope" - description = "Full path (MG or subscription) of an **existing** hub — the spoke peering RBAC scope. Leave unset when `foundation.hub` provisions the hub." - type = "STRING" - assignment_type = "USER_INPUT" - is_optional = true - } - - azure_hub_resource_group_name = { - display_name = "Hub Resource Group" - description = "Resource group holding an **existing** hub vnet that spokes peer into. Leave unset when `foundation.hub` provisions the hub (it names the RG itself)." - type = "STRING" - assignment_type = "USER_INPUT" - is_optional = true - } - - azure_hub_vnet_name = { - display_name = "Hub VNet" - description = "Name of an **existing** hub vnet that spokes peer into. Leave unset when `foundation.hub` provisions the hub." - type = "STRING" - assignment_type = "USER_INPUT" - is_optional = true - } } outputs = { From 3436a10d7bf415c528243a279088056db6b2847f Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Fri, 4 Sep 2026 10:42:06 +0200 Subject: [PATCH 3/4] feat: testing azure ref. arch. --- modules/azure/meshstack_integration.tf | 15 +++++++++++---- .../azure-landingzone/bootstrap/main.tf | 9 +++++++++ .../azure-landingzone/buildingblock/main.tf | 5 +++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/modules/azure/meshstack_integration.tf b/modules/azure/meshstack_integration.tf index ef002cf3..ccc538fa 100644 --- a/modules/azure/meshstack_integration.tf +++ b/modules/azure/meshstack_integration.tf @@ -3,6 +3,13 @@ variable "azure_management_group" { description = "Azure management group used for platform integration." } +variable "resource_name_prefix" { + type = string + nullable = false + default = "" + description = "Prefix for the created meshStack service principals (replicator/metering/mca) and their custom role definitions, to keep their (tenant-unique) names distinct across deployments. e.g. 'flotest-az-'." +} + variable "azure_subscription_provisioning" { type = object({ pre_provisioned = optional(object({ @@ -143,14 +150,14 @@ module "azure_meshplatform" { version = ">= 0.14.0" replicator_enabled = true - replicator_service_principal_name = "meshstack-replicator" + replicator_service_principal_name = "${var.resource_name_prefix}meshstack-replicator" replicator_custom_role_scope = var.azure_management_group replicator_assignment_scopes = [var.azure_management_group] can_cancel_subscriptions_in_scopes = ["/providers/Microsoft.Management/managementGroups/${var.azure_management_group}"] metering_enabled = true - metering_service_principal_name = "meshstack-metering" + metering_service_principal_name = "${var.resource_name_prefix}meshstack-metering" metering_assignment_scopes = [var.azure_management_group] create_passwords = false # Use only workload identity federation @@ -167,7 +174,7 @@ module "azure_meshplatform" { billing_account_name = local.customer_agreement.billing_account_name billing_profile_name = local.customer_agreement.billing_profile_name invoice_section_name = local.customer_agreement.invoice_section_name - service_principal_names = ["meshstack-mca"] + service_principal_names = ["${var.resource_name_prefix}meshstack-mca"] } : null } @@ -233,7 +240,7 @@ resource "meshstack_platform" "azure" { destination_entra_id = module.azure_meshplatform.azure_ad_tenant_id source_service_principal = { - client_id = module.azure_meshplatform.mca_service_principal["meshstack-mca"].Application_Client_ID + client_id = module.azure_meshplatform.mca_service_principal["${var.resource_name_prefix}meshstack-mca"].Application_Client_ID auth = {} # workload identity federation } subscription_creation_error_cooldown_sec = 900 diff --git a/reference-architectures/azure-landingzone/bootstrap/main.tf b/reference-architectures/azure-landingzone/bootstrap/main.tf index 56885ee7..86c9e642 100644 --- a/reference-architectures/azure-landingzone/bootstrap/main.tf +++ b/reference-architectures/azure-landingzone/bootstrap/main.tf @@ -69,6 +69,15 @@ resource "azuread_app_role_assignment" "graph_directory_read" { resource_object_id = data.azuread_service_principal.msgraph.object_id } +# AppRoleAssignment.ReadWrite.All — the meshplatform module grants the replicator service principal +# its own Graph app roles (directory/group/user read); assigning app roles to another SP requires +# this permission on the identity performing the run. +resource "azuread_app_role_assignment" "graph_approleassignment_readwrite" { + app_role_id = data.azuread_service_principal.msgraph.app_role_ids["AppRoleAssignment.ReadWrite.All"] + principal_object_id = azurerm_user_assigned_identity.bootstrap.principal_id + resource_object_id = data.azuread_service_principal.msgraph.object_id +} + # Enterprise-Scale management group hierarchy, created here in the bootstrap phase so the management # groups already EXIST when the ordered building block run reaches the meshplatform module (which # looks them up via a data source — creating and reading them in the same run is a catch-22). The diff --git a/reference-architectures/azure-landingzone/buildingblock/main.tf b/reference-architectures/azure-landingzone/buildingblock/main.tf index 025e07f7..2304a9af 100644 --- a/reference-architectures/azure-landingzone/buildingblock/main.tf +++ b/reference-architectures/azure-landingzone/buildingblock/main.tf @@ -118,6 +118,7 @@ module "azure_platform" { source = "github.com/meshcloud/meshstack-hub//modules/azure?ref=${var.hub.git_ref}" azure_management_group = local.lz_management_group + resource_name_prefix = var.azure_management_groups.name_prefix azure_subscription_provisioning = var.azure_subscription_provisioning azure_subscription_owner_object_ids = var.azure_subscription_owner_object_ids @@ -161,6 +162,7 @@ module "budget_alert" { azure_subscription_id = var.azure_platform_subscription_id azure_scope = local.landing_zones_scope azure_location = var.azure_location + backplane_name = "${var.azure_management_groups.name_prefix}budget-alert" meshstack = { owning_workspace_identifier = var.workspace @@ -176,6 +178,7 @@ module "storage_account" { azure_subscription_id = var.azure_platform_subscription_id azure_scope = local.landing_zones_scope azure_location = var.azure_location + backplane_name = "${var.azure_management_groups.name_prefix}storage-account" meshstack = { owning_workspace_identifier = var.workspace @@ -195,6 +198,7 @@ module "spoke_network" { azure_hub_resource_group_name = local.spoke_hub.resource_group_name azure_hub_vnet_name = local.spoke_hub.vnet_name azure_backplane_subscription_id = local.backplane_subscription_id + backplane_name = "${var.azure_management_groups.name_prefix}spoke-network" meshstack = { owning_workspace_identifier = var.workspace @@ -239,6 +243,7 @@ module "hub_network" { azure_connectivity_subscription_id = var.azure_connectivity_subscription_id azure_scope = local.connectivity_scope azure_location = var.azure_location + backplane_name = "${var.azure_management_groups.name_prefix}hub-network" meshstack = { owning_workspace_identifier = var.workspace From d111b74299d13a60c1f2f3068e383d6df44848fe Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Fri, 4 Sep 2026 11:22:08 +0200 Subject: [PATCH 4/4] feat: testing azure ref. arch. --- modules/azure/meshstack_integration.tf | 14 +++++++++++--- .../azure-landingzone/buildingblock/main.tf | 15 ++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/modules/azure/meshstack_integration.tf b/modules/azure/meshstack_integration.tf index ccc538fa..9dbd2306 100644 --- a/modules/azure/meshstack_integration.tf +++ b/modules/azure/meshstack_integration.tf @@ -41,15 +41,18 @@ variable "azure_subscription_owner_object_ids" { variable "landing_zones" { type = map(object({ - management_group_id = string - display_name = string - description = optional(string, "") + management_group_id = string + display_name = string + description = optional(string, "") + mandatory_building_block_definition_uuids = optional(list(string), []) })) nullable = false description = <<-EOT Landing zones to create on the Azure platform, keyed by archetype (e.g. `corp`, `online`, `sandbox`). Each entry points a meshStack landing zone at an existing Azure management group via `management_group_id`. Landing zones inherit the platform-level role mappings. The landing zone name is `-`. + `mandatory_building_block_definition_uuids`: building block definitions every tenant in this landing + zone must have — meshStack requires ordering them (e.g. a spoke network on Corp). Defaults to none. EOT } @@ -328,6 +331,11 @@ resource "meshstack_landingzone" "this" { automate_deletion_approval = true automate_deletion_replication = true + # Building blocks every tenant in this landing zone must order (e.g. a spoke network on Corp). + mandatory_building_block_refs = [ + for uuid in each.value.mandatory_building_block_definition_uuids : { uuid = uuid } + ] + platform_ref = { uuid = meshstack_platform.azure.metadata.uuid } diff --git a/reference-architectures/azure-landingzone/buildingblock/main.tf b/reference-architectures/azure-landingzone/buildingblock/main.tf index 2304a9af..a4092249 100644 --- a/reference-architectures/azure-landingzone/buildingblock/main.tf +++ b/reference-architectures/azure-landingzone/buildingblock/main.tf @@ -126,17 +126,30 @@ module "azure_platform" { corp = { management_group_id = local.corp_management_group display_name = "Azure Corp" - description = "Corp-connected landing zone: subscriptions are placed in the Corp management group for internal, hub-connected workloads. Order the Azure Spoke Network building block inside a project for routed connectivity to the hub." + description = "Corp-connected landing zone: subscriptions are placed in the Corp management group for internal, hub-connected workloads. The Azure Spoke Network building block is mandatory here, giving every tenant routed connectivity to the hub." + # Corp tenants must have a spoke network (hub connectivity) plus a budget alert. + mandatory_building_block_definition_uuids = [ + module.spoke_network.building_block_definition.uuid, + module.budget_alert.building_block_definition.uuid, + ] } online = { management_group_id = local.online_management_group display_name = "Azure Online" description = "Internet-facing landing zone: subscriptions are placed in the Online management group for public-facing workloads without a mandatory hub connection." + # Online tenants must have a budget alert. + mandatory_building_block_definition_uuids = [ + module.budget_alert.building_block_definition.uuid, + ] } sandbox = { management_group_id = local.sandbox_management_group display_name = "Azure Sandbox" description = "Experimentation landing zone: subscriptions are placed in the Sandbox management group with relaxed guardrails for trying things out." + # Sandbox tenants must have a budget alert. + mandatory_building_block_definition_uuids = [ + module.budget_alert.building_block_definition.uuid, + ] } }