First plan to first apply¶
The end-to-end walkthrough for onboarding your first customer. It starts at an empty GitHub organisation and a customer Azure tenant, and ends with an applied landing-zone foundation.
Six steps. Budget half a day of hands-on work, plus sixty to ninety minutes of waiting on the first apply.
| Step | What it does | Hands-on time |
|---|---|---|
| 1 | Run the bootstrap | 30 to 45 min |
| 2 | Fill in the customer values | 20 min |
| 3 | Repo settings the bootstrap does not cover | 10 min |
| 4 | First pull request and plan | 10 min |
| 5 | First apply | 5 min, then 60 to 90 min waiting |
| 6 | What next | - |
Before you start, work through the prerequisites. You need
Owner at the scope the deploy identities operate at, a management subscription, a
GitHub organisation for the customer, and a GitHub token with repo and
admin:org scope.
1. Run the bootstrap¶
The bootstrap is Terraform in nrit-solutions/nrit-alz-bootstrap. It runs once
per customer, against the customer tenant. Full detail is on
the bootstrap page; this is the shape of the run.
# Copy customers/_example.tfvars to customers/<customer>.tfvars and fill it in.
az login --tenant <customer-tenant-id>
az account set --subscription <bootstrap-subscription-id>
export GITHUB_TOKEN=<pat> # repo + admin:org, never in tfvars
export TF_VAR_github_runners_token=<pat> # self_hosted_private only
export TF_VAR_catalog_app_private_key="$(cat /path/to/catalog-reader-app.pem)"
scripts/create-backend.sh -f customers/<customer>.tfvars
terraform init \
-backend-config="resource_group_name=rg-cicd-bootstrap-<loc>" \
-backend-config="storage_account_name=<printed by the script>" \
-backend-config="container_name=tfstate" \
-backend-config="key=bootstrap.tfstate"
terraform plan -var-file=customers/<customer>.tfvars
terraform apply -var-file=customers/<customer>.tfvars
After the apply, the customer has:
- the customer repository, generated from
nrit-alz-customer-template, so it starts with the wholelive/tree and both caller workflows, - the deploy identities, plan (Reader) and apply (Owner), with GitHub Actions federated credentials, so no Azure secrets are stored,
- the
planandapplyenvironments, each carrying its ownAZURE_CLIENT_ID, - the state storage account and
tfstatecontainer, Entra ID auth only, - the
require-approved-pr-to-mainruleset, which is the merge gate, - the self-hosted runner, when
network_posture = self_hosted_private.
The backend firewall allows one IP
scripts/create-backend.sh resets the allowlist to your current public IP.
If your IP changes, re-run it before any terraform command. On the very
first run the operator role assignment can take a minute to propagate before
terraform init succeeds.
2. Clone the customer repository and set the customer values¶
Five files hold values that are specific to this customer.
live/tenant.hcl¶
tenant_root_id is the management group the ALZ hierarchy is created under. It
defaults to local.tenant_id, which is the tenant root group. That is what most
customers want.
Set an intermediate management group before the first apply
If the customer already has an intermediate management group and wants the
hierarchy under it, set tenant_root_id to that group's plain name now. The
group must already exist; this repository does not create it.
Changing it after the first apply moves the whole hierarchy to a new parent. Policy assignment scopes and role assignments follow the move, and subscriptions can be left unplaced while it runs. That is a migration, not an edit.
live/_foundation/region.hcl¶
Change location and location_short only if the customer specifies another
region. Set them here and nowhere else: root.hcl generates a context.tf into
each unit from this file. environment becomes the env tag. The tag policy
allows prod, staging, and dev only, so a foundation that is not the
customer's production estate must use one of those.
live/_foundation/management-resources/main.tf¶
Replace the businessunit tag, currently changeme, with the customer's short
name.
live/_foundation/amba/main.tf¶
Replace the amba_action_group_email placeholder with a real monitored inbox.
Azure Monitor Baseline Alerts is mandatory in the NRIT baseline and every AMBA
alert routes to this address.
.github/CODEOWNERS and LICENSE¶
.github/CODEOWNERS assigns every path to @nrit-solutions/platform-engineering.
That team does not exist in the customer's organisation, so GitHub reports the
file as invalid and the owners never apply. Replace the team with one that exists
there, or delete the file.
LICENSE ships as a placeholder that points at the partnership agreement.
Replace it with the agreed licence text, or confirm with the agreement owner that
the placeholder is acceptable for this customer.
Optional: a connectivity subscription¶
live/_foundation/landing-zones/main.tf has connectivity_subscription_id,
which defaults to an empty string and omits the connectivity entry from
subscription_placement. Set it when the customer has a connectivity
subscription. Placement is not permanent: set the id later and re-apply to move
the subscription under the Connectivity MG.
Some values are never edited in files
The tenant id, subscription id, and the three backend names arrive as
repository variables the bootstrap wrote (AZURE_TENANT_ID,
AZURE_SUBSCRIPTION_ID, BACKEND_AZURE_*). The placeholders in
live/tenant.hcl and live/_foundation/subscription.hcl are only used for
local runs. All units share the one state account; each folder's
subscription.hcl is the deploy target, not the state location. See
secrets and variables.
The engine pins in .github/workflows/terraform-pr-ops.yml and
.github/workflows/drift.yml are already at v1.5.0 on both the uses: ref and
engine_ref. Leave them alone for the first onboarding.
3. Repo settings the bootstrap does not cover¶
Three things to check before the first pull request.
Org Actions access to the private engine. The caller workflows use the PR-ops
engine (nrit-tf-pr-ops) reusable workflows. Without this, the uses: reference
fails to resolve and the first pull request never starts.
gh api -X PUT repos/nrit-solutions/nrit-tf-pr-ops/actions/permissions/access \
-f access_level=organization
This is an org-wide setting on the NRIT source repository. Set it once and it holds for every later customer.
The cost gate key. INFRACOST_API_KEY is the only thing the bootstrap does
not set. Add it as a repository secret.
The bootstrap already sets TFPR_EXTRA_TOOLS and its value includes infracost.
Check the variable rather than editing it blindly.
The required status check. Confirm tf-pr-ops / merge-gate is required on
the main branch.
The require-approved-pr-to-main ruleset takes the check context from the
bootstrap's required_status_checks variable, which is empty by default. Set it
to ["tf-pr-ops / merge-gate"] in the customer tfvars and re-run the bootstrap,
or add the check by hand.
Check the App credentials too
The engine checkout needs CATALOG_APP_CLIENT_ID (variable) and
CATALOG_APP_PRIVATE_KEY (secret). The bootstrap sets both when a client id
is supplied. The names are historical: the App token authenticates the
private engine checkout, not a module catalog.
4. Open the first pull request¶
Opening the PR runs /plan automatically. The engine plans every impacted unit,
one job per unit, in parallel. Because you touched live/tenant.hcl, which every
unit reads, expect the full foundation fan-out.
What to look for:
- A plan comment per unit. Each run posts a fresh comment led by a validation
check, then the plan output, a change summary, the
/applyinstructions, and the gate findings. - The gate panel. It is collapsed with a check mark when policy, security, and cost are all clean, and auto-expanded with a warning when a gate reports something.
- The
tf-pr-ops / merge-gatestatus. It is red until the PR is applied. That is the point: nothing merges with unapplied changes.
Read the plans before approving. The first foundation plan creates the whole management group hierarchy, the policy assignments, the central Log Analytics workspace, and the AMBA alerts. See plan and apply for how the impacted set is worked out.
5. First apply¶
Get the review. /apply respects the repository's review decision, and the
bootstrap defaults the required approving review count to 1.
Then comment on the PR:
The foundation applies in dependency order automatically:
management-resources, then landing-zones, then amba. One unit at a time.
The engine reacts to your comment so you can follow progress without opening the
run.
Expect sixty to ninety minutes
The first foundation apply is slow because of policy propagation. This is normal and there is nothing to fix. Later applies on the same foundation are much faster.
Success looks like this:
- every unit reports Applied in its comment,
- the
tf-pr-ops / merge-gatecheck turns green, - the PR can merge.
Merge the PR. Nothing applies on merge; the apply already happened from the comment.
6. What next¶
- Growing the tree: onboard a connectivity hub and the customer's first landing zones.
- Plan and apply: the day-to-day flow, the comment commands, and how the impacted set is calculated.
- Policy: what the baseline enforces and how to change it.