Skip to content

Test a policy change

A policy change is a Terraform change on one unit, live/_foundation/landing-zones/. You test it the same way you test any change in the platform: open a pull request, read the plan the engine posts, then apply. The difference is that the diff shows Azure Policy assignments, definitions, and sets rather than infrastructure.

Through a pull request

Plans run in CI only; there are no local plan credentials by design. Open a PR that touches landing-zones/, let discovery plan it, and review the plan in the run comment. Planning the unit takes its cross-PR lock, so a second policy PR waits for the first. The policy, security, and cost gates run on the plan as usual. Comment /apply once the plan is approved, and merge when tf-pr-ops / merge-gate turns green. See Plan and apply for the command flow.

Read the plan before applying:

  • A parameter or effect change shows as an in-place update to the assignment's azapi_resource (this unit declares only the alz and azapi providers), with the parameter value changing in place.
  • creation_enabled = false shows the assignment being destroyed.
  • A new library definition or set shows as a create.

Local validation

What is safe locally is validation, not a plan. mise pins the Terraform and Terragrunt versions for the repo:

terragrunt --working-dir live/_foundation/landing-zones init -backend=false
TG_NO_AUTO_INIT=true terragrunt --working-dir live/_foundation/landing-zones validate

-backend=false and TG_NO_AUTO_INIT are what keep the run local: without them Terragrunt configures the real backend. The custom library is read straight from the vendored lib/ in the working tree; only the stock ALZ library reference is fetched, cached under .alzlib, which stays in .gitignore.

Rollout discipline

Land the change advisory first, confirm compliance data, then enforce. Keep one control per pull request so the plan stays readable and reversible. A parameter or effect change reverts by restoring the previous value and applying again.

Worked example

A parameter change is the reliable end-to-end test: it always shows in the plan. On a branch, flip one tag effect from Audit to Deny on the tag-governance assignment in landing-zones/main.tf:

Enforce-Tag-Gov = {
  parameters = {
    environmentEffect = jsonencode({ value = "Deny" })
  }
}

The plan is a single in-place update to the assignment on the intermediate root. Nothing else changes: no management group, definition, or set is touched.

  # module.management_groups...azapi_resource.policy_assignments["Enforce-Tag-Gov"] will be updated in-place
  ~ resource "azapi_resource" "policy_assignments" {
      ~ body = {
          ~ properties = {
              ~ parameters = {
                  ~ environmentEffect = { value = "Audit" -> "Deny" }
                }
            }
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Review the plan, comment /apply, then check the assignment in the Azure Policy compliance view. Revert by restoring Audit and applying again. Roll a real control out advisory-first: land it on Audit, confirm compliance, then move to Deny.

Not every attribute round-trips

Some assignment attributes do not produce a plan diff on an already-applied assignment in the current provider version. A non-compliance message change, for example, can plan as no change even though the input is valid. Always confirm your specific edit shows in the plan before you rely on it. Parameter and effect changes are the reliable signal.