ARM Templates and Bicep: AZ-104 Deployment Guide
Azure Resource Manager (ARM) templates and Bicep files are the two infrastructure-as-code formats Azure uses to deploy resources declaratively: you describe the end state, and Azure Resource Manager creates or updates resources to match it. An ARM template is a JSON document with a fixed set of top-level elements (schema, version, parameters, variables, resources, outputs). Bicep is a more concise language that transpiles to that same ARM JSON, so both formats reach the same deployment engine and cover the same resource types. As an Azure administrator you need to read either format, modify it safely, deploy it at the right scope with the right mode, preview the result first, and export or convert templates. This lesson covers template structure, symbolic references and dependencies, parameter files and secrets, loops, conditions and modules, Incremental versus Complete mode and the deployment stacks that replace Complete mode, what-if and validation, deployment scopes and commands, linked templates and template specs, and exporting and decompiling.
On this page11 sections
- Infrastructure as code with ARM and Bicep
- Interpreting an ARM template: the JSON structure
- Interpreting a Bicep file
- ARM template versus Bicep: a comparison
- Modifying an existing template or Bicep file
- Loops, conditions and modules in Bicep
- Deployment modes: Incremental versus Complete
- Previewing and validating a deployment: what-if, validate and lint
- Deployment scopes and deployment commands
- Sharing templates: linked templates and template specs
- Exporting a deployment and converting ARM to Bicep
- Identify the required and optional elements of an ARM template and trace a value from parameter to output
- Read a Bicep file, including symbolic references, existing resources, loops, conditions and modules
- Modify a template safely: parameter files, secure parameters, Key Vault references and redeploy behaviour
- Predict the effect of Incremental and Complete mode, and choose a deployment stack when deletion of removed resources is required
- Preview a deployment with what-if and distinguish it from validation and linting
- Deploy at the correct scope with the Azure CLI, Azure PowerShell or the portal, and share templates through template specs or modules
- Export a deployment as a template and convert ARM JSON to Bicep with decompile
Infrastructure as code with ARM and Bicep
Infrastructure as code means describing the Azure resources you want in a text file and letting Azure Resource Manager create them, rather than building each resource by hand in the portal. The file is declarative: you state the desired end result and Resource Manager works out the order and the API calls needed to reach it.
There are two authoring formats. An ARM template is a JSON file. A Bicep file is a purpose-built language that is easier to read and write; when you deploy it, the Bicep tooling transpiles it into an ordinary ARM template, so Resource Manager still receives JSON. Bicep is a different authoring language, not a different engine, and it has no state file of its own: the current state is whatever exists in Azure.
Both formats are idempotent: deploying the same unchanged file to the same target again succeeds and leaves resources that already match the template unchanged. That is why templates are safe to run on a schedule or in every pipeline run. Idempotent does not mean "never changes anything" — if someone changed a declared property by hand, the redeploy puts it back (see Modifying an existing template).
Interpreting an ARM template: the JSON structure
An ARM template is a JSON object whose top-level elements each have one job. Only three are required: $schema (the version of the template language), contentVersion (your own version stamp) and resources (what to deploy). Everything else is optional:
| Element | Required? | Job |
|---|---|---|
$schema | Yes | Points at the JSON schema for the template language and scope |
contentVersion | Yes | Your version of the template, for example 1.0.0.0 |
resources | Yes | The resources to create or update, each with type, apiVersion, name and usually location and properties |
parameters | No | Values supplied at deployment time (VM name, admin username, environment), so one template serves many environments |
variables | No | Values computed or reused inside the template, such as a name built from a parameter |
functions | No | User-defined template functions |
outputs | No | Values returned after deployment, such as an FQDN or resource ID, for a pipeline or another template |
Values are made dynamic with template expressions in square brackets, such as [parameters('vmName')], [resourceGroup().location] or [concat(variables('prefix'), 'stg')]. Reading a template means tracing a value from a parameter, through a variable, into a resource property and out through an output.
Runtime functions and where they are allowed
Some functions read a resource's runtime state: reference() (properties such as a storage account's endpoints) and the list*() functions (such as listKeys()). Their values only exist during the deployment, so they can be used only in resource properties and in outputs — not in variables, parameter defaults or resource names. Validation rejects a variable that calls reference(); the fix is to put the expression directly in the property and the output that need it.
A resource with a condition that evaluates to false is simply not deployed. Any reference() to it elsewhere (for example in an output) still runs and fails, because the resource does not exist. Guard it with the same test, for example [if(parameters('deployPublicIp'), reference(...).ipAddress, '')].
Resources in ARM JSON are ordered with dependsOn. If a property refers to another resource only through a resourceId() string, Resource Manager does not know about the dependency and may deploy the two in parallel; add dependsOn so the dependent waits.
Interpreting a Bicep file
A Bicep file declares the same parameters, variables, resources and outputs as an ARM template, with less ceremony. It still uses braces for objects, square brackets for arrays and single quotes for strings; the gain is that expressions are written plainly instead of inside bracketed JSON template expressions, and strings use interpolation — '${prefix}stg' instead of [concat(...)].
param location string = resourceGroup().location
@secure()
param adminPassword string
var storageName = '${uniqueString(resourceGroup().id)}stg'
resource stg 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageName
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}
output blobEndpoint string = stg.properties.primaryEndpoints.blobEvery resource has a symbolic name (stg above) used to refer to it elsewhere in the file. Referencing a symbolic name — stg.id, vnet1.properties.subnets[0].id — creates an implicit dependency, so Resource Manager deploys the referenced resource first. Building the same ID as a string with resourceId(...) creates no dependency; either switch to the symbolic reference or add an explicit dependsOn: [ vnet1 ].
To use a resource the deployment must not create or change, declare it with the existing keyword. If it lives elsewhere, give it a scope: resource hub 'Microsoft.Network/virtualNetworks@2024-05-01' existing with name: 'vnet-hub' and scope: resourceGroup('rg-network'). An existing declaration is read-only; it lets you read properties such as a subnet ID without redeploying the resource.
Outputs return values that only exist after deployment, such as a public IP's FQDN, to the caller or a pipeline. The editor tooling adds type checking and IntelliSense, so many mistakes surface before you deploy.
ARM template versus Bicep: a comparison
Both formats deploy the same resources through Azure Resource Manager, so the choice is about authoring experience, not capability.
| Aspect | ARM template (JSON) | Bicep file |
|---|---|---|
| Syntax | JSON; logic lives in bracketed expressions such as [concat(...)] | Braces, brackets and single-quoted strings, with plain expressions and ${} string interpolation |
| Dependencies | Explicit dependsOn | Inferred from symbolic references; dependsOn still available |
| Reuse | Linked templates fetched from a URI, or nested templates | Modules, compiled into one template; no hosted URIs needed |
| Engine and coverage | Resource Manager | Same: transpiled to ARM JSON, same resource types and API versions |
| State | None (Azure is the state) | None (Azure is the state) |
| File extension | .json | .bicep (parameters in .bicepparam) |
Anything you can express in an ARM template you can express in Bicep. Microsoft recommends Bicep for new work while ARM JSON stays fully supported, and you can convert between them (see the last section).
Modifying an existing template or Bicep file
Modifying a template usually means changing a value, adding a resource, or redeploying an edited file — and each has a behaviour you need to predict.
Changing values: parameter files
Keep one template and supply environment-specific values separately instead of editing the template per environment. ARM templates use a JSON parameter file (for example azuredeploy.parameters.json); Bicep can use the same JSON format or a .bicepparam file whose using statement names the Bicep file. One parameter file per environment, passed at deployment time, is the least-effort pattern; individual values can also be overridden on the command line (--parameters vmSize=Standard_D4s_v5).
Secrets
- @secure() (or
securestringin ARM JSON) is what keeps a parameter's value out of the deployment history and logs. Supplying the value from a parameter file or adding@description()does not hide it. - Key Vault reference in a JSON parameter file: the parameter value is a
referenceto the vault's resource ID and secret name. It works only if the vault hasenabledForTemplateDeploymentset to true (off by default) and the identity running the deployment holdsMicrosoft.KeyVault/vaults/deploy/actionon the vault (Owner and Contributor include it). The vault can be in another resource group or subscription, and no access policy or data-plane role on the secret is needed. - In Bicep,
keyVault.getSecret('name')on anexistingvault can be used only to pass a value to a module parameter marked@secure()— not directly in a resource property. In a.bicepparamfile, useaz.getSecret(subscriptionId, resourceGroup, vaultName, secretName). Both rely on the same vault setting and permission.
Adding a resource
Add an entry to the resources array in ARM JSON (with dependsOn where needed), or a new resource block in Bicep that references other resources symbolically.
What a redeploy does to existing resources
When Resource Manager redeploys a resource that is in the template, it reapplies every property the template declares — it does not merge with what is in Azure. Manual drift is reverted: a VM resized in the portal goes back to the size in the file on the next pipeline run. Properties you leave out of a redeployed resource can be reset, which matters most for virtual networks. If subnets are declared as separate virtualNetworks/subnets child resources and the network itself has no subnets property, each redeploy of the network asks Resource Manager to remove its subnets, and once NICs use them the deployment fails because an in-use subnet cannot be deleted. Declare subnets inside the virtual network's subnets property instead.
Loops, conditions and modules in Bicep
Bicep repeats, skips and reuses resources with loops, conditions and modules, so one file can describe many similar resources.
- Loops:
[for i in range(0, vmCount): { name: 'vm${i}' ... }]deploys one copy per index; changevmCountto change the count. You can also loop over an array:[for app in appList: { ... }]. ARM JSON does the same with thecopyelement. - Filtered loops:
[for app in appList: if (app.enabled) { ... }]skips items whose condition is false. - Batching: loop copies deploy in parallel by default. The
@batchSize(n)decorator on the resource deploys them n at a time, in sequence —@batchSize(1)is fully serial. Use it when updating everything at once would take a service offline. - Conditions:
resource pip '...' = if (deployPublicIp) { ... }deploys only when true. Anything that reads a conditional resource must handle the case where it was not deployed (for exampledeployPublicIp ? pip.properties.ipAddress : '').
Modules
A module is another Bicep file called from your file with its own parameters, which is how a team shares a standard definition (a virtual network, a VM baseline) without copying it. At build time modules are compiled into the single ARM template that is deployed. A module can come from:
| Source | Reference format |
|---|---|
| Local file | module vnet './vnet.bicep' = { ... } |
Private registry (Azure Container Registry), published with bicep publish | 'br:contosoregistry.azurecr.io/bicep/modules/vnet:v2' |
| Public module registry | 'br/public:...' |
| Template spec | 'ts:subscriptionId/resourceGroup/specName:version' |
A module also has its own scope. A file with targetScope = 'subscription' can create a resource group directly, but resources such as a storage account must be deployed at resource-group scope — so you put them in a module and set scope: rg (the new group's symbolic name) or scope: resourceGroup('rg-logs'). One subscription-level command then creates the group and its contents.
Deployment modes: Incremental versus Complete
The deployment mode decides what happens to resources that already exist in the target resource group but are not in the template. Incremental is the default; Complete must be requested explicitly (--mode Complete).
| Behaviour | Incremental (default) | Complete |
|---|---|---|
| Resources in the template | Created or updated (declared properties reapplied) | Created or updated (declared properties reapplied) |
| Resources in the group but not in the template | Left unchanged | Deleted |
| Risk of accidental deletion | None | High |
Scenario: a resource group contains resources A, B and C, and the template defines A, B and D. Incremental mode leaves the group with A, B, C and D; Complete mode deploys D, updates A and B, and deletes C because the template no longer mentions it.
Where Complete mode does not apply
- It is a resource-group deployment feature: subscription-scope deployments do not support it.
- The portal's custom deployment does not offer it; use the CLI, PowerShell or REST.
- Nested and linked templates must use Incremental mode; only the main template can be Complete.
- If the resource group is locked, Complete mode does not delete its resources.
- Microsoft is gradually deprecating Complete mode and recommends deployment stacks instead.
Deployment stacks
A deployment stack (az stack group create, az stack sub create, az stack mg create, or the New-AzResourceGroupDeploymentStack family) deploys a template and remembers which resources it manages. Two settings make it the replacement for Complete mode:
| Setting | Values | Effect |
|---|---|---|
--action-on-unmanage | detachAll, deleteResources, deleteAll | What happens to a managed resource removed from the template: left in Azure but no longer managed (detach), deleted (resources only), or deleted including managed resource groups and management groups |
--deny-settings-mode | none, denyDelete, denyWriteAndDelete | Blocks deletion (or changes and deletion) of managed resources by anyone outside the stack, even users with Contributor; exclusions can be listed |
Unlike Complete mode, a stack deletes only resources it created and later dropped — not unrelated resources other people put in the same group.
Previewing and validating a deployment: what-if, validate and lint
Three checks run before a deployment changes anything, and each answers a different question.
| Check | Command | What it tells you |
|---|---|---|
| Lint | az bicep lint / bicep lint (also runs during build) | Static analysis of the Bicep source against best-practice rules; never contacts the target |
| Validate | az deployment group validate; PowerShell Test-AzResourceGroupDeployment | Whether Resource Manager would accept the template and parameters (syntax, preflight checks); does not list changes |
| What-if | az deployment group what-if; PowerShell New-AzResourceGroupDeployment -WhatIf | Which resources would be created, modified, deleted or left unchanged, and which properties would change; nothing is deployed |
What-if respects the deployment mode, so in Complete mode it shows the deletions. To preview and approve in one command, use az deployment group create --confirm-with-what-if (-c) or PowerShell's New-AzResourceGroupDeployment -Confirm: the command shows the what-if result and waits for you to confirm before deploying. Every scope has the matching variant (az deployment sub what-if and so on).
Deployment scopes and deployment commands
A deployment targets one scope, and the scope decides which resource types the template can declare and which command runs it. Bicep files declare it with targetScope (default resourceGroup); ARM JSON uses a scope-specific $schema.
| Scope | Bicep targetScope | Azure CLI | Azure PowerShell | Typical content |
|---|---|---|---|---|
| Resource group | resourceGroup | az deployment group create | New-AzResourceGroupDeployment | Most resources: VMs, networks, storage, web apps |
| Subscription | subscription | az deployment sub create --location | New-AzSubscriptionDeployment or New-AzDeployment | Resource groups, policy and role assignments for the subscription |
| Management group | managementGroup | az deployment mg create | New-AzManagementGroupDeployment | Policy definitions and assignments for many subscriptions |
| Tenant | tenant | az deployment tenant create | New-AzTenantDeployment | Management groups, tenant-wide settings |
Resource groups themselves can be declared directly only in a subscription-scope deployment; to also fill the new group, use a module scoped to it. A subscription, management-group or tenant deployment needs a location because Resource Manager stores the deployment's metadata there.
Ways to run a resource-group deployment:
- Azure CLI:
az deployment group create --resource-group rg --template-file main.bicep --parameters @dev.parameters.json(the CLI builds Bicep for you). - Azure PowerShell:
New-AzResourceGroupDeployment -ResourceGroupName rg -TemplateFile ./azuredeploy.json;Test-AzResourceGroupDeploymentonly validates. - Portal: Deploy a custom template lets you paste or load a template, pick the subscription and resource group, and fill in parameters.
Exporting a deployment and converting ARM to Bicep
There are two export paths, and they return different templates.
| Export from | Returns | Commands |
|---|---|---|
| Resource group or selected resources (portal Export template) | The current state of the resources, including changes made by hand after deployment | az group export; Export-AzResourceGroup |
| Deployment history (a past deployment) | The original template that deployment used — no later manual changes | az deployment group export; Save-AzResourceGroupDeploymentTemplate |
Exporting from a resource group or from selected resources can produce ARM JSON or Bicep. Saving from deployment history returns ARM JSON only; run az bicep decompile on it if you want Bicep. Exported templates are a starting point, not a finished artifact: values are hard-coded, some resource types are not exported at all (Azure Data Factory, for example), secret values such as a VM administrator password parameter can be missing and must be added by hand, and a single resource-group export is limited to 200 resources.
Converting between formats
| Command | Converts |
|---|---|
az bicep decompile --file azuredeploy.json | ARM JSON template to a Bicep file (may need cleanup) |
az bicep build --file main.bicep | Bicep file to ARM JSON |
az bicep decompile-params --file azuredeploy.parameters.json --bicep-file ./main.bicep | JSON parameter file to a .bicepparam file that points at the Bicep file |
az bicep build-params --file dev.bicepparam | .bicepparam file to a JSON parameter file |
az bicep generate-params --file main.bicep | Creates a new parameter file from the parameters a Bicep file declares |
az bicep publish | Publishes a Bicep file as a module to a registry |
Export plus decompile turns a hand-built resource group into an editable Bicep file.
Tip. Most items are scenarios that hinge on predicting Resource Manager's behaviour: which resources survive an Incremental or Complete deployment, what a redeploy does to manual changes or child subnets, and when deployment stacks replace Complete mode. Expect to choose commands by job and scope (what-if versus validate, az deployment group versus sub create, decompile versus build versus decompile-params), to read templates for required elements, runtime-function placement, dependencies, loops and conditions, and to pick the right way to handle secrets (@secure(), Key Vault references and their permissions) and to share templates (linked templates and SAS, template specs and their RBAC, registry modules). The exam also uses multiple-answer items, so learn each command and setting together with the job and scope it belongs to.
- An ARM template requires only $schema, contentVersion and resources; parameters, variables, functions and outputs are optional.
- Bicep transpiles to ARM JSON: same engine and resource coverage, no state file, plain expressions and string interpolation instead of bracketed template expressions.
- Symbolic references create implicit dependencies; a resourceId() string does not, so add dependsOn or switch to the symbolic reference. Use existing with a scope to read resources you must not change.
- reference() and list*() run only in resource properties and outputs, and references to a conditional resource need an if() guard.
- Only @secure() keeps a value out of deployment history. Key Vault references need enabledForTemplateDeployment on the vault and deploy/action for the deployer; in Bicep, getSecret() feeds only a @secure() module parameter.
- Redeploys reapply every declared property, reverting manual drift; declare subnets inside the virtual network, not as separate child resources.
- Incremental (default) leaves undeclared resources alone; Complete deletes them, but not at subscription scope, not from the portal, not for nested templates and not past a lock. Deployment stacks are the recommended replacement.
- what-if previews changes, validate checks acceptability, lint checks source quality; --confirm-with-what-if or -Confirm previews and approves in one command.
- Resource groups are declared at subscription scope (az deployment sub create); scope a module to the new group to fill it.
- Linked templates are fetched by Resource Manager (SAS needed for private blobs); template specs and modules avoid hosted URIs, and deploying a spec needs Template Spec Reader on the spec plus deploy rights on the target scope.
- Exporting a resource group captures current state; exporting from deployment history returns the original template. Decompile converts JSON to Bicep; build goes the other way.
Frequently asked questions
What is the difference between Incremental and Complete deployment mode?
Incremental mode, the default, creates or updates the resources defined in the template and leaves any other resources in the resource group untouched. Complete mode also deploys the template's resources and then deletes every resource in the resource group that the template does not define. Complete mode applies only to resource-group deployments, is not offered by the portal, cannot be used by nested or linked templates and does not delete resources in a locked resource group. Microsoft is gradually deprecating it in favour of deployment stacks.
What is a deployment stack and why is it recommended over Complete mode?
A deployment stack is an Azure resource that deploys a template and tracks the resources it manages. With --action-on-unmanage set to deleteResources or deleteAll, a resource removed from the template is deleted on the next update (detachAll leaves it in place, unmanaged). With --deny-settings-mode denyDelete, nobody outside the stack can delete the managed resources, even with Contributor. Unlike Complete mode, a stack never deletes unrelated resources that other people created in the same resource group.
What is the difference between an ARM template and a Bicep file?
An ARM template is a JSON document whose required elements are $schema, contentVersion and resources, with optional parameters, variables, functions and outputs. A Bicep file declares the same things in a more concise language that uses plain expressions and string interpolation instead of bracketed JSON expressions, infers dependencies from symbolic references, and reuses code through modules. Deploying a Bicep file transpiles it to an ARM template, so both formats use the same engine and support the same resources.
How does what-if differ from template validation?
Validation (az deployment group validate or Test-AzResourceGroupDeployment) checks whether Resource Manager would accept the template and parameters, but does not tell you what would change. What-if (az deployment group what-if or New-AzResourceGroupDeployment -WhatIf) lists the resources that would be created, modified or deleted and the property changes, without deploying anything. Bicep linting is a third, purely static check of the source code. Adding --confirm-with-what-if to az deployment group create shows the what-if result and waits for approval before deploying.
How do you pass a secret from Azure Key Vault to a template deployment?
In a JSON parameter file, set the parameter value to a reference containing the vault's resource ID and the secret name. The vault must have enabledForTemplateDeployment set to true, and the identity running the deployment needs Microsoft.KeyVault/vaults/deploy/action on the vault, which Owner and Contributor include. The vault can be in a different resource group or subscription. In Bicep, use az.getSecret() in a .bicepparam file, or getSecret() on an existing vault to pass a value to a module parameter marked @secure().
Why does a linked template fail to load from a private storage container?
Resource Manager downloads the URI in templateLink itself, so the deploying user's role on the storage account does not grant access. For a blob in a private container, append a SAS token to the URI at deployment time. If the storage account firewall blocks public network access, a SAS token does not help because Resource Manager cannot reach the endpoint. Template specs and Bicep modules avoid the problem because no hosted URI has to be fetched.
How do you export an existing deployment as a template?
Use Export template on a resource group or selected resources (az group export) to capture their current state, including changes made by hand. Use the deployment history (az deployment group export) to get the original template a specific deployment used. Resource-group and resource export can produce ARM JSON or Bicep; deployment-history export returns ARM JSON only, which az bicep decompile converts to Bicep. Expect cleanup: values are hard-coded, some resource types such as Azure Data Factory are not exported, secret parameters such as VM passwords can be missing, and one export covers at most 200 resources.
How do you convert an ARM template to a Bicep file?
Run az bicep decompile --file azuredeploy.json to convert an ARM JSON template to a Bicep file; decompile always means JSON to Bicep. To convert the matching JSON parameter file to a .bicepparam file, run az bicep decompile-params with --bicep-file pointing at the new Bicep file. To go the other way, az bicep build compiles a Bicep file to ARM JSON and az bicep build-params converts a .bicepparam file to JSON. Decompiled files usually need some cleanup.
Source
This lesson covers the "Deploy and manage Azure compute resources" domain of the official AZ-104 exam guide. Vendors revise their guides — check the source for the current version.
- Microsoft AZ-104 study guide — Microsoft Learn
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.