Skip to main content

Theoretical Foundation: Interpret an Azure Resource Manager template or a Bicep file


1. Initial Intuition​

Imagine you need to set up exactly the same office in ten different cities: same furniture, same layout, same equipment. You could go city by city setting everything up manually. Or you could create a detailed blueprint that describes exactly how the office should be, and send that blueprint to all ten cities at once.

An ARM Template or a Bicep file is that office blueprint. It's a document that declaratively describes which Azure resources should exist, with which configurations, in what order, and with which dependencies. Instead of clicking through the portal or running imperative commands one by one, you declare the desired state of the infrastructure and Azure takes care of creating, modifying, or deleting resources to achieve that state.

The central difference between imperative and declarative thinking is: imperative says "do this, then do that, then do that other thing". Declarative says "the final result should be this". Azure figures out how to get there.


2. Context​

ARM Templates and Bicep are Azure's native mechanisms for Infrastructure as Code (IaC). They operate directly on the Azure Resource Manager (ARM) layer, which is the orchestration layer that processes all operations in Azure, whether through portal, CLI, PowerShell, or API.

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Bicep is a domain-specific language (DSL) that compiles to ARM Template JSON. All Bicep functionality is available in ARM, but Bicep has much more readable and concise syntax. The AZ-104 requires you to be able to read and interpret both.


3. Concept Construction​

3.1 ARM Template Structure​

An ARM Template is a JSON file with a well-defined structure. Each section has a specific purpose:

{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"variables": {},
"functions": [],
"resources": [],
"outputs": {}
}
SectionRequiredPurpose
$schemaYesURL that defines the template schema version
contentVersionYesTemplate version defined by the author (free string)
parametersNoInput values provided at deployment time
variablesNoValues calculated internally, reused in the template
functionsNoCustom template functions (rarely used)
resourcesYesThe Azure resources that will be created or modified
outputsNoValues returned after deployment (e.g., VM IP, app URL)

3.2 The Parameters Section​

Parameters make the template reusable. Instead of hardcoding "brazilsouth" as location, you define a location parameter and pass the value at deployment time.

"parameters": {
"location": {
"type": "string",
"defaultValue": "brazilsouth",
"allowedValues": [
"brazilsouth",
"eastus",
"westeurope"
],
"metadata": {
"description": "Azure region for resource deployment"
}
},
"storageAccountName": {
"type": "string",
"minLength": 3,
"maxLength": 24,
"metadata": {
"description": "Storage account name (must be globally unique)"
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Administrator password - secure value, does not appear in logs"
}
}
}

Available parameter types: string, securestring, int, bool, object, secureObject, array.

The securestring and secureObject types are used for sensitive values: the value is not logged or shown in deployment history.

3.3 The Variables Section​

Variables are values calculated once and reused. They help avoid repetition and make the template more readable.

"variables": {
"storageAccountSku": "Standard_LRS",
"vnetAddressPrefix": "10.0.0.0/16",
"subnetFrontendPrefix": "10.0.1.0/24",
"uniqueSuffix": "[uniqueString(resourceGroup().id)]",
"storageNameWithSuffix": "[concat(parameters('storageAccountName'), variables('uniqueSuffix'))]"
}

The [...] (brackets) notation indicates an ARM expression: a value calculated at runtime. The difference between parameters and variables is that parameters receive values from outside (at deployment); variables are calculated internally.

3.4 ARM Functions​

ARM Template has a rich set of functions that can be used in expressions:

CategoryMain FunctionsExample
Stringconcat, toLower, toUpper, substring, length, trimconcat('storage', uniqueString(resourceGroup().id))
ResourceresourceGroup, subscription, resourceId, referenceresourceGroup().location
Arraylength, first, last, concat, unionlength(parameters('allowedIPs'))
Mathadd, sub, mul, div, modadd(parameters('instanceCount'), 1)
Logicalif, and, or, not, boolif(equals(parameters('env'), 'prod'), 'Premium', 'Standard')
Deploymentdeployment, environmentdeployment().name
UniqueuniqueString, newGuiduniqueString(resourceGroup().id)

The uniqueString() function is very important: it generates a deterministic 13-character hash from the input value. If you pass resourceGroup().id, the result is always the same for the same resource group, making it useful for generating unique names per resource group.

The reference() function allows reading properties of an already created resource, including resources created in the same template. For example, reference(resourceId('Microsoft.Network/publicIPAddresses', 'my-ip')).ipAddress returns the IP assigned to the public IP after its creation.

3.5 The Resources Section: The Heart of the Template​

Each resource has a required structure:

"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[variables('storageNameWithSuffix')]",
"location": "[parameters('location')]",
"sku": {
"name": "Standard_LRS"
},
"kind": "StorageV2",
"properties": {
"accessTier": "Hot",
"minimumTlsVersion": "TLS1_2",
"allowSharedKeyAccess": false
}
}
]
FieldRequiredPurpose
typeYesThe Azure resource type (Microsoft.Provider/resourceType)
apiVersionYesThe resource provider API version
nameYesThe resource name
locationYes (for most)The region where the resource is created
skuVariesThe service tier
kindVariesThe resource subtype
propertiesVariesResource-specific configurations
dependsOnNoExplicit dependencies on other resources
tagsNoOrganization metadata

3.6 DependsOn: Order Control​

ARM processes resources in parallel by default. To ensure one resource is created before another, use dependsOn:

{
"type": "Microsoft.Compute/virtualMachines",
"name": "my-vm",
"dependsOn": [
"[resourceId('Microsoft.Network/networkInterfaces', 'nic-my-vm')]",
"[resourceId('Microsoft.Compute/availabilitySets', 'as-web')]"
],
...
}

Important behavior: when you use the reference() or resourceId() function to reference another resource in the template, ARM automatically infers the dependency. Explicit dependsOn is only necessary when the dependency cannot be inferred via direct reference.

3.7 The Outputs Section​

Outputs return information about created resources for later use (in pipelines, scripts, nested modules):

"outputs": {
"storageAccountEndpoint": {
"type": "string",
"value": "[reference(variables('storageNameWithSuffix')).primaryEndpoints.blob]"
},
"vmPublicIpAddress": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Network/publicIPAddresses', 'pip-my-vm')).ipAddress]"
}
}

3.8 Bicep File Structure​

Bicep has more concise and readable syntax. See the comparison:

ARM Template (JSON):

{
"$schema": "...",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"type": "string",
"defaultValue": "brazilsouth"
}
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "myaccount",
"location": "[parameters('location')]",
"sku": { "name": "Standard_LRS" },
"kind": "StorageV2",
"properties": {
"accessTier": "Hot"
}
}
]
}

Bicep (equivalent):

param location string = 'brazilsouth'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'myaccount'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
}
}

The verbosity reduction is considerable. Bicep eliminates expression brackets, unnecessary quotes, and nested JSON structure.

3.9 Main Bicep Elements​

Parameters:

@description('Region for deployment')
@allowed(['brazilsouth', 'eastus'])
param location string = 'brazilsouth'

@description('Secure password')
@secure()
param adminPassword string

@minLength(3)
@maxLength(24)
param storageAccountName string

Variables:

var uniqueSuffix = uniqueString(resourceGroup().id)
var fullStorageName = '${storageAccountName}${uniqueSuffix}'
var storageSkuName = environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'

Outputs:

output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob
output storageAccountId string = storageAccount.id

Reference to other resources (implicit dependency):

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: fullStorageName
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
properties: {}
}

resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount // implicit dependency via 'parent'
name: 'default'
properties: {
deleteRetentionPolicy: {
enabled: true
days: 7
}
}
}

Modules (equivalent to nested templates in ARM):

module vnetModule './modules/vnet.bicep' = {
name: 'vnet-deploy'
params: {
vnetName: 'vnet-production'
addressPrefix: '10.0.0.0/16'
location: location
}
}

// Access module output
output vnetId string = vnetModule.outputs.vnetId

4. Structural View​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

5. How It Works in Practice​

Deployment Mode: Incremental vs. Complete​

This is one of the most critical behaviors to understand and one that causes the most unexpected impacts:

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular
  • Incremental (default): the template defines resources that should exist. Existing resources in the resource group but absent from the template are preserved.
  • Complete: the template defines the complete desired state. Existing resources in the resource group but absent from the template are deleted.

Complete mode is powerful for keeping the resource group exactly as defined in the template, but highly destructive if used carelessly.

What-If: Preview Changes​

The --what-if command (and --confirm-with-what-if) lets you see exactly what changes will be made before running the deployment:

az deployment group what-if \
--resource-group rg-production \
--template-file main.bicep \
--parameters @parameters.json

The result shows each resource with its predicted status: + Create, ~ Modify, - Delete, = Nochange, x Ignore.

Deployment Scopes​

Templates can be deployed at different scopes:

ScopeCLI CommandWhat it can create
Resource Groupaz deployment group createResources within the specified RG
Subscriptionaz deployment sub createResource Groups, subscription-level resources (policies, RBAC)
Management Groupaz deployment mg createMG-level policies, RBAC
Tenantaz deployment tenant createTenant configurations, Management Groups

The targetScope in Bicep defines the file scope:

targetScope = 'subscription'  // or 'resourceGroup' (default), 'managementGroup', 'tenant'

6. Implementation Methods​

6.1 Azure Portal: Template Specs and Deployment​

Direct template deployment in portal: Create a resource > Deploy a custom template

Create Template Spec (versioned reusable template library in Azure):

az ts create \
--name template-standard-vnet \
--resource-group rg-templates \
--version "1.0" \
--template-file vnet.bicep \
--display-name "Corporate Standard VNet"

6.2 Azure CLI​

ARM Template deployment:

az deployment group create \
--resource-group rg-production \
--template-file main.json \
--parameters @parameters.prod.json \
--parameters adminPassword="MySecurePassword123" \
--mode Incremental \
--name "deploy-$(date +%Y%m%d%H%M%S)"

Bicep deployment (CLI compiles automatically):

az deployment group create \
--resource-group rg-production \
--template-file main.bicep \
--parameters location=brazilsouth \
storageAccountName=mystorage \
--mode Incremental

Compile Bicep to ARM Template manually:

# Compile Bicep to ARM JSON
az bicep build --file main.bicep

# Decompile ARM JSON to Bicep (best effort, may need adjustments)
az bicep decompile --file main.json

Validate syntax without deployment:

# Validate template before deployment
az deployment group validate \
--resource-group rg-production \
--template-file main.bicep \
--parameters @parameters.json

Monitor deployment history:

# List resource group deployments
az deployment group list \
--resource-group rg-production \
--output table

# View specific deployment details and results
az deployment group show \
--resource-group rg-production \
--name "deploy-20240315120000"

# View deployment outputs
az deployment group show \
--resource-group rg-production \
--name "deploy-20240315120000" \
--query properties.outputs

6.3 PowerShell​

# Bicep Deploy
New-AzResourceGroupDeployment `
-ResourceGroupName "rg-producao" `
-TemplateFile "./main.bicep" `
-TemplateParameterFile "./parameters.prod.json" `
-Mode Incremental `
-Name "deploy-$(Get-Date -Format 'yyyyMMddHHmmss')" `
-Verbose

# What-If
Get-AzResourceGroupDeploymentWhatIfResult `
-ResourceGroupName "rg-producao" `
-TemplateFile "./main.bicep" `
-TemplateParameterFile "./parameters.prod.json"

7. Control and Security​

Secret Management in Templates​

Templates should not contain hardcoded secrets. The correct practice is to use securestring parameters and provide values via:

  1. Parameter file not versioned (.gitignore for parameters.prod.json files with secrets)
  2. Key Vault reference in parameter file:
// parameters.prod.json
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"adminPassword": {
"reference": {
"keyVault": {
"id": "/subscriptions/<sub-id>/resourceGroups/rg-secrets/providers/Microsoft.KeyVault/vaults/meu-keyvault"
},
"secretName": "vm-admin-password"
}
}
}
}

With this approach, the password value never appears in the parameter file or in the deployment history.

Locks and Resource Protection in Templates​

Templates can create resource locks like any other resource:

resource lock 'Microsoft.Authorization/locks@2020-05-01' = {
name: 'lock-storage-producao'
scope: storageAccount
properties: {
level: 'CanNotDelete'
notes: 'Production storage protected against accidental deletion'
}
}

8. Decision Making​

ARM Template vs. Bicep: which to use?​

SituationChoiceReason
New project, new infrastructureBicepCleaner syntax, better DX, modern tooling
Existing template in ARM JSONKeep ARM or decompileDecompile as starting point, review before using
Integration with system that only accepts JSONARM TemplateBicep compiles to ARM, use generated JSON
Modules and compositionBicepBicep modules are much simpler than ARM linked templates
Team with no experience in eitherBicepLower learning curve

Incremental vs. Complete mode: when to use?​

SituationModeCaution
Deploy new resourcesIncrementalSafe, preserves what exists
Update existing resourcesIncrementalSafe
Ensure RG is exactly as templateCompleteDangerous: deletes unlisted resources
Cleanup legacy resourcesComplete with reviewRun --what-if first to see what will be deleted

9. Best Practices​

Version templates alongside application code: infrastructure templates should be in the same Git repository as the application code they support, with the same branching, code review, and CI/CD practices.

Use parameters for everything that changes between environments: location, SKUs, names, instance counts. Never hardcode values that differ between dev, staging, and production.

Prefer uniqueString() for generating unique names instead of timestamps: uniqueString(resourceGroup().id) is deterministic for the same resource group, generating the same result in repeated deployments. A timestamp would generate a different name for each deployment, potentially creating duplicate resources.

Use --what-if before any production deployment: especially before using Complete mode or before deployments that modify critical resources. Review each expected change before confirming.

Document parameters and outputs: use the description property (in ARM) and the @description() decorator (in Bicep) to document the purpose of each parameter and output. This makes it easier for other team members to use the template.


10. Common Errors​

Using Complete mode without checking what will be deleted

A template with 5 resources is deployed with --mode Complete in a resource group that has 8 resources (the 5 from the template plus 3 created manually later). The 3 additional resources are deleted silently. Always use --what-if before any Complete deployment.

Hardcoding apiVersion and not updating

An outdated apiVersion may not support the latest properties of a resource, or may be close to being discontinued. Regularly check the resource provider documentation to use supported and current versions.

Confusing variables with parameters

Parameters receive external values at deployment; variables are calculated internally. A common mistake is putting values that should be parameters in variables (losing the ability to customize), or putting values that are always the same in parameters (adding unnecessary complexity).

Expecting that dependsOn is always necessary

ARM automatically infers dependencies when you use reference() or resourceId() referencing another template resource. Adding explicit dependsOn on top of implicit references is redundant and increases verbosity without benefit. Use dependsOn only when the dependency exists but cannot be expressed via direct reference.

Using securestring but logging the value in the application

The securestring type protects the value in ARM, but if the application logs the environment variable containing the secret, the value is exposed in the logs. The securestring protection is limited to the ARM control plane.


11. Operations and Maintenance​

Deployment History​

Each deployment creates an entry in the resource group history, visible for 90 days. The deployment name is important for traceability:

# List last 10 deployments with status
az deployment group list \
--resource-group rg-producao \
--query "[0:10].{Nome:name, Estado:properties.provisioningState, Horario:properties.timestamp}" \
--output table

Important limit: deployment history has a limit of 800 entries per resource group. After reaching this limit, new deployments fail with history limit error. In environments with many deployments (frequent CI/CD), configure automatic history cleanup or use deployment names that replace old entries (fixed name like deploy-main that gets overwritten).

Deployment Rollback​

ARM does not have automatic rollback. To revert to a previous state, you need to:

  1. Have the template and parameters of the previous version in version control
  2. Make a new deployment with the previous version
# Rollback: redeploy previous version
git checkout <previous-commit>
az deployment group create \
--resource-group rg-producao \
--template-file main.bicep \
--parameters @parameters.prod.json

12. Integration and Automation​

CI/CD Pipeline with GitHub Actions​

# .github/workflows/infrastructure.yml
name: Deploy Infrastructure

on:
push:
branches: [main]
paths: ['infrastructure/**']

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: What-If
run: |
az deployment group what-if \
--resource-group rg-producao \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/parameters.prod.json

- name: Deploy
run: |
az deployment group create \
--resource-group rg-producao \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/parameters.prod.json \
--name "deploy-${{ github.run_id }}"

Template Specs: Centralized Library​

Template Specs allow storing versioned templates in Azure and reusing them:

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular
// Reference a Template Spec in a Bicep
module vnet 'ts:/subscriptions/<sub-id>/resourceGroups/rg-templates/providers/Microsoft.Resources/templateSpecs/vnet-padrao/versions/1.0' = {
name: 'vnet-deploy'
params: {
vnetName: 'vnet-producao'
location: location
}
}

13. Final Summary​

Essential points:

  • ARM Templates (JSON) and Bicep are forms of declarative Infrastructure as Code in Azure. You define the desired state; ARM decides how to get there.
  • Bicep compiles to ARM Template JSON. All Bicep is supported in ARM, but Bicep has more concise and modern syntax.
  • The template's resources section defines what will be created. parameters receive external values. variables calculate internal values. outputs return results.
  • ARM automatically infers dependencies via reference() and resourceId(). Explicit dependsOn is only necessary when the dependency cannot be inferred.

Critical differences:

  • Incremental mode (default): template resources are created/updated; existing resources absent from template are preserved.
  • Complete mode: template resources are created/updated; existing resources absent from template are deleted.
  • securestring vs. string: securestring does not appear in ARM logs; string does. Use securestring for any sensitive value.
  • variables vs. parameters: variables are calculated internally and cannot be changed at deployment; parameters receive external values and customize the template.
  • uniqueString(resourceGroup().id) is deterministic (same result for the same RG in repeated deployments). A timestamp would be unique but different for each deployment.

What needs to be remembered:

  • ARM expression uses brackets [...] to denote calculated values. Everything outside brackets is a string literal.
  • --what-if is mandatory before any production deployment, especially with Complete mode.
  • Deployment history has a limit of 800 entries per resource group. In frequent pipelines, use fixed deployment names or configure cleanup.
  • For secrets in parameters, use Key Vault reference in the parameter file instead of putting the value directly.
  • Bicep uses parent: to express resource hierarchy (e.g., blob service within a storage account), which automatically creates the dependency and correct hierarchical name.
  • targetScope in Bicep defines whether the file deploys to resource group (default), subscription, management group, or tenant.