{"id":1229,"date":"2026-10-12T15:24:18","date_gmt":"2026-10-12T15:24:18","guid":{"rendered":"https:\/\/www.devopsschool.com\/tutorials\/?p=1229"},"modified":"2026-10-12T15:24:19","modified_gmt":"2026-10-12T15:24:19","slug":"terraform-lab-azure-local-values-and-outputs","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/tutorials\/terraform-lab-azure-local-values-and-outputs\/","title":{"rendered":"Terraform Lab \u2013 Azure \u2013\u00a0 Local Values and Outputs"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Continuation of Labs 1\u20134 and Part 2 (All Six Input Variable Types)<\/strong><br><strong>Level:<\/strong>&nbsp;Beginner \u2192 Early Intermediate<br><strong>Terraform:<\/strong>&nbsp;1.15.9+ within version 1.x<br><strong>AzureRM provider:<\/strong>&nbsp;5.9.x (same provider constraint as Part 2)<br><strong>Date:<\/strong>&nbsp;12 October 2026<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab objective<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Extend the&nbsp;<strong>exact final Part 2 Terraform configuration<\/strong>&nbsp;by introducing only two new Terraform block types:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>locals { ... }<\/code><\/strong>\u00a0\u2014 give names to internal, reusable expressions.<\/li>\n\n\n\n<li><strong><code>output \"...\" { ... }<\/code><\/strong>\u00a0\u2014 expose useful infrastructure information after an apply.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>No new Azure infrastructure is introduced.<\/strong>&nbsp;The existing&nbsp;<strong>12 input variables<\/strong>&nbsp;and&nbsp;<strong>13 Terraform-managed Azure resources<\/strong>&nbsp;remain, and their Terraform resource addresses are unchanged. We do not introduce data sources, modules,&nbsp;<code>count<\/code>,&nbsp;<code>for_each<\/code>, provisioners, new providers, or any other resource blocks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Lab progression<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>Labs 1\u20134:  Resource Group -&gt; Network -&gt; Windows VM -&gt; Ubuntu VM\nPart 2:     All six input variable types\nPart 3:     Local Values + Output Values   &lt;-- THIS LESSON\n<\/code><\/pre>\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>Critical:<\/strong>&nbsp;Work in the&nbsp;<strong>same working directory, same Azure subscription, and same Terraform state<\/strong>&nbsp;as Part 2. Replace the existing&nbsp;<code>main.tf<\/code>&nbsp;with the full code below, but&nbsp;<strong>keep your working&nbsp;<code>terraform.tfvars<\/code>&nbsp;values unchanged<\/strong>. Do not run&nbsp;<code>terraform destroy<\/code>&nbsp;between these lessons.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">1. What are Local Values?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Local values are named expressions defined inside a&nbsp;<code>locals<\/code>&nbsp;block. They help reduce repeated logic and make configuration easier to read. Unlike&nbsp;<strong>input variables<\/strong>, local values are&nbsp;<strong>not assigned through&nbsp;<code>terraform.tfvars<\/code>&nbsp;or&nbsp;<code>-var<\/code><\/strong>; Terraform calculates them from expressions in the configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Syntax:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>locals {\n  my_location = var.azure_region\n  my_message  = \"Deploying to ${var.azure_region}\"\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Reference syntax:<\/strong>&nbsp;<code>local.my_location<\/code>&nbsp;\u2014&nbsp;<strong>singular&nbsp;<code>local<\/code><\/strong>, even though the declaration is&nbsp;<code>locals<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Input variables vs. local values vs. output values<\/h3>\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\">Concept<\/th><th class=\"has-text-align-left\" data-align=\"left\">Block<\/th><th class=\"has-text-align-left\" data-align=\"left\">Reference<\/th><th class=\"has-text-align-left\" data-align=\"left\">Purpose<\/th><th class=\"has-text-align-left\" data-align=\"left\">Set using&nbsp;<code>terraform.tfvars<\/code>?<\/th><\/tr><\/thead><tbody><tr><td>Input variable<\/td><td><code>variable \"name\"<\/code><\/td><td><code>var.name<\/code><\/td><td>Accept configurable inputs<\/td><td>Yes<\/td><\/tr><tr><td>Local value<\/td><td><code>locals<\/code><\/td><td><code>local.name<\/code><\/td><td>Reuse or transform values inside configuration<\/td><td>No<\/td><\/tr><tr><td>Output value<\/td><td><code>output \"name\"<\/code><\/td><td><code>terraform output name<\/code>&nbsp;(root module CLI)<\/td><td>Expose results such as assigned public IPs<\/td><td>No<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Our single new&nbsp;<code>locals<\/code>&nbsp;block contains:<\/p>\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\">Local value<\/th><th class=\"has-text-align-left\" data-align=\"left\">Derived from<\/th><th class=\"has-text-align-left\" data-align=\"left\">Used by<\/th><th class=\"has-text-align-left\" data-align=\"left\">Result with Part 2 values<\/th><\/tr><\/thead><tbody><tr><td><code>local.resource_group_name<\/code><\/td><td><code>var.resource_names[\"resource_group\"]<\/code><\/td><td>Resource Group<\/td><td><code>rg-terraform-lab-eastus<\/code><\/td><\/tr><tr><td><code>local.azure_location<\/code><\/td><td><code>var.azure_region<\/code><\/td><td>Resource Group<\/td><td><code>eastus<\/code><\/td><\/tr><tr><td><code>local.vm_size<\/code><\/td><td><code>var.vm_size<\/code><\/td><td>Both VMs<\/td><td><code>Standard_B2s_v2<\/code><\/td><\/tr><tr><td><code>local.vm_names<\/code><\/td><td>Two existing map entries<\/td><td>Both VMs and an output<\/td><td>Windows and Ubuntu VM names<\/td><\/tr><tr><td><code>local.admin_source_cidrs<\/code><\/td><td><code>sort(tolist(var.admin_allowed_cidrs))<\/code><\/td><td>Both NSG rules and an output<\/td><td>Stable list of trusted CIDRs<\/td><\/tr><tr><td><code>local.access_ports<\/code><\/td><td>The existing RDP\/SSH port numbers<\/td><td>NSG rules and RDP connection output<\/td><td><code>rdp = \"3389\"<\/code>,&nbsp;<code>ssh = \"22\"<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The key teaching example is&nbsp;<strong><code>local.admin_source_cidrs<\/code><\/strong>. In Part 2, both NSG rules independently repeated&nbsp;<code>sort(tolist(var.admin_allowed_cidrs))<\/code>. In Part 3, the expression appears&nbsp;<strong>once<\/strong>&nbsp;in&nbsp;<code>locals<\/code>&nbsp;and both rules refer to the same result. The effective Azure configuration is unchanged.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. What are Output Values?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Outputs expose information&nbsp;<strong>from resources and expressions<\/strong>, such as the actual public IP Azure assigned to a VM. Terraform displays new or changed root-module output values after&nbsp;<code>terraform apply<\/code>. Later, students can retrieve them with&nbsp;<code>terraform output<\/code>&nbsp;instead of searching the Azure Portal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>output \"windows_public_ip\" {\n  description = \"Public IPv4 address of the Windows Server VM.\"\n  value       = azurerm_public_ip.windows.ip_address\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Output references are&nbsp;<strong>not data sources<\/strong>. This output reads an attribute of the&nbsp;<code>azurerm_public_ip.windows<\/code>&nbsp;<strong>resource already created by Terraform<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Outputs included in this lab<\/h3>\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\">Output<\/th><th class=\"has-text-align-left\" data-align=\"left\">Data type<\/th><th class=\"has-text-align-left\" data-align=\"left\">Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>resource_group_name<\/code><\/td><td>String<\/td><td>Existing Resource Group name<\/td><\/tr><tr><td><code>virtual_network_name<\/code><\/td><td>String<\/td><td>Existing VNet name<\/td><\/tr><tr><td><code>subnet_name<\/code><\/td><td>String<\/td><td>Existing subnet name<\/td><\/tr><tr><td><code>vm_names<\/code><\/td><td>Map\/object<\/td><td>Both VM names in one output<\/td><\/tr><tr><td><code>windows_public_ip<\/code><\/td><td>String<\/td><td>Public IP assigned to Windows<\/td><\/tr><tr><td><code>ubuntu_public_ip<\/code><\/td><td>String<\/td><td>Public IP assigned to Ubuntu<\/td><\/tr><tr><td><code>windows_rdp_target<\/code><\/td><td>String<\/td><td>Windows IP and RDP port (<code>IP:3389<\/code>)<\/td><\/tr><tr><td><code>ubuntu_ssh_command<\/code><\/td><td>String<\/td><td>Ready-to-copy SSH command<\/td><\/tr><tr><td><code>allowed_admin_cidrs<\/code><\/td><td>List of strings<\/td><td>CIDRs permitted by the inbound NSG rules<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>No passwords are output.<\/strong>&nbsp;Output values are saved in Terraform state. Avoid exporting passwords, tokens, private keys, or other secrets. Even&nbsp;<code>sensitive = true<\/code>&nbsp;outputs can be revealed via certain Terraform CLI flags or the state file.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. File layout<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>azure-terraform-labs\/\n\u251c\u2500\u2500 main.tf             # Full Part 3: provider + variables + locals + 13 resources + outputs\n\u251c\u2500\u2500 terraform.tfvars    # UNCHANGED input values from Part 2\n\u251c\u2500\u2500 terraform.tfstate   # Existing state (when using local state)\n\u2514\u2500\u2500 .terraform.lock.hcl # Existing provider dependency lock\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use&nbsp;<strong>only the two authored files<\/strong>,&nbsp;<code>main.tf<\/code>&nbsp;and&nbsp;<code>terraform.tfvars<\/code>, for this lesson. Do not add other&nbsp;<code>.tf<\/code>&nbsp;files or redeclare resource blocks.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Complete&nbsp;<code>main.tf<\/code>&nbsp;\u2014 copy everything<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The following is a&nbsp;<strong>complete replacement file<\/strong>, not a fragment. It retains every Part 2 variable and resource and adds the&nbsp;<code>locals<\/code>&nbsp;and&nbsp;<code>output<\/code>&nbsp;blocks.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># ============================================\n# PART 3 - INPUT VARIABLES + LOCAL VALUES + OUTPUTS\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# INPUT VARIABLES FROM PART 2 (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\n# ============================================================\n# PART 3 - LOCAL VALUES (DERIVED FROM PART 2 INPUT VARIABLES)\n# ============================================================\nlocals {\n  # Name and region from the existing input variables.\n  resource_group_name = var.resource_names&#91;\"resource_group\"]\n  azure_location      = var.azure_region\n  vm_size             = var.vm_size\n\n  # Reusable map of the names of the two existing VMs.\n  vm_names = {\n    windows = var.resource_names&#91;\"windows_vm\"]\n    ubuntu  = var.resource_names&#91;\"ubuntu_vm\"]\n  }\n\n  # Turn the input set into a consistently ordered list for both NSG rules.\n  admin_source_cidrs = sort(tolist(var.admin_allowed_cidrs))\n\n  # Reuse the same port definitions in security rules and outputs.\n  access_ports = {\n    rdp = \"3389\"\n    ssh = \"22\"\n  }\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# --------------------------------------------\n# 1. Resource Group\n# --------------------------------------------\nresource \"azurerm_resource_group\" \"lab\" {\n  name     = local.resource_group_name\n  location = local.azure_location\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      = local.access_ports.rdp\n  source_address_prefixes     = local.admin_source_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      = local.access_ports.ssh\n  source_address_prefixes     = local.admin_source_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                = local.vm_names.windows\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  size           = local.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                = local.vm_names.ubuntu\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n\n  size           = local.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\n# ============================================================\n# PART 3 - OUTPUT VALUES (NO NEW AZURE RESOURCES)\n# ============================================================\n\n# Single string output: Resource Group name\noutput \"resource_group_name\" {\n  description = \"Name of the existing Azure resource group.\"\n  value       = azurerm_resource_group.lab.name\n}\n\n# Single string output: VNet name\noutput \"virtual_network_name\" {\n  description = \"Name of the lab virtual network.\"\n  value       = azurerm_virtual_network.lab.name\n}\n\n# Single string output: Subnet name\noutput \"subnet_name\" {\n  description = \"Name of the VM subnet.\"\n  value       = azurerm_subnet.vms.name\n}\n\n# Map\/object output: both VM names\noutput \"vm_names\" {\n  description = \"Windows and Ubuntu VM names, grouped by operating system.\"\n  value       = local.vm_names\n}\n\n# Computed Azure IP addresses (available once resources exist)\noutput \"windows_public_ip\" {\n  description = \"Public IPv4 address of the Windows Server VM.\"\n  value       = azurerm_public_ip.windows.ip_address\n}\n\noutput \"ubuntu_public_ip\" {\n  description = \"Public IPv4 address of the Ubuntu Server VM.\"\n  value       = azurerm_public_ip.ubuntu.ip_address\n}\n\n# Computed text outputs: connection hints (no passwords)\noutput \"windows_rdp_target\" {\n  description = \"RDP target for Windows Remote Desktop.\"\n  value       = \"${azurerm_public_ip.windows.ip_address}:${local.access_ports.rdp}\"\n}\n\noutput \"ubuntu_ssh_command\" {\n  description = \"SSH command to access Ubuntu from an authorized IP address.\"\n  value       = \"ssh ${var.admin_username}@${azurerm_public_ip.ubuntu.ip_address}\"\n}\n\n# List output: stable, sorted CIDRs used by both NSG rules\noutput \"allowed_admin_cidrs\" {\n  description = \"Sorted source CIDRs authorized for SSH and RDP by the lab NSG.\"\n  value       = local.admin_source_cidrs\n}\n\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">5. Complete&nbsp;<code>terraform.tfvars<\/code>&nbsp;\u2014 keep the same values as Part 2<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>If your Part 2 lab is already deployed, leave the actual values unchanged.<\/strong>&nbsp;In particular, preserve the two existing VM passwords. Replacing them with the illustrative passwords below when your real deployment uses different passwords can cause a VM replacement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The placeholder&nbsp;<code>203.0.113.10\/32<\/code>&nbsp;is&nbsp;<strong>not a usable remote-access address<\/strong>. If you already replaced it with your current public IPv4&nbsp;<code>\/32<\/code>&nbsp;in Part 2, keep that working address.<\/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\">6. What exactly changed compared with Part 2?<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">6.1 The new&nbsp;<code>locals<\/code>&nbsp;block<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>locals {\n  resource_group_name = var.resource_names&#91;\"resource_group\"]\n  azure_location      = var.azure_region\n  vm_size             = var.vm_size\n\n  vm_names = {\n    windows = var.resource_names&#91;\"windows_vm\"]\n    ubuntu  = var.resource_names&#91;\"ubuntu_vm\"]\n  }\n\n  admin_source_cidrs = sort(tolist(var.admin_allowed_cidrs))\n\n  access_ports = {\n    rdp = \"3389\"\n    ssh = \"22\"\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform evaluates these expressions; there is no separate Azure object called a&nbsp;<em>local value<\/em>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6.2 Changes to existing resources \u2014 values remain the same<\/h3>\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\">In Part 2<\/th><th class=\"has-text-align-left\" data-align=\"left\">In Part 3<\/th><th class=\"has-text-align-left\" data-align=\"left\">Why<\/th><\/tr><\/thead><tbody><tr><td><code>name = var.resource_names[\"resource_group\"]<\/code><\/td><td><code>name = local.resource_group_name<\/code><\/td><td>Readability<\/td><\/tr><tr><td><code>location = var.azure_region<\/code><\/td><td><code>location = local.azure_location<\/code><\/td><td>Demonstrate local references<\/td><\/tr><tr><td><code>size = var.vm_size<\/code><\/td><td><code>size = local.vm_size<\/code><\/td><td>Reuse VM size in both VMs<\/td><\/tr><tr><td><code>name = var.resource_names[\"windows_vm\"]<\/code><\/td><td><code>name = local.vm_names.windows<\/code><\/td><td>Use a local map<\/td><\/tr><tr><td><code>name = var.resource_names[\"ubuntu_vm\"]<\/code><\/td><td><code>name = local.vm_names.ubuntu<\/code><\/td><td>Use a local map<\/td><\/tr><tr><td><code>source_address_prefixes = sort(tolist(var.admin_allowed_cidrs))<\/code><\/td><td><code>source_address_prefixes = local.admin_source_cidrs<\/code><\/td><td>Avoid duplicate conversion\/sorting<\/td><\/tr><tr><td><code>destination_port_range = \"3389\"<\/code><\/td><td><code>destination_port_range = local.access_ports.rdp<\/code><\/td><td>Name RDP port<\/td><\/tr><tr><td><code>destination_port_range = \"22\"<\/code><\/td><td><code>destination_port_range = local.access_ports.ssh<\/code><\/td><td>Name SSH port<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Important:<\/strong>&nbsp;<code>local<\/code>&nbsp;expressions produce the same&nbsp;<strong>values<\/strong>&nbsp;as the original Part 2 code; they don&#8217;t automatically create, destroy, or rename resources. These expression-only changes should not require changing your Azure resources, provided your prior state and&nbsp;<code>.tfvars<\/code>&nbsp;still match. Unexpected Azure resource changes must be investigated before applying.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6.3 Nine new outputs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Outputs reference the existing resources and computed locals. The most useful ones are:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>output \"windows_public_ip\" {\n  description = \"Public IPv4 address of the Windows Server VM.\"\n  value       = azurerm_public_ip.windows.ip_address\n}\n\noutput \"ubuntu_ssh_command\" {\n  description = \"SSH command to access Ubuntu from an authorized IP address.\"\n  value       = \"ssh ${var.admin_username}@${azurerm_public_ip.ubuntu.ip_address}\"\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The username and IP are exposed,&nbsp;<strong>not<\/strong>&nbsp;the administrator password. RDP and SSH connections also depend on your real public IP being permitted by the NSG, the guest OS being ready, and any organizational network restrictions.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Execute Part 3 safely<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1 \u2014 Verify correct directory, identity, and existing state<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code><em># Run inside the SAME Part 2 directory.<\/em>\naz login\naz account set --subscription \"YOUR_SUBSCRIPTION_ID\"\nexport ARM_SUBSCRIPTION_ID=\"$(az account show --query id -o tsv)\"\n\nterraform version\nterraform state list\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With an applied Part 2,&nbsp;<code>terraform state list<\/code>&nbsp;should show the&nbsp;<strong>same 13 Terraform resource addresses<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2 \u2014 Back up your existing code and state securely<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code><em># Save a private copy of the existing main.tf before replacing it.<\/em>\ncp main.tf main.part2.backup.txt\n\n<em># If and only if you are using local Terraform state:<\/em>\ncp terraform.tfstate terraform.tfstate.part2.backup\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Note:<\/strong>&nbsp;The backups may contain or reference secrets; keep them outside public version control. Do not save the backup as another&nbsp;<code>.tf<\/code>&nbsp;file, because Terraform loads every&nbsp;<code>.tf<\/code>&nbsp;file in the directory, and duplicate blocks would break validation. Remote state users should follow their backend&#8217;s snapshot\/backup procedure instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3 \u2014 Replace files<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Replace\u00a0<code>main.tf<\/code>\u00a0with the\u00a0<strong>complete Part 3<\/strong>\u00a0file in Section 4.<\/li>\n\n\n\n<li>Keep your existing Part 2\u00a0<code>terraform.tfvars<\/code>\u00a0(Section 5 is a full reference copy).<\/li>\n\n\n\n<li>Don&#8217;t modify the 13 resource addresses, existing images, VM passwords, resource names, network settings, or state.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4 \u2014 Format, validate, and inspect the plan<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform init\nterraform fmt\nterraform validate\nterraform plan -out=lab-part3.tfplan\nterraform show lab-part3.tfplan\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Expected for a previously applied, unchanged Part 2 environment:<\/strong>&nbsp;no Azure resource additions, updates, deletions, or replacements. Terraform should show output-value additions. The real plan is authoritative: if it proposes a VM replacement, resource deletion, or unexpected network change,&nbsp;<strong>do not apply<\/strong>&nbsp;until the difference is understood.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5 \u2014 Apply the reviewed plan<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform apply lab-part3.tfplan\nterraform state list\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Adding outputs changes Terraform&#8217;s recorded output values, even if Azure resources themselves require no updates.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. Practice the&nbsp;<code>terraform output<\/code>&nbsp;command<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code><em># Display every root-module output.<\/em>\nterraform output\n\n<em># Query a specific output.<\/em>\nterraform output windows_public_ip\n\n<em># Get a raw string without quotes; helpful for terminal scripts.<\/em>\nterraform output -raw ubuntu_public_ip\n\n<em># Display a single complex value such as vm_names as JSON.<\/em>\nterraform output -json vm_names\n\n<em># Display all outputs in JSON (does not mask sensitive outputs).<\/em>\nterraform output -json\n\n<em># Get your connection hint directly.<\/em>\nterraform output -raw ubuntu_ssh_command\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A&nbsp;<strong>hypothetical<\/strong>&nbsp;result after&nbsp;<code>terraform apply<\/code>&nbsp;(the IPs below are illustrative; your actual IPs will differ):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>allowed_admin_cidrs = tolist(&#91;\n  \"YOUR.PUBLIC.IP.ADDRESS\/32\",\n])\nresource_group_name = \"rg-terraform-lab-eastus\"\nsubnet_name = \"snet-vms\"\nubuntu_public_ip = \"ACTUAL_UBUNTU_PUBLIC_IP\"\nubuntu_ssh_command = \"ssh labadmin@ACTUAL_UBUNTU_PUBLIC_IP\"\nvirtual_network_name = \"vnet-terraform-lab\"\nvm_names = {\n  \"ubuntu\" = \"vm-ubuntu2604-01\"\n  \"windows\" = \"vm-win2025-01\"\n}\nwindows_public_ip = \"ACTUAL_WINDOWS_PUBLIC_IP\"\nwindows_rdp_target = \"ACTUAL_WINDOWS_PUBLIC_IP:3389\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use your actual output values; don&#8217;t attempt to connect to the illustrative text above.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Optional: inspect the locals in Terraform Console<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform console\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then enter one expression per line:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>local.vm_names\nlocal.admin_source_cidrs\nlocal.access_ports.rdp\nlocal.vm_size\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Type&nbsp;<code>exit<\/code>&nbsp;to return to your shell. This is only a read\/evaluation exercise; it doesn&#8217;t add an Azure resource.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">9. Student exercises<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Identify:<\/strong>\u00a0Which expressions begin with\u00a0<code>var.<\/code>\u00a0and which begin with\u00a0<code>local.<\/code>\u00a0in\u00a0<code>main.tf<\/code>?<\/li>\n\n\n\n<li><strong>Inspect:<\/strong>\u00a0Run\u00a0<code>terraform output -raw windows_public_ip<\/code>\u00a0and locate the same IP in the Azure Portal.<\/li>\n\n\n\n<li><strong>Compare:<\/strong>\u00a0Run\u00a0<code>terraform output -json vm_names<\/code>\u00a0and explain why\u00a0<code>-raw vm_names<\/code>\u00a0is not suitable for a map\/object.<\/li>\n\n\n\n<li><strong>Understand reuse:<\/strong>\u00a0Find both NSG security rules that reference\u00a0<code>local.admin_source_cidrs<\/code>.<\/li>\n\n\n\n<li><strong>Safety check:<\/strong>\u00a0Explain why\u00a0<code>windows_admin_password<\/code>\u00a0should not be exported as a plain output value.<\/li>\n\n\n\n<li><strong>Review drift:<\/strong>\u00a0After using locals, explain why an existing Azure VM should not need replacement when its configured values stay unchanged.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">10. Success checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ] Working directory and Terraform state are the same as in Part 2.<\/li>\n\n\n\n<li>[ ] All\u00a0<strong>12 input variables<\/strong>\u00a0remain declared and supplied.<\/li>\n\n\n\n<li>[ ] All\u00a0<strong>13 original Azure resources<\/strong>\u00a0retain the same Terraform addresses.<\/li>\n\n\n\n<li>[ ] Exactly\u00a0<strong>one\u00a0<code>locals<\/code>\u00a0block<\/strong>\u00a0exists and its values are actually referenced.<\/li>\n\n\n\n<li>[ ] Exactly\u00a0<strong>nine new output blocks<\/strong>\u00a0exist.<\/li>\n\n\n\n<li>[ ] No extra\u00a0<code>resource<\/code>,\u00a0<code>data<\/code>,\u00a0<code>module<\/code>,\u00a0<code>count<\/code>, or\u00a0<code>for_each<\/code>\u00a0blocks were added.<\/li>\n\n\n\n<li>[ ]\u00a0<code>terraform fmt<\/code>\u00a0and\u00a0<code>terraform validate<\/code>\u00a0succeed on the student&#8217;s machine.<\/li>\n\n\n\n<li>[ ]\u00a0<code>terraform plan<\/code>\u00a0contains no unexpected Azure changes or replacements.<\/li>\n\n\n\n<li>[ ]\u00a0<code>terraform apply<\/code>\u00a0completes and\u00a0<code>terraform output<\/code>\u00a0displays the expected values.<\/li>\n\n\n\n<li>[ ] No administrator passwords are exposed in output blocks.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">11. Common problems<\/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\">Problem<\/th><th class=\"has-text-align-left\" data-align=\"left\">Likely cause<\/th><th class=\"has-text-align-left\" data-align=\"left\">Solution<\/th><\/tr><\/thead><tbody><tr><td><code>Reference to undeclared local value<\/code><\/td><td>Typo in&nbsp;<code>local.some_name<\/code><\/td><td>Match the local name declared inside&nbsp;<code>locals {}<\/code>&nbsp;exactly.<\/td><\/tr><tr><td><code>Duplicate output definition<\/code><\/td><td>Old output blocks or other&nbsp;<code>.tf<\/code>&nbsp;files have duplicate names<\/td><td>Use the full replacement file, not multiple copies.<\/td><\/tr><tr><td><code>No outputs found<\/code><\/td><td>Output blocks were not applied or wrong Terraform state<\/td><td>Apply the reviewed plan in the original Part 2 directory and state.<\/td><\/tr><tr><td><code>Unsupported attribute<\/code>&nbsp;on resource output<\/td><td>Mistyped provider resource attribute<\/td><td>Use&nbsp;<code>azurerm_public_ip.windows.ip_address<\/code>&nbsp;(not&nbsp;<code>.public_ip_address<\/code>).<\/td><\/tr><tr><td><code>-raw is not supported for complex types<\/code><\/td><td>Attempt to use&nbsp;<code>-raw<\/code>&nbsp;on a map\/list<\/td><td>Use&nbsp;<code>terraform output -json vm_names<\/code>&nbsp;instead.<\/td><\/tr><tr><td>Terraform proposes replacement<\/td><td>Password\/name\/image\/subnet\/provider drift, or wrong state<\/td><td>Stop; compare against the actual applied Part 2 configuration.<\/td><\/tr><tr><td>RDP or SSH cannot connect<\/td><td>Public IP allowlist, credentials, host readiness or Azure networking<\/td><td>Use current public IPv4&nbsp;<code>\/32<\/code>, correct VM IP, and inspect NSG rules.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">12. Cleanup \u2014 only at the end of the entire course<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Don&#8217;t destroy between Parts 2 and 3. When finished with the training environment:<\/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\">Terraform removes managed Azure infrastructure.&nbsp;<code>locals<\/code>&nbsp;and&nbsp;<code>output<\/code>&nbsp;are not separate Azure resources; they don&#8217;t have Azure cleanup operations of their own.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">13. Accuracy, security and compatibility notes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The\u00a0<strong>Part 2 uploaded lab<\/strong>\u00a0is the source for the 12 variable declarations,\u00a0<code>terraform.tfvars<\/code>\u00a0values, 13 Azure resources, resource names, and image settings; this document extends it with locals and outputs only.<\/li>\n\n\n\n<li>The example uses Windows Server 2025 Datacenter and Ubuntu Server 26.04 LTS, exactly as in Part 2. No image upgrade is attempted in this lesson.<\/li>\n\n\n\n<li>A sensitive input variable can still be stored in Terraform state. Do not commit\u00a0<code>terraform.tfvars<\/code>, state, saved plans, or password-bearing backups to a public repository.<\/li>\n\n\n\n<li>Outputs are visible to anyone with access to the Terraform output\/state. This lab outputs\u00a0<strong>names, IDs\/addresses and connectivity hints<\/strong>, not passwords.<\/li>\n\n\n\n<li>The local values and outputs were checked for consistency with the uploaded Part 2 file.\u00a0<strong>A live Azure\u00a0<code>terraform plan<\/code>\u00a0\/\u00a0<code>apply<\/code>\u00a0was not run<\/strong>, and a successful Azure deployment depends on permissions, quota, resource availability, policy and current state. Do not treat any training example as a guarantee of a particular subscription&#8217;s deployment result.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Official references<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/values\/locals\">HashiCorp \u2014 Use locals to reuse expressions<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/values\/outputs\">HashiCorp \u2014 Use outputs to expose module data<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/cli\/commands\/output\">HashiCorp \u2014\u00a0<code>terraform output<\/code>\u00a0command<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/registry.terraform.io\/providers\/hashicorp\/azurerm\/latest\/docs\/resources\/public_ip\">AzureRM provider \u2014\u00a0<code>azurerm_public_ip<\/code>\u00a0resource<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/registry.terraform.io\/providers\/hashicorp\/azurerm\/latest\/docs\/resources\/windows_virtual_machine\">AzureRM provider \u2014\u00a0<code>azurerm_windows_virtual_machine<\/code>\u00a0resource<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/registry.terraform.io\/providers\/hashicorp\/azurerm\/latest\/docs\/resources\/linux_virtual_machine\">AzureRM provider \u2014\u00a0<code>azurerm_linux_virtual_machine<\/code>\u00a0resource<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Continuation of Labs 1\u20134 and Part 2 (All Six Input Variable Types)Level:&nbsp;Beginner \u2192 Early IntermediateTerraform:&nbsp;1.15.9+ within version 1.xAzureRM provider:&nbsp;5.9.x (same provider constraint as Part 2)Date:&nbsp;12 October 2026&#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-1229","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1229","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=1229"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1229\/revisions"}],"predecessor-version":[{"id":1230,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1229\/revisions\/1230"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/media?parent=1229"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/categories?post=1229"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/tags?post=1229"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}