
Hub-Deployment mit Azure Verified Modules
Azure Verified Modules (AVM) haben in der letzten Zeit an Bedeutung gewonnen und sind seit einiger Zeit mein präferierter Ansatz für Infrastructure-as-Code Implementierungen. In diesem Beitrag zeige ich euch, wie ihr mit AVM ein Hub-and-Spoke Netzwerk in Azure aufbauen könnt. Dabei verwende ich ein AVM Pattern-Modul, um eine oft genutzte Konfiguration zu demonstrieren.
Auf GitHub verfügbar
Den vollständigen Code für dieses Beispiel findet ihr in meinem GitHub Repository: https://github.com/cloudchristoph/lab-connectivity-hub-with-avm
Was ist AVM? Was ist ein Pattern-Modul?
Azure Verified Modules (AVM) sind von Microsoft unterstützte und empfohlene Terraform- oder Bicep-Module, die Best Practices für die Bereitstellung von Azure-Ressourcen implementieren. Sie bieten vor allem eine standardisierte und getestete Implementierung für häufig verwendete Funktionalitäten, wie Diagnostics Settings, Tags, Private Link, etc.
Ein Pattern-Modul ist eine spezielle Art von AVM, die ein komplettes Architektur- oder Designmuster implementiert. Es enthält nicht nur die Ressourcen, sondern auch die Logik und Konfiguration, um ein bestimmtes Szenario oder Muster in Azure umzusetzen. In diesem Fall implementiert das Pattern-Modul avm-ptn-alz-connectivity-hub-and-spoke-vnet das Hub-and-Spoke Netzwerkdesign, das häufig in Azure-Umgebungen verwendet wird.
Warum AVM für Hub-and-Spoke?
Bisher hatten wir verschiedenste Möglichkeiten, ein Hub-and-Spoke Netzwerk in Azure zu implementieren, sei es durch eigene Terraform- oder Bicep-Module oder auch das altbekannte terraform-azurerm-caf-enterprise-scale Modul. Dieses Modul war sehr mächtig und hat versucht quasi alles abzudecken – von Netzwerk, über Management Strukturen bis hin zu Policies. Dieser All-in-one-Ansatz hatte jedoch den Nachteil, dass er oft zu komplex und schwergewichtig war. Man war quasi gezwungen, das Modul mehrfach mit verschiedenen Parametern zu verwenden um nicht einen monolithischen Aufruf in seinen IaC Code zu haben.
Der neue Ansatz mit AVM ermöglicht uns es jetzt die einzelnen Komponenten des Hub-and-Spoke Designs modular und flexibel zu implementieren. Die Pattern-Module wiederrum bündeln jetzt einzelne Themenbereiche und verwenden dabei die AVM Module als Bausteine. In unserem Beispiel beschäftigen wir uns jetzt mit dem Connectivity-Stack, aber in einer realen Umgebung kommen noch weitere Module wie das `avm-ptn-alz’ dazu. Dieses kümmert sich dann um Management-Struktur und Policies.
Wie fangen wir an?
Mit Planung. Klar! Der erste Schritt ist die Zielarchitektur zu definieren. In unserem Fall wollen wir einen vollständigen Connectivity-Stack in der Region germanywestcentral aufbauen, der als zentrale Plattform für Hub-and-Spoke dient.
Komponenten der Zielarchitektur
Wir benötigen:
- Ein Hub-VNet mit den entsprechenden Subnetzen für Firewall + Management, VPN Gateway und Bastion Host
- Eine Azure Firewall Standard
- Ein VPN Gateway für die Verbindung zum On-Premises Netzwerk
- Routing, um den Traffic über die Firewall zu leiten
- Logging, um die Aktivitäten der Firewall zu überwachen
- Ein Bastion Host für den Zugriff auf alle Ressourcen in den Spokes
Zusätzlich stellen wir uns die Frage nach dem Lebenszyklus der Ressourcen. Beispielsweise gehe ich davon aus, dass Firewall-Regeln wesentlich häufiger angepasst werden als die Firewall selbst. Daher könnte es Sinn ergeben, die Basis-Komponenten wie Firewall/VPN/Bastion/Netz in einem eigenen Terraform-Projekt zu verwalten, während die Regeln in einem separaten Projekt liegen.
Das ist aber natürlich abhängig von der individuellen Umgebung und den Anforderungen.
Datei-Struktur
Unser Code-Beispiel verzichtet auf die Trennung in mehrere Projekte, damit ihr den Code in einem GitHub-Repository einfacher nachvollziehen könnt. Wir “simulieren” die Trennung der Projekte hier durch simple Ordner-Struktur.
Aufbau des Hub-Deployments
Schritt 1: Modul einbinden und grundsätzliche Konfiguration
Zunächst binden wir das Pattern-Modul avm-ptn-alz-connectivity-hub-and-spoke-vnet in unserem main.tf ein und bauen uns eine passende Resource Group.
Am Modul konfigureren wir im ersten Schritt, welche Ressourcen wir überhaupt bereitstellen wollen. Wie diese Ressourcen konfiguriert werden, also z.B. die Adressbereiche, die Firewall- und VPN-Einstellungen, etc. wird in den nächsten Schritten angepasst.
Das Modul erlaubt es, mehrere Hubs gleichzeitig bereitzustellen. Deshalb können wir einerseits die auszurollenden Ressourcen global angeben und andererseits pro Hub anpassen. In diesem Beispiel verwenden wir die globale Konfiguration, um DDoS Protection Pläne zu deaktivieren und die Hub-spezifische Konfiguration, um Firewall, VPN Gateway und Bastion Host zu aktivieren.
module "connectivity" {
source = "Azure/avm-ptn-alz-connectivity-hub-and-spoke-vnet/azurerm"
version = "0.16.13"
hub_and_spoke_networks_settings = {
enabled_resources = {
ddos_protection_plan = false
}
}
hub_virtual_networks = {
gwc = {
location = "germanywestcentral"
default_parent_id = azurerm_resource_group.hub.id
enabled_resources = {
firewall = true
firewall_policy = true
virtual_network_gateway_vpn = true
virtual_network_gateway_express_route = false
bastion = true
private_dns_zones = false
private_dns_resolver = false
}
}
}
}
resource "azurerm_resource_group" "hub" {
name = "rg-hub-network-gwc"
location = "germanywestcentral"
}
Schritt 2: IP-Addressplanung
An sich müssten wir keine IP-Adressbereiche angeben, da das Modul automatisch Adressbereiche für die Firewall, VPN Gateway, etc. generiert. Es ist aber natürlich sinnvoll, die Adressplanung selbst in der Hand zu haben und nicht dem Modul zu überlassen. Zumal das Modul 10.0.0.0/22 als Hub-Adressraum nutzen möchte. Das ist für einen Test okay, aber meistens nicht für produktive Umgebungen geeignet (weil bereits besetzt) und ich die Größe auch für etwas übertriebe groß halte (/22 sind rund 1000 nutzbare Adressen).
Wir planen für unsere Hub-Umgebung den Adressraum 10.20.0.0/23.
Tipp: Zur Addressplanung nutze ich das wunderbare Tool Visual Subnet Calculator. Unsere Planung mit Aufteilung der Subnetze könnt ihr hier sehen: Hub-Subnetze auf Visual Subnet Calculator. Ich stelle gerne die Firewall vorne in’s Netzwerk und Gateway ganz hinten in das Netzwerk.
Wie bilden wir die Subnetze?
In Terraform nutze ich dafür die Funktion cidrsubnet(), um aus dem Hub-Adressraum konsistent Subnetze für Firewall, Firewall Management, VPN Gateway und Bastion Host zu generieren. So vermeide ich manuelle Fehler bei der Subnetzplanung und kann für weitere Hubs einfach einen neuen Adressraum definieren und die Subnetze errechnen sich automatisch.
Die Funktion ist etwas tricky zu verstehen (finde ich). Der erste Parameter ist der Adressraum – klar, der zweite Parameter gibt an, wie viele zusätzliche Bits angefügt werden sollen (bei uns /23 + 3 = /26) und der dritte Parameter ist die Nummer des Subnetzes, das generiert werden soll – beginned bei 0 (bei uns bspw. für Bastion das siebente /26 Subnetz aus /23 = 10.20.1.128/26).
Um die Übersicht zu behalten, definiere ich die Adressräume für Hub und Subnetze als locals und nutze diese dann in der Modul-Konfiguration.
locals {
hub_address_prefix = "10.20.0.0/23"
subnet_firewall_prefix = cidrsubnet(local.hub_address_prefix, 3, 0)
subnet_firewall_management_prefix = cidrsubnet(local.hub_address_prefix, 3, 1)
subnet_bastion_prefix = cidrsubnet(local.hub_address_prefix, 3, 6)
subnet_gateway_prefix = cidrsubnet(local.hub_address_prefix, 3, 7)
}
Über den Parameter hub_virtual_network und den Unter-Parameter address_space können wir diesen Adressraum angeben und dann für unsere Komponenten die Subnetze definieren. Diese folgen immer dem gleichen Muster. Komponententyp als Parameter und subnet_address_prefix als Unter-Parameter. Die Azure Firewall bekommt zwei Subnetze, eines für die Firewall selbst und eines für das Management-Interface. Ich empfehle euch sehr, neue Firewall-Deployments mit Management Subnetz zu planen, selbst wenn dies noch nicht verpflichtend ist. Das lohnt sich in Zukunft!
Im Code nutzen wir dann diese locals.
hub_virtual_networks = {
gwc = {
location = "germanywestcentral"
default_parent_id = azurerm_resource_group.hub.id
enabled_resources = {
firewall = true
firewall_policy = true
virtual_network_gateway_vpn = true
virtual_network_gateway_express_route = false
bastion = true
private_dns_zones = false
private_dns_resolver = false
}
hub_virtual_network = {
address_space = [local.hub_address_prefix]
}
firewall = {
subnet_address_prefix = local.subnet_firewall_prefix
management_subnet_address_prefix = local.subnet_firewall_management_prefix
}
virtual_network_gateways = {
subnet_address_prefix = local.subnet_gateway_prefix
}
bastion = {
subnet_address_prefix = local.subnet_bastion_prefix
}
}
}
Schritt 3: Ausgestaltung unserer Komponenten
Nachdem wir die grundlegende Struktur unseres Hubs definiert haben, können wir jetzt die einzelnen Komponenten weiter konfigurieren. Wir werden eine Azure Firewall Standard bereistellen, ein zonen-redundantes VPN Gateway kleinerer Größe und für den Bastion Host aktivieren wir uns noch ein paar gute Funktionen.
Unser Firewall-Parameterblock wird jetzt so aussehen:
firewall = {
subnet_address_prefix = local.subnet_firewall_prefix
management_subnet_address_prefix = local.subnet_firewall_management_prefix
sku_name = "AZFW_VNet"
sku_tier = "Standard"
management_ip_enabled = true
}
Für das VPN Gateway wählen wir die SKU VpnGw1AZ. Wir können hier auch gleich einen Tunnel konfigurieren und damit sofort die Verbindung zum On-Premises Netzwerk herstellen – es sprengt nur unser Beispiel. Dinge, wie die Erstellung einer Route Table für das GatewaySubnet steuern wir ebenfalls direkt in der Config.
virtual_network_gateways = {
subnet_address_prefix = local.subnet_gateway_prefix
route_table_creation_enabled = true
route_table_gateway_firewall_route_enabled = false
vpn = {
sku = "VpnGw1AZ"
vpn_bgp_enabled = false
}
}
Für unseren Bastion Host aktivieren wir einige Funktionen, die das Leben erleichtern.
bastion = {
subnet_address_prefix = local.subnet_bastion_prefix
zones = []
bastion_public_ip = {
zones = []
}
file_copy_enabled = true
copy_paste_enabled = true
ip_connect_enabled = true
tunneling_enabled = true
shareable_link_enabled = true
}
Schritt 4: Firewall Policy in anderer Resource Group
Die Firewall Policy wird in einer eigenen Resource Group bereitgestellt. Wir haben ja am Anfang definiert, dass die Policy einem anderen Lebenszyklus unterliegen könnte als die Firewall selbst. Auch dies können wir einfach über die Parameter steuern.
DNS Proxy Funktionalität schalten wir hier direkt mit ein, damit die Firewall auch in Netzwerk-Regeln / Layer 3 mit FQDNs umgehen kann.
Der Firewall-Policy Parameterblock sieht dann so aus:
firewall_policy = {
resource_group_name = azurerm_resource_group.firewall_policy.name
dns = {
proxy_enabled = true
servers = []
}
}
Die Resource Group für die Firewall Policy definieren wir weiter unten im Code:
resource "azurerm_resource_group" "firewall_policy" {
name = "rg-hub-firewallpolicy-gwc"
location = "germanywestcentral"
}
Huch. Was haben wir denn hier?! Ein Osterei.
Merkt euch die Farbe für euren Eggs of Knowledge Preis.

Firewall-Regeln
Die Firewall-Regeln werden nicht über das Pattern-Modul konfiguriert, sondern über ein separates Projekt (oder simpler in unserem Beispiel als separater Ordner). Das hat den Vorteil, dass wir die Regeln unabhängig von der Firewall selbst anpassen können, ohne Gefahr zu laufen, die Firewall oder andere Ressourcen zu verändern.
Schritt 1: Firewall Policy Data Source
Zunächst müssen wir die Firewall Policy, die wir im Hub-Deployment erstellt haben, in unserem Firewall-Regel-Projekt referenzieren. Dafür gibt es zwei Möglichkeiten – entweder direkt die Resource ID als Variable übergeben oder die Firewall Policy über eine Data Source referenzieren.
In diesem Fall halte ich eine Data Source für sinnvoll.
data "azurerm_firewall_policy" "hub_fw_policy" {
name = "fwp-hub-gwc"
resource_group_name = "rg-hub-firewallpolicy-gwc"
}
Schritt 2: Firewall-Regeln definieren
Für unser Beispiel habe ich uns ein paar einfache Network und Application Rules überlegt und diese auch mit IP Groups umgsetzt. Schaut euch gerne den Code im GitHub Repository an, um die vollständige Konfiguration zu sehen. Hier zeige ich euch exemplarisch, wie eine einfache Network Rule Collection definiert wird.
resource "azurerm_firewall_policy_rule_collection_group" "example" {
name = "rcg-hub-gwc"
firewall_policy_id = data.azurerm_firewall_policy.hub_fw_policy.id
priority = 100
rule_collections {
name = "rc-hub-gwc"
action = "Allow"
rules {
name = "Allow-OnPrem-Server"
rule_type = "NetworkRule"
source_addresses = ["*"]
destination_addresses = ["10.1.0.0/23"]
destination_ports = ["3389"]
protocols = ["TCP"]
}
}
}
}
Weitere Gedanken und Tipps
Das Pattern-Modul ist meines Erachtens noch nicht vollständig ausgereift, aber es bietet schon jetzt eine solide Grundlage für die Bereitstellung eines Hub-and-Spoke Netzwerks in Azure.
Bspw. fehlt mir momentan noch ein guter Weg, um die Diagnostics Settings für die Firewall zu konfigurieren. Im Beispiel-Repository hab ich zusätzlich noch main.firewalldiagnostics.tf angelegt, um die Diagnostics Settings für die Firewall zu konfigurieren.
Auch bin ich über seltene Rand-Konfigurationen gestolpert, die bisher noch nicht mit dem Modul abgebildet werden können. Beispielsweise ein Active-Active VPN Gateway mit zusätzlicher P2S VPN Konfiguration.
Ausgerollte Umgebung
Nach der Bereitstellung unserer Hub-Umgebung können wir uns – dieses mal mit Hilfe von AI – ein Diagramm der ausgerollten Ressourcen generieren lassen. Das sieht dann für unsere Umgebung so aus:

Ich experimentiere noch ein wenig mit den Skills und MCP-Servern. Wahrscheinlich gibts hier noch ein paar Updates über die nächsten Tage.
Fazit
AVM und die Pattern-Module bieten eine moderne und flexible Möglichkeit, Azure-Infrastruktur zu implementieren. Sie ermöglichen es uns, Best Practices zu nutzen und gleichzeitig die Kontrolle über die Konfiguration zu behalten. Das Hub-and-Spoke Pattern-Modul ist ein großartiges Beispiel dafür, wie wir komplexe Netzwerkarchitekturen in Azure mit AVM umsetzen können.
Eggs of Knowledge
Dieser Beitrag ist Teil der “Eggs of Knowledge” Serie des MVP-Treffs. Schaut gerne auf der Webseite vorbei, um weitere spannende Beiträge zu entdecken: https://www.mvptreff.de/beitraege-aus-der-community/