Skip to content

The repository root

The root of a landing-zone repository holds meta: documentation, the pinned toolchain, the conftest policy, and the ignore rules. Everything deployable lives under live/. This page covers the root, minus live/ and .github/.

The root files

File Purpose You edit it
README.md What the repository is, how a change is made, the live/ layout Optional
ONBOARDING.md Step-by-step for standing up a new customer landing zone No
CHANGELOG.md Per-customer record of version bumps and new workloads Yes, on every version bump
LICENSE Placeholder proprietary license Yes, before external distribution
mise.toml The pinned toolchain: Terragrunt, Terraform, and the commit-time linters Only to move the toolchain
.pre-commit-config.yaml The commit hooks; the PR caller's pre-commit job runs the same set on every PR Rarely
.tflint.hcl tflint config for the hooks and the pre-commit job Rarely
.checkov.yaml checkov config for the hooks and the pre-commit job Rarely
.editorconfig UTF-8, LF, two-space indent, trailing-whitespace rules No
.gitignore Keeps generated and local files out of git Rarely
projects.yml Engine discovery, the gate_ignore list, and gate hooks, covered on the next page Yes
AGENTS.md Instructions for AI coding agents, identical in every platform repository No
CLAUDE.md A one-line pointer at AGENTS.md No
.mcp.json Two MCP servers for agents: Microsoft Learn and the Terraform registry Optional
.claude/ Vendored agent skills, with a README on provenance and refresh No

mise.toml: the pinned toolchain

The repository pins everything the workflows and the commit hooks run:

[tools]
terragrunt = "1.0.7"
terraform = "1.15.5"

pre-commit = "4.6.1"
tflint = "0.64.0"
checkov = "3.3.8"

Run mise install in the repository root and you get exactly those versions locally. CI runs the same pins, so a local terragrunt plan and a plan in a pull request see the same binaries, and the commit hooks and the PR caller's pre-commit job run identical linter versions. To move the toolchain, change the versions here, open a pull request, and review the plans.

.pre-commit-config.yaml: the commit hooks

Wire the hooks up once with pre-commit install. On every commit they format HCL and Terraform, run tflint and checkov over the changed units, and check the repository invariants: no generated backend.tf, providers.tf, or context.tf committed, no Terraform state, no terragrunt.stack.hcl, and no directory holding Terraform without a terragrunt.hcl. The PR caller's pre-commit job runs the identical set on every pull request, so a commit made with --no-verify is caught there, and because the dispatch job needs it, a lint failure also stops the plan and leaves the required merge gate uncreated. What each layer checks and why is on Policy, security, and cost gates.

.tflint.hcl and .checkov.yaml configure the two linters. Both disable checks this repository's shape makes permanently unsatisfiable (providers.tf is generated, so the required-providers rules cannot pass; registry sources cannot carry a commit hash) and in exchange enforce the exact-version rule on every Azure Verified Module call.

.gitignore: what must never be committed

The ignore list is short, and two entries carry real weight.

backend.tf, providers.tf, and context.tf are generated. live/root.hcl writes them into each unit at run time, from the shared backend, provider, and context contract. They are build output, not source. If you commit one, that unit stops taking the shared contract and starts carrying a stale copy, and the next contract change silently skips it. A committed context.tf is the worst of the three: the unit keeps deploying to the old region after region.hcl has moved.

.alzlib/ is the alz provider's local cache of the ALZ library it downloads at the pinned library_references. It is a cache, it is rebuilt on demand, and it is large.

The rest is routine: **/.terraform/, **/.terragrunt-cache/, **/.terragrunt-stack/, local state files, **/*.local.hcl and **/*.local.tfvars for local overrides, and editor directories.

LICENSE

The shipped LICENSE is a self-declared NRIT Solutions proprietary notice, and it says so in its own text: the binding terms live in the partnership agreement. Replace it with the agreed license before the repository is distributed outside the customer.

policy/: the conftest gate, inert by default

policy/ is where the conftest gate looks. It globs policy/*.rego, and the repository ships no file matching that. The gate therefore reports No policies found in `policy`; skipped and passes until you add one. That is the intended starting state: a template that shipped a blocking policy would stop your first pull request before the estate is tagged.

What it does ship is policy/tags.rego.example. The .example suffix is what keeps it inert, because the name no longer ends in .rego. Activate it with one command:

git mv policy/tags.rego.example policy/tags.rego

Read what it denies before you do. It is a shift-left mirror of the ALZ Enforce-Tag-Gov assignment: it denies a resource group missing any of the four mandatory tags (app, opsteam, criticality, confidentiality) or carrying a value outside the allowed sets, and warns on the three tags Azure inherits from the subscription (businessunit, env, costcenter). It also warns on any resource a plan would destroy, so a destructive plan is obvious in review.

Against a tenant that is not yet tagged, that deny blocks every pull request touching a resource group. The usual order is to demote the deny rules to warn, clear the findings, then promote them back. For when and in what order to enforce, see Enforcement strategy.

Note the asymmetry once it is on. In the tenant the assignment is typically Audit, so Azure records a violation after deployment. The gate is deny, so the pull request never reaches Azure. The repository is deliberately stricter than the subscription.

Patterns that are easy to get wrong

The example documents four plan-JSON traps. All four fail the same way: the policy looks correct, the gate reports clean, and nothing was checked.

  • A rule that matches no resource reports a clean gate. Azure Verified Modules increasingly declare azapi_resource rather than an azurerm_* type, so a rule keyed on the azurerm type can match nothing at all. Match both shapes, using change.after.type for the azapi case.
  • change.actions is an array. A replace is ["delete","create"], an unchanged resource is ["no-op"]. Test membership with in, never actions[0].
  • change.after is null on a destroy. Guard it, or the rule evaluates to undefined and the deny fails open silently instead of firing.
  • An empty string is a present value. not tags[key] passes a tag set to "", so check the value, not just the key.

An unguarded deny fails open

Rego returns undefined, not false, when it walks into a null. A deny that reads change.after without a null guard produces no message on a destroy plan, which reads as a pass. Guard change.after != null in every rule that touches it.

Test a rule before trusting a pass

conftest test --policy policy --namespace main <plan.json>

Run it against a plan you know should fail. A rule that matches nothing is indistinguishable from one that passed, so a green result on its own proves nothing.

Where the documentation lives

The repository carries no docs/ folder. This site is the single documentation source: the repository's README.md links here, and everything the old in-repo pages covered has a home on the site (upgrades on Versions and upgrades, the gates on Policy, security, and cost gates, the foundation shape on The foundation units, value sharing on Sharing data between units, and day-2 operations on the Runbook). One source means one place to update, so the pages cannot drift apart. For an offline copy, clone this site's public source repository and build it, or read the Markdown directly.

The step-by-step for standing up a new customer is ONBOARDING.md at the repository root. There is no separate onboarding checklist file.

Next: Workflows and discovery config.