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 Terraform input variables, introducing all six requested types in one exercise without creating any additional Azure resource or introducing new Terraform features such as data sources, modules, locals, outputs, count, or for_each.

The infrastructure remains the same 13 Terraform-managed resources: resource group, VNet, subnet, NSG, subnet/NSG association, two inbound security rules, two public IPs, two NICs, one Windows Server 2025 VM and one Ubuntu Server 26.04 LTS VM.

Important for existing students: Continue from the same Lab 4 folder and Terraform state. Do not start with an empty state. Keep the original resource names, region, CIDRs, VM size and passwords unchanged in terraform.tfvars unless you intend to modify Azure resources. The only intentional representation change in this lesson is that the RDP/SSH rules use the plural source_address_prefixes attribute to demonstrate a set(string) allowlist; Azure may plan in-place updates to those two rules.

1. Variable types — the six required concepts

Terraform typeWhat it representsSimple exampleWhere it is used in this lab
stringA text value"production"Region, VM size, administrator usernames/passwords
numberInteger or decimal3, 2.5NSG rule priorities (100, 110)
boolTrue or falsetrue, falseLinux password authentication setting
list(type)Ordered collection of values["dev", "prod"]VNet and subnet CIDRs (list(string))
set(type)Unique collection; order is not guaranteedtoset(["a", "b"])Allowed source CIDRs for RDP/SSH (set(string))
map(type)Named keys with values of a single type{ env = "prod" }Existing resource names (map(string))

A variable is declared using variable "name" { type = ... }. Its value is referenced using var.name or, for a map value, var.resource_names["key"].

Special note on sets: toset(["a", "b"]) is a valid Terraform expression. In terraform.tfvars, write a list-like literal such as ["203.0.113.10/32"]; the declared set(string) type converts it to a set automatically. Duplicate set values are removed; use sort(tolist(...)) when an Azure argument expects a stable list.

2. File structure

Use only these two files for the new lesson (alongside your existing Terraform state and provider lock file):

azure-terraform-labs/
├── main.tf           # Provider + 12 variable blocks + same 13 Azure resources
├── terraform.tfvars  # Values for all six variable types
├── terraform.tfstate # Existing state from Lab 4; protect this file
└── .terraform.lock.hcl

terraform.tfvars is automatically loaded by Terraform. It is not another block of infrastructure or another Azure resource. Do not commit terraform.tfvars or state files to a public repository because the example uses passwords.

3. Complete main.tf — same infrastructure, now with variables

This file is a complete replacement for the Lab 4 main.tf. It contains no new resource types or resources. source_image_reference and os_disk remain the same existing nested resource settings; no new Terraform blocks are introduced beyond variable declarations.

# ============================================
# PART 2 - ALL SIX VARIABLE TYPES (WINDOWS + UBUNTU)
# Keeps the same 13 resources and images from Labs 1-4
# ============================================

terraform {
  required_version = ">= 1.15.0, < 2.0.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

# ============================================================
# PART 2 - TERRAFORM INPUT VARIABLES (ALL SIX TYPES)
# ============================================================

# STRING - Azure region and VM settings
variable "azure_region" {
  description = "Azure region for every resource in this lab."
  type        = string
}

variable "vm_size" {
  description = "Size used by both virtual machines."
  type        = string
}

variable "admin_username" {
  description = "Local administrator username on both VMs."
  type        = string
}

variable "windows_admin_password" {
  description = "Administrator password for the Windows Server VM."
  type        = string
  sensitive   = true
}

variable "ubuntu_admin_password" {
  description = "Administrator password for the Ubuntu VM."
  type        = string
  sensitive   = true
}

# NUMBER - NSG rule priorities
variable "rdp_priority" {
  description = "Inbound RDP rule priority, between 100 and 4096."
  type        = number
}

variable "ssh_priority" {
  description = "Inbound SSH rule priority, between 100 and 4096."
  type        = number
}

# BOOL - keep Linux password authentication enabled for this lab
variable "disable_linux_password_authentication" {
  description = "Must be false while ubuntu_admin_password is configured."
  type        = bool
}

# LIST(STRING) - ordered Azure network address prefixes
variable "vnet_address_spaces" {
  description = "Address spaces configured on the Azure virtual network."
  type        = list(string)
}

variable "subnet_address_prefixes" {
  description = "Address prefixes configured on the VM subnet."
  type        = list(string)
}

# SET(STRING) - unique source CIDRs permitted to use SSH/RDP
variable "admin_allowed_cidrs" {
  description = "Unique public IPv4 CIDRs allowed to reach SSH and RDP."
  type        = set(string)
}

# MAP(STRING) - stable names of the existing Azure resources
variable "resource_names" {
  description = "Resource names from Labs 1-4. Keep unchanged to retain state."
  type        = map(string)
}

provider "azurerm" {
  features {}
}

# --------------------------------------------
# 1. Resource Group
# --------------------------------------------
resource "azurerm_resource_group" "lab" {
  name     = var.resource_names["resource_group"]
  location = var.azure_region
}

# --------------------------------------------
# 2. Virtual Network
# --------------------------------------------
resource "azurerm_virtual_network" "lab" {
  name                = var.resource_names["virtual_network"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  address_space = var.vnet_address_spaces
}

# --------------------------------------------
# 3. Subnet
# --------------------------------------------
resource "azurerm_subnet" "vms" {
  name                 = var.resource_names["subnet"]
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name

  address_prefixes = var.subnet_address_prefixes
}

# --------------------------------------------
# 4. Network Security Group
# --------------------------------------------
resource "azurerm_network_security_group" "lab" {
  name                = var.resource_names["nsg"]
  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 approved public IPv4 CIDRs
# --------------------------------------------
resource "azurerm_network_security_rule" "rdp" {
  name                        = var.resource_names["rdp_rule"]
  priority                    = var.rdp_priority
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "3389"
  source_address_prefixes     = sort(tolist(var.admin_allowed_cidrs))
  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 approved public IPv4 CIDRs
# --------------------------------------------
resource "azurerm_network_security_rule" "ssh" {
  name                        = var.resource_names["ssh_rule"]
  priority                    = var.ssh_priority
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefixes     = sort(tolist(var.admin_allowed_cidrs))
  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                = var.resource_names["windows_public_ip"]
  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                = var.resource_names["windows_nic"]
  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                = var.resource_names["windows_vm"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  size           = var.vm_size
  admin_username = var.admin_username

  # Keep the same Windows password used when the VM was created
  admin_password = var.windows_admin_password

  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                = var.resource_names["ubuntu_public_ip"]
  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                = var.resource_names["ubuntu_nic"]
  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                = var.resource_names["ubuntu_vm"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  size           = var.vm_size
  admin_username = var.admin_username

  # Training only - supplied through terraform.tfvars; keep secret
  admin_password = var.ubuntu_admin_password

  # Enable SSH password authentication
  disable_password_authentication = var.disable_linux_password_authentication

  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"
  }
}

4. Complete terraform.tfvars — values for every variable

Save the following as terraform.tfvars beside main.tf. Before applying, replace the documentation-only public IP with your real IPv4 /32 and replace the sample passwords. If continuing from a previously applied Lab 4, use the same passwords already configured on the VMs: changing them can cause VM replacement.

# ============================================================
# PART 2 - TERRAFORM VARIABLE VALUES
# Save as terraform.tfvars in the SAME directory as main.tf.
# IMPORTANT: Preserve the actual values used in Labs 1-4.
# ============================================================

# STRING
azure_region  = "eastus"
vm_size       = "Standard_B2s_v2"
admin_username = "labadmin"

# Replace these EXAMPLE passwords with the ones used in Lab 3 and Lab 4.
# If the VMs already exist, changing the passwords can replace the VMs.
windows_admin_password = "ChangeMe!2026WinLab#"
ubuntu_admin_password  = "ChangeMe!2026LinuxLab#"

# NUMBER: unique NSG priorities (100-4096)
rdp_priority = 100
ssh_priority = 110

# BOOL: must remain false for Ubuntu password-based login
disable_linux_password_authentication = false

# LIST(STRING): ordered collections
vnet_address_spaces      = ["10.20.0.0/16"]
subnet_address_prefixes  = ["10.20.1.0/24"]

# SET(STRING): Terraform converts this list-like literal to a set.
# REPLACE 203.0.113.10/32 with YOUR public IPv4 address/32.
# The example address is reserved for documentation, not your real IP.
admin_allowed_cidrs = ["203.0.113.10/32"]

# MAP(STRING): resource names must match the previous labs exactly.
resource_names = {
  resource_group    = "rg-terraform-lab-eastus"
  virtual_network   = "vnet-terraform-lab"
  subnet            = "snet-vms"
  nsg               = "nsg-terraform-lab"
  rdp_rule          = "Allow-RDP"
  ssh_rule          = "Allow-SSH"
  windows_public_ip = "pip-win2025-lab"
  windows_nic       = "nic-win2025-lab"
  windows_vm        = "vm-win2025-01"
  ubuntu_public_ip  = "pip-ubuntu2604-lab"
  ubuntu_nic        = "nic-ubuntu2604-lab"
  ubuntu_vm         = "vm-ubuntu2604-01"
}

5. How the six variable types work in the actual resources

5.1 string: a single text value

resource "azurerm_resource_group" "lab" {
  name     = var.resource_names["resource_group"]
  location = var.azure_region
}

var.azure_region becomes "eastus". The same string var.vm_size becomes "Standard_B2s_v2" on both VMs. admin_username and the two password values are also strings.

5.2 number: numeric NSG priority

priority = var.rdp_priority

var.rdp_priority becomes the number 100, not the quoted string "100". The second rule uses 110. Azure NSG rule priorities must be unique within the NSG, in the range 100–4096; lower values are evaluated first.

5.3 bool: on/off setting on Ubuntu VM

disable_password_authentication = var.disable_linux_password_authentication

false means password authentication is enabled, which is necessary because the Ubuntu VM has admin_password. Do not change this setting to true while supplying admin_password; AzureRM rejects that combination, and changing the setting on an existing VM can force replacement.

5.4 list(string): ordered network CIDRs

address_space    = var.vnet_address_spaces
address_prefixes = var.subnet_address_prefixes

The VNet gets ["10.20.0.0/16"] and the subnet gets ["10.20.1.0/24"]. Although there is just one entry in each list, the underlying Azure arguments expect a collection.

5.5 set(string): unique allowed administrator CIDRs

source_address_prefixes = sort(tolist(var.admin_allowed_cidrs))

For example, a student’s allowlist may later contain two distinct, trusted /32 IP addresses. A set automatically eliminates duplicates. Because a set has no guaranteed order, tolist converts it and sort gives the Azure rule a stable list. The subnet-level NSG applies these RDP/SSH rules across the subnet, not only to a single VM.

5.6 map(string): resource-name lookup

name = var.resource_names["windows_vm"]

This resolves to "vm-win2025-01". All existing names are grouped in one map with string keys and string values. Do not rename those entries if you want Terraform to continue managing the exact same deployed resources; Azure resource-name changes often force replacement.

6. Run the lesson, in the same state directory

First ensure you are in the same folder used for Labs 1–4 and that the correct Azure subscription is selected:

az login
az account set --subscription "YOUR_SUBSCRIPTION_ID"
export ARM_SUBSCRIPTION_ID="$(az account show --query id -o tsv)"

# Check your existing resources and confirm the same state is in use.
terraform state list

# Find your public IPv4 and update terraform.tfvars to use it with /32.
curl -4 https://api.ipify.org

Keep a private backup of your existing main.tf, then paste the complete files above into the same directory. Check that your terraform.tfvars passwords match the existing VMs.

terraform fmt
terraform validate
terraform plan -out=lab-part2.tfplan

Review the plan carefully before applying. Because the Azure infrastructure should remain the same, do not approve an unexpected destroy or -/+ replacement. The two NSG rules may show in-place changes due to changing from source_address_prefix to source_address_prefixes. If you changed your own IP allowlist, rule updates are also expected. If the plan contains unexpected VM replacement, reconcile the variable values with those used in Lab 4 before proceeding.

Once the plan is acceptable:

terraform apply lab-part2.tfplan
terraform state list

The final managed resource count is still 13; declaring variables does not create Azure resources. A no-change plan is possible when AzureRM normalizes equivalent rule settings, but a zero-change result is not guaranteed until you inspect the actual plan.

7. Student verification checklist

  • [ ] terraform validate passes.
  • [ ] terraform plan references only the expected Azure subscription and existing state.
  • [ ] No new Azure resource blocks were added.
  • [ ] No data, module, locals, output, count, or for_each was added.
  • [ ] All six types are declared and used: string, number, bool, list(string), set(string), map(string).
  • [ ] RDP (3389) and SSH (22) source CIDRs are your authorized public IPs only.
  • [ ] disable_linux_password_authentication is false.
  • [ ] Resource names, images, VM sizes and passwords match the already-applied lab when the goal is to preserve VMs.
  • [ ] terraform state list still shows 13 managed resource addresses.

8. Common errors and how to avoid them

SymptomLikely causeFix
No value for required variableAn input variable was omittedEnsure terraform.tfvars exists in the same directory and includes the variable
Invalid value for input variableWrong type, for example a map passed where a list is expectedMatch the variable’s declared type; set values in .tfvars use ["..."]
Invalid combination of arguments on Ubuntu VMdisable_linux_password_authentication = true while supplying a passwordKeep it false for this password-based lab
NSG rule priority errorEqual priorities or a value outside 100–4096Use distinct valid values such as 100 and 110
Cannot reach RDP or SSHPlaceholder IP left unchanged, incorrect public IP, network filtering or guest firewallUse the public IPv4 from curl -4 https://api.ipify.org followed by /32; verify rule and VM IP
Terraform proposes a VM replacementResource name, username, password, OS image or another ForceNew setting changedCompare terraform.tfvars against your previous Lab 4 values; do not blindly apply
Resource provider not registered or insufficient quotaSubscription setupRegister Microsoft.Compute / Microsoft.Network, check size availability and quotas

9. Clean up (only when the entire lab series is finished)

Do not destroy the environment between Labs 4 and Part 2. When you are truly finished:

terraform plan -destroy
terraform destroy

This will destroy the full 13-resource training environment, including both VMs. VM and public IP resources can incur charges until destroyed.

10. Accuracy and security notes

  • The resource configuration is based on the exact final Lab 4 code. All original resource types, image names and dependency references are retained.
  • Windows image: MicrosoftWindowsServer:WindowsServer:2025-datacenter:latest.
  • Ubuntu image: Canonical:ubuntu-26_04-lts:server:latest.
  • Provider version constraint: ~> 5.9.0 (AzureRM 5.9.x); Terraform: >= 1.15.0, < 2.0.0.
  • The source CIDR 203.0.113.10/32 is a documentation address. It must be replaced for working remote access.
  • Passwords in terraform.tfvars are provided as learning examples. sensitive = true redacts CLI display, but passwords can still be stored in Terraform state. Keep configuration, state, saved plans and credentials secure. For production, use safer credential handling and SSH keys.
  • No live Azure apply was performed for this guide. Exact VM SKU/image availability, policy and quotas depend on your subscription. terraform validate and a real terraform plan on your workstation are still necessary.

Reference documentation

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 – Resources – VMS & Netowork Demo

Level: Absolute BeginnerTerraform version: 1.15.9AzureRM Provider: 5.9.0Approach: Each lab builds on the previous lab. These are four complete Terraform configurations designed for students to execute sequentially. Learning Path 1. Selected Azure…

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