Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
62 changes: 62 additions & 0 deletions modules/azure/hub-network/backplane/README.md
Original file line number Diff line number Diff line change
@@ -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.
- **`<name>-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.

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
| <a name="requirement_azurerm"></a> [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 |
|------|-------------|------|---------|:--------:|
| <a name="input_location"></a> [location](#input\_location) | Azure region for the UAMI resource group. | `string` | n/a | yes |
| <a name="input_name"></a> [name](#input\_name) | Name for the building block identity, resource group and role definition. | `string` | n/a | yes |
| <a name="input_scope"></a> [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 |
| <a name="input_subscription_id"></a> [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 |
| <a name="input_workload_identity_federation"></a> [workload\_identity\_federation](#input\_workload\_identity\_federation) | WIF issuer and subjects for federated authentication of the automation identity. | <pre>object({<br/> issuer = string<br/> subjects = list(string)<br/> })</pre> | n/a | yes |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_identity"></a> [identity](#output\_identity) | The managed identity used as the automation principal for this building block. |
| <a name="output_role_definition_id"></a> [role\_definition\_id](#output\_role\_definition\_id) | The ID of the role definition that enables deployment of the hub network to the connectivity scope. |
| <a name="output_role_definition_name"></a> [role\_definition\_name](#output\_role\_definition\_name) | The name of the role definition that enables deployment of the hub network to the connectivity scope. |
| <a name="output_scope"></a> [scope](#output\_scope) | The scope where the hub deploy role definition and role assignment are applied. |
<!-- END_TF_DOCS -->
61 changes: 61 additions & 0 deletions modules/azure/hub-network/backplane/main.tf
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions modules/azure/hub-network/backplane/outputs.tf
Original file line number Diff line number Diff line change
@@ -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."
}
7 changes: 7 additions & 0 deletions modules/azure/hub-network/backplane/provider.tf
Original file line number Diff line number Diff line change
@@ -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
}
41 changes: 41 additions & 0 deletions modules/azure/hub-network/backplane/variables.tf
Original file line number Diff line number Diff line change
@@ -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/<guid>' 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."
}
10 changes: 10 additions & 0 deletions modules/azure/hub-network/backplane/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.3.0"

required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.36.0"
}
}
}
81 changes: 81 additions & 0 deletions modules/azure/hub-network/buildingblock/README.md
Original file line number Diff line number Diff line change
@@ -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).

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.12.0 |
| <a name="requirement_azurerm"></a> [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 |
|------|-------------|------|---------|:--------:|
| <a name="input_address_space"></a> [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 |
| <a name="input_create_gateway_subnet"></a> [create\_gateway\_subnet](#input\_create\_gateway\_subnet) | Create a GatewaySubnet for a future VPN/ExpressRoute gateway. | `bool` | `true` | no |
| <a name="input_deploy_firewall"></a> [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 |
| <a name="input_firewall_sku_tier"></a> [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 |
| <a name="input_firewall_threat_intel_mode"></a> [firewall\_threat\_intel\_mode](#input\_firewall\_threat\_intel\_mode) | Azure Firewall threat intelligence mode: Off, Alert or Deny. | `string` | `"Alert"` | no |
| <a name="input_hub_resource_group_name"></a> [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 |
| <a name="input_hub_vnet_name"></a> [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 |
| <a name="input_location"></a> [location](#input\_location) | Azure region where the hub resource group, vnet and firewall are created. | `string` | `"germanywestcentral"` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_firewall_private_ip"></a> [firewall\_private\_ip](#output\_firewall\_private\_ip) | Private IP of the Azure Firewall, if deployed. Spokes route egress traffic here. |
| <a name="output_resource_group_name"></a> [resource\_group\_name](#output\_resource\_group\_name) | Name of the hub resource group. |
| <a name="output_summary"></a> [summary](#output\_summary) | Markdown summary of the created hub network. |
| <a name="output_vnet_id"></a> [vnet\_id](#output\_vnet\_id) | Azure resource ID of the hub virtual network. |
| <a name="output_vnet_name"></a> [vnet\_name](#output\_vnet\_name) | Name of the hub virtual network. Spoke networks peer into this vnet. |
<!-- END_TF_DOCS -->
Binary file added modules/azure/hub-network/buildingblock/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions modules/azure/hub-network/buildingblock/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
resource "azurerm_resource_group" "hub" {
name = var.hub_resource_group_name
location = var.location
}

resource "azurerm_virtual_network" "hub" {
name = var.hub_vnet_name
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
address_space = [var.address_space]
}

# GatewaySubnet — required by Azure for a VPN/ExpressRoute gateway, and a stable anchor even before
# a gateway is deployed. The name must be exactly "GatewaySubnet".
resource "azurerm_subnet" "gateway" {
count = var.create_gateway_subnet ? 1 : 0

name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.hub.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = [cidrsubnet(var.address_space, 2, 1)]
}

# ── Optional Azure Firewall ──
# When enabled, the hub gets an AzureFirewallSubnet (name is fixed by Azure), a static public IP,
# an Azure Firewall, and a route table with a default route pointing at the firewall so spokes can
# egress through it.

resource "azurerm_subnet" "firewall" {
count = var.deploy_firewall ? 1 : 0

name = "AzureFirewallSubnet"
resource_group_name = azurerm_resource_group.hub.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = [cidrsubnet(var.address_space, 2, 0)]
}

resource "azurerm_public_ip" "firewall" {
count = var.deploy_firewall ? 1 : 0

name = "${var.hub_vnet_name}-fw-pip"
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
allocation_method = "Static"
sku = "Standard"
}

resource "azurerm_firewall" "hub" {
count = var.deploy_firewall ? 1 : 0

name = "${var.hub_vnet_name}-fw"
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name
sku_name = "AZFW_VNet"
sku_tier = var.firewall_sku_tier
threat_intel_mode = var.firewall_threat_intel_mode

ip_configuration {
name = "primary"
subnet_id = azurerm_subnet.firewall[0].id
public_ip_address_id = azurerm_public_ip.firewall[0].id
}
}

resource "azurerm_route_table" "egress" {
count = var.deploy_firewall ? 1 : 0

name = "${var.hub_vnet_name}-egress-rt"
location = azurerm_resource_group.hub.location
resource_group_name = azurerm_resource_group.hub.name

route {
name = "default-via-firewall"
address_prefix = "0.0.0.0/0"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = azurerm_firewall.hub[0].ip_configuration[0].private_ip_address
}
}
Loading
Loading