MOV
5. Provisioning a VM Using AZ CLI, Configuring Nginx, and Allowing HTTP Traffic
Overview
This exercise introduces creating a Virtual Machine (VM) with Azure CLI, using default SSH keys for secure authentication, installing Nginx, and configuring the network to allow HTTP traffic. You will work exclusively in the VSCode Integrated Terminal (Windows users: Make sure you use Git Bash).
Step 1: Set Up the Environment
Open VSCode and launch the Integrated Terminal:
- On Windows, use Git Bash inside VSCode.
- On Mac or Linux, use the default terminal.
Verify that Azure CLI is installed by running:
»
5. Securely Manage Servers Behind a Bastion Host
In this tutorial, you’ll learn how to copy files from your local laptop to a web server running in Azure.
We’ll cover two scenarios:
- Direct access (server has a public IP).
- Indirect access via a bastion host (server only has a private IP).
We’ll also use a staging folder (/tmp/app) on the server to keep things clean and make updates easier.
1. Local Project Setup
On your laptop, create a folder named app/ with three files:
6. Automating VM Creation, Nginx Installation, and Port Configuration Using a Bash Script
Overview
In this exercise, you will create a Bash script that automates the provisioning of a resource group, a VM, and the opening of port 80. You will also use a custom data file to automatically install and configure Nginx on the VM during provisioning. This exercise enables a “one-click” solution for deploying a web server.
Step 1: Prepare the Custom Data File
Create a file named
»custom_data_nginx.sh(You can use VSCode)
6. Use A Reverse Proxy In Front of the App
Introduction
This tutorial is designed for developers and system administrators looking to configure Nginx as a reverse proxy in front of an application server on Azure. Both servers will reside within the same virtual network.
Prerequisites
- An active Azure account. Sign up here if you don’t have one.
- Familiarity with Azure portal, cloud networking, and SSH.
- Basic knowledge of Nginx.
In order to “set the table” so that we can verify that the reverse proxy works as expected, let’s create two VMs - one Application Server and one Reverse Proxy. These two servers should be connected to the same subnet in this example.
»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
»