ARM And Bicep

Exercise 7. Exporting a Template from Azure Portal

Goal: See your week‑one Ubuntu VM and its network as Infrastructure as Code by exporting the deployment to an ARM template. This will give you a concrete reference before we author templates ourselves.

Estimated time: 15–25 minutes


Learning outcomes

  • Understand how Azure resources are represented in ARM.
  • Identify parameters, variables, resources, and dependsOn.
  • Map resource types to our diagram: VNet/Subnet, NSG, Public IP, NIC, Ubuntu VM.

Prerequisites

  • You already created the VM in the Azure Portal (from Exercise 1/2/3/5/6).
  • Access to the Resource Group that holds the VM and its networking.

What you’ll produce

  • A downloaded ZIP containing template.json and parameters.json (generated by the portal).
  • A short note with the resource types you found and how they relate.

Steps

  1. Open your Resource Group

    »

Exercise 8. Minimal ARM Template: Ubuntu VM + Network

Goal: Author the smallest viable ARM template that deploys an Ubuntu VM with a minimal network (VNet/Subnet, NSG, Public IP, NIC). Preview with what-if, deploy, and capture the public IP as an output.

Estimated time: 40–60 minutes


Learning outcomes

  • Structure an ARM template: parameters, variables, resources, outputs.
  • Deploy to a resource group with az deployment group and use what-if.
  • Wire up NSG → NIC → VM and output the Public IP.

Prerequisites

  • Azure CLI, VS Code.
  • A resource group (or create one).
  • Your SSH public key (~/.ssh/id_rsa.pub or id_ed25519.pub).

What you’ll produce

  • main.json — minimal template.
  • parameters.dev.json — parameter file for your environment.
  • A successful deployment and the public IP printed as an output.

Steps

  1. Scaffold files
    Create a new folder and two files: main.json and parameters.dev.json.

main.json (paste & save):

»

Exercise 9. From ARM to Bicep: Ubuntu VM + Network

Goal: Rebuild Exercise 8 using Bicep instead of ARM JSON. Produce a clean main.bicep and a .bicepparam file, preview with what-if, then deploy.

Estimated time: 40–60 minutes


Learning outcomes

  • Author resources in Bicep and understand how it compiles to ARM.
  • Use .bicepparam files for environment-specific values.
  • Output the Public IP and verify SSH/Nginx like in Exercise 8.

Prerequisites

  • Exercise 8 completed (or read through).
  • Azure CLI + Bicep installed (az bicep install or standalone).
  • SSH public key available.

What you’ll produce

  • main.bicep — minimal VM + network in Bicep.
  • dev.bicepparam — parameters for your environment.
  • A successful deployment and the public IP printed as an output.

Steps

  1. Create a bicep template

Create main.bicep and paste:

»

Exercise 10. Nginx via Cloud-init (bash) and Custom Script Extension

Goal: Install Nginx on an Ubuntu VM using two approaches and compare them:

  1. Cloud-init (customData) — runs at VM creation, script as a separate bash file
  2. Custom Script extension — can run on an existing VM, script as a separate bash file

Estimated time: 45–60 minutes


Learning outcomes

  • Pass a bash script to cloud-init via customData and loadTextContent().
  • Use the Custom Script extension to execute a bash script on an existing VM.
  • Know when to choose cloud-init vs extension.

Prerequisites

  • Exercise 9 (Bicep) or similar main.bicep + dev.bicepparam in place.
  • Azure CLI and Bicep installed.
  • An Ubuntu VM template (from earlier exercises).

What you’ll produce

  • cloud-init.sh — bash script for cloud-init.
  • install-nginx.sh — bash script for the Custom Script extension.
  • Updated main.bicep and dev.bicepparam that wire both methods.

Part A — Cloud-init (runs on new VM creation)

  1. Create cloud-init_nginx.sh (keep it minimal; no HTML changes):
#!/bin/bash
apt-get update
apt-get install -y nginx
  1. Update main.bicep — add parameter and pass it to osProfile.customData:
@description('User-data passed to cloud-init; start the script with #!/bin/bash')
param cloudInitContent string = ''

// in the VM resource:
osProfile: {
  computerName: vmName
  adminUsername: adminUsername
  customData: base64(cloudInitContent)
  linuxConfiguration: {
    disablePasswordAuthentication: true
    ssh: {
      publicKeys: [{ path: '/home/${adminUsername}/.ssh/authorized_keys', keyData: adminPublicKey }]
    }
  }
}
  1. Update dev.bicepparam — load the bash script from file:
param cloudInitContent = loadTextContent('./cloud-init_nginx.sh')
  1. Deploy (requires a new VM)
    Cloud-init is only read during creation:
az deployment group create -g rg-bicep-demo2 -f main.bicep -p dev.bicepparam adminPublicKey=@~/.ssh/id_rsa.pub

az deployment group show -g rg-bicep-demo2 -n main --query properties.outputs.publicIpAddress.value -o tsv

Verification

  • Browse to your public IP and verify Nginx welcome page

Part B — Custom Script Extension (works on existing VM)

  1. Create cse_deploy_webpage.sh (sh; minimal):
#!/bin/sh
set -eu # Exit on error, treat unset variables as an error

# Create the web root even if nginx hasn’t created it yet
install -d -m 0755 /var/www/html

# Write the web page
cat >/var/www/html/index.html <<'HTML'
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Azure VM — Nginx</title></head>
<body><h1>It works 🎉</h1><p>Deployed via Azure <strong>Custom Script Extension</strong>.</p></body>
</html>
HTML
  1. Add parameter in dev.bicepparam for script content:
param customScriptContent = loadTextContent('./cse_deploy_webpage.sh')
  1. Add extension resource to main.bicep (after VM resource):
@description('Custom Script content to execute on the VM (bash)')
param customScriptContent string = ''

resource vmExt 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = {
  name: 'deploy-webpage'
  parent: vm
  location: location
  properties: {
    publisher: 'Microsoft.Azure.Extensions'
    type: 'CustomScript'
    typeHandlerVersion: '2.1'
    autoUpgradeMinorVersion: true
    settings: {
      // Inline script executed by /bin/sh (extension decodes Base64 and runs it as script.sh)
      script: base64(customScriptContent)
    }
  }
}
  1. Deploy (can be applied to an existing VM)
    This will add/update only the extension:
az deployment group what-if -g rg-bicep-demo2 -f main.bicep -p dev.bicepparam adminPublicKey=@~/.ssh/id_rsa.pub
az deployment group create -g rg-bicep-demo2 -f main.bicep -p dev.bicepparam adminPublicKey=@~/.ssh/id_rsa.pub

Verify

curl <public-ip>

You can decode the string from what-if like this

»