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

  1. Create a resource group:

    az group create --name LempRG --location northeurope
    
  2. Provision the Ubuntu VM:

    az vm create \
      --resource-group LempRG \
      --name LempVM \
      --image Ubuntu2204 \
      --size Standard_B1s \
      --admin-username azureuser \
      --generate-ssh-keys
    
  3. Open port 80 for HTTP traffic:

    az vm open-port --resource-group LempRG --name LempVM --port 80
    
  4. Retreive 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.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

»