The first group in the AZ-104 compute domain is "automate deployment of resources by using ARM templates or Bicep files." Its five sub-items, "interpret an ARM template or a Bicep file," "modify an existing ARM template," "modify an existing Bicep file," "deploy resources," and "export a deployment as an ARM template or convert an ARM template to a Bicep file," all test whether you can read and edit code yourself. The real exam shows part of a template and asks things like "What do you add to create three storage accounts?" or "What happens to existing resources when this deployment runs?"
This article puts the same storage account side by side in ARM JSON and Bicep and reads it line by line, so that administrators without a development background can follow templates. It covers template sections and common functions, loops, conditions, and dependencies, parameter files and Key Vault references, deployment scopes and modes, and exporting and converting. What ARM templates and Bicep are was introduced first in Virtual Machines and Deployment Automation.
---
A Template Is a Form With Fixed Fields
A government application form has fixed places for the fields the applicant fills in, the fields a clerk calculates, the actual request, and the receipt you get after processing. An ARM template is likewise a JSON document made of fixed sections.
| Section | Role | On a paper form | |------|------|------| | $schema | Template language version and deployment scope | Form number | | contentVersion | Version of the template itself | Document revision number | | parameters | Values supplied from outside at deployment time | Fields the applicant fills in | | variables | Values computed inside the template and reused | Fields the clerk calculates | | resources | Resources to create or update | The actual request | | outputs | Values returned after deployment | The receipt |
Here is an ARM template that creates one storage account.
Template Functions You'll See Often
A value inside square brackets is an expression evaluated at deployment time.
| Function | Result | Typical use | |------|------|------| | resourceGroup().location | The region of the target resource group | Default for location | | parameters('name') · variables('name') | A parameter or variable value | Reusing values | | concat() · format() | Joins strings | Building names | | uniqueString(resourceGroup().id) | A 13-character hash that's stable per resource group | Names that must be globally unique | | resourceId('type', 'name') | A resource's full ID | Referencing other resources, outputs | | copyIndex() | The current iteration number in a loop | Names of looped resources |
Exam tip: "If no value is provided, deploy to the same region as the resource group" means using resourceGroup().location as the defaultValue. "Accept only approved SKUs" means allowedValues.
---
Reading the Same Thing in Bicep
Bicep is a language that expresses ARM JSON in a shorter, more readable way, and it's converted to ARM JSON at deployment. The template above looks like this in Bicep.
| ARM JSON | Bicep | What to remember | |------|------|------| | parameters + defaultValue | param name type = default | The default comes after an equals sign | | allowedValues · minLength | @allowed · @minLength decorators | Placed above the parameter | | securestring | @secure() param | Passwords and keys must be secure parameters | | variables | var | No type declaration | | type + apiVersion | 'type@apiVersion' | Combined into one string | | dependsOn | Mostly automatic (symbolic name references) | Referencing something like stg.id creates the dependency | | copy | [for ... in ...: {...}] | Loop expression | | condition | if (condition) | Conditional deployment | | outputs | output | Type declaration required |
Exam tip: In Bicep questions, an option like "add dependsOn so the subnet is referenced after the VNet is created" is usually unnecessary. When you reference a symbolic name (for example, vnet.id), Bicep creates the dependency automatically.
---
Loops, Conditions, and Dependencies: The Most Common Edits
Template edit questions are almost always one of three things: creating several copies of a resource, deciding whether to create something based on a condition, and controlling the order of creation.
To create three storage accounts in ARM JSON, add a copy property to the resource and append copyIndex() to the name.
In Bicep, use a for expression.
| Requirement | ARM JSON | Bicep | |------|------|------| | N copies of the same resource | copy + count, copyIndex() | [for i in range(0, N): {...}] | | Deploy only when a condition is true | "condition": "[equals(parameters('env'), 'prod')]" | resource ... = if (env == 'prod') {...} | | Create the VM after the NIC | "dependsOn": [ "[resourceId('Microsoft.Network/networkInterfaces', 'nic1')]" ] | Automatic when you reference nic.id | | Reference a resource that already exists | reference() · resourceId() | resource vnet '...' existing = { name: 'vnet1' } |
Exam tip: copyIndex() starts at 0. To "name them stg1, stg2, stg3, starting from 1," pass a start value as in copyIndex(1).
---
Parameter Files and Secret Values
When you deploy the same template to dev and production with different values, keep a separate parameter file. Secret values such as passwords aren't written into the file; they reference a Key Vault secret instead.
The template's parameter must be securestring (@secure() in Bicep) so the value isn't kept in deployment history or logs The key vault must allow access for template deployment (enabledForTemplateDeployment) so the secret can be read during deployment Bicep does the same with a .bicepparam file that contains using './main.bicep' and param name = value lines
Exam tip: "Don't expose the VM administrator password in the template or the parameter file" means a Key Vault secret reference plus a securestring parameter. If a deployment fails with "can't access the key vault," check the key vault's setting that allows template deployment.
---
Deployment Scopes and Deployment Modes
The command depends on where you deploy the template.
| Deployment scope | Azure CLI | PowerShell | |------|------|------| | Resource group | az deployment group create --resource-group rg1 --template-file main.bicep | New-AzResourceGroupDeployment | | Subscription | az deployment sub create --location eastus --template-file main.bicep | New-AzSubscriptionDeployment | | Management group | az deployment mg create --management-group-id mg1 --location eastus | New-AzManagementGroupDeployment | | Tenant | az deployment tenant create --location eastus | New-AzTenantDeployment |
Resource group deployments have two modes. They differ in how they treat existing resources that aren't in the template.
!Incremental and complete modes treat resources that aren't in the template differently
| Mode | Existing resources not in the template | Resources in the template | |------|------|------| | Incremental (default, recommended) | Left unchanged | Created, or reapplied with the property values in the template | | Complete (not recommended) | Deleted | Created or reapplied |
Incremental mode has a trap too. When you redeploy an existing resource, its properties aren't "added to"; the resource is reapplied as the template defines it, so properties missing from the template can be overwritten with defaults. Microsoft also doesn't recommend complete mode and plans to deprecate it gradually; if a deployment needs to delete resources, use deployment stacks instead. Running what-if (az deployment group what-if) before deploying shows what will be created, changed, and deleted.
Exam tip: If "RG1 contains VM1 and Storage1, and a template that defines only VNet1 is deployed in complete mode," VM1 and Storage1 are deleted and only VNet1 remains. Deploying the same template in incremental mode keeps all three.
---
Exporting and Converting
When you want a template for resources you already created in the portal, use export. The result differs depending on the method.
| Method | Output | Characteristics | |------|------|------| | Export template from a resource group or resource | Bicep or ARM JSON | A snapshot of the current state, including changes made after deployment; many properties, few parameters | | Save from deployment history | ARM JSON only | The exact template used for the deployment, with parameters; only that deployment's resources | | az bicep decompile --file main.json | ARM JSON → Bicep | A best-effort conversion that may need cleanup | | az bicep build --file main.bicep | Bicep → ARM JSON | Check the converted result before deploying |
Exam tip: "You need a template that includes settings changed in the portal after deployment" means exporting from the resource group. "You want to reuse the original template and parameters from the first deployment" means saving from deployment history. "Convert an existing ARM template to Bicep" means decompile.
---
Exam Quick Reference
Key points to check one last time before the exam.
| Topic | What to remember | |------|------| | Template sections | $schema · contentVersion · parameters · variables · resources · outputs | | Default region | resourceGroup().location | | Unique names | uniqueString(resourceGroup().id) | | Input limits | allowedValues · minLength (@allowed · @minLength in Bicep) | | Secret values | securestring · @secure() + Key Vault reference (enabledForTemplateDeployment) | | Loops | ARM copy + copyIndex() (starts at 0), Bicep for expression | | Conditions | ARM condition, Bicep if | | Dependencies | ARM dependsOn; Bicep creates them automatically from symbolic name references | | Deployment modes | Incremental (default) leaves others alone vs complete deletes resources not in the template | | Complete mode | Not recommended; use deployment stacks when deletion is needed | | what-if | Preview changes before depl