Skip to Content
AzureBicep

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.

Note

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?

FeatureARM JSONBicep
SyntaxVerbose, deeply nested JSONClean, concise DSL
CommentsNot supportedSupported (// and /* */)
Type safetyMinimalFull type system
ModulesLinked templates (complex)Native module keyword
IDE supportLimitedFull IntelliSense in VS Code
Loopscopy propertyfor expressions

Prerequisites

Install the Bicep CLI

az bicep install az bicep upgrade az bicep version

Install 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 show

Anatomy 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.id

The sections are:

  • param — inputs passed at deploy time
  • var — internal computed values
  • resource — the Azure resource to create or manage
  • output — 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 array

Parameters with Default Values

param location string = resourceGroup().location param sku string = 'Standard_LRS' param instanceCount int = 2

Parameters 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 string
Important

Always 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 = 3

Variables

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' } } }
Tip

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.blob
Warning

Do 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 : 30

Modules

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.name

Consuming 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.id

Deploying 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().name

Real-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.vaultUri

Deploying Bicep

# 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.bicepparam

Compiling 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.json

Best Practices

  • Use @description on all parameters and outputs — it surfaces in the Azure Portal and makes templates self-documenting
  • Set targetScope explicitly — defaults to resourceGroup, but always make it obvious at the top of the file
  • Prefer @allowed over 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 — storageAccount not createStorageAccount
  • Use existing for cross-resource-group references — avoids duplicating resource management across templates
  • Run what-if before 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
Last updated on