AZ-104
Deep Dive
ARM Templates and Bicep are Azure's native tools for Infrastructure as Code. The AZ-104 tests template structure, deployment modes, key functions, and the difference between Complete and Incremental — a frequent trap.
Contents
Azure offers several options for IaC. The AZ-104 focuses on Azure's native tools.
| Tool | Format | Native to Azure? | When to use |
|---|---|---|---|
| ARM Templates | JSON | ✅ Yes | Historical standard. Verbose but complete. Appears on the AZ-104 exam. |
| Bicep | Bicep (DSL) | ✅ Yes (transpiles to ARM) | Simplified ARM syntax. Microsoft recommends it over JSON. |
| Terraform | HCL | ❌ Third-party | Multi-cloud. Not tested on AZ-104 but common in enterprises. |
| Azure CLI / PowerShell | Imperative | ✅ Yes | Automation scripts. Not declarative — does not manage state. |
An ARM template is a JSON file that describes the resources to deploy. It has required and optional sections. Only $schema, contentVersionand resources are required.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": { // Configurable values at deploy time
"storageAccountName": {
"type": "string",
"defaultValue": "mystorage",
"minLength": 3,
"maxLength": 24,
"metadata": { "description": "Storage account name" }
},
"location": {
"type": "string",
"defaultValue": "[resourceGroup().location]"
}
},
"variables": { // Internally computed values
"storageAccountId": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
},
"functions": [], // UDF functions (rare on the exam)
"resources": [ // Resources to deploy (required)
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[parameters('storageAccountName')]",
"location": "[parameters('location')]",
"sku": { "name": "Standard_LRS" },
"kind": "StorageV2",
"properties": {
"supportsHttpsTrafficOnly": true
},
"dependsOn": [] // Explicit dependencies
}
],
"outputs": { // Values returned after the deployment
"storageEndpoint": {
"type": "string",
"value": "[reference(parameters('storageAccountName')).primaryEndpoints.blob]"
}
}
}$schema✅URL of the JSON schema that validates the template. Different for subscription vs RG scope.
contentVersion✅Semantic version of the template. Convention: "1.0.0.0". Has no automatic functionality.
resources✅Array of resources to create. Can be empty [] but must exist.
parameters❌Configurable values at deploy time. Can have defaultValue, allowedValues, min/maxLength.
variables❌Internally computed values. Not configurable from outside — only useful within the template.
outputs❌Values returned when the deployment finishes. Useful for passing data between templates (linked templates).
ARM functions are used inside [...] expressions to build values dynamically. They are essential for reusable templates.
| Function | Use | Example |
|---|---|---|
| parameters('name') | Accesses the value of a parameter | [parameters('location')] |
| variables('name') | Accesses the value of a variable | [variables('storageId')] |
| resourceGroup() | Properties of the current RG (name, location, id) | [resourceGroup().location] |
| subscription() | Properties of the subscription (id, tenantId, etc.) | [subscription().subscriptionId] |
| resourceId(type, name) | Unique ID of an Azure resource | [resourceId('Microsoft.Storage/storageAccounts', 'mystorage')] |
| reference(name) | Runtime properties of a deployed resource | [reference('mystorage').primaryEndpoints.blob] |
| concat(a, b, c) | Concatenates strings or arrays | [concat('storage', uniqueString(resourceGroup().id))] |
| uniqueString(seed) | Generates a unique 13-char string based on a seed | [uniqueString(resourceGroup().id)] |
| toLower(str) | Converts a string to lowercase | [toLower(parameters('name'))] |
| format(template, args) | Formats a string with values | [format('{0}-storage', parameters('env'))] |
| if(cond, true, false) | Ternary conditional | [if(equals(parameters('env'), 'prod'), 'Premium_LRS', 'Standard_LRS')] |
| copyIndex() | Current index in a copy loop | [copyIndex()] |
The deployment mode determines how Azure handles resources that already exist in the RG but are NOT in the template. This is the most frequent trap in the AZ-104 regarding ARM Templates.
Incremental (default)
--mode Incremental (default)
Complete
--mode Complete
Exam trap example
You have an RG with VM-A, VM-B and StorageAccount-1. You deploy a template in Complete mode that only defines VM-A. Result: VM-B and StorageAccount-1 are DELETED automatically.
Nested Templates
The child resource's template is embedded inline within the parent template under properties.template. Does not require an accessible URL. Useful for small templates or when you have no external storage.
Linked Templates
The child template lives at an accessible URL (Storage Account, GitHub). It is referenced via properties.templateLink.uri. Enables modular, reusable templates. Requires the URL to be accessible during the deployment.
// Reference to a linked template
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2022-09-01",
"name": "storageDeployment",
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "https://mystorageaccount.blob.core.windows.net/templates/storage.json",
"contentVersion": "1.0.0.0"
},
"parameters": {
"storageAccountName": { "value": "[parameters('storageName')]" }
}
}
}Bicep is a DSL (Domain Specific Language) that compiles to ARM JSON. Same API, same resources — just simpler syntax. Azure CLI and the Portal can deploy Bicep directly without pre-compiling.
| Concept | ARM JSON | Bicep |
|---|---|---|
| Parameter | "parameters": { "name": { "type": "string" } } | param name string |
| Variable | "variables": { "id": "value" } | var id = 'value' |
| Resource | "type": "Microsoft.Storage/...", "name": "mystorage" | resource mystorage 'Microsoft.Storage/storageAccounts@2023-01-01' |
| Output | "outputs": { "ep": { "type": "string", "value": "..." } } | output ep string = mystorage.properties.primaryEndpoints.blob |
| Reference | [reference('name').property] | existingResource.properties.property |
| Module (=linked) | Microsoft.Resources/deployments + templateLink | module storage './storage.bicep' = { ... } |
| Loop | "copy": { "name": "loop", "count": 3 } | resource vm 'Microsoft.Compute/virtualMachines@...' = [for i in range(0,3): {...}] |
| Conditional | "condition": "[equals(param, 'prod')]" | resource vm '...' = if (env == 'prod') { ... } |
// Bicep equivalent of the ARM JSON from the earlier example
param storageAccountName string = 'mystorage'
param location string = resourceGroup().location
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
properties: {
supportsHttpsTrafficOnly: true
}
}
output storageEndpoint string = storageAccount.properties.primaryEndpoints.blob// Azure CLI — deployments at different scopes
# Resource Group scope (most common on the AZ-104) az deployment group create \ --resource-group myRG \ --template-file main.bicep \ --parameters storageAccountName=mystorage \ --mode Incremental # Subscription scope az deployment sub create \ --location westeurope \ --template-file main.bicep # Management Group scope az deployment mg create \ --management-group-id myMG \ --location westeurope \ --template-file main.bicep # Tenant scope az deployment tenant create \ --location westeurope \ --template-file main.bicep
// Equivalent PowerShell
# Resource Group scope New-AzResourceGroupDeployment ` -ResourceGroupName myRG ` -TemplateFile main.bicep ` -storageAccountName mystorage ` -Mode Incremental # Subscription scope New-AzDeployment ` -Location westeurope ` -TemplateFile main.bicep
What-if — preview changes
Shows which resources would be created, modified or deleted WITHOUT running the deployment. Essential for validating before a Complete-mode deploy.
az deployment group what-if \
--resource-group myRG \
--template-file main.bicep
+ Create: VM-new (new)
~ Modify: StorageAccount-1 (changes)
- Delete: OldVM (in Complete mode)
= NoChange: VNet-1 (no changes)
Deployment Stacks
Manages the lifecycle of deployed resources as a group. Lets you create, update and delete the entire stack as a unit. Avoids orphaned resources when a template is removed.
Complete mode deletes resources that are NOT in the template
This is the most frequent trap regarding ARM. If you deploy a template in Complete mode that does not include every resource in the RG, the missing resources are DELETED. By default the mode is Incremental (it does not delete anything that is not in the template).
Bicep transpiles to ARM — same API, same resources
Bicep is NOT a separate service and does not have its own deployment engine. It compiles to ARM JSON and Azure deploys the resulting ARM JSON. The difference is only syntax. The AZ-104 may ask whether Bicep supports a certain ARM feature — yes, because the end result is identical.
dependsOn is needed for implicit dependencies that cannot be detected
ARM automatically detects dependencies when you use reference() or resourceId() in properties. However, if the dependency is external (e.g. a script that waits for a DB to be ready), you must use an explicit dependsOn with the resource name.
parameters() does NOT work inside variables in the "variables" section
False — it does work. Variables can reference parameters. What a variable cannot do is reference another variable that already depends on it (circular reference). parameters() inside variables is a legitimate and common pattern.
Linked templates require an accessible URL during the deployment
The ARM engine downloads the linked template while the deployment is running. If the file is in a private Storage Account, the deployment will fail. Solution: use a temporary SAS URL or host it on public GitHub / Template Specs.
Did you get this topic?
Test what you just learned
An administrator deploys an ARM template in Complete mode onto a Resource Group that contains: VM-A, VM-B, VNet-1 and StorageAccount-1. The template only defines VM-A and VNet-1. What is the result?
Put what you learned to the test with original AZ-104 exam questions.
Start AZ-104 mock examUnlimited mock exams with the Pro plan
Log in to track your progress.