open-navigator / web_docs /docs /deployment /azure-subscriptions-setup.md
jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
|
Raw
History Blame Contribute Delete
7.45 kB
metadata
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

  1. An app registration + service principal β€” the non-human identity Terraform authenticates as.
  2. Federated credentials (OIDC) on it β€” so GitHub Actions can authenticate without a stored secret.
  3. 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/macOS
    
    $env:INSTALL_INFRA_TOOLS = "1"; .\install.ps1   # Windows
    
    It prefers the OS package manager (apt/dnf/brew/winget/choco) and falls back to a rootless install into .venv when sudo isn't available. Then az 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 in TF_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:
    export TF_VAR_billing_scope_id="<scope-id-from-step-3>"
    
    (Your user account must itself have the Step 3 permission for this to work.)
  • Use the SP via OIDC/secret as documented in .env.example if 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.