This repository contains a production-grade CI/CD setup for deploying Azure infrastructure using Terraform, with GitHub Actions and OpenID Connect (OIDC) for secure authentication.
- Integration: GitHub Actions
- Cloud provider: Azure
- IaC: Terraform
- Terraform version: 1.14.7
- Default branch:
main
.github/workflows/deploy.yml– CI/CD pipelineinfra/main.tf– Terraform configurationREADME.md– Documentation
- Azure Subscription with permission to create:
- Resource groups
- Storage accounts
- GitHub Repository with:
- Default branch:
main - Admin access to configure secrets and environments
- Default branch:
- Tools (for local development)
- Terraform 1.14.7
- Azure CLI (optional but recommended)
The pipeline uses GitHub OIDC to authenticate to Azure without storing long-lived credentials.
Use Azure CLI (replace placeholders as needed):
SUBSCRIPTION_ID="<your-subscription-id>"
AZURE_AD_APP_NAME="github-oidc-terraform-sp"
az account set --subscription "$SUBSCRIPTION_ID"
# Create the app registration
APP_ID=$(az ad app create \
--display-name "$AZURE_AD_APP_NAME" \
--query appId -o tsv)
# Create a service principal for the app
az ad sp create --id "$APP_ID"
# Assign a role (example: Contributor) at subscription scope
az role assignment create \
--assignee "$APP_ID" \
--role "Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID"
echo "APP_ID: $APP_ID"Note the following values:
APP_ID→ used asAZURE_CLIENT_ID- Your tenant ID →
AZURE_TENANT_ID(can be obtained viaaz account show --query tenantId -o tsv) SUBSCRIPTION_ID→AZURE_SUBSCRIPTION_ID
- Go to Azure Portal → Microsoft Entra ID → App registrations.
- Open the app you created (
github-oidc-terraform-sp). - Go to Certificates & secrets → Federated credentials → Add credential. (with enviernment - production)
- Configure:
- Federated credential scenario:
GitHub Actions deploying Azure resources(if available) orOther issuer. - Issuer:
https://token.actions.githubusercontent.com - Subject identifier (for repository-level access on
mainbranch):- Format:
repo:<org-or-user>/<repo-name>:ref:refs/heads/main
- Format:
- Audience:
api://AzureADTokenExchange(default for Azure).
- Federated credential scenario:
This allows workflows in the specified repository and branch to obtain tokens for the app using OIDC.
In your GitHub repository:
- Go to Settings → Secrets and variables → Actions → Secrets.
- Add these repository secrets:
AZURE_CLIENT_ID–APP_IDof your Azure AD applicationAZURE_TENANT_ID– Tenant ID of your Azure ADAZURE_SUBSCRIPTION_ID– Azure subscription ID
The workflow uses these secrets with azure/login@v2 and OIDC.
infra/main.tf contains a minimal Azure setup:
- Pins Terraform version to
1.14.7:terraform { required_version = "= 1.14.7" }
- Configures the
azurermprovider. - Creates:
- A resource group
- A storage account
- Exposes outputs:
resource_group_namestorage_account_id
You can customize:
var.location(default:eastus)var.resource_group_namevar.storage_account_name(must be globally unique and lowercase, 3–24 chars)
Workflow file: .github/workflows/deploy.yml
pushtomainaffecting:.github/workflows/deploy.ymlinfra/**README.md
pull_requesttargetingmainaffectinginfra/**workflow_dispatch(manual run)
-
validate
- Checks out code.
- Installs Terraform
1.14.7. - Runs
terraform initandterraform validateininfra/.
-
plan (depends on
validate)- Logs into Azure via OIDC using
azure/login@v2. - Runs
terraform init. - Runs
terraform plan -out=tfplan.out. - Shows the plan.
- Calculates whether there are changes (
plan_has_changesoutput). - Uploads
tfplan.outas an artifact.
- Logs into Azure via OIDC using
-
apply (depends on
plan)- Runs only when:
- Branch is
main(github.ref == 'refs/heads/main'), and - The plan detected changes (
plan_has_changes == 'true').
- Branch is
- Uses
productionenvironment (you can add manual approval to this environment in GitHub settings if desired). - Logs into Azure via OIDC.
- Downloads the saved
tfplan.outartifact. - Re-runs
terraform init. - Executes
terraform apply tfplan.out.
- Runs only when:
This ensures that apply always corresponds exactly to the previously generated plan.
- Complete Azure OIDC setup (service principal + federated credentials).
- Add GitHub secrets:
AZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_SUBSCRIPTION_ID. - Push the repository contents to the
mainbranch.
Install Terraform 1.14.7 and Azure CLI.
cd infra
terraform init
terraform plan
terraform applyTo authenticate locally with Azure CLI:
az login
az account set --subscription "<your-subscription-id>"-
Pull Requests to
main- Run
validateandplanjobs. - You can inspect the plan output in the Actions logs.
applydoes not run for PRs.
- Run
-
Pushes to
main- Run
validate→plan→apply(if there are changes). - Plan is stored as an artifact and then applied.
- Run
-
Manual Run
- Go to Actions → CI/CD - Terraform Azure → Run workflow.
- Select the
mainbranch and run.
-
Change Terraform resources
- Edit
infra/main.tfto add or modify resources.
- Edit
-
Variables and remote state
- Introduce
terraform.tfvarsor environment variables for configuration. - For production use, consider configuring a remote backend (e.g., Azure Storage) in the
terraformblock.
- Introduce
-
Environment approvals
- In GitHub: Settings → Environments → New environment → production.
- Add required reviewers to gate the
applystep.
-
OIDC login failures
- Verify federated credential subject matches exactly:
repo:<org>/<repo>:ref:refs/heads/main. - Ensure
AZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_SUBSCRIPTION_IDare correct.
- Verify federated credential subject matches exactly:
-
Terraform version mismatch
- Ensure local Terraform is
1.14.7. - Confirm
TF_VERSIONin the workflow andrequired_versioninmain.tfare= 1.14.7.
- Ensure local Terraform is
-
Storage account name errors
- Must be globally unique, 3–24 characters, lowercase letters and numbers only.
- Adjust
var.storage_account_nameininfra/main.tfor viaterraform.tfvars.
This setup provides a secure, reproducible CI/CD pipeline for managing Azure infrastructure with Terraform using GitHub Actions and OIDC-based authentication.