Level: Absolute Beginner
Terraform version: 1.15.9
AzureRM Provider: 5.9.0
Approach: Each lab builds on the previous lab.
These are four complete Terraform configurations designed for students to execute sequentially.
Learning Path
- Lab 1 — Azure Resource Group: Create your first Azure resource.
- Lab 2 — Azure Networking: Add VNet, subnet, NSG and subnet association.
- Lab 3 — Windows Server VM: Add Windows Server 2025, public IP and RDP access.
- Lab 4 — Ubuntu Server VM: Add Ubuntu Server 26.04 LTS, public IP and SSH access.
1. Selected Azure configuration
I checked Microsoft’s Azure Marketplace documentation and Canonical’s official Azure image catalog.
| Setting | Selected value |
|---|---|
| Azure region | eastus (East US) |
| Resource Group | rg-terraform-lab-eastus |
| VNet | vnet-terraform-lab |
| VNet CIDR | 10.20.0.0/16 |
| Subnet | snet-vms |
| Subnet CIDR | 10.20.1.0/24 |
| Windows OS | Windows Server 2025 Datacenter |
| Ubuntu OS | Ubuntu Server 26.04 LTS |
| VM size | Standard_B2s_v2 |
| Authentication | Username + password |
| Provider | hashicorp/azurerm 5.9.x |
Windows clarification: Microsoft’s September 2026 Marketplace catalog lists Windows Server 2025 as the newest Windows Server release, not Windows Server 2026.
Ubuntu: Canonical publishes Canonical:ubuntu-26_04-lts:server:latest as the Ubuntu 26.04 LTS Gen2 image.
The selected Standard_B2s_v2 is a burstable VM with 2 vCPUs and 8 GiB RAM, suitable for these introductory labs.
These image IDs are supported by the published catalogs. Availability in your particular subscription and region must still be checked before deployment.
2. Prerequisites — Complete once
You need an Azure subscription, Azure CLI, Terraform, and permissions to create resources.
Log in and select your subscription:
az login
az account list --output table
# Select the subscription you want to use
az account set --subscription "YOUR_SUBSCRIPTION_ID"
# Required for AzureRM provider 5.x
export ARM_SUBSCRIPTION_ID="$(az account show --query id -o tsv)"
# Check active subscription
az account show --output table
Have your subscription administrator ensure Microsoft.Compute and Microsoft.Network resource providers are registered. AzureRM 5.x no longer automatically registers them by default.
Check the images directly in Azure:
# Windows Server 2025 Datacenter
az vm image show \
--location eastus \
--urn MicrosoftWindowsServer:WindowsServer:2025-datacenter:latest \
--output table
# Ubuntu Server 26.04 LTS
az vm image show \
--location eastus \
--urn Canonical:ubuntu-26_04-lts:server:latest \
--output table
# Check selected VM size and regional restrictions
az vm list-skus \
--location eastus \
--size Standard_B2s_v2 \
--all \
--output table
Before Lab 3, obtain your public IPv4 address:
curl -4 https://api.ipify.org
You’ll use it to restrict RDP and SSH access to your own computer.
3. Important classroom rules
- No Terraform input variables (
variableor.tfvars). - No data sources.
- No Terraform modules or local values.
- No output blocks.
- Only native AzureRM resources, plus the required Terraform and provider configuration.
- Each lab provides a complete replacement
main.tf. - Use the same directory and Terraform state for all four labs. Do not create a new directory and state for each.
- Replace the sample VM passwords with your own strong passwords and use your real public IPv4
/32for the firewall rules. Carry these changes forward when advancing to the next lab.
The examples use passwords because this is a teaching requirement. Terraform stores these passwords in its state, so keep main.tf, state files, and saved plans private. Use SSH keys and secret management for real deployments.
Lab 1 — Create an Azure Resource Group
Level: Beginner
Objective: Learn the basic structure of a Terraform configuration and create your first Azure resource.
main.tf — Complete code
# ============================================
# LAB 1 - CREATE AZURE RESOURCE GROUP
# ============================================
# Terraform configuration
terraform {
required_version = ">= 1.15.0, < 2.0.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 5.9.0"
}
}
}
# Azure provider configuration
provider "azurerm" {
features {}
}
# Create Resource Group
resource "azurerm_resource_group" "lab" {
name = "rg-terraform-lab-eastus"
location = "eastus"
}
Execute Lab 1
Create a directory and save the code above as main.tf.
mkdir azure-terraform-labs
cd azure-terraform-labs
# Save the code as main.tf, then:
terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
Enter yes when Terraform asks for confirmation.
Expected result: 1 added, 0 changed, 0 destroyed.
Verify in Azure:
az group show \
--name rg-terraform-lab-eastus \
--output table
Student learns: Provider configuration, resource blocks, terraform init, plan, apply, and Terraform state.
Lab 2 — Create Azure Networking
Objective: Extend Lab 1 to create a VNet, subnet, Network Security Group (NSG), and associate the NSG with the subnet.
main.tf — Complete code, includes Lab 1
# ============================================
# LAB 2 - CREATE AZURE NETWORKING
# Includes all resources from Lab 1
# ============================================
terraform {
required_version = ">= 1.15.0, < 2.0.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 5.9.0"
}
}
}
provider "azurerm" {
features {}
}
# 1. Resource Group
resource "azurerm_resource_group" "lab" {
name = "rg-terraform-lab-eastus"
location = "eastus"
}
# 2. Virtual Network
resource "azurerm_virtual_network" "lab" {
name = "vnet-terraform-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
address_space = ["10.20.0.0/16"]
}
# 3. Subnet
resource "azurerm_subnet" "vms" {
name = "snet-vms"
resource_group_name = azurerm_resource_group.lab.name
virtual_network_name = azurerm_virtual_network.lab.name
address_prefixes = ["10.20.1.0/24"]
}
# 4. Network Security Group
resource "azurerm_network_security_group" "lab" {
name = "nsg-terraform-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
}
# 5. Associate NSG with the Subnet
resource "azurerm_subnet_network_security_group_association" "lab" {
subnet_id = azurerm_subnet.vms.id
network_security_group_id = azurerm_network_security_group.lab.id
}
Execute Lab 2
Replace the existing main.tf with the complete Lab 2 code, keeping the same working directory and state.
terraform fmt
terraform validate
terraform plan
terraform apply
Expected result: 4 added, 0 changed, 0 destroyed.
The existing Resource Group remains unchanged.
Verify the networking:
# Verify Virtual Network
az network vnet show \
--resource-group rg-terraform-lab-eastus \
--name vnet-terraform-lab \
--output table
# Verify Subnet
az network vnet subnet show \
--resource-group rg-terraform-lab-eastus \
--vnet-name vnet-terraform-lab \
--name snet-vms \
--output table
# Verify Network Security Group
az network nsg show \
--resource-group rg-terraform-lab-eastus \
--name nsg-terraform-lab \
--output table
Student learns: Azure VNet, CIDR, subnets, NSGs, resource references, resource dependencies, and how Terraform adds infrastructure to an existing state.
Lab 3 — Create a Windows Server 2025 Virtual Machine
Objective: Add a Windows Server VM to the infrastructure created in Labs 1 and 2.
New resources: Public IP, network interface, RDP security rule, and Windows VM.
The RDP rule uses 203.0.113.10/32, a documentation-only IP address. Replace this with your own public IPv4 address followed by /32 before running Terraform. Also change the demonstration password.
main.tf — Complete code, includes Labs 1 + 2
# ============================================
# LAB 3 - CREATE WINDOWS SERVER 2025 VM
# Includes all resources from Labs 1 and 2
# ============================================
terraform {
required_version = ">= 1.15.0, < 2.0.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 5.9.0"
}
}
}
provider "azurerm" {
features {}
}
# --------------------------------------------
# 1. Resource Group
# --------------------------------------------
resource "azurerm_resource_group" "lab" {
name = "rg-terraform-lab-eastus"
location = "eastus"
}
# --------------------------------------------
# 2. Virtual Network
# --------------------------------------------
resource "azurerm_virtual_network" "lab" {
name = "vnet-terraform-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
address_space = ["10.20.0.0/16"]
}
# --------------------------------------------
# 3. Subnet
# --------------------------------------------
resource "azurerm_subnet" "vms" {
name = "snet-vms"
resource_group_name = azurerm_resource_group.lab.name
virtual_network_name = azurerm_virtual_network.lab.name
address_prefixes = ["10.20.1.0/24"]
}
# --------------------------------------------
# 4. Network Security Group
# --------------------------------------------
resource "azurerm_network_security_group" "lab" {
name = "nsg-terraform-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
}
# --------------------------------------------
# 5. Associate NSG with Subnet
# --------------------------------------------
resource "azurerm_subnet_network_security_group_association" "lab" {
subnet_id = azurerm_subnet.vms.id
network_security_group_id = azurerm_network_security_group.lab.id
}
# --------------------------------------------
# 6. Allow RDP from Student's Public IP
# --------------------------------------------
resource "azurerm_network_security_rule" "rdp" {
name = "Allow-RDP"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = "203.0.113.10/32" # REPLACE with your IP
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.lab.name
network_security_group_name = azurerm_network_security_group.lab.name
}
# --------------------------------------------
# 7. Windows VM Public IP
# --------------------------------------------
resource "azurerm_public_ip" "windows" {
name = "pip-win2025-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
allocation_method = "Static"
sku = "Standard"
}
# --------------------------------------------
# 8. Windows VM Network Interface
# --------------------------------------------
resource "azurerm_network_interface" "windows" {
name = "nic-win2025-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.vms.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.windows.id
}
}
# --------------------------------------------
# 9. Windows Server 2025 Virtual Machine
# --------------------------------------------
resource "azurerm_windows_virtual_machine" "windows" {
name = "vm-win2025-01"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
size = "Standard_B2s_v2"
admin_username = "labadmin"
# Training only - replace with a unique strong password
admin_password = "ChangeMe!2026WinLab#"
network_interface_ids = [
azurerm_network_interface.windows.id
]
# OS Disk
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
# Windows Server 2025 Datacenter Image
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2025-datacenter"
version = "latest"
}
}
Execute Lab 3
Replace main.tf with the above, preserve any earlier changes, and then execute:
terraform fmt
terraform validate
terraform plan
terraform apply
Expected result: 4 added, 0 changed, 0 destroyed, assuming you have carried forward the earlier configuration unchanged.
Verify and connect to Windows
Get the public IP:
az network public-ip show \
--resource-group rg-terraform-lab-eastus \
--name pip-win2025-lab \
--query ipAddress \
--output tsv
Check the VM:
az vm show \
--resource-group rg-terraform-lab-eastus \
--name vm-win2025-01 \
--show-details \
--output table
Then use Windows App (Remote Desktop) on your Mac, or Remote Desktop Connection on Windows.
| Setting | Value |
|---|---|
| Host | Public IP returned above |
| Port | 3389 |
| Username | labadmin |
| Password | Your replacement Windows password |
| Operating System | Windows Server 2025 Datacenter |
Student learns: VM creation, image selection, managed OS disks, network interfaces, public IP addresses, NSG inbound rules, and RDP authentication.
Lab 4 — Create an Ubuntu Server 26.04 Virtual Machine
Objective: Extend the existing infrastructure by adding an Ubuntu Linux VM with password-based SSH authentication.
At the end of this lab, students will have both Windows and Ubuntu VMs running in the same Azure Virtual Network.
New resources: Linux public IP, Linux network interface, SSH security rule, and Ubuntu VM.
main.tf — Complete code, includes Labs 1 + 2 + 3
# ============================================
# LAB 4 - CREATE UBUNTU SERVER 26.04 VM
# Includes all resources from Labs 1, 2, 3
# ============================================
terraform {
required_version = ">= 1.15.0, < 2.0.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 5.9.0"
}
}
}
provider "azurerm" {
features {}
}
# --------------------------------------------
# 1. Resource Group
# --------------------------------------------
resource "azurerm_resource_group" "lab" {
name = "rg-terraform-lab-eastus"
location = "eastus"
}
# --------------------------------------------
# 2. Virtual Network
# --------------------------------------------
resource "azurerm_virtual_network" "lab" {
name = "vnet-terraform-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
address_space = ["10.20.0.0/16"]
}
# --------------------------------------------
# 3. Subnet
# --------------------------------------------
resource "azurerm_subnet" "vms" {
name = "snet-vms"
resource_group_name = azurerm_resource_group.lab.name
virtual_network_name = azurerm_virtual_network.lab.name
address_prefixes = ["10.20.1.0/24"]
}
# --------------------------------------------
# 4. Network Security Group
# --------------------------------------------
resource "azurerm_network_security_group" "lab" {
name = "nsg-terraform-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
}
# --------------------------------------------
# 5. Associate NSG with Subnet
# --------------------------------------------
resource "azurerm_subnet_network_security_group_association" "lab" {
subnet_id = azurerm_subnet.vms.id
network_security_group_id = azurerm_network_security_group.lab.id
}
# --------------------------------------------
# 6. Allow RDP from Student's Public IP
# --------------------------------------------
resource "azurerm_network_security_rule" "rdp" {
name = "Allow-RDP"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = "203.0.113.10/32" # REPLACE with your IP
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.lab.name
network_security_group_name = azurerm_network_security_group.lab.name
}
# --------------------------------------------
# 7. Allow SSH from Student's Public IP
# --------------------------------------------
resource "azurerm_network_security_rule" "ssh" {
name = "Allow-SSH"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "203.0.113.10/32" # REPLACE with your IP
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.lab.name
network_security_group_name = azurerm_network_security_group.lab.name
}
# --------------------------------------------
# 8. Windows VM Public IP
# --------------------------------------------
resource "azurerm_public_ip" "windows" {
name = "pip-win2025-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
allocation_method = "Static"
sku = "Standard"
}
# --------------------------------------------
# 9. Windows VM Network Interface
# --------------------------------------------
resource "azurerm_network_interface" "windows" {
name = "nic-win2025-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.vms.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.windows.id
}
}
# --------------------------------------------
# 10. Windows Server 2025 VM
# --------------------------------------------
resource "azurerm_windows_virtual_machine" "windows" {
name = "vm-win2025-01"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
size = "Standard_B2s_v2"
admin_username = "labadmin"
# Keep the same replacement password from Lab 3
admin_password = "ChangeMe!2026WinLab#"
network_interface_ids = [
azurerm_network_interface.windows.id
]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2025-datacenter"
version = "latest"
}
}
# --------------------------------------------
# 11. Ubuntu VM Public IP
# --------------------------------------------
resource "azurerm_public_ip" "ubuntu" {
name = "pip-ubuntu2604-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
allocation_method = "Static"
sku = "Standard"
}
# --------------------------------------------
# 12. Ubuntu VM Network Interface
# --------------------------------------------
resource "azurerm_network_interface" "ubuntu" {
name = "nic-ubuntu2604-lab"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.vms.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.ubuntu.id
}
}
# --------------------------------------------
# 13. Ubuntu Server 26.04 LTS VM
# --------------------------------------------
resource "azurerm_linux_virtual_machine" "ubuntu" {
name = "vm-ubuntu2604-01"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
size = "Standard_B2s_v2"
admin_username = "labadmin"
# Training only - replace with a unique strong password
admin_password = "ChangeMe!2026LinuxLab#"
# Enable SSH password authentication
disable_password_authentication = false
network_interface_ids = [
azurerm_network_interface.ubuntu.id
]
# OS Disk
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
# Ubuntu Server 26.04 LTS Gen2 Image
source_image_reference {
publisher = "Canonical"
offer = "ubuntu-26_04-lts"
sku = "server"
version = "latest"
}
}
Execute Lab 4
Replace the Lab 3 configuration with this complete Lab 4 configuration. Before applying, preserve your custom Windows password and RDP source IP, and set your own Ubuntu password and SSH source IP.
terraform fmt
terraform validate
terraform plan
terraform apply
Expected result: 4 added, 0 changed, 0 destroyed, provided the existing Windows VM and earlier resource configuration have been preserved.
Verify and connect to Ubuntu
Retrieve the public IP:
az network public-ip show \
--resource-group rg-terraform-lab-eastus \
--name pip-ubuntu2604-lab \
--query ipAddress \
--output tsv
Connect using SSH:
ssh labadmin@YOUR_UBUNTU_PUBLIC_IP
Enter the Ubuntu password you configured.
Once connected, verify the operating system:
cat /etc/os-release
hostnamectl
ip addr
Student learns: Linux VM provisioning, Ubuntu Marketplace images, password authentication, SSH, and running multiple operating systems in one Azure VNet.
4. Final infrastructure after all four labs
Final Azure resources (East US):
Azure Subscription
└── Resource Group: rg-terraform-lab-eastus
└── VNet: vnet-terraform-lab (10.20.0.0/16)
└── Subnet: snet-vms (10.20.1.0/24)
├── NSG: Restricted inbound RDP 3389 and SSH 22
├── Windows Server 2025 VM (NIC + Public IP, RDP)
└── Ubuntu Server 26.04 LTS VM (NIC + Public IP, SSH)
5. Expected Terraform resource count
| Lab | New Terraform resources | Cumulative |
|---|---|---|
| Lab 1 — Resource Group | 1 | 1 |
| Lab 2 — Networking | 4 | 5 |
| Lab 3 — Windows VM | 4 | 9 |
| Lab 4 — Ubuntu VM | 4 | 13 |
Resource counts exclude Azure-managed supporting objects such as the VM OS disks.
After Lab 4, run:
terraform state list
You should see 13 Terraform-managed resource addresses.
Finally, clean up the lab to avoid ongoing Azure VM and public IP charges:
terraform plan -destroy
terraform destroy
Important: These configurations have been checked against published Terraform, Microsoft, and Canonical documentation, but have not been applied to a live Azure subscription. Actual deployment is subject to your subscription’s regional image availability, VM quota, Azure policies, and permissions.
This gives students a progressive path through resource groups, networking, Windows, and Ubuntu while learning how Terraform maintains and extends infrastructure through a single state.