Secrets and variables¶
The full list of what to set on the landing-zone repository, in one place. Most of the variables are written by the bootstrap; the secrets you set yourself.
Repository variables¶
Set under Settings, Secrets and variables, Actions, Variables.
| Variable | Required | Purpose |
|---|---|---|
AZURE_TENANT_ID |
yes | Tenant id. |
AZURE_SUBSCRIPTION_ID |
yes | Subscription id for the state account and default context. |
BACKEND_AZURE_RESOURCE_GROUP_NAME |
yes | State storage resource group, read by root.hcl. |
BACKEND_AZURE_STORAGE_ACCOUNT_NAME |
yes | State storage account, read by root.hcl. |
BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME |
yes | State container, read by root.hcl. |
RUNNER_LABEL |
for private state | Self-hosted runner label. Falls back to ubuntu-latest. |
TFPR_EXTRA_TOOLS |
for the gates | Space-separated gate tools to install per run. infracost on the self-hosted posture (conftest and checkov are baked into the runner image); conftest checkov infracost on a GitHub-hosted posture. |
CATALOG_APP_CLIENT_ID |
yes | GitHub App client id. Authenticates the private engine checkout in reusable mode. The name is historical. |
TF_PR_OPS_ALLOW_UNREVIEWED_APPLY |
only for a repo with no required reviews | Set to true to allow /apply when the repository requires no approving reviews. Leave unset everywhere else. |
TF_PR_OPS_PLAN_ENVIRONMENT |
optional | Override the plan environment name. Default plan. |
TF_PR_OPS_APPLY_ENVIRONMENT |
optional | Override the apply environment name. Default apply. |
TFPR_LOCK_STORAGE_ACCOUNT |
optional | Storage account for the cross-PR unit lock table. Defaults to the BACKEND_AZURE_STORAGE_ACCOUNT_NAME account; required only for a plain Terraform repo with no backend variables. |
TF_PR_OPS_DISABLE_LOCKS |
optional | Set to true to turn cross-PR unit locking off. Leave unset everywhere else. |
TF_PR_OPS_ALLOW_UNREVIEWED_APPLY is a real relaxation
The engine reads GitHub's review decision to gate /apply. A repository that
requires no approving reviews reports an empty decision, which the engine
refuses, so /apply never runs and there is nothing to approve to unblock it.
Only that case needs this variable.
An empty decision is refused rather than allowed because it cannot be told apart from a ruleset that was deleted. Setting the variable gives that up knowingly. The usual reason is a single-writer organisation, which cannot self-approve on GitHub and so sets the required review count to 0 on purpose. Everywhere else, set the required review count instead and leave this unset.
Variables or ARM secrets
The workflow reads the identity as vars.AZURE_* || secrets.ARM_*. Prefer the
AZURE_* variables written by the bootstrap. The ARM_* secrets exist as a
fallback for demos that set the identity by hand.
Repository secrets¶
Set under Settings, Secrets and variables, Actions, Secrets.
| Secret | Required | Purpose |
|---|---|---|
CATALOG_APP_PRIVATE_KEY |
yes | GitHub App private key. Required in reusable mode: it authenticates the private engine checkout. The name is historical. |
INFRACOST_API_KEY |
for the cost gate | Hosted pricing API key. Until set, the infracost gate notes it and skips. |
ARM_CLIENT_ID |
fallback only | Identity client id when not using AZURE_* variables. |
ARM_TENANT_ID |
fallback only | Tenant id fallback. |
ARM_SUBSCRIPTION_ID |
fallback only | Subscription id fallback. |
GITHUB_TOKEN is provided automatically by GitHub Actions and posts the comments,
checks, and statuses. You do not set it.
Why the CATALOG_APP_* names
The GitHub App originally fetched modules from a private Terragrunt catalog. That consumption model was dropped in July 2026: the custom Azure Policy library is now vendored into the landing-zone repository, and the App's only remaining job is authenticating the private engine checkout. The names were kept so existing repositories keep working. This is the whole story; other pages link here rather than repeating it.
Environments¶
Two GitHub environments on the repo, each with an Azure federated credential:
| Environment | Identity | Used by |
|---|---|---|
plan |
Reader (read-only) | PR plan jobs and the daily drift plan. |
apply |
Owner | The apply job. |
Each environment carries its own AZURE_CLIENT_ID variable, pointing at that
environment's identity. The bootstrap sets both. It is an environment variable,
not a repository variable: a single repository-level value would give the plan jobs
the Owner identity, or the apply jobs the Reader one.
The OIDC subject binds the repository and the environment name, built with immutable
repository ids (repo:org@id/repo@id:environment:env) for repos created after
GitHub's rollout, so keep the names stable. If you must rename them, set
TF_PR_OPS_PLAN_ENVIRONMENT and TF_PR_OPS_APPLY_ENVIRONMENT and update the
federated credential subjects to match.
Branch protection¶
Make tf-pr-ops / merge-gate a required status check on the main
branch, and set the required reviews you want /apply to respect.
The bootstrap provisions a require-approved-pr-to-main ruleset that requires an
approved pull request. It adds the required status check only when its
required_status_checks variable is set, and that variable is empty by default,
so by default the merge gate is not required. Set the variable to
["tf-pr-ops / merge-gate"] before running the bootstrap, or add the check by
hand afterwards.
The cost gate key¶
The infracost gate uses the legacy infracost line. Get a static API key from the
Infracost dashboard, set it as INFRACOST_API_KEY, and add infracost to
TFPR_EXTRA_TOOLS. See the gates for detail and the data
residency note.