Introduction

ARM templates have long been the go-to for declarative Azure infrastructure, but their verbose JSON syntax makes them painful to read and maintain. Bicep is Microsoft’s answer — a cleaner domain-specific language that compiles down to ARM JSON. Pair it with GitHub Actions and you have a fully automated deployment pipeline that runs every time you push to your repository.

This article walks through setting up the full pipeline: writing Bicep templates, creating a service principal, configuring GitHub secrets, and building the workflow that deploys your infrastructure on every push.

Why Bicep over ARM Templates?

Bicep gives you everything ARM templates offer, but with a much cleaner syntax. A storage account in ARM JSON runs to 30+ lines; in Bicep it’s closer to 10. You also get first-class VS Code support with IntelliSense, type checking, and inline documentation. The tooling is mature and it’s the direction Microsoft is actively investing in.

Prerequisites

Writing Your First Bicep Template

Let’s deploy a resource group with a storage account. Create a file called main.bicep:

param location string = resourceGroup().location
param storageAccountName string = 'jocha${uniqueString(resourceGroup().id)}'
param sku string = 'Standard_LRS'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: sku
  }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
    supportsHttpsTrafficOnly: true
  }
}

output storageAccountId string = storageAccount.id

You can validate and preview the deployment locally before pushing:

# Login and set subscription
az login
az account set --subscription "your-subscription-id"

# Validate the template
az deployment group validate   --resource-group your-rg   --template-file main.bicep

# Preview changes without deploying
az deployment group what-if   --resource-group your-rg   --template-file main.bicep

Creating a Service Principal for GitHub Actions

GitHub Actions needs credentials to deploy into your Azure subscription. Create a service principal with Contributor rights scoped to your resource group:

az ad sp create-for-rbac   --name "github-actions-deployer"   --role Contributor   --scopes /subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_RG   --sdk-auth

This outputs a JSON block. Copy the entire output — you’ll need it in the next step.

Adding Secrets to GitHub

In your GitHub repository, go to Settings → Secrets and variables → Actions and add the following secrets:

  • AZURE_CREDENTIALS — the full JSON output from the service principal command above
  • AZURE_SUBSCRIPTION_ID — your Azure subscription ID
  • AZURE_RG — the target resource group name

Building the GitHub Actions Workflow

Create the file .github/workflows/deploy-infrastructure.yml in your repository:

name: Deploy Azure Infrastructure

on:
  push:
    branches: [ main ]
    paths:
      - 'infrastructure/**'
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Login to Azure
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Run what-if analysis
        uses: azure/arm-deploy@v2
        with:
          subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          resourceGroupName: ${{ secrets.AZURE_RG }}
          template: ./infrastructure/main.bicep
          additionalArguments: "--what-if"

      - name: Deploy Bicep template
        uses: azure/arm-deploy@v2
        with:
          subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          resourceGroupName: ${{ secrets.AZURE_RG }}
          template: ./infrastructure/main.bicep
          failOnStdErr: false

This workflow triggers on any push to main that touches files in the infrastructure/ folder. It first runs a what-if analysis (so you can inspect the plan in the Actions log), then applies the deployment.

Structuring Your Repository

A clean folder structure keeps infrastructure code maintainable as the project grows:

repo-root/
├── .github/
│   └── workflows/
│       └── deploy-infrastructure.yml
└── infrastructure/
    ├── main.bicep
    ├── modules/
    │   ├── storage.bicep
    │   └── network.bicep
    └── parameters/
        ├── dev.bicepparam
        └── prod.bicepparam

Breaking infrastructure into modules keeps each file focused and reusable. The parameters/ folder holds environment-specific values, letting the same Bicep templates deploy consistently across dev, staging, and production.

Wrapping Up

With Bicep and GitHub Actions in place, your Azure infrastructure becomes code: versioned, reviewed, and automatically deployed. The what-if step in the pipeline acts as a built-in safety net, surfacing unintended changes before they hit production. From here you can extend the pattern to multiple environments, add approval gates via GitHub Environments, or integrate policy-as-code checks using Azure Policy or Checkov.