Skip to content

Deploying with Bicep

Scope: one small Bicep template that builds the VM from Your First Linux VM, explained line by line. The template was compiled with the real bicep build — it is syntactically valid. Deploying it needs a live subscription (documented).

Why Bicep

Clicking through the portal doesn't scale, and a shell script full of az commands is fragile — re-running it creates duplicates or errors. Bicep is Azure's infrastructure-as-code language: you declare what you want, and Azure figures out the order and skips what already exists. It's a thin, readable layer over ARM templates (the older JSON format — if you meet ARM, Bicep compiles to it).

The template lives at examples/main.bicep in this repo. It builds the same neighborhood az vm create made: VNet, public IP, NSG with an SSH rule, network interface, and the VM.

Line by line

targetScope = 'resourceGroup'

Everything deploys into one resource group — the nuke-button unit from Resource Groups and Naming.

@description('Azure region for all resources.')
param location string = 'eastus'

Parameters are the template's inputs, with defaults. location, namePrefix, adminUsername, and vmSize all have defaults; sshPublicKey doesn't — you must supply it, because a default SSH key would be everyone's key.

var vnetName = '${namePrefix}-vnet'

Variables build the resource names from the prefix: demo-vnet, demo-pip, demo-nsg, demo-nic, demo-vm. Same naming habit as the CLI chapter.

resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
  name: vnetName
  location: location
  properties: { ... }
}

A resource block: the type (Microsoft.Network/ virtualNetworks), an API version (@2023-09-01 — Bicep needs one; newer versions add features), and the properties. The five resources are the VNet (10.0.0.0/16 with one 10.0.1.0/24 subnet), a Standard-SKU static public IP, an NSG allowing inbound TCP 22, a network interface wiring the three together, and the VM itself.

Notice what the template doesn't say: no ordering. Bicep reads the references — nic uses vnet.properties .subnets[0].id and pip.id, so the NIC deploys after both — and builds the dependency graph itself. You declare the what; Azure handles the when.

The VM's osProfile disables password auth and installs your SSH key, and storageProfile picks the Ubuntu 22.04 image with a Standard SSD OS disk — the same choices as the CLI chapter.

output publicIp string = pip.properties.ipAddress
output sshCommand string = 'ssh ${adminUsername}@${pip.properties.ipAddress}'

Outputs print after deployment — here, the IP and the exact SSH command. Small thing, big convenience.

Build it, then deploy it

Compile (no subscription needed — this was actually run):

$ az bicep build --file examples/main.bicep

Check: no output means success, and a main.json appears beside it — the compiled ARM template. (Bicep is installed on demand: az bicep install fetches it the first time.)

Deploy (needs a live subscription; (documented)):

$ az deployment group create \
    --resource-group rg-demo \
    --template-file examples/main.bicep \
    --parameters sshPublicKey="$(cat ~/.ssh/id_rsa.pub)"

Re-run it with a different vmSize and Bicep updates the VM in place instead of creating a second one — that's the whole point of declaring rather than scripting. And when the experiment ends, the group delete still nukes everything, Bicep-made or not.

Bicep vs the CLI

CLI script Bicep
Re-running Errors or duplicates Updates in place
Ordering You sequence the commands Inferred from references
Readability Fine for 5 lines, soup at 50 Structured at any size
Learning curve You already know it An afternoon

Start with the CLI. Reach for Bicep when you're rebuilding the same thing twice — which, in the cloud, is about a week in.