Azure · lesson 8 of 10
Bicep and ARM Templates Basics
Write a minimal Bicep file, deploy it with az deployment group create, then read the exact error output from a deployment that fails validation.
By SkillByExample · Updated
Bicep is a declarative language for Azure infrastructure that compiles to ARM JSON — you
write Bicep, Azure Resource Manager deploys the JSON it compiles to, and the two are
interchangeable at any point (az bicep build converts one to the other). This lesson
writes a small storage account definition, deploys it, then deliberately breaks it to see
what a rejected deployment looks like.
A minimal Bicep file
// main.bicep
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'
}
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob
param location string = resourceGroup().location defaults to the resource group’s own
region — a common pattern so callers don’t have to repeat a location that’s already implied
by where they’re deploying.
Deploying it
az deployment group create \
--resource-group learn-rg \
--template-file main.bicep \
--parameters storageAccountName=learnbicep482
$ az deployment group create --resource-group learn-rg --template-file main.bicep --parameters storageAccountName=learnbicep482
{
"properties": {
"provisioningState": "Succeeded",
"outputs": {
"blobEndpoint": {
"type": "String",
"value": "https://learnbicep482.blob.core.windows.net/"
}
}
},
"resourceGroup": "learn-rg"
}
The CLI transparently compiles main.bicep to ARM JSON before submitting it — nothing about
the deploy call changes whether you write Bicep or JSON directly. The outputs block
surfaces blobEndpoint back to whoever ran the deployment, useful for chaining into a
script without a separate az storage account show call.
A deployment that fails validation
Change the storage account name to something that violates Azure’s naming rule (lowercase alphanumeric only, no hyphens):
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'learn-bicep-482'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
az deployment group create \
--resource-group learn-rg \
--template-file main.bicep \
--parameters storageAccountName=learn-bicep-482
$ az deployment group create --resource-group learn-rg --template-file main.bicep --parameters storageAccountName=learn-bicep-482
ERROR: {"code": "InvalidTemplateDeployment", "message": "The template deployment
'main' is not valid according to the validation procedure. The tracked resource types
require a valid location: 'storageAccounts'."}
{"error": {"code": "InvalidResourceName", "message": "The storage account name is invalid.
Field 'accountName' must be between 3 and 24 characters in length and use numbers and
lower-case letters only.", "target": "learn-bicep-482"}}
Two error levels here: InvalidTemplateDeployment is Resource Manager’s generic wrapper,
and the nested InvalidResourceName is the actual, specific reason — always read to the
innermost message first, since the outer one repeats for every kind of validation failure
regardless of cause.
Previewing changes before applying them
az deployment group what-if \
--resource-group learn-rg \
--template-file main.bicep \
--parameters storageAccountName=learnbicep482
$ az deployment group what-if --resource-group learn-rg --template-file main.bicep --parameters storageAccountName=learnbicep482
Resource and property changes are indicated with these symbols:
= Nomodify
The deployment will update the following scope:
Scope: /subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/resourceGroups/learn-rg
= Microsoft.Storage/storageAccounts/learnbicep482
Resource changes: 1 no change, 0 to create, 0 to delete, 0 to modify.
what-if runs the same validation and dependency resolution as a real deployment but stops
before provisioning — re-running the original template shows “no change” because Bicep
deployments are idempotent against matching existing state, not because nothing was checked.
Practice
1. Compile the Bicep file to ARM JSON and look at what it actually produces.
az bicep build --file main.bicep --stdout
$ az bicep build --file main.bicep --stdout
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccountName": { "type": "string" }
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[parameters('storageAccountName')]"
}
]
}
This is the file Azure Resource Manager actually receives regardless of authoring language — useful for confirming exactly what a Bicep construct compiles to when the behavior isn’t obvious from the Bicep source alone.
2. List past deployments to the resource group, including the failed one.
az deployment group list --resource-group learn-rg --query "[].{Name:name, State:properties.provisioningState}" --output table
$ az deployment group list --resource-group learn-rg --query "[].{Name:name, State:properties.provisioningState}" --output table
Name State
------ -------
main Succeeded
Only one entry — a deployment that fails validation before provisioning starts (like the naming error above) is often rejected client-side or fails so early it isn’t recorded as a distinct deployment operation, unlike a deployment that fails mid-provisioning.
3. Add a second resource — a blob container — that depends on the storage account, using implicit dependency via reference.
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount
name: 'default'
}
resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobService
name: 'reports'
}
$ az deployment group create --resource-group learn-rg --template-file main.bicep --parameters storageAccountName=learnbicep482
{
"properties": { "provisioningState": "Succeeded" }
}
parent: establishes both the nested resource name (learnbicep482/default/reports) and
an implicit deployment-order dependency — Bicep won’t try to create the container before the
storage account exists, without an explicit dependsOn.
4. Validate a template without deploying or previewing changes, just checking it's well-formed.
az deployment group validate \
--resource-group learn-rg \
--template-file main.bicep \
--parameters storageAccountName=learnbicep482
$ az deployment group validate --resource-group learn-rg --template-file main.bicep --parameters storageAccountName=learnbicep482
{
"error": null,
"properties": {
"provisioningState": "Succeeded"
}
}
validate is faster than what-if because it skips the state comparison — good for a CI
step that just needs to know “does this template even parse and pass schema checks.”
5. Delete the resources this lesson created, by deploying an empty template rather than deleting them manually.
az deployment group create \
--resource-group learn-rg \
--mode Complete \
--template-file empty.bicep \
--confirm-with-what-if
$ az deployment group create --resource-group learn-rg --mode Complete --template-file empty.bicep --confirm-with-what-if
Resource and property changes are indicated with these symbols:
- Delete
- Microsoft.Storage/storageAccounts/learnbicep482
Are you sure you want to execute the deployment? (y/n): y
{
"properties": { "provisioningState": "Succeeded" }
}
--mode Complete makes the deployment authoritative — anything in the resource group not
declared in the template gets deleted, which is why --confirm-with-what-if is worth
using here: the default Incremental mode never deletes resources that aren’t mentioned.
Next: Azure Monitor — metrics, a Log Analytics KQL query, and setting up a basic alert rule.