RO EN

Professional CI/CD with GitHub Actions (3): Infrastructure as Code with Bicep

Professional CI/CD with GitHub Actions (3): Infrastructure as Code with Bicep ✨ Imagine generată cu AI
Doru Bulubasa
07 September 2026
43 views

Until now, in the series, we have built a pipeline that produces an artifact and promotes it in a controlled way through dev, staging, and production, with approval gates. But we elegantly skipped over one question: where exactly do the Azure resources we deploy to come from?

If the answer is "we made them manually from the portal," then you have a modern pipeline that deploys on an infrastructure created by clicks. And that's where the problems start.

A manually created App Service cannot be versioned, cannot be recreated identically, and no one knows, six months later, what settings it has and why.

🎯 What Infrastructure as Code means

Infrastructure as Code (IaC) means you define your infrastructure – App Service, database, storage, networking – in code files that you keep in git, alongside the application.

The benefits are exactly what you expect from code:

  • Versioning – you see in git who changed a setting and when.

  • Reproducibility – you recreate the entire environment identically, anytime, in a few minutes.

  • Review – an infrastructure change goes through a pull request like any other code.

  • No drift – what’s in the files is the source of truth, not what someone clicked in the portal three months ago.


💠 Why Bicep

On Azure, you have a few options. ARM templates in JSON are powerful but painful to write and read. Terraform is excellent and multi-cloud, but it requires managing a separate state file.

Bicep is Azure’s native language for IaC and strikes a very good balance:

  • Clean, declarative syntax – much more readable than ARM JSON.

  • Transpiles to ARM – under the hood, ARM still runs, so you don’t lose anything from the platform.

  • No state file to manage – Azure already knows the state of the resources.

  • Idempotent – you run it ten times, the result is the same.


🧱 Essential syntax: a main.bicep file

A Bicep file has four basic pieces: parameters (param), variables (var), resources (resource), and outputs (output). Here is an example that creates an App Service Plan and a Web App for .NET 8:

@description('Base name of the application')
param appName string

@description('Azure region')
param location string = resourceGroup().location

@allowed([ 'dev', 'staging', 'prod' ])
param environment string

var planName = '${appName}-plan-${environment}'
var webAppName = '${appName}-${environment}'

resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: planName
  location: location
  sku: {
    name: environment == 'prod' ? 'P1v3' : 'B1'
  }
}

resource webApp 'Microsoft.Web/sites@2023-12-01' = {
  name: webAppName
  location: location
  properties: {
    serverFarmId: plan.id
    httpsOnly: true
    siteConfig: {
      netFrameworkVersion: 'v8.0'
      alwaysOn: environment == 'prod'
    }
  }
}

output webAppName string = webApp.name
output webAppUrl string = 'https://${webApp.properties.defaultHostName}'

Notice how natural it reads. The environment parameter controls the differences between environments: in prod we request a P1v3 plan with alwaysOn, otherwise a cheaper B1. The same file, different environments – exactly the philosophy from part 2, but now applied to infrastructure.


⚙️ How to apply it from the command line

A Bicep deployment is applied to a resource group. First, you create the group, then you run the deployment:

az group create --name rg-myapp-prod --location westeurope

az deployment group create \
  --resource-group rg-myapp-prod \
  --template-file ./infra/main.bicep \
  --parameters appName=myapp environment=prod

Run the second command again. Nothing new happens – the resources already exist in the requested state. That is idempotency, and it is the reason you can put IaC in a pipeline without fear: it always applies the desired state, not "create again".


🔍 See what changes before applying: what-if

Before applying anything in production, you want to know exactly what will change. Bicep gives you a preview:

az deployment group what-if \
  --resource-group rg-myapp-prod \
  --template-file ./infra/main.bicep \
  --parameters appName=myapp environment=prod

what-if shows you what resources would be created, modified, or deleted – like a git diff, but for infrastructure. It’s the kind of step you put before an approval gate.


🔗 IaC in pipeline: provision before deploy

In the pipeline, you add a job that applies the Bicep before deploying the application. This way, infrastructure and application are delivered together, from the same commit:

  provision-infra:
    runs-on: ubuntu-latest
    needs: build-test
    environment:
      name: production
    steps:
      - uses: actions/checkout@v4

      - name: Azure login
        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 Bicep
        uses: azure/arm-deploy@v2
        with:
          resourceGroupName: rg-myapp-prod
          template: ./infra/main.bicep
          parameters: appName=myapp environment=prod

You see the azure/login step here without any password – just client-id, tenant-id, and subscription-id. How this authentication works without a client secret, via OIDC, is exactly the subject of part 5. For now, just remember that it’s possible and that it’s the correct way.


✅ Some good habits

  • One resource group per environment (rg-myapp-dev, rg-myapp-prod) – clean isolation between environments.

  • Break large files into reusable Bicep modules (one module for the database, one for the web app).

  • Keep a clear naming convention, derived from appName + environment.

  • Use outputs (webAppName, webAppUrl) in the next steps of the pipeline.

  • Run what-if before production – no infrastructure deploy blindly.


Conclusion

With Bicep, infrastructure is no longer a series of clicks that no one remembers, but versioned, reproducible code that goes through review – delivered in the same pipeline as the application. An entire environment becomes something you recreate identically in a few minutes.

In part 4, we use exactly this foundation for a concrete objective: deployment slots in Azure App Service, to deploy with zero downtime – and the slot can also be defined in Bicep.