Spaces:
Running on CPU Upgrade
sidebar_position: 5
Azure Subscriptions Setup
How to prepare Azure so the infra/azure Terraform can create and name subscriptions. You do this once. After it's done, subscriptions are managed by editing subscriptions.auto.tfvars and running make azure-plan / make azure-apply (or the CI workflow).
What you're creating
- An app registration + service principal β the non-human identity Terraform authenticates as.
- Federated credentials (OIDC) on it β so GitHub Actions can authenticate without a stored secret.
- A billing-scope role assignment β the permission that actually lets it create subscriptions.
Recommended name
sp-opennav-tf-subscriptions
Rationale (CAF convention): sp- = service principal, opennav = org, tf = managed by Terraform, subscriptions = its job (Microsoft calls this pattern subscription vending). Use the same string for the app registration display name and the SP.
Prerequisites
- An EA, MCA, or MPA billing account. Pay-As-You-Go cannot create subscriptions via API/Terraform β verify with
az billing account list -o table(see Azure Subscriptions Setup billing scope). - Your own account must be Owner / Global Admin (to create the app registration) and have rights on the billing scope (to delegate subscription-creation to the SP).
- Azure CLI + Terraform installed. Get both via the installer's opt-in infra flag:
INSTALL_INFRA_TOOLS=1 ./install.sh # Linux/macOSIt prefers the OS package manager (apt/dnf/brew/winget/choco) and falls back to a rootless install into$env:INSTALL_INFRA_TOOLS = "1"; .\install.ps1 # Windows.venvwhen sudo isn't available. Thenaz login.
Shortcut: run the bootstrap script
Steps 1β2 (and printing every value you need) are automated. After az login:
./infra/azure/setup-azure.sh # creates the SP + OIDC creds, prints values
./infra/azure/setup-azure.sh --set-github # also sets the GitHub repo Variables
It prints ARM_CLIENT_ID, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID ready to paste into
.env β the only value you must still look up yourself is the billing scope (Step 3).
The manual steps below explain what it does.
Step 1 β Create the app registration + service principal
# Create the app registration and capture its appId (client id)
APP_ID=$(az ad app create \
--display-name "sp-opennav-tf-subscriptions" \
--query appId -o tsv)
# Create the service principal for that app
az ad sp create --id "$APP_ID"
# Note these for later β they are NOT secrets:
echo "ARM_CLIENT_ID = $APP_ID"
echo "ARM_TENANT_ID = $(az account show --query tenantId -o tsv)"
We deliberately do not create a client secret here β CI uses OIDC (Step 2), and local runs can use your own az login (Step 4).
Step 2 β Add federated credentials (OIDC) for GitHub Actions
This lets the CI workflow exchange a short-lived GitHub token for an Azure token β no secret is stored anywhere.
# Trust pushes to main
az ad app federated-credential create --id "$APP_ID" --parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:getcommunityone/open-navigator:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
# Trust pull requests (so PR plans authenticate too)
az ad app federated-credential create --id "$APP_ID" --parameters '{
"name": "github-pr",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:getcommunityone/open-navigator:pull_request",
"audiences": ["api://AzureADTokenExchange"]
}'
These subjects match the triggers in .github/workflows/azure-subscriptions.yml (push to main and pull_request).
Step 3 β Grant the SP rights on the billing scope (the key step)
Subscription creation is a billing permission, not normal subscription RBAC β az role assignment does not do this. The exact role/path depends on your agreement:
MCA (Microsoft Customer Agreement)
Assign the Azure subscription creator role on the invoice section:
- Portal: Cost Management + Billing β Billing scopes β your account β Billing profiles β Invoice sections β pick the section β Access control (IAM) β Add β role Azure subscription creator β select
sp-opennav-tf-subscriptions. - The invoice section's resource ID is your
TF_VAR_billing_scope_id.
EA (Enterprise Agreement)
Add the SP as owner of the enrollment account (a.k.a. SubscriptionCreator):
- Portal: Cost Management + Billing β Billing scopes β enrollment account β assign the SP, or via the EA portal (
ea.azure.com) β Enrollment account β Add the service principal. - Scope shape:
/providers/Microsoft.Billing/billingAccounts/<enrollmentNumber>/enrollmentAccounts/<id>.
MPA (Partner)
Assign on the customer scope: /providers/Microsoft.Billing/billingAccounts/<ba>/customers/<id>.
See Azure Subscriptions Setup billing scope for the
az billing β¦commands that print each scope's full resource ID β that string is what goes inTF_VAR_billing_scope_id.
Step 4 β (Optional) Management Group placement
If any subscription in subscriptions.auto.tfvars sets management_group_id, also grant the SP Management Group Contributor on that MG (this one is normal RBAC):
az role assignment create \
--assignee "$APP_ID" \
--role "Management Group Contributor" \
--scope "/providers/Microsoft.Management/managementGroups/<mg-id>"
Step 5 β Wire the values in
CI (GitHub Actions)
gh variable set AZURE_CLIENT_ID --body "$APP_ID"
gh variable set AZURE_TENANT_ID --body "$(az account show --query tenantId -o tsv)"
gh variable set AZURE_SUBSCRIPTION_ID --body "<any-existing-sub-id-to-auth-against>"
gh secret set AZURE_BILLING_SCOPE_ID --body "<scope-id-from-step-3>"
Local (infra/azure/.env, gitignored)
Two options:
- Simplest β use your own
az login. The azurerm provider falls back to Azure CLI auth, so locally you only need the billing scope:(Your user account must itself have the Step 3 permission for this to work.)export TF_VAR_billing_scope_id="<scope-id-from-step-3>" - Use the SP via OIDC/secret as documented in
.env.exampleif you want local runs to use the same identity as CI.
Verify
cd infra/azure
set -a && source .env && set +a
make azure-plan # from repo root, or: terraform plan
A clean plan listing the opennav-* subscriptions means everything is wired. Then make azure-apply (or merge the PR β CI applies on main).
Reference
- Azure Naming Conventions β the CAF naming/tagging standard for resources created inside these subscriptions.
- Module + naming convention:
infra/azure/README.md - CI workflow:
.github/workflows/azure-subscriptions.yml - Microsoft docs: Programmatically create Azure subscriptions and Workload identity federation.