ARM Templates and Bicep for AZ-104: Deployment, What-If and Scope
Introduction
ARM templates and Bicep for AZ-104 turn repeatable infrastructure designs into reviewed Azure deployments. This lesson opens the skills area Deploy and manage Azure compute resources (20–25% of the exam). The first objective of this area is automate deployment of resources by using Azure Resource Manager (ARM) templates or Bicep files: interpret an ARM template or a Bicep file, modify an existing ARM template, modify an existing Bicep file, deploy resources by using an ARM template or a Bicep file, and export a deployment as an ARM template or convert an ARM template to a Bicep file.
You are not expected to write large templates from memory, but you must read them fluently, spot what a deployment will do, fix a broken fragment, and choose the right command. Questions typically show a short snippet and ask what happens when it is deployed, or which change satisfies a requirement.
After this lesson you will be able to explain the structure of ARM templates and Bicep files, use parameters, variables, outputs, loops and conditions, deploy at the right scope in complete or incremental mode, preview changes with what-if, and export or decompile templates.
Learning objectives
- Describe the sections of an ARM template and the equivalent Bicep constructs.
- Interpret
resource,param,var,output,existing,module,forandifin Bicep. - Modify templates: add a parameter, change a SKU, add a dependency, add a copy loop.
- Deploy with the Azure CLI and Azure PowerShell at resource group, subscription, management group and tenant scope.
- Distinguish incremental from complete deployment mode and use what-if.
- Export a resource group or deployment to a template and convert JSON to Bicep.
Apply the scopes and permissions from Azure governance and resource organization when selecting a deployment target.
Infrastructure as code with Azure Resource Manager
Every Azure management operation (portal, CLI, PowerShell, SDK, REST) goes through Azure Resource Manager (ARM). Declarative templates describe the desired end state; ARM works out the order and calls the resource providers. Benefits you should be able to name: repeatability, consistency across environments, parallel deployment of independent resources, reviewable source control, and idempotency when resource names, scope and inputs remain stable. Deployment scripts, extensions and mutable external inputs can still have side effects on a repeated deployment.
Two template languages target ARM:
| ARM template (JSON) | Bicep | |
|---|---|---|
| Syntax | JSON with expressions in [ ] |
Concise declarative language |
| Compiles to | Is the native format | Transpiles to ARM JSON |
| Readability | Verbose | Short, typed, with IntelliSense |
| Dependencies | dependsOn or reference() |
Mostly implicit via symbolic names |
| Modules | Linked/nested templates | module keyword |
| Status | Fully supported | Recommended by Microsoft for new work |
Bicep is not a separate deployment engine: the Bicep CLI compiles a .bicep file to JSON, and ARM deploys the JSON. Bicep targets the same ARM resource model and supports new resource API versions without a separate deployment engine. Tooling/type support and language-specific features can differ, so review conversion diagnostics instead of assuming every JSON construct decompiles perfectly.
Anatomy of an ARM template
An ARM template is a JSON file with these top-level elements:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": { },
"variables": { },
"functions": [ ],
"resources": [ ],
"outputs": { }
}$schemaandcontentVersionare required.resourcesis required: the list of resources to deploy.parameterslet callers supply values (withtype,defaultValue,allowedValues,minLength,secureStringfor secrets).variableshold computed values reused in the template.outputsreturn values after deployment (for example a public IP address or an endpoint).- Expressions live inside
[ ]and use functions such asparameters(),variables(),resourceGroup().location,concat(),resourceId(),uniqueString().
A typical regional resource has type, apiVersion, name, location and service-specific properties, with optional sku, dependsOn, copy and condition. Exact required properties depend on the resource type; not every resource has a location or SKU.
Anatomy of a Bicep file
The same ideas in Bicep:
@description('Storage account name prefix')
@minLength(3)
@maxLength(11)
param namePrefix string
param location string = resourceGroup().location
@allowed([
'Standard_LRS'
'Standard_GRS'
])
param skuName string = 'Standard_LRS'
var storageName = '${namePrefix}${uniqueString(resourceGroup().id)}'
resource stg 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageName
location: location
sku: {
name: skuName
}
kind: 'StorageV2'
properties: {
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
}
}
output blobEndpoint string = stg.properties.primaryEndpoints.blobSupply namePrefix as 3–11 lowercase letters/digits; the length decorators do not validate the permitted characters. The resulting name combines that prefix with the 13-character deterministic suffix.
Reading it:
paramdeclares an input; decorators such as@allowed,@minLength,@secure()and@descriptionvalidate it. A parameter without a default is mandatory.vardeclares a computed value. No type is needed; it is inferred.resource stg '<type>@<apiVersion>' = { ... }declares a resource.stgis a symbolic name used inside the file only; the Azure resource name is thenameproperty.outputreturns a value. Never output secrets.uniqueString()creates a deterministic hash, useful for reducing name collisions across scopes. It does not reserve a name or mathematically guarantee global uniqueness.
For the account properties in this example, review Azure storage account configuration.
Dependencies
ARM deploys independent resources in parallel and orders dependent ones. In Bicep a reference to another resource's symbolic name, for example stg.id or vnet.properties.subnets[0].id, creates an implicit dependency. You use dependsOn only when no property reference exists but order still matters. In ARM JSON use explicit dependsOn where needed. A reference() or list*() expression can create an implicit dependency when it refers to a resource deployed in the template by name; an ID string alone, including resourceId(), does not establish one. An existing Bicep resource is referenced but not deployed.
Loops and conditions
param deployDiagnostics bool = false
param subnetNames array = [ 'web', 'app', 'data' ]
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: 'vnet-contoso'
location: resourceGroup().location
properties: {
addressSpace: { addressPrefixes: [ '10.10.0.0/16' ] }
subnets: [for (name, i) in subnetNames: {
name: name
properties: { addressPrefix: '10.10.${i}.0/24' }
}]
}
}
resource diag 'Microsoft.OperationalInsights/workspaces@2023-09-01' = if (deployDiagnostics) {
name: 'law-${uniqueString(resourceGroup().id)}'
location: resourceGroup().location
properties: {
sku: { name: 'PerGB2018' }
retentionInDays: 30
}
}The conditional resource is a complete Log Analytics workspace example. It does not configure diagnostic settings on the VNet; those would be separate resources with destinations and supported log/metric categories.
[for item in array: { ... }]creates one element per item (ARM JSON equivalent: thecopyproperty).= if (condition) { ... }deploys the resource only when the condition is true (JSON:condition).existingreferences a resource that is already deployed without redeploying it:
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: 'kv-contoso-prod'
}Modules
A module is a Bicep file called from another file:
module network './modules/network.bicep' = {
name: 'networkDeployment'
params: {
location: location
vnetName: 'vnet-contoso'
}
}Modules break big deployments into reusable pieces. Each module becomes a nested deployment. Modules can also target a different scope (scope: resourceGroup('rg-other')) and can be pulled from a Bicep registry (an Azure Container Registry) or from template specs. Parameter files (.bicepparam or JSON parameters.json) keep environment values separate from the template.
Deploying templates
Scopes
A deployment targets a scope, and the command changes with it:
| Scope | Azure CLI | Azure PowerShell | Typical use |
|---|---|---|---|
| Resource group | az deployment group create |
New-AzResourceGroupDeployment |
VMs, storage, networks |
| Subscription | az deployment sub create |
New-AzSubscriptionDeployment |
Resource groups, policy, RBAC at subscription level |
| Management group | az deployment mg create |
New-AzManagementGroupDeployment |
Policy and RBAC across subscriptions |
| Tenant | az deployment tenant create |
New-AzTenantDeployment |
Management groups, tenant-wide assignments |
In Bicep the file declares its scope with targetScope = 'subscription' (default resourceGroup).
# main.bicepparam contains using './main.bicep' and the storage parameters
az group create --name rg-app --location westeurope
az deployment group create \
--resource-group rg-app \
--parameters main.bicepparamFor the storage example, main.bicepparam can contain:
using './main.bicep'
param namePrefix = 'contoso'
param skuName = 'Standard_GRS'With a .bicepparam file, current CLI guidance uses its using reference without an additional --template-file. With a JSON parameter file, use --template-file main.bicep --parameters @parameters.json. Subscription, management-group and tenant deployment commands also need a deployment metadata --location; this does not force every deployed resource into that region.
New-AzResourceGroupDeployment -ResourceGroupName rg-app `
-TemplateFile main.bicep -namePrefix contoso -skuName Standard_GRSThe Azure CLI and PowerShell compile Bicep for you, provided the Bicep tooling is installed. Deployments you run are recorded in the resource group's Deployments blade, where you can see status, inputs, outputs and operation errors. Resource groups keep up to 800 deployments in history, and old ones are removed automatically as needed.
Incremental versus complete mode
| Mode | Behaviour | Default |
|---|---|---|
| Incremental | Adds or updates resources defined in the template. Resources in the group that are not in the template are left unchanged | Yes |
| Complete | Resources in the group that are not in the template are deleted | No (--mode Complete) |
Incremental mode is Microsoft's recommended default. Complete mode is being gradually deprecated; use deployment stacks for managed resource deletions in new designs. Complete mode remains relevant when interpreting existing resource-group deployments: omitted resources can be deleted, subject to resource-type behavior and locks. It applies to root resource-group deployments, not subscription/management-group/tenant deployments or nested modules. Always review what-if. Incremental mode still reapplies properties of resources present in the template, so omitted non-default settings can reset.
What-if
what-if previews the changes without applying them. It reports which resources would be created, modified, deleted or ignored.
az deployment group what-if --resource-group rg-app --parameters main.bicepparamIn PowerShell, add the -WhatIf switch to New-AzResourceGroupDeployment (or use Get-AzResourceGroupDeploymentWhatIfResult). Resource group, subscription, management group and tenant scopes are supported. What-if needs deployment/write permissions comparable to deployment itself and can show unresolved expressions or ignored resources at expansion limits; it is a prediction to review, not a guarantee of successful execution. You can also validate a template (az deployment group validate) to catch syntax and API problems without a preview of changes.
Template specs and Deployment Stacks (awareness)
A template spec stores a versioned template in Azure as a resource so that teams can deploy it with RBAC control rather than sharing files. Deployment stacks manage a group of resources as a unit and can delete or detach resources when they are removed from the template, as an alternative to complete mode. Stacks track the resources they manage; they do not discover and remove unrelated resources created outside the stack. Use an inventory and an approved cleanup process for unmanaged resources.
Modifying templates
Typical modifications that appear in questions:
- Change a SKU or size: edit
sku.nameorhardwareProfile.vmSizeand redeploy in incremental mode. Properties that cannot be changed in place (for example the location of a resource or some storage account kinds) require redeployment. - Add a parameter: add
param(Bicep) or an entry inparameters(JSON) and replace the hardcoded value with the parameter. - Make a name unique: combine a prefix with
uniqueString(resourceGroup().id). - Add a dependency: reference the other resource's property, or add
dependsOn. - Create several similar resources: add a loop (
for/copy). - Keep secrets out of the template: use
@secure()parameters or reference a Key Vault secret from the parameter file, never a hardcoded password.
Worked example: fixing a failing deployment
Scenario. Contoso deploys this Bicep file and receives the error "Storage account name is not valid" in the second region.
param location string
resource stg 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: 'Contoso-Prod-Storage-${location}'
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}Reasoning.
- Storage account names must be 3–24 characters, lowercase letters and numbers only. The name contains uppercase letters and hyphens and is too long.
- The name must also be globally unique across Azure, so a static value fails once someone else uses it.
- A deterministic hash solves both:
name: 'stcontoso${uniqueString(resourceGroup().id, location)}'givesstcontoso+ 13 characters = 22 characters, lowercase letters and digits.
Solution. Replace the name expression with the prefix plus uniqueString(...).
Why other fixes are worse. Truncating location still leaves uppercase letters and hyphens. utcNow() and newGuid() are restricted to parameter default expressions, so directly placing them in the resource name is invalid. Passing a freshly evaluated default through a parameter would also produce unstable names. Switching to complete mode fixes nothing and could delete resources.
Worked example: predicting what a deployment creates
Scenario. Adatum deploys the earlier virtual network file with deployDiagnostics = false and subnetNames = [ 'web', 'app' ] into a resource group that already contains a subnet named data created by hand in the same virtual network. The deployment mode is incremental. What is the result?
Reasoning.
- The loop produces two subnet entries,
web(10.10.0.0/24) andapp(10.10.1.0/24). - The
diagresource hasif (deployDiagnostics); withfalseit is skipped entirely. - Subnets are declared inline in the virtual network's
properties.subnetsarray, so the template defines the complete list of subnets for that virtual network. Incremental mode only protects other resources, not child items you omit from an array property. The PUT replaces the property, so the hand-madedatasubnet is removed (or the deployment fails if something is attached to it).
Solution. Keep the full intended subnet list in the VNet's properties.subnets array, following the deployment-modes guidance, and review what-if before deploying. Do not mix inline and separate child definitions for the same subnets.
Why the intuitive answer is wrong. Many learners expect incremental mode to leave data untouched. It leaves other resources untouched, but array properties of a resource you redeploy are replaced as a whole.
Exporting and converting templates
You can capture existing resources as code:
- Export template from a resource group, resource or deployment in the portal (Automation → Export template). Exporting a resource group generates JSON for the resources present at that moment. It can fail or be incomplete for some resource types, and secrets are not exported.
- Export from deployment history returns the stored deployment template, which is usually cleaner than exporting the group. Secure parameter values and external runtime state are not recoverable from history.
- Azure CLI:
az group export --name rg-app > template.json; PowerShell:Export-AzResourceGroup. - Decompile to Bicep:
az bicep decompile --file template.jsoncreatestemplate.bicep. Decompilation is a best-effort conversion: review the output, fix warnings, and replace generated names with parameters. - Build to JSON:
az bicep build --file main.bicepgeneratesmain.json. - Existing resources can also be imported into a Bicep file through the Bicep VS Code extension ("Insert Resource").
Comparison: choosing the mechanism
| Need | Choice |
|---|---|
| Author new infrastructure as code | Bicep |
| Reuse a template an existing team already maintains in JSON | ARM template (deploy as-is) |
| Convert legacy JSON to Bicep | az bicep decompile |
| See what a deployment would change | what-if |
| Delete previously managed resources removed from code | Deployment stack with an explicit delete action; legacy complete mode for an isolated, fully described resource group |
| Share a versioned template with RBAC-controlled access | Template spec |
| Create resource groups from a template | Subscription-scope deployment |
| Capture an existing environment | Export template, then clean up |
Pitfalls and exam tips
- Default mode is incremental. Undeclared resources are retained, but redeployed resource properties can reset and an inline array update can remove entries. Use stacks for deliberate lifecycle deletion; recognize legacy complete-mode behavior.
dependsOnis rarely needed in Bicep. A property reference is an implicit dependency.- Resource group deployments cannot create resource groups. Use subscription scope for that (the
targetScopeandaz deployment sub create). - Secrets: use
@secure()parameters or Key Vault references; do not output them. - Names: deterministic
uniqueString()reduces collisions and keeps names stable; still validate allowed characters, length and actual name availability. - Export is not a backup. It captures configuration, not data, and may omit settings.
- Decompilation needs review. Do not assume the generated Bicep is production ready.
- Exam tip: read the snippet for what it declares, not what you expect. Check
kind,sku,location,conditionand the loop bounds before answering.
Mini case study: Tailwind Traders environments
Tailwind Traders keeps a hand-edited JSON template for its test environment: one virtual network, one storage account and two VMs. The team wants to:
- Deploy the same design to dev, test and prod with different VM sizes.
- Make sure no one leaves unmanaged test VMs in the resource group.
- See the impact before every production deployment.
- Move to Bicep without rewriting everything.
Proposed solution.
- Run
az bicep decompileon the JSON, review and clean the Bicep, replace hardcoded values with parameters, and create one.bicepparamfile per environment containingvmSize. - Deploy production in incremental mode with reviewed inputs. For the test environment, adopt a deployment stack with a deliberate delete-or-detach policy for previously managed resources removed from the code.
- Inventory test VMs created outside that stack and approve their cleanup separately. A stack cannot delete arbitrary unmanaged VMs merely because they are absent from its template.
- Add a pipeline step that runs
az deployment group what-ifand requires approval beforecreate.
Commentary. A partial template in complete mode could delete unrelated production resources. Incremental deployments plus reviewed stack deletion policies provide an explicit ownership boundary; inventory and approved cleanup handle resources outside it. Decompiling saves time but yields generic names that must be parameterized. Separate parameter files keep the template identical across environments, which is the purpose of infrastructure as code.
Next, apply these deployment patterns to Azure virtual machines, disks and availability.
Key terms
- ARM template: JSON declaration of Azure resources deployed by Azure Resource Manager.
- Bicep: concise language that compiles to ARM JSON.
- Parameter / variable / output: input, computed value and return value of a template.
- Module: Bicep file reused from another file.
- Incremental / complete mode: add-or-update versus add-update-and-delete behaviour.
- What-if: preview of deployment changes.
- Template spec: versioned template stored as an Azure resource.
- Scope: resource group, subscription, management group or tenant.
Summary checklist
- I can name the sections of an ARM template and their Bicep equivalents.
- I can read param, var, resource, output, module, loop and condition in Bicep.
- I know the four deployment scopes and their commands.
- I know incremental is the default and complete deletes extra resources.
- I use what-if before risky deployments.
- I can export a template and decompile JSON to Bicep.
- I keep secrets out of templates.
Official sources
-
AZ-104 study guide: https://learn.microsoft.com/en-us/credentials/certifications/resources/study-guides/az-104 (opens in a new tab)
-
Bicep documentation: https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview (opens in a new tab)
-
ARM template deployment what-if: https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/deploy-what-if (opens in a new tab)
-
Deployment modes: https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/deployment-modes (opens in a new tab)
-
Deploy Bicep parameter files with Azure CLI (opens in a new tab)
