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:
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_resourcerather than anazurerm_*type, so a rule keyed on the azurerm type can match nothing at all. Match both shapes, usingchange.after.typefor the azapi case. change.actionsis an array. A replace is["delete","create"], an unchanged resource is["no-op"]. Test membership within, neveractions[0].change.afteris null on a destroy. Guard it, or the rule evaluates to undefined and thedenyfails 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
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.