LAB24_AWS_Input Variables: Validation Rules, Types, and Sensitive Values

Input Variables: Validation Rules, Types, and Sensitive Values

☁ AWS ⭐ Beginner Variable Validation Type Constraints Sensitive Variables tfvars locals

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.

Your Objectives
  • 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"
✓ Expected Output After Apply
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