Introduction
Azure Bicep is Microsoft's official DSL (Domain-Specific Language) for declaratively describing Azure infrastructure deployments, gradually replacing verbose ARM JSON templates. In 2026, with the rise of IaC (Infrastructure as Code), Bicep stands out for its concise syntax, compilation to ARM JSON, and native integration with Azure CLI and VS Code. Unlike Terraform (multi-cloud), Bicep excels in the Azure ecosystem thanks to IntelliSense autocompletion and real-time validation.
Why adopt it? It reduces errors by 70% compared to JSON (per Microsoft), enables easy modularity, and supports advanced loops and conditions. This intermediate tutorial guides you step by step to deploy realistic infrastructure: a storage account, VNet, and App Service, covering parameters, modules, and outputs. By the end, you'll master pro patterns you can bookmark to scale your Azure deployments.
Prerequisites
- Active Azure account with a subscription (free credits via azure.microsoft.com/free).
- Azure CLI version 2.60+ installed (
az --versionto check). - VS Code with the official Bicep extension (for IntelliSense and validation).
- Basic knowledge of ARM (JSON) and IaC.
- PowerShell or Bash for deployments.
Login and resource group creation
az login
az account set --subscription "Votre-ID-Abonnement"
az group create --name rg-bicep-demo --location francecentral
az group deployment list --resource-group rg-bicep-demo --query "[].[name,timestamp]" -o tableThis script logs you into Azure, selects the subscription, and creates a resource group rg-bicep-demo in France Central. The final command lists existing deployments (empty at first) for validation. Replace "Votre-ID-Abonnement" with your actual ID from az account list; avoid unsupported regions to minimize costs.
First simple Bicep template
Let's start with a basic template that deploys a storage account. Bicep compiles to valid ARM JSON with an intuitive YAML-like syntax. Think of Bicep as 'lightweight JSON': no extra quotes, symbolic references (@resourceId).
Basic storage account template
@description('Primary storage account')
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: 'stbicep${uniqueString(resourceGroup().id)}'
location: resourceGroup().location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
}
}This template declares a unique storage account (using uniqueString to avoid collisions). It inherits the resource group's location with resourceGroup().location. Deploy it with az deployment group create -f main.bicep -g rg-bicep-demo; pitfall: storage names must be globally unique (max 24 lowercase chars).
Deploying the simple template
az deployment group create \
--resource-group rg-bicep-demo \
--template-file main.bicep \
--confirm-prompt falseDeploys the template to the specified resource group without prompting. --confirm-prompt false automates for CI/CD. Verify with az storage account list -g rg-bicep-demo; watch regional quotas (500 storage accounts per RG).
Parameters and variables for reusability
At the intermediate level, parameterize everything: SKU, location, tags. Variables centralize calculations. Analogy: parameters = CLI arguments, variables = locals/const in TypeScript.
Template with parameters and variables
@minValue(1)
@maxValue(512)
@param sizeInGb int = 5
@description('Location')
@param location string = resourceGroup().location
@description('Common tags')
@param tags object = {}
var storageName = 'stparam${uniqueString(resourceGroup().id)}'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageName
location: location
tags: tags
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
}
}
output storageName string = storageName
output storageId string = storageAccount.idAdds validated @param (min/max for size), @tags, and output for use elsewhere. var computes the name once. Deploy with --parameters @{sizeInGb=10;tags={env='dev'}}; pitfall: don't forget @minValue for prod safety.
Parameterized deployment
az deployment group create \
--resource-group rg-bicep-demo \
--template-file storage-params.bicep \
--parameters sizeInGb=10 tags='{"env":"dev","costcenter":"team1"}' \
--query properties.outputs.storageName.value -o tsvPasses JSON-like parameters and queries the storageName output. Great for GitHub Actions pipelines. Avoid nested quotes by using a params.json file; validate with bicep build first.
Modules for advanced modularity
Break things into reusable modules (like React components). Create a VNet module and import it. Ideal for microservices or multi-env setups.
Reusable VNet module
@description('Address prefix')
@param vnetNamePrefix string
@param addressPrefix string = '10.0.0.0/16'
@param subnetPrefix string = '10.0.1.0/24'
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: '${vnetNamePrefix}vnet'
location: resourceGroup().location
properties: {
addressSpace: {
addressPrefixes: [addressPrefix]
}
subnets: [
{
name: 'default'
properties: {
addressPrefix: subnetPrefix
}
}
]
}
}
output vnetId string = vnet.id
output subnetId string = vnet.subnets[0].idStandalone module with flexible params and outputs. Deploy standalone or import. Analogy: pure function with return; test with az deployment group what-if for a no-cost preview.
Main with VNet module and loop
module vnetModule './vnet.bicep' = {
name: 'vnetDeployment'
params: {
vnetNamePrefix: 'demo'
addressPrefix: '10.1.0.0/16'
subnetPrefix: '10.1.1.0/24'
}
}
// Loop for multiple subnets
@batchSize(2)
resource additionalSubnets 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = [for i in range(0, 3): {
name: 'subnet-${i}'
parent: vnetModule
properties: {
addressPrefix: '10.1.${i+2}.0/24'
}
}]module imports like a sub-deployment. for loop with @batchSize parallelizes (avoids timeouts). parent references the module output; pitfall: module outputs scoped to the module.
Full modular deployment
bicep build --file main-module.bicep
az deployment group create \
--resource-group rg-bicep-demo \
--template-file main-module.bicep \
--what-ifbicep build compiles to JSON for debugging. --what-if simulates without deploying (pro dry-run). Useful in GitHub PRs; clean up with az group delete -g rg-bicep-demo --yes at the end.
Best practices
- Always modularize: Keep main.bicep under 400 lines, one module per resource.
- Use
what-ifandbicep buildin CI/CD for validation. - Secure secrets with
@secure()and Key Vault references (vaultUri). - Systematic tags:
{ 'managedBy': 'bicep', 'env': param('environment') }. - Explicit outputs: Reference them in Azure DevOps pipelines.
Common errors to avoid
- Non-unique names: Storage/VM names are global → collisions; always use
uniqueString(rg.id). - Inconsistent regions: Mixing
westeurope/francecentral→ VNet peering fails. - Missing
@batchSizein loops: Timeouts on >10 iterations. - No
dependsOn: Wrong deployment order (e.g., NSG before VNet) → use implicitreference().
Next steps
- Official docs: Bicep Language.
- Example repo: Azure Bicep Modules.
- Advanced: Integrate with GitHub Actions or Azure Pipelines.
- Azure DevOps Training for AZ-400 certification.