Architecture¶
The repositories¶
The platform is one shared engine and a per-customer landing-zone repository, with two supporting repositories under NRIT control.
| Repository | Role |
|---|---|
PR-ops engine (nrit-tf-pr-ops) |
The shared automation. GitHub Actions reusable workflows plus a set of shell scripts that plan, apply, detect drift, and run the gates. Customer repos call it at a pinned tag; they do not copy it. |
| Landing-zone repository | A per-customer Terragrunt monorepo. It holds the actual Azure landing zone under live/, two thin caller workflows that invoke the engine, and a projects.yml that tells the engine what to discover and which gates to run. nrit-alz-customer-template is the template the bootstrap generates new customer repos from; nrit-alz-live is NRIT's own production repository, generated from the same template, and serves as the reference implementation. |
Two supporting repositories sit behind them:
nrit-alz-bootstrapprovisions everything a landing-zone repo needs: the state storage account, the plan and apply OIDC identities and their federated credentials, theplanandapplyenvironments, the customer repository generated from the template, the merge-gate ruleset, and the self-hosted runner and its image. See Bootstrap a customer.nrit-terragrunt-catalogholds the source history of the custom Azure Policy library, nothing more. The library is vendored into each landing-zone repository atlive/_foundation/landing-zones/lib/, nothing fetches the catalog at run time, and customer repositories need no access to it.
flowchart TB
subgraph GH["GitHub organisation"]
LZ["Landing-zone repo<br/>(live/ + projects.yml<br/>+ two caller workflows)"]
ENG["nrit-tf-pr-ops<br/>reusable workflows @v1.5.0"]
LZ -->|uses: @v1.5.0| ENG
ENG -.->|checkout at engine_ref<br/>into .tfpr-engine| LZ
end
subgraph AZ["Customer Azure tenant"]
RUN["Self-hosted runner<br/>(private network)"]
STATE[("Terraform state<br/>private endpoint")]
ALZ["Landing zone<br/>(management groups,<br/>subscriptions, resources)"]
end
ENG -->|OIDC login| AZ
RUN -->|plan / apply| ALZ
RUN -->|read / write| STATE
AVM["Public AVM registry<br/>(Terraform Registry)"]
RUN -.->|fetch modules| AVM
How the engine is consumed¶
The customer repo carries two ~30-line caller workflows pinned to
nrit-tf-pr-ops @v1.5.0. At run time the reusable workflow checks the engine scripts
out at the requested engine_ref into .tfpr-engine, exposed to hooks as
$TFPR_ENGINE_DIR. So the customer repo holds only its own Terraform and the two
callers; the engine is never copied in. See
The PR-ops engine.
Runner topology¶
Jobs run on one of two runner types, chosen per job.
- Hosted runners (
ubuntu-latest) run the jobs that only talk to the GitHub API: resolving the event, selecting projects, setting the merge gate, and reporting drift issues. These runners haveghand Python available. - Self-hosted runners run every job that touches Azure: plan, apply, and the
drift plan. They sit on a private network with access to the state account behind
its firewall. The runner label is set with the
RUNNER_LABELvariable, and jobs fall back toubuntu-latestwhen it is not set (useful for demos).
The runner image, the tools it bakes in, and the sizing the bootstrap applies (and why the AVM defaults are too small for ALZ policy plans) are covered in Self-hosted runner and the engine GitHub App.
Identity and state¶
- Azure access is Entra workload-identity OIDC. Two GitHub environments,
planandapply, each bind a federated credential. The plan identity is Reader (read-only) and the apply identity is Owner at the management group scope. There is no cloud secret in the repo. - Only the Azure jobs request a token. The trusted jobs (
resolve,gate,report) hold only a read-only GitHub App token and never request anid-token. The Azure jobs (run,apply,drift-plan) run under theplanorapplyenvironment and request the token there. This split keeps PR-branch code away from write credentials. - The OIDC subject uses immutable repository ids. GitHub renders the
subclaim of newly created repos asrepo:org@id/repo@id:environment:env. The bootstrap builds the federated credential subjects in that format by default (oidc_subject_uses_repo_ids), and only falls back to the legacy name-only form for repos created before that rollout. - State lives in an Azure storage account in the management subscription, with
Entra ID auth (no account keys). On the
self_hosted_privateposture a firewall allows only the runner's egress address; on the GitHub-hosted posture Entra auth is the only control. Terragrunt writes each unit's state to a key derived from the unit path.
The Security model page covers the identities, the trust split, and the gates in one place.
The landing zone¶
The reference landing zone follows the Cloud Adoption Framework layout under
live/:
- A
_foundation/tree that is deployed first and creates the management group hierarchy, base policy, management resources (Log Analytics, monitoring), and the Azure Monitor Baseline Alerts. - A
platform/tree for connectivity (hub network) and other platform subscriptions. - A
landingzones/tree for workload subscriptions (for example acorplanding zone with its spoke network and workloads).
Every unit is plain Terraform: a main.tf sourcing a public Azure Verified Module
at a pinned version, beside a thin terragrunt.hcl. The custom Azure Policy
library is vendored into the repository at live/_foundation/landing-zones/lib/
and layered over the stock ALZ library. See
The live tree for the full layout.
The same path, from a bootstrap-generated repo to a first applied plan, is what
NRIT runs on its own tenant. nrit-alz-live is generated from the same template
and calls the same engine release as a customer repo, so the platform is exercised
in production before it is handed over.
Read on: How it works.