AskLearn
Loading...
← Back to Terraform Course
AdvancedConfiguration

Built-in Functions

Terraform function library

Tutorial 22: Built-in Functions and Expressions

Learning Objectives

By the end of this tutorial, you will be able to:

  • Use Terraform's built-in functions for data manipulation
  • Implement complex expressions and transformations
  • Apply string, numeric, collection, and date functions effectively
  • Use type conversion and validation functions
  • Create reusable expression patterns for common operations

Prerequisites

  • Understanding of HCL syntax and expressions
  • Completed Tutorial 17: Local Values and Computed Values
  • Basic knowledge of data types and structures

Introduction

Terraform provides over 100 built-in functions that enable powerful data manipulation, transformation, and computation within your configurations. These functions allow you to create dynamic, flexible infrastructure code that adapts to changing requirements.

String Functions

Basic String Manipulation

# variables.tf
variable "project_name" {
  description = "Project name"
  type        = string
  default     = "my-awesome-project"
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "development"
}

variable "user_list" {
  description = "Comma-separated list of users"
  type        = string
  default     = "alice,bob,charlie,dave"
}

# main.tf
locals {
  # String formatting and manipulation
  resource_prefix = upper(replace(var.project_name, "-", "_"))
  env_short      = substr(var.environment, 0, 3)
  
  # String concatenation and formatting
  full_name = format("%s-%s", var.project_name, var.environment)
  
  # String case conversion
  project_upper = upper(var.project_name)
  project_lower = lower(var.project_name)
  project_title = title(replace(var.project_name, "-", " "))
  
  # String splitting and joining
  users = split(",", var.user_list)
  user_emails = [
    for user in local.users :
    format("%s@%s.com", user, replace(var.project_name, "-", ""))
  ]
  
  # String trimming and padding
  cleaned_name = trimspace(var.project_name)
  padded_env   = format("%04s", local.env_short)
  
  # String searching and replacing
  sanitized_name = replace(replace(var.project_name, " ", "-"), "_", "-")
  
  # Regular expressions
  is_valid_name = can(regex("^[a-z][a-z0-9-]*[a-z0-9]$", var.project_name))
  extracted_version = regex("v([0-9]+\\.[0-9]+\\.[0-9]+)", "app-v1.2.3-release")[0]
}

# Using string functions in resources
resource "aws_s3_bucket" "app" {
  bucket = "${local.sanitized_name}-${local.env_short}-${random_id.suffix.hex}"
  
  tags = {
    Name        = local.project_title
    Environment = title(var.environment)
    Prefix      = local.resource_prefix
  }
}

resource "random_id" "suffix" {
  byte_length = 4
}

# outputs.tf
output "string_examples" {
  description = "Examples of string function usage"
  value = {
    original_name    = var.project_name
    resource_prefix  = local.resource_prefix
    full_name       = local.full_name
    users           = local.users
    user_emails     = local.user_emails
    is_valid_name   = local.is_valid_name
    sanitized_name  = local.sanitized_name
  }
}

Advanced String Operations

# Template and formatting functions
locals {
  # Template rendering
  user_data_script = templatefile("${path.module}/user_data.tpl", {
    project_name = var.project_name
    environment  = var.environment
    users        = local.users
    config_data  = jsonencode({
      app_name = var.project_name
      env      = var.environment
      debug    = var.environment != "prod"
    })
  })
  
  # Advanced formatting
  instance_names = [
    for i in range(var.instance_count) :
    format("%s-%s-%02d", var.project_name, var.environment, i + 1)
  ]
  
  # String interpolation with conditionals
  bucket_policy = templatefile("${path.module}/bucket_policy.json", {
    bucket_name = aws_s3_bucket.app.bucket
    principals  = var.environment == "prod" ? 
      ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] :
      ["*"]
    readonly_access = var.environment != "prod"
  })
  
  # URL and path manipulation
  api_endpoint = format("https://%s.%s", 
    var.subdomain, 
    trimsuffix(var.domain_name, ".")
  )
  
  # Base64 encoding/decoding
  encoded_config = base64encode(jsonencode({
    database_url = format("postgres://%s:%s@%s:%d/%s",
      var.db_username,
      var.db_password,
      aws_db_instance.main.endpoint,
      aws_db_instance.main.port,
      var.db_name
    )
  }))
}

# user_data.tpl template file example:
# #!/bin/bash
# echo "Setting up ${project_name} in ${environment} environment"
# 
# # Configure users
# %{ for user in users ~}
# useradd -m ${user}
# %{ endfor ~}
# 
# # Write config
# cat > /etc/app/config.json << 'EOF'
# ${config_data}
# EOF
# 
# # Start services
# systemctl enable app
# systemctl start app

Numeric Functions

# variables.tf
variable "instance_counts" {
  description = "Instance counts per environment"
  type        = map(number)
  default = {
    dev     = 1
    staging = 2
    prod    = 5
  }
}

variable "cpu_limits" {
  description = "CPU limits in millicores"
  type        = list(number)
  default     = [100, 250, 500, 1000]
}

variable "memory_sizes" {
  description = "Memory sizes in MB"
  type        = list(number)
  default     = [128, 256, 512, 1024, 2048]
}

# main.tf
locals {
  # Basic arithmetic
  total_instances = sum(values(var.instance_counts))
  max_instances   = max(values(var.instance_counts)...)
  min_instances   = min(values(var.instance_counts)...)
  avg_instances   = local.total_instances / length(var.instance_counts)
  
  # Rounding and ceiling/floor
  cpu_cores = [
    for cpu_milli in var.cpu_limits :
    ceil(cpu_milli / 1000)
  ]
  
  memory_gb = [
    for memory_mb in var.memory_sizes :
    floor(memory_mb / 1024)
  ]
  
  # Mathematical operations
  fibonacci_sizes = [1, 1, 2, 3, 5, 8, 13, 21]
  scaled_sizes = [
    for size in local.fibonacci_sizes :
    pow(2, size)
  ]
  
  # Absolute values and sign
  cost_difference = abs(var.budget_limit - var.current_cost)
  
  # Logarithmic functions
  log_based_scaling = [
    for i in range(1, 10) :
    floor(log(i, 2))
  ]
  
  # Modulo operations for distribution
  az_distribution = [
    for i in range(local.total_instances) :
    data.aws_availability_zones.available.names[i % length(data.aws_availability_zones.available.names)]
  ]
}

# Resource sizing based on calculations
resource "aws_instance" "app" {
  count = var.instance_counts[var.environment]
  
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.cpu_cores[count.index % length(local.cpu_cores)] <= 1 ? "t3.micro" : "t3.small"
  
  availability_zone = local.az_distribution[count.index]
  
  tags = {
    Name      = "${var.project_name}-${count.index + 1}"
    CPUCores  = local.cpu_cores[count.index % length(local.cpu_cores)]
    MemoryGB  = local.memory_gb[count.index % length(local.memory_gb)]
  }
}

# Dynamic scaling calculations
locals {
  # Calculate target group weights based on instance counts
  environment_weights = {
    for env, count in var.instance_counts :
    env => floor((count / local.total_instances) * 100)
  }
  
  # Calculate storage requirements
  storage_per_instance = 20  # GB
  total_storage = local.total_instances * local.storage_per_instance
  
  # Cost estimation
  hourly_cost_per_instance = 0.0464  # t3.micro cost
  monthly_cost = local.total_instances * local.hourly_cost_per_instance * 24 * 30
}

data "aws_availability_zones" "available" {
  state = "available"
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
}

Collection Functions

List Operations

# variables.tf
variable "allowed_cidrs" {
  description = "List of allowed CIDR blocks"
  type        = list(string)
  default     = ["10.0.0.0/16", "172.16.0.0/12", "192.168.0.0/16"]
}

variable "environments" {
  description = "Environment configurations"
  type = list(object({
    name         = string
    instance_type = string
    replicas     = number
    enabled      = bool
  }))
  default = [
    {
      name         = "dev"
      instance_type = "t3.micro"
      replicas     = 1
      enabled      = true
    },
    {
      name         = "staging"
      instance_type = "t3.small"
      replicas     = 2
      enabled      = true
    },
    {
      name         = "prod"
      instance_type = "t3.medium"
      replicas     = 3
      enabled      = false
    }
  ]
}

# main.tf
locals {
  # List length and element access
  cidr_count = length(var.allowed_cidrs)
  first_cidr = element(var.allowed_cidrs, 0)
  last_cidr  = element(var.allowed_cidrs, length(var.allowed_cidrs) - 1)
  
  # List filtering and transformation
  enabled_environments = [
    for env in var.environments :
    env if env.enabled
  ]
  
  production_environments = [
    for env in var.environments :
    env if env.name == "prod"
  ]
  
  # List slicing and chunking
  first_two_cidrs = slice(var.allowed_cidrs, 0, 2)
  
  # List concatenation and flattening
  all_cidrs = concat(var.allowed_cidrs, ["203.0.113.0/24"])
  
  nested_lists = [
    ["web1", "web2"],
    ["api1", "api2", "api3"],
    ["db1"]
  ]
  all_services = flatten(local.nested_lists)
  
  # List sorting and reversing
  sorted_cidrs = sort(var.allowed_cidrs)
  reversed_envs = reverse([for env in var.environments : env.name])
  
  # List deduplication
  unique_instance_types = distinct([
    for env in var.environments : env.instance_type
  ])
  
  # List indexing and contains
  has_dev_env = contains([for env in var.environments : env.name], "dev")
  dev_env_index = index([for env in var.environments : env.name], "dev")
  
  # List chunking for subnet distribution
  az_count = length(data.aws_availability_zones.available.names)
  subnets_per_az = chunklist(range(24), local.az_count)
}

# Create subnets using list functions
resource "aws_subnet" "app" {
  count = length(local.enabled_environments) * local.az_count
  
  vpc_id            = aws_vpc.main.id
  availability_zone = element(data.aws_availability_zones.available.names, count.index % local.az_count)
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 1)
  
  tags = {
    Name         = "${var.project_name}-subnet-${count.index + 1}"
    Environment  = element([for env in local.enabled_environments : env.name], floor(count.index / local.az_count))
    AZ           = element(data.aws_availability_zones.available.names, count.index % local.az_count)
  }
}

Map Operations

# variables.tf
variable "service_configs" {
  description = "Service configuration map"
  type = map(object({
    port         = number
    protocol     = string
    health_path  = string
    replicas     = number
    cpu_request  = string
    memory_request = string
  }))
  default = {
    frontend = {
      port           = 80
      protocol       = "HTTP"
      health_path    = "/health"
      replicas       = 2
      cpu_request    = "100m"
      memory_request = "128Mi"
    }
    backend = {
      port           = 8080
      protocol       = "HTTP"
      health_path    = "/api/health"
      replicas       = 3
      cpu_request    = "200m"
      memory_request = "256Mi"
    }
    database = {
      port           = 5432
      protocol       = "TCP"
      health_path    = ""
      replicas       = 1
      cpu_request    = "500m"
      memory_request = "1Gi"
    }
  }
}

# main.tf
locals {
  # Map keys and values
  service_names = keys(var.service_configs)
  service_configs_list = values(var.service_configs)
  
  # Map lookup with defaults
  frontend_config = lookup(var.service_configs, "frontend", {
    port = 80
    protocol = "HTTP"
    health_path = "/"
    replicas = 1
    cpu_request = "100m"
    memory_request = "128Mi"
  })
  
  # Map merging
  default_config = {
    timeout = 30
    retries = 3
    enabled = true
  }
  
  enhanced_configs = {
    for name, config in var.service_configs :
    name => merge(local.default_config, config, {
      full_name = "${var.project_name}-${name}"
      port_name = "${name}-port"
    })
  }
  
  # Map filtering and transformation
  http_services = {
    for name, config in var.service_configs :
    name => config if config.protocol == "HTTP"
  }
  
  high_replica_services = {
    for name, config in var.service_configs :
    name => config if config.replicas > 2
  }
  
  # Map to list transformations
  service_ports = [
    for name, config in var.service_configs :
    {
      service = name
      port    = config.port
      protocol = config.protocol
    }
  ]
  
  # Nested map operations
  service_env_matrix = {
    for service_name in local.service_names :
    service_name => {
      for env in ["dev", "staging", "prod"] :
      env => merge(var.service_configs[service_name], {
        replicas = env == "prod" ? var.service_configs[service_name].replicas : 1
        resources = {
          cpu = env == "prod" ? "500m" : var.service_configs[service_name].cpu_request
          memory = env == "prod" ? "512Mi" : var.service_configs[service_name].memory_request
        }
      })
    }
  }
}

# Create target groups using map functions
resource "aws_lb_target_group" "services" {
  for_each = local.http_services
  
  name     = "${var.project_name}-${each.key}-tg"
  port     = each.value.port
  protocol = each.value.protocol
  vpc_id   = aws_vpc.main.id
  
  health_check {
    enabled             = true
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 5
    interval            = 30
    path                = each.value.health_path
    matcher             = "200"
  }
  
  tags = {
    Name    = "${var.project_name}-${each.key}"
    Service = each.key
    Port    = each.value.port
  }
}

Date and Time Functions

# Date and time functions
locals {
  # Current timestamp
  deployment_time = timestamp()
  
  # Formatted timestamps
  deployment_date = formatdate("YYYY-MM-DD", local.deployment_time)
  deployment_hour = formatdate("hh", local.deployment_time)
  
  # Time-based logic
  is_business_hours = tonumber(formatdate("hh", local.deployment_time)) >= 9 && 
                     tonumber(formatdate("hh", local.deployment_time)) <= 17
  
  is_weekend = contains(["Saturday", "Sunday"], formatdate("EEEE", local.deployment_time))
  
  # Date arithmetic for retention
  backup_retention_date = timeadd(local.deployment_time, "-${var.backup_retention_days * 24}h")
  
  # Time-based resource naming
  timestamped_name = "${var.project_name}-${formatdate("YYYY-MM-DD-hhmm", local.deployment_time)}"
  
  # Scheduled operations
  maintenance_window = var.environment == "prod" ? "sun:03:00-sun:04:00" : "sat:02:00-sat:03:00"
  backup_window = formatdate("hh:mm-", timeadd(local.deployment_time, "2h"))
}

# Resources with time-based configuration
resource "aws_db_instance" "main" {
  identifier = "${var.project_name}-db"
  
  engine         = "mysql"
  engine_version = "8.0"
  instance_class = "db.t3.micro"
  
  allocated_storage = 20
  
  db_name  = var.db_name
  username = var.db_username
  password = var.db_password
  
  backup_retention_period = var.backup_retention_days
  backup_window          = "${formatdate("hh:mm", timeadd(local.deployment_time, "3h"))}-${formatdate("hh:mm", timeadd(local.deployment_time, "4h"))}"
  maintenance_window     = local.maintenance_window
  
  # Time-based final snapshot naming
  skip_final_snapshot       = false
  final_snapshot_identifier = "${var.project_name}-final-${formatdate("YYYY-MM-DD-hhmm", local.deployment_time)}"
  
  tags = {
    Name         = "${var.project_name}-database"
    CreatedAt    = local.deployment_date
    CreatedHour  = local.deployment_hour
    BusinessHours = local.is_business_hours
  }
}

Type Conversion and Validation Functions

# variables.tf
variable "mixed_config" {
  description = "Mixed configuration values"
  type = map(any)
  default = {
    instance_count = "3"
    enable_monitoring = "true"
    cpu_threshold = "80.5"
    tags = "Name=test,Environment=dev"
    ports = "80,443,8080"
  }
}

# main.tf
locals {
  # Type conversion functions
  instance_count = tonumber(var.mixed_config.instance_count)
  enable_monitoring = tobool(var.mixed_config.enable_monitoring)
  cpu_threshold = tonumber(var.mixed_config.cpu_threshold)
  
  # String to list conversion
  port_list = [
    for port in split(",", var.mixed_config.ports) :
    tonumber(port)
  ]
  
  # String to map conversion
  tag_pairs = split(",", var.mixed_config.tags)
  parsed_tags = {
    for pair in local.tag_pairs :
    split("=", pair)[0] => split("=", pair)[1]
  }
  
  # Type validation and conversion
  validated_config = {
    instance_count = can(tonumber(var.mixed_config.instance_count)) ? 
      max(1, min(10, tonumber(var.mixed_config.instance_count))) : 1
    
    enable_monitoring = can(tobool(var.mixed_config.enable_monitoring)) ? 
      tobool(var.mixed_config.enable_monitoring) : false
    
    cpu_threshold = can(tonumber(var.mixed_config.cpu_threshold)) ? 
      max(0, min(100, tonumber(var.mixed_config.cpu_threshold))) : 80
  }
  
  # Complex type conversions
  service_list = tolist([
    {
      name = "web"
      port = 80
    },
    {
      name = "api"
      port = 8080
    }
  ])
  
  service_map = {
    for service in local.service_list :
    service.name => service
  }
  
  # JSON encoding/decoding
  config_json = jsonencode({
    instance_count = local.validated_config.instance_count
    monitoring = local.validated_config.enable_monitoring
    threshold = local.validated_config.cpu_threshold
    ports = local.port_list
    tags = local.parsed_tags
  })
  
  parsed_config = jsondecode(local.config_json)
  
  # YAML encoding/decoding (if yaml functions are available)
  config_yaml = yamlencode({
    services = local.service_map
    config = local.validated_config
  })
}

# Validation functions
locals {
  # Input validation
  valid_cidr = can(cidrhost(var.vpc_cidr, 0))
  valid_email = can(regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", var.admin_email))
  valid_instance_type = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
  
  # Conditional values based on validation
  final_vpc_cidr = local.valid_cidr ? var.vpc_cidr : "10.0.0.0/16"
  final_instance_type = local.valid_instance_type ? var.instance_type : "t3.micro"
  
  # Try/catch equivalent using can()
  safe_division = can(var.numerator / var.denominator) ? var.numerator / var.denominator : 0
  
  # Complex validation patterns
  validation_results = {
    cidr_valid = local.valid_cidr
    email_valid = local.valid_email
    instance_type_valid = local.valid_instance_type
    all_valid = local.valid_cidr && local.valid_email && local.valid_instance_type
  }
}

Advanced Function Combinations

# Complex data transformations
locals {
  # Multi-step data processing
  raw_user_data = "alice:admin,bob:user,charlie:admin,dave:user"
  
  # Step 1: Split into pairs
  user_pairs = split(",", local.raw_user_data)
  
  # Step 2: Parse each pair
  user_roles = {
    for pair in local.user_pairs :
    split(":", pair)[0] => split(":", pair)[1]
  }
  
  # Step 3: Group by role
  users_by_role = {
    for role in distinct(values(local.user_roles)) :
    role => [
      for user, user_role in local.user_roles :
      user if user_role == role
    ]
  }
  
  # Step 4: Generate policies
  role_policies = {
    for role, users in local.users_by_role :
    role => templatefile("${path.module}/policies/${role}_policy.json", {
      users = users
      resources = role == "admin" ? ["*"] : ["arn:aws:s3:::${var.project_name}-*"]
    })
  }
  
  # Complex subnet calculation
  vpc_cidr = "10.0.0.0/16"
  az_count = length(data.aws_availability_zones.available.names)
  
  # Calculate subnet distribution
  subnet_configs = flatten([
    for tier_index, tier in ["public", "private", "database"] : [
      for az_index in range(local.az_count) : {
        name = "${tier}-${az_index + 1}"
        cidr = cidrsubnet(
          local.vpc_cidr,
          8,  # Additional bits for subnet
          tier_index * local.az_count + az_index + 1
        )
        availability_zone = data.aws_availability_zones.available.names[az_index]
        tier = tier
        index = az_index
        route_table = tier == "public" ? "public" : "private"
      }
    ]
  ])
  
  # Convert to map for for_each
  subnets = {
    for subnet in local.subnet_configs :
    subnet.name => subnet
  }
  
  # Generate security group rules dynamically
  application_ports = [80, 443, 8080, 8443]
  database_ports = [3306, 5432, 27017]
  
  security_rules = flatten([
    # Application tier rules
    for port in local.application_ports : [
      {
        type        = "ingress"
        from_port   = port
        to_port     = port
        protocol    = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
        description = "Allow ${port} from anywhere"
        tier        = "application"
      }
    ],
    # Database tier rules
    for port in local.database_ports : [
      {
        type        = "ingress"
        from_port   = port
        to_port     = port
        protocol    = "tcp"
        cidr_blocks = [local.vpc_cidr]
        description = "Allow ${port} from VPC"
        tier        = "database"
      }
    ]
  ])
  
  # Group rules by tier
  rules_by_tier = {
    for tier in ["application", "database"] :
    tier => [
      for rule in local.security_rules :
      rule if rule.tier == tier
    ]
  }
}

# Create resources using complex functions
resource "aws_subnet" "main" {
  for_each = local.subnets
  
  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.availability_zone
  
  map_public_ip_on_launch = each.value.tier == "public"
  
  tags = {
    Name = "${var.project_name}-${each.key}"
    Tier = each.value.tier
    AZ   = each.value.availability_zone
  }
}

# IAM policies using complex functions
resource "aws_iam_policy" "role_policies" {
  for_each = local.role_policies
  
  name        = "${var.project_name}-${each.key}-policy"
  description = "Policy for ${each.key} role"
  policy      = each.value
  
  tags = {
    Role = each.key
    Users = join(",", local.users_by_role[each.key])
  }
}

Performance and Optimization

# Optimized function usage
locals {
  # Avoid repeated expensive calculations
  availability_zones = data.aws_availability_zones.available.names
  az_count = length(local.availability_zones)
  
  # Pre-calculate commonly used values
  environment_multipliers = {
    dev     = 1
    staging = 2
    prod    = 3
  }
  
  base_instance_count = 2
  scaled_instance_count = local.base_instance_count * local.environment_multipliers[var.environment]
  
  # Efficient list processing
  instance_configs = [
    for i in range(local.scaled_instance_count) : {
      name = format("%s-%s-%02d", var.project_name, var.environment, i + 1)
      az   = local.availability_zones[i % local.az_count]
      size = i < 2 ? "small" : "medium"  # First 2 are small, rest are medium
    }
  ]
  
  # Batch operations
  all_tags = merge(
    var.common_tags,
    {
      Environment = var.environment
      Project     = var.project_name
      ManagedBy   = "terraform"
      CreatedAt   = formatdate("YYYY-MM-DD", timestamp())
    }
  )
}

Key Takeaways

  1. String Manipulation: Use string functions for formatting, validation, and transformation
  2. Numeric Calculations: Apply math functions for scaling, distribution, and resource sizing
  3. Collection Operations: Leverage list and map functions for data transformation
  4. Type Safety: Use type conversion and validation functions for robust configurations
  5. Performance: Pre-calculate expensive operations and reuse results
  6. Complex Logic: Combine multiple functions for sophisticated data processing

Next Steps

  • Tutorial 23: Learn about conditional expressions and dynamic blocks
  • Practice combining multiple functions for complex transformations
  • Experiment with template functions for configuration generation
  • Review the Terraform Functions Documentation

Additional Resources