Martin Rylko
  • Services
  • Blog
  • About
  • Contact
  • Get in Touch
Martin Rylko

Senior Cloud Architect & DevOps Engineer. Specializing in Microsoft Azure, IaC, Cloud Security and AI.

Navigation

  • Services
  • Blog
  • About
  • Contact

Collaboration

Looking for an experienced architect for your Azure project? Get in touch.

rylko@cloudmasters.cz

© 2026 Martin Rylko. All rights reserved.

Built in the cloud. Deployed via Azure Static Web Apps.

Home/Blog/Bicep CI/CD: GitHub Actions Pipeline for Azure
All articlesČíst česky

Bicep CI/CD: GitHub Actions Pipeline for Azure

2/15/2025 5 min
#Bicep#Azure#IaC#DevOps#CI/CD#GitHub Actions

Last year, our team hit three environments -- dev, staging, production -- and the "just run az deployment group create from your laptop" workflow fell apart in about two weeks. Staging got a VNet CIDR that overlapped with production because someone deployed from a stale branch. That was the week we built the pipeline.

Effort: 1-2 days for the initial pipeline, half a day per additional environment Cost: $0 for GitHub Actions (2,000 free minutes/month on public repos), ~$0.008/min on Ubuntu runners for private repos Prerequisites: Azure subscription, GitHub repository with Bicep files, Azure AD app registration with federated credentials (or a service principal if you cannot use OIDC)

What Changed in 2025

GitHub Actions for Azure got noticeably better this year. The changes that matter most for Bicep pipelines:

  • OIDC federation is the default path. The azure/login@v2 action dropped the requirement for AZURE_CREDENTIALS JSON. You configure a federated identity credential on an Azure AD app registration and the runner authenticates with a short-lived token. No more client secrets rotting in GitHub Secrets.
  • Reusable workflows hit GA. You can define a deployment workflow once and call it from multiple repositories. For organizations managing 10+ Bicep modules, this eliminates copy-paste drift across repos.
  • az deployment group what-if output can be posted directly as a PR comment with actions/github-script. Reviewers see exactly what will change before approving the merge.
  • Environment protection rules now support required reviewers, wait timers, and branch restrictions -- all native in GitHub, no third-party gates needed.

Why This Matters

Manual Bicep deployments work fine when it is one person, one subscription, one environment. The moment you scale past that, problems stack up:

  • No review before deploy. Someone deploys a breaking change to a Key Vault access policy and the application goes down at 11 PM. There is no PR, no diff, no approval trail.
  • Drift between environments. Dev gets parameters that staging never received. Production runs three versions behind because the last manual deploy was two sprints ago.
  • Secret sprawl. Every developer has an az login session with their personal credentials. One laptop gets compromised and the blast radius is every subscription they can access.

A CI/CD pipeline solves all three: changes are reviewed via PR diff, every environment gets the same template with different parameters, and authentication uses a scoped identity with no persistent secrets.

Implementation: Production Pipeline

Here is the pipeline we run. The workflow file lives at .github/workflows/deploy-infra.yml. It triggers on PR (what-if preview) and on merge to main (actual deployment).

Step 1: Configure OIDC Federation

In Azure AD, create an app registration and add a federated credential for your GitHub repository:

# Create the app registration
az ad app create --display-name "gh-actions-bicep-deployer"
 
# Note the appId from output, then create a service principal
az ad sp create --id <appId>
 
# Add federated credential for main branch deployments
az ad app federated-credential create --id <appId> --parameters '{
  "name": "github-main-deploy",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:contoso/infra-bicep:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'
 
# Assign Contributor role scoped to the target resource group
az role assignment create \
  --assignee <appId> \
  --role "Contributor" \
  --scope "/subscriptions/a1b2c3d4-5678-90ab-cdef-1234567890ab/resourceGroups/rg-platform-prod-westeurope"

Add three secrets in your GitHub repository settings: AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID. No client secret needed -- that is the whole point.

Step 2: The Workflow

# .github/workflows/deploy-infra.yml
name: Deploy Bicep Infrastructure
 
on:
  pull_request:
    paths:
      - 'infra/**'
  push:
    branches:
      - main
    paths:
      - 'infra/**'
 
permissions:
  id-token: write   # Required for OIDC
  contents: read
  pull-requests: write  # Post what-if as PR comment
 
env:
  RESOURCE_GROUP: rg-platform-prod-westeurope
  LOCATION: westeurope
  TEMPLATE: infra/main.bicep
  PARAMETERS: infra/parameters/prod.bicepparam
 
jobs:
  what-if:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    environment: preview
    steps:
      - uses: actions/checkout@v4
 
      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Run what-if
        id: whatif
        run: |
          result=$(az deployment group what-if \
            --resource-group ${{ env.RESOURCE_GROUP }} \
            --template-file ${{ env.TEMPLATE }} \
            --parameters ${{ env.PARAMETERS }} \
            --no-pretty-print 2>&1)
          echo "output<<EOF" >> $GITHUB_OUTPUT
          echo "$result" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
 
      - name: Post what-if to PR
        uses: actions/github-script@v7
        with:
          script: |
            const output = `### Bicep What-If Preview
            \`\`\`
            ${{ steps.whatif.outputs.output }}
            \`\`\`
            *Triggered by @${{ github.actor }}*`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });
 
  deploy-staging:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
 
      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Deploy to staging
        run: |
          az deployment group create \
            --resource-group rg-platform-staging-westeurope \
            --template-file ${{ env.TEMPLATE }} \
            --parameters infra/parameters/staging.bicepparam \
            --name "staging-$(date +%Y%m%d-%H%M%S)"
 
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://portal.azure.com
    steps:
      - uses: actions/checkout@v4
 
      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Deploy to production
        run: |
          az deployment group create \
            --resource-group ${{ env.RESOURCE_GROUP }} \
            --template-file ${{ env.TEMPLATE }} \
            --parameters ${{ env.PARAMETERS }} \
            --name "prod-$(date +%Y%m%d-%H%M%S)"

Step 3: Environment Protection Rules

In GitHub repository settings under Environments, configure production with:

  • Required reviewers: Add at least one team lead. No one deploys to production without a second pair of eyes.
  • Wait timer: 5 minutes after approval. Enough time for someone to say "wait, actually no."
  • Branch restriction: Only main. Feature branches cannot bypass staging.

Here is what the docs do not tell you about OIDC setup: the federated credential subject field is case-sensitive and must match exactly. If your repo is Contoso/Infra-Bicep but you configured contoso/infra-bicep, the token exchange silently fails with a generic AADSTS700024 error. We spent two hours on that one.

Real-World Results

After switching from manual az deployment calls to this pipeline, here is what actually changed.

The first deployment failure we caught was a missing role assignment. The OIDC service principal had Contributor on the resource group but the Bicep template was creating a Key Vault with an access policy referencing a managed identity in a different resource group. The error in the GitHub Actions log:

ERROR: {"status":"Failed","error":{"code":"DeploymentFailed","target":
"/subscriptions/a1b2c3d4-.../resourceGroups/rg-platform-prod-westeurope/
providers/Microsoft.Resources/deployments/prod-20250210-143022",
"message":"At least one resource deployment operation failed. Please list
deployment operations for details.","details":[{"code":"Forbidden",
"message":"The client 'f8a3b1c2-...' with object id 'f8a3b1c2-...' does
not have authorization to perform action
'Microsoft.KeyVault/vaults/accessPolicies/write' over scope
'/subscriptions/a1b2c3d4-.../resourceGroups/rg-security-prod-westeurope/
providers/Microsoft.KeyVault/vaults/kv-platform-prod'."}]}}

The fix was adding a second role assignment scoped to the security resource group. In the old workflow, someone would have run az login with their Owner-level personal account and never noticed the permission gap.

Pipeline execution times (averaged over 30 runs):

StageDuration
What-if preview (PR)1m 42s
Deploy staging3m 15s
Deploy production (after approval)3m 22s
Total (staging + prod)~7 min

Compare that to the old manual process: SSH into a jump box, run az login, navigate to the right directory, remember which parameter file to use, run the deployment, wait, check the portal. Conservatively 20 minutes per environment, plus the cognitive overhead of doing it right.

Key Takeaways

  • OIDC first, service principal secrets never. Federated credentials eliminate secret rotation entirely. The subject claim must match your repo name exactly -- including capitalization.
  • What-if on every PR. Reviewers should see infrastructure diffs the same way they see code diffs. Post the output as a PR comment so it is impossible to miss.
  • Environment gates are not optional. A 5-minute wait timer on production has saved us from at least two "oh wait" moments.
  • Name your deployments. The --name "prod-$(date +%Y%m%d-%H%M%S)" pattern makes it trivial to correlate a GitHub Actions run with an Azure deployment in the portal.
  • Start with one resource group, expand later. Get the OIDC plumbing working for a single RG before adding subscription-level deployments or management group scopes.

If you are already working with Terraform and wondering how it compares, we covered the foundational patterns in our Terraform Azure best practices guide. The choice between Bicep and Terraform often comes down to whether your team is Azure-only or multi-cloud -- the CI/CD patterns are remarkably similar either way.

Looking for help setting up infrastructure deployment pipelines for your Azure environment? Check out our infrastructure and DevOps consulting services -- we have built these pipelines for teams ranging from 3-person startups to enterprise platform teams.

Tags:#Bicep#Azure#IaC#DevOps#CI/CD#GitHub Actions
LinkedInX / Twitter

About the author

Martin Rylko

Martin Rylko

Senior Cloud Architect & DevOps Engineer

14+ years in IT – from on-premises datacenters and Hyper-V clustering to cloud infrastructure on Microsoft Azure. I specialize in Landing Zones, IaC automation, Kubernetes and security compliance.

Email LinkedInFull profile

Frequently Asked Questions

Should I use OIDC federation or a service principal secret for GitHub Actions to Azure?▾
Always use OIDC (OpenID Connect) federation. It eliminates stored secrets entirely -- GitHub Actions gets a short-lived token from Entra ID for each workflow run. Service principal secrets expire, can be leaked, and need manual rotation. OIDC federation is configured once via az ad app federated-credential create and works with environment-scoped deployments.
What deployment scope should I use for Bicep in CI/CD -- subscription or resource group?▾
It depends on what you are deploying. Resource group scope (az deployment group create) works for application resources within a single RG. Subscription scope (az deployment sub create) is needed for resource group creation, policy assignments, or role assignments. Landing Zone deployments typically use subscription or management group scope.
How do I handle rollback when a Bicep deployment fails in the pipeline?▾
Bicep deployments are idempotent but do not auto-rollback. Use the what-if preview step as a gate before production deployment. For critical rollbacks, keep the last known-good Bicep parameter files versioned and redeploy from the previous commit. Azure also supports deployment stacks (GA since 2024) which can delete resources that are removed from the template.
Can I run Bicep what-if in a pull request check before merging?▾
Yes, and you should. Add a PR-triggered workflow that runs az deployment group what-if and posts the output as a PR comment. This gives reviewers a clear diff of infrastructure changes before merge. Use the --no-pretty-print flag to get machine-parseable JSON output for custom formatting.

You might also like

Terraform Azure Modules: Private Registry and Testing

Build reusable Terraform modules for Azure with private registry publishing, automated testing with Terratest, and versioned module consumption in production.

Read

Terraform Azure Best Practices: Modules & CI/CD

Terraform Azure best practices for production projects. Covers remote state locking, module structure, drift detection, naming conventions, and testing.

Read

Azure Landing Zone with Bicep: Enterprise Setup

Deploy an enterprise-ready Azure Landing Zone using Bicep modules. Covers hub-spoke networking, policy governance, and CI/CD pipeline integration.

Read