Terraform Lab – Azure – Resources – VMS & Netowork Demo

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

  1. Lab 1 — Azure Resource Group: Create your first Azure resource.
  2. Lab 2 — Azure Networking: Add VNet, subnet, NSG and subnet association.
  3. Lab 3 — Windows Server VM: Add Windows Server 2025, public IP and RDP access.
  4. 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.

SettingSelected value
Azure regioneastus (East US)
Resource Grouprg-terraform-lab-eastus
VNetvnet-terraform-lab
VNet CIDR10.20.0.0/16
Subnetsnet-vms
Subnet CIDR10.20.1.0/24
Windows OSWindows Server 2025 Datacenter
Ubuntu OSUbuntu Server 26.04 LTS
VM sizeStandard_B2s_v2
AuthenticationUsername + password
Providerhashicorp/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 (variable or .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 /32 for 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.

SettingValue
HostPublic IP returned above
Port3389
Usernamelabadmin
PasswordYour replacement Windows password
Operating SystemWindows 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

LabNew Terraform resourcesCumulative
Lab 1 — Resource Group11
Lab 2 — Networking45
Lab 3 — Windows VM49
Lab 4 — Ubuntu VM413

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.

Related Posts

Terraform Lab – Azure –  Local Values and Outputs

Continuation of Labs 1–4 and Part 2 (All Six Input Variable Types)Level: Beginner → Early IntermediateTerraform: 1.15.9+ within version 1.xAzureRM provider: 5.9.x (same provider constraint as Part 2)Date: 12 October 2026…

Read More

Terraform Lab – Azure – Input Variables – VMS & Netowork Demo

Continuation of Labs 1–4 · Level: Beginner · Terraform: 1.15.9 · AzureRM: 5.9.x · Updated: 12 October 2026 Objective Terraform Lab – Azure – Resources – VMS & Netowork Demo Convert the final, consolidated Lab 4 infrastructure to…

Read More

Azure: Terraform Azure Authentication: Azure CLI

Latest Terraform Azure Authentication: Azure CLI The recommended approach in the HashiCorp AzureRM provider’s Azure CLI authentication guide is to authenticate through Azure CLI when running Terraform…

Read More

Azure: Authenticating to Azure using a Service Principal and a Client Secret

Azure Portal: Get all 4 Terraform authentication values You can do this entirely through the Azure Portal web interface. No terminal commands or Azure CLI are required….

Read More

Azure: How to Create an Azure User, Assign Permissions, and Log In

Objective Create a new user in Microsoft Azure, grant access to cloud resources, and verify that the user can log in independently without sharing your administrator credentials….

Read More

Falcosidekick on Ubuntu: Complete Installation, Configuration, Alert Forwarding, and Troubleshooting Tutorial

I read your Falco: Zero to Intermediate – Gold-Standard Hands-on Master Tutorial. It uses Ubuntu Server 24.04 LTS as its reference lab and covers Falco installation, runtime…

Read More