Chapter 4
ARM templates & Bicep
Clicking in the portal is fine for learning, but real environments are deployed as code. The exam expects you to read a template, fix one, deploy one and export one. In the lab you extend the network from chapter 3 with Bicep.
Theory
Azure Resource Manager
Every way of managing Azure (the portal, CLI, PowerShell, Terraform, templates) ends up as a call to the same API: Azure Resource Manager (ARM). ARM authenticates you, checks RBAC, Policy and locks, and then passes the request to a resource provider such as Microsoft.Network.
ARM templates and Bicep files describe the end state you want ("a VNet with these subnets"), not the steps to get there. That makes them:
- Declarative: you say what, ARM figures out how and in what order.
- Idempotent: deploying the same template twice gives the same result. The second run changes nothing.
- Parallel: ARM deploys resources that don't depend on each other at the same time.
ARM template structure (JSON)
An ARM template is a JSON file with up to six top-level sections. Only $schema, contentVersion and resources are required:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageName": { "type": "string", "minLength": 3, "maxLength": 24 },
"sku": { "type": "string", "defaultValue": "Standard_LRS",
"allowedValues": [ "Standard_LRS", "Standard_ZRS" ] }
},
"variables": {
"location": "[resourceGroup().location]"
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-05-01",
"name": "[parameters('storageName')]",
"location": "[variables('location')]",
"sku": { "name": "[parameters('sku')]" },
"kind": "StorageV2",
"properties": { "minimumTlsVersion": "TLS1_2" }
}
],
"outputs": {
"blobEndpoint": { "type": "string",
"value": "[reference(parameters('storageName')).primaryEndpoints.blob]" }
}
}
What each part does:
- parameters: values you pass in at deploy time. They can have a
defaultValue,allowedValues, length or value limits. Use the typesecurestringfor passwords so they aren't logged. - variables: values calculated inside the template, to avoid repeating yourself.
- resources: what to deploy. Each has a
type, anapiVersion, anameand usuallylocationandproperties. - outputs: values returned after deployment, such as an endpoint or an ID.
- functions (rare): your own template functions.
Anything between [ ] is an expression. Functions you should recognise: parameters(), variables(), resourceGroup().location, concat(), uniqueString() (a deterministic hash, great for globally unique names), resourceId() and reference().
In JSON, if resource B needs resource A, you must say so with "dependsOn": [ "[resourceId(...)]" ]. Otherwise ARM may try to create them at the same time and fail.
Parameter values usually live in a separate parameter file (main.parameters.json), so the same template can deploy dev, test and prod.
The same thing in Bicep
@minLength(3)
@maxLength(24)
param storageName string
@allowed([ 'Standard_LRS', 'Standard_ZRS' ])
param sku string = 'Standard_LRS'
param location string = resourceGroup().location
resource st 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageName
location: location
sku: { name: sku }
kind: 'StorageV2'
properties: { minimumTlsVersion: 'TLS1_2' }
}
output blobEndpoint string = st.properties.primaryEndpoints.blob
Bicep does exactly what the JSON does. The differences that matter:
- Resource declarations look like
resource <symbolic name> '<type>@<apiVersion>' = { … }. The symbolic name (st) is only used inside the file; the real Azure name is thenameproperty. - Implicit dependencies: when you refer to
st.idorst.properties…in another resource, Bicep adds thedependsOnfor you. - Decorators like
@allowed,@minLength,@secure()and@descriptionreplace the JSON parameter properties. - String interpolation:
'st${uniqueString(resourceGroup().id)}'instead ofconcat(). - existing:
resource vnet '…' existing = { name: 'vnet-spoke' }refers to something already deployed without changing it. - Child resources use
parent: vnet(or are nested inside the parent). - Loops (
[for i in range(0, 3): { … }]) and conditions (= if (deployBastion) { … }). - Modules:
module net './network.bicep' = { name: 'net', params: { … } }to split big deployments into reusable files. - Parameter files can be written as
.bicepparamfiles.
The exam shows you a template and asks "how many resources will be created?" or "in which region?". Read the copy loop / for count, the if condition and the default value of location carefully. resourceGroup().location means "the resource group's region", not the region you happen to be in.
Deploying
A deployment targets one of four scopes, and the CLI command tells you which:
| Scope | Azure CLI | PowerShell | Typical content |
|---|---|---|---|
| Resource group | az deployment group create | New-AzResourceGroupDeployment | VNets, VMs, storage |
| Subscription | az deployment sub create | New-AzSubscriptionDeployment | Resource groups, policy, RBAC, budgets |
| Management group | az deployment mg create | New-AzManagementGroupDeployment | Policy, RBAC |
| Tenant | az deployment tenant create | New-AzTenantDeployment | Management groups |
Incremental vs Complete mode
Resource-group deployments have two modes. This is a favourite exam topic:
- Incremental (the default): resources in the template are created or updated. Resources in the resource group that aren't in the template are left alone.
- Complete: resources in the resource group that aren't in the template are deleted. Dangerous, and only available at resource-group scope. (Locked resources aren't deleted.)
what-if before any Complete deployment.There's a subtle trap even in Incremental mode: a resource's properties are replaced as a whole. If you redeploy a VNet template whose subnets array lists only one subnet, ARM tries to remove the other subnets from the VNet (it fails if they're in use). That's why the lab adds a subnet as a separate child resource on an existing VNet.
What-if and deployment history
What-if (az deployment group what-if, or create --confirm-with-what-if) shows what would be created, modified or deleted, without changing anything. Use it every time.
Every deployment is recorded under the resource group's Deployments blade, including portal deployments. There you can see the inputs, outputs, errors and the exact template that was used. Azure keeps up to 800 deployments per resource group and cleans up old ones automatically.
Exporting and converting
You can get a template out of Azure in two ways, and the exam asks about the difference:
- Export from a resource group or resource (Automation → Export template): a snapshot of the current state of the resources. It includes many default properties, hard-coded values and sometimes resources that can't be redeployed as-is. Not all resource types support export. The portal shows ARM JSON and Bicep.
- Export from deployment history: the exact template that a past deployment used. Cleaner, but it doesn't include later changes made outside that deployment.
Converting between formats:
- JSON → Bicep:
az bicep decompile --file main.json. A best-effort conversion; you usually tidy the result. - Bicep → JSON:
az bicep build --file main.bicep.
In AZ-305, infrastructure as code shows up in governance and landing zone designs: deploy policies, RBAC and management groups from code, use template specs or a module registry to share approved building blocks, and deployment stacks to manage and protect a group of resources as one unit.
Self-check
1. A resource group contains VM1, VM2 and a VNet. You deploy a template containing only the VNet in Complete mode. What happens?
B. Complete mode deletes resources that aren't in the template (unless they're locked).
2. Which Bicep feature lets you use a VNet that was deployed earlier, without redeploying it?
D. existing references a resource without managing it.
3. You need the exact template used for last week's deployment of a web app. Where do you get it?
A. "Export template" on the group gives the current state, not the original template.
4. Which command deploys a Bicep file that creates resource groups?
C. Resource groups live in a subscription, so it's a subscription-scope deployment.
Lab: extend the network with code
You read an exported template, add snet-app and nsg-web with Bicep (both are used later), and fix an ARM template that creates the Log Analytics workspace for chapter 9. Work in a terminal on your Linux VM; use nano (or any editor you like) to create files. Paste the variables first:
RG=rg-az104-lab; LOC=westeurope; LOC2=northeurope; INIT=xx
mkdir -p ~/az104 && cd ~/az104
-
Task
Read an exported template
Export the template of
vnet-spokeand answer these questions from the template, not from the portal: which parameters did the export create, where issnet-webdefined (and is it defined more than once?), and how is the route table linked?Done when- You can answer the three questions.
- You have the template as Bicep too.
Solution · Portal
vnet-spoke→ Automation → Export template. Switch between the ARM template and Bicep tabs. Exports usually create one parameter per referenced resource name (the VNet, the route table).snet-webshows up twice: inside the VNet'ssubnetsarray and as a separatevirtualNetworks/subnetsresource. The route table is linked with arouteTable.idon the subnet.Solution · Azure CLI
VNETID=$(az network vnet show -g $RG -n vnet-spoke --query id -o tsv) az group export -n $RG --resource-ids $VNETID > spoke.json jq '.parameters | keys' spoke.json jq '.resources[] | {type, name}' spoke.json az bicep decompile --file spoke.json # creates spoke.bicep cat spoke.bicep -
Task
Write a Bicep file that adds two resources
Write
network-add.bicepthat creates:- Subnet
snet-app10.20.3.0/24in the existingvnet-spoke, delegated toMicrosoft.Web/serverFarms(for App Service in chapter 8), as a private subnet. - An empty network security group
nsg-webin the resource group's region (you add rules in chapter 6).
The file must not redefine the VNet itself. Run a what-if before deploying.
Done when- What-if shows exactly two creates and nothing deleted or modified.
- After deploying,
vnet-spokehas three subnets andsnet-appshows the delegation.
Hint
Declare the VNet with
existing, then the subnet as typeMicrosoft.Network/virtualNetworks/subnetswithparent: vnet.Solution · Bicep file
param location string = resourceGroup().location resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { name: 'vnet-spoke' } resource snetApp 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { parent: vnet name: 'snet-app' properties: { addressPrefix: '10.20.3.0/24' defaultOutboundAccess: false delegations: [ { name: 'appservice' properties: { serviceName: 'Microsoft.Web/serverFarms' } } ] } } resource nsgWeb 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { name: 'nsg-web' location: location } output subnetId string = snetApp.idSolution · Azure CLI
nano network-add.bicep # paste the file, save with Ctrl+O, Enter, exit with Ctrl+X az deployment group what-if -g $RG -f network-add.bicep az deployment group create -g $RG -f network-add.bicep -n network-add az network vnet subnet show -g $RG --vnet-name vnet-spoke -n snet-app --query "delegations[].serviceName" - Subnet
-
Task
Fix a broken ARM template
Save the template below as
law.jsonand deploy it torg-az104-lab. It will fail. Find out why, then change the template so that:- the location is a parameter that defaults to the resource group's region;
- the retention is a parameter, default 30 days, only accepting values from 30 to 730;
and deploy it again.
{ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "workspaceName": { "type": "string", "defaultValue": "log-az104" } }, "resources": [ { "type": "Microsoft.OperationalInsights/workspaces", "apiVersion": "2023-09-01", "name": "[parameters('workspaceName')]", "location": "eastus", "properties": { "sku": { "name": "PerGB2018" }, "retentionInDays": 90 } } ], "outputs": { "workspaceId": { "type": "string", "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspaceName'))]" } } }Done when- You know why the first deployment failed.
- Workspace
log-az104exists in your lab region with 30-day retention. (Chapter 9 uses it.)
Hint
The error code is
RequestDisallowedByPolicy. Which policy did you assign in chapter 2? For the limits, useminValueandmaxValue.Solution · fixed template
{ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "workspaceName": { "type": "string", "defaultValue": "log-az104" }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]" }, "retentionInDays": { "type": "int", "defaultValue": 30, "minValue": 30, "maxValue": 730 } }, "resources": [ { "type": "Microsoft.OperationalInsights/workspaces", "apiVersion": "2023-09-01", "name": "[parameters('workspaceName')]", "location": "[parameters('location')]", "properties": { "sku": { "name": "PerGB2018" }, "retentionInDays": "[parameters('retentionInDays')]" } } ], "outputs": { "workspaceId": { "type": "string", "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspaceName'))]" } } }The first attempt failed because
eastusisn't allowed by your Allowed locations policy.Solution · Azure CLI
nano law.json az deployment group create -g $RG -f law.json -n law # 1st time: RequestDisallowedByPolicy # fix the file, then: az deployment group create -g $RG -f law.json -n law az monitor log-analytics workspace show -g $RG -n log-az104 --query "{loc:location, days:retentionInDays}"Portal alternative: Create a resource → Template deployment (deploy using custom templates) → Build your own template in the editor, paste, Save, Review + create.
-
Task
See what Complete mode would destroy, without destroying anything
Find out what would happen if you deployed
network-add.biceptorg-az104-labin Complete mode. Do not actually deploy it in Complete mode. Then find the deployment history of this resource group and the template of yourlawdeployment.Done when- You've seen the list of resources that would be deleted. Which of them is protected by a lock?
- You found the
lawdeployment in the history.
Solution · Azure CLI
az deployment group what-if -g $RG -f network-add.bicep --mode Complete az deployment group list -g $RG --query "[].{name:name, state:properties.provisioningState}" -o tableWhat-if shows almost everything as Delete, because none of it is in the file. What-if doesn't look at locks:
vnet-hubhas the CanNotDelete lock from chapter 3, so ARM couldn't delete it in a real run.Solution · Portal
rg-az104-lab→ Settings → Deployments →law→ Template. (What-if is CLI/PowerShell only.)
vnet-spokenow hassnet-web,snet-dataandsnet-app(delegated to App Service).nsg-webexists (no rules, not associated yet: chapter 6).- Log Analytics workspace
log-az104exists (used in chapter 9).
Log Analytics only costs money for data you send to it, so nothing yet.