AZ-104 Refresher Chapter 4 · ARM templates & Bicep

Chapter 4

ARM templates & Bicep

📖 Theory 25 min🛠 Lab 20 minCompute · 20–25%

Clicking in the portal is fine for learning, but real environments are deployed as code. The exam expects you to read a template, fix one, deploy one and export one. In the lab you extend the network from chapter 3 with Bicep.

Part A

Theory

Azure Resource Manager

Every way of managing Azure (the portal, CLI, PowerShell, Terraform, templates) ends up as a call to the same API: Azure Resource Manager (ARM). ARM authenticates you, checks RBAC, Policy and locks, and then passes the request to a resource provider such as Microsoft.Network.

ARM templates and Bicep files describe the end state you want ("a VNet with these subnets"), not the steps to get there. That makes them:

main.bicepyou write this build main.jsonARM template decompile Azure ResourceManagerRBAC · Policy · locks Microsoft.Network Microsoft.Compute …
Bicep is a nicer language on top of ARM JSON. The CLI compiles it for you when you deploy, so ARM only ever sees JSON.

ARM template structure (JSON)

An ARM template is a JSON file with up to six top-level sections. Only $schema, contentVersion and resources are required:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageName": { "type": "string", "minLength": 3, "maxLength": 24 },
    "sku": { "type": "string", "defaultValue": "Standard_LRS",
             "allowedValues": [ "Standard_LRS", "Standard_ZRS" ] }
  },
  "variables": {
    "location": "[resourceGroup().location]"
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2023-05-01",
      "name": "[parameters('storageName')]",
      "location": "[variables('location')]",
      "sku": { "name": "[parameters('sku')]" },
      "kind": "StorageV2",
      "properties": { "minimumTlsVersion": "TLS1_2" }
    }
  ],
  "outputs": {
    "blobEndpoint": { "type": "string",
      "value": "[reference(parameters('storageName')).primaryEndpoints.blob]" }
  }
}

What each part does:

Anything between [ ] is an expression. Functions you should recognise: parameters(), variables(), resourceGroup().location, concat(), uniqueString() (a deterministic hash, great for globally unique names), resourceId() and reference().

In JSON, if resource B needs resource A, you must say so with "dependsOn": [ "[resourceId(...)]" ]. Otherwise ARM may try to create them at the same time and fail.

Parameter values usually live in a separate parameter file (main.parameters.json), so the same template can deploy dev, test and prod.

The same thing in Bicep

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

@allowed([ 'Standard_LRS', 'Standard_ZRS' ])
param sku string = 'Standard_LRS'

param location string = resourceGroup().location

resource st 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageName
  location: location
  sku: { name: sku }
  kind: 'StorageV2'
  properties: { minimumTlsVersion: 'TLS1_2' }
}

output blobEndpoint string = st.properties.primaryEndpoints.blob

Bicep does exactly what the JSON does. The differences that matter:

Exam trap

The exam shows you a template and asks "how many resources will be created?" or "in which region?". Read the copy loop / for count, the if condition and the default value of location carefully. resourceGroup().location means "the resource group's region", not the region you happen to be in.

Deploying

A deployment targets one of four scopes, and the CLI command tells you which:

ScopeAzure CLIPowerShellTypical content
Resource groupaz deployment group createNew-AzResourceGroupDeploymentVNets, VMs, storage
Subscriptionaz deployment sub createNew-AzSubscriptionDeploymentResource groups, policy, RBAC, budgets
Management groupaz deployment mg createNew-AzManagementGroupDeploymentPolicy, RBAC
Tenantaz deployment tenant createNew-AzTenantDeploymentManagement groups

Incremental vs Complete mode

Resource-group deployments have two modes. This is a favourite exam topic:

Resource group before: A, B, C — template contains: A (changed), D Incremental (default) A′ B C D B and C are untouched. Complete A′ B ✕ C ✕ D B and C are deleted.
Run what-if before any Complete deployment.

There's a subtle trap even in Incremental mode: a resource's properties are replaced as a whole. If you redeploy a VNet template whose subnets array lists only one subnet, ARM tries to remove the other subnets from the VNet (it fails if they're in use). That's why the lab adds a subnet as a separate child resource on an existing VNet.

What-if and deployment history

What-if (az deployment group what-if, or create --confirm-with-what-if) shows what would be created, modified or deleted, without changing anything. Use it every time.

Every deployment is recorded under the resource group's Deployments blade, including portal deployments. There you can see the inputs, outputs, errors and the exact template that was used. Azure keeps up to 800 deployments per resource group and cleans up old ones automatically.

Exporting and converting

You can get a template out of Azure in two ways, and the exam asks about the difference:

Converting between formats:

AZ-305 link

In AZ-305, infrastructure as code shows up in governance and landing zone designs: deploy policies, RBAC and management groups from code, use template specs or a module registry to share approved building blocks, and deployment stacks to manage and protect a group of resources as one unit.

Self-check

1. A resource group contains VM1, VM2 and a VNet. You deploy a template containing only the VNet in Complete mode. What happens?

B. Complete mode deletes resources that aren't in the template (unless they're locked).

2. Which Bicep feature lets you use a VNet that was deployed earlier, without redeploying it?

D. existing references a resource without managing it.

3. You need the exact template used for last week's deployment of a web app. Where do you get it?

A. "Export template" on the group gives the current state, not the original template.

4. Which command deploys a Bicep file that creates resource groups?

C. Resource groups live in a subscription, so it's a subscription-scope deployment.

Part B

Lab: extend the network with code

You read an exported template, add snet-app and nsg-web with Bicep (both are used later), and fix an ARM template that creates the Log Analytics workspace for chapter 9. Work in a terminal on your Linux VM; use nano (or any editor you like) to create files. Paste the variables first:

RG=rg-az104-lab; LOC=westeurope; LOC2=northeurope; INIT=xx
mkdir -p ~/az104 && cd ~/az104
  1. Task

    Read an exported template

    Export the template of vnet-spoke and answer these questions from the template, not from the portal: which parameters did the export create, where is snet-web defined (and is it defined more than once?), and how is the route table linked?

    Done when
    • You can answer the three questions.
    • You have the template as Bicep too.
    Solution · Portal

    vnet-spoke → Automation → Export template. Switch between the ARM template and Bicep tabs. Exports usually create one parameter per referenced resource name (the VNet, the route table). snet-web shows up twice: inside the VNet's subnets array and as a separate virtualNetworks/subnets resource. The route table is linked with a routeTable.id on the subnet.

    Solution · Azure CLI
    VNETID=$(az network vnet show -g $RG -n vnet-spoke --query id -o tsv)
    az group export -n $RG --resource-ids $VNETID > spoke.json
    jq '.parameters | keys' spoke.json
    jq '.resources[] | {type, name}' spoke.json
    az bicep decompile --file spoke.json      # creates spoke.bicep
    cat spoke.bicep
  2. Task

    Write a Bicep file that adds two resources

    Write network-add.bicep that creates:

    • Subnet snet-app 10.20.3.0/24 in the existing vnet-spoke, delegated to Microsoft.Web/serverFarms (for App Service in chapter 8), as a private subnet.
    • An empty network security group nsg-web in the resource group's region (you add rules in chapter 6).

    The file must not redefine the VNet itself. Run a what-if before deploying.

    Done when
    • What-if shows exactly two creates and nothing deleted or modified.
    • After deploying, vnet-spoke has three subnets and snet-app shows the delegation.
    Hint

    Declare the VNet with existing, then the subnet as type Microsoft.Network/virtualNetworks/subnets with parent: vnet.

    Solution · Bicep file
    param location string = resourceGroup().location
    
    resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = {
      name: 'vnet-spoke'
    }
    
    resource snetApp 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = {
      parent: vnet
      name: 'snet-app'
      properties: {
        addressPrefix: '10.20.3.0/24'
        defaultOutboundAccess: false
        delegations: [
          {
            name: 'appservice'
            properties: { serviceName: 'Microsoft.Web/serverFarms' }
          }
        ]
      }
    }
    
    resource nsgWeb 'Microsoft.Network/networkSecurityGroups@2024-05-01' = {
      name: 'nsg-web'
      location: location
    }
    
    output subnetId string = snetApp.id
    Solution · Azure CLI
    nano network-add.bicep        # paste the file, save with Ctrl+O, Enter, exit with Ctrl+X
    az deployment group what-if -g $RG -f network-add.bicep
    az deployment group create  -g $RG -f network-add.bicep -n network-add
    az network vnet subnet show -g $RG --vnet-name vnet-spoke -n snet-app --query "delegations[].serviceName"
  3. Task

    Fix a broken ARM template

    Save the template below as law.json and deploy it to rg-az104-lab. It will fail. Find out why, then change the template so that:

    • the location is a parameter that defaults to the resource group's region;
    • the retention is a parameter, default 30 days, only accepting values from 30 to 730;

    and deploy it again.

    {
      "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "parameters": {
        "workspaceName": { "type": "string", "defaultValue": "log-az104" }
      },
      "resources": [
        {
          "type": "Microsoft.OperationalInsights/workspaces",
          "apiVersion": "2023-09-01",
          "name": "[parameters('workspaceName')]",
          "location": "eastus",
          "properties": {
            "sku": { "name": "PerGB2018" },
            "retentionInDays": 90
          }
        }
      ],
      "outputs": {
        "workspaceId": {
          "type": "string",
          "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspaceName'))]"
        }
      }
    }
    Done when
    • You know why the first deployment failed.
    • Workspace log-az104 exists in your lab region with 30-day retention. (Chapter 9 uses it.)
    Hint

    The error code is RequestDisallowedByPolicy. Which policy did you assign in chapter 2? For the limits, use minValue and maxValue.

    Solution · fixed template
    {
      "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "parameters": {
        "workspaceName": { "type": "string", "defaultValue": "log-az104" },
        "location": { "type": "string", "defaultValue": "[resourceGroup().location]" },
        "retentionInDays": { "type": "int", "defaultValue": 30, "minValue": 30, "maxValue": 730 }
      },
      "resources": [
        {
          "type": "Microsoft.OperationalInsights/workspaces",
          "apiVersion": "2023-09-01",
          "name": "[parameters('workspaceName')]",
          "location": "[parameters('location')]",
          "properties": {
            "sku": { "name": "PerGB2018" },
            "retentionInDays": "[parameters('retentionInDays')]"
          }
        }
      ],
      "outputs": {
        "workspaceId": {
          "type": "string",
          "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspaceName'))]"
        }
      }
    }

    The first attempt failed because eastus isn't allowed by your Allowed locations policy.

    Solution · Azure CLI
    nano law.json
    az deployment group create -g $RG -f law.json -n law      # 1st time: RequestDisallowedByPolicy
    # fix the file, then:
    az deployment group create -g $RG -f law.json -n law
    az monitor log-analytics workspace show -g $RG -n log-az104 --query "{loc:location, days:retentionInDays}"

    Portal alternative: Create a resource → Template deployment (deploy using custom templates) → Build your own template in the editor, paste, Save, Review + create.

  4. Task

    See what Complete mode would destroy, without destroying anything

    Find out what would happen if you deployed network-add.bicep to rg-az104-lab in Complete mode. Do not actually deploy it in Complete mode. Then find the deployment history of this resource group and the template of your law deployment.

    Done when
    • You've seen the list of resources that would be deleted. Which of them is protected by a lock?
    • You found the law deployment in the history.
    Solution · Azure CLI
    az deployment group what-if -g $RG -f network-add.bicep --mode Complete
    az deployment group list -g $RG --query "[].{name:name, state:properties.provisioningState}" -o table

    What-if shows almost everything as Delete, because none of it is in the file. What-if doesn't look at locks: vnet-hub has the CanNotDelete lock from chapter 3, so ARM couldn't delete it in a real run.

    Solution · Portal

    rg-az104-lab → Settings → Deployments → law → Template. (What-if is CLI/PowerShell only.)

Checkpoint
  • vnet-spoke now has snet-web, snet-data and snet-app (delegated to App Service).
  • nsg-web exists (no rules, not associated yet: chapter 6).
  • Log Analytics workspace log-az104 exists (used in chapter 9).

Log Analytics only costs money for data you send to it, so nothing yet.