CI/CD pipeline
This repo ships 8 GitHub Actions workflows under .github/workflows/ β a plan + apply pair for each of the four root modules (Terraform foundation, Bicep foundation, Terraform management groups, Bicep management groups).
| Workflow | Trigger | Purpose |
|---|---|---|
terraform-plan.yml | PR to main touching infra/terraform/foundation/** | fmt -check, validate, tfsec, matrix-plan over all 4 scenarios, uploads each plan as an artifact. |
terraform-apply.yml | Manual workflow_dispatch (pick scenario) | init, select workspace, apply -auto-approve. Gated by the prod environment. |
bicep-plan.yml | PR to main touching infra/bicep/foundation/** | bicep build, az deployment sub what-if for the chosen scenario, posts results as a PR comment. |
bicep-apply.yml | Manual workflow_dispatch or push to main | az deployment sub create for the chosen scenario. Gated by the apply environment. |
terraform-mg-plan.yml | PR to main touching infra/terraform/management-groups/** | validate + plan for the MG hierarchy at tenant scope. |
terraform-mg-apply.yml | Manual workflow_dispatch | apply for the MG hierarchy. Gated by the prod-tenant environment. |
bicep-mg-plan.yml | PR to main touching infra/bicep/management-groups/** | bicep build + az deployment tenant what-if. |
bicep-mg-apply.yml | Manual workflow_dispatch | az deployment tenant create. Gated by the prod-tenant environment. |
Step 1 β Bootstrap state (Terraform only)
Bicep stores deployment history in Azure automatically; Terraform needs a remote backend. Skip this step if youβre only using Bicep.
# From the repo root, against the target subscription:az loginaz account set --subscription <subscription-id>./scripts/bootstrap-state.shThis creates rg-tfstate-<prefix>-<region> containing a Storage Account and a tfstate container, then prints the values you need for the repo variables in step 4. Run it once per customer subscription.
Step 2 β Create the OIDC identity in Entra
No client secrets β GitHub Actions exchanges a workflow-issued OIDC token for an Azure access token. Run this once per repo:
APP_NAME="azure-launchpad-gha"SUB_ID=<subscription-id>TENANT_ID=<tenant-id>REPO=<owner>/azure-launchpad # e.g. travishankins/azure-launchpad
# 2a. Create app registration + service principalAPP_ID=$(az ad app create --display-name $APP_NAME --query appId -o tsv)az ad sp create --id $APP_ID
# 2b. Grant Contributor on the subscription (required for foundation deploy)az role assignment create \ --assignee $APP_ID \ --role Contributor \ --scope /subscriptions/$SUB_ID
# 2c. (Optional) If you'll deploy the management-groups module,# grant Management Group Contributor at Tenant Root:TENANT_ROOT_MG=/providers/Microsoft.Management/managementGroups/$TENANT_IDaz role assignment create \ --assignee $APP_ID \ --role "Management Group Contributor" \ --scope $TENANT_ROOT_MGStep 3 β Add federated credentials
Add one credential per GitHub trigger you want to use. The plan workflows run on pull requests; the apply workflows run inside protected GitHub environments.
# 3a. Plan (pull requests) β used by all *-plan workflowsaz ad app federated-credential create --id $APP_ID --parameters '{ "name":"gha-pr", "issuer":"https://token.actions.githubusercontent.com", "subject":"repo:'$REPO':pull_request", "audiences":["api://AzureADTokenExchange"]}'
# 3b. Foundation apply β Terraform uses environment "prod"az ad app federated-credential create --id $APP_ID --parameters '{ "name":"gha-prod", "issuer":"https://token.actions.githubusercontent.com", "subject":"repo:'$REPO':environment:prod", "audiences":["api://AzureADTokenExchange"]}'
# 3c. Foundation apply β Bicep uses environment "apply"az ad app federated-credential create --id $APP_ID --parameters '{ "name":"gha-apply", "issuer":"https://token.actions.githubusercontent.com", "subject":"repo:'$REPO':environment:apply", "audiences":["api://AzureADTokenExchange"]}'
# 3d. Management Groups apply (TF + Bicep) β environment "prod-tenant"az ad app federated-credential create --id $APP_ID --parameters '{ "name":"gha-prod-tenant", "issuer":"https://token.actions.githubusercontent.com", "subject":"repo:'$REPO':environment:prod-tenant", "audiences":["api://AzureADTokenExchange"]}'Step 4 β Configure repo variables
Settings β Secrets and variables β Actions β Variables (these are non-sensitive identifiers, so use Variables, not Secrets):
| Variable | Value | Required for |
|---|---|---|
AZURE_CLIENT_ID | $APP_ID from step 2a | All workflows |
AZURE_TENANT_ID | Your Entra tenant ID | All workflows |
AZURE_SUBSCRIPTION_ID | Target subscription | All workflows |
AZURE_DEFAULT_LOCATION | e.g. westcentralus | Bicep (subscription-scoped deployments) |
TFSTATE_RG | From bootstrap-state.sh output | Terraform foundation |
TFSTATE_SA | From bootstrap-state.sh output | Terraform foundation |
TFSTATE_CONTAINER | tfstate | Terraform foundation |
TFSTATE_MG_RG | From a separate bootstrap-state.sh run, if used | Terraform management groups (optional) |
TFSTATE_MG_SA | β | Terraform management groups (optional) |
TFSTATE_MG_CONTAINER | tfstate-mg | Terraform management groups (optional) |
Step 5 β Create protected environments
Settings β Environments β New environment, create these and enable Required reviewers on each:
| Environment | Used by |
|---|---|
prod | terraform-apply.yml |
apply | bicep-apply.yml |
prod-tenant | terraform-mg-apply.yml, bicep-mg-apply.yml |
This forces a human approval click before any apply runs against your subscription or tenant.
Step 6 β Deploy
Foundation (Terraform)
- Open a PR that adds or edits a tfvars under
infra/terraform/foundation/scenarios/(or run the wizard and commit its output). terraform-plan.ymlruns automatically; review the plan artifact attached to the PR check.- Merge the PR.
- Actions β terraform-apply β Run workflow, pick the scenario, click Run.
- A reviewer approves the
prodenvironment gate. - The job runs
terraform apply. ~10β25 min depending on scenario.
Foundation (Bicep)
- Open a PR that adds or edits a
.bicepparamunderinfra/bicep/foundation/scenarios/. bicep-plan.ymlrunswhat-ifand posts the diff as a PR comment.- Merge the PR.
- Actions β bicep-apply β Run workflow, pick the scenario, click Run (or just push to
mainβ it runs automatically with thebaselinescenario by default). - A reviewer approves the
applyenvironment gate. - The job runs
az deployment sub create. ~10β25 min depending on scenario.
Management Groups (either IaC)
Heads up β runs at tenant root, not in your subscription. The OIDC principal needs
Management Group Contributoron the Tenant Root Group (step 2c).
- Open a PR touching
infra/terraform/management-groups/**orinfra/bicep/management-groups/**. - The matching
*-mg-planworkflow runs. - Merge the PR.
- Actions β terraform-mg-apply (or bicep-mg-apply) β Run workflow β Run.
- A reviewer approves the
prod-tenantenvironment gate. - The job creates / updates the management-group hierarchy and any opt-in policy assignments.
Local pre-commit equivalence
The plan workflows just wrap commands you can also run locally.
Terraform:
cd infra/terraform/foundationterraform fmt -recursive -checkterraform init -backend=falseterraform validateterraform testtfsec .Bicep:
cd infra/bicep/foundationaz bicep build --file main.bicepaz deployment sub what-if \ --location westcentralus \ --parameters scenarios/baseline.bicepparam