Skip to main content
Azure · lesson 8 of 10
Azure intermediate 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

azure bicep arm-templates infrastructure-as-code

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.

Frequently Asked Questions

What is the relationship between Bicep and ARM templates?
ARM (Azure Resource Manager) templates are JSON documents that describe Azure resources declaratively; Bicep is a newer, more concise language that compiles down to that same ARM JSON. Every Bicep file has an ARM JSON equivalent (you can generate it with az bicep build), and Azure Resource Manager only ever actually deploys the JSON — Bicep is purely an authoring-time convenience.
What does az deployment group create actually check before creating anything?
It validates the template's syntax and resource schema, resolves parameters and dependencies, and runs a preflight check against Azure Resource Manager before provisioning starts. A resource whose properties don't match its type's schema, or a naming rule violation (like an invalid storage account name), is caught and reported before any resource is actually created.
Is a Bicep deployment all-or-nothing?
Not by resource, but by validation stage — if the template fails validation, nothing is created. Once deployment starts, though, resources are provisioned independently, so a template with three resources where the third fails can leave the first two created and the third missing; re-running the same deployment is safe and idempotent, since Resource Manager treats a matching existing resource as already satisfied.
What's the difference between a deployment at resource-group scope and one at subscription scope?
az deployment group create targets a single existing resource group and deploys resources into it. az deployment sub create targets an entire subscription and can create resource groups themselves as part of the deployment, which resource-group-scoped deployments cannot do. Most day-to-day infrastructure work uses resource-group scope.