Terraform: Which Variable Types Support for Loops?

0

A Terraform for expression works on:

list

set

tuple

map

object

map(object)

list(object)

You can loop through any collection type.

1. for loop on LIST (EC2 AMIs)

Variable

variable "amis" {
type = list(string)
default = ["ami-11111", "ami-22222", "ami-33333"]
}

Loop Example

locals {
ami_prefix_list = [for ami in var.amis : "${ami}-latest"]
}

Output

output "ami_prefix_list" {
value = local.ami_prefix_list
}

2. for loop on SET (Security groups)

Variable

variable "security_groups" {
type = set(string)
default = ["sg-web", "sg-app", "sg-db"]
}

Loop

locals {
sg_tags = [for sg in var.security_groups : "tag-${sg}"]
}

Output

output "sg_tags" {
value = local.sg_tags
}

3. for loop on MAP (NLB Target Groups)

Variable

variable "nlb_target_groups" {
type = map(string)
default = {
tg1 = "80"
tg2 = "443"
tg3 = "8080"
        }
}

Loop

locals {
nlb_ports_list = [
for name, port in var.nlb_target_groups :
"target-group-${name}-${port}"
]
}

Output

output "nlb_ports_list" {
value = local.nlb_ports_list
}

4. for loop on OBJECT (Single EC2 config)

Variable

variable "single_ec2" {
type = object({
instance_type = string
ebs_size = number
subnet_id = string
})
default = {
instance_type = "t3.micro"
ebs_size = 20
subnet_id = "subnet-123"
     }
}

Loop over object attributes

locals {
ec2_values = [for k, v in var.single_ec2 : "${k}=${v}"]
}

Output

output "ec2_values" {
value = local.ec2_values
}

5. for loop on LIST(OBJECT) (ALB listener rules)

Variable

variable "alb_listeners" {
type = list(object({
port = number
protocol = string
}))
default = [
{ port = 80,
protocol = "HTTP"
},
{ port = 443,
protocol = "HTTPS" }
]
}

Loop

locals {
listener_strings = [
for listener in var.alb_listeners :
"${listener.protocol}:${listener.port}"
]
}

Output

output "listener_strings" {
value = local.listener_strings
}

6. for loop on MAP(OBJECT) (Multiple EC2 Instances)

Variable

variable "ec2_instances" {
type = map(object({
instance_type = string
subnet_id = string
}))
default = {
web1 = {
instance_type = "t3.micro", subnet_id = "subnet-a"
}
web2 = {
instance_type = "t3.micro",
subnet_id = "subnet-b"
}
}
}

Loop

locals {
instance_subnets = {
for name, cfg in var.ec2_instances :
name => cfg.subnet_id
}
}

Output

output "instance_subnets" {
value = local.instance_subnets
}

7. for loop on TUPLE (mixed values example)

Variable

variable "mixed_values" {
type = tuple([string, number, bool])
default = ["alpha", 100, true]
}

Loop

locals {
mixed_string = [for v in var.mixed_values : tostring(v)]
}

Output

output "mixed_string" {
value = local.mixed_string
}

8. for loop on LIST of VPC Endpoints

Variable

variable "vpc_endpoints" {
type = list(string)
default = ["s3", "logs", "monitoring"]
}

Loop

locals {
endpoint_service_names = [
for ep in var.vpc_endpoints :
"com.amazonaws.${ep}"
]
}

Output

output "endpoint_service_names" {
value = local.endpoint_service_names
}

🎯 Final Summary — You Can Use for Loop On:

Type Example
list AMIs
set SGs
map NLB target groups
object EC2 config
list(object) ALB listeners
map(object) EC2 instances
tuple mixed values
list(string) VPC endpoints
1. contains() — Check if a list has a value
✔ What it does:
Checks whether a given item exists inside a list.
✔ Syntax:
contains(list, value)
✔ Example:
contains([“t2.micro”, “t3.micro”], “t3.micro”)  # true
contains([“443”, “8443”], 80)                   # false
✔ Use in validation:
validation {
  condition     = contains([“dev”, “stage”, “prod”], var.env)
  error_message = “Invalid environment.”
}
⭐ 2. can() — Prevent errors (checks if Terraform can evaluate an expression)
✔ What it does:
Returns true if Terraform can safely evaluate the expression without error.
✔ Useful when:
Some values may be null
Types may not match
Value might not exist
You want validations to be safe
✔ Example:
❌ If we do this:
var.port > 0
If port = “abc” → Terraform crashes.
✔ Using can():
can(var.port > 0)
Returns false instead of crashing.
✔ Used in validation:
validation {
  condition = alltrue([
    for rule in var.alb_ingress_rules :
    can(rule.port) && rule.port >= 1 && rule.port <= 65535
  ])
}
This ensures:
rule.port is a number
It does NOT crash Terraform
⭐ 3. lookup() — Safely retrieve values from a map
✔ What it does:
Extracts a value from a map without failing if the key doesn’t exist.
✔ Syntax:
lookup(map, key, default)
✔ Example:
lookup({dev = “t3.micro”, prod = “m5.large”}, “dev”, “notfound”)
# result: “t3.micro”
lookup({dev = “t3.micro”}, “stage”, “default-type”)
# result: “default-type”
✔ Use inside validations:
Sometimes used to avoid key errors:
lookup(var.allowed_types, var.env, [])  # list fallback
⭐ 4. regex() — Pattern matching
✔ What it does:
Matches a string using regular expression.
✔ Syntax:
regex(pattern, string)
✔ Example:
Validate EC2 instance family:
regex(“^t3\\.”, var.instance_type)  # true for t3.micro, t3.large
✔ Used in validations:
validation {
  condition     = can(regex(“^ami-[0-9a-f]+$”, var.ami_id))
  error_message = “Invalid AMI ID”
}
⭐ 5. alltrue() — All conditions must be true
✔ What it does:
If every element in a list is true, returns true.
✔ Example:
alltrue([true, true, true])   # true
alltrue([true, false, true])  # false
✔ Used in list validations:
validation {
  condition = alltrue([
    for p in var.alb_ports : contains([443, 8443], p)
  ])
}
⭐ 6. anytrue() — At least one must be true
✔ What it does:
Checks whether one or more elements are true.
✔ Example:
anytrue([false, false, true])  # true
anytrue([false, false])        # false
⭐ 7. startswith() & endswith()
Very simple string checks.
✔ Examples:
startswith(“prod-app”, “prod”)   # true
endswith(“file.txt”, “.txt”)     # true
✔ Use case: Validate naming conventions
validation {
  condition     = startswith(var.bucket_name, “myorg-“)
  error_message = “Bucket names must start with myorg-“
}
⭐ 8. length() — Count items
✔ Example:
length([“a”,”b”,”c”])   # 3
length(“hello”)         # 5
✔ In validation:
Check list is not empty:
length(var.subnets) > 0
⭐ 9. for Expressions inside validations
Terraform allows loops inside validations.
✔ Example:
[for p in var.ports : p if p > 1024]
You can count invalid values:
length([
  for p in var.ports : p
  if !(contains([443,8443], p))
]) == 0
This ensures all ports are valid.
⭐ 10. toset() / tolist()
Used for type conversion.
✔ Example:
toset([“a”,”b”,”b”])  # {“a”,”b”} — duplicates removed
tolist({“a”,”b”})     # [“a”,”b”] — converts set → list
Useful when validating unique values.
🚀 Summary Table
Function    Simple Meaning  Best Used For
contains()  Check if item exists    allow-list validations
can()   Prevent Terraform crash type safety validations
lookup()    Safe map access environment → values
regex() Pattern match   AMI ID, naming rules
alltrue()   All checks must be true list validation
anytrue()   At least one true   OR logic
startswith() / endswith()   Prefix/suffix checks    naming standards
length()    Count elements  non-empty lists
for expressions Loop inside validation  complex rules

1. Environment Variable With Default + Validation

variable "environment" {
type = string
default = "dev"
validation {
condition = contains([“dev”, “stage”, “prod”], var.environment)
error_message = “Environment must be one of: dev, stage, prod.”
}
}

✔ Default is dev
✔ Only dev, stage, prod allowed


🚀 2. EC2 Instance Variable With Default + Validation (per environment)

Allowed instance types per env:

  • dev: t3.micro, t3.small

  • stage: t3.medium, t3.large

  • prod: m5.large, m5.xlarge


variable "ec2_instance" {
type = object({
instance_type = string
subnet_id = string
})
default = {
instance_type = “t3.micro”
subnet_id = “subnet-dev-a”
}

validation {
condition = contains(
lookup(
{
dev = [“t3.micro”, “t3.small”]
stage = [“t3.medium”, “t3.large”]
prod = [“m5.large”, “m5.xlarge”]
},
var.environment
),
var.ec2_instance.instance_type
)

error_message = “EC2 instance type is not allowed for the selected environment.”
}
}

What this does:

✔ Default EC2 instance type = t3.micro
✔ Automatically valid for default environment = dev
✔ If environment = prod, user MUST choose m5.large or m5.xlarge
✔ Prevents invalid instance types in prod


🚀 3. ALB Security Group Ingress Ports — Default + Validation

Only 443 and 8443 are allowed.

variable "alb_ingress_ports" {
type = list(number)
default = [443]

validation {
condition = alltrue([
for port in var.alb_ingress_ports :
contains([443, 8443], port)
])
error_message = “Only ports 443 and 8443 are allowed in ALB security group.”
}
}

What this does:

✔ Default port = 443 (industry standard)
✔ If user adds 80, Terraform will throw an error


🚀 4. Optional: Local Block (Allowed Instance Matrix)

(Makes code even cleaner)

locals {
allowed_instance_types = {
dev = ["t3.micro", "t3.small"]
stage = ["t3.medium", "t3.large"]
prod = ["m5.large", "m5.xlarge"]
}
}

Then replace the long validation condition with:

validation {
condition = contains(local.allowed_instance_types[var.environment], var.ec2_instance.instance_type)
error_message = "Instance type does not match the allowed types for environment."
}

Much cleaner. 💎


⭐ Final Version (All Variables Together)

variable "environment" {
type = string
default = "dev"
validation {
condition = contains([“dev”, “stage”, “prod”], var.environment)
error_message = “Environment must be one of: dev, stage, prod.”
}
}

variable “ec2_instance” {
type = object({
instance_type = string
subnet_id = string
})

default = {
instance_type = “t3.micro”
subnet_id = “subnet-dev-a”
}

validation {
condition = contains(
lookup(
{
dev = [“t3.micro”, “t3.small”]
stage = [“t3.medium”, “t3.large”]
prod = [“m5.large”, “m5.xlarge”]
},
var.environment
),
var.ec2_instance.instance_type
)

error_message = “EC2 instance type is not allowed for this environment.”
}
}

variable “alb_ingress_ports” {
type = list(number)

default = [443]

validation {
condition = alltrue([
for port in var.alb_ingress_ports :
contains([443, 8443], port)
])
error_message = “Only ports 443 and 8443 are allowed in ALB security group.”
}
}


🎯 You Now Have:

✅ Default values
✅ Complete environment-based EC2 validation
✅ Complete ALB port validation
✅ Cloud-ready variable structure

1. Advanced Validation — EC2 Instance Types by Environment
We now validate:
Only dev can use small instance types
Stage has medium
Prod has large
Also enforce instance type family (t3, m5, c6i, etc.)
Default value included
Variables
variable “env” {
  description = “Environment name”
  type        = string
  default     = “dev”
  validation {
    condition     = contains([“dev”, “stage”, “prod”], var.env)
    error_message = “Environment must be one of: dev, stage, prod.”
  }
}
EC2 Instance Type with Advanced Validation
variable “ec2_instance_type” {
  description = “EC2 instance type per environment”
  type        = string
  default     = “t3.micro”
  validation {
    condition = (
      (
        var.env == “dev” &&
        contains([“t2.micro”, “t3.micro”, “t3.small”], var.ec2_instance_type)
      )
      ||
      (
        var.env == “stage” &&
        contains([“t3.medium”, “t3.large”, “m5.large”], var.ec2_instance_type)
      )
      ||
      (
        var.env == “prod” &&
        contains([“m5.large”, “m5.xlarge”, “c6i.large”, “c6i.xlarge”], var.ec2_instance_type)
      )
    )
    error_message = “Invalid EC2 type for environment.
    DEV allowed: t2.micro, t3.micro, t3.small
    STAGE allowed: t3.medium, t3.large, m5.large
    PROD allowed: m5.large, m5.xlarge, c6i.large, c6i.xlarge.”
  }
}
✅ 2. Advanced Validation — ALB SG Allowed Ports
Now we’ll enforce:
Only 443, 8443 allowed for ALB
Validate list of ports
Default ports included
Ensure numeric ranges and type safety with can()
Variable
variable “alb_allowed_ports” {
  description = “Security group ports for ALB ingress”
  type        = list(number)
  default     = [443, 8443]
  validation {
    condition = length([
      for p in var.alb_allowed_ports : p if !(contains([443, 8443], p))
    ]) == 0
    error_message = “ALB can only allow ports 443 and 8443.”
  }
}
✔ This ensures every element is valid
✔ Invalid ports instantly fail
🔥 Advanced: Port Range + Protocol Validation
A more complex version checking:
Only valid TCP ports
Must be within range
Must be integers
variable “alb_ingress_rules” {
  description = “ALB ingress rules with port and protocol”
  type = list(object({
    port     = number
    protocol = string
  }))
  default = [
    { port = 443,  protocol = “tcp” },
    { port = 8443, protocol = “tcp” }
  ]
  validation {
    condition = alltrue([
      for rule in var.alb_ingress_rules :
      (
        can(rule.port)
        && rule.port >= 1
        && rule.port <= 65535
        && contains([443, 8443], rule.port)
        && lower(rule.protocol) == “tcp”
      )
    ])
    error_message = “Each ALB ingress rule must use port 443 or 8443 and protocol must be TCP.”
  }
}
✅ 3. Advanced Validation — VPC Endpoint Types
Validate:
Interface endpoints only allowed for: s3, logs, secretsmanager, sqs
Gateway endpoints only for s3 & dynamodb
Variable
variable “vpc_endpoint_service” {
  description = “AWS service for VPC endpoint”
  type        = string
  default     = “s3”
  validation {
    condition = (
      (var.vpc_endpoint_service == “s3” || var.vpc_endpoint_service == “dynamodb”)
      ||
      contains([
        “logs”,
        “secretsmanager”,
        “sqs”,
        “kms”,
        “ec2messages”
      ], var.vpc_endpoint_service)
    )
    error_message = “Invalid VPC endpoint service. Gateway allowed: s3, dynamodb.
                     Interface allowed: logs, secretsmanager, sqs, kms, ec2messages.”
  }
}
🔥 Advanced Cross-Validation: Endpoint Type + Service
E.g.,
if type = gateway → only s3 or dynamodb
if type = interface → others allowed
variable “vpc_endpoint_type” {
  type    = string
  default = “Interface”
  validation {
    condition     = contains([“Gateway”, “Interface”], var.vpc_endpoint_type)
    error_message = “vpc_endpoint_type must be Gateway or Interface.”
  }
}
variable “vpc_endpoint_service” {
  type    = string
  default = “s3”
  validation {
    condition = (
      (var.vpc_endpoint_type == “Gateway” &&
       contains([“s3”, “dynamodb”], var.vpc_endpoint_service))
      ||
      (var.vpc_endpoint_type == “Interface” &&
       contains([“logs”, “secretsmanager”, “sqs”, “kms”], var.vpc_endpoint_service))
    )
    error_message = “Service does not match endpoint type.”
  }
}
✅ 4. Advanced Validation — EFS & EBS Configuration
We enforce:
EFS throughput must be one of valid options
EBS volume size must be between 8–16384
EBS type must match allowed family
EBS validation
variable “ebs_volume” {
  type = object({
    size = number
    type = string
  })
  default = {
    size = 20
    type = “gp3”
  }
  validation {
    condition = (
      var.ebs_volume.size >= 8 &&
      var.ebs_volume.size <= 16384 &&
      contains([“gp2”, “gp3”, “io1”, “sc1”, “st1”], var.ebs_volume.type)
    )
    error_message = “EBS size must be 8–16384 GiB and type must be one of gp2,gp3,io1,sc1,st1.”
  }
}
EFS Validation
variable “efs_settings” {
  type = object({
    performance_mode = string
    throughput_mode  = string
  })
  default = {
    performance_mode = “generalPurpose”
    throughput_mode  = “bursting”
  }
  validation {
    condition = (
      contains([“generalPurpose”, “maxIO”], var.efs_settings.performance_mode) &&
      contains([“bursting”, “provisioned”, “elastic”], var.efs_settings.throughput_mode)
    )
    error_message = “Invalid EFS configuration. Supported performance: generalPurpose, maxIO.
                     Throughput: bursting, provisioned, elastic.”
  }
}
🟦 5. OUTPUTS + LOCALS
Now we expose validated values.
Locals Example
locals {
  selected_instance     = var.ec2_instance_type
  alb_ports             = var.alb_allowed_ports
  endpoint_service_name = var.vpc_endpoint_service
}
Outputs
output “instance_type” {
  value = local.selected_instance
}
output “alb_ports” {
  value = local.alb_ports
}
output “endpoint_service” {
  value = local.endpoint_service_name
}

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to Top