Skip to content
Learni
View all tutorials
DevOps

How to Deploy Azure Infrastructure with Bicep in 2026

Lire en français

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 --version to 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

setup.sh
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 table

This 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

main.bicep
@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

deploy-simple.sh
az deployment group create \
  --resource-group rg-bicep-demo \
  --template-file main.bicep \
  --confirm-prompt false

Deploys 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

storage-params.bicep
@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.id

Adds 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

deploy-params.sh
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 tsv

Passes 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

vnet.bicep
@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].id

Standalone 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

main-module.bicep
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

deploy-module.sh
bicep build --file main-module.bicep
az deployment group create \
  --resource-group rg-bicep-demo \
  --template-file main-module.bicep \
  --what-if

bicep 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-if and bicep build in 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 @batchSize in loops: Timeouts on >10 iterations.
  • No dependsOn: Wrong deployment order (e.g., NSG before VNet) → use implicit reference().

Next steps

How to Deploy Azure Infra with Bicep in 2026 | Learni