D3 · Compute

ARM Templates and Bicep

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.

Infrastructure as Code in Azure

Azure offers several options for IaC. The AZ-104 focuses on Azure's native tools.

ToolFormatNative to Azure?When to use
ARM TemplatesJSON✅ YesHistorical standard. Verbose but complete. Appears on the AZ-104 exam.
BicepBicep (DSL)✅ Yes (transpiles to ARM)Simplified ARM syntax. Microsoft recommends it over JSON.
TerraformHCL❌ Third-partyMulti-cloud. Not tested on AZ-104 but common in enterprises.
Azure CLI / PowerShellImperative✅ YesAutomation scripts. Not declarative — does not manage state.
Icon-general-9

ARM Template Structure

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

ARM functions are used inside [...] expressions to build values dynamically. They are essential for reusable templates.

FunctionUseExample
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()]
Icon-general-7

Deployment Modes — Complete vs Incremental

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)

  • ✅Resources in the template → created or updated
  • ✅Resources in the RG but NOT in the template → left untouched
  • ⚠️Does not guarantee the final state matches the template exactly
  • ✅Safer: nothing is deleted unexpectedly

--mode Incremental (default)

Complete

  • ✅Resources in the template → created or updated
  • 🚫Resources in the RG but NOT in the template → DELETED
  • ✅Guarantees the RG matches what the template says (desired state)
  • ⚠️Dangerous if the template is not 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 and Linked Templates

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 — Syntax and Equivalences

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.

ConceptARM JSONBicep
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 + templateLinkmodule 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

Deployment Commands

// 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 and

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.

  • •Defines what happens to unmanaged resources: detach, delete, deleteAll
  • •Automatically applies deny assignments to protect the stack's resources
  • •Supports scope: RG, sub, MG

Exam Traps

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?

Ready to test "ARM Templates and Bicep"?

Put what you learned to the test with original AZ-104 exam questions.

Start AZ-104 mock exam

Unlimited mock exams with the Pro plan

Log in to track your progress.