Input Variables: Validation Rules, Types, and Sensitive Values
Scenario
Your Terraform modules are being used by multiple teams who occasionally pass invalid values — wrong environment names, invalid CIDR blocks, non-positive replica counts. Rather than letting Terraform apply with bad values and failing at the AWS API level with cryptic errors, you want to use input variable validation to catch mistakes early with clear, human-readable error messages.
Build a module that provisions an EC2 instance with fully validated, typed, and safe inputs.
- Define variables with complex types: list(string), map(string), and object({...}).
- Add validation blocks to: restrict environment to a known set, validate CIDR format with can(cidrhost(...)), and ensure replica count is between 1 and 10.
- Mark a db_password variable as sensitive = true so it never appears in CLI output.
- Use locals to compute derived values (e.g., full instance name) rather than repeating logic in multiple places.
- Provide a sample dev.tfvars showing how variables are supplied.
Additional Context
The can() function returns true if an expression succeeds without error. Combined with functions like cidrhost() or regex(), it lets you write validation rules that test value format. For example, can(cidrhost(var.vpc_cidr, 0)) returns true only if var.vpc_cidr is a valid CIDR notation.
locals are expressions computed once and reused. They help avoid duplication and keep resources clean — instead of writing "${var.project}-${var.environment}-web" in 10 places, define it once in locals and reference local.instance_name.
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}variables.tf
# variables.tf — full demonstration of variable features
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "project" {
type = string
description = "Short project name used in all resource names."
validation {
condition = length(var.project) <= 12
error_message = "Project name must be 12 characters or fewer to fit AWS naming limits."
}
}
variable "environment" {
type = string
description = "Deployment environment. Must be one of: dev, staging, prod."
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be 'dev', 'staging', or 'prod'."
}
}
variable "vpc_cidr" {
type = string
description = "CIDR block for the VPC."
default = "10.0.0.0/16"
validation {
# can() returns true if the expression succeeds without error.
# cidrhost() throws if the CIDR is invalid, so can() wraps it safely.
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "vpc_cidr must be a valid IPv4 CIDR block, e.g., 10.0.0.0/16."
}
}
variable "replica_count" {
type = number
description = "Number of replicas. Must be between 1 and 10."
default = 1
validation {
condition = var.replica_count >= 1
error_message = "replica_count must be at least 1."
}
validation {
condition = var.replica_count <= 10
error_message = "replica_count cannot exceed 10 (cost and quota limits)."
}
}
# Sensitive variable — value never shown in plan/apply output or logs.
variable "db_password" {
type = string
sensitive = true
description = "Database password. Never shown in CLI output."
}
# Object type — groups related settings into a structured variable.
variable "instance_config" {
type = object({
instance_type = string
volume_size = number
encrypted = bool
})
default = {
instance_type = "t3.micro"
volume_size = 20
encrypted = true
}
validation {
condition = var.instance_config.volume_size >= 8
error_message = "Root volume must be at least 8 GB."
}
}
# Map type — flexible key/value tags.
variable "extra_tags" {
type = map(string)
description = "Additional tags to apply to all resources."
default = {}
}locals.tf
# locals.tf — computed values derived from variables.
# Define once, reference everywhere — avoids duplication.
locals {
# Full instance name: used in resource Names and tags.
instance_name = "${var.project}-${var.environment}-web"
# Merge extra tags with mandatory tags — extra_tags can override.
common_tags = merge({
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}, var.extra_tags)
# Is this a production environment? Used in conditional expressions.
is_prod = var.environment == "prod"
}main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
data "aws_ami" "amazon_linux_2" {
most_recent = true
owners = ["amazon"]
filter { name = "name"; values = ["amzn2-ami-hvm-*-x86_64-gp2"] }
}
resource "aws_instance" "web" {
count = var.replica_count # create the validated number of instances
ami = data.aws_ami.amazon_linux_2.id
instance_type = var.instance_config.instance_type
root_block_device {
volume_size = var.instance_config.volume_size
encrypted = var.instance_config.encrypted
}
# Use local.instance_name — computed once, consistent everywhere.
tags = merge(local.common_tags, {
Name = "${local.instance_name}-${count.index}"
})
}dev.tfvars
# dev.tfvars — supply this file with: terraform apply -var-file="dev.tfvars"
project = "myapp"
environment = "dev"
vpc_cidr = "10.10.0.0/16"
replica_count = 2
db_password = "change-me-in-ci" # in real CI, inject via TF_VAR_db_password
instance_config = {
instance_type = "t3.micro"
volume_size = 20
encrypted = true
}
extra_tags = {
CostCenter = "engineering-001"
Owner = "platform-team"
}outputs.tf
output "instance_ids" { value = aws_instance.web[*].id }
output "instance_name" { value = local.instance_name }
output "common_tags" { value = local.common_tags }
output "is_prod" { value = local.is_prod }Workflow Commands
terraform init
# Apply with the dev tfvars file
terraform apply -var-file="dev.tfvars"
# Test validation: pass an invalid environment
terraform plan -var="environment=qa" -var-file="dev.tfvars"
# Error: Environment must be 'dev', 'staging', or 'prod'.
# Test validation: pass an invalid CIDR
terraform plan -var="vpc_cidr=not-a-cidr" -var-file="dev.tfvars"
# Error: vpc_cidr must be a valid IPv4 CIDR block...
# Pass sensitive password via environment variable (recommended in CI)
export TF_VAR_db_password="my-secure-password"
terraform apply -var-file="dev.tfvars"
terraform destroy -var-file="dev.tfvars"Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
common_tags = { CostCenter = "engineering-001", Environment = "dev", ... }
instance_ids = ["i-0abc...", "i-0def..."]
instance_name = "myapp-dev-web"
is_prod = false
