Skip to content

The vendored policy library

live/_foundation/landing-zones/lib/ is a complete Azure Landing Zones library, carried inside the customer repository. The alz provider reads it on top of the stock ALZ library. This page walks the folder asset by asset and explains the one maintenance obligation it creates.

For the recipes that change policy, read Customizing policies. This page is the map of the folder, not the how-to.

Why it is vendored

The library could be a git catalog shared across repositories. It is not. Each customer repository carries its own copy, so the alz provider resolves it from the working directory at plan time.

That removes a private dependency from the plan path. A shared catalog means every plan reaches a second private repository, with credentials, network access, and a ref to bump on release. Vendored, a policy change is reviewable in the same diff as the code that consumes it. The cost is that each repository diverges over time, a deliberate trade for a per-customer platform.

How the provider consumes it

From landing-zones/terragrunt.hcl:

provider "alz" {
  library_overwrite_enabled = true
  library_references = [
    { path = "platform/alz", ref = "2026.04.2" },
    { custom_url = "${get_terragrunt_dir()}/lib" },
  ]
}

Three things are doing work here.

  • The list is ordered. The stock library is read first and the local one last, so local assets layer on top.
  • custom_url is built with get_terragrunt_dir(), the directory of the terragrunt.hcl being run. The path is absolute at runtime, so it resolves the same from any working directory.
  • library_overwrite_enabled = true allows a local asset to replace a stock asset of the same name. Without it, a name collision is an error. Use it sparingly: an overwritten asset stops tracking upstream.

Assets are discovered by filename suffix, not by registration. <Name>.alz_policy_definition.json is a policy definition because of how it is named. Add a file with the right suffix and the provider picks it up on the next plan. There is no index to update and no provider change.

The folder

lib/
├── alz_library_metadata.json                                   # library identity
├── README.md                                                   # in-repo notes
├── architecture_definitions/
│   └── nrit.alz_architecture_definition.json                   # the MG hierarchy
├── archetype_definitions/
│   └── nrit_tags.alz_archetype_definition.json                 # what the root gets
├── policy_definitions/
│   └── Deny-Tag-NotAllowedValues.alz_policy_definition.json    # custom rule
├── policy_set_definitions/
│   └── Enforce-Tag-Governance.alz_policy_set_definition.json   # the initiative
├── policy_assignments/
│   └── Enforce-Tag-Gov.alz_policy_assignment.json              # the assignment
└── policy_default_values/
    └── .gitkeep                                                # empty, on purpose

alz_library_metadata.json

Names the library nrit, gives it a display name and description, sets path = "platform/nrit", and declares dependencies: []. The empty dependency list is correct here: the stock library is supplied by the provider's ordered library_references, not pulled in by this metadata.

architecture_definitions/nrit.alz_architecture_definition.json

The management group hierarchy the module deploys. It is the stock alz architecture reproduced in full, plus exactly one line: nrit_tags added to the archetypes list on the intermediate root.

{
  "archetypes": ["root", "nrit_tags"],
  "display_name": "Azure Landing Zones",
  "exists": false,
  "id": "alz",
  "parent_id": null
}

The other eleven groups (platform, landingzones, corp, online, local, sandbox, security, management, connectivity, identity, decommissioned) carry their stock archetypes and parents, unchanged.

The duplication is not a style choice. The ALZ module needs a complete architecture to deploy any customization at all. There is no way to extend the stock architecture in place, so adding one archetype to one management group means carrying the whole hierarchy. This is the file architecture_name = "nrit" in landing-zones/main.tf selects.

Re-sync this file on every library upgrade

When you bump the platform/alz ref in landing-zones/terragrunt.hcl, you must re-sync nrit.alz_architecture_definition.json by hand: fetch the stock alz architecture at the new version, diff it against this file, apply any hierarchy changes upstream made, and keep nrit_tags on the intermediate root. The only intended local delta is that one entry.

Nothing tracks this. No test fails and no plan warns if the hierarchy has moved on. Put the re-sync in the same pull request as the ref bump. See Versions and upgrades.

The placeholder management group segment

Resource ids that point from one asset to another use a literal placeholder segment:

"policyDefinitionId": "/providers/Microsoft.Management/managementGroups/placeholder/providers/Microsoft.Authorization/policySetDefinitions/Enforce-Tag-Governance"

alzlib resolves assets by name, not by resource id, so the management group in the path is irrelevant at authoring time. It is rewritten to the real management group when the plan is built. Write placeholder in any cross-asset reference and leave it alone.

archetype_definitions/nrit_tags.alz_archetype_definition.json

The bundle attached to a management group. Four lists, three of them populated: policy_assignments holds Enforce-Tag-Gov, policy_definitions holds Deny-Tag-NotAllowedValues, policy_set_definitions holds Enforce-Tag-Governance, and role_definitions is empty.

An archetype lists names. It does not contain the assets, and it does not decide scope. Scope comes from the architecture attaching the archetype to a management group.

policy_definitions/Deny-Tag-NotAllowedValues.alz_policy_definition.json

One custom rule, mode: All, with three parameters: effect (Audit, Deny, or Disabled, defaulting to Deny), tagName, and tagAllowedValues.

The rule fires when both conditions hold: the tag exists, and its value is not in the allowed list.

"if": {
  "allOf": [
    { "exists": "true", "field": "[concat('tags[', parameters('tagName'), ']')]" },
    { "field": "[concat('tags[', parameters('tagName'), ']')]", "notIn": "[parameters('tagAllowedValues')]" }
  ]
}

A resource missing the tag entirely is never evaluated. This policy validates values, it does not require the tag. On its own it looks like tag enforcement and enforces nothing, so it must be paired with a mandatory-tags policy. The initiative below does exactly that.

policy_set_definitions/Enforce-Tag-Governance.alz_policy_set_definition.json

The initiative that wires the definitions together. It exposes four parameter pairs, an allowed-values list and an effect, each effect defaulting to Audit:

Values parameter Default values Effect parameter
rgMandatoryTags app, opsteam, criticality, confidentiality rgMandatoryTagsEffect
criticalityAllowedValues mission-critical, medium, low criticalityEffect
confidentialityAllowedValues public, private, confidential, restricted confidentialityEffect
environmentAllowedValues prod, staging, dev environmentEffect

Eleven members:

Count Members Definition
1 RgMandatoryTags Stock Audit-Tags-Mandatory-Rg, requires the mandatory tags on resource groups.
3 CriticalityAllowedValues, ConfidentialityAllowedValues, EnvironmentAllowedValues Local Deny-Tag-NotAllowedValues, one per tag.
4 InheritAppFromRg, InheritOpsteamFromRg, InheritCriticalityFromRg, InheritConfidentialityFromRg Built-in inherit-tag-from-resource-group.
3 InheritBusinessunitFromSub, InheritEnvFromSub, InheritCostcenterFromSub Built-in inherit-tag-from-subscription.

The mandatory-tags member and the three allowed-values members are the pairing the custom definition needs: one requires the tags, the others police their values. The seven inherit members are Modify policies, so they need a remediation identity and only touch existing resources once a remediation task runs.

policy_assignments/Enforce-Tag-Gov.alz_policy_assignment.json

Assigns the initiative, with a SystemAssigned identity for the Modify members, a non-compliance message, empty notScopes, and parameters: {}.

It ships with enforcementMode: "Default", which means enforcing. The advisory posture does not come from here: it comes from the four effect parameters pinned to Audit in landing-zones/main.tf under policy_assignments_to_modify. The assignment fires, the effects are just set to report. Change the file and the parameters and you have changed two different things.

policy_default_values/

Empty, holding only a .gitkeep. The directory exists so the shape of the library is complete. Default values are supplied from landing-zones/main.tf through the module's policy_default_values input instead.

Adding a policy

  1. Write the definition as lib/policy_definitions/<Name>.alz_policy_definition.json.
  2. If it belongs in an initiative, add it as a member of a set in lib/policy_set_definitions/, referencing it by name with a placeholder management group segment. Skip this if you will assign it directly.
  3. Add the assignment as lib/policy_assignments/<Name>.alz_policy_assignment.json.
  4. List the definition, the set, and the assignment on an archetype in lib/archetype_definitions/. An asset that no archetype names is never deployed.
  5. Attach that archetype to a management group in architecture_definitions/nrit.alz_architecture_definition.json, by adding it to that group's archetypes list. This is what gives the policy its scope.
  6. Optionally tune it per management group from landing-zones/main.tf with policy_assignments_to_modify, for example to start the effect on Audit.

Steps 1 to 5 are all one commit in lib/. The provider reads the folder directly, so there is no version to bump and the change is live on the next apply.

Next: Growing the tree.