Bicep for .NET Developers: Infrastructure as Code Without the YAML

If you've ever deployed Azure resources through the portal and thought "I should automate this," Bicep is where you start. It's Azure's domain-specific language for infrastructure as code — a clean, readable alternative to ARM templates that compiles down to the same JSON under the hood.

For .NET developers, Bicep fits naturally into your workflow. It lives in your repository alongside your application code, deploys through CI/CD, and describes exactly what your application needs to run.

Your First Bicep File

A Bicep file declares Azure resources. Here's an App Service plan and web app:

main.bicep
param location string = resourceGroup().location
param appName string

resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: '${appName}-plan'
  location: location
  sku: {
    name: 'B1'
    tier: 'Basic'
  }
  kind: 'linux'
  properties: {
    reserved: true
  }
}

resource webApp 'Microsoft.Web/sites@2023-12-01' = {
  name: appName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|9.0'
      alwaysOn: true
    }
  }
  identity: {
    type: 'SystemAssigned'
  }
}

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

Notice how appServicePlan.id references the plan's resource ID directly. Bicep understands the dependency graph and deploys resources in the correct order.

Parameters and Environment Separation

Use parameter files to separate configuration from definition:

main.bicep
param environment string
param location string = resourceGroup().location
param sqlAdminPassword string

var prefix = 'myapp-${environment}'

resource sqlServer 'Microsoft.Sql/servers@2023-08-01-preview' = {
  name: '${prefix}-sql'
  location: location
  properties: {
    administratorLogin: 'sqladmin'
    administratorLoginPassword: sqlAdminPassword
  }
}

resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
  parent: sqlServer
  name: 'appdb'
  location: location
  sku: {
    name: environment == 'production' ? 'S1' : 'Basic'
  }
}
parameters.production.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "environment": { "value": "production" },
    "sqlAdminPassword": { "reference": {
      "keyVault": { "id": "/subscriptions/.../providers/Microsoft.KeyVault/vaults/my-vault" },
      "secretName": "sql-admin-password"
    }}
  }
}

The SQL password comes from Key Vault at deployment time — never stored in source control.

Modules for Reusable Components

Break your infrastructure into modules. A typical .NET project might have:

modules/app-service.bicep
param name string
param location string
param planId string

resource webApp 'Microsoft.Web/sites@2023-12-01' = {
  name: name
  location: location
  properties: {
    serverFarmId: planId
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|9.0'
      alwaysOn: true
    }
  }
  identity: {
    type: 'SystemAssigned'
  }
}

output principalId string = webApp.identity.principalId
output name string = webApp.name

Consume modules from the main file:

bicep
module api 'modules/app-service.bicep' = {
  name: 'deploy-api'
  params: {
    name: '${prefix}-api'
    location: location
    planId: appServicePlan.id
  }
}

Modules can also be published to a Bicep registry (backed by Azure Container Registry) and shared across teams.

Role Assignments

Assign RBAC roles to managed identities directly in Bicep:

bicep
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: '${replace(prefix, '-', '')}storage'
  location: location
  kind: 'StorageV2'
  sku: { name: 'Standard_LRS' }
}

resource blobContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(storageAccount.id, api.outputs.principalId, 'Storage Blob Data Contributor')
  scope: storageAccount
  properties: {
    roleDefinitionId: subscriptionResourceId(
      'Microsoft.Authorization/roleDefinitions',
      'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor
    principalId: api.outputs.principalId
    principalType: 'ServicePrincipal'
  }
}

This wires up managed identity permissions as part of your deployment. No manual portal clicks, no forgetting to grant access.

Deploying

Deploy with the Azure CLI:

terminal
az deployment group create \
    --resource-group my-rg \
    --template-file main.bicep \
    --parameters @parameters.production.json

Or in a GitHub Actions workflow:

config.yaml
- uses: azure/arm-deploy@v2
  with:
    resourceGroupName: my-rg
    template: ./infra/main.bicep
    parameters: ./infra/parameters.production.json

What-If Deployments

Preview changes before applying them:

terminal
az deployment group what-if \
    --resource-group my-rg \
    --template-file main.bicep \
    --parameters @parameters.production.json

This shows you exactly what will be created, modified, or deleted — similar to terraform plan. Use it in CI/CD as a review step before applying changes.

Practical Tips

Bicep is the most approachable infrastructure-as-code tool for Azure. If you're a .NET developer who's been clicking through the portal, start with a single Bicep file for your next project. Once your infrastructure is in code, you'll never go back.