{"id":1227,"date":"2026-10-12T14:09:01","date_gmt":"2026-10-12T14:09:01","guid":{"rendered":"https:\/\/www.devopsschool.com\/tutorials\/?p=1227"},"modified":"2026-10-12T14:09:03","modified_gmt":"2026-10-12T14:09:03","slug":"terraform-lab-azure-input-variables-vms-netowork-demo","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/tutorials\/terraform-lab-azure-input-variables-vms-netowork-demo\/","title":{"rendered":"Terraform Lab \u2013 Azure \u2013 Input Variables \u2013 VMS &amp; Netowork Demo"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Continuation of Labs 1\u20134<\/strong>&nbsp;\u00b7&nbsp;<strong>Level:<\/strong>&nbsp;Beginner \u00b7&nbsp;<strong>Terraform:<\/strong>&nbsp;1.15.9 \u00b7&nbsp;<strong>AzureRM:<\/strong>&nbsp;5.9.x \u00b7&nbsp;<strong>Updated:<\/strong>&nbsp;12 October 2026<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Objective<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform Lab \u2013 Azure \u2013 Resources \u2013 VMS &amp; Netowork Demo<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Convert the&nbsp;<strong>final, consolidated Lab 4 infrastructure<\/strong>&nbsp;to Terraform input variables, introducing&nbsp;<strong>all six requested types in one exercise<\/strong>&nbsp;without creating any additional Azure resource or introducing new Terraform features such as data sources, modules, locals, outputs,&nbsp;<code>count<\/code>, or&nbsp;<code>for_each<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The infrastructure remains the&nbsp;<strong>same 13 Terraform-managed resources<\/strong>: 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.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Important for existing students:<\/strong>&nbsp;Continue from the&nbsp;<strong>same Lab 4 folder and Terraform state<\/strong>. Do not start with an empty state. Keep the original resource names, region, CIDRs, VM size and passwords unchanged in&nbsp;<code>terraform.tfvars<\/code>&nbsp;unless you intend to modify Azure resources. The only intentional representation change in this lesson is that the RDP\/SSH rules use the plural&nbsp;<code>source_address_prefixes<\/code>&nbsp;attribute to demonstrate a&nbsp;<code>set(string)<\/code>&nbsp;allowlist; Azure may plan in-place updates to those two rules.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">1. Variable types \u2014 the six required concepts<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Terraform type<\/th><th class=\"has-text-align-left\" data-align=\"left\">What it represents<\/th><th class=\"has-text-align-left\" data-align=\"left\">Simple example<\/th><th class=\"has-text-align-left\" data-align=\"left\">Where it is used in this lab<\/th><\/tr><\/thead><tbody><tr><td><code>string<\/code><\/td><td>A text value<\/td><td><code>\"production\"<\/code><\/td><td>Region, VM size, administrator usernames\/passwords<\/td><\/tr><tr><td><code>number<\/code><\/td><td>Integer or decimal<\/td><td><code>3<\/code>,&nbsp;<code>2.5<\/code><\/td><td>NSG rule priorities (<code>100<\/code>,&nbsp;<code>110<\/code>)<\/td><\/tr><tr><td><code>bool<\/code><\/td><td>True or false<\/td><td><code>true<\/code>,&nbsp;<code>false<\/code><\/td><td>Linux password authentication setting<\/td><\/tr><tr><td><code>list(type)<\/code><\/td><td>Ordered collection of values<\/td><td><code>[\"dev\", \"prod\"]<\/code><\/td><td>VNet and subnet CIDRs (<code>list(string)<\/code>)<\/td><\/tr><tr><td><code>set(type)<\/code><\/td><td>Unique collection; order is not guaranteed<\/td><td><code>toset([\"a\", \"b\"])<\/code><\/td><td>Allowed source CIDRs for RDP\/SSH (<code>set(string)<\/code>)<\/td><\/tr><tr><td><code>map(type)<\/code><\/td><td>Named keys with values of a single type<\/td><td><code>{ env = \"prod\" }<\/code><\/td><td>Existing resource names (<code>map(string)<\/code>)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A variable is declared using&nbsp;<code>variable \"name\" { type = ... }<\/code>. Its value is referenced using&nbsp;<strong><code>var.name<\/code><\/strong>&nbsp;or, for a map value,&nbsp;<strong><code>var.resource_names[\"key\"]<\/code><\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Special note on sets:<\/strong>&nbsp;<code>toset([\"a\", \"b\"])<\/code>&nbsp;is a valid Terraform&nbsp;<em>expression<\/em>. In&nbsp;<code>terraform.tfvars<\/code>, write a list-like literal such as&nbsp;<code>[\"203.0.113.10\/32\"]<\/code>; the declared&nbsp;<code>set(string)<\/code>&nbsp;type converts it to a set automatically. Duplicate set values are removed; use&nbsp;<code>sort(tolist(...))<\/code>&nbsp;when an Azure argument expects a stable list.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. File structure<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use&nbsp;<strong>only these two files<\/strong>&nbsp;for the new lesson (alongside your existing Terraform state and provider lock file):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>azure-terraform-labs\/\n\u251c\u2500\u2500 main.tf           # Provider + 12 variable blocks + same 13 Azure resources\n\u251c\u2500\u2500 terraform.tfvars  # Values for all six variable types\n\u251c\u2500\u2500 terraform.tfstate # Existing state from Lab 4; protect this file\n\u2514\u2500\u2500 .terraform.lock.hcl\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>terraform.tfvars<\/code>&nbsp;is&nbsp;<strong>automatically loaded<\/strong>&nbsp;by Terraform. It is not another block of infrastructure or another Azure resource. Do&nbsp;<strong>not<\/strong>&nbsp;commit&nbsp;<code>terraform.tfvars<\/code>&nbsp;or state files to a public repository because the example uses passwords.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Complete&nbsp;<code>main.tf<\/code>&nbsp;\u2014 same infrastructure, now with variables<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This file is a&nbsp;<strong>complete replacement<\/strong>&nbsp;for the Lab 4&nbsp;<code>main.tf<\/code>. It contains no new resource types or resources.&nbsp;<code>source_image_reference<\/code>&nbsp;and&nbsp;<code>os_disk<\/code>&nbsp;remain the same existing nested resource settings;&nbsp;<strong>no new Terraform blocks are introduced beyond&nbsp;<code>variable<\/code>&nbsp;declarations<\/strong>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># ============================================\n# PART 2 - ALL SIX VARIABLE TYPES (WINDOWS + UBUNTU)\n# Keeps the same 13 resources and images from Labs 1-4\n# ============================================\n\nterraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n\n  required_providers {\n    azurerm = {\n      source  = \"hashicorp\/azurerm\"\n      version = \"~&gt; 5.9.0\"\n    }\n  }\n}\n\n# ============================================================\n# PART 2 - TERRAFORM INPUT VARIABLES (ALL SIX TYPES)\n# ============================================================\n\n# STRING - Azure region and VM settings\nvariable \"azure_region\" {\n  description = \"Azure region for every resource in this lab.\"\n  type        = string\n}\n\nvariable \"vm_size\" {\n  description = \"Size used by both virtual machines.\"\n  type        = string\n}\n\nvariable \"admin_username\" {\n  description = \"Local administrator username on both VMs.\"\n  type        = string\n}\n\nvariable \"windows_admin_password\" {\n  description = \"Administrator password for the Windows Server VM.\"\n  type        = string\n  sensitive   = true\n}\n\nvariable \"ubuntu_admin_password\" {\n  description = \"Administrator password for the Ubuntu VM.\"\n  type        = string\n  sensitive   = true\n}\n\n# NUMBER - NSG rule priorities\nvariable \"rdp_priority\" {\n  description = \"Inbound RDP rule priority, between 100 and 4096.\"\n  type        = number\n}\n\nvariable \"ssh_priority\" {\n  description = \"Inbound SSH rule priority, between 100 and 4096.\"\n  type        = number\n}\n\n# BOOL - keep Linux password authentication enabled for this lab\nvariable \"disable_linux_password_authentication\" {\n  description = \"Must be false while ubuntu_admin_password is configured.\"\n  type        = bool\n}\n\n# LIST(STRING) - ordered Azure network address prefixes\nvariable \"vnet_address_spaces\" {\n  description = \"Address spaces configured on the Azure virtual network.\"\n  type        = list(string)\n}\n\nvariable \"subnet_address_prefixes\" {\n  description = \"Address prefixes configured on the VM subnet.\"\n  type        = list(string)\n}\n\n# SET(STRING) - unique source CIDRs permitted to use SSH\/RDP\nvariable \"admin_allowed_cidrs\" {\n  description = \"Unique public IPv4 CIDRs allowed to reach SSH and RDP.\"\n  type        = set(string)\n}\n\n# MAP(STRING) - stable names of the existing Azure resources\nvariable \"resource_names\" {\n  description = \"Resource names from Labs 1-4. Keep unchanged to retain state.\"\n  type        = map(string)\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# --------------------------------------------\n# 1. Resource Group\n# --------------------------------------------\nresource \"azurerm_resource_group\" \"lab\" {\n  name     = var.resource_names&#91;\"resource_group\"]\n  location = var.azure_region\n}\n\n# --------------------------------------------\n# 2. Virtual Network\n# --------------------------------------------\nresource \"azurerm_virtual_network\" \"lab\" {\n  name                = var.resource_names&#91;\"virtual_network\"]\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  address_space = var.vnet_address_spaces\n}\n\n# --------------------------------------------\n# 3. Subnet\n# --------------------------------------------\nresource \"azurerm_subnet\" \"vms\" {\n  name                 = var.resource_names&#91;\"subnet\"]\n  resource_group_name  = azurerm_resource_group.lab.name\n  virtual_network_name = azurerm_virtual_network.lab.name\n\n  address_prefixes = var.subnet_address_prefixes\n}\n\n# --------------------------------------------\n# 4. Network Security Group\n# --------------------------------------------\nresource \"azurerm_network_security_group\" \"lab\" {\n  name                = var.resource_names&#91;\"nsg\"]\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n}\n\n# --------------------------------------------\n# 5. Associate NSG with Subnet\n# --------------------------------------------\nresource \"azurerm_subnet_network_security_group_association\" \"lab\" {\n  subnet_id                 = azurerm_subnet.vms.id\n  network_security_group_id = azurerm_network_security_group.lab.id\n}\n\n# --------------------------------------------\n# 6. Allow RDP from approved public IPv4 CIDRs\n# --------------------------------------------\nresource \"azurerm_network_security_rule\" \"rdp\" {\n  name                        = var.resource_names&#91;\"rdp_rule\"]\n  priority                    = var.rdp_priority\n  direction                   = \"Inbound\"\n  access                      = \"Allow\"\n  protocol                    = \"Tcp\"\n  source_port_range           = \"*\"\n  destination_port_range      = \"3389\"\n  source_address_prefixes     = sort(tolist(var.admin_allowed_cidrs))\n  destination_address_prefix  = \"*\"\n  resource_group_name         = azurerm_resource_group.lab.name\n  network_security_group_name = azurerm_network_security_group.lab.name\n}\n\n# --------------------------------------------\n# 7. Allow SSH from approved public IPv4 CIDRs\n# --------------------------------------------\nresource \"azurerm_network_security_rule\" \"ssh\" {\n  name                        = var.resource_names&#91;\"ssh_rule\"]\n  priority                    = var.ssh_priority\n  direction                   = \"Inbound\"\n  access                      = \"Allow\"\n  protocol                    = \"Tcp\"\n  source_port_range           = \"*\"\n  destination_port_range      = \"22\"\n  source_address_prefixes     = sort(tolist(var.admin_allowed_cidrs))\n  destination_address_prefix  = \"*\"\n  resource_group_name         = azurerm_resource_group.lab.name\n  network_security_group_name = azurerm_network_security_group.lab.name\n}\n\n# --------------------------------------------\n# 8. Windows VM Public IP\n# --------------------------------------------\nresource \"azurerm_public_ip\" \"windows\" {\n  name                = var.resource_names&#91;\"windows_public_ip\"]\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  allocation_method = \"Static\"\n  sku               = \"Standard\"\n}\n\n# --------------------------------------------\n# 9. Windows VM Network Interface\n# --------------------------------------------\nresource \"azurerm_network_interface\" \"windows\" {\n  name                = var.resource_names&#91;\"windows_nic\"]\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  ip_configuration {\n    name                          = \"internal\"\n    subnet_id                     = azurerm_subnet.vms.id\n    private_ip_address_allocation = \"Dynamic\"\n    public_ip_address_id          = azurerm_public_ip.windows.id\n  }\n}\n\n# --------------------------------------------\n# 10. Windows Server 2025 VM\n# --------------------------------------------\nresource \"azurerm_windows_virtual_machine\" \"windows\" {\n  name                = var.resource_names&#91;\"windows_vm\"]\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  size           = var.vm_size\n  admin_username = var.admin_username\n\n  # Keep the same Windows password used when the VM was created\n  admin_password = var.windows_admin_password\n\n  network_interface_ids = &#91;\n    azurerm_network_interface.windows.id\n  ]\n\n  os_disk {\n    caching              = \"ReadWrite\"\n    storage_account_type = \"Standard_LRS\"\n  }\n\n  source_image_reference {\n    publisher = \"MicrosoftWindowsServer\"\n    offer     = \"WindowsServer\"\n    sku       = \"2025-datacenter\"\n    version   = \"latest\"\n  }\n}\n\n# --------------------------------------------\n# 11. Ubuntu VM Public IP\n# --------------------------------------------\nresource \"azurerm_public_ip\" \"ubuntu\" {\n  name                = var.resource_names&#91;\"ubuntu_public_ip\"]\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  allocation_method = \"Static\"\n  sku               = \"Standard\"\n}\n\n# --------------------------------------------\n# 12. Ubuntu VM Network Interface\n# --------------------------------------------\nresource \"azurerm_network_interface\" \"ubuntu\" {\n  name                = var.resource_names&#91;\"ubuntu_nic\"]\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  ip_configuration {\n    name                          = \"internal\"\n    subnet_id                     = azurerm_subnet.vms.id\n    private_ip_address_allocation = \"Dynamic\"\n    public_ip_address_id          = azurerm_public_ip.ubuntu.id\n  }\n}\n\n# --------------------------------------------\n# 13. Ubuntu Server 26.04 LTS VM\n# --------------------------------------------\nresource \"azurerm_linux_virtual_machine\" \"ubuntu\" {\n  name                = var.resource_names&#91;\"ubuntu_vm\"]\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  size           = var.vm_size\n  admin_username = var.admin_username\n\n  # Training only - supplied through terraform.tfvars; keep secret\n  admin_password = var.ubuntu_admin_password\n\n  # Enable SSH password authentication\n  disable_password_authentication = var.disable_linux_password_authentication\n\n  network_interface_ids = &#91;\n    azurerm_network_interface.ubuntu.id\n  ]\n\n  # OS Disk\n  os_disk {\n    caching              = \"ReadWrite\"\n    storage_account_type = \"Standard_LRS\"\n  }\n\n  # Ubuntu Server 26.04 LTS Gen2 Image\n  source_image_reference {\n    publisher = \"Canonical\"\n    offer     = \"ubuntu-26_04-lts\"\n    sku       = \"server\"\n    version   = \"latest\"\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">4. Complete&nbsp;<code>terraform.tfvars<\/code>&nbsp;\u2014 values for every variable<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Save the following as&nbsp;<code>terraform.tfvars<\/code>&nbsp;<strong>beside&nbsp;<code>main.tf<\/code><\/strong>. Before applying, replace the documentation-only public IP with your real IPv4&nbsp;<code>\/32<\/code>&nbsp;and replace the sample passwords.&nbsp;<strong>If continuing from a previously applied Lab 4, use the same passwords already configured on the VMs<\/strong>: changing them can cause VM replacement.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># ============================================================\n# PART 2 - TERRAFORM VARIABLE VALUES\n# Save as terraform.tfvars in the SAME directory as main.tf.\n# IMPORTANT: Preserve the actual values used in Labs 1-4.\n# ============================================================\n\n# STRING\nazure_region  = \"eastus\"\nvm_size       = \"Standard_B2s_v2\"\nadmin_username = \"labadmin\"\n\n# Replace these EXAMPLE passwords with the ones used in Lab 3 and Lab 4.\n# If the VMs already exist, changing the passwords can replace the VMs.\nwindows_admin_password = \"ChangeMe!2026WinLab#\"\nubuntu_admin_password  = \"ChangeMe!2026LinuxLab#\"\n\n# NUMBER: unique NSG priorities (100-4096)\nrdp_priority = 100\nssh_priority = 110\n\n# BOOL: must remain false for Ubuntu password-based login\ndisable_linux_password_authentication = false\n\n# LIST(STRING): ordered collections\nvnet_address_spaces      = &#91;\"10.20.0.0\/16\"]\nsubnet_address_prefixes  = &#91;\"10.20.1.0\/24\"]\n\n# SET(STRING): Terraform converts this list-like literal to a set.\n# REPLACE 203.0.113.10\/32 with YOUR public IPv4 address\/32.\n# The example address is reserved for documentation, not your real IP.\nadmin_allowed_cidrs = &#91;\"203.0.113.10\/32\"]\n\n# MAP(STRING): resource names must match the previous labs exactly.\nresource_names = {\n  resource_group    = \"rg-terraform-lab-eastus\"\n  virtual_network   = \"vnet-terraform-lab\"\n  subnet            = \"snet-vms\"\n  nsg               = \"nsg-terraform-lab\"\n  rdp_rule          = \"Allow-RDP\"\n  ssh_rule          = \"Allow-SSH\"\n  windows_public_ip = \"pip-win2025-lab\"\n  windows_nic       = \"nic-win2025-lab\"\n  windows_vm        = \"vm-win2025-01\"\n  ubuntu_public_ip  = \"pip-ubuntu2604-lab\"\n  ubuntu_nic        = \"nic-ubuntu2604-lab\"\n  ubuntu_vm         = \"vm-ubuntu2604-01\"\n}\n\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">5. How the six variable types work in the actual resources<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">5.1&nbsp;<code>string<\/code>: a single text value<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>resource \"azurerm_resource_group\" \"lab\" {\n  name     = var.resource_names&#91;\"resource_group\"]\n  location = var.azure_region\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>var.azure_region<\/code>&nbsp;becomes&nbsp;<code>\"eastus\"<\/code>. The same string&nbsp;<code>var.vm_size<\/code>&nbsp;becomes&nbsp;<code>\"Standard_B2s_v2\"<\/code>&nbsp;on both VMs.&nbsp;<code>admin_username<\/code>&nbsp;and the two password values are also strings.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.2&nbsp;<code>number<\/code>: numeric NSG priority<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>priority = var.rdp_priority\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>var.rdp_priority<\/code>&nbsp;becomes the number&nbsp;<code>100<\/code>,&nbsp;<strong>not<\/strong>&nbsp;the quoted string&nbsp;<code>\"100\"<\/code>. The second rule uses&nbsp;<code>110<\/code>. Azure NSG rule priorities must be&nbsp;<strong>unique within the NSG<\/strong>, in the range 100\u20134096; lower values are evaluated first.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.3&nbsp;<code>bool<\/code>: on\/off setting on Ubuntu VM<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>disable_password_authentication = var.disable_linux_password_authentication\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>false<\/code>&nbsp;means password authentication is&nbsp;<strong>enabled<\/strong>, which is necessary because the Ubuntu VM has&nbsp;<code>admin_password<\/code>.&nbsp;<strong>Do not change this setting to&nbsp;<code>true<\/code>&nbsp;while supplying&nbsp;<code>admin_password<\/code><\/strong>; AzureRM rejects that combination, and changing the setting on an existing VM can force replacement.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.4&nbsp;<code>list(string)<\/code>: ordered network CIDRs<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>address_space    = var.vnet_address_spaces\naddress_prefixes = var.subnet_address_prefixes\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The VNet gets&nbsp;<code>[\"10.20.0.0\/16\"]<\/code>&nbsp;and the subnet gets&nbsp;<code>[\"10.20.1.0\/24\"]<\/code>. Although there is just one entry in each list, the underlying Azure arguments expect a collection.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.5&nbsp;<code>set(string)<\/code>: unique allowed administrator CIDRs<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>source_address_prefixes = sort(tolist(var.admin_allowed_cidrs))\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For example, a student&#8217;s allowlist may later contain two&nbsp;<strong>distinct, trusted<\/strong>&nbsp;<code>\/32<\/code>&nbsp;IP addresses. A set automatically eliminates duplicates. Because a set has no guaranteed order,&nbsp;<code>tolist<\/code>&nbsp;converts it and&nbsp;<code>sort<\/code>&nbsp;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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.6&nbsp;<code>map(string)<\/code>: resource-name lookup<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>name = var.resource_names&#91;\"windows_vm\"]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This resolves to&nbsp;<code>\"vm-win2025-01\"<\/code>. All existing names are grouped in one map with string keys and string values.&nbsp;<strong>Do not rename those entries<\/strong>&nbsp;if you want Terraform to continue managing the exact same deployed resources; Azure resource-name changes often force replacement.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Run the lesson, in the same state directory<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">First ensure you are in the same folder used for Labs 1\u20134 and that the correct Azure subscription is selected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>az login\naz account set --subscription \"YOUR_SUBSCRIPTION_ID\"\nexport ARM_SUBSCRIPTION_ID=\"$(az account show --query id -o tsv)\"\n\n<em># Check your existing resources and confirm the same state is in use.<\/em>\nterraform state list\n\n<em># Find your public IPv4 and update terraform.tfvars to use it with \/32.<\/em>\ncurl -4 https:\/\/api.ipify.org\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Keep a private backup of your existing&nbsp;<code>main.tf<\/code>, then paste the complete files above into the same directory. Check that your&nbsp;<code>terraform.tfvars<\/code>&nbsp;passwords match the existing VMs.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform fmt\nterraform validate\nterraform plan -out=lab-part2.tfplan\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Review the plan carefully before applying.<\/strong>&nbsp;Because the Azure infrastructure should remain the same,&nbsp;<strong>do not approve an unexpected&nbsp;<code>destroy<\/code>&nbsp;or&nbsp;<code>-\/+<\/code>&nbsp;replacement<\/strong>. The two NSG rules&nbsp;<strong>may<\/strong>&nbsp;show in-place changes due to changing from&nbsp;<code>source_address_prefix<\/code>&nbsp;to&nbsp;<code>source_address_prefixes<\/code>. 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once the plan is acceptable:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform apply lab-part2.tfplan\nterraform state list\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The&nbsp;<strong>final managed resource count is still 13<\/strong>; declaring variables does&nbsp;<strong>not<\/strong>&nbsp;create Azure resources. A no-change plan is possible when AzureRM normalizes equivalent rule settings, but a zero-change result is&nbsp;<strong>not guaranteed<\/strong>&nbsp;until you inspect the actual plan.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Student verification checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ]\u00a0<code>terraform validate<\/code>\u00a0passes.<\/li>\n\n\n\n<li>[ ]\u00a0<code>terraform plan<\/code>\u00a0references only the expected Azure subscription and existing state.<\/li>\n\n\n\n<li>[ ] No new Azure resource blocks were added.<\/li>\n\n\n\n<li>[ ] No\u00a0<code>data<\/code>,\u00a0<code>module<\/code>,\u00a0<code>locals<\/code>,\u00a0<code>output<\/code>,\u00a0<code>count<\/code>, or\u00a0<code>for_each<\/code>\u00a0was added.<\/li>\n\n\n\n<li>[ ] All six types are declared and used:\u00a0<code>string<\/code>,\u00a0<code>number<\/code>,\u00a0<code>bool<\/code>,\u00a0<code>list(string)<\/code>,\u00a0<code>set(string)<\/code>,\u00a0<code>map(string)<\/code>.<\/li>\n\n\n\n<li>[ ] RDP (3389) and SSH (22) source CIDRs are\u00a0<strong>your authorized public IPs only<\/strong>.<\/li>\n\n\n\n<li>[ ]\u00a0<code>disable_linux_password_authentication<\/code>\u00a0is\u00a0<code>false<\/code>.<\/li>\n\n\n\n<li>[ ] Resource names, images, VM sizes and passwords match the already-applied lab when the goal is to preserve VMs.<\/li>\n\n\n\n<li>[ ]\u00a0<code>terraform state list<\/code>\u00a0still shows 13 managed resource addresses.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">8. Common errors and how to avoid them<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Symptom<\/th><th class=\"has-text-align-left\" data-align=\"left\">Likely cause<\/th><th class=\"has-text-align-left\" data-align=\"left\">Fix<\/th><\/tr><\/thead><tbody><tr><td><code>No value for required variable<\/code><\/td><td>An input variable was omitted<\/td><td>Ensure&nbsp;<code>terraform.tfvars<\/code>&nbsp;exists in the same directory and includes the variable<\/td><\/tr><tr><td><code>Invalid value for input variable<\/code><\/td><td>Wrong type, for example a map passed where a list is expected<\/td><td>Match the variable&#8217;s declared type; set values in&nbsp;<code>.tfvars<\/code>&nbsp;use&nbsp;<code>[\"...\"]<\/code><\/td><\/tr><tr><td><code>Invalid combination of arguments<\/code>&nbsp;on Ubuntu VM<\/td><td><code>disable_linux_password_authentication = true<\/code>&nbsp;while supplying a password<\/td><td>Keep it&nbsp;<code>false<\/code>&nbsp;for this password-based lab<\/td><\/tr><tr><td>NSG rule priority error<\/td><td>Equal priorities or a value outside 100\u20134096<\/td><td>Use distinct valid values such as 100 and 110<\/td><\/tr><tr><td>Cannot reach RDP or SSH<\/td><td>Placeholder IP left unchanged, incorrect public IP, network filtering or guest firewall<\/td><td>Use the public IPv4 from&nbsp;<code>curl -4 https:\/\/api.ipify.org<\/code>&nbsp;followed by&nbsp;<code>\/32<\/code>; verify rule and VM IP<\/td><\/tr><tr><td>Terraform proposes a VM replacement<\/td><td>Resource name, username, password, OS image or another ForceNew setting changed<\/td><td>Compare&nbsp;<code>terraform.tfvars<\/code>&nbsp;against your previous Lab 4 values;&nbsp;<strong>do not blindly apply<\/strong><\/td><\/tr><tr><td><code>Resource provider not registered<\/code>&nbsp;or insufficient quota<\/td><td>Subscription setup<\/td><td>Register&nbsp;<code>Microsoft.Compute<\/code>&nbsp;\/&nbsp;<code>Microsoft.Network<\/code>, check size availability and quotas<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">9. Clean up (only when the entire lab series is finished)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do not destroy the environment between Labs 4 and Part 2.<\/strong>&nbsp;When you are truly finished:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform plan -destroy\nterraform destroy\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This will destroy the full 13-resource training environment, including both VMs. VM and public IP resources can incur charges until destroyed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">10. Accuracy and security notes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The\u00a0<strong>resource configuration is based on the exact final Lab 4 code<\/strong>. All original resource types, image names and dependency references are retained.<\/li>\n\n\n\n<li>Windows image:\u00a0<code>MicrosoftWindowsServer:WindowsServer:2025-datacenter:latest<\/code>.<\/li>\n\n\n\n<li>Ubuntu image:\u00a0<code>Canonical:ubuntu-26_04-lts:server:latest<\/code>.<\/li>\n\n\n\n<li>Provider version constraint:\u00a0<code>~> 5.9.0<\/code>\u00a0(AzureRM 5.9.x); Terraform:\u00a0<code>>= 1.15.0, &lt; 2.0.0<\/code>.<\/li>\n\n\n\n<li>The source CIDR\u00a0<code>203.0.113.10\/32<\/code>\u00a0is a documentation address. It\u00a0<strong>must<\/strong>\u00a0be replaced for working remote access.<\/li>\n\n\n\n<li>Passwords in\u00a0<code>terraform.tfvars<\/code>\u00a0are provided as learning examples.\u00a0<code>sensitive = true<\/code>\u00a0<strong>redacts CLI display<\/strong>, 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.<\/li>\n\n\n\n<li><strong>No live Azure apply was performed for this guide.<\/strong>\u00a0Exact VM SKU\/image availability, policy and quotas depend on your subscription.\u00a0<code>terraform validate<\/code>\u00a0and a real\u00a0<code>terraform plan<\/code>\u00a0on your workstation are still necessary.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Reference documentation<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/values\/variables\">Terraform input variables<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/block\/variable\">Terraform variable block reference<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/registry.terraform.io\/providers\/hashicorp\/azurerm\/latest\/docs\/resources\/network_security_rule\">AzureRM network security rule attributes<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/registry.terraform.io\/providers\/hashicorp\/azurerm\/latest\/docs\/resources\/linux_virtual_machine\">AzureRM Linux VM password settings<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/ubuntu.com\/azure\/docs\/azure-how-to\/instances\/find-ubuntu-images\/\">Canonical Azure Ubuntu image catalog<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Continuation of Labs 1\u20134&nbsp;\u00b7&nbsp;Level:&nbsp;Beginner \u00b7&nbsp;Terraform:&nbsp;1.15.9 \u00b7&nbsp;AzureRM:&nbsp;5.9.x \u00b7&nbsp;Updated:&nbsp;12 October 2026 Objective Terraform Lab \u2013 Azure \u2013 Resources \u2013 VMS &amp; Netowork Demo Convert the&nbsp;final, consolidated Lab 4 infrastructure&nbsp;to&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1227","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1227","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/comments?post=1227"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1227\/revisions"}],"predecessor-version":[{"id":1228,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1227\/revisions\/1228"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/media?parent=1227"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/categories?post=1227"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/tags?post=1227"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}