Azure Bicep
Bicep is a domain-specific language (DSL) for deploying Azure resources declaratively. It compiles down to ARM JSON templates but with significantly cleaner syntax, better tooling support, and first-class IDE integration. Bicep is the recommended approach for infrastructure-as-code on Azure.
Bicep files use the .bicep extension and are compiled to ARM JSON before deployment. You never need to write or edit the JSON directly.
Why Bicep Over ARM JSON?
| Feature | ARM JSON | Bicep |
|---|---|---|
| Syntax | Verbose, deeply nested JSON | Clean, concise DSL |
| Comments | Not supported | Supported (// and /* */) |
| Type safety | Minimal | Full type system |
| Modules | Linked templates (complex) | Native module keyword |
| IDE support | Limited | Full IntelliSense in VS Code |
| Loops | copy property | for expressions |
Prerequisites
Install the Bicep CLI
az bicep install
az bicep upgrade
az bicep versionInstall the VS Code extension
Search for Bicep in the VS Code extensions marketplace (published by Microsoft). This gives you IntelliSense, linting, and type checking.
Verify your Azure CLI is logged in
az login
az account showAnatomy of a Bicep File
A Bicep file is made up of several building blocks. Here is a minimal but complete example deploying a storage account:
// Parameters - inputs to your template
param location string = resourceGroup().location
param storageAccountName string
// Variables - computed values used within the template
var sku = 'Standard_LRS'
// Resource declaration
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: sku
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
}
}
// Output - values returned after deployment
output storageAccountId string = storageAccount.idThe sections are:
param— inputs passed at deploy timevar— internal computed valuesresource— the Azure resource to create or manageoutput— values exported after deployment
Parameters
Parameters make your templates reusable. They accept values at deploy time and can have defaults, constraints, and descriptions.
Basic Parameters
param location string
param instanceCount int
param enableHttps bool
param tags object
param allowedIPs arrayParameters with Default Values
param location string = resourceGroup().location
param sku string = 'Standard_LRS'
param instanceCount int = 2Parameters with Decorators
Decorators add validation and metadata. They sit directly above the param declaration.
@description('The name of the storage account. Must be globally unique.')
@minLength(3)
@maxLength(24)
param storageAccountName string
@description('Number of instances to deploy.')
@minValue(1)
@maxValue(10)
param instanceCount int = 2
@description('The pricing tier for the App Service Plan.')
@allowed(['F1', 'B1', 'B2', 'S1', 'S2', 'P1v3'])
param appServicePlanSku string = 'B1'
@secure()
@description('The administrator password. This value is never logged.')
param adminPassword stringAlways use @secure() for passwords, connection strings, and keys. Secure parameters are never logged in deployment history.
Parameter Files
For different environments, use .bicepparam files instead of passing values on the command line:
// main.bicepparam
using './main.bicep'
param location = 'uksouth'
param storageAccountName = 'stmyappprod001'
param instanceCount = 3Variables
Variables store computed or reused values. They are evaluated at deploy time and cannot be changed after that.
param appName string
param environment string
// Simple string
var location = 'uksouth'
// Concatenation
var resourcePrefix = '${appName}-${environment}'
// Object
var commonTags = {
application: appName
environment: environment
managedBy: 'bicep'
createdDate: '2025-01-01'
}
// Conditional
var storageSkuName = environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'Resource Declarations
Resources follow a consistent structure:
resource <symbolic-name> '<resource-type>@<api-version>' = {
name: <resource-name>
location: <location>
...properties
}The symbolic name is used only within the Bicep file to reference the resource. The name property is the actual Azure resource name.
Example: Storage Account
param storageAccountName string
param location string = resourceGroup().location
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
}
}Example: Key Vault
param keyVaultName string
param location string = resourceGroup().location
param tenantId string = subscription().tenantId
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: tenantId
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
enablePurgeProtection: true
}
}Example: App Service Plan + Web App
param appName string
param location string = resourceGroup().location
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: 'asp-${appName}'
location: location
sku: {
name: 'B1'
tier: 'Basic'
}
properties: {
reserved: true // Required for Linux
}
}
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: 'app-${appName}'
location: location
properties: {
serverFarmId: appServicePlan.id // Reference the plan defined above
httpsOnly: true
siteConfig: {
linuxFxVersion: 'DOTNETCORE|8.0'
ftpsState: 'Disabled'
minTlsVersion: '1.2'
}
}
}When one resource references another via its symbolic name (e.g. appServicePlan.id), Bicep automatically infers the deployment dependency. You don’t need dependsOn in most cases.
Outputs
Outputs return values after a deployment completes. They are useful for passing values to scripts, pipelines, or chained deployments.
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'stmyapp001'
location: 'uksouth'
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
properties: {}
}
output storageAccountId string = storageAccount.id
output storageAccountName string = storageAccount.name
output primaryBlobEndpoint string = storageAccount.properties.primaryEndpoints.blobDo not output sensitive values like connection strings or keys as plain strings — they will be visible in deployment history. Use Key Vault references instead.
Loops
Loops let you deploy multiple instances of a resource from a single declaration.
Loop Over an Array
param storageAccountNames array = [
'stapp001'
'stapp002'
'stapp003'
]
resource storageAccounts 'Microsoft.Storage/storageAccounts@2023-01-01' = [for name in storageAccountNames: {
name: name
location: resourceGroup().location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
properties: {}
}]Loop with Index
param count int = 3
resource storageAccounts 'Microsoft.Storage/storageAccounts@2023-01-01' = [for i in range(0, count): {
name: 'st${padLeft(i + 1, 3, '0')}' // st001, st002, st003
location: resourceGroup().location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
properties: {}
}]Loop Over an Array of Objects
param subnets array = [
{ name: 'snet-app', addressPrefix: '10.0.1.0/24' }
{ name: 'snet-data', addressPrefix: '10.0.2.0/24' }
{ name: 'snet-mgmt', addressPrefix: '10.0.3.0/24' }
]
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: 'vnet-myapp-prod-uks-01'
location: resourceGroup().location
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
subnets: [for subnet in subnets: {
name: subnet.name
properties: {
addressPrefix: subnet.addressPrefix
}
}]
}
}Referencing Looped Resources
// Access a specific item by index
output firstStorageId string = storageAccounts[0].id
// Output all IDs
output allStorageIds array = [for i in range(0, count): storageAccounts[i].id]Conditions
Use the if keyword to deploy a resource conditionally.
param environment string
param deployDiagnostics bool = false
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = if (deployDiagnostics) {
name: 'log-myapp-${environment}'
location: resourceGroup().location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}Conditional expressions also work inline:
param isProd bool
var replicationSku = isProd ? 'Standard_GRS' : 'Standard_LRS'
var retentionDays = isProd ? 90 : 30Modules
Modules let you break large templates into reusable components. Each module is its own .bicep file.
Creating a Module
// modules/storage.bicep
@description('Name of the storage account.')
param name string
@description('Azure region for the storage account.')
param location string
@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_ZRS'])
param sku string = 'Standard_LRS'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: name
location: location
sku: { name: sku }
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
allowBlobPublicAccess: false
}
}
output id string = storageAccount.id
output name string = storageAccount.nameConsuming a Module
// main.bicep
param location string = resourceGroup().location
module primaryStorage 'modules/storage.bicep' = {
name: 'primaryStorage'
params: {
name: 'stmyappprod001'
location: location
sku: 'Standard_GRS'
}
}
module backupStorage 'modules/storage.bicep' = {
name: 'backupStorage'
params: {
name: 'stmyappbackup001'
location: location
sku: 'Standard_LRS'
}
}
output primaryStorageId string = primaryStorage.outputs.idDeploying a Module to a Different Scope
Useful for creating resource groups or subscription-level resources:
targetScope = 'subscription'
module resourceGroupDeploy 'modules/resource-group.bicep' = {
name: 'rg-deployment'
scope: subscription()
params: {
resourceGroupName: 'rg-myapp-prod-uks-01'
location: 'uksouth'
}
}Existing Resources
Reference resources that already exist in Azure without managing them in your template:
// Reference an existing Key Vault to read a secret
resource existingKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: 'kv-myapp-prod-001'
scope: resourceGroup('rg-shared-services')
}
// Use the Key Vault reference in another resource
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: 'app-myapp-prod'
location: resourceGroup().location
properties: {
serverFarmId: existingPlan.id
siteConfig: {
appSettings: [
{
name: 'ConnectionString'
value: '@Microsoft.KeyVault(VaultName=${existingKeyVault.name};SecretName=db-connection-string)'
}
]
}
}
}Scope Functions
Bicep provides built-in functions to retrieve information about the deployment context:
// Subscription details
var subId = subscription().subscriptionId
var tenantId = subscription().tenantId
// Resource group details
var rgName = resourceGroup().name
var rgLocation = resourceGroup().location
// Deployment details
var deploymentName = deployment().nameReal-World Example: Complete Web Application Stack
The following deploys a full web application: App Service Plan, Web App, and a Key Vault with a diagnostic settings resource.
// main.bicep
@description('Short application name used in resource naming.')
@maxLength(10)
param appName string
@description('Deployment environment.')
@allowed(['dev', 'test', 'prod'])
param environment string
@description('Azure region for all resources.')
param location string = resourceGroup().location
@description('Enable diagnostic logging to Log Analytics.')
param enableDiagnostics bool = false
@description('Log Analytics workspace resource ID (required if diagnostics enabled).')
param logAnalyticsWorkspaceId string = ''
var isProd = environment == 'prod'
var prefix = '${appName}-${environment}'
var appServicePlanSku = isProd ? 'P1v3' : 'B1'
var commonTags = {
application: appName
environment: environment
managedBy: 'bicep'
}
// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: 'asp-${prefix}-uks-01'
location: location
tags: commonTags
sku: {
name: appServicePlanSku
}
properties: {
reserved: true
}
}
// Web App
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: 'app-${prefix}-uks-01'
location: location
tags: commonTags
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'DOTNETCORE|8.0'
ftpsState: 'Disabled'
minTlsVersion: '1.2'
http20Enabled: true
}
}
identity: {
type: 'SystemAssigned'
}
}
// Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: 'kv-${prefix}-uks-01'
location: location
tags: commonTags
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: isProd ? 90 : 7
enablePurgeProtection: isProd
}
}
// Diagnostic settings (optional)
resource webAppDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagnostics) {
name: 'diag-${webApp.name}'
scope: webApp
properties: {
workspaceId: logAnalyticsWorkspaceId
logs: [
{
category: 'AppServiceHTTPLogs'
enabled: true
}
{
category: 'AppServiceAppLogs'
enabled: true
}
]
metrics: [
{
category: 'AllMetrics'
enabled: true
}
]
}
}
// Outputs
output webAppName string = webApp.name
output webAppUrl string = 'https://${webApp.properties.defaultHostName}'
output webAppPrincipalId string = webApp.identity.principalId
output keyVaultName string = keyVault.name
output keyVaultUri string = keyVault.properties.vaultUriDeploying Bicep
Azure CLI
# Deploy to a resource group
az deployment group create \
--resource-group "rg-myapp-prod-uks-01" \
--template-file main.bicep \
--parameters appName=myapp environment=prod
# Deploy using a parameter file
az deployment group create \
--resource-group "rg-myapp-prod-uks-01" \
--template-file main.bicep \
--parameters main.bicepparam
# What-if (dry run - shows what would change)
az deployment group what-if \
--resource-group "rg-myapp-prod-uks-01" \
--template-file main.bicep \
--parameters main.bicepparam
# Deploy at subscription scope
az deployment sub create \
--location uksouth \
--template-file main.bicep \
--parameters environment=prod
# Validate without deploying
az deployment group validate \
--resource-group "rg-myapp-prod-uks-01" \
--template-file main.bicep \
--parameters main.bicepparamCompiling Bicep to ARM JSON
If you need the ARM JSON (e.g. for a tool that doesn’t support Bicep natively):
# Compile to ARM JSON
az bicep build --file main.bicep
# Decompile ARM JSON back to Bicep (best-effort)
az bicep decompile --file azuredeploy.jsonBest Practices
- Use
@descriptionon all parameters and outputs — it surfaces in the Azure Portal and makes templates self-documenting - Set
targetScopeexplicitly — defaults toresourceGroup, but always make it obvious at the top of the file - Prefer
@allowedover runtime validation — fail fast at deploy time rather than after a resource is partially created - Name symbolic names after what they are, not what they do —
storageAccountnotcreateStorageAccount - Use
existingfor cross-resource-group references — avoids duplicating resource management across templates - Run
what-ifbefore every production deployment — it shows exactly what will change, be added, or deleted - Store Bicep files in source control — treat infrastructure changes with the same review process as application code
- Use Bicep Registry or Template Specs for sharing modules across teams rather than copying files