Skip to content

Growing the tree

The template ships platform/ and landingzones/ as README-only placeholder folders. This page explains what each placeholder becomes, and how you fill one in when a customer needs a connectivity hub, a landing zone, or another unit.

The placeholders are deliberate

A folder with no terragrunt.hcl is not a unit. Discovery walks straight past it, so a README-only folder costs nothing at plan time and nothing at apply time. The tree documents the shape of the estate before the resources exist.

That is why every management group in the Cloud Adoption Framework hierarchy has a folder from day one, even the ones with no subscription. You read the tree and see where things will go. When a subscription arrives, you add files to a folder that is already in the right place, and the state key follows the path automatically.

What each placeholder is for

Each README states the management group it represents and what belongs there.

Folder Management group What it becomes
platform/connectivity/ connectivity The hub network. Onboarded per customer.
platform/identity/ identity Identity services. No subscription in v1.
platform/management/ management Management-subscription workloads, for example a backup vault.
platform/security/ security Security tooling. No subscription in v1.
landingzones/corp/ corp Corp-connected workloads, no public ingress by default.
landingzones/online/ online Internet-facing workloads.
landingzones/local/ local Azure Local landing zones. Carries the local archetype and its single Enforce-ALDO-Services assignment. Most customers never use it, but the management group is still created, because the ALZ module needs a complete architecture.
sandbox/ sandbox Loosely governed experimentation subscriptions.
decommissioned/ decommissioned Holding area for subscriptions being retired.

platform/management/ does not hold the tenant foundation. The management group hierarchy, the base policy, the Log Analytics workspace, the data collection rules, and the AMA identity all live in live/_foundation/, deployed once at the top of the tree. The landing-zones unit's policy assignments reference the management resources by id and need them to exist first. See The foundation units.

The flattening rule

A platform management group with exactly one subscription puts subscription.hcl directly in the management group folder. There is no named subscription level, because there is nothing to name it apart from.

live/platform/connectivity/
├── subscription.hcl        # the connectivity subscription
└── westeurope/
    ├── region.hcl
    └── caf-connectivity-hub/

A landing zone uses a named subscription folder, because a management group holds many of them.

live/landingzones/corp/
└── corp-workload/          # the subscription, named for its purpose
    ├── subscription.hcl
    ├── _global/
    └── westeurope/

Onboarding a connectivity hub

Three things go in: a subscription.hcl with the connectivity subscription id, a region folder with its region.hcl, and a hub unit under that region folder.

The hub deploys into the connectivity subscription, which is a different subscription from the foundation's management subscription. Its subscription.hcl sets that id explicitly. State still goes to the one shared backend in the management subscription, over Entra ID, because root.hcl reads the backend subscription separately from the per-folder subscription.hcl.

Start from the minimal, low-cost hub: one virtual network, with the firewall, firewall policy, bastion, both gateways, private DNS zones, private DNS resolver, NAT gateway, and DDoS plan all off. Turn an option on per hub when a workload needs it.

In nrit-alz-live, live/platform/connectivity/westeurope/caf-connectivity-hub/ sources two modules:

Module Version Creates
Azure/avm-res-resources-resourcegroup/azurerm 0.4.0 rg-hub-weu.
Azure/avm-ptn-alz-connectivity-hub-and-spoke-vnet/azurerm 0.17.1 vnet-hub-weu, 10.0.0.0/16.

The hub module defaults every optional resource to on and gates creation on the flag alone, so each one is set false explicitly in main.tf. The DDoS plan is turned off in hub_and_spoke_networks_settings instead.

Private DNS zones need a foundation edit

If you enable private_dns_zones on the hub, also wire the foundation policy default values in live/_foundation/landing-zones/main.tf, so the ALZ policy assignments reference the live resources rather than nothing.

Onboarding a landing zone

A landing zone is two units in one named subscription folder, and they own different things on purpose.

  1. _global/lz-vending/ owns the subscription lifecycle only: alias or adoption, placement under the target management group, identities, RBAC, and budgets. It creates no network. It is region-agnostic, so it sits under _global/ with a co-located region.hcl that exists only because root.hcl reads one from every unit.
  2. <region>/network/ owns all spoke network config: the resource group, the spoke virtual network, subnets, NSGs, route tables, and hub peering.

Apply order is the connectivity hub, then lz-vending, then the spoke network. Corp versus online is just the target management group set in lz-vending.

In nrit-alz-live, live/landingzones/corp/corp-workload/ uses the same module for both units, configured differently:

Unit Module Version Configured for
_global/lz-vending/ Azure/avm-ptn-alz-sub-vending/azure 0.2.1 Adopt the existing subscription (subscription_alias_enabled = false), place it under corp. No UMI, RBAC, or budget.
westeurope/network/ Azure/avm-ptn-alz-sub-vending/azure 0.2.1 Network only. Every subscription-level input fixed off so the two units never contend for subscription-level state.

The spoke peers to the hub by a resource id computed from the connectivity subscription, resource group, and VNet name. That keeps it known at plan time and avoids reading another unit's output.

Two settings you will get wrong once:

  • Every subnet needs a network_security_groups entry associated to it. The ALZ landing-zone policy Deny-Subnet-Without-Nsg denies any subnet with none.
  • Set hub_peering_options_tohub = { use_remote_gateways = false } when the hub has no VPN or ExpressRoute gateway. The module default is true, and the peering fails against a gateway-less hub.

Adding a unit

The same four steps whatever the unit deploys.

  1. Create the leaf folder under the right scope, so it inherits the subscription and region you want.
  2. Add a terragrunt.hcl that includes root.hcl and declares any ordering in a dependencies block.
  3. Add a main.tf that sources an Azure Verified Module at a pinned version and sets its inputs.
  4. Open a pull request. Discovery picks the new unit up, plans it, and posts the result on the PR.

Adding a region

Add the region folder with its region.hcl, then the units under it. What multiplies per region is connectivity (a hub per region) and the workload spokes.

You do not touch _foundation/. The foundation is region-independent: the management groups and policy are global, and location on the two policy modules only picks the region for their managed identities. The one real regional decision is the Log Analytics workspace, and the template keeps a single central workspace, which is the ALZ single-pane default. AMBA alerts still land in the new region, because the AMBA policy's deployIfNotExists deploys them wherever the target resources are.

Move the workspace to a per-region folder under platform/management/<region>/management-resources/ only when data residency requires it. Governance stays global either way. The template's docs/foundation-structure.md has the full reasoning and the cross-tree references that shape costs.

Adding a workload resource

A common single resource (a storage account, a key vault, a Log Analytics workspace, a user-assigned identity) goes in its own unit under a region folder, next to the network unit. Each gets its own state, so it plans and applies without touching the spoke.

nrit-alz-live keeps a minimal example of this shape at live/landingzones/corp/corp-workload/westeurope/example/: a terragrunt.hcl that includes the root and nothing else, and a main.tf sourcing Azure/naming/azurerm 0.4.3 and Azure/avm-res-resources-resourcegroup/azurerm 0.4.0.

Next: Runbook.