Skip to content

Customizing policies

Recipes for the two layers. The in-repo recipes change _foundation/landing-zones/main.tf in the landing-zone repo and take effect on the next apply. The in-library recipes change the vendored library under landing-zones/lib/ and take effect once committed (no external version to bump; a shared git catalog would need a ref bump instead).

Read Azure Policy first for the model and where each piece lives. The examples below use the starter's tag-governance archetype (Enforce-Tag-Gov) as a concrete illustration; substitute your own assignment names.

The landing-zone unit ships two commented reference blocks with worked, ready-to-lift examples of everything here: common_customizations (day-one settings plus every modify capability) and prod_hardening (the enforcement recommendation). See Enforcement strategy.

In-repo: tune an existing assignment

policy_assignments_to_modify changes assignments the archetypes already created. The outer key is the management group id, then policy_assignments, then the assignment name. You only specify the properties you want to change.

locals {
  policy_assignments_to_modify = {
    alz = {
      policy_assignments = {
        Enforce-Tag-Gov = {
          parameters = {
            rgMandatoryTagsEffect = jsonencode({ value = "Deny" })
            criticalityEffect     = jsonencode({ value = "Deny" })
            confidentialityEffect = jsonencode({ value = "Deny" })
            environmentEffect     = jsonencode({ value = "Deny" })
          }
        }
      }
    }
  }
}

Every parameter value is wrapped in jsonencode({ value = ... }). That is a workaround for the Terraform type system, not optional. The parameter names come from the assignment's policy set, so check the set definition for the names and their allowed values.

The attributes you can set on an assignment:

Attribute Effect
parameters Override parameter values. Map of name = jsonencode({ value = ... }).
enforcement_mode Default enforces, DoNotEnforce evaluates compliance without acting.
non_compliance_messages Custom messages shown on non-compliant resources.
not_scopes Resource ids excluded from evaluation, even below the assigned MG.
overrides Change the effect of the whole assignment or selected definitions.
resource_selectors Scope the assignment to certain locations or resource types.
identity / identity_ids The managed identity for deployIfNotExists and modify.
creation_enabled false stops the assignment being created at all.

Change enforcement mode

Deploy-MDFC-Config = {
  enforcement_mode = "DoNotEnforce"
}

DoNotEnforce reports compliance but never applies the effect. Use it to preview the impact of an assignment before you let it act.

Set a non-compliance message

Enforce-Tag-Gov = {
  non_compliance_messages = [
    { message = "Resource groups must carry workload, owner, criticality, and confidentiality tags." },
  ]
}

For an assignment that assigns an initiative, target one policy in the set with policy_definition_reference_id.

Verify the plan

In the current provider version a non-compliance message change on an already-applied assignment may plan as no change, even though the input is valid. Confirm your edit shows in the plan before relying on it. Parameter and effect changes always diff.

Disable a single assignment

connectivity = {
  policy_assignments = {
    Enable-DDoS-VNET = { creation_enabled = false }
  }
}

creation_enabled = false is a convenience for very small changes. For anything beyond one or two assignments, the module recommends excluding the assignment in your library archetype instead, so the intent is versioned rather than patched in the consuming repo.

In-repo: feed a default value

policy_default_values supplies a value once and fans it out to every assignment parameter in the library that declares that default name. The reference uses it for the shared monitoring resources: the Log Analytics workspace, the data collection rules, and the monitoring agent identity.

policy_default_values = {
  log_analytics_workspace_id = jsonencode({ value = local.law_id })
  # ...one entry per default name in the library
}

Known values only, no depends_on

The alz provider reads a data source at plan time, so it cannot take a value that is unknown until apply, and the module does not support depends_on. Build ids as strings from known names rather than reading them from another module's output. When you genuinely must order against a computed value, use the module's policy_assignments_dependencies and policy_role_assignments_dependencies inputs, not depends_on.

In-library: add a custom policy definition

Author in the vendored library under landing-zones/lib/. A definition is one file, <Name>.alz_policy_definition.json, holding the standard Azure Policy definition. The starter library ships a worked example, Deny-Tag-NotAllowedValues, which denies a resource when a given tag holds a value outside an allowed list.

To make a new definition live:

  1. Add lib/policy_definitions/<Name>.alz_policy_definition.json.
  2. Reference it from an initiative in lib/policy_set_definitions/, or assign it directly.
  3. List it on an archetype in lib/archetype_definitions/ so a management group actually receives it.
  4. Commit and open a pull request. The provider reads lib/ directly, so the change is live on the next apply, with no version to bump. (With a shared git catalog instead, bump the library version, tag the repo, and update the ref in library_references.)

The chain from definition to an assigned management group is: definition goes into an initiative (policy set), the initiative is assigned by an assignment, the assignment is listed on an archetype, and the archetype is attached to a management group by the architecture. The starter's tag archetype shows the whole chain: it lists a tag-governance assignment, the initiative behind it, and the Deny-Tag-NotAllowedValues definition, and the architecture attaches that archetype to the intermediate root.

In-library: replace a stock asset

To change a stock ALZ asset rather than add one, create an asset with the same name in your library. library_overwrite_enabled = true is already set in the provider, so your version wins. Use this sparingly: an overwritten asset no longer tracks the upstream library when you update the stock ref.

Rolling out a change

Introduce policy advisory-first.

  1. Plan first. Every change here is a terragrunt plan on the landing-zones unit. Read the diff before you apply. See Test a policy change.
  2. Advisory before enforced. Land a new control with its effect on Audit (or the assignment on DoNotEnforce), review compliance, then move to Deny.
  3. One control at a time. A single assignment or effect per pull request keeps the plan readable and the blast radius small.

This advisory-first path maps to the CAF Govern methodology and keeps the rollout inside WAF Operational Excellence. Enforcing afterwards is what delivers the WAF Security benefit.

Next: Test a policy change.