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, called through the public tf-pr-ops entrypoint) |
The shared automation: the compiled tfpr engine binary, driven by GitHub Actions reusable workflows and a composite action, that plans, applies, detects drift, and runs the gates. Customer repos call the public entrypoint at a pinned tag; the jobs check the private core out at the same tag. They do not copy it. |
| Landing-zone repository | A per-customer Terragrunt monorepo. It holds the actual Azure landing zone under live/, four thin caller workflows that invoke the engine, and a projects.yml that tells the engine what to discover, which paths to exempt from the merge gate, 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 (blob state and the unit-lock table, with the role assignments for both), the plan and apply OIDC identities and their federated credentials, theplanandapplyenvironments, the customer repository generated from the template, the branch ruleset, and the self-hosted runner and its image. See Bootstrap a customer.
flowchart TB
subgraph GH["GitHub organization"]
LZ["Landing-zone repo<br/>(live/ + projects.yml<br/>+ four caller workflows)"]
ENG["tf-pr-ops entrypoint + nrit-tf-pr-ops core<br/>engine at a pinned tag"]
LZ -->|uses: @tag| ENG
ENG -.->|tfpr binary + gate scripts<br/>at engine_ref| LZ
end
subgraph AZ["Customer Azure tenant"]
RUN["Self-hosted runner<br/>(private network)"]
STATE[("Terraform state<br/>+ unit-lock table")]
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 four thin caller workflows, all pinned to the same
engine release. Two call the engine's reusable workflows (comment commands and
the drift sweep) and carry the pin twice, as the uses: ref and the
engine_ref input; two call the engine's dispatch composite action (the
pull request path and the unlock on close) and carry a single action pin. At
run time the engine installs the prebuilt tfpr binary for that tag and checks
the engine repo out into .tfpr-engine, exposed to hooks as
$TFPR_ENGINE_DIR, which is where the gate scripts resolve from. So the
customer repo holds only its own Terraform and the four callers; the engine is
never copied in. The current pins are on
Versions and upgrades; the mechanics are on
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. - Self-hosted runners run every job that touches Azure: plan, apply, the
drift plan, the unlock, and the drift run's lock sweep. 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, drift'sreport, and the PR caller'sdispatch) never request anid-token. The Azure jobs (the plan matrix,apply,unlock,drift-plan, and the lock sweep) 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.