From 0094684647a41699acdadc1657481dd8ca59c9a6 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Wed, 2 Sep 2026 13:44:58 +0200 Subject: [PATCH 1/2] feat: testing azure ref. arch. --- .agents/references/meshstack-integration.md | 15 +- .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 | 218 ++++++-- .../spoke-network/meshstack_integration.tf | 5 + .../azure-landingzone/README.md | 132 +++++ .../azure-landingzone/azure-landingzone.dot | 80 +++ .../azure-landingzone/azure-landingzone.svg | 263 ++++++++++ .../azure-landingzone/buildingblock/README.md | 116 +++++ .../buildingblock/SUMMARY.md.tftpl | 56 +++ .../azure-landingzone/buildingblock/logo.svg | 25 + .../azure-landingzone/buildingblock/main.tf | 304 ++++++++++++ .../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 | 55 +++ .../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 | 20 + .../buildingblock/variables.tf | 235 +++++++++ .../buildingblock/versions.tf | 22 + .../azure-landingzone/logo.svg | 25 + .../meshstack_integration.tf | 465 ++++++++++++++++++ 46 files changed, 3196 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/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/.agents/references/meshstack-integration.md b/.agents/references/meshstack-integration.md index 8ee0bf244..51d5628e2 100644 --- a/.agents/references/meshstack-integration.md +++ b/.agents/references/meshstack-integration.md @@ -1,5 +1,5 @@ --- -description: Conventions for meshstack_integration.tf in meshstack-hub modules — file structure and variable ordering, the shared `hub` and `meshstack` variables, pinning module sources and BBD `ref_name` to `var.hub.git_ref`, the required meshcloud/meshstack provider, the BBD `terraform_version`, and the `building_block_definition` output. +description: Conventions for meshstack_integration.tf in meshstack-hub modules — file structure and variable ordering, the shared `hub` and `meshstack` variables, pinning module sources and BBD `ref_name` to `var.hub.git_ref`, the required meshcloud/meshstack provider, the BBD `terraform_version`, the `building_block_definition` output, and the 255-character cap on input descriptions. --- @@ -138,6 +138,19 @@ resource "meshstack_building_block_definition" "this" { **If a `meshstack_building_block_definition` input's `argument` field references a variable, that variable must have an explicit default** — do not rely on nested `optional()` defaults (for example via a bare `default = {}`), since some downstream consumers don't evaluate Terraform's object-attribute defaulting and would see unset fields instead. Keep the `optional()` type constraints regardless — they still document intent and protect callers who omit keys. +## Input description length limit + +**Every `version_spec.inputs[*].description` must be at most 255 characters.** The provider rejects a +longer one at apply with `Invalid Attribute Value Length … must be at most 255` — and `tofu +validate`, `tofu fmt` and the scorecard all miss it, so only a real apply surfaces the error. +(Historically an over-long description got no client-side check and the server answered with a +`500 InternalError` on the version update instead.) + +The descriptions that blow past 255 are the multi-clause ones documenting each field of a `CODE` +object input — a `tags` input that spells out `landingzone` / `building_block` / `owner_tag_key`, for +example. Keep them terse; put the full explanation in the buildingblock variable's own `description` +(which has no such cap) and the readme. There is no length cap on `spec.readme`. + ## Overridable Catalog Properties diff --git a/.gitignore b/.gitignore index 391eecd9b..e98b8dfd9 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 000000000..44427f70e --- /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 000000000..1f405a5ea --- /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 000000000..6d1473320 --- /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 000000000..9d11da14f --- /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 000000000..08cb53ff8 --- /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 000000000..653c1354e --- /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 000000000..f82dc87b3 --- /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%-`. + `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 +} + 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." + 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) + }) + 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,31 +129,39 @@ 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" version = ">= 0.14.0" 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_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 = [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_service_principal_name = "${var.resource_name_prefix}meshstack-metering" + 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 - service_principal_names = ["meshstack-mca"] - } + # 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 = ["${var.resource_name_prefix}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["${var.resource_name_prefix}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,30 +315,48 @@ 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 + # 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 = meshstack_platform.azure.ref platform_properties = { azure = { - azure_management_group_id = var.azure_management_group + azure_management_group_id = each.value.management_group_id + # Landing zones inherit the platform-level role mappings. 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 ee4aecfe0..5f77c04b4 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 000000000..5176c9534 --- /dev/null +++ b/reference-architectures/azure-landingzone/README.md @@ -0,0 +1,132 @@ +--- +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 **provisioned by +the architecture** under a parent management group the platform engineer provides when ordering +(`azure_management_groups.parent_management_group_id`), so end users configure nothing about +management groups. 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**, driven by toggles that reveal their settings only when switched on: + +4. Provisions a central **hub vnet** (with an optional Azure Firewall) in the connectivity + subscription — enable **Provision Hub Network**; spoke networks then peer into it. +5. Assigns curated **Enterprise-Scale policies** to the Corp/Online/Sandbox management groups + (Corp locked down, Online region-restricted, Sandbox audit-only) — **Assign Enterprise-Scale + Policies**, on by default. +6. Creates extra platform-owned **resource groups** in the platform subscription — **Platform + Resource Groups**. + +**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 registers the building block definition by applying + [`meshstack_integration.tf`](meshstack_integration.tf) once. This only creates the definition in + meshStack — no Azure resources and no Azure credentials are needed at this step. +2. Beforehand the engineer creates one **service principal** — Owner on the parent management group + and granted the Microsoft Graph app roles `Application.ReadWrite.All`, `Directory.Read.All` and + `AppRoleAssignment.ReadWrite.All` — and, when **ordering the building block in meshStack**, enters + its client id, secret and tenant (the secret is a sensitive input). The ordered run authenticates + as that principal and does the actual work: it creates the management group hierarchy under the + given parent, 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. + +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 | +|-------------|-------------| +| Parent management group | An existing management group (or the tenant ID) under which the architecture creates the Corp/Online/Sandbox/Connectivity hierarchy. | +| 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 service principal | A service principal with **Owner** on the parent management group and the Microsoft Graph app roles `Application.ReadWrite.All`, `Directory.Read.All` and `AppRoleAssignment.ReadWrite.All`. Its client id, secret and tenant are entered when ordering; the ordered run authenticates as it. Creating such a principal requires a tenant admin (Global Administrator / Privileged Role Administrator) once. | + +### 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 000000000..517fb4c85 --- /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 000000000..35a6539e1 --- /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/buildingblock/README.md b/reference-architectures/azure-landingzone/buildingblock/README.md new file mode 100644 index 000000000..1b78b2446 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/README.md @@ -0,0 +1,116 @@ +--- +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 **creates** the Enterprise-Scale management group hierarchy (a parent "Landing Zones" management +group with Corp, Online and Sandbox beneath it, plus Connectivity) under a parent management group +the platform engineer provides, and assumes a central network **hub** vnet is already in place. The +run then: + +- creates the management group hierarchy and 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 a **service principal the platform engineer supplies when ordering** (`azure_client_id`, +`azure_client_secret`, `azure_tenant_id` — the secret is a sensitive input, the Azure equivalent of +the STACKIT service account key). That principal needs **Owner** on the parent management group and +the Microsoft Graph app roles `Application.ReadWrite.All`, `Directory.Read.All` and +`AppRoleAssignment.ReadWrite.All`. The `azurerm`/`azuread` providers are configured directly from +those inputs (see [`provider.tf`](provider.tf)) — no bootstrap identity, no separate apply. + +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 | +| ---- | ----------- | ---- | ------- | :------: | +| [assign\_policies](#input\_assign\_policies) | Assign curated Enterprise-Scale policies to the Corp/Online/Sandbox management groups (Corp locked down, Online region-restricted, Sandbox audit-only). | `bool` | `true` | no | +| [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\_client\_id](#input\_azure\_client\_id) | Client ID of the service principal this run authenticates as. Needs Owner on the management group scope and Microsoft Graph app roles (Application.ReadWrite.All, Directory.Read.All, AppRoleAssignment.ReadWrite.All) to create the management groups, platform service principals and backplane identities. | `string` | n/a | yes | +| [azure\_client\_secret](#input\_azure\_client\_secret) | Client secret of the service principal this run authenticates as. The Azure equivalent of the STACKIT service account key — reused on every run and rotatable from the meshStack UI. | `string` | n/a | yes | +| [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 provision_hub is set, the hub vnet and firewall are created. | `string` | n/a | yes | +| [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) this run creates under `parent_management_group_id` (an existing management group or
the tenant ID), with names prefixed by `name_prefix` to keep them unique across the tenant. |
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\_tenant\_id](#input\_azure\_tenant\_id) | Azure Entra tenant ID. Used to authenticate the providers and as the ARM tenant for the building block backplanes. | `string` | n/a | yes | +| [creator](#input\_creator) | The user who ordered this architecture, injected by meshStack. Their display name is written to the landing zones' owner tag (see `tags.owner_tag_key`). |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | +| [customer\_agreement](#input\_customer\_agreement) | Customer-agreement (MCA) model: the billing scope meshStack creates subscriptions under. Required when subscription\_provisioning\_model is customer\_agreement. |
object({
billing_account_name = string
billing_profile_name = string
invoice_section_name = string
})
| `null` | no | +| [foundation\_resource\_groups](#input\_foundation\_resource\_groups) | Extra platform-owned resource groups created in the platform subscription. |
list(object({
name = string
location = string
}))
| `[]` | 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 | +| [hub\_network](#input\_hub\_network) | Hub vnet settings, used when provision\_hub is true. The vnet and resource-group names here are also the ones spoke networks peer into. |
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")
})
| `{}` | 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 | +| [pre\_provisioned](#input\_pre\_provisioned) | Pre-provisioned model: meshStack assigns subscriptions from existing ones whose name starts with `unused_subscription_name_prefix`. |
object({
unused_subscription_name_prefix = optional(string, "unused-")
})
|
{
"unused_subscription_name_prefix": "unused-"
}
| no | +| [provision\_hub](#input\_provision\_hub) | Provision a central hub vnet (via the Azure Hub Network building block) in the connectivity subscription for spoke networks to peer into. When false, spoke networks peer into an existing hub. | `bool` | `false` | no | +| [subscription\_provisioning\_model](#input\_subscription\_provisioning\_model) | How meshStack gets Azure subscriptions: `pre_provisioned` (assign from an existing pool) or `customer_agreement` (create via MCA billing). | `string` | `"pre_provisioned"` | no | +| [tags](#input\_tags) | Tags forwarded to the nested integrations, each built in the meshPanel form as a list of {key, values} entries.
`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).
`owner_tag_key` names the landing-zone tag that receives the creator's display name (empty to set none). |
object({
landingzone = list(object({ key = string, values = list(string) }))
building_block = list(object({ key = string, values = list(string) }))
owner_tag_key = optional(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 provision_hub is false. | +| [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 000000000..bd90bd029 --- /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 — @buildingblock[${hub_bb_uuid}]. 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 000000000..71d042afa --- /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 000000000..06e1fa286 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/main.tf @@ -0,0 +1,304 @@ +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 + + # meshStack caps a name at 45 characters, and a landing zone name is "-" + # (longest archetype "sandbox" adds 8), so the name feeding the platform, its landing zones and the + # location has to fit in 37. Cut the identifier short of that, drop whatever dashes the cut exposed, + # and end with a hash of the full identifier so two names sharing a prefix stay apart. A name that + # already fits passes through untouched, so existing platforms keep their plain names. + platform_name = length(local.platform_identifier) <= 37 ? local.platform_identifier : format( + "%s-%s", + replace(substr(local.platform_identifier, 0, 32), "/-+$/", ""), + substr(sha256(local.platform_identifier), 0, 4) + ) + + # The person who ordered this architecture, written to the landing zones' owner tag when the + # platform team named one (var.tags.owner_tag_key). Empty key => no owner tag. + owner_tags = var.tags.owner_tag_key == "" ? {} : { (var.tags.owner_tag_key) = [var.creator.displayName] } + + # The tags form hands landingzone/building_block back as lists of {key, values} entries (friendlier + # than a dynamic-key map); turn each into the map(list(string)) the nested integrations take. + tags = { + landingzone = { for e in var.tags.landingzone : e.key => e.values } + building_block = { for e in var.tags.building_block : e.key => e.values } + } + + # ── Foundation ── + hub_enabled = var.provision_hub + policies_enabled = var.assign_policies + foundation_rgs = { for rg in var.foundation_resource_groups : rg.name => { location = rg.location } } + + # The two conditional provisioning inputs reassembled into the single object modules/azure expects + # (exactly one of pre_provisioned / customer_agreement). meshStack shows only the selected one. + azure_subscription_provisioning = var.subscription_provisioning_model == "customer_agreement" ? { + customer_agreement = var.customer_agreement + } : { + pre_provisioned = var.pre_provisioned + } + + # Management group identifiers come from the hierarchy this architecture creates under the + # user-provided parent management group (var.azure_management_groups). + 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 in the connectivity subscription/scope (the Connectivity MG is + # always created). The hub vnet/RG names come from var.hub_network — the platform team must + # provision the hub (provision_hub) before app teams order spokes. + spoke_hub = { + subscription_id = var.azure_connectivity_subscription_id + scope = local.connectivity_scope + resource_group_name = var.hub_network.hub_resource_group_name + vnet_name = var.hub_network.hub_vnet_name + } +} + +# ── Enterprise-Scale management group hierarchy ── +# This run creates the Landing Zones → Corp/Online/Sandbox (+ Connectivity) hierarchy under the +# user-provided parent management group. The platform module below builds its scopes from the group +# names (module outputs, so ordered after creation) rather than a data source, so creating and using +# them in the same run works (see modules/azure/meshstack_integration.tf). +module "management_groups" { + source = "./modules/management-groups" + + 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 +} + +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_name + 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 parent management group (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 + resource_name_prefix = var.azure_management_groups.name_prefix + azure_subscription_provisioning = local.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. 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, + ] + } + } + + meshstack = { + owning_workspace_identifier = var.workspace + platform_name = local.platform_name + location_name = var.use_global_location ? "global" : meshstack_location.this.metadata.name + tags = merge(local.tags.landingzone, local.owner_tags) + } + + 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 + backplane_name = "${var.azure_management_groups.name_prefix}budget-alert" + + meshstack = { + owning_workspace_identifier = var.workspace + tags = local.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 + backplane_name = "${var.azure_management_groups.name_prefix}storage-account" + + meshstack = { + owning_workspace_identifier = var.workspace + tags = local.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 + backplane_name = "${var.azure_management_groups.name_prefix}spoke-network" + + meshstack = { + owning_workspace_identifier = var.workspace + tags = local.tags.building_block + } + hub = var.hub +} + +# ── Optional foundation ── +# Provisioned per the provision_hub / assign_policies / foundation_resource_groups inputs. With all +# off/empty the architecture is just the meshStack-side wiring (platform, landing zones, 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.provision_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 + backplane_name = "${var.azure_management_groups.name_prefix}hub-network" + + meshstack = { + owning_workspace_identifier = var.workspace + tags = local.tags.building_block + } + hub = var.hub +} + +resource "meshstack_building_block" "hub" { + lifecycle { + enabled = local.hub_enabled + + postcondition { + condition = self.status.status == "SUCCEEDED" + error_message = "Building block ${self.metadata.uuid} is ${self.status.status}, not SUCCEEDED. See its run in meshPanel." + } + } + + 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(var.hub_network.hub_resource_group_name) } + hub_vnet_name = { value = jsonencode(var.hub_network.hub_vnet_name) } + address_space = { value = jsonencode(var.hub_network.address_space) } + create_gateway_subnet = { value = jsonencode(var.hub_network.create_gateway_subnet) } + deploy_firewall = { value = jsonencode(var.hub_network.deploy_firewall) } + firewall_sku_tier = { value = jsonencode(var.hub_network.firewall_sku_tier) } + } + } +} 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 000000000..b3a0f5055 --- /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 000000000..227447034 --- /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 000000000..a7ed6635e --- /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 000000000..3941f00b2 --- /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 000000000..be557054d --- /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 000000000..c3f74a3a8 --- /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 000000000..040460468 --- /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` the platform engineer supplies when ordering — so the hierarchy is +created under that parent management group. + +The applying identity needs permission to create management groups (Management Group Contributor at +the parent, or Owner on it — which the service principal the run authenticates as 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 000000000..bfcb29f10 --- /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 000000000..2a7d09159 --- /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 000000000..611ce3097 --- /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 000000000..c3f74a3a8 --- /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 000000000..de6050a41 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/outputs.tf @@ -0,0 +1,55 @@ +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 provision_hub is false." + 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 + hub_bb_uuid = local.hub_enabled ? meshstack_building_block.hub.metadata.uuid : "" + 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 000000000..9c3647947 --- /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 000000000..a6d33e9e9 --- /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 000000000..bfac9dc8a --- /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 000000000..3d6db06b5 --- /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 000000000..c83406a91 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/provider.tf @@ -0,0 +1,20 @@ +# The ordered run authenticates as a service principal the platform engineer supplies when ordering +# (client id + secret + tenant, entered in the meshStack UI — see the azure_client_* inputs). That +# principal needs Owner on the scope the management groups are created under and the Microsoft Graph +# app roles to register the meshStack platform service principals, because this run creates the +# management groups, the platform service principals, management-group role assignments and the +# per-building-block backplane identities. The secret is the Azure equivalent of the STACKIT service +# account key: a sensitive input, reused on every run and rotatable from the UI. +provider "azurerm" { + features {} + subscription_id = var.azure_platform_subscription_id + client_id = var.azure_client_id + client_secret = var.azure_client_secret + tenant_id = var.azure_tenant_id +} + +provider "azuread" { + client_id = var.azure_client_id + client_secret = var.azure_client_secret + tenant_id = var.azure_tenant_id +} diff --git a/reference-architectures/azure-landingzone/buildingblock/variables.tf b/reference-architectures/azure-landingzone/buildingblock/variables.tf new file mode 100644 index 000000000..bd2c2fa45 --- /dev/null +++ b/reference-architectures/azure-landingzone/buildingblock/variables.tf @@ -0,0 +1,235 @@ +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 = list(object({ key = string, values = list(string) })) + building_block = list(object({ key = string, values = list(string) })) + owner_tag_key = optional(string, "") + }) + nullable = false + description = <<-EOT + Tags forwarded to the nested integrations, each built in the meshPanel form as a list of {key, values} entries. + `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). + `owner_tag_key` names the landing-zone tag that receives the creator's display name (empty to set none). + EOT +} + +variable "creator" { + type = object({ + type = string + identifier = string + displayName = string + username = optional(string) + email = optional(string) + euid = optional(string) + }) + nullable = false + description = "The user who ordered this architecture, injected by meshStack. Their display name is written to the landing zones' owner tag (see `tags.owner_tag_key`)." +} + +# ── Azure platform (existing management group hierarchy is assumed) ── + +variable "azure_client_id" { + type = string + nullable = false + description = "Client ID of the service principal this run authenticates as. Needs Owner on the management group scope and Microsoft Graph app roles (Application.ReadWrite.All, Directory.Read.All, AppRoleAssignment.ReadWrite.All) to create the management groups, platform service principals and backplane identities." +} + +variable "azure_client_secret" { + type = string + nullable = false + sensitive = true + description = "Client secret of the service principal this run authenticates as. The Azure equivalent of the STACKIT service account key — reused on every run and rotatable from the meshStack UI." +} + +variable "azure_tenant_id" { + type = string + nullable = false + description = "Azure Entra tenant ID. Used to authenticate the providers and as the ARM tenant for the building block backplanes." +} + +# ── Subscription provisioning (a SINGLE_SELECT model + the matching conditional input) ── +# meshStack shows only the block matching the selected model (see the `condition` on each in +# meshstack_integration.tf); main.tf reassembles them into the object modules/azure expects. + +variable "subscription_provisioning_model" { + type = string + nullable = false + default = "pre_provisioned" + description = "How meshStack gets Azure subscriptions: `pre_provisioned` (assign from an existing pool) or `customer_agreement` (create via MCA billing)." + + validation { + condition = contains(["pre_provisioned", "customer_agreement"], var.subscription_provisioning_model) + error_message = "subscription_provisioning_model must be pre_provisioned or customer_agreement." + } +} + +variable "pre_provisioned" { + type = object({ + unused_subscription_name_prefix = optional(string, "unused-") + }) + nullable = false + default = { unused_subscription_name_prefix = "unused-" } + description = "Pre-provisioned model: meshStack assigns subscriptions from existing ones whose name starts with `unused_subscription_name_prefix`." +} + +variable "customer_agreement" { + type = object({ + billing_account_name = string + billing_profile_name = string + invoice_section_name = string + }) + nullable = true + default = null + description = "Customer-agreement (MCA) model: the billing scope meshStack creates subscriptions under. Required when subscription_provisioning_model is 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 provision_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." + } +} + +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) this run creates under `parent_management_group_id` (an existing management group or + the tenant ID), with names prefixed by `name_prefix` to keep them unique across the tenant. + EOT +} + +# ── Foundation — optional Azure-side infra, exposed as toggles that reveal their settings ── + +variable "provision_hub" { + type = bool + nullable = false + default = false + description = "Provision a central hub vnet (via the Azure Hub Network building block) in the connectivity subscription for spoke networks to peer into. When false, spoke networks peer into an existing hub." +} + +variable "hub_network" { + type = 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") + }) + nullable = false + default = {} + description = "Hub vnet settings, used when provision_hub is true. The vnet and resource-group names here are also the ones spoke networks peer into." +} + +variable "assign_policies" { + type = bool + nullable = false + default = true + description = "Assign curated Enterprise-Scale policies to the Corp/Online/Sandbox management groups (Corp locked down, Online region-restricted, Sandbox audit-only)." +} + +variable "foundation_resource_groups" { + type = list(object({ + name = string + location = string + })) + nullable = false + default = [] + description = "Extra platform-owned resource groups created in the platform subscription." +} + +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 000000000..600db44f0 --- /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 000000000..71d042afa --- /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 000000000..90971a991 --- /dev/null +++ b/reference-architectures/azure-landingzone/meshstack_integration.tf @@ -0,0 +1,465 @@ +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." +} + +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 + } +} + +# JSON-schema for the Tags input: each section is a list of {key, values} entries — friendlier in the +# meshPanel form than a dynamic-key map — adopted from the STACKIT Landing Zone reference architecture. +# main.tf turns each list back into the map(list(string)) the nested integrations take. +locals { + tag_list_schema = { + type = "array" + items = { + type = "object" + required = ["key", "values"] + properties = { + key = { type = "string", title = "Tag Key" } + values = { type = "array", title = "Values", minItems = 1, items = { type = "string" } } + } + } + } + + tags_json_schema = { + "$schema" = "http://json-schema.org/draft-07/schema#" + type = "object" + required = ["landingzone", "building_block", "owner_tag_key"] + properties = { + landingzone = merge(local.tag_list_schema, { title = "Landing Zone Tags" }) + building_block = merge(local.tag_list_schema, { title = "Building Block Tags" }) + owner_tag_key = { type = "string", title = "Owner Tag Key", default = "" } + } + } +} + +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 a fresh Azure Enterprise-Scale + hierarchy into a self-service-ready meshStack platform in one run — you enter the service + principal it runs as when ordering, no separate bootstrap step. + + Running it once: + - creates the Enterprise-Scale management group hierarchy (a **Landing Zones** group with + **Corp**, **Online** and **Sandbox** beneath it, plus **Connectivity**) under a parent + management group you provide, + - 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 orders this once for a workspace. Beforehand they create one **service + principal** and enter its **client id, client secret and tenant** here. That principal needs + **Owner** on the parent management group (to create the hierarchy, role assignments and + identities beneath it) and the Microsoft Graph app roles **Application.ReadWrite.All**, + **Directory.Read.All** and **AppRoleAssignment.ReadWrite.All** (to register the meshStack + platform service principals). The secret is stored as a sensitive input and can be rotated here. + + 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 = { + # display_order groups the order form top-to-bottom: platform identity (10s), Azure auth (30s), + # Azure placement (60s), subscription provisioning (100s), foundation toggles (130s), tags (170), + # and the injected/STATIC inputs last (900s) since the platform engineer never edits those. + + # ── Platform identity ── + + 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." + display_order = 10 + } + + 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) + display_order = 20 + } + + # ── Azure authentication ── + # The ordered run authenticates as a service principal the platform engineer supplies here. It + # needs Owner on the parent management group and the Microsoft Graph app roles listed in the + # readme, because the run creates the management groups, the platform service principals and the + # backplane identities. The secret is the Azure equivalent of the STACKIT service account key — + # a sensitive input, reused on every run and rotatable here. + + azure_client_id = { + display_name = "Azure Client ID" + description = "Client ID of the service principal this run authenticates as." + type = "STRING" + assignment_type = "USER_INPUT" + display_order = 30 + } + azure_client_secret = { + display_name = "Azure Client Secret" + description = "Client secret of that service principal. Reused on every run and rotatable here." + type = "STRING" + assignment_type = "USER_INPUT" + sensitive = {} + display_order = 40 + } + azure_tenant_id = { + display_name = "Azure Tenant ID" + description = "Azure Entra tenant ID of the service principal and the target tenant." + type = "STRING" + assignment_type = "USER_INPUT" + display_order = 50 + } + + # ── Azure placement ── + + # Creates the Corp/Online/Sandbox/Connectivity hierarchy under the parent management group the + # platform engineer enters. A JSON form (two labelled fields) rather than a raw JSON editor; the + # display-name fields on the variable keep their defaults. + azure_management_groups = { + display_name = "Management Groups" + description = "The parent management group the Corp/Online/Sandbox/Connectivity hierarchy is created under, and a name prefix to keep the created group names unique across the tenant." + type = "JSON" + assignment_type = "USER_INPUT" + display_order = 60 + json_schema = jsonencode({ + "$schema" = "http://json-schema.org/draft-07/schema#" + type = "object" + required = ["parent_management_group_id"] + properties = { + parent_management_group_id = { type = "string", title = "Parent Management Group ID", description = "Existing management group name or tenant ID the hierarchy is created under." } + name_prefix = { type = "string", title = "Name Prefix", description = "Prefix for the created group names, to keep them unique across the tenant.", default = "" } + } + }) + } + + 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") + display_order = 70 + } + + 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." + display_order = 80 + } + + azure_connectivity_subscription_id = { + display_name = "Connectivity Subscription ID" + description = "Bare GUID of the **connectivity** subscription. Hosts the Hub Network backplane and, when `provision_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." + display_order = 90 + } + + # ── Subscription provisioning ── + # A SINGLE_SELECT model drives which of the two blocks below meshStack shows — pick the pool model + # and the pool prefix appears; pick MCA and the billing fields appear (and are required). main.tf + # reassembles the shown one into the object modules/azure expects. + subscription_provisioning_model = { + display_name = "Subscription Provisioning Model" + description = "How meshStack gets subscriptions: assign from an existing pool, or create them via an MCA billing agreement." + type = "SINGLE_SELECT" + assignment_type = "USER_INPUT" + selectable_values = ["pre_provisioned", "customer_agreement"] + default_value = jsonencode("pre_provisioned") + display_order = 100 + } + + pre_provisioned = { + display_name = "Pre-provisioned Pool" + description = "Shown for the pre-provisioned model. meshStack assigns subscriptions from existing ones whose name starts with this prefix." + type = "JSON" + assignment_type = "USER_INPUT" + condition = "input.subscription_provisioning_model == \"pre_provisioned\"" + display_order = 110 + json_schema = jsonencode({ + "$schema" = "http://json-schema.org/draft-07/schema#" + type = "object" + properties = { + unused_subscription_name_prefix = { type = "string", title = "Unused Subscription Name Prefix", default = "unused-" } + } + }) + } + + customer_agreement = { + display_name = "Customer Agreement (MCA)" + description = "Shown for the customer-agreement model. The MCA billing scope meshStack creates subscriptions under." + type = "JSON" + assignment_type = "USER_INPUT" + condition = "input.subscription_provisioning_model == \"customer_agreement\"" + display_order = 120 + json_schema = jsonencode({ + "$schema" = "http://json-schema.org/draft-07/schema#" + type = "object" + required = ["billing_account_name", "billing_profile_name", "invoice_section_name"] + properties = { + billing_account_name = { type = "string", title = "Billing Account Name" } + billing_profile_name = { type = "string", title = "Billing Profile Name" } + invoice_section_name = { type = "string", title = "Invoice Section Name" } + } + }) + } + + # ── Foundation — optional Azure-side infra, as toggles that reveal their settings ── + + provision_hub = { + display_name = "Provision Hub Network" + description = "Provision a central hub vnet for spoke networks to peer into. Turning this on reveals the hub network settings below." + type = "BOOLEAN" + assignment_type = "USER_INPUT" + default_value = jsonencode(false) + display_order = 130 + } + + # Shown only when Provision Hub Network is on — the whole point of conditional inputs: the hub + # fields appear once, and only once, the toggle above is set. + hub_network = { + display_name = "Hub Network" + description = "Shown when Provision Hub Network is on. Address space and firewall settings for the hub vnet spoke networks peer into." + type = "JSON" + assignment_type = "USER_INPUT" + condition = "input.provision_hub == true" + display_order = 140 + json_schema = jsonencode({ + "$schema" = "http://json-schema.org/draft-07/schema#" + type = "object" + properties = { + address_space = { type = "string", title = "Address Space (CIDR)", default = "10.0.0.0/22" } + hub_vnet_name = { type = "string", title = "Hub VNet Name", default = "hub-vnet" } + hub_resource_group_name = { type = "string", title = "Hub Resource Group Name", default = "hub-network" } + create_gateway_subnet = { type = "boolean", title = "Create Gateway Subnet", default = true } + deploy_firewall = { type = "boolean", title = "Deploy Azure Firewall", default = false } + firewall_sku_tier = { type = "string", title = "Firewall SKU Tier", enum = ["Basic", "Standard", "Premium"], default = "Standard" } + } + }) + } + + assign_policies = { + display_name = "Assign Enterprise-Scale Policies" + description = "Assign curated ES policies to Corp/Online/Sandbox (Corp locked down, Online region-restricted, Sandbox audit-only)." + type = "BOOLEAN" + assignment_type = "USER_INPUT" + default_value = jsonencode(true) + display_order = 150 + } + + foundation_resource_groups = { + display_name = "Platform Resource Groups" + description = "Extra platform-owned resource groups created in the platform subscription." + type = "JSON" + assignment_type = "USER_INPUT" + display_order = 160 + json_schema = jsonencode({ + "$schema" = "http://json-schema.org/draft-07/schema#" + type = "array" + items = { + type = "object" + required = ["name", "location"] + properties = { + name = { type = "string", title = "Name" } + location = { type = "string", title = "Location", default = "germanywestcentral" } + } + } + }) + } + + # ── Tags ── + + tags = { + display_name = "Tags" + description = "Tags forwarded to the nested integrations. Add {key, values} entries for the landing zones and the building block definitions, plus the owner tag key that gets the creator's display name." + type = "JSON" + assignment_type = "USER_INPUT" + updateable_by_consumer = true + display_order = 170 + json_schema = jsonencode(local.tags_json_schema) + } + + # ── Injected by meshStack / set at registration — not shown on the order form ── + + 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" + display_order = 900 + } + + creator = { + display_name = "Creator" + description = "The user who ordered this architecture. Their display name is written to the landing zones' owner tag when `tags.owner_tag_key` is set." + type = "CODE" + assignment_type = "AUTHOR" + display_order = 910 + } + + 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)) + display_order = 920 + } + + 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) + display_order = 930 + } + } + + outputs = { + summary = { + display_name = "Summary" + type = "STRING" + assignment_type = "SUMMARY" + } + } + } +} + +# Registration only creates the building block definition in meshStack — no Azure resources, so no +# Azure providers and no Azure credentials are needed here. The ordered run authenticates to Azure +# with the service principal entered as an input (see buildingblock/provider.tf). +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.24.0" + } + } +} From 1456e64ddb6a5e565e3429c9423a45997506dd04 Mon Sep 17 00:00:00 2001 From: Florian Nowarre Date: Mon, 5 Oct 2026 14:31:06 +0200 Subject: [PATCH 2/2] feat: testing azure ref. arch. --- .../meshstack_integration.tf | 31 +++++++++++++++++++ .../azure-landingzone/scp.sh | 20 ++++++++++++ 2 files changed, 51 insertions(+) create mode 100755 reference-architectures/azure-landingzone/scp.sh diff --git a/reference-architectures/azure-landingzone/meshstack_integration.tf b/reference-architectures/azure-landingzone/meshstack_integration.tf index 90971a991..874649bea 100644 --- a/reference-architectures/azure-landingzone/meshstack_integration.tf +++ b/reference-architectures/azure-landingzone/meshstack_integration.tf @@ -113,6 +113,37 @@ resource "meshstack_building_block_definition" "this" { **Directory.Read.All** and **AppRoleAssignment.ReadWrite.All** (to register the meshStack platform service principals). The secret is stored as a sensitive input and can be rotated here. + Create that service principal once with the Azure CLI (a tenant admin runs this — granting Owner + and consenting the Graph roles both need tenant-admin rights), then paste the three values it + prints: + + ```bash + PARENT_MG="" # the hierarchy is created under this + SP_NAME="azure-landingzone-deployer" + + # Create the service principal and grant it Owner on the parent management group in one step. + SP=$(az ad sp create-for-rbac --name "$SP_NAME" \ + --role Owner \ + --scopes "/providers/Microsoft.Management/managementGroups/$PARENT_MG" -o json) + echo "$SP" # appId = client id, password = client secret, tenant = tenant id + + # Grant the three Microsoft Graph app roles the run needs, resolving every id from $SP so there is + # nothing to copy-paste (Application.ReadWrite.All, Directory.Read.All, AppRoleAssignment.ReadWrite.All). + SP_OID=$(az ad sp show --id "$(echo "$SP" | jq -r .appId)" --query id -o tsv) + GRAPH_OID=$(az ad sp show --id 00000003-0000-0000-c000-000000000000 --query id -o tsv) + for ROLE in 1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9 \ + 7ab1d382-f21e-4acd-a863-ba3e13f7da61 \ + 06b708a9-e830-4db3-a914-8e69da51d44f; do + az rest --method POST \ + --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_OID/appRoleAssignments" \ + --body "{\"principalId\":\"$SP_OID\",\"resourceId\":\"$GRAPH_OID\",\"appRoleId\":\"$ROLE\"}" + done + ``` + + Enter `appId` as **Azure Client ID**, `password` as **Azure Client Secret** and `tenant` as + **Azure Tenant ID** when ordering. (For a local `terraform apply` of the buildingblock, set the + same values as `azure_client_id`, `azure_client_secret` and `azure_tenant_id`.) + 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. diff --git a/reference-architectures/azure-landingzone/scp.sh b/reference-architectures/azure-landingzone/scp.sh new file mode 100755 index 000000000..0117dd96e --- /dev/null +++ b/reference-architectures/azure-landingzone/scp.sh @@ -0,0 +1,20 @@ +PARENT_MG="flo-test-ref-arch" # the hierarchy is created under this +SP_NAME="flo-azure-landingzone-deployer" + +# Create the service principal and grant it Owner on the parent management group in one step. +SP=$(az ad sp create-for-rbac --name "$SP_NAME" \ + --role Owner \ + --scopes "/providers/Microsoft.Management/managementGroups/$PARENT_MG" -o json) +echo "$SP" # appId = client id, password = client secret, tenant = tenant id + +# Grant the three Microsoft Graph app roles the run needs, resolving every id from $SP so there is +# nothing to copy-paste (Application.ReadWrite.All, Directory.Read.All, AppRoleAssignment.ReadWrite.All). +SP_OID=$(az ad sp show --id "$(echo "$SP" | jq -r .appId)" --query id -o tsv) +GRAPH_OID=$(az ad sp show --id 00000003-0000-0000-c000-000000000000 --query id -o tsv) +for ROLE in 1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9 \ + 7ab1d382-f21e-4acd-a863-ba3e13f7da61 \ + 06b708a9-e830-4db3-a914-8e69da51d44f; do + az rest --method POST \ + --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_OID/appRoleAssignments" \ + --body "{\"principalId\":\"$SP_OID\",\"resourceId\":\"$GRAPH_OID\",\"appRoleId\":\"$ROLE\"}" +done