Find the Best Cosmetic Hospitals

Explore trusted cosmetic hospitals and make a confident choice for your transformation.

“Invest in yourself — your confidence is always worth it.”

Explore Cosmetic Hospitals

Start your journey today — compare options in one place.

Terraform Conditional Expressions and Logic: Complete Guide with Examples

Terraform Conditional Expressions and Logic — Summary Table

#ConceptPurposeExampleCreates Resources?
1Ternary operatorChoose between two valuesvar.env == "prod" ? "t3.large" : "t3.micro"No
2Equality operatorsCheck equality or inequalityvar.env == "prod"No
3Comparison operatorsCompare valuesvar.count > 2No
4Logical AND (&&)All conditions must be truevar.env == "prod" && var.monitoringNo
5Logical OR (\|\|)At least one condition must be truevar.env == "prod" \|\| var.env == "stage"No
6Logical NOT (!)Reverse a Boolean value!var.enable_public_ipNo
7ParenthesesGroup complex conditions(var.env == "prod" \|\| var.env == "stage") && var.enabledNo
8Conditional countCreate zero or one resourcecount = var.enabled ? 1 : 0Yes
9Conditional for_eachCreate selected named resourcesfor_each = var.enabled ? var.instances : {}Yes
10Conditional localsSelect configuration by environmentinstance_type = var.env == "prod" ? "t3.large" : "t3.micro"No
11Filter with forSelect collection elements that meet a condition{ for k, v in var.rules : k => v if v.enabled }No, but can feed for_each
12lookup()Retrieve a map value with a defaultlookup(var.sizes, var.env, "t3.micro")No
13try()Fall back when an expression errorstry(var.settings.instance_type, "t3.micro")No
14can()Check whether an expression evaluates without errorcan(var.settings.port)No
15coalesce()Return the first non-null, non-empty stringcoalesce(var.name, "default-name")No
16Variable validationReject invalid inputvalidation { ... }No
17PreconditionsEnforce requirements before an operationprecondition { ... }No
18PostconditionsVerify a resource or data-source resultpostcondition { ... }No
19Conditional dynamic blockInclude nested configuration conditionallyfor_each = var.enabled ? [1] : []No standalone resource
20Conditional moduleDeploy a module only when neededcount = var.create_network ? 1 : 0Yes, module instances

Complete Master Guide with AWS and Azure Labs

Audience: Beginners to advanced DevOps / Cloud engineers
Terraform: Terraform 1.5+ recommended (some examples use features available in Terraform 1.2+)
Clouds: AWS EC2 and Azure Linux Virtual Machines
Format: Self-contained labs, explanations, run commands, expected outcomes, troubleshooting, and cleanup


Table of Contents

  1. Learning objectives
  2. Before you begin
  3. Conditional logic: the basic idea
  4. Lab 1 — Ternary conditional expression
  5. Lab 2 — Logical operators and compound conditions
  6. Lab 3 — Conditional resource creation with count
  7. Lab 4 — for_each with conditional filtering
  8. Lab 5 — Conditional configuration using locals
  9. Lab 6 — Dynamic blocks and optional nested rules
  10. Lab 7 — Input variable validation
  11. Lab 8 — Preconditions and postconditions
  12. Lab 9 — try(), can(), lookup(), and coalesce()
  13. Lab 10 — Conditional module instances
  14. Important patterns and gotchas
  15. Troubleshooting
  16. Best practices
  17. Quick reference
  18. Exercises and answers
  19. Glossary

1. Learning objectives

After completing this guide, you should be able to:

  • Choose a value using Terraform’s conditional (condition ? true_value : false_value) expression.
  • Combine conditions using &&, ||, !, and comparison operators.
  • Create zero, one, or many resources using count and for_each.
  • Filter collections using for expressions and if.
  • Generate nested configuration using dynamic blocks.
  • Validate input values and enforce assumptions with preconditions and postconditions.
  • Handle optional values and expression errors with try, can, lookup, and coalesce.
  • Test a configuration with terraform plan, apply, output, and destroy.

2. Before you begin

Install and authenticate

You need Terraform and credentials for the cloud you are testing.

AWS – Install the AWS CLI and Terraform. – Configure credentials, for example with aws configure, an AWS profile, or an IAM role. – The identity needs permission to create and delete EC2 instances and security groups. Depending on the lab, it may also need permissions for VPC-related resources. – Choose a region where your account can create the requested instance type.

Azure – Install Azure CLI and Terraform. – Sign in with az login. – Select the correct subscription: az account set --subscription "<SUBSCRIPTION_ID_OR_NAME>". – Your identity needs permission to create and delete resource groups and networking/compute resources. Subscription policy and regional quotas can affect VM creation.

Check tools:

terraform version
aws sts get-caller-identity
az account show --output table
Code language: JavaScript (javascript)

A safe lab workflow

For each lab: 1. Create a new, empty directory. 2. Save the complete example as main.tf. 3. Run terraform init. 4. Run terraform fmt. 5. Run terraform validate. 6. Review terraform plan. 7. Run terraform apply and confirm when ready. 8. Inspect the outputs and cloud console. 9. Run terraform destroy when finished.

Cost and security warning: Applying cloud labs can incur charges. Destroy resources after testing. Some examples create public IP addresses or permit SSH only from an address you specify. Do not use 0.0.0.0/0 for SSH in production. Never commit cloud credentials or private keys to source control.

How the examples are organized

Each cloud example below is intended to be a complete main.tf for that scenario. AWS examples create EC2 infrastructure. Azure VM examples include a resource group, virtual network, subnet, network interface, public IP, network security group, and Linux VM. For simplicity, examples use common Ubuntu image identifiers; image availability and cloud policies can vary by region and subscription.


3. Conditional logic: the basic idea

Terraform configuration is declarative. You describe the desired infrastructure, and Terraform determines the actions needed to reach that state. Terraform does not use ordinary procedural if { ... } else { ... } statements around resource blocks. Instead, conditions are expressed through values, collection filtering, meta-arguments, and validation constructs.

The conditional expression

condition ? value_if_true : value_if_false

Example:

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

locals {
  instance_type = var.environment == "prod" ? "t3.medium" : "t3.micro"
}
Code language: JavaScript (javascript)

If environment is "prod", instance_type is "t3.medium". Otherwise, it is "t3.micro".

Logical and comparison operators


Operator Meaning Example


== Equal var.environment == "prod"

!= Not equal var.environment != "dev"

> / < Greater / less than var.instance_count > 1

>= / <= Greater/less than or var.port >= 1024 equal

&& AND: both sides must be var.enabled && var.environment == "prod" true

|| OR: either side may be var.environment == "dev" || var.environment == "test" true

! NOT: reverse a Boolean !var.disabled

Use parentheses for readability when combining multiple conditions.


4. Lab 1 — Ternary conditional expression

Goal

Choose a smaller compute size for non-production and a larger size for production. This lab demonstrates a condition selecting a value; it does not conditionally omit the resource.

4A. AWS — choose EC2 instance type

Create main.tf:

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "environment" {
  type        = string
  description = "Set to dev or prod."
  default     = "dev"

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

variable "ami_id" {
  type        = string
  description = "A valid Ubuntu AMI ID in the selected AWS region."
}

variable "ssh_cidr" {
  type        = string
  description = "Your public IPv4 address in CIDR notation, for example 203.0.113.10/32."
}

locals {
  instance_type = var.environment == "prod" ? "t3.small" : "t3.micro"
}

resource "aws_security_group" "web" {
  name_prefix = "conditional-lab-"
  description = "SSH for a temporary Terraform conditional-expression lab"

  ingress {
    description = "SSH from the student's address only"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "conditional-lab-sg"
  }
}

resource "aws_instance" "example" {
  ami                    = var.ami_id
  instance_type          = local.instance_type
  vpc_security_group_ids = [aws_security_group.web.id]

  tags = {
    Name        = "conditional-${var.environment}"
    Environment = var.environment
  }
}

output "selected_instance_type" {
  value = local.instance_type
}

output "instance_id" {
  value = aws_instance.example.id
}

output "public_ip" {
  value = aws_instance.example.public_ip
}
Code language: JavaScript (javascript)

Run it (replace the placeholders with values valid for your account and region):

terraform init
terraform fmt
terraform validate
terraform plan \
  -var="ami_id=ami-REPLACE_WITH_VALID_UBUNTU_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply \
  -var="ami_id=ami-REPLACE_WITH_VALID_UBUNTU_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
terraform destroy \
  -var="ami_id=ami-REPLACE_WITH_VALID_UBUNTU_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

Observe: With the default environment = "dev", output selected_instance_type is t3.micro. To test production, add -var="environment=prod" to both plan and apply.

AMI tip: AMI IDs are regional. Find a current Ubuntu AMI in the selected region using the AWS console or Canonical’s published images. Do not copy an AMI ID from a different region.

4B. Azure — choose Linux VM size

Create main.tf:

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

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

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

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type        = string
  description = "Contents of your SSH public key, not the private key."
}

variable "ssh_cidr" {
  type        = string
  description = "Your public IPv4 address in CIDR notation, for example 203.0.113.10/32."
}

locals {
  vm_size = var.environment == "prod" ? "Standard_B2s" : "Standard_B1s"
  prefix  = "tfcond-${var.environment}"
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.20.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.20.1.0/24"]
}

resource "azurerm_public_ip" "lab" {
  name                = "${local.prefix}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromMyIP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_interface" "lab" {
  name                = "${local.prefix}-nic"
  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_network_interface_security_group_association" "lab" {
  network_interface_id      = azurerm_network_interface.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "lab" {
  name                = "${local.prefix}-vm"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = local.vm_size
  admin_username      = var.admin_username
  network_interface_ids = [
    azurerm_network_interface.lab.id
  ]

  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Environment = var.environment
  }
}

output "selected_vm_size" {
  value = local.vm_size
}

output "public_ip" {
  value = azurerm_public_ip.lab.ip_address
}

output "ssh_command" {
  value = "ssh ${var.admin_username}@${azurerm_public_ip.lab.ip_address}"
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
terraform destroy \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

If you use a different public key file, substitute its path. If ~/.ssh/id_ed25519.pub does not exist, create a key pair with ssh-keygen -t ed25519. Check that the selected VM size is available and permitted in the subscription and region.

Observe: The default selects Standard_B1s; -var="environment=prod" selects Standard_B2s. These are example sizes, not a guarantee of regional availability or quota.


5. Lab 2 — Logical operators and compound conditions

Goal

Use more than one input to decide a value. This is useful when production settings should be selected only when the environment is production and a feature is enabled.

5A. AWS — conditionally choose instance type

Save as main.tf:

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

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

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

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

locals {
  use_large_instance = var.environment == "prod" && var.high_performance_enabled
  instance_type      = local.use_large_instance ? "t3.small" : "t3.micro"
  environment_class  = var.environment == "dev" || var.environment == "test" ? "nonproduction" : "production-like"
  feature_disabled   = !var.high_performance_enabled
}

resource "aws_security_group" "lab" {
  name_prefix = "logic-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "lab" {
  ami                    = var.ami_id
  instance_type          = local.instance_type
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name               = "logic-lab"
    EnvironmentClass   = local.environment_class
    HighPerformanceOff = tostring(local.feature_disabled)
  }
}

output "use_large_instance" {
  value = local.use_large_instance
}

output "instance_type" {
  value = local.instance_type
}

output "environment_class" {
  value = local.environment_class
}
Code language: JavaScript (javascript)

Run with the same terraform init, validate, plan, apply, output, and destroy workflow used in Lab 1. Supply ami_id and ssh_cidr as variables. Try:

terraform plan \
  -var="ami_id=ami-REPLACE_WITH_VALID_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32" \
  -var="environment=prod" \
  -var="high_performance_enabled=true"
Code language: JavaScript (javascript)

Then change high_performance_enabled=false and inspect the plan. The selected instance type changes from t3.small to t3.micro.

5B. Azure — logical operators choose VM size

Save as main.tf:

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

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

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

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

locals {
  use_large_vm       = var.environment == "prod" && var.high_performance_enabled
  vm_size            = local.use_large_vm ? "Standard_B2s" : "Standard_B1s"
  environment_class  = var.environment == "dev" || var.environment == "test" ? "nonproduction" : "production-like"
  prefix             = "tflogic"
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.30.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.30.1.0/24"]
}

resource "azurerm_public_ip" "lab" {
  name                = "${local.prefix}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromMyIP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_interface" "lab" {
  name                = "${local.prefix}-nic"
  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_network_interface_security_group_association" "lab" {
  network_interface_id      = azurerm_network_interface.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "lab" {
  name                = "${local.prefix}-vm"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = local.vm_size
  admin_username      = var.admin_username
  network_interface_ids = [
    azurerm_network_interface.lab.id
  ]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    EnvironmentClass = local.environment_class
  }
}

output "use_large_vm" {
  value = local.use_large_vm
}

output "vm_size" {
  value = local.vm_size
}

output "environment_class" {
  value = local.environment_class
}

output "public_ip" {
  value = azurerm_public_ip.lab.ip_address
}
Code language: JavaScript (javascript)

Run the standard workflow and provide:

-var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
-var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

Test these combinations: – environment=prod, high_performance_enabled=true → larger VM. – environment=prod, high_performance_enabled=false → smaller VM. – environment=dev, high_performance_enabled=true → smaller VM.


6. Lab 3 — Conditional resource creation with count

Concept

Use count = condition ? 1 : 0 when a resource should exist only when a condition is true. When the condition is false, Terraform plans zero instances of that resource.

Important: Resources using count are addressed with numeric indexes, such as aws_instance.optional[0]. Changing count from one to zero destroys that instance when you apply the change.

6A. AWS — optional EC2 instance

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

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

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

resource "aws_security_group" "lab" {
  name_prefix = "count-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "optional" {
  count                  = var.create_instance ? 1 : 0
  ami                    = var.ami_id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name = "optional-ec2"
  }
}

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

output "instance_count" {
  value = length(aws_instance.optional)
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform validate
terraform plan -var="ami_id=ami-REPLACE_WITH_VALID_AMI" -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply -var="ami_id=ami-REPLACE_WITH_VALID_AMI" -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
Code language: JavaScript (javascript)

To turn off the instance:

terraform plan -var="create_instance=false" -var="ami_id=ami-REPLACE_WITH_VALID_AMI" -var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

If the state currently contains the instance, applying this plan destroys it. Finish with terraform destroy, using the same required variable values.

6B. Azure — optional Linux VM

This example conditionally creates the VM. The network foundation remains present while create_vm = false, which makes the effect easy to observe.

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

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

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

locals {
  prefix = "tfcount"
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.40.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.40.1.0/24"]
}

resource "azurerm_public_ip" "lab" {
  count               = var.create_vm ? 1 : 0
  name                = "${local.prefix}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromMyIP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_interface" "lab" {
  count               = var.create_vm ? 1 : 0
  name                = "${local.prefix}-nic"
  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[0].id
  }
}

resource "azurerm_network_interface_security_group_association" "lab" {
  count                     = var.create_vm ? 1 : 0
  network_interface_id      = azurerm_network_interface.lab[0].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "lab" {
  count               = var.create_vm ? 1 : 0
  name                = "${local.prefix}-vm"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = "Standard_B1s"
  admin_username      = var.admin_username
  network_interface_ids = [
    azurerm_network_interface.lab[0].id
  ]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }
}

output "vm_count" {
  value = length(azurerm_linux_virtual_machine.lab)
}

output "public_ip" {
  value = var.create_vm ? azurerm_public_ip.lab[0].ip_address : null
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
Code language: JavaScript (javascript)

To test no VM:

terraform plan \
  -var="create_vm=false" \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

The VM, NIC, and public IP are removed if you apply that change. The resource group, VNet, subnet, and NSG remain until destroyed. Destroy the lab when finished.


7. Lab 4 — for_each with conditional filtering

Concept

Use for_each when each instance has a stable key. A filtered map can include only items that meet a condition. Unlike count, for_each instances are addressed by keys rather than numeric positions.

7A. AWS — create EC2 instances only for enabled applications

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type = map(object({
    enabled = bool
    type    = string
  }))
  default = {
    frontend = { enabled = true,  type = "t3.micro" }
    backend  = { enabled = true,  type = "t3.micro" }
    sandbox  = { enabled = false, type = "t3.micro" }
  }
}

locals {
  enabled_applications = {
    for name, app in var.applications : name => app
    if app.enabled
  }
}

resource "aws_security_group" "lab" {
  name_prefix = "foreach-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "app" {
  for_each               = local.enabled_applications
  ami                    = var.ami_id
  instance_type          = each.value.type
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name        = "app-${each.key}"
    Application = each.key
  }
}

output "created_instance_ids" {
  value = { for name, instance in aws_instance.app : name => instance.id }
}

output "created_application_names" {
  value = keys(aws_instance.app)
}
Code language: JavaScript (javascript)

Run using the standard workflow and supply ami_id and ssh_cidr. The defaults create frontend and backend, but not sandbox.

To enable sandbox, pass a complete replacement map through a .tfvars file or change the default. For example, create terraform.tfvars:

applications = {
  frontend = { enabled = true,  type = "t3.micro" }
  backend  = { enabled = true,  type = "t3.micro" }
  sandbox  = { enabled = true,  type = "t3.micro" }
}
Code language: JavaScript (javascript)

Then run terraform plan and terraform apply.

7B. Azure — create Linux VMs only for enabled applications

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type = map(object({
    enabled = bool
    size    = string
  }))
  default = {
    frontend = { enabled = true,  size = "Standard_B1s" }
    backend  = { enabled = true,  size = "Standard_B1s" }
    sandbox  = { enabled = false, size = "Standard_B1s" }
  }
}

locals {
  prefix = "tfforeach"
  enabled_applications = {
    for name, app in var.applications : name => app
    if app.enabled
  }
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.50.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.50.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromMyIP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "app" {
  for_each            = local.enabled_applications
  name                = "${local.prefix}-${each.key}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "app" {
  for_each            = local.enabled_applications
  name                = "${local.prefix}-${each.key}-nic"
  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.app[each.key].id
  }
}

resource "azurerm_network_interface_security_group_association" "app" {
  for_each                  = local.enabled_applications
  network_interface_id      = azurerm_network_interface.app[each.key].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "app" {
  for_each            = local.enabled_applications
  name                = "${local.prefix}-${each.key}-vm"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = each.value.size
  admin_username      = var.admin_username
  network_interface_ids = [
    azurerm_network_interface.app[each.key].id
  ]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Application = each.key
  }
}

output "created_vm_names" {
  value = keys(azurerm_linux_virtual_machine.app)
}

output "public_ips" {
  value = { for name, ip in azurerm_public_ip.app : name => ip.ip_address }
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
terraform destroy \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

The default creates two VMs. Enabling or disabling an application changes only that keyed instance and its related networking resources.


8. Lab 5 — Conditional configuration using locals

Concept

A local value can centralize decisions that are reused across resources. This makes configuration easier to read and reduces repeated expressions.

8A. AWS — environment-based sizing and tags

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

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

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

locals {
  settings = {
    dev  = { type = "t3.micro", monitoring = false }
    test = { type = "t3.micro", monitoring = false }
    prod = { type = "t3.small", monitoring = true }
  }

  selected = local.settings[var.environment]
  name     = "app-${var.environment}"
}

resource "aws_security_group" "lab" {
  name_prefix = "locals-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "lab" {
  ami                    = var.ami_id
  instance_type          = local.selected.type
  monitoring             = local.selected.monitoring
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name        = local.name
    Environment = var.environment
  }
}

output "selected_settings" {
  value = local.selected
}
Code language: JavaScript (javascript)

Run with environment=dev, then environment=prod. Observe the instance type and monitoring setting in terraform plan.

8B. Azure — environment-based VM size and tags

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

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

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

locals {
  settings = {
    dev  = { size = "Standard_B1s", monitoring = "basic" }
    test = { size = "Standard_B1s", monitoring = "basic" }
    prod = { size = "Standard_B2s", monitoring = "enhanced" }
  }
  selected = local.settings[var.environment]
  prefix   = "tflocals-${var.environment}"
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
  tags = {
    Environment = var.environment
    Monitoring  = local.selected.monitoring
  }
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.60.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.60.1.0/24"]
}

resource "azurerm_public_ip" "lab" {
  name                = "${local.prefix}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  security_rule {
    name                       = "AllowSSHFromMyIP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_interface" "lab" {
  name                = "${local.prefix}-nic"
  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_network_interface_security_group_association" "lab" {
  network_interface_id      = azurerm_network_interface.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "lab" {
  name                = "${local.prefix}-vm"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = local.selected.size
  admin_username      = var.admin_username
  network_interface_ids = [azurerm_network_interface.lab.id]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Environment = var.environment
    Monitoring  = local.selected.monitoring
  }
}

output "selected_settings" {
  value = local.selected
}

output "public_ip" {
  value = azurerm_public_ip.lab.ip_address
}
Code language: JavaScript (javascript)

Run using the Azure workflow from Lab 1, with the SSH public key and CIDR variables. Compare the plan for environment=dev and environment=prod.


9. Lab 6 — Dynamic blocks and optional nested rules

Concept

A dynamic block generates nested blocks inside a resource. It is useful for a variable number of ingress rules or similar nested provider configuration. It does not create multiple top-level resources.

9A. AWS — dynamic ingress rules

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ssh_cidr" {
  type = string
}

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

locals {
  base_rules = [
    { name = "ssh",  port = 22,  cidr = var.ssh_cidr },
  ]
  http_rules = var.enable_http ? [
    { name = "http", port = 80, cidr = "10.0.0.0/8" }
  ] : []
  ingress_rules = concat(local.base_rules, local.http_rules)
}

resource "aws_security_group" "lab" {
  name_prefix = "dynamic-lab-"
  description = "Dynamic ingress rule demonstration"

  dynamic "ingress" {
    for_each = local.ingress_rules
    iterator = rule
    content {
      description = rule.value.name
      from_port   = rule.value.port
      to_port     = rule.value.port
      protocol    = "tcp"
      cidr_blocks = [rule.value.cidr]
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "dynamic-lab-sg"
  }
}

output "configured_ingress_rules" {
  value = local.ingress_rules
}
Code language: JavaScript (javascript)

Run terraform init, terraform validate, and terraform plan. Set -var="enable_http=false" to see the HTTP rule disappear from the plan. This lab creates only a security group, so it does not need an EC2 instance.

9B. Azure — optional SSH and HTTP security rules

Azure NSG rules are separate resources, so use for_each rather than a dynamic nested block.

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "ssh_cidr" {
  type = string
}

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

locals {
  rules = merge(
    {
      ssh = {
        priority = 100
        port     = "22"
        source   = var.ssh_cidr
      }
    },
    var.enable_http ? {
      http = {
        priority = 110
        port     = "80"
        source   = "10.0.0.0/8"
      }
    } : {}
  )
}

resource "azurerm_resource_group" "lab" {
  name     = "tfdynamic-rg"
  location = var.location
}

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

resource "azurerm_network_security_rule" "rule" {
  for_each                    = local.rules
  name                        = "Allow-${each.key}"
  priority                    = each.value.priority
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = each.value.port
  source_address_prefix       = each.value.source
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

output "created_rule_names" {
  value = keys(azurerm_network_security_rule.rule)
}
Code language: JavaScript (javascript)

Run terraform plan, then repeat with -var="enable_http=false". This example intentionally shows the Azure-native pattern: separate NSG rules are managed with for_each.


10. Lab 7 — Input variable validation

Concept

Variable validation checks user-provided values and stops planning with a helpful message when the value is invalid. Validation is suitable for allowed environment names, port ranges, naming rules, and other input constraints.

7A. AWS — validate environment and port

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

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

  validation {
    condition     = contains(["dev", "test", "prod"], var.environment)
    error_message = "environment must be dev, test, or prod."
  }
}

variable "application_port" {
  type    = number
  default = 8080

  validation {
    condition     = var.application_port >= 1 && var.application_port <= 65535
    error_message = "application_port must be between 1 and 65535."
  }
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

resource "aws_security_group" "lab" {
  name_prefix = "validation-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  ingress {
    from_port   = var.application_port
    to_port     = var.application_port
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "lab" {
  ami                    = var.ami_id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.lab.id]
  tags = {
    Name        = "validation-${var.environment}"
    Environment = var.environment
  }
}

output "application_port" {
  value = var.application_port
}
Code language: JavaScript (javascript)

Test valid input, then invalid input:

terraform init
terraform validate
terraform plan -var="ami_id=ami-REPLACE_WITH_VALID_AMI" -var="ssh_cidr=YOUR_PUBLIC_IP/32" -var="environment=prod" -var="application_port=8080"
terraform plan -var="ami_id=ami-REPLACE_WITH_VALID_AMI" -var="ssh_cidr=YOUR_PUBLIC_IP/32" -var="application_port=70000"
Code language: JavaScript (javascript)

The second plan should fail variable validation before infrastructure is created.

7B. Azure — validate environment and SSH port concept

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

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

variable "ssh_port" {
  type    = number
  default = 22
  validation {
    condition     = var.ssh_port >= 1 && var.ssh_port <= 65535
    error_message = "ssh_port must be between 1 and 65535."
  }
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

resource "azurerm_resource_group" "lab" {
  name     = "tfvalidation-${var.environment}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "tfvalidation-vnet"
  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                 = "default"
  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_public_ip" "lab" {
  name                = "tfvalidation-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_security_group" "lab" {
  name                = "tfvalidation-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = tostring(var.ssh_port)
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_interface" "lab" {
  name                = "tfvalidation-nic"
  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_network_interface_security_group_association" "lab" {
  network_interface_id      = azurerm_network_interface.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "lab" {
  name                = "tfvalidation-vm"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = "Standard_B1s"
  admin_username      = var.admin_username
  network_interface_ids = [azurerm_network_interface.lab.id]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Environment = var.environment
  }
}

output "environment" {
  value = var.environment
}

output "public_ip" {
  value = azurerm_public_ip.lab.ip_address
}
Code language: JavaScript (javascript)

Run with ssh_public_key and ssh_cidr. Try -var="ssh_port=70000"; validation should reject it. The example permits a custom SSH port to demonstrate numeric validation; the VM’s SSH daemon itself remains configured on port 22 unless separately changed, so keep ssh_port=22 for actual SSH access.


11. Lab 8 — Preconditions and postconditions

Concept

  • Precondition: verifies an assumption before Terraform proceeds with the relevant operation.
  • Postcondition: verifies a property of a resource after Terraform reads or creates it.
  • Check block: performs an assertion and reports a warning if it fails; it is not a substitute for blocking validation.

These constructs are useful for expressing requirements close to the resource or output they protect.

8A. AWS — enforce a production tag with a precondition

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

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

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

resource "aws_security_group" "lab" {
  name_prefix = "condition-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "lab" {
  ami                    = var.ami_id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name        = "precondition-lab"
    Environment = var.environment
  }

  lifecycle {
    precondition {
      condition     = contains(["test", "prod"], var.environment)
      error_message = "This lab permits only test or prod environments."
    }

    postcondition {
      condition     = self.instance_type == "t3.micro"
      error_message = "The instance type must remain t3.micro for this lab."
    }
  }
}

output "instance_id" {
  value = aws_instance.lab.id
}
Code language: PHP (php)

Run with environment=prod and then try environment=dev. The latter should fail the precondition. This example’s postcondition checks the configured instance type; it demonstrates the syntax rather than a cloud health check.

8B. Azure — precondition on VM configuration

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

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

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

locals {
  vm_size = "Standard_B1s"
}

resource "azurerm_resource_group" "lab" {
  name     = "tfcondition-rg"
  location = var.location
}

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

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.80.1.0/24"]
}

resource "azurerm_public_ip" "lab" {
  name                = "tfcondition-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_security_group" "lab" {
  name                = "tfcondition-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  security_rule {
    name                       = "AllowSSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_interface" "lab" {
  name                = "tfcondition-nic"
  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_network_interface_security_group_association" "lab" {
  network_interface_id      = azurerm_network_interface.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "lab" {
  name                = "tfcondition-vm"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = local.vm_size
  admin_username      = var.admin_username
  network_interface_ids = [azurerm_network_interface.lab.id]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  lifecycle {
    precondition {
      condition     = contains(["test", "prod"], var.environment)
      error_message = "This lab permits only test or prod environments."
    }

    postcondition {
      condition     = self.size == local.vm_size
      error_message = "The VM size does not match the selected configuration."
    }
  }

  tags = {
    Environment = var.environment
  }
}

output "vm_id" {
  value = azurerm_linux_virtual_machine.lab.id
}
Code language: PHP (php)

Run with environment=prod, then try environment=dev and inspect the validation failure. Use the same Azure key/CIDR inputs and cleanup procedure as earlier Azure labs.


12. Lab 9 — try(), can(), lookup(), and coalesce()

Concept

These functions are often used alongside conditions, but they are not all conditional expressions.

  • try(a, b) returns the first expression that does not produce an error.
  • can(expression) returns true if the expression can be evaluated without an error.
  • lookup(map, key, default) returns a map value or a fallback.
  • coalesce(a, b, ...) returns the first non-null, non-empty-string value.
  • contains(list, value) tests membership.

Do not use these functions to hide errors that should be fixed directly.

9A. AWS — safe defaults and conditional configuration

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

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

variable "custom_instance_type" {
  type    = string
  default = ""
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

locals {
  instance_types = {
    dev  = "t3.micro"
    test = "t3.micro"
    prod = "t3.small"
  }

  default_type = lookup(local.instance_types, var.environment, "t3.micro")
  chosen_type  = coalesce(var.custom_instance_type, local.default_type)
  environment_is_known = contains(keys(local.instance_types), var.environment)
  parsed_number_is_valid = can(tonumber("123"))
  safe_environment = try(trimspace(var.environment), "dev")
}

resource "aws_security_group" "lab" {
  name_prefix = "functions-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "lab" {
  ami                    = var.ami_id
  instance_type          = local.chosen_type
  vpc_security_group_ids = [aws_security_group.lab.id]
  tags = {
    Name        = "functions-lab"
    Environment = local.safe_environment
  }
}

output "selected_instance_type" {
  value = local.chosen_type
}

output "environment_is_known" {
  value = local.environment_is_known
}

output "parsed_number_is_valid" {
  value = local.parsed_number_is_valid
}
Code language: JavaScript (javascript)

Run with the usual AWS commands and required inputs. Try -var="environment=prod" and then -var="custom_instance_type=t3.small". The explicit custom value takes precedence. The example uses can() and try() to illustrate safe expression evaluation; they do not verify that an instance type is available in your account or region.

9B. Azure — lookup and coalesce for VM size and name

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

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

variable "custom_vm_size" {
  type    = string
  default = ""
}

variable "custom_name" {
  type    = string
  default = ""
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

locals {
  vm_sizes = {
    dev  = "Standard_B1s"
    test = "Standard_B1s"
    prod = "Standard_B2s"
  }
  default_size = lookup(local.vm_sizes, var.environment, "Standard_B1s")
  vm_size      = coalesce(var.custom_vm_size, local.default_size)
  vm_name      = coalesce(var.custom_name, "tf-functions-${var.environment}-vm")
  environment_is_known = contains(keys(local.vm_sizes), var.environment)
  number_parse_works   = can(tonumber("123"))
  safe_environment     = try(trimspace(var.environment), "dev")
}

resource "azurerm_resource_group" "lab" {
  name     = "tffunctions-rg"
  location = var.location
}

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

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.90.1.0/24"]
}

resource "azurerm_public_ip" "lab" {
  name                = "tffunctions-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_security_group" "lab" {
  name                = "tffunctions-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  security_rule {
    name                       = "AllowSSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_interface" "lab" {
  name                = "tffunctions-nic"
  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_network_interface_security_group_association" "lab" {
  network_interface_id      = azurerm_network_interface.lab.id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "lab" {
  name                = local.vm_name
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = local.vm_size
  admin_username      = var.admin_username
  network_interface_ids = [azurerm_network_interface.lab.id]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Environment = local.safe_environment
  }
}

output "selected_vm_size" {
  value = local.vm_size
}

output "vm_name" {
  value = local.vm_name
}

output "environment_is_known" {
  value = local.environment_is_known
}

output "number_parse_works" {
  value = local.number_parse_works
}

output "public_ip" {
  value = azurerm_public_ip.lab.ip_address
}
Code language: JavaScript (javascript)

Run with the standard Azure workflow. Try -var="environment=prod" and -var="custom_vm_size=Standard_B1s". coalesce chooses the non-empty custom value when supplied. Confirm the chosen size is available before applying.


13. Lab 10 — Conditional module instances

Concept

Modules can use count or for_each, just like resources. When using count, outputs from the module become a list of module instances. A module using for_each is addressed by its keys.

This lab is deliberately a small local module example so it can be tested without creating cloud resources. It demonstrates Terraform language behavior; cloud-specific modules can use the same pattern.

Create this directory layout:

conditional-module-lab/
├── main.tf
└── modules/
    └── label/
        └── main.tf
Code language: JavaScript (javascript)

Root main.tf:

terraform {
  required_version = ">= 1.3.0"
}

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

module "label" {
  source = "./modules/label"
  count  = var.create_label ? 1 : 0

  label = "demo"
}

output "label_value" {
  value = var.create_label ? module.label[0].value : null
}
Code language: JavaScript (javascript)

modules/label/main.tf:

variable "label" {
  type = string
}

output "value" {
  value = upper(var.label)
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform validate
terraform plan
terraform apply
terraform output
terraform plan -var="create_label=false"
Code language: JavaScript (javascript)

This demonstrates conditional module instantiation. No cloud infrastructure is created.


14. Important patterns and gotchas

count versus for_each


Feature count for_each


Input Non-negative integer Map or set of strings

Address Numeric index, Key, e.g. [0] e.g. ["frontend"]

Best for Fixed number or simple Named resources with on/off stable identity

Main risk Removing an element Renaming a key changes from a list can shift the resource address indexes

Conditional expressions must have compatible result types

Good:

var.enabled ? "enabled" : "disabled"
Code language: JavaScript (javascript)

Potentially problematic if the branches have incompatible types:

var.enabled ? 1 : "no"
Code language: JavaScript (javascript)

Keep both branches the same type where possible.

Do not use count to make a resource depend on an unknown result

Terraform must know count and for_each instance keys during planning. If the condition depends on an attribute that is unknown until apply, Terraform may not be able to determine the instance count. Base instance selection on known input variables or known configuration whenever possible.

Resource references change when count or for_each is used

Without count:

aws_instance.web.id
Code language: CSS (css)

With count:

aws_instance.web[0].id
aws_instance.web[*].id
Code language: CSS (css)

With for_each:

aws_instance.web["frontend"].id
Code language: CSS (css)

null is not the same as false

  • false is a Boolean value.
  • null means absence of a value and can omit an optional argument.
  • An empty string ("") is still a string.
  • An empty list ([]) is a collection with zero elements.

dynamic is for nested blocks

Use for_each on a resource or module to repeat top-level instances. Use dynamic when you need repeated nested blocks within one resource.

Azure NSG rules and AWS security group rules differ

The AWS provider supports inline ingress/egress blocks, including dynamic blocks in supported resource schemas. Azure NSG rules are commonly represented as separate azurerm_network_security_rule resources, making for_each the natural pattern.

try() and can() are not generic exception handling

Use them for expressions that might be absent or invalid, especially when normalizing external or loosely structured data. Do not wrap every expression in try() to conceal configuration mistakes.

Precondition, postcondition, and check are different

  • Preconditions and postconditions can block an operation when their assertions fail.
  • check blocks are designed for ongoing checks and generally report warnings rather than blocking the operation.
  • Variable validation is usually the clearest choice for invalid user inputs.

15. Troubleshooting

Error: Invalid for_each argument

Common cause: The keys or set members cannot be determined until apply, or the value is not a map/set of strings.

Fix: Use stable keys known during planning. For maps of objects, keep the keys independent of computed cloud resource IDs.

Error: Invalid count argument

Common cause: Terraform cannot determine the count during planning.

Fix: Use a Boolean input variable or another value known before apply.

Error: Invalid index

Common cause: Referencing resource.example[0] when count = 0.

Fix: Guard the reference with a condition, or use a splat expression such as resource.example[*].id.

Azure VM size or quota errors

Common cause: The selected VM size is unavailable in the region, restricted by policy, or exceeds quota.

Fix: Check Azure regional availability, subscription quota, and policy. Choose a size permitted by the subscription; changing a Terraform condition cannot grant quota.

AWS AMI not found

Common cause: AMI IDs are regional, stale, or incorrect.

Fix: Look up a valid image in the configured AWS region and pass its ID to ami_id.

SSH connection fails

Check: – ssh_cidr matches your current public IPv4 address in CIDR notation (often /32). – You supplied the public key, not the private key, to Azure. – The matching private key is available locally. – The cloud resource has finished provisioning. – The selected region, route, security rules, and OS image are correct. – For Azure, if you changed the NSG destination port, the Linux SSH daemon must also be configured for that port.

Provider version or registration issues

Run:

terraform init -upgrade
terraform providers
terraform validate

Review provider constraints and your organization’s Azure provider-registration policy. Do not run init -upgrade blindly in production without reviewing the provider version changes.


16. Best practices

  1. Prefer for_each for named resources where stable keys matter.
  2. Use count for simple on/off resources or truly interchangeable instances.
  3. Put complex decisions in locals and give them descriptive names.
  4. Validate inputs early with variable validation blocks.
  5. Use preconditions and postconditions for assumptions tied to resources.
  6. Keep environment settings in a map/object instead of deeply nested ternaries.
  7. Keep security rules restrictive. Never expose SSH publicly just to simplify a lab.
  8. Pin provider major versions and review upgrades.
  9. Inspect terraform plan before every apply.
  10. Use separate directories or isolated state for each hands-on lab.
  11. Destroy temporary lab resources to avoid ongoing costs.
  12. Avoid changing for_each keys or count list order in existing production state without understanding the resulting resource address changes.
  13. Keep secrets out of .tf files and source control.
  14. Test conditions by changing one input at a time and comparing plans.

17. Quick reference


Need Recommended technique


Choose one value or another condition ? a : b

Combine Boolean requirements &&, ||, !

Create a resource only when enabled count = var.enabled ? 1 : 0

Create named resources from a for_each map/set

Filter a list or map for ... if ...

Transform a collection for expression

Repeat nested provider blocks dynamic

Reject invalid input Variable validation

Enforce an assumption before/after precondition / postcondition a resource operation

Report an ongoing assertion warning check block

Safely attempt an expression try()

Test if an expression evaluates can() without error

Get a map value with a fallback lookup()

Choose first non-null/non-empty coalesce() string

Check whether a collection contains contains() a value

Provide optional argument absence null


18. Exercises and answers

Exercise 1

Choose t3.small only if environment is prod; otherwise choose t3.micro.

Answer:

locals {
  instance_type = var.environment == "prod" ? "t3.small" : "t3.micro"
}
Code language: JavaScript (javascript)

Exercise 2

Create an EC2 instance only if create_instance is true.

Answer:

resource "aws_instance" "optional" {
  count = var.create_instance ? 1 : 0
  # Include the required AMI, instance type, and other settings.
}
Code language: PHP (php)

Exercise 3

Filter a map of applications to keep only enabled applications.

Answer:

locals {
  enabled_applications = {
    for name, app in var.applications : name => app
    if app.enabled
  }
}
Code language: JavaScript (javascript)

Exercise 4

Allow only dev, test, or prod.

Answer:

variable "environment" {
  type = string

  validation {
    condition     = contains(["dev", "test", "prod"], var.environment)
    error_message = "Use dev, test, or prod."
  }
}
Code language: JavaScript (javascript)

Exercise 5

What is the difference between dynamic and for_each?

Answer: for_each repeats resource or module instances. A dynamic block generates repeated nested blocks inside a resource.

Exercise 6

Why might Terraform reject a count expression?

Answer: Terraform must know how many instances to plan. If the count depends on a value that is unknown until apply, Terraform may not be able to calculate it.


19. Glossary

  • Conditional expression: Selects a value based on a Boolean condition.
  • Boolean: A value that is either true or false.
  • Meta-argument: A special argument such as count, for_each, or depends_on that changes Terraform’s resource/module behavior.
  • Collection: A list, tuple, set, map, or object.
  • Iteration: Processing or creating configuration for multiple elements.
  • Filtering: Selecting only collection elements that meet a condition.
  • Dynamic block: A construct that generates nested blocks from a collection.
  • Precondition: An assertion that must hold before a relevant operation can proceed.
  • Postcondition: An assertion evaluated after a resource operation/read.
  • Plan: Terraform’s preview of proposed infrastructure changes.
  • State: Terraform’s record of managed infrastructure objects.

Final checklist

Before finishing a lab, verify:

  • [ ] terraform fmt completed.
  • [ ] terraform validate passed.
  • [ ] The plan matches the condition you intended to test.
  • [ ] Outputs show the expected value or number of instances.
  • [ ] You inspected the cloud console or CLI where appropriate.
  • [ ] You ran terraform destroy for temporary infrastructure.
  • [ ] You checked that no lab resources remain.

Remember: Terraform conditions decide values and desired configuration. Terraform then compares that configuration with state and real infrastructure to calculate a plan. Always review the plan before applying it.

Find Trusted Cardiac Hospitals

Compare heart hospitals by city and services — all in one place.

Explore Hospitals
I'm Rajesh Kumar, a DevOps, SRE, DevSecOps, Cloud, and Platform Engineering expert passionate about sharing practical knowledge, real-world experiences, and industry best practices. I have worked at Cotocus and regularly write about technology, travel, investing, health, product reviews, and digital marketing through my various platforms. I publish technical articles at DevOps School, travel stories at Holiday Landmark, stock market insights at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow, and SEO and digital marketing strategies at Wizbrand.

Related Posts

Terraform Loops and Iteration: Complete Guide to count, for_each, for Expressions, and Dynamic Blocks

Terraform Loops and Iteration — Summary Table # Option / Method Purpose Input Common use case Creates resources? 1 count Repeats a resource or module a specified…

Read More

Terraform workspace explained!!!

What is Terraform workspace? Have you worked with terraform workflow? such as terraform init/plan/apply/destroy with terraform configuration file? If Yes, you already have worked with Terraform workspace….

Read More

Terraform Tutorials: Templating in Terraform

Top 10 Templating Engines Across Major Programming Languages # Templating Engine Programming Language Common Framework / Ecosystem Typical Real Use 1 Jinja2 Python Flask, Ansible HTML generation,…

Read More

Terraform provisioners Tutorials and Complete Guide

Terraform provisioners are used to execute scripts or shell commands on a local or remote machine as part of resource creation/deletion. They are similar to “EC2 instance…

Read More

Terraform Tutorials: Local Values using Local Block

What is local value in terraform? In Terraform, a locals block is used to define local variables within a module, allowing you to create reusable expressions and…

Read More

Terraform Tutorials: Module Complete Guide

A Terraform module is a collection of configuration files that encapsulate resources used together to achieve a specific outcome. Modules promote reusability, organization, and maintainability in infrastructure…

Read More