Skip to content

Self-hosted runner and the engine GitHub App

Why a self-hosted runner

Terraform state lives in a storage account behind a firewall that allows only known addresses. The jobs that read and write state (plan, apply, and the drift plan) run on a self-hosted runner on a private network with a fixed egress address that the state firewall allows.

The jobs that only talk to the GitHub API (event parsing, project selection, the merge gate, and drift issue reporting) run on hosted ubuntu-latest runners, because the self-hosted runner is network-scoped and does not carry gh.

Set the runner label with the RUNNER_LABEL repository variable. When it is not set, jobs fall back to ubuntu-latest, which is fine for a demo that does not need private state access.

The runner image

The self-hosted runner image bakes in the tools the gates need so nothing is installed on the critical path:

  • conftest and checkov are baked into the image. checkov is a Python tool with no static binary, so baking it is the production path. On the self-hosted posture, set TFPR_EXTRA_TOOLS to just infracost.
  • infracost is a static binary that the workflow installs per run when TFPR_EXTRA_TOOLS includes infracost. It installs to a user-writable path because the runner runs as a non-root user.

On a GitHub-hosted posture there is no baked image, so TFPR_EXTRA_TOOLS installs all three per run: conftest checkov infracost.

The image is built by the bootstrap in nrit-alz-bootstrap/runner-image/github-runner-aca.

Runner sizing

Runner replicas run at 2 vCPU and 4Gi with a 7200-second replica timeout. This is deliberate. The AVM module defaults (1 vCPU, 2Gi, 1800s) OOM-hang the heavy ALZ policy-library plans and kill the runner mid-job, which leaves stale state locks. The bootstrap sets the larger size through container_app_container_cpu, container_app_container_memory, and container_app_replica_timeout.

The engine GitHub App

nrit-tf-pr-ops is a private repository, so the reusable workflow needs a credential to check its own scripts out at engine_ref. A GitHub App mints that short-lived token. That is the App's only job.

Wire it with:

  • CATALOG_APP_CLIENT_ID (repository variable): the GitHub App client id. The bootstrap sets this.
  • CATALOG_APP_PRIVATE_KEY (repository secret): the GitHub App private key.

The CATALOG_APP_* names are historical and were kept so existing repositories keep working; see Secrets and variables for the background. The App installation must grant read access to nrit-tf-pr-ops. Because reusable mode depends on the engine checkout, both values are required on every landing-zone repository.

The private key is a real secret

Store CATALOG_APP_PRIVATE_KEY only as a GitHub Actions secret. Never put it in a tfvars file or commit it. Rotating it means updating the secret; the workflow reads it fresh on every run.

Cross-repo access

The landing-zone repo and the engine are separate private repositories in the same organisation. Set nrit-tf-pr-ops to organisation-accessible so the callers may invoke its reusable workflows. The GitHub App token described above is what authenticates the engine checkout itself.

Units source public Azure Verified Modules only, so no private module fetch happens on the plan path and nothing else needs a cross-repo grant.

Next: Secrets and variables.