Skip to content

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-bootstrap provisions everything a landing-zone repo needs: the state storage account, the plan and apply OIDC identities and their federated credentials, the plan and apply environments, 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-catalog holds the source history of the custom Azure Policy library, nothing more. The library is vendored into each landing-zone repository at live/_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 have gh and 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_LABEL variable, and jobs fall back to ubuntu-latest when 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, plan and apply, 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 an id-token. The Azure jobs (run, apply, drift-plan) run under the plan or apply environment 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 sub claim of newly created repos as repo: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_private posture 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 a corp landing 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.