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.tffor 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:
- Select values with ternary expressions and combine logical/comparison operators.
- Create zero/one resources using
countand filter selected resources withfor_each. - Conditionally create repeatable nested configuration via
dynamic. - Distinguish variable validation, resource preconditions/postconditions, output preconditions, and nonblocking checks.
- Use
try,can,lookup,coalesce,coalescelist,contains,alltrue,anytrue,length, regex,startswith, andendswithsafely. - Work with
null, optional attributes,nullable, conditional maps,merge,compact, andflatten.
B. Correct mental model
condition ? true_value : false_valueselects one value. Terraform does not have a general-purposeif { } else { }statement for resource declarations.countrepeats a resource using numeric indexes ([0],[1], …).for_eachrepeats it using stable keys. Usecount = condition ? 1 : 0for an optional VM.forwithiffilters data; it does not directly create cloud resources unless its result is supplied to something likefor_each.dynamicmakes zero/more supported nested blocks. It cannot generate top-levelresource,module,lifecycle, orprovisionerblocks.- Preconditions / postconditions stop an operation when their assumptions fail. A
checkemits warnings but normally does not stop plan/apply. try()andcan()catch expression evaluation errors, not arbitrary Terraform syntax errors or invalid references to undeclared resources.nullnormally 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 configureor AWS SSO. Do not put credentials in Terraform code. - Region
us-east-1; instance typet3.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.Networknamespaces. - Region
eastus; VM sizeStandard_B1s(large alternative:Standard_B2s); official Canonical Ubuntu 24.04 LTS imageCanonical: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/0for 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 useaws ec2 describe-instances). Azureaz 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
| # | Concept | AWS folder | Azure folder |
|---|---|---|---|
| 1 | Conditional expression (ternary) | aws/01-ternary | azure/01-ternary |
| 2 | Logical AND (&&) | aws/02-and | azure/02-and |
| 3 | Logical OR ( | ) | |
| 4 | Logical NOT (!) | aws/04-not | azure/04-not |
| 5 | Comparison operators | aws/05-comparisons | azure/05-comparisons |
| 6 | Parentheses and compound logic | aws/06-compound | azure/06-compound |
| 7 | count with condition | aws/07-count | azure/07-count |
| 8 | for_each with filtering | aws/08-for-each | azure/08-for-each |
| 9 | for expressions with if | aws/09-for-if | azure/09-for-if |
| 10 | Conditional dynamic blocks | aws/10-dynamic | azure/10-dynamic |
| 11 | Conditional module instantiation | aws/11-module | azure/11-module |
| 12 | Input variable validation | aws/12-validation | azure/12-validation |
| 13 | Resource precondition | aws/13-precondition | azure/13-precondition |
| 14 | Resource postcondition | aws/14-postcondition | azure/14-postcondition |
| 15 | Output precondition | aws/15-output-precondition | azure/15-output-precondition |
| 16 | check block (warning) | aws/16-check | azure/16-check |
| 17 | try() | aws/17-try | azure/17-try |
| 18 | can() | aws/18-can | azure/18-can |
| 19 | lookup() | aws/19-lookup | azure/19-lookup |
| 20 | coalesce() | aws/20-coalesce | azure/20-coalesce |
| 21 | coalescelist() | aws/21-coalescelist | azure/21-coalescelist |
| 22 | contains() | aws/22-contains | azure/22-contains |
| 23 | alltrue() | aws/23-alltrue | azure/23-alltrue |
| 24 | anytrue() | aws/24-anytrue | azure/24-anytrue |
| 25 | length() | aws/25-length | azure/25-length |
| 26 | regex() and regexall() | aws/26-regex | azure/26-regex |
| 27 | startswith() and endswith() | aws/27-startswith-endswith | azure/27-startswith-endswith |
| 28 | null values | aws/28-null | azure/28-null |
| 29 | Optional object attributes | aws/29-optional | azure/29-optional |
| 30 | nullable variable setting | aws/30-nullable | azure/30-nullable |
| 31 | merge() and conditional maps | aws/31-merge | azure/31-merge |
| 32 | compact() and flatten() | aws/32-compact-flatten | azure/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)
| Mechanism | Timing | Failure behavior | Typical use |
|---|---|---|---|
variable validation | As Terraform evaluates inputs | Error / stop | Reject invalid regions, names, formats |
Resource lifecycle.precondition | Before resource operation, once inputs are known | Error / stop | Guard assumptions before provisioning |
Resource lifecycle.postcondition | After resource/data read or change when values are known | Error / stop | Assert properties of the resulting object |
Output precondition | Before a root output is recorded/published | Error / stop | Ensure output can be trusted |
check / assert | Checked during plan/apply and rechecked as appropriate | Warning, normally continues | Nonblocking quality/health indicator |
F. Common mistakes and troubleshooting
- Copying several labs into one directory: all use similar Terraform local names; duplicate declarations will fail. Copy exactly one scenario’s
main.tfinto a fresh directory, or use the ZIP folders directly. - Setting
count = 0and still referencing[0]: use conditional indexing ortry(); the templates demonstrate splats ([*].id) that return an empty list safely. - Using
for_eachwith an unknown key set: its keys must be known before apply. Filter a known map/list of inputs rather than discovered remote IDs. - Expecting a Terraform
if { } else { }statement: Terraform uses conditional expressions and collection filtering, not a top-level imperativeifstatement. - Confusing a dynamic block with a resource:
dynamiccan only generate repeatable nested blocks supported by the provider schema. - Assuming
trycatches static errors: it cannot repair typos such as undeclaredlocalor undeclaredresourcereferences. - Confusing
canwithtry:can()yields Boolean success/failure, especially useful for validation;try()returns a fallback value. - Assuming
nulland empty values are identical: null means absent/omitted in an optional argument;[],{}, and""mean empty, which may behave differently. - Assuming an empty list makes
alltruefalse:alltrue([])is true;anytrue([])is false. - Assuming a failed
checkblocks deployment: check assertions are advisory warnings, not policy enforcement. - Using
countorfor_eachto 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. - 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. - Azure invalid credentials: Azure password must satisfy the subscription’s password rules; the Terraform variable only tests minimum length.
- 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.
- 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
- HashiCorp: Conditional expressions
- HashiCorp: For expressions and filtering
- HashiCorp: Dynamic blocks
- HashiCorp: Validation, preconditions, postconditions and checks
- HashiCorp: Check block
- HashiCorp: Functions reference
- HashiCorp: Optional object attributes
- HashiCorp: Null values
- HashiCorp: Input variables and nullable
- AWS: Amazon Linux SSM AMI parameters
- Terraform Registry: AWS instance
- Azure: official Ubuntu 24.04 image alias
- 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.