Terraform Lab – Azure Hands-On – Conditional Expressions and Logic

This workbook teaches the complete set of Terraform conditionals, filtering, validation, null handling, and related functions from the requested syllabus. Every numbered concept includes one standalone AWS EC2 example and one standalone Azure Ubuntu Linux VM example, with provider configuration, authentication instructions, network dependencies, complete HCL, observable outputs, a change-to-test command, and cleanup steps.

Important: Each example is independent. Do NOT paste all 64 Terraform examples into a single directory. The full main.tf for each example is included below, and the companion ZIP contains one directory per cloud/scenario, pre-populated with the same files. Only the conditional-module scenario uses a small local child module; both files are included.

A. Learning objectives

After the workbook, students can:

  1. Select values with ternary expressions and combine logical/comparison operators.
  2. Create zero/one resources using count and filter selected resources with for_each.
  3. Conditionally create repeatable nested configuration via dynamic.
  4. Distinguish variable validation, resource preconditions/postconditions, output preconditions, and nonblocking checks.
  5. Use try, can, lookup, coalesce, coalescelist, contains, alltrue, anytrue, length, regex, startswith, and endswith safely.
  6. Work with null, optional attributes, nullable, conditional maps, merge, compact, and flatten.

B. Correct mental model

  • condition ? true_value : false_value selects one value. Terraform does not have a general-purpose if { } else { } statement for resource declarations.
  • count repeats a resource using numeric indexes ([0], [1], …). for_each repeats it using stable keys. Use count = condition ? 1 : 0 for an optional VM.
  • for with if filters data; it does not directly create cloud resources unless its result is supplied to something like for_each.
  • dynamic makes zero/more supported nested blocks. It cannot generate top-level resource, module, lifecycle, or provisioner blocks.
  • Preconditions / postconditions stop an operation when their assumptions fail. A check emits warnings but normally does not stop plan/apply.
  • try() and can() catch expression evaluation errors, not arbitrary Terraform syntax errors or invalid references to undeclared resources.
  • null normally means omit an optional resource argument; it cannot satisfy a required argument.

C. Cloud prerequisites (do once per cloud)

AWS prerequisites

  • An AWS account with EC2, VPC, security-group, SSM Parameter Store read access, and IAM permissions to create/delete the listed resources.
  • Terraform 1.15.x / 1.16.x and AWS CLI. Configure AWS authentication using aws configure or AWS SSO. Do not put credentials in Terraform code.
  • Region us-east-1; instance type t3.micro (large alternative: t3.small); Amazon Linux 2023 uses the official SSM public parameter instead of a region-specific hard-coded AMI ID.
  • AWS configurations build their own VPC, subnet, Internet Gateway, route table, security group (no inbound access), and EC2 instance(s). SSH is not enabled; the learning goal is Terraform logic, observed through plan, AWS CLI, and outputs. To add shell access later, use a separately secured SSM/EC2 Instance Connect lab.
aws sts get-caller-identity
aws configure get region
aws ssm get-parameter --region us-east-1 \
  --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --query Parameter.Value --output text

Azure prerequisites

  • An Azure subscription, Azure CLI, permissions to create resource groups/networks/VMs, and registered Microsoft.Compute / Microsoft.Network namespaces.
  • Region eastus; VM size Standard_B1s (large alternative: Standard_B2s); official Canonical Ubuntu 24.04 LTS image Canonical:ubuntu-24_04-lts:server:latest.
  • Each Azure lab includes a Resource Group, VNet, subnet, Network Security Group and SSH rule, subnet-NSG association, public IP, NIC, OS disk and Ubuntu VM (except when a condition sets VM count to zero). SSH is allowed only from the IP in TF_VAR_admin_cidr.
  • Azure VM password is supplied at runtime, never hard-coded in the file. The password is still stored in Terraform state; protect the state, shell session and saved plan. For production prefer SSH keys / managed access.
az login
az account set --subscription "YOUR_AZURE_SUBSCRIPTION_ID"
export ARM_SUBSCRIPTION_ID="$(az account show --query id -o tsv)"

# Set the external IPv4 address of the workstation used for SSH.
export TF_VAR_admin_cidr="$(curl -fsS -4 https://api.ipify.org)/32"

# Interactive, private Azure VM password input (12+ chars, strong complexity).
printf "Enter a strong Azure lab VM password: "
read -r -s TF_VAR_vm_password
printf "
"
export TF_VAR_vm_password

# Image and VM SKU verification (region/subscription restrictions may apply)
az vm image show --location eastus \
  --urn Canonical:ubuntu-24_04-lts:server:latest --output table
az vm list-skus --location eastus --size Standard_B1s --all --output table

The provided Azure source-IP variable is required for all Azure labs, even if you never SSH. The IP restriction is designed for supervised short-lived training. Change it if your current office/VPN egress IP changes. Never use 0.0.0.0/0 for password-based SSH access.

General per-lab workflow

Use one directory and state per example (the ZIP already supplies them):

# Example; choose one cloud and scenario directory.
cd aws/01-ternary        # or: cd azure/01-ternary
terraform init
terraform fmt -recursive
terraform fmt -check -recursive
terraform validate
terraform plan
terraform apply
terraform output

# Inspect the EC2 or Azure VM / outputs, try that scenario's test command,
# and DESTROY the lab before moving on to avoid charges.
terraform plan -destroy
terraform destroy
  • For cloud verification: AWS terraform output -json instance_ids, then check those IDs in the AWS EC2 Console (or use aws ec2 describe-instances). Azure az vm list -g rg-cond-01-eastus -o table (substitute the current scenario number).
  • In all examples, a test command shown as terraform plan -var=... demonstrates changed configuration without automatically applying it. If you apply a test change, use the same override at destroy time or restore the default value before cleaning up; carefully inspect plans for replacements.
  • Azure VM size/image availability, subscription quotas, required provider registrations, Azure policies and cloud costs can prevent an otherwise correctly structured lab from deploying. Avoid running all 64 labs concurrently.

D. Complete 32-concept syllabus and standalone files

#ConceptAWS folderAzure folder
1Conditional expression (ternary)aws/01-ternaryazure/01-ternary
2Logical AND (&&)aws/02-andazure/02-and
3Logical OR ()
4Logical NOT (!)aws/04-notazure/04-not
5Comparison operatorsaws/05-comparisonsazure/05-comparisons
6Parentheses and compound logicaws/06-compoundazure/06-compound
7count with conditionaws/07-countazure/07-count
8for_each with filteringaws/08-for-eachazure/08-for-each
9for expressions with ifaws/09-for-ifazure/09-for-if
10Conditional dynamic blocksaws/10-dynamicazure/10-dynamic
11Conditional module instantiationaws/11-moduleazure/11-module
12Input variable validationaws/12-validationazure/12-validation
13Resource preconditionaws/13-preconditionazure/13-precondition
14Resource postconditionaws/14-postconditionazure/14-postcondition
15Output preconditionaws/15-output-preconditionazure/15-output-precondition
16check block (warning)aws/16-checkazure/16-check
17try()aws/17-tryazure/17-try
18can()aws/18-canazure/18-can
19lookup()aws/19-lookupazure/19-lookup
20coalesce()aws/20-coalesceazure/20-coalesce
21coalescelist()aws/21-coalescelistazure/21-coalescelist
22contains()aws/22-containsazure/22-contains
23alltrue()aws/23-alltrueazure/23-alltrue
24anytrue()aws/24-anytrueazure/24-anytrue
25length()aws/25-lengthazure/25-length
26regex() and regexall()aws/26-regexazure/26-regex
27startswith() and endswith()aws/27-startswith-endswithazure/27-startswith-endswith
28null valuesaws/28-nullazure/28-null
29Optional object attributesaws/29-optionalazure/29-optional
30nullable variable settingaws/30-nullableazure/30-nullable
31merge() and conditional mapsaws/31-mergeazure/31-merge
32compact() and flatten()aws/32-compact-flattenazure/32-compact-flatten

1. Conditional expression (ternary)

Category: Expressions. Student goal: Choose an EC2/VM size with condition ? value_if_true : value_if_false.

Plain English: A ternary is a one-line choice: test a Boolean expression, then use either the first or second value. Terraform has no resource-level if/else statement, so this is the usual way to select a VM size or optional setting. Both result branches should have compatible types.

1.1 AWS EC2 – complete standalone code

Files: aws/01-ternary/main.tf. Default: small instance. With environment=prod: larger instance.

# Standalone AWS lab 01: Conditional expression (ternary)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "environment" {
  type = string
  default = "dev"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-01-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-01-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-01-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-01-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-01-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-01-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = var.environment == "prod" ? "t3.small" : "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-01-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "selected_size" {
  value = var.environment == "prod" ? "t3.small" : "t3.micro"
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=prod'

Expected learning: Default: small instance. With environment=prod: larger instance. Clean up with terraform destroy in this exact lab directory.

1.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/01-ternary/main.tf. Default: small instance. With environment=prod: larger instance.

# Standalone Azure lab 01: Conditional expression (ternary)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "environment" {
  type = string
  default = "dev"
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-01-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-01"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-01"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-01"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-01"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-01"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-01"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = var.environment == "prod" ? "Standard_B2s" : "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "selected_size" {
  value = var.environment == "prod" ? "Standard_B2s" : "Standard_B1s"
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=prod'

Expected learning: Default: small instance. With environment=prod: larger instance. Clean up with terraform destroy in this exact lab directory.


2. Logical AND (&&)

Category: Expressions. Student goal: Both conditions must be true before enabling an option.

Plain English: && means both conditions must be true. Here, environment must be production and monitoring must be requested. The EC2 example enables detailed EC2 monitoring (which can cost extra); the Azure example demonstrates the decision as a tag, not a Monitor Agent installation.

2.1 AWS EC2 – complete standalone code

Files: aws/02-and/main.tf. The decision is true only when both inputs are true. AWS uses EC2 detailed monitoring; Azure records the decision as a VM tag (not Azure Monitor deployment).

# Standalone AWS lab 02: Logical AND (&&)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "environment" {
  type = string
  default = "prod"
}

variable "enable_monitoring" {
  type = bool
  default = true
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  monitor_enabled = var.environment == "prod" && var.enable_monitoring
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-02-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-02-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-02-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-02-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-02-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-02-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  monitoring = local.monitor_enabled
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-02-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "monitoring_decision" {
  value = local.monitor_enabled
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='enable_monitoring=false'

Expected learning: The decision is true only when both inputs are true. AWS uses EC2 detailed monitoring; Azure records the decision as a VM tag (not Azure Monitor deployment). Clean up with terraform destroy in this exact lab directory.

2.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/02-and/main.tf. The decision is true only when both inputs are true. AWS uses EC2 detailed monitoring; Azure records the decision as a VM tag (not Azure Monitor deployment).

# Standalone Azure lab 02: Logical AND (&&)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "environment" {
  type = string
  default = "prod"
}

variable "enable_monitoring" {
  type = bool
  default = true
}
locals {
  monitor_enabled = var.environment == "prod" && var.enable_monitoring
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-02-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-02"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-02"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-02"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-02"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-02"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-02"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { MonitoringRequested = tostring(local.monitor_enabled) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "monitoring_decision" {
  value = local.monitor_enabled
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='enable_monitoring=false'

Expected learning: The decision is true only when both inputs are true. AWS uses EC2 detailed monitoring; Azure records the decision as a VM tag (not Azure Monitor deployment). Clean up with terraform destroy in this exact lab directory.


3. Logical OR (||)

Category: Expressions. Student goal: Match either of two environments to select a tag.

Plain English: || means either condition may be true. The dev and test values both count as nonproduction, and anything else takes the other branch. This affects visible tags rather than requiring extra networking.

3.1 AWS EC2 – complete standalone code

Files: aws/03-or/main.tf. dev or test -> nonproduction=true; prod -> false.

# Standalone AWS lab 03: Logical OR (||)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "environment" {
  type = string
  default = "dev"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  nonproduction = var.environment == "dev" || var.environment == "test"
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-03-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-03-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-03-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-03-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-03-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-03-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = local.nonproduction ? "logic-nonprod" : "logic-production" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "nonproduction" {
  value = local.nonproduction
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=prod'

Expected learning: dev or test -> nonproduction=true; prod -> false. Clean up with terraform destroy in this exact lab directory.

3.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/03-or/main.tf. dev or test -> nonproduction=true; prod -> false.

# Standalone Azure lab 03: Logical OR (||)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "environment" {
  type = string
  default = "dev"
}
locals {
  nonproduction = var.environment == "dev" || var.environment == "test"
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-03-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-03"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-03"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-03"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-03"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-03"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-03"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Tier = local.nonproduction ? "nonprod" : "production" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "nonproduction" {
  value = local.nonproduction
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=prod'

Expected learning: dev or test -> nonproduction=true; prod -> false. Clean up with terraform destroy in this exact lab directory.


4. Logical NOT (!)

Category: Expressions. Student goal: Invert a flag to choose a smaller or larger VM.

Plain English: ! reverses a Boolean. !false is true; !true is false. The VM size selection makes it easy to see what changes when you flip the flag.

4.1 AWS EC2 – complete standalone code

Files: aws/04-not/main.tf. NOT reverses the Boolean. false -> smaller; true -> larger.

# Standalone AWS lab 04: Logical NOT (!)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "high_capacity" {
  type = bool
  default = false
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-04-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-04-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-04-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-04-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-04-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-04-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = !var.high_capacity ? "t3.micro" : "t3.small"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-04-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "small_selected" {
  value = !var.high_capacity
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='high_capacity=true'

Expected learning: NOT reverses the Boolean. false -> smaller; true -> larger. Clean up with terraform destroy in this exact lab directory.

4.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/04-not/main.tf. NOT reverses the Boolean. false -> smaller; true -> larger.

# Standalone Azure lab 04: Logical NOT (!)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "high_capacity" {
  type = bool
  default = false
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-04-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-04"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-04"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-04"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-04"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-04"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-04"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = !var.high_capacity ? "Standard_B1s" : "Standard_B2s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "small_selected" {
  value = !var.high_capacity
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='high_capacity=true'

Expected learning: NOT reverses the Boolean. false -> smaller; true -> larger. Clean up with terraform destroy in this exact lab directory.


5. Comparison operators

Category: Expressions. Student goal: Compare numbers and expose ==, !=, >, <, >=, <=.

Plain English: Comparison operators produce true or false. The output map shows equality (==), inequality (!=), greater/less-than and their inclusive variants. The sample also uses > 2 to select the larger VM.

5.1 AWS EC2 – complete standalone code

Files: aws/05-comparisons/main.tf. Outputs show all six comparison operators; target above 2 selects the larger VM.

# Standalone AWS lab 05: Comparison operators
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "replica_target" {
  type = number
  default = 2
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  comparisons = {
    eq  = var.replica_target == 2
    ne  = var.replica_target != 2
    gt  = var.replica_target > 2
    lt  = var.replica_target < 2
    gte = var.replica_target >= 2
    lte = var.replica_target <= 2
  }
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-05-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-05-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-05-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-05-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-05-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-05-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = local.comparisons.gt ? "t3.small" : "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-05-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "comparison_results" {
  value = local.comparisons
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='replica_target=3'

Expected learning: Outputs show all six comparison operators; target above 2 selects the larger VM. Clean up with terraform destroy in this exact lab directory.

5.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/05-comparisons/main.tf. Outputs show all six comparison operators; target above 2 selects the larger VM.

# Standalone Azure lab 05: Comparison operators
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "replica_target" {
  type = number
  default = 2
}
locals {
  comparisons = {
    eq  = var.replica_target == 2
    ne  = var.replica_target != 2
    gt  = var.replica_target > 2
    lt  = var.replica_target < 2
    gte = var.replica_target >= 2
    lte = var.replica_target <= 2
  }
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-05-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-05"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-05"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-05"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-05"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-05"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-05"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = local.comparisons.gt ? "Standard_B2s" : "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "comparison_results" {
  value = local.comparisons
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='replica_target=3'

Expected learning: Outputs show all six comparison operators; target above 2 selects the larger VM. Clean up with terraform destroy in this exact lab directory.


6. Parentheses and compound logic

Category: Expressions. Student goal: Use grouping to control operator precedence.

Plain English: Parentheses show which parts should be evaluated together. This lab checks prod AND (critical OR force). Explicit grouping makes multi-part logic easier to read and review.

6.1 AWS EC2 – complete standalone code

Files: aws/06-compound/main.tf. Grouping evaluates the OR first, then AND with environment=prod.

# Standalone AWS lab 06: Parentheses and compound logic
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "environment" {
  type = string
  default = "prod"
}

variable "critical" {
  type = bool
  default = false
}

variable "force" {
  type = bool
  default = true
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  selected = (var.environment == "prod") && (var.critical || var.force)
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-06-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-06-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-06-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-06-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-06-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-06-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = local.selected ? "t3.small" : "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-06-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "compound_result" {
  value = local.selected
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='force=false'

Expected learning: Grouping evaluates the OR first, then AND with environment=prod. Clean up with terraform destroy in this exact lab directory.

6.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/06-compound/main.tf. Grouping evaluates the OR first, then AND with environment=prod.

# Standalone Azure lab 06: Parentheses and compound logic
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "environment" {
  type = string
  default = "prod"
}

variable "critical" {
  type = bool
  default = false
}

variable "force" {
  type = bool
  default = true
}
locals {
  selected = (var.environment == "prod") && (var.critical || var.force)
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-06-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-06"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-06"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-06"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-06"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-06"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-06"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = local.selected ? "Standard_B2s" : "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "compound_result" {
  value = local.selected
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='force=false'

Expected learning: Grouping evaluates the OR first, then AND with environment=prod. Clean up with terraform destroy in this exact lab directory.


7. count with condition

Category: Resource creation. Student goal: Create zero or one VM based on a Boolean.

Plain English: Set count to 1 to make one VM, or 0 to make none. The surrounding VPC/VNet networking stays because its resources are not conditional. Using a splat ([*].id) makes outputs safe even when the VM count is zero.

7.1 AWS EC2 – complete standalone code

Files: aws/07-count/main.tf. Default creates one VM. false creates zero VMs (the network resources remain).

# Standalone AWS lab 07: count with condition
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "create_vm" {
  type = bool
  default = true
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-07-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-07-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-07-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-07-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-07-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-07-sg" }
}

resource "aws_instance" "lab" {
  count = var.create_vm ? 1 : 0
  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-07-ec2" }
}

output "instance_ids" {
  value = aws_instance.lab[*].id
}

output "public_ips" {
  value = aws_instance.lab[*].public_ip
}

output "vm_created" {
  value = var.create_vm
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='create_vm=false'

Expected learning: Default creates one VM. false creates zero VMs (the network resources remain). Clean up with terraform destroy in this exact lab directory.

7.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/07-count/main.tf. Default creates one VM. false creates zero VMs (the network resources remain).

# Standalone Azure lab 07: count with condition
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "create_vm" {
  type = bool
  default = true
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-07-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-07"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-07"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-07"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-07"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-07"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {
  count = var.create_vm ? 1 : 0
  name                            = "vm-cond-07"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = azurerm_linux_virtual_machine.lab[*].id
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "vm_created" {
  value = var.create_vm
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='create_vm=false'

Expected learning: Default creates one VM. false creates zero VMs (the network resources remain). Clean up with terraform destroy in this exact lab directory.


8. for_each with filtering

Category: Resource creation. Student goal: Create only enabled VMs from a map.

Plain English: for_each makes one VM per item in a map. The preceding for ... if enabled expression removes disabled entries before Terraform creates anything. Azure must also make one NIC and public IP for each selected VM.

8.1 AWS EC2 – complete standalone code

Files: aws/08-for-each/main.tf. Default creates two VMs, for web and api. jobs is excluded.

# Standalone AWS lab 08: for_each with filtering
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "workloads" {
  type = map(bool)
  default = { web = true, jobs = false, api = true }
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  enabled_workloads = { for name, enabled in var.workloads : name => enabled if enabled }
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-08-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-08-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-08-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-08-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-08-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-08-sg" }
}

resource "aws_instance" "lab" {
  for_each = local.enabled_workloads
  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-08-${each.key}" }
}

output "instance_ids" {
  value = [for vm in values(aws_instance.lab) : vm.id]
}

output "public_ips" {
  value = [for vm in values(aws_instance.lab) : vm.public_ip]
}

output "selected_workloads" {
  value = keys(local.enabled_workloads)
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='workloads={web=true,jobs=true,api=false}'

Expected learning: Default creates two VMs, for web and api. jobs is excluded. Clean up with terraform destroy in this exact lab directory.

8.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/08-for-each/main.tf. Default creates two VMs, for web and api. jobs is excluded.

# Standalone Azure lab 08: for_each with filtering
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "workloads" {
  type = map(bool)
  default = { web = true, jobs = false, api = true }
}
locals {
  enabled_workloads = { for name, enabled in var.workloads : name => enabled if enabled }
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-08-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-08"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-08"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-08"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {
  for_each = local.enabled_workloads
  name                = "pip-cond-08-${each.key}"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {
  for_each = local.enabled_workloads
  name                = "nic-cond-08-${each.key}"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab[each.key].id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {
  for_each = local.enabled_workloads
  name                            = "vm-cond-08-${each.key}"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab[each.key].id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [for vm in values(azurerm_linux_virtual_machine.lab) : vm.id]
}

output "public_ips" {
  value = [for pip in values(azurerm_public_ip.lab) : pip.ip_address]
}

output "selected_workloads" {
  value = keys(local.enabled_workloads)
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='workloads={web=true,jobs=true,api=false}'

Expected learning: Default creates two VMs, for web and api. jobs is excluded. Clean up with terraform destroy in this exact lab directory.


9. for expressions with if

Category: Resource creation. Student goal: Filter names with a for expression before using the result.

Plain English: A for expression transforms a collection, and its trailing if filters out unwanted items. The lab creates one VM but publishes the filtered list in a tag and output, keeping the example focused on data selection.

9.1 AWS EC2 – complete standalone code

Files: aws/09-for-if/main.tf. Filters out batch01 and keeps web01, web02. Creates one demonstration VM; the filtered list is exposed in a tag and output.

# Standalone AWS lab 09: for expressions with if
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "workload_names" {
  type = list(string)
  default = ["web01", "batch01", "web02"]
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  web_names = [for name in var.workload_names : name if startswith(name, "web")]
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-09-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-09-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-09-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-09-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-09-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-09-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-web-filter", Selected = join(",", local.web_names) }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "selected_workload_names" {
  value = local.web_names
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='workload_names=["batch","web03"]'

Expected learning: Filters out batch01 and keeps web01, web02. Creates one demonstration VM; the filtered list is exposed in a tag and output. Clean up with terraform destroy in this exact lab directory.

9.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/09-for-if/main.tf. Filters out batch01 and keeps web01, web02. Creates one demonstration VM; the filtered list is exposed in a tag and output.

# Standalone Azure lab 09: for expressions with if
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "workload_names" {
  type = list(string)
  default = ["web01", "batch01", "web02"]
}
locals {
  web_names = [for name in var.workload_names : name if startswith(name, "web")]
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-09-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-09"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-09"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-09"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-09"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-09"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-09"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Selected = join(",", local.web_names) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "selected_workload_names" {
  value = local.web_names
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='workload_names=["batch","web03"]'

Expected learning: Filters out batch01 and keeps web01, web02. Creates one demonstration VM; the filtered list is exposed in a tag and output. Clean up with terraform destroy in this exact lab directory.


10. Conditional dynamic blocks

Category: Resource creation. Student goal: Include or omit an optional nested VM block.

Plain English: An optional nested block may exist once or not at all. dynamic loops over [1] when enabled and [] when disabled. AWS optionally attaches an additional EBS disk, while Azure optionally enables managed boot diagnostics.

10.1 AWS EC2 – complete standalone code

Files: aws/10-dynamic/main.tf. false omits the nested block; true generates one optional nested block. AWS adds an EBS data disk; Azure enables managed boot diagnostics.

# Standalone AWS lab 10: Conditional dynamic blocks
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "enable_optional_block" {
  type = bool
  default = false
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-10-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-10-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-10-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-10-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-10-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-10-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  dynamic "ebs_block_device" {
    for_each = var.enable_optional_block ? [1] : []
    content {
      device_name           = "/dev/sdf"
      volume_size           = 8
      volume_type           = "gp3"
      encrypted             = true
      delete_on_termination = true
    }
  }
  tags = { Name = "logic-10-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "optional_block_requested" {
  value = var.enable_optional_block
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='enable_optional_block=true'

Expected learning: false omits the nested block; true generates one optional nested block. AWS adds an EBS data disk; Azure enables managed boot diagnostics. Clean up with terraform destroy in this exact lab directory.

10.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/10-dynamic/main.tf. false omits the nested block; true generates one optional nested block. AWS adds an EBS data disk; Azure enables managed boot diagnostics.

# Standalone Azure lab 10: Conditional dynamic blocks
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "enable_optional_block" {
  type = bool
  default = false
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-10-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-10"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-10"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-10"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-10"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-10"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-10"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  dynamic "boot_diagnostics" {
    for_each = var.enable_optional_block ? [1] : []
    content {}
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "optional_block_requested" {
  value = var.enable_optional_block
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='enable_optional_block=true'

Expected learning: false omits the nested block; true generates one optional nested block. AWS adds an EBS data disk; Azure enables managed boot diagnostics. Clean up with terraform destroy in this exact lab directory.


11. Conditional module instantiation

Category: Resource creation. Student goal: Create zero or one VM via a local child module.

Plain English: A module call also supports count, so a whole child module can be present or absent. A module must have source code: this is the only topic requiring two .tf files, supplied completely in the Markdown and prepared in the ZIP.

11.1 AWS EC2 – complete standalone code

Files: aws/11-module/main.tf and modules/vm/main.tf. True creates one local child module and its VM; false creates zero module instances while leaving root networking.

# Standalone AWS lab 11: Conditional module instantiation
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "enable_module" {
  type = bool
  default = true
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-11-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-11-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-11-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-11-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-11-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-11-sg" }
}
module "vm" {
  source = "./modules/vm"
  count  = var.enable_module ? 1 : 0

  ami               = data.aws_ssm_parameter.al2023.value
  subnet_id         = aws_subnet.lab.id
  security_group_id = aws_security_group.lab.id
  name              = "logic-11-ec2"
}

output "instance_ids" {
  value = module.vm[*].id
}

output "public_ips" {
  value = module.vm[*].public_ip
}

output "module_enabled" {
  value = var.enable_module
}

Local child module: modules/vm/main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

variable "ami" {
  type = string
}
variable "subnet_id" {
  type = string
}
variable "security_group_id" {
  type = string
}
variable "name" {
  type = string
}

resource "aws_instance" "vm" {
  ami                         = var.ami
  instance_type               = "t3.micro"
  subnet_id                   = var.subnet_id
  vpc_security_group_ids      = [var.security_group_id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = var.name }
}

output "id" {
  value = aws_instance.vm.id
}
output "public_ip" {
  value = aws_instance.vm.public_ip
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='enable_module=false'

Expected learning: True creates one local child module and its VM; false creates zero module instances while leaving root networking. Clean up with terraform destroy in this exact lab directory.

11.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/11-module/main.tf and modules/vm/main.tf. True creates one local child module and its VM; false creates zero module instances while leaving root networking.

# Standalone Azure lab 11: Conditional module instantiation
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "enable_module" {
  type = bool
  default = true
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-11-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-11"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-11"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-11"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-11"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-11"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}

module "vm" {
  source = "./modules/vm"
  count  = var.enable_module ? 1 : 0

  name                = "vm-cond-11"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  nic_id              = azurerm_network_interface.lab.id
  vm_password         = var.vm_password
}

output "vm_ids" {
  value = module.vm[*].id
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "module_enabled" {
  value = var.enable_module
}

Local child module: modules/vm/main.tf

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

variable "name" {
  type = string
}
variable "location" {
  type = string
}
variable "resource_group_name" {
  type = string
}
variable "nic_id" {
  type = string
}
variable "vm_password" {
  type      = string
  sensitive = true
}

resource "azurerm_linux_virtual_machine" "vm" {
  name                            = var.name
  location                        = var.location
  resource_group_name             = var.resource_group_name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [var.nic_id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
}

output "id" {
  value = azurerm_linux_virtual_machine.vm.id
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='enable_module=false'

Expected learning: True creates one local child module and its VM; false creates zero module instances while leaving root networking. Clean up with terraform destroy in this exact lab directory.


12. Input variable validation

Category: Validation. Student goal: Reject unsupported environment names.

Plain English: validation belongs inside an input variable. Terraform refuses unsupported values before normal deployment, returning the error message you chose. This is best for rules about user-supplied input.

12.1 AWS EC2 – complete standalone code

Files: aws/12-validation/main.tf. Bad input is rejected during planning with the custom variable error.

# Standalone AWS lab 12: Input variable validation
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "environment" {
  type = string
  default = "dev"
  validation {
    condition     = contains(["dev", "stage", "prod"], var.environment)
    error_message = "environment must be dev, stage or prod."
  }
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-12-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-12-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-12-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-12-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-12-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-12-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-12-ec2", Environment = var.environment }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "environment" {
  value = var.environment
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=invalid'

Expected learning: Bad input is rejected during planning with the custom variable error. Clean up with terraform destroy in this exact lab directory.

12.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/12-validation/main.tf. Bad input is rejected during planning with the custom variable error.

# Standalone Azure lab 12: Input variable validation
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "environment" {
  type = string
  default = "dev"
  validation {
    condition     = contains(["dev", "stage", "prod"], var.environment)
    error_message = "environment must be dev, stage or prod."
  }
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-12-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-12"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-12"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-12"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-12"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-12"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-12"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Environment = var.environment }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "environment" {
  value = var.environment
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=invalid'

Expected learning: Bad input is rejected during planning with the custom variable error. Clean up with terraform destroy in this exact lab directory.


13. Resource precondition

Category: Validation. Student goal: Reject an unsupported VM size before the operation.

Plain English: lifecycle.precondition is a resource guard. If the requested VM size is outside a small approved list, Terraform raises a useful error instead of trying to provision the instance.

13.1 AWS EC2 – complete standalone code

Files: aws/13-precondition/main.tf. An invalid size fails the precondition before VM creation.

# Standalone AWS lab 13: Resource precondition
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "requested_size" {
  type = string
  default = "t3.micro"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-13-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-13-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-13-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-13-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-13-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-13-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = var.requested_size
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-13-ec2" }
  lifecycle {
    precondition {
      condition     = contains(["t3.micro", "t3.small"], var.requested_size)
      error_message = "requested_size is outside the approved training sizes."
    }
  }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='requested_size=invalid-size'

Expected learning: An invalid size fails the precondition before VM creation. Clean up with terraform destroy in this exact lab directory.

13.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/13-precondition/main.tf. An invalid size fails the precondition before VM creation.

# Standalone Azure lab 13: Resource precondition
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "requested_size" {
  type = string
  default = "Standard_B1s"
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-13-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-13"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-13"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-13"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-13"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-13"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-13"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = var.requested_size
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
  lifecycle {
    precondition {
      condition     = contains(["Standard_B1s", "Standard_B2s"], var.requested_size)
      error_message = "requested_size is outside the approved training sizes."
    }
  }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='requested_size=invalid-size'

Expected learning: An invalid size fails the precondition before VM creation. Clean up with terraform destroy in this exact lab directory.


14. Resource postcondition

Category: Validation. Student goal: Verify a resulting VM ID meets an expectation.

Plain English: lifecycle.postcondition checks a result after Terraform can read it. Here, the provider-created VM ID must start with the normal AWS or Azure prefix. A failure blocks successful completion and dependent operations.

14.1 AWS EC2 – complete standalone code

Files: aws/14-postcondition/main.tf. Default verifies the created EC2 or Azure resource ID. A wrong prefix fails after the ID is known (plan for existing VMs, otherwise apply).

# Standalone AWS lab 14: Resource postcondition
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "expected_id_prefix" {
  type = string
  default = "i-"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-14-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-14-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-14-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-14-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-14-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-14-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-14-ec2" }
  lifecycle {
    postcondition {
      condition     = startswith(lower(self.id), lower(var.expected_id_prefix))
      error_message = "The created VM ID does not start with expected_id_prefix."
    }
  }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "expected_id_prefix" {
  value = var.expected_id_prefix
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='expected_id_prefix=wrong-prefix'

Expected learning: Default verifies the created EC2 or Azure resource ID. A wrong prefix fails after the ID is known (plan for existing VMs, otherwise apply). Clean up with terraform destroy in this exact lab directory.

14.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/14-postcondition/main.tf. Default verifies the created EC2 or Azure resource ID. A wrong prefix fails after the ID is known (plan for existing VMs, otherwise apply).

# Standalone Azure lab 14: Resource postcondition
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "expected_id_prefix" {
  type = string
  default = "/subscriptions/"
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-14-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-14"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-14"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-14"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-14"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-14"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-14"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
  lifecycle {
    postcondition {
      condition     = startswith(lower(self.id), lower(var.expected_id_prefix))
      error_message = "The created VM ID does not start with expected_id_prefix."
    }
  }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "expected_id_prefix" {
  value = var.expected_id_prefix
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='expected_id_prefix=wrong-prefix'

Expected learning: Default verifies the created EC2 or Azure resource ID. A wrong prefix fails after the ID is known (plan for existing VMs, otherwise apply). Clean up with terraform destroy in this exact lab directory.


15. Output precondition

Category: Validation. Student goal: Check a condition before publishing an output.

Plain English: An output may have a precondition block. Instead of blindly reporting a resource ID, Terraform checks the ID format before publishing the output value. This is a hard assertion, not a warning.

15.1 AWS EC2 – complete standalone code

Files: aws/15-output-precondition/main.tf. The output precondition passes for the real resource ID, and rejects a wrong expected prefix.

# Standalone AWS lab 15: Output precondition
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "expected_id_prefix" {
  type = string
  default = "i-"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-15-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-15-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-15-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-15-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-15-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-15-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-15-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "validated_vm_id" {
  value = aws_instance.lab.id
  precondition {
    condition     = startswith(lower(aws_instance.lab.id), lower(var.expected_id_prefix))
    error_message = "VM resource ID must start with expected_id_prefix."
  }
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='expected_id_prefix=wrong-prefix'

Expected learning: The output precondition passes for the real resource ID, and rejects a wrong expected prefix. Clean up with terraform destroy in this exact lab directory.

15.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/15-output-precondition/main.tf. The output precondition passes for the real resource ID, and rejects a wrong expected prefix.

# Standalone Azure lab 15: Output precondition
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "expected_id_prefix" {
  type = string
  default = "/subscriptions/"
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-15-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-15"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-15"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-15"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-15"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-15"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-15"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "validated_vm_id" {
  value = azurerm_linux_virtual_machine.lab.id
  precondition {
    condition     = startswith(lower(azurerm_linux_virtual_machine.lab.id), lower(var.expected_id_prefix))
    error_message = "VM resource ID must start with expected_id_prefix."
  }
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='expected_id_prefix=wrong-prefix'

Expected learning: The output precondition passes for the real resource ID, and rejects a wrong expected prefix. Clean up with terraform destroy in this exact lab directory.


16. check block (warning)

Category: Validation. Student goal: Report a nonblocking configuration assertion.

Plain English: A check block evaluates an assertion but normally only reports a warning. It is good for ongoing diagnostics, not for mandatory security policy or preventing a resource from being built.

16.1 AWS EC2 – complete standalone code

Files: aws/16-check/main.tf. A failed check is reported as a warning; Terraform does not block the operation.

# Standalone AWS lab 16: check block (warning)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "expected_name_prefix" {
  type = string
  default = "logic"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-16-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-16-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-16-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-16-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-16-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-16-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-16-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "expected_name_prefix" {
  value = var.expected_name_prefix
}

check "vm_name_standard" {
  assert {
    condition     = startswith(aws_instance.lab.tags["Name"], var.expected_name_prefix)
    error_message = "VM name does not match the expected prefix (warning, not failure)."
  }
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='expected_name_prefix=other-name'

Expected learning: A failed check is reported as a warning; Terraform does not block the operation. Clean up with terraform destroy in this exact lab directory.

16.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/16-check/main.tf. A failed check is reported as a warning; Terraform does not block the operation.

# Standalone Azure lab 16: check block (warning)
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "expected_name_prefix" {
  type = string
  default = "vm-cond"
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-16-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-16"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-16"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-16"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-16"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-16"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-16"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "expected_name_prefix" {
  value = var.expected_name_prefix
}

check "vm_name_standard" {
  assert {
    condition     = startswith(azurerm_linux_virtual_machine.lab.name, var.expected_name_prefix)
    error_message = "VM name does not match the expected prefix (warning, not failure)."
  }
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='expected_name_prefix=other-name'

Expected learning: A failed check is reported as a warning; Terraform does not block the operation. Clean up with terraform destroy in this exact lab directory.


17. try()

Category: Functions. Student goal: Fall back when an optional map key is missing.

Plain English: try(first, fallback) picks the first expression that can evaluate successfully. A missing optional map key triggers the fallback size. It will not repair syntax mistakes or references to undeclared Terraform objects.

17.1 AWS EC2 – complete standalone code

Files: aws/17-try/main.tf. With a missing key, try() returns the small-size fallback.

# Standalone AWS lab 17: try()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "settings" {
  type = map(string)
  default = {}
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  selected_size = try(var.settings["instance_type"], "t3.micro")
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-17-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-17-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-17-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-17-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-17-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-17-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = local.selected_size
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-17-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "chosen_size" {
  value = local.selected_size
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='settings={instance_type="t3.small"}'

Expected learning: With a missing key, try() returns the small-size fallback. Clean up with terraform destroy in this exact lab directory.

17.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/17-try/main.tf. With a missing key, try() returns the small-size fallback.

# Standalone Azure lab 17: try()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "settings" {
  type = map(string)
  default = {}
}
locals {
  selected_size = try(var.settings["vm_size"], "Standard_B1s")
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-17-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-17"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-17"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-17"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-17"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-17"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-17"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = local.selected_size
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "chosen_size" {
  value = local.selected_size
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='settings={vm_size="Standard_B2s"}'

Expected learning: With a missing key, try() returns the small-size fallback. Clean up with terraform destroy in this exact lab directory.


18. can()

Category: Functions. Student goal: Turn a regex evaluation error into variable validation.

Plain English: regex fails when a pattern does not match. can(regex(...)) converts that evaluation failure into false, making regex-based variable validation simple and safe.

18.1 AWS EC2 – complete standalone code

Files: aws/18-can/main.tf. can(regex(…)) returns false for a mismatch, so input validation fails gracefully.

# Standalone AWS lab 18: can()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "deployment_code" {
  type = string
  default = "dev01"
  validation {
    condition     = can(regex("^(dev|test|prod)[0-9]+$", var.deployment_code))
    error_message = "deployment_code must look like dev01, test02, prod03."
  }
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-18-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-18-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-18-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-18-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-18-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-18-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-18-ec2", Code = var.deployment_code }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "validated_code" {
  value = var.deployment_code
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='deployment_code=invalid'

Expected learning: can(regex(…)) returns false for a mismatch, so input validation fails gracefully. Clean up with terraform destroy in this exact lab directory.

18.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/18-can/main.tf. can(regex(…)) returns false for a mismatch, so input validation fails gracefully.

# Standalone Azure lab 18: can()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "deployment_code" {
  type = string
  default = "dev01"
  validation {
    condition     = can(regex("^(dev|test|prod)[0-9]+$", var.deployment_code))
    error_message = "deployment_code must look like dev01, test02, prod03."
  }
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-18-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-18"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-18"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-18"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-18"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-18"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-18"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Code = var.deployment_code }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "validated_code" {
  value = var.deployment_code
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='deployment_code=invalid'

Expected learning: can(regex(…)) returns false for a mismatch, so input validation fails gracefully. Clean up with terraform destroy in this exact lab directory.


19. lookup()

Category: Functions. Student goal: Get a VM size from an environment map with a fallback.

Plain English: lookup(map, key, fallback) reads a map item without failing when the key is absent. An unknown environment therefore gets a sensible small VM size.

19.1 AWS EC2 – complete standalone code

Files: aws/19-lookup/main.tf. Known keys select a size; unknown chooses the provided fallback.

# Standalone AWS lab 19: lookup()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "environment" {
  type = string
  default = "dev"
}

variable "size_by_env" {
  type = map(string)
  default = { dev = "t3.micro", prod = "t3.small" }
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  selected_size = lookup(var.size_by_env, var.environment, "t3.micro")
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-19-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-19-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-19-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-19-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-19-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-19-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = local.selected_size
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-19-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "selected_size" {
  value = local.selected_size
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=unknown'

Expected learning: Known keys select a size; unknown chooses the provided fallback. Clean up with terraform destroy in this exact lab directory.

19.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/19-lookup/main.tf. Known keys select a size; unknown chooses the provided fallback.

# Standalone Azure lab 19: lookup()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "environment" {
  type = string
  default = "dev"
}

variable "size_by_env" {
  type = map(string)
  default = { dev = "Standard_B1s", prod = "Standard_B2s" }
}
locals {
  selected_size = lookup(var.size_by_env, var.environment, "Standard_B1s")
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-19-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-19"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-19"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-19"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-19"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-19"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-19"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = local.selected_size
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Purpose = "conditional-logic" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "selected_size" {
  value = local.selected_size
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=unknown'

Expected learning: Known keys select a size; unknown chooses the provided fallback. Clean up with terraform destroy in this exact lab directory.


20. coalesce()

Category: Functions. Student goal: Use the first non-null, nonempty display name.

Plain English: coalesce() chooses the first value that is neither null nor the empty string. Use it to provide a user-friendly label if the optional display name is missing.

20.1 AWS EC2 – complete standalone code

Files: aws/20-coalesce/main.tf. null or empty string uses lab-default; a supplied string wins.

# Standalone AWS lab 20: coalesce()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "display_name" {
  type = string
  default = null
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  display_name = coalesce(var.display_name, "lab-default")
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-20-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-20-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-20-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-20-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-20-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-20-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = local.display_name }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "display_name" {
  value = local.display_name
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='display_name=student-lab'

Expected learning: null or empty string uses lab-default; a supplied string wins. Clean up with terraform destroy in this exact lab directory.

20.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/20-coalesce/main.tf. null or empty string uses lab-default; a supplied string wins.

# Standalone Azure lab 20: coalesce()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "display_name" {
  type = string
  default = null
}
locals {
  display_name = coalesce(var.display_name, "lab-default")
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-20-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-20"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-20"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-20"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-20"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-20"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-20"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { DisplayName = local.display_name }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "display_name" {
  value = local.display_name
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='display_name=student-lab'

Expected learning: null or empty string uses lab-default; a supplied string wins. Clean up with terraform destroy in this exact lab directory.


21. coalescelist()

Category: Functions. Student goal: Choose the first nonempty list of owners.

Plain English: coalescelist() takes the first nonempty list. An empty owner list falls back to a default team, but a real list of owners is preserved.

21.1 AWS EC2 – complete standalone code

Files: aws/21-coalescelist/main.tf. Empty list uses platform-team; a nonempty list is retained.

# Standalone AWS lab 21: coalescelist()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "preferred_owners" {
  type = list(string)
  default = []
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  selected_owners = coalescelist(var.preferred_owners, ["platform-team"])
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-21-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-21-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-21-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-21-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-21-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-21-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-21-ec2", Owners = join(",", local.selected_owners) }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "selected_owners" {
  value = local.selected_owners
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='preferred_owners=["devops","security"]'

Expected learning: Empty list uses platform-team; a nonempty list is retained. Clean up with terraform destroy in this exact lab directory.

21.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/21-coalescelist/main.tf. Empty list uses platform-team; a nonempty list is retained.

# Standalone Azure lab 21: coalescelist()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "preferred_owners" {
  type = list(string)
  default = []
}
locals {
  selected_owners = coalescelist(var.preferred_owners, ["platform-team"])
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-21-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-21"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-21"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-21"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-21"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-21"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-21"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Owners = join(",", local.selected_owners) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "selected_owners" {
  value = local.selected_owners
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='preferred_owners=["devops","security"]'

Expected learning: Empty list uses platform-team; a nonempty list is retained. Clean up with terraform destroy in this exact lab directory.


22. contains()

Category: Functions. Student goal: Test membership in a list of environments.

Plain English: contains(collection, value) returns a Boolean membership result. It is clearer than a long chain such as env == "prod" || env == "stage" when the options form a list.

22.1 AWS EC2 – complete standalone code

Files: aws/22-contains/main.tf. prod and stage return true; dev returns false. AWS also toggles detailed monitoring.

# Standalone AWS lab 22: contains()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "environment" {
  type = string
  default = "prod"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  is_restricted_env = contains(["prod", "stage"], var.environment)
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-22-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-22-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-22-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-22-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-22-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-22-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  monitoring = local.is_restricted_env
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-22-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "restricted_environment" {
  value = local.is_restricted_env
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=dev'

Expected learning: prod and stage return true; dev returns false. AWS also toggles detailed monitoring. Clean up with terraform destroy in this exact lab directory.

22.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/22-contains/main.tf. prod and stage return true; dev returns false. AWS also toggles detailed monitoring.

# Standalone Azure lab 22: contains()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "environment" {
  type = string
  default = "prod"
}
locals {
  is_restricted_env = contains(["prod", "stage"], var.environment)
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-22-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-22"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-22"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-22"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-22"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-22"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-22"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { RestrictedEnv = tostring(local.is_restricted_env) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "restricted_environment" {
  value = local.is_restricted_env
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=dev'

Expected learning: prod and stage return true; dev returns false. AWS also toggles detailed monitoring. Clean up with terraform destroy in this exact lab directory.


23. alltrue()

Category: Functions. Student goal: Check whether every port number is in range.

Plain English: alltrue() requires every item to be true. A for expression converts numeric ports to true/false checks before alltrue combines them. Remember: on an empty list, alltrue([]) is true.

23.1 AWS EC2 – complete standalone code

Files: aws/23-alltrue/main.tf. 22/443 -> true; adding 70000 -> false. alltrue([]) is true (vacuous truth).

# Standalone AWS lab 23: alltrue()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "ports" {
  type = list(number)
  default = [22, 443]
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  all_ports_valid = alltrue([for p in var.ports : p >= 1 && p <= 65535])
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-23-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-23-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-23-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-23-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-23-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-23-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-23-ec2", PortsValid = tostring(local.all_ports_valid) }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "all_ports_valid" {
  value = local.all_ports_valid
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='ports=[22,70000]'

Expected learning: 22/443 -> true; adding 70000 -> false. alltrue([]) is true (vacuous truth). Clean up with terraform destroy in this exact lab directory.

23.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/23-alltrue/main.tf. 22/443 -> true; adding 70000 -> false. alltrue([]) is true (vacuous truth).

# Standalone Azure lab 23: alltrue()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "ports" {
  type = list(number)
  default = [22, 443]
}
locals {
  all_ports_valid = alltrue([for p in var.ports : p >= 1 && p <= 65535])
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-23-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-23"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-23"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-23"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-23"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-23"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-23"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { PortsValid = tostring(local.all_ports_valid) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "all_ports_valid" {
  value = local.all_ports_valid
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='ports=[22,70000]'

Expected learning: 22/443 -> true; adding 70000 -> false. alltrue([]) is true (vacuous truth). Clean up with terraform destroy in this exact lab directory.


24. anytrue()

Category: Functions. Student goal: Check whether at least one feature is active.

Plain English: anytrue() returns true when at least one item is true. It turns several feature switches into one concise decision. An empty list or all-false flags return false.

24.1 AWS EC2 – complete standalone code

Files: aws/24-anytrue/main.tf. true when any supplied flag is true; all false -> false. AWS toggles EC2 detailed monitoring, not backups/tracing.

# Standalone AWS lab 24: anytrue()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "metrics_enabled" {
  type = bool
  default = false
}

variable "backups_enabled" {
  type = bool
  default = true
}

variable "tracing_enabled" {
  type = bool
  default = false
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  any_feature_enabled = anytrue([var.metrics_enabled, var.backups_enabled, var.tracing_enabled])
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-24-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-24-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-24-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-24-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-24-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-24-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  monitoring = local.any_feature_enabled
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-24-ec2" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "any_feature_enabled" {
  value = local.any_feature_enabled
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='backups_enabled=false'

Expected learning: true when any supplied flag is true; all false -> false. AWS toggles EC2 detailed monitoring, not backups/tracing. Clean up with terraform destroy in this exact lab directory.

24.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/24-anytrue/main.tf. true when any supplied flag is true; all false -> false. AWS toggles EC2 detailed monitoring, not backups/tracing.

# Standalone Azure lab 24: anytrue()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "metrics_enabled" {
  type = bool
  default = false
}

variable "backups_enabled" {
  type = bool
  default = true
}

variable "tracing_enabled" {
  type = bool
  default = false
}
locals {
  any_feature_enabled = anytrue([var.metrics_enabled, var.backups_enabled, var.tracing_enabled])
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-24-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-24"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-24"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-24"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-24"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-24"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-24"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { AnyFeature = tostring(local.any_feature_enabled) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "any_feature_enabled" {
  value = local.any_feature_enabled
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='backups_enabled=false'

Expected learning: true when any supplied flag is true; all false -> false. AWS toggles EC2 detailed monitoring, not backups/tracing. Clean up with terraform destroy in this exact lab directory.


25. length()

Category: Functions. Student goal: Choose a label based on the size of a list.

Plain English: length() counts items in a list/map or characters in a string. Checking length(var.owners) > 0 determines whether an owner has been assigned.

25.1 AWS EC2 – complete standalone code

Files: aws/25-length/main.tf. Returns 2 by default and 0 with [] and selects the corresponding tag.

# Standalone AWS lab 25: length()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "owners" {
  type = list(string)
  default = ["alice", "bob"]
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  has_owners = length(var.owners) > 0
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-25-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-25-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-25-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-25-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-25-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-25-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-25-ec2", OwnerStatus = local.has_owners ? "assigned" : "unassigned" }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "owner_count" {
  value = length(var.owners)
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='owners=[]'

Expected learning: Returns 2 by default and 0 with [] and selects the corresponding tag. Clean up with terraform destroy in this exact lab directory.

25.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/25-length/main.tf. Returns 2 by default and 0 with [] and selects the corresponding tag.

# Standalone Azure lab 25: length()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "owners" {
  type = list(string)
  default = ["alice", "bob"]
}
locals {
  has_owners = length(var.owners) > 0
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-25-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-25"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-25"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-25"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-25"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-25"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-25"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { OwnerStatus = local.has_owners ? "assigned" : "unassigned" }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "owner_count" {
  value = length(var.owners)
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='owners=[]'

Expected learning: Returns 2 by default and 0 with [] and selects the corresponding tag. Clean up with terraform destroy in this exact lab directory.


26. regex() and regexall()

Category: Functions. Student goal: Extract letters and detect digits in a label.

Plain English: regex() returns the first match or an error; regexall() collects every match and safely returns an empty list if there is no match. The example extracts letters and checks whether the label contains any digits.

26.1 AWS EC2 – complete standalone code

Files: aws/26-regex/main.tf. web01 -> letters=web, has_digits=true. regex throws on no match; try() handles this fallback.

# Standalone AWS lab 26: regex() and regexall()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "label" {
  type = string
  default = "web01"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  letters = try(regex("[a-z]+", lower(var.label)), "none")
    has_digits = length(regexall("[0-9]+", var.label)) > 0
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-26-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-26-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-26-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-26-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-26-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-26-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-26-ec2", Letters = local.letters, Digits = tostring(local.has_digits) }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "regex_results" {
  value = { letters = local.letters, has_digits = local.has_digits }
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='label=backend'

Expected learning: web01 -> letters=web, has_digits=true. regex throws on no match; try() handles this fallback. Clean up with terraform destroy in this exact lab directory.

26.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/26-regex/main.tf. web01 -> letters=web, has_digits=true. regex throws on no match; try() handles this fallback.

# Standalone Azure lab 26: regex() and regexall()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "label" {
  type = string
  default = "web01"
}
locals {
  letters = try(regex("[a-z]+", lower(var.label)), "none")
    has_digits = length(regexall("[0-9]+", var.label)) > 0
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-26-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-26"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-26"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-26"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-26"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-26"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-26"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Letters = local.letters, Digits = tostring(local.has_digits) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "regex_results" {
  value = { letters = local.letters, has_digits = local.has_digits }
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='label=backend'

Expected learning: web01 -> letters=web, has_digits=true. regex throws on no match; try() handles this fallback. Clean up with terraform destroy in this exact lab directory.


27. startswith() and endswith()

Category: Functions. Student goal: Check standardized workload naming patterns.

Plain English: startswith() tests the beginning of a string and endswith() tests the ending. Combining them lets teams consistently label workloads such as prod-payments-api.

27.1 AWS EC2 – complete standalone code

Files: aws/27-startswith-endswith/main.tf. True for prod-payments-api; false for prefixes or suffixes that do not match.

# Standalone AWS lab 27: startswith() and endswith()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "workload_name" {
  type = string
  default = "prod-payments-api"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  standard_name = startswith(var.workload_name, "prod-") && endswith(var.workload_name, "-api")
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-27-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-27-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-27-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-27-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-27-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-27-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-27-ec2", NamingValid = tostring(local.standard_name) }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "standard_name" {
  value = local.standard_name
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='workload_name=dev-payments-job'

Expected learning: True for prod-payments-api; false for prefixes or suffixes that do not match. Clean up with terraform destroy in this exact lab directory.

27.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/27-startswith-endswith/main.tf. True for prod-payments-api; false for prefixes or suffixes that do not match.

# Standalone Azure lab 27: startswith() and endswith()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "workload_name" {
  type = string
  default = "prod-payments-api"
}
locals {
  standard_name = startswith(var.workload_name, "prod-") && endswith(var.workload_name, "-api")
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-27-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-27"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-27"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-27"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-27"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-27"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-27"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { NamingValid = tostring(local.standard_name) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "standard_name" {
  value = local.standard_name
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='workload_name=dev-payments-job'

Expected learning: True for prod-payments-api; false for prefixes or suffixes that do not match. Clean up with terraform destroy in this exact lab directory.


28. null values

Category: Null and optional. Student goal: Omit optional VM tags when no owner is provided.

Plain English: null means not supplied for an optional argument. In this example, tags = null omits VM tags; setting an owner supplies a tag map. It is different from tags = {} or an empty string.

28.1 AWS EC2 – complete standalone code

Files: aws/28-null/main.tf. No owner -> tags argument omitted with null; setting owner creates the tag.

# Standalone AWS lab 28: null values
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "owner" {
  type = string
  default = null
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-28-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-28-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-28-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-28-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-28-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-28-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = var.owner == null ? null : { Owner = var.owner }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "owner_value" {
  value = var.owner
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='owner=platform'

Expected learning: No owner -> tags argument omitted with null; setting owner creates the tag. Clean up with terraform destroy in this exact lab directory.

28.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/28-null/main.tf. No owner -> tags argument omitted with null; setting owner creates the tag.

# Standalone Azure lab 28: null values
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "owner" {
  type = string
  default = null
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-28-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-28"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-28"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-28"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-28"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-28"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-28"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = var.owner == null ? null : { Owner = var.owner }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "owner_value" {
  value = var.owner
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='owner=platform'

Expected learning: No owner -> tags argument omitted with null; setting owner creates the tag. Clean up with terraform destroy in this exact lab directory.


29. Optional object attributes

Category: Null and optional. Student goal: Supply defaults for optional settings of a typed object.

Plain English: Optional attributes in an object(...) type automatically receive declared defaults. Callers can pass {} or override only tier, leaving owner to default to student.

29.1 AWS EC2 – complete standalone code

Files: aws/29-optional/main.tf. With {}, defaults are applied: tier=small and owner=student. Override only tier.

# Standalone AWS lab 29: Optional object attributes
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "vm_options" {
  type = object({ tier = optional(string, "small"), owner = optional(string, "student") })
  default = {}
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-29-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-29-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-29-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-29-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-29-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-29-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = var.vm_options.tier == "large" ? "t3.small" : "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-29-ec2", Owner = var.vm_options.owner }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "effective_options" {
  value = var.vm_options
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='vm_options={tier="large"}'

Expected learning: With {}, defaults are applied: tier=small and owner=student. Override only tier. Clean up with terraform destroy in this exact lab directory.

29.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/29-optional/main.tf. With {}, defaults are applied: tier=small and owner=student. Override only tier.

# Standalone Azure lab 29: Optional object attributes
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "vm_options" {
  type = object({ tier = optional(string, "small"), owner = optional(string, "student") })
  default = {}
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-29-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-29"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-29"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-29"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-29"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-29"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-29"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = var.vm_options.tier == "large" ? "Standard_B2s" : "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Owner = var.vm_options.owner }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "effective_options" {
  value = var.vm_options
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='vm_options={tier="large"}'

Expected learning: With {}, defaults are applied: tier=small and owner=student. Override only tier. Clean up with terraform destroy in this exact lab directory.


30. nullable variable setting

Category: Null and optional. Student goal: Require a non-null owner variable.

Plain English: nullable = false stops a variable from accepting the null value. This variable has no default, so students must supply a non-null owner using TF_VAR_owner before planning.

30.1 AWS EC2 – complete standalone code

Files: aws/30-nullable/main.tf. No default: owner must be provided, and null is disallowed. Supply TF_VAR_owner=student.

# Standalone AWS lab 30: nullable variable setting
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "owner" {
  type = string
  nullable = false
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}


resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-30-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-30-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-30-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-30-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-30-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-30-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-30-ec2", Owner = var.owner }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "required_owner" {
  value = var.owner
}

Run and observe:

export TF_VAR_owner="student"
terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

export TF_VAR_owner="student" # required for this one lab
terraform plan

Expected learning: No default: owner must be provided, and null is disallowed. Supply TF_VAR_owner=student. Clean up with terraform destroy in this exact lab directory.

30.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/30-nullable/main.tf. No default: owner must be provided, and null is disallowed. Supply TF_VAR_owner=student.

# Standalone Azure lab 30: nullable variable setting
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "owner" {
  type = string
  nullable = false
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-30-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-30"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-30"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-30"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-30"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-30"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-30"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Owner = var.owner }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "required_owner" {
  value = var.owner
}

Run and observe:

export TF_VAR_owner="student"
terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

export TF_VAR_owner="student" # required for this one lab
terraform plan

Expected learning: No default: owner must be provided, and null is disallowed. Supply TF_VAR_owner=student. Clean up with terraform destroy in this exact lab directory.


31. merge() and conditional maps

Category: Null and optional. Student goal: Add environment-specific tags only for production.

Plain English: merge() combines maps. The conditional map contributes Compliance = "strict" only for production; tomap() makes both conditional branches compatible maps. The common tags remain for all environments.

31.1 AWS EC2 – complete standalone code

Files: aws/31-merge/main.tf. dev gets common tags only; prod also gets Compliance=strict.

# Standalone AWS lab 31: merge() and conditional maps
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "environment" {
  type = string
  default = "dev"
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  common_tags = { Course = "terraform-conditions", Owner = "student" }
    extra_tags  = var.environment == "prod" ? tomap({ Compliance = "strict" }) : tomap({})
    final_tags  = merge(local.common_tags, local.extra_tags)
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-31-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-31-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-31-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-31-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-31-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-31-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = merge(local.final_tags, { Name = "logic-31-ec2" })
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "final_tags" {
  value = local.final_tags
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=prod'

Expected learning: dev gets common tags only; prod also gets Compliance=strict. Clean up with terraform destroy in this exact lab directory.

31.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/31-merge/main.tf. dev gets common tags only; prod also gets Compliance=strict.

# Standalone Azure lab 31: merge() and conditional maps
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "environment" {
  type = string
  default = "dev"
}
locals {
  common_tags = { Course = "terraform-conditions", Owner = "student" }
    extra_tags  = var.environment == "prod" ? tomap({ Compliance = "strict" }) : tomap({})
    final_tags  = merge(local.common_tags, local.extra_tags)
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-31-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-31"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-31"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-31"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-31"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-31"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-31"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = local.final_tags
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "final_tags" {
  value = local.final_tags
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='environment=prod'

Expected learning: dev gets common tags only; prod also gets Compliance=strict. Clean up with terraform destroy in this exact lab directory.


32. compact() and flatten()

Category: Null and optional. Student goal: Flatten nested owner lists and remove blank entries.

Plain English: flatten() turns a list of lists into a single list; compact() removes null or empty strings from that list. The resulting names can be joined safely for a tag or inspected as an output.

32.1 AWS EC2 – complete standalone code

Files: aws/32-compact-flatten/main.tf. Nested lists become one flat list, and blank strings are removed.

# Standalone AWS lab 32: compact() and flatten()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "nested_owners" {
  type = list(list(string))
  default = [["platform", ""], ["security", "operations"]]
}
# Official Amazon Linux 2023 public AMI parameter (region-specific AMI ID).
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

locals {
  owners = compact(flatten(var.nested_owners))
}

resource "aws_vpc" "lab" {
  cidr_block           = "10.70.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "cond-32-vpc" }
}

resource "aws_subnet" "lab" {
  vpc_id                  = aws_vpc.lab.id
  cidr_block              = "10.70.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "cond-32-subnet" }
}

resource "aws_internet_gateway" "lab" {
  vpc_id = aws_vpc.lab.id
  tags   = { Name = "cond-32-igw" }
}

resource "aws_route_table" "lab" {
  vpc_id = aws_vpc.lab.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.lab.id
  }
  tags = { Name = "cond-32-rt" }
}

resource "aws_route_table_association" "lab" {
  subnet_id      = aws_subnet.lab.id
  route_table_id = aws_route_table.lab.id
}

resource "aws_security_group" "lab" {
  name_prefix = "cond-32-"
  description = "No inbound traffic; outbound for package access"
  vpc_id      = aws_vpc.lab.id

  # No inbound rules: this learning VM is NOT reachable from the internet.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "cond-32-sg" }
}

resource "aws_instance" "lab" {

  ami                         = data.aws_ssm_parameter.al2023.value
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.lab.id
  vpc_security_group_ids      = [aws_security_group.lab.id]
  associate_public_ip_address = true
  metadata_options {
    http_tokens = "required"
  }
  tags = { Name = "logic-32-ec2", Owners = join(",", local.owners) }
}

output "instance_ids" {
  value = [aws_instance.lab.id]
}

output "public_ips" {
  value = [aws_instance.lab.public_ip]
}

output "clean_owners" {
  value = local.owners
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='nested_owners=[["devops",""],["qa"]]' 

Expected learning: Nested lists become one flat list, and blank strings are removed. Clean up with terraform destroy in this exact lab directory.

32.2 Azure Ubuntu Linux VM – complete standalone code

Files: azure/32-compact-flatten/main.tf. Nested lists become one flat list, and blank strings are removed.

# Standalone Azure lab 32: compact() and flatten()
terraform {
  required_version = ">= 1.15.0, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 5.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "vm_password" {
  type        = string
  sensitive   = true
  description = "Unique strong lab password (supplied by TF_VAR_vm_password)."
  validation {
    condition     = length(var.vm_password) >= 12
    error_message = "Use a VM password of at least 12 characters."
  }
}

variable "admin_cidr" {
  type        = string
  description = "Your own public IPv4 address with /32 (supplied by TF_VAR_admin_cidr)."
  validation {
    condition     = can(cidrhost(var.admin_cidr, 0)) && endswith(var.admin_cidr, "/32")
    error_message = "Enter your public IPv4 as a /32 CIDR, such as YOUR.PUBLIC.IP.ADDRESS/32."
  }
}

variable "nested_owners" {
  type = list(list(string))
  default = [["platform", ""], ["security", "operations"]]
}
locals {
  owners = compact(flatten(var.nested_owners))
}

resource "azurerm_resource_group" "lab" {
  name     = "rg-cond-32-eastus"
  location = "eastus"
}

resource "azurerm_virtual_network" "lab" {
  name                = "vnet-cond-32"
  address_space       = ["10.70.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "snet-cond-32"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.70.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "nsg-cond-32"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "ssh" {
  name                        = "AllowSSHFromStudent"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = var.admin_cidr
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

resource "azurerm_subnet_network_security_group_association" "lab" {
  subnet_id                 = azurerm_subnet.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_public_ip" "lab" {

  name                = "pip-cond-32"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "lab" {

  name                = "nic-cond-32"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.lab.id
  }
}


resource "azurerm_linux_virtual_machine" "lab" {

  name                            = "vm-cond-32"
  location                        = azurerm_resource_group.lab.location
  resource_group_name             = azurerm_resource_group.lab.name
  size                            = "Standard_B1s"
  admin_username                  = "labadmin"
  admin_password                  = var.vm_password
  disable_password_authentication = false
  network_interface_ids           = [azurerm_network_interface.lab.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "Canonical"
    offer     = "ubuntu-24_04-lts"
    sku       = "server"
    version   = "latest"
  }
  tags = { Owners = join(",", local.owners) }
}

output "vm_ids" {
  value = [azurerm_linux_virtual_machine.lab.id]
}

output "public_ips" {
  value = [azurerm_public_ip.lab.ip_address]
}

output "clean_owners" {
  value = local.owners
}

Run and observe:

terraform init
terraform validate
terraform plan
terraform apply
terraform output

Experiment (run after init; preview only):

terraform plan -var='nested_owners=[["devops",""],["qa"]]' 

Expected learning: Nested lists become one flat list, and blank strings are removed. Clean up with terraform destroy in this exact lab directory.


E. Validation differences (important for students)

MechanismTimingFailure behaviorTypical use
variable validationAs Terraform evaluates inputsError / stopReject invalid regions, names, formats
Resource lifecycle.preconditionBefore resource operation, once inputs are knownError / stopGuard assumptions before provisioning
Resource lifecycle.postconditionAfter resource/data read or change when values are knownError / stopAssert properties of the resulting object
Output preconditionBefore a root output is recorded/publishedError / stopEnsure output can be trusted
check / assertChecked during plan/apply and rechecked as appropriateWarning, normally continuesNonblocking quality/health indicator

F. Common mistakes and troubleshooting

  1. Copying several labs into one directory: all use similar Terraform local names; duplicate declarations will fail. Copy exactly one scenario’s main.tf into a fresh directory, or use the ZIP folders directly.
  2. Setting count = 0 and still referencing [0]: use conditional indexing or try(); the templates demonstrate splats ([*].id) that return an empty list safely.
  3. Using for_each with an unknown key set: its keys must be known before apply. Filter a known map/list of inputs rather than discovered remote IDs.
  4. Expecting a Terraform if { } else { } statement: Terraform uses conditional expressions and collection filtering, not a top-level imperative if statement.
  5. Confusing a dynamic block with a resource: dynamic can only generate repeatable nested blocks supported by the provider schema.
  6. Assuming try catches static errors: it cannot repair typos such as undeclared local or undeclared resource references.
  7. Confusing can with try: can() yields Boolean success/failure, especially useful for validation; try() returns a fallback value.
  8. Assuming null and empty values are identical: null means absent/omitted in an optional argument; [], {}, and "" mean empty, which may behave differently.
  9. Assuming an empty list makes alltrue false: alltrue([]) is true; anytrue([]) is false.
  10. Assuming a failed check blocks deployment: check assertions are advisory warnings, not policy enforcement.
  11. Using count or for_each to refactor an existing applied resource without state moves: that changes state addresses, and may lead to recreation. These labs deliberately use distinct directories and states.
  12. Azure subscription missing: AzureRM 5.x needs a subscription ID in the provider configuration or environment (ARM_SUBSCRIPTION_ID), and resource providers must be registered.
  13. Azure invalid credentials: Azure password must satisfy the subscription’s password rules; the Terraform variable only tests minimum length.
  14. EC2 instance cannot be reached by SSH: intentional. AWS security groups have no inbound SSH and no key pair; inspect instance IDs/metadata via Terraform outputs and the AWS Console.
  15. Unexpected -/+ replacements: changing a VM name, image, subnet, or another immutable argument can force recreation; inspect the plan before applying.

G. Quick verification and cleanup

terraform fmt -recursive
terraform fmt -check -recursive
terraform validate
terraform plan
terraform apply
terraform output -json
terraform state list
terraform plan -destroy
terraform destroy

For an AWS VM ID from a single-instance lab:

terraform output -json instance_ids
aws ec2 describe-instances --region us-east-1 --filters \
  "Name=tag:Name,Values=logic-*" \
  --query 'Reservations[].Instances[].[InstanceId,State.Name]' --output table

For the Azure scenario number 01:

az group show -n rg-cond-01-eastus -o table
az vm list -g rg-cond-01-eastus -o table
terraform output -json vm_ids

Do not run terraform destroy in an unrelated directory. Do not share state or terraform.tfvars containing real secrets publicly.

H. Official reference sources

  1. HashiCorp: Conditional expressions
  2. HashiCorp: For expressions and filtering
  3. HashiCorp: Dynamic blocks
  4. HashiCorp: Validation, preconditions, postconditions and checks
  5. HashiCorp: Check block
  6. HashiCorp: Functions reference
  7. HashiCorp: Optional object attributes
  8. HashiCorp: Null values
  9. HashiCorp: Input variables and nullable
  10. AWS: Amazon Linux SSM AMI parameters
  11. Terraform Registry: AWS instance
  12. Azure: official Ubuntu 24.04 image alias
  13. Terraform Registry: Azure Linux VM

Documentation verification note

The examples have been generated from common complete cloud-network/VM templates and cross-checked for structural consistency and current official expression semantics. They have not been executed against live AWS or Azure accounts from this environment. A real terraform init, validate, plan, and subscription-specific image/SKU check remain necessary before claiming a particular deployment works in a particular account. Costs, quotas, credentials, and cloud policy can affect results.

Related Posts

Terraform tflint terraform-docs trivy tenv pre-commit Tools — Installation Guide for Windows PowerShell

Updated: October 2026 | Windows 10/11 | PowerShell 5.1 / 7 This guide covers installing, verifying, and running the first commands for five Terraform development tools. Tool…

Read More

HCP Terraform Cloud CLI — Master Command Summary Table

Organization: motoshare | Project: proezi | Workspaces: proezi_dev, proezi_stage, proezi_prod 1. Terraform CLI — Complete Command Reference Category Command Purpose / Usage Installation terraform version Check installed…

Read More

HCP Terraform Cloud CLI — Complete Master Tutorial

HCP Terraform Cloud — CLI Command Summary Organization: motoshare | Project: proezi # Cloud operation Command / Example Usage 1 Login to Terraform Cloud terraform login Authenticate…

Read More

Terraform Lab – Azure Hands-On – Terraform Functions

Beginner to Intermediate · 8 modules · 22 progressively designed labs · 20 local-only labs + 2 optional cloud labs · Updated 14 October 2026 Purpose: learn Terraform…

Read More

Terraform Lab – Azure Hands-On – Terraform Template

Level: Beginner to IntermediateFormat: Tutorial + concept guide + practical lab workbookModules: 6 | Labs: 12 | Cloud emphasis: Azure Linux VM, AWS EC2 comparisonBaseline: Terraform 1.15.9+ (within 1.x); AzureRM 5.9.x; AWS provider 6.xUpdated: 14 October…

Read More

Terraform Lab – Azure Hands-On – Terraform Modules

Course level: Beginner → IntermediateCourse type: Tutorial + runnable hands-on exercises + instructor checkpointsUpdated: 14 October 2026Terraform: >= 1.15.0, < 2.0.0 (lab baseline: 1.15.9)AzureRM provider: ~> 5.9.0 (root; child modules declare >= 5.9.0)Azure baseline: eastus, Ubuntu…

Read More