Skip to content

Bootstrap reference

Everything nrit-alz-bootstrap creates, every input it takes, and what it deliberately leaves to you. For the procedure, see Bootstrap a customer.

What it creates

State backend

Terraform state for the customer's landing zone, in the management subscription (state.tf).

Resource Detail
Resource group rg-cicd-state-<location_short>
Storage account stcicd<location_short><random4>, Standard ZRS, StorageV2, TLS 1.2 minimum
Auth Entra ID only. shared_access_key_enabled = false, no public blob access
Data protection Blob versioning on, seven-day blob and container soft delete
Container tfstate, created through the ARM control plane with azapi because the data plane needs a shared key
Role assignments Storage Blob Data Contributor on the container for both deploy identities

One account holds the state for every unit in the customer repository. Units are isolated by blob key (path_relative_to_include() in root.hcl), not by separate accounts. See The root contract.

Identities and OIDC

Two user-assigned managed identities and their federated credentials (identity.tf), in rg-cicd-identity-<location_short>.

Identity Name Management group role GitHub environment
Plan id-cicd-plan-<location_short> plan_role_definition_name, default Reader plan
Apply id-cicd-apply-<location_short> apply_role_definition_name, default Owner apply

Apply needs Owner because the foundation creates role assignments and policy.

The federated credential subject binds two claims, the customer repository and the deployment environment, for example repo:nrit-solutions/nrit-alz-live:environment:plan. The trust boundary is the repository plus the GitHub environment. The environments carry no protection rules of their own.

There is no separate drift identity. The scheduled drift workflow runs in the plan environment on the plan identity, so the plan credential covers it and drift reads the tenant with the same least-privilege identity as the pull-request plan. See Drift detection.

Why the engine tag is not in the subject

The reusable-pipeline tag and workflow file are deliberately absent from the subject, so bumping the engine version needs no re-bootstrap. This was a trade, not an oversight: Entra's flexible federated identity credential (preview) caps the claim-matching expression at 128 characters, which a full repository plus environment plus job_workflow_ref plus tag subject exceeds. Keeping the workflow-file pin would have meant dropping the repository or the environment claim. The Owner path is gated instead by the engine's review-decision check, backed by the approved-PR ruleset.

GitHub

In the organisation named by github_organization (github.tf).

Thing Detail
Customer repository Private, generated from nrit-alz-customer-template. Issues enabled, because drift reports to Issues and template repositories can inherit issues off. The template block is ignored after creation
OIDC subject customisation include_claim_keys = ["repository", "environment"], matching the federated credentials
Environments plan and apply, no protection rules
Approver team <customer_name>-alz-apply-approvers, members from apply_approvers, push permission on the repository. Created only when the list is non-empty
Ruleset require-approved-pr-to-main, active on the default branch. Requires an approved pull request and any required_status_checks. Stale reviews dismissed on push. Organisation admins can bypass, for break-glass
Repository variables AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, BACKEND_AZURE_RESOURCE_GROUP_NAME, BACKEND_AZURE_STORAGE_ACCOUNT_NAME, BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME, RUNNER_LABEL, TFPR_EXTRA_TOOLS, and CATALOG_APP_CLIENT_ID when a client id is set
Repository secret CATALOG_APP_PRIVATE_KEY, when catalog_app_private_key is set
Environment variables AZURE_CLIENT_ID per environment, pointing at that environment's identity

AZURE_CLIENT_ID is an environment variable rather than a repository variable on purpose. A single repository-level value would hand the plan jobs the Owner identity, or the apply jobs the Reader one.

The ruleset gates the merge, not the apply. There is no push trigger and nothing applies on merge. The PR-ops engine (nrit-tf-pr-ops) applies from an /apply comment on the open pull request and checks the same review decision itself. A ruleset is used rather than environment required-reviewers because GitHub does not offer environment protection on private repositories on the Team plan, whereas rulesets work there. That keeps the gate portable.

Runner infrastructure

Created only when network_posture = "self_hosted_private" (runners.tf, state-private-endpoint.tf), in rg-cicd-runners-<location_short>.

Thing Detail
Runner module Azure/avm-ptn-cicd-agents-and-runners/azurerm version 0.6.0, repo-scoped GitHub runners registered with a PAT
Network A standalone bootstrap virtual network from runner_vnet_address_space, single zone. Independent of the landing zone connectivity stack, so there is no circular dependency at onboarding. It can be peered to the hub later
Egress A public IP the bootstrap owns, attached to the module's NAT gateway. This is the single address allowlisted on the state firewall
State access A private endpoint for the state account's blob service, plus a privatelink.blob.core.windows.net private DNS zone linked to the runner network
Compute Container App Jobs by default, scaling to zero when idle. Container Instances as the always-on alternative
Registry The module's ACR, used when building a custom runner image

The private endpoint is the real path to state, not the IP allowlist. Azure Container Instances do not reliably egress through a subnet NAT gateway, so state traffic goes over the virtual network and never touches the public endpoint.

Network postures

One variable, network_posture, selects the model, and it sets the pattern every later IaC deployment for the customer follows.

github_hosted (default) self_hosted_private
Runner GitHub-hosted, ubuntu-latest Self-hosted on the bootstrap network, one concurrent runner by default
RUNNER_LABEL ubuntu-latest self-hosted
State account network rules None. Public endpoint, Entra ID auth Default-deny, allowing the runner egress IP (and the operator IP when allow_state_access_from_my_ip is true). Reached over a private endpoint
TFPR_EXTRA_TOOLS conftest checkov infracost infracost
Use for Non-production, NRIT dogfood, UNO demos Customer production

self_hosted_private is the production standard. A production landing zone's state should be reachable only from the runner, and later workloads sit behind private endpoints that only an in-network runner can reach. github_hosted is the simple, low-cost path for everything else.

TFPR_EXTRA_TOOLS differs because the custom self-hosted runner image already carries conftest and checkov, so only infracost is installed per run. GitHub-hosted runners carry none of them.

Variables

Required

No default. Set every one of these in customers/<customer>.tfvars.

Variable Type Purpose
customer_name string Customer slug used in resource names. Two to twenty characters, lower case letters, digits, hyphens
tenant_id string The customer's Entra ID tenant
management_subscription_id string Holds the state, the CI/CD identities, and the runners. There is no separate bootstrap subscription
customer_repository_name string Name of the infrastructure-live repository to generate

Deploy identity scope

Variable Default Purpose
management_group_scope_id null Management group the plan and apply identities operate at, the ALZ parent. Null means the tenant root group. For an apply test, point this at a throwaway parent management group, never the live ALZ tree
plan_role_definition_name "Reader" Built-in role for the plan identity at that scope
apply_role_definition_name "Owner" Built-in role for the apply identity at that scope. Owner is required because the foundation creates role assignments and policy

GitHub

Variable Default Purpose
github_organization "nrit-solutions" Organisation that owns the customer repository
template_repository {owner = "nrit-solutions", repository = "nrit-alz-customer-template"} Template the customer repository is generated from
default_branch "main" Default branch of the customer repository
oidc_subject_uses_repo_ids true Build federated credential subjects with immutable repository ids. See the note below
require_approved_pr true Create the require-approved-pr-to-main ruleset. False skips the merge gate entirely
required_approving_review_count 1 Approving reviews the ruleset requires. Only applies when require_approved_pr is true
required_status_checks [] Status-check contexts the ruleset requires green before merge. Only applies when require_approved_pr is true
apply_approvers [] GitHub usernames that review the customer repository's pull requests. A team is created, populated, and given push permission
catalog_app_client_id "" Client id of the GitHub App, set as the CATALOG_APP_CLIENT_ID repository variable. Empty skips it

required_status_checks defaults to no check at all

The recommended value is ["tf-pr-ops / merge-gate"], already present in customers/_example.tfvars. That gives the apply-before-merge model: a pull request cannot merge until /apply has run, and the engine seeds the check success when nothing is affected. The default [] means no status check is required, so a pull request can merge with unapplied changes.

required_approving_review_count = 0 needs a manual follow-up

A single-writer organisation cannot self-approve on GitHub, so it sets 0 to keep the pull-request gate without an unmeetable review. GitHub then returns an empty reviewDecision, and the engine refuses to apply on an empty decision. Such a repository also needs the repository variable TF_PR_OPS_ALLOW_UNREVIEWED_APPLY set to true by hand. The bootstrap does not manage that variable, so a re-run will not create or restore it. With the default of 1 the decision is never empty and no follow-up is needed. See Secrets and variables.

The OIDC subject format

GitHub presents the sub claim of newly created repositories with immutable ids, repo:org@<org id>/repo@<repo id>:environment:<env>. oidc_subject_uses_repo_ids defaults to true and builds the federated credential subjects in that format. Set it to false only for a customer repository created before GitHub's immutable-reference rollout, which still presents the legacy repo:org/repo:environment:<env> form.

Runners and network

Variable Default Purpose
network_posture "github_hosted" github_hosted or self_hosted_private. self_hosted_private is the production standard
allow_state_access_from_my_ip false On self_hosted_private, also allow the operator's public IP through the state account firewall, for the initial apply. Leave false in steady state
runner_compute_type "azure_container_app" azure_container_app (Container App Jobs, scale to zero) or azure_container_instance (always-on, no cold start, standing cost)
container_app_max_execution_count 1 Maximum concurrent Container App Job executions, one ephemeral runner each. One to ten. Raising it is close to cost-neutral because the job still scales to zero
container_instance_count 1 Always-on container instance runners. One to ten. Each is a direct standing cost
container_app_container_cpu 2 vCPU per runner replica. The AVM default of 1 starves the heavy ALZ policy-library plans
container_app_container_memory "4Gi" Memory per replica. The AVM default of 2Gi OOM-kills provider subprocesses on the heaviest units (amba runs two ALZ provider instances), leaving Terraform hanging on the plugin RPC. Container Apps requires 2Gi per vCPU
container_app_replica_timeout 7200 Seconds a replica may live before the job kills it, which also kills the workflow job. The AVM default of 1800 is too short for the slow ALZ policy-library plans
runner_vnet_address_space "10.0.0.0/24" Address space for the standalone bootstrap network. The runner module carves its runner, registry, and endpoint subnets from this
runner_image_commit null The nrit-alz-bootstrap git ref holding runner-image/github-runner-aca. Set it to build a custom runner image carrying python3, checkov, and conftest. Null keeps the AVM default image. Use a commit SHA so the tag changes when the Dockerfile does

Naming and tagging

Variable Default Purpose
location "westeurope" Azure region for the bootstrap resources
location_short "weu" Short region code used in resource names
tags {} Tags applied to all bootstrap resources

Secrets

Never in a tfvars file. All four are Terraform variables supplied through the environment.

Environment variable Required Purpose
GITHUB_TOKEN or TF_VAR_github_token yes PAT with repo and admin:org scope. Authenticates the github provider
TF_VAR_github_runners_token self_hosted_private PAT that registers the self-hosted runners, with repo or org admin scope. Falls back to github_token
TF_VAR_catalog_app_private_key when catalog_app_client_id is set GitHub App private key, PEM. Set as the CATALOG_APP_PRIVATE_KEY repository secret. Omitting it destroys any existing secret
TF_VAR_runner_image_context_token optional PAT the ACR build task uses to clone the context repo for a custom runner image. Falls back to github_token

GITHUB_TOKEN is not always enough

GITHUB_TOKEN only authenticates the provider. Building a custom runner image needs the token as a real Terraform value, because it is passed into the ACR build task, so export TF_VAR_github_token or TF_VAR_runner_image_context_token for that path.

Variable ownership

Which values the customer repository depends on, and who puts them there. The full list of what each one does is in Secrets and variables.

Name Kind Owner
AZURE_CLIENT_ID Environment variable Bootstrap, one per environment
AZURE_TENANT_ID Repository variable Bootstrap
AZURE_SUBSCRIPTION_ID Repository variable Bootstrap
BACKEND_AZURE_RESOURCE_GROUP_NAME Repository variable Bootstrap
BACKEND_AZURE_STORAGE_ACCOUNT_NAME Repository variable Bootstrap
BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME Repository variable Bootstrap
RUNNER_LABEL Repository variable Bootstrap, derived from network_posture
TFPR_EXTRA_TOOLS Repository variable Bootstrap, derived from network_posture
CATALOG_APP_CLIENT_ID Repository variable Bootstrap, when a client id is supplied
CATALOG_APP_PRIVATE_KEY Secret Bootstrap, when a private key is supplied
INFRACOST_API_KEY Secret You. The bootstrap never sets it
TF_PR_OPS_ALLOW_UNREVIEWED_APPLY Repository variable You. Only for a repository with zero required reviews
TF_PR_OPS_PLAN_ENVIRONMENT, TF_PR_OPS_APPLY_ENVIRONMENT Repository variables You, and only if you rename the environments
Engine version pins in the two caller workflows Repository files You. The bootstrap writes no files
Tree placeholders (businessunit, amba_action_group_email) Repository files You. The bootstrap writes no files
CODEOWNERS, LICENSE Repository files You

The bootstrap does no templating. It creates the repository with the GitHub template feature and never writes into a file, so every file in a generated repository is a verbatim copy of the template at the moment of creation. Values reach the tree at run time through the Action variables above, which root.hcl reads with get_env.

INFRACOST_API_KEY is the one gap that affects a gate. Until it is set, the cost gate notes it is missing and skips. The infracost binary itself is already in the TFPR_EXTRA_TOOLS value the bootstrap writes, so check that variable before editing it. See The gates.

Keep the environment names

The plan and apply environments must exist and keep those names. The OIDC subject embeds the environment claim, so a renamed environment produces a token that matches no federated credential, and every Azure job fails at login.

Outputs

Output Value
state_resource_group_name Resource group holding the state storage account
state_storage_account_name State storage account name, also written as BACKEND_AZURE_STORAGE_ACCOUNT_NAME
state_container_name State container name, tfstate
plan_identity_client_id Client id of the plan identity, also written as AZURE_CLIENT_ID in the plan environment
apply_identity_client_id Client id of the apply identity, also written as AZURE_CLIENT_ID in the apply environment
plan_identity_principal_id Object id of the plan identity, for role assignment auditing
apply_identity_principal_id Object id of the apply identity, for role assignment auditing
repository_full_name The generated customer repository, org/repo
repository_html_url URL of the generated repository
runner_egress_ip Public egress IP of the self-hosted runners, allowlisted on the state firewall. Null on github_hosted