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
- An Azure subscription
- The Azure CLI installed locally
- The Bicep VS Code extension (recommended)
- A GitHub repository to hold your infrastructure code
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 aboveAZURE_SUBSCRIPTION_ID— your Azure subscription IDAZURE_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.