Week2
2. Provision a Virtual Machine on Azure using an ARM template
Introduction
This tutorial is designed to give a basic understanding of cloud concepts to those who are interested in learning how to set up a virtual machine (VM) on Azure using an ARM template. We will go through the process step-by-step deploying an Ubuntu 22.04 LTS VM, creating a minimalistic solution that can be used as a foundation for further development.
Method
- We will use an ARM template to define and provision the Azure resources required for our VM, including network configurations and security settings.
- The ARM template will be launched using the Azure CLI, having a code-first approach to cloud infrastructure.
- Access to the deployed VM will be via password (in order to adhere to the minimalistic approach).
Prerequisites
- An Azure account. If you don’t have one, sign up here.
- Basic familiarity with JSON and the Azure Command-Line Interface (CLI).
- For Windows users: GitBash or similar terminal (Mac and Linux users can use the pre-installed terminal)
Provision a Virtual Machine Using ARM Template
Prepare Your ARM Template
»3. Install PHP and Create a Simple Form Handler
๐ฏ Goal
Install PHP on your Ubuntu VM and create a simple web application that processes HTML form submissions, demonstrating basic server-side processing with PHP.
๐ Prerequisites
Before beginning this exercise, you should:
- Have completed the previous VM creation exercises
- Have an Ubuntu VM running with Nginx installed
- Be able to SSH into your VM
- Understand basic HTML form structure
๐ Learning Objectives
By the end of this exercise, you will:
»4. Install the LEMP Stack on an Ubuntu VM Using Azure CLI
Overview
This tutorial explains how to install the LEMP stack (Linux, Nginx, MySQL, PHP) on an Ubuntu Virtual Machine (VM).
Step 1: Provision the Virtual Machine
Create a resource group:
az group create --name LempRG --location northeuropeProvision the Ubuntu VM:
az vm create \ --resource-group LempRG \ --name LempVM \ --image Ubuntu2204 \ --size Standard_B1s \ --admin-username azureuser \ --generate-ssh-keysOpen port 80 for HTTP traffic:
az vm open-port --resource-group LempRG --name LempVM --port 80Retreive the Public IP of the VM:
»
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
»