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.jsonandparameters.json(generated by the portal). - A short note with the resource types you found and how they relate.
Steps
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 groupand 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.puborid_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
- Scaffold files
Create a new folder and two files:main.jsonandparameters.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 installor 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
- 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:
- Cloud-init (
customData) — runs at VM creation, script as a separate bash file - 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
customDataandloadTextContent(). - 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.bicepparamin 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.bicepanddev.bicepparamthat wire both methods.
Part A — Cloud-init (runs on new VM creation)
- Create
cloud-init_nginx.sh(keep it minimal; no HTML changes):
#!/bin/bash
apt-get update
apt-get install -y nginx
- Update
main.bicep— add parameter and pass it toosProfile.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 }]
}
}
}
- Update
dev.bicepparam— load the bash script from file:
param cloudInitContent = loadTextContent('./cloud-init_nginx.sh')
- 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)
- 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
- Add parameter in
dev.bicepparamfor script content:
param customScriptContent = loadTextContent('./cse_deploy_webpage.sh')
- 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)
}
}
}
- 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
»