Logo
CloudWithSingh
Back to all field notes
Cheatsheet
Azure
Intermediate

Azure Bicep Syntax Quick Reference

The Bicep syntax surface on one page: file shape, parameters, decorators, resources, modules, loops, scopes, functions, and the traps that actually break deploys.

Parveen Singh
August 22, 2026
10 min read
28 commands
Prerequisites:Azure CLI installed (az --version)VS Code with the Bicep extensionYou have deployed at least one resource group
TLDR

Bicep is the language. ARM is the engine. This note is the lookup page: how a file is shaped, how parameters and .bicepparam work, every decorator you actually use, resources (symbolic names, nested, existing), modules across scopes, loops and conditions, the four target scopes with their az deployment commands, the functions that show up in real templates, user-defined types and functions, outputs, and five traps that waste an afternoon.

This is not a learning path. If you want order plus labs, use the Bicep learning roadmap. If you want the syntax while you are in a file, stay here.

📄 File shape

A .bicep file is declarations. Order does not matter. Bicep builds the graph.

targetScope = 'resourceGroup' // optional. resourceGroup is the default
 
param location string = resourceGroup().location
var namePrefix = 'app'
resource stg 'Microsoft.Storage/storageAccounts@2023-05-01' = { ... }
output blobEndpoint string = stg.properties.primaryEndpoints.blob

Typical order people use, only because it reads well: targetScope, metadata, params, variables, resources and modules, outputs.

Compile check without deploying:

az bicep build --file main.bicep
az bicep format --file main.bicep
Pro Tip

The Bicep extension's "Insert Resource" command is how you stop guessing API versions. Pick the type, take the current version, then freeze it. Do not chase "latest" on every save.

🎛️ Parameters

@description('Azure region for all resources')
param location string = resourceGroup().location
 
@allowed(['dev', 'test', 'prod'])
param env string
 
@minLength(3)
@maxLength(24)
param storagePrefix string
 
@secure()
param adminPassword string
KindWhen you use it
Required param name typeCaller must pass it
Default = expressionOptional. Evaluated at deploy time
@secure()Secrets. Never in outputs. Never in plaintext logs

Pass values at deploy:

az deployment group create \
  --resource-group rg-demo \
  --template-file main.bicep \
  --parameters env=dev storagePrefix=appstg

📦 .bicepparam files

Do not keep environment values in the template. Keep them next to it.

// main.bicepparam
using 'main.bicep'
 
param env = 'dev'
param location = 'canadacentral'
param storagePrefix = 'appstg'
az deployment group create \
  --resource-group rg-demo \
  --template-file main.bicep \
  --parameters main.bicepparam

One template. One param file per environment. Same pattern as Terraform .tfvars, without a state file.

Expressions, parameters, variables, outputs
Hands-on LabCloudlearn.io

Expressions, parameters, variables, outputs

The lab that turns a one-off template into something you can reuse across environments.

labs.cloudlearn.ioStart the lab

🏷️ Decorators

Decorators sit above the declaration they change.

DecoratorOnWhat it does
@description('...')param, type, func, outputDocs in the portal and compiled ARM
@allowed([...])paramEnum. Prefer this over a comment
@minLength / @maxLengthstring or array paramLength bounds
@minValue / @maxValueint paramNumeric bounds
@secure()paramMarks a secret
@metadata({...})paramExtra portal/tooling hints
@discriminator('type')union typeTagged union for object variants
@export()type, var, funcMakes it importable from another file
@sealed()user-defined typeRejects extra properties
@batchSize(n)resource or module loopParallelism cap for the loop
When to Use

@allowed and @description on every public parameter. @secure the moment a value is a password, a key, or a connection string. @batchSize only after a loop 429s or thrashes a subscription.

🧱 Resources and symbolic names

resource stg 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
  }
}

The symbolic name (stg) is how the rest of the file talks to this resource. The ARM type string is 'Provider/type@apiVersion'. Pin the API version.

You wantYou write
Resource idstg.id
Namestg.name
A propertystg.properties.primaryEndpoints.blob
A child namestg.name plus the child segment, or parent:

Bicep infers dependsOn from those references. If you are typing dependsOn, you probably missed a property reference.

🔗 Nested and existing resources

Child with parent: (name is only the child segment):

resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' = {
  parent: stg
  name: 'default'
}
 
resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = {
  parent: blobService
  name: 'data'
}

Reference something that already exists. This does not deploy it:

resource existingStg 'Microsoft.Storage/storageAccounts@2023-05-01' existing = {
  name: storageName
}
 
output alreadyThere string = existingStg.id

Cross-resource-group existing:

resource existingStg 'Microsoft.Storage/storageAccounts@2023-05-01' existing = {
  name: storageName
  scope: resourceGroup('rg-shared')
}
Warning

existing is a read at deploy time. Wrong name or API version and the deploy fails at ARM, not at compile. The compile will still look clean.

📦 Modules and cross-scope deploys

module storage 'modules/storage.bicep' = {
  name: 'storageDeploy'
  params: {
    name: storageName
    location: location
  }
}
 
output blobEndpoint string = storage.outputs.blobEndpoint

The name on a module is the nested deployment name in ARM. Keep it stable. Changing it looks like a new nested deployment.

Deploy a module into another resource group:

module network 'modules/vnet.bicep' = {
  name: 'networkDeploy'
  scope: resourceGroup('rg-network')
  params: {
    location: location
  }
}

Episode 4 of the series is this pattern: one output in, one parameter out. Modules, one output.

Deploy a VNet and VM with Bicep
Hands-on LabCloudlearn.io

Deploy a VNet and VM with Bicep

A real stack, not a storage account. Dependencies, networking, and a VM in one template.

labs.cloudlearn.ioStart the lab

🔁 Loops

param subnets array
 
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-09-01' = [
  for (s, i) in subnets: {
    name: 'nsg-${s.name}'
    location: location
    properties: {
      securityRules: []
    }
  }
]
FormUse
for item in collectionWalk an array
for (item, i) in collectionYou need the index
for i in range(0, count)Fixed count
if condition after the forFilter items out of the loop

Throttle a noisy loop:

@batchSize(3)
resource nics 'Microsoft.Network/networkInterfaces@2023-09-01' = [
  for vm in vms: {
    name: 'nic-${vm.name}'
    // ...
  }
]

You cannot nest resource loops inside resource loops. Push the inner loop into a module and loop the module.

❓ Conditions and operators

Resource exists or it does not:

param deployBastion bool
 
resource bastion 'Microsoft.Network/bastionHosts@2023-09-01' = if (deployBastion) {
  name: 'bastion'
  // ...
}
OperatorMeaning
cond ? a : bTernary
a ?? bIf a is null, use b
obj.?propSafe navigate. Missing property becomes null
item in list / contains()Membership
var skuName = env == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
var endpoint = stg.?properties.?primaryEndpoints.?blob ?? ''
Conditions, loops, what-if
Hands-on LabCloudlearn.io

Conditions, loops, what-if

The production patterns: optional resources, copy loops, and previewing the change before you apply it.

labs.cloudlearn.ioStart the lab

🌐 Four scopes

targetScope at the top of the file has to match the az deployment command you run.

targetScopeDeploy withNeeds
'resourceGroup' (default)az deployment group create --resource-group <rg>RG already exists
'subscription'az deployment sub create --location <region>Often used to create RGs
'managementGroup'az deployment mg create --management-group-id <id> --location <region>MG write permission
'tenant'az deployment tenant create --location <region>Tenant-wide, rare
# what-if before you apply
az deployment group what-if \
  --resource-group rg-demo \
  --template-file main.bicep \
  --parameters main.bicepparam

Module scope: is how a resourceGroup template reaches another RG, or a subscription template reaches a specific RG. Mixing this up is the usual "why did this land in the wrong place" ticket.

🧰 Functions you actually use

Everything else is in the docs. These show up in real templates.

FunctionTypical use
resourceGroup() / .location / .idDefault region, unique seeds
subscription() / tenant()Scope ids
uniqueString(...)Short, stable hash for names
guid(...)Stable GUID from seeds
utcNow()Timestamps. Default values only, not resource bodies that must be idempotent
'${a}-${b}'String interpolation. Prefer this over concat()
format('{0}-{1}', a, b)Same idea, numbered
toLower() / replace() / substring()Name cleanup
length() / empty() / contains()Guards
first() / last() / take() / skip()Array slices
union() / intersection()Object or array merge
items() / keys()Walk an object
loadJsonContent() / loadTextContent()Compile-time file load
resourceId(type, ...names)Build an id when you do not have a symbolic ref
existing.listKeys().keys[0].valueKeys from an existing account. Treat as secret
param prefix string
var stgName = toLower('st${prefix}${uniqueString(resourceGroup().id)}')

uniqueString(resourceGroup().id) is how storage account names stay globally unique without becoming random every deploy.

🧩 User-defined types and functions

@export()
type subnet = {
  name: string
  addressPrefix: string
}
 
param subnets subnet[]
 
func storageName(prefix string) string => toLower('st${prefix}${uniqueString(resourceGroup().id)}')
 
var stgName = storageName('app')

Put shared types in types.bicep, @export() them, then import { subnet } from 'types.bicep'. That is the same idea as a Terraform module's variables file, minus the state argument.

@sealed() on a type rejects extra properties. Use it when a param object should not silently accept junk.

📤 Outputs

output storageId string = stg.id
output blobEndpoint string = stg.properties.primaryEndpoints.blob
 
output nsgIds array = [for (s, i) in subnets: nsg[i].id]

Read them after deploy:

az deployment group show \
  --resource-group rg-demo \
  --name main \
  --query properties.outputs

Modules expose outputs as moduleName.outputs.foo. That is the contract between modules. Do not reach into another module's resources.

Warning

You cannot output an @secure() parameter. ARM will refuse it. If a caller needs a secret, they already have it, or they read it from Key Vault at runtime, not from a deployment output.

⚠️ Five traps

TrapWhat happensWhat to do
Hardcoded namesSecond deploy to another RG collides, or a storage name is taken globallyuniqueString(resourceGroup().id) in the name. Keep it deterministic
existing with a guessed API versionCompile succeeds. Deploy fails looking up the resourceMatch the type version you would use to deploy it
Nested resource loopsBicep will not let you, or you invent dependsOn spaghettiInner loop lives in a module. Outer loop deploys the module
Outputting @secure() valuesARM error at deployKey Vault reference, or the caller already knows the secret
Wrong az deployment for targetScopeCLI error, or resources in an unexpected subscriptionTable above. group / sub / mg / tenant must match the file
Pro Tip

Run what-if on anything that already exists. A clean plan with noChange on the resources you care about is the cheapest test you have. The lab for that is the conditions and loops one above.

🔗 Where to go next

CloudLearn: Bicep from first template to production
CourseCloudlearn.io - Interactive Cloud & Security Learning Platform

CloudLearn: Bicep from first template to production

The labs linked above, in order, in a real Azure environment. No subscription of your own required.

labs.cloudlearn.ioStart a free lab

What's Next

Bookmark this page

Save it for your next project sprint

Start a project

Apply what you just learned hands-on

Follow on Instagram

Daily cloud tips & behind-the-scenes

Try hands-on labs

Practice in a real cloud environment

Parveen Singh

Parveen Singh

Microsoft Certified Trainer & Cloud Solutions Consultant

Related Field Notes

Found this useful?

Stay in the loop

Weekly cloud insights, no spam

Subscribe

Explore CloudLearn

Hands-on labs & projects

Start Learning

Book Training

Custom cloud training for your team

Get in Touch

On this page