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:
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:
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'
}
}
{
"$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:
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:
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:
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:
az deployment group create \
--resource-group my-rg \
--template-file main.bicep \
--parameters @parameters.production.json
Or in a GitHub Actions workflow:
- 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:
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
- Commit Bicep files alongside your application code. Infrastructure and application evolve together.
- Use
existingkeyword to reference resources defined elsewhere:resource vault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { name: 'my-vault' }. - Avoid hardcoding resource IDs. Use
resourceId()and references between resources. - Use the VS Code Bicep extension. It provides IntelliSense, validation, and visualisation of your resource graph.
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.