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_urlis built withget_terragrunt_dir(), the directory of theterragrunt.hclbeing run. The path is absolute at runtime, so it resolves the same from any working directory.library_overwrite_enabled = trueallows 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¶
- Write the definition as
lib/policy_definitions/<Name>.alz_policy_definition.json. - If it belongs in an initiative, add it as a member of a set in
lib/policy_set_definitions/, referencing it by name with aplaceholdermanagement group segment. Skip this if you will assign it directly. - Add the assignment as
lib/policy_assignments/<Name>.alz_policy_assignment.json. - List the definition, the set, and the assignment on an archetype in
lib/archetype_definitions/. An asset that no archetype names is never deployed. - Attach that archetype to a management group in
architecture_definitions/nrit.alz_architecture_definition.json, by adding it to that group'sarchetypeslist. This is what gives the policy its scope. - Optionally tune it per management group from
landing-zones/main.tfwithpolicy_assignments_to_modify, for example to start the effect onAudit.
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.