Skip to content

ARM Templates and Bicep: Deployment, What-If and Scope

Step 13 of 30

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, for and if in 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:

JSON
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": { },
  "variables": { },
  "functions": [ ],
  "resources": [ ],
  "outputs": { }
}
  • $schema and contentVersion are required.
  • resources is required: the list of resources to deploy.
  • parameters let callers supply values (with type, defaultValue, allowedValues, minLength, secureString for secrets).
  • variables hold computed values reused in the template.
  • outputs return values after deployment (for example a public IP address or an endpoint).
  • Expressions live inside [ ] and use functions such as parameters(), 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:

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

Supply 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:

  • param declares an input; decorators such as @allowed, @minLength, @secure() and @description validate it. A parameter without a default is mandatory.
  • var declares a computed value. No type is needed; it is inferred.
  • resource stg '<type>@<apiVersion>' = { ... } declares a resource. stg is a symbolic name used inside the file only; the Azure resource name is the name property.
  • output returns 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

Bicep
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: the copy property).
  • = if (condition) { ... } deploys the resource only when the condition is true (JSON: condition).
  • existing references a resource that is already deployed without redeploying it:
Bicep
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: 'kv-contoso-prod'
}

Modules

A module is a Bicep file called from another file:

Bicep
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).

Bash
# 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.bicepparam

For the storage example, main.bicepparam can contain:

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

PowerShell
New-AzResourceGroupDeployment -ResourceGroupName rg-app `
  -TemplateFile main.bicep -namePrefix contoso -skuName Standard_GRS

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

Bash
az deployment group what-if --resource-group rg-app --parameters main.bicepparam

In 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:

  1. Change a SKU or size: edit sku.name or hardwareProfile.vmSize and 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.
  2. Add a parameter: add param (Bicep) or an entry in parameters (JSON) and replace the hardcoded value with the parameter.
  3. Make a name unique: combine a prefix with uniqueString(resourceGroup().id).
  4. Add a dependency: reference the other resource's property, or add dependsOn.
  5. Create several similar resources: add a loop (for / copy).
  6. 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.

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

  1. Storage account names must be 3–24 characters, lowercase letters and numbers only. The name contains uppercase letters and hyphens and is too long.
  2. The name must also be globally unique across Azure, so a static value fails once someone else uses it.
  3. A deterministic hash solves both: name: 'stcontoso${uniqueString(resourceGroup().id, location)}' gives stcontoso + 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.

  1. The loop produces two subnet entries, web (10.10.0.0/24) and app (10.10.1.0/24).
  2. The diag resource has if (deployDiagnostics); with false it is skipped entirely.
  3. Subnets are declared inline in the virtual network's properties.subnets array, 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-made data subnet 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.json creates template.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.bicep generates main.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.
  • dependsOn is rarely needed in Bicep. A property reference is an implicit dependency.
  • Resource group deployments cannot create resource groups. Use subscription scope for that (the targetScope and az 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, condition and 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:

  1. Deploy the same design to dev, test and prod with different VM sizes.
  2. Make sure no one leaves unmanaged test VMs in the resource group.
  3. See the impact before every production deployment.
  4. Move to Bicep without rewriting everything.

Proposed solution.

  • Run az bicep decompile on the JSON, review and clean the Bicep, replace hardcoded values with parameters, and create one .bicepparam file per environment containing vmSize.
  • 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-if and requires approval before create.

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