SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Deploy and manage Azure compute resources

ARM Templates and Bicep: AZ-104 Deployment Guide

22 min readAZ-104 · Deploy and manage Azure compute resourcesUpdated

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.

What you’ll learn
  • 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:

ElementRequired?Job
$schemaYesPoints at the JSON schema for the template language and scope
contentVersionYesYour version of the template, for example 1.0.0.0
resourcesYesThe resources to create or update, each with type, apiVersion, name and usually location and properties
parametersNoValues supplied at deployment time (VM name, admin username, environment), so one template serves many environments
variablesNoValues computed or reused inside the template, such as a name built from a parameter
functionsNoUser-defined template functions
outputsNoValues 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.blob

Every 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.

AspectARM template (JSON)Bicep file
SyntaxJSON; logic lives in bracketed expressions such as [concat(...)]Braces, brackets and single-quoted strings, with plain expressions and ${} string interpolation
DependenciesExplicit dependsOnInferred from symbolic references; dependsOn still available
ReuseLinked templates fetched from a URI, or nested templatesModules, compiled into one template; no hosted URIs needed
Engine and coverageResource ManagerSame: transpiled to ARM JSON, same resource types and API versions
StateNone (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 securestring in 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 reference to the vault's resource ID and secret name. It works only if the vault has enabledForTemplateDeployment set to true (off by default) and the identity running the deployment holds Microsoft.KeyVault/vaults/deploy/action on 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 an existing vault can be used only to pass a value to a module parameter marked @secure() — not directly in a resource property. In a .bicepparam file, use az.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; change vmCount to change the count. You can also loop over an array: [for app in appList: { ... }]. ARM JSON does the same with the copy element.
  • 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 example deployPublicIp ? 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:

SourceReference format
Local filemodule 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).

BehaviourIncremental (default)Complete
Resources in the templateCreated or updated (declared properties reapplied)Created or updated (declared properties reapplied)
Resources in the group but not in the templateLeft unchangedDeleted
Risk of accidental deletionNoneHigh

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:

SettingValuesEffect
--action-on-unmanagedetachAll, deleteResources, deleteAllWhat 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-modenone, denyDelete, denyWriteAndDeleteBlocks 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.

CheckCommandWhat it tells you
Lintaz bicep lint / bicep lint (also runs during build)Static analysis of the Bicep source against best-practice rules; never contacts the target
Validateaz deployment group validate; PowerShell Test-AzResourceGroupDeploymentWhether Resource Manager would accept the template and parameters (syntax, preflight checks); does not list changes
What-ifaz deployment group what-if; PowerShell New-AzResourceGroupDeployment -WhatIfWhich 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.

ScopeBicep targetScopeAzure CLIAzure PowerShellTypical content
Resource groupresourceGroupaz deployment group createNew-AzResourceGroupDeploymentMost resources: VMs, networks, storage, web apps
Subscriptionsubscriptionaz deployment sub create --locationNew-AzSubscriptionDeployment or New-AzDeploymentResource groups, policy and role assignments for the subscription
Management groupmanagementGroupaz deployment mg createNew-AzManagementGroupDeploymentPolicy definitions and assignments for many subscriptions
Tenanttenantaz deployment tenant createNew-AzTenantDeploymentManagement 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-AzResourceGroupDeployment only validates.
  • Portal: Deploy a custom template lets you paste or load a template, pick the subscription and resource group, and fill in parameters.

Sharing templates: linked templates and template specs

Teams share templates in three ways: linked templates, template specs and Bicep modules. They differ in where the template lives and who has to be able to read it.

A linked template is an ARM JSON template that a main template calls through templateLink with a URI. Resource Manager fetches that URI itself, not your client and not under your identity, so your RBAC role on the storage account does not help. A blob in a private container needs a SAS token appended to the URI at deployment time. If the storage account blocks public network access with its firewall, even a SAS token fails, because Resource Manager cannot reach the endpoint.

A template spec is an Azure resource (Microsoft.Resources/templateSpecs) that stores a template with numbered versions. You create one with az ts create --name web-standard --version 2.0 --resource-group rg-specs --template-file main.bicep and deploy a version by its resource ID (az deployment group create --template-spec ..., or a ts: module reference). Access is plain Azure RBAC: a deployer needs Template Spec Reader on the spec (it can sit in another subscription) plus rights to deploy into the target resource group. No storage account or SAS token is involved, and Reader-level access cannot change the spec.

Bicep modules, whether local or from a registry, are compiled into the main template before deployment, so there are no URIs for Resource Manager to fetch.

Exporting a deployment and converting ARM to Bicep

There are two export paths, and they return different templates.

Export fromReturnsCommands
Resource group or selected resources (portal Export template)The current state of the resources, including changes made by hand after deploymentaz group export; Export-AzResourceGroup
Deployment history (a past deployment)The original template that deployment used — no later manual changesaz 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

CommandConverts
az bicep decompile --file azuredeploy.jsonARM JSON template to a Bicep file (may need cleanup)
az bicep build --file main.bicepBicep file to ARM JSON
az bicep decompile-params --file azuredeploy.parameters.json --bicep-file ./main.bicepJSON 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.bicepCreates a new parameter file from the parameters a Bicep file declares
az bicep publishPublishes 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.

Key takeaways
  • 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.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?