Introducción

Terraform no es un lenguaje de programación de propósito general, pero incluye un conjunto rico de funciones integradas que permiten transformar, manipular y calcular valores dentro de tus configuraciones HCL. Estas funciones son esenciales para escribir configuraciones dinámicas, reutilizables y elegantes sin necesidad de duplicar valores o escribir lógica compleja.

En esta lección estudiaremos todas las categorías de funciones disponibles, sus casos de uso más importantes y cómo combinarlas para resolver problemas reales de infraestructura.


  1. ¿Qué son las Funciones en Terraform?

Las funciones en Terraform son expresiones que reciben uno o más argumentos y devuelven un resultado. Se invocan con la sintaxis:

nombre_funcion(argumento1, argumento2, ...)

Características importantes

  • Son puras: Dada la misma entrada, siempre devuelven la misma salida. No tienen efectos secundarios.
  • No puedes definir funciones propias: A diferencia de lenguajes de programación, Terraform no permite crear funciones personalizadas (aunque sí puedes usar locals para encapsular expresiones).
  • Se evalúan en tiempo de planificación: Las funciones se ejecutan cuando Terraform planifica los cambios, no cuando aplica.
  • Tipado estricto: Cada función espera tipos específicos de argumentos.

  1. Cómo Probar Funciones con terraform console

Antes de usar una función en tu código, puedes probarla de forma interactiva con terraform console. Este comando abre una consola REPL (Read-Eval-Print Loop) donde puedes evaluar expresiones de Terraform.

# Abrimos la consola de Terraform
terraform console

Una vez dentro de la consola:

# Probamos funciones directamente
> upper("hola mundo")
"HOLA MUNDO"

> length(["a", "b", "c"])
3

> format("web-%s-%03d", "prod", 5)
"web-prod-005"

# Para salir de la consola
> exit

Consejo: Usa terraform console siempre que no estés seguro de cómo se comporta una función. Es mucho más rápido que escribir código, hacer plan y revisar errores.


  1. Categorías de Funciones

Terraform organiza sus funciones en varias categorías. A continuación presentamos una tabla resumen y luego exploraremos cada categoría en detalle.

Tabla resumen de categorías

Categoría Funciones principales Uso típico
String format, join, split, replace, upper, lower, trimspace, substr, startswith, endswith Manipulación de texto, nombres de recursos
Numérica abs, ceil, floor, max, min, pow, signum, parseint Cálculos matemáticos, límites
Colección length, keys, values, merge, flatten, distinct, concat, lookup, element, toset, tolist, tomap, zipmap, contains, index Manipulación de listas y mapas
Fecha/Hora timestamp, formatdate, timeadd Timestamps, expiración de recursos
Hash/Codificación base64encode, base64decode, jsonencode, jsondecode, yamlencode, yamldecode, md5, sha256, urlencode Codificación de datos, generación de IDs únicos
Filesystem file, filebase64, templatefile, fileset, filemd5 Lectura de archivos, plantillas
Red cidrsubnet, cidrhost, cidrnetmask, cidrrange, cidrcontains Cálculo automático de subredes IP
Tipo tostring, tonumber, tobool, type Conversión explícita de tipos

  1. Funciones de String

format()

Produce una cadena formateada al estilo printf. Muy útil para generar nombres de recursos consistentes.

# Ejemplo: Generar nombres de recursos con formato estandarizado
locals {
  # format(plantilla, arg1, arg2, ...)
  # %s = string, %d = entero, %f = float, %03d = entero con 3 dígitos y ceros a la izquierda
  bucket_name = format("%s-%s-%s", var.project, var.environment, "assets")
  # Resultado: "myapp-prod-assets"

  instance_name = format("web-%s-%03d", var.environment, count.index + 1)
  # Resultado: "web-prod-001", "web-prod-002", etc.
}

join() y split()

locals {
  # join(separador, lista)
  # Une los elementos de una lista con el separador indicado
  sg_ids_string = join(",", var.security_group_ids)
  # ["sg-111", "sg-222", "sg-333"] → "sg-111,sg-222,sg-333"

  # split(separador, cadena)
  # Divide una cadena en una lista usando el separador
  availability_zones = split(",", "us-east-1a,us-east-1b,us-east-1c")
  # "us-east-1a,us-east-1b,us-east-1c" → ["us-east-1a", "us-east-1b", "us-east-1c"]

  # Caso de uso real: Convertir una variable string de entornos a lista
  # Si el usuario pasa allowed_envs = "dev,staging,prod"
  env_list = split(",", var.allowed_envs)
  # → ["dev", "staging", "prod"]
}

replace()

locals {
  # replace(cadena, buscar, reemplazar)
  # Reemplaza todas las ocurrencias de "buscar" con "reemplazar"

  # Caso de uso: Convertir nombre del proyecto a formato válido para S3
  # Los nombres de S3 no pueden tener mayúsculas ni caracteres especiales
  bucket_name = replace(lower(var.project_name), " ", "-")
  # "My Project Name" → "my-project-name"

  # También admite expresiones regulares
  clean_name = replace(var.raw_name, "/[^a-z0-9-]/", "")
  # Elimina cualquier carácter que no sea letra minúscula, número o guión
}

trimspace(), upper(), lower()

locals {
  # trimspace() elimina espacios al inicio y al final
  clean_input = trimspace(var.user_input)
  # "  hola mundo  " → "hola mundo"

  # upper() convierte a mayúsculas
  env_upper = upper(var.environment)
  # "prod" → "PROD"

  # lower() convierte a minúsculas (muy usado para nombres de recursos)
  safe_name = lower(var.name)
  # "MyApp" → "myapp"
}

substr()

locals {
  # substr(cadena, offset, longitud)
  # Extrae una subcadena desde la posición offset con la longitud indicada

  # Caso de uso: Acortar un nombre largo para cumplir límites de AWS
  # Algunos recursos tienen límites de 24-32 caracteres
  short_name = substr(var.long_project_name, 0, 20)
  # "my-very-long-project-name-2024" → "my-very-long-project"
}

  1. Funciones Numéricas

locals {
  # abs(número) - valor absoluto
  positive_diff = abs(var.target_count - var.current_count)
  # abs(-3) = 3

  # ceil(número) - redondeo hacia arriba
  instances_needed = ceil(var.load / var.capacity_per_instance)
  # ceil(7.2) = 8 ← Necesitamos 8 instancias si cada una soporta 7.2 unidades de carga

  # floor(número) - redondeo hacia abajo
  full_batches = floor(var.total_items / var.batch_size)
  # floor(17.8) = 17

  # max(a, b, ...) - valor máximo
  # Útil para garantizar un mínimo de instancias
  min_instances = max(var.desired_instances, 2)
  # Siempre al menos 2 instancias, aunque desired_instances sea menor

  # min(a, b, ...) - valor mínimo
  # Útil para garantizar un máximo
  capped_instances = min(var.desired_instances, 10)
  # Nunca más de 10 instancias
}

  1. Funciones de Colección

Estas son las funciones más utilizadas en configuraciones Terraform reales.

length()

locals {
  # length() cuenta elementos en listas, sets o keys en mapas

  num_subnets   = length(var.subnet_ids)
  # ["subnet-1", "subnet-2", "subnet-3"] → 3

  num_tags      = length(var.tags)
  # {Name="web", Env="prod"} → 2

  # Caso de uso: Validar que se han proporcionado suficientes subnets
}

variable "subnet_ids" {
  type = list(string)
  validation {
    condition     = length(var.subnet_ids) >= 2
    error_message = "Se requieren al menos 2 subnets para alta disponibilidad."
    # length() en validaciones para garantizar requisitos mínimos
  }
}

keys() y values()

variable "environment_configs" {
  type = map(string)
  default = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.large"
  }
}

locals {
  # keys() devuelve una lista de las claves del mapa (ordenadas)
  env_names = keys(var.environment_configs)
  # → ["dev", "prod", "staging"]  (ordenadas alfabéticamente)

  # values() devuelve una lista de los valores del mapa (en el mismo orden que keys)
  instance_types = values(var.environment_configs)
  # → ["t3.micro", "t3.large", "t3.small"]
}

merge()

locals {
  # merge(mapa1, mapa2, ...) combina mapas
  # Si hay claves duplicadas, el último mapa gana

  # Tags comunes para todos los recursos
  default_tags = {
    ManagedBy   = "Terraform"
    Project     = var.project_name
    Owner       = "infrastructure-team"
  }

  # Tags específicos del recurso
  resource_tags = {
    Name        = "web-server"
    Environment = var.environment
    Owner       = "web-team"  # ← Sobreescribe el Owner de default_tags
  }

  # Combinamos los tags: resource_tags sobreescribe default_tags en caso de conflicto
  final_tags = merge(local.default_tags, local.resource_tags)
  # Resultado: {
  #   ManagedBy = "Terraform"
  #   Project   = "myapp"
  #   Owner     = "web-team"       ← Ganó resource_tags
  #   Name      = "web-server"
  #   Environment = "prod"
  # }
}

flatten()

locals {
  # flatten() convierte una lista de listas en una lista plana

  # Caso de uso: Tenemos las subnets de varias VPCs y queremos una lista única
  vpc_subnets = [
    ["10.0.1.0/24", "10.0.2.0/24"],      # Subnets de VPC 1
    ["10.1.1.0/24"],                       # Subnets de VPC 2
    ["10.2.1.0/24", "10.2.2.0/24", "10.2.3.0/24"]  # Subnets de VPC 3
  ]

  all_subnets = flatten(local.vpc_subnets)
  # → ["10.0.1.0/24", "10.0.2.0/24", "10.1.1.0/24", "10.2.1.0/24", "10.2.2.0/24", "10.2.3.0/24"]
}

distinct()

locals {
  # distinct() elimina duplicados de una lista (preserva el orden de primera aparición)

  all_regions = ["us-east-1", "eu-west-1", "us-east-1", "ap-southeast-1", "eu-west-1"]
  unique_regions = distinct(local.all_regions)
  # → ["us-east-1", "eu-west-1", "ap-southeast-1"]
}

lookup()

variable "instance_types" {
  type = map(string)
  default = {
    dev  = "t3.micro"
    prod = "t3.large"
  }
}

locals {
  # lookup(mapa, clave, valor_por_defecto)
  # Busca una clave en un mapa; si no existe, devuelve el valor por defecto

  instance_type = lookup(var.instance_types, var.environment, "t3.small")
  # Si environment = "staging" (no existe en el mapa), devuelve "t3.small"
  # Si environment = "prod", devuelve "t3.large"
}

element()

locals {
  # element(lista, índice)
  # Accede a un elemento por índice, con soporte para índices circulares

  azs = ["us-east-1a", "us-east-1b", "us-east-1c"]

  # Para 5 subnets con solo 3 AZs, el índice circular reparte automáticamente
  # element(azs, 0) → "us-east-1a"
  # element(azs, 1) → "us-east-1b"
  # element(azs, 2) → "us-east-1c"
  # element(azs, 3) → "us-east-1a"  (vuelve al principio)
  # element(azs, 4) → "us-east-1b"
}

resource "aws_subnet" "public" {
  count             = 5
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = element(local.azs, count.index)
  # Las 5 subnets se distribuyen cíclicamente entre 3 AZs
}

concat()

locals {
  # concat(lista1, lista2, ...) une múltiples listas en una sola

  primary_admins    = ["alice", "bob"]
  secondary_admins  = ["charlie", "diana"]
  emergency_admins  = ["eve"]

  all_admins = concat(local.primary_admins, local.secondary_admins, local.emergency_admins)
  # → ["alice", "bob", "charlie", "diana", "eve"]
}

  1. Funciones de Fecha y Hora

locals {
  # timestamp() devuelve la fecha/hora UTC actual en formato RFC 3339
  # IMPORTANTE: Se evalúa en cada plan/apply, por lo que puede causar diffs constantes
  current_time = timestamp()
  # → "2024-01-15T10:30:00Z"

  # formatdate(formato, timestamp) formatea un timestamp
  # Documentación de formatos: https://www.terraform.io/language/functions/formatdate
  current_date = formatdate("YYYY-MM-DD", timestamp())
  # → "2024-01-15"

  human_date = formatdate("DD/MM/YYYY hh:mm:ss", timestamp())
  # → "15/01/2024 10:30:00"

  # timeadd(timestamp, duración) suma una duración a un timestamp
  # Formato de duración: "1h", "30m", "24h", "168h" (7 días), etc.
  expiry_time = timeadd(timestamp(), "720h")
  # Añade 30 días (30 * 24 = 720 horas) al timestamp actual
}

# Caso de uso real: Crear certificados o tokens con fecha de expiración
resource "aws_iam_role" "temp_access" {
  name = "temp-access-role"

  tags = {
    CreatedAt = timestamp()
    ExpiresAt = formatdate("YYYY-MM-DD", timeadd(timestamp(), "720h"))
    # La etiqueta de expiración es solo informativa; la revocación es manual
  }
}

  1. Funciones de Hash y Codificación

jsonencode() y jsondecode()

# jsonencode() convierte un valor HCL/Terraform a formato JSON string
# jsondecode() convierte un string JSON a un valor HCL/Terraform

locals {
  policy = {
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject", "s3:PutObject"]
        Resource = "arn:aws:s3:::my-bucket/*"
      }
    ]
  }
}

resource "aws_iam_role_policy" "s3_access" {
  name   = "s3-access"
  role   = aws_iam_role.app.id
  policy = jsonencode(local.policy)
  # Convierte el objeto HCL a string JSON para la API de AWS
}

# jsondecode() para leer datos JSON externos
data "local_file" "config" {
  filename = "${path.module}/config.json"
}

locals {
  # Convertimos el contenido JSON del archivo a un objeto HCL
  config_data = jsondecode(data.local_file.config.content)
  # Ahora podemos acceder: local.config_data.database_url, etc.
}

base64encode() y base64decode()

# Muy usado para user_data en instancias EC2
resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = "t3.micro"

  # user_data se pasa en base64 automáticamente, pero a veces necesitas hacerlo manualmente
  user_data = base64encode(<<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    systemctl start nginx
    systemctl enable nginx
    echo "Servidor configurado por Terraform" > /var/www/html/index.html
  EOF
  )
}

md5() y sha256()

locals {
  # md5() genera un hash MD5 de una cadena
  # Útil para generar sufijos únicos basados en el contenido

  config_hash = md5(file("${path.module}/config.json"))
  # Si el archivo cambia, el hash cambia y puedes forzar redeploys

  # sha256() más seguro que MD5, recomendado para verificaciones de integridad
  script_hash = sha256(file("${path.module}/startup.sh"))
}

resource "aws_launch_template" "web" {
  name_prefix = "web-"

  # Al incluir el hash en el nombre, un cambio en el script fuerza una nueva versión
  user_data = base64encode(templatefile("${path.module}/startup.sh.tpl", {
    environment = var.environment
  }))

  tags = {
    ScriptHash = local.script_hash
    # Registramos el hash como etiqueta para auditabilidad
  }
}

  1. La Función templatefile()

templatefile() es una de las funciones más poderosas de Terraform. Permite leer un archivo de plantilla y sustituir variables dentro de él.

Sintaxis

templatefile(ruta_archivo, variables)

Ejemplo completo: Configuración de Nginx

# Estructura de archivos
templates/
└── nginx.conf.tpl   ← Archivo de plantilla
main.tf
# templates/nginx.conf.tpl
# Las variables van entre ${ } en las plantillas Terraform

server {
    listen 80;
    server_name ${domain_name};
    # ${domain_name} será reemplazado por el valor de la variable

    root /var/www/${app_name};
    index index.html;

    # Límite de conexiones según el entorno
    worker_connections ${worker_connections};

    location / {
        try_files $uri $uri/ =404;
        # El $ de Nginx NO se confunde con las variables Terraform
        # porque las variables Terraform usan ${ }
    }

    %{ for upstream in upstreams ~}
    upstream backend_${upstream.name} {
        server ${upstream.host}:${upstream.port};
    }
    %{ endfor ~}
    # %{ for } y %{ endfor } son bucles dentro de plantillas
}
# main.tf

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = "t3.micro"

  user_data = base64encode(templatefile("${path.module}/templates/nginx.conf.tpl", {
    # Pasamos las variables que la plantilla necesita
    domain_name        = var.domain_name             # "example.com"
    app_name           = var.app_name                # "myapp"
    worker_connections = var.environment == "prod" ? 1024 : 256
    # En producción más conexiones, en dev menos

    upstreams = [
      { name = "api",    host = "10.0.1.10", port = 8080 },
      { name = "static", host = "10.0.1.11", port = 80 }
    ]
    # Lista de upstreams que la plantilla iterará con %{ for }
  }))
}

  1. Funciones de Red: cidrsubnet() y cidrhost()

Estas funciones son especialmente valiosas para calcular rangos de IPs automáticamente, eliminando la necesidad de calcularlos a mano.

cidrsubnet()

# cidrsubnet(prefix, newbits, netnum)
# prefix: el bloque CIDR base
# newbits: bits adicionales para la máscara de subred
# netnum: número de la subred (índice)

variable "vpc_cidr" {
  default = "10.0.0.0/16"
  # Rango base: 10.0.0.0 - 10.0.255.255 (65536 IPs)
}

locals {
  # Creamos subnets /24 (256 IPs cada una) desde un bloque /16
  # newbits = 8 porque /16 + 8 bits = /24

  public_subnets = [
    cidrsubnet(var.vpc_cidr, 8, 0),  # 10.0.0.0/24  (IPs: 10.0.0.0 - 10.0.0.255)
    cidrsubnet(var.vpc_cidr, 8, 1),  # 10.0.1.0/24  (IPs: 10.0.1.0 - 10.0.1.255)
    cidrsubnet(var.vpc_cidr, 8, 2),  # 10.0.2.0/24  (IPs: 10.0.2.0 - 10.0.2.255)
  ]

  private_subnets = [
    cidrsubnet(var.vpc_cidr, 8, 10), # 10.0.10.0/24 (privadas, índice 10+)
    cidrsubnet(var.vpc_cidr, 8, 11), # 10.0.11.0/24
    cidrsubnet(var.vpc_cidr, 8, 12), # 10.0.12.0/24
  ]
}

# Creamos subnets dinámicamente usando for_each
resource "aws_subnet" "public" {
  count             = length(local.public_subnets)
  vpc_id            = aws_vpc.main.id
  cidr_block        = local.public_subnets[count.index]
  # Cada subred recibe su CIDR calculado automáticamente
  availability_zone = element(var.availability_zones, count.index)
  map_public_ip_on_launch = true

  tags = {
    Name = format("public-subnet-%02d", count.index + 1)
    # "public-subnet-01", "public-subnet-02", etc.
    Tier = "public"
  }
}

cidrhost()

# cidrhost(prefix, hostnum)
# Calcula la IP de un host específico dentro de un bloque CIDR

locals {
  subnet_cidr = "10.0.1.0/24"

  # Hosts especiales dentro de la subred
  gateway_ip    = cidrhost(local.subnet_cidr, 1)    # 10.0.1.1  (gateway)
  dns_server_ip = cidrhost(local.subnet_cidr, 2)    # 10.0.1.2  (DNS)
  ntp_server_ip = cidrhost(local.subnet_cidr, 3)    # 10.0.1.3  (NTP)
  broadcast_ip  = cidrhost(local.subnet_cidr, -1)   # 10.0.1.255 (broadcast, índice -1)
}

  1. Ejercicio Práctico: Usar 5 Funciones en una Configuración Real

Objetivo

Crear una configuración de red completa que use cidrsubnet, format, merge, length y lookup para construir una VPC con subnets en múltiples AZs.

Solución Detallada

# network/variables.tf

variable "project" {
  description = "Nombre del proyecto"
  type        = string
  default     = "myapp"
}

variable "environment" {
  description = "Entorno de despliegue"
  type        = string
  default     = "dev"
}

variable "vpc_cidr" {
  description = "Bloque CIDR de la VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "availability_zones" {
  description = "Lista de zonas de disponibilidad"
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

variable "environment_settings" {
  description = "Configuraciones específicas por entorno"
  type        = map(any)
  default = {
    dev = {
      subnet_count = 2         # Solo 2 subnets en dev para reducir costes
      enable_nat   = false     # Sin NAT gateway en dev
    }
    staging = {
      subnet_count = 2
      enable_nat   = true
    }
    prod = {
      subnet_count = 3         # 3 subnets en prod para alta disponibilidad
      enable_nat   = true
    }
  }
}
# network/main.tf

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

locals {
  # FUNCIÓN 1: lookup() - Obtenemos la configuración del entorno actual
  # Si el entorno no existe en el mapa, usamos la config de "dev" por defecto
  env_config = lookup(var.environment_settings, var.environment, var.environment_settings["dev"])

  # FUNCIÓN 2: merge() - Combinamos tags comunes con tags específicos
  common_tags = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "Terraform"
  }

  vpc_tags = merge(local.common_tags, {
    Name = format("%s-%s-vpc", var.project, var.environment)
    # FUNCIÓN 3: format() - Nombre estandarizado para la VPC
    # Resultado ejemplo: "myapp-prod-vpc"
  })

  # Calculamos los CIDRs de las subnets públicas usando cidrsubnet
  # FUNCIÓN 4: length() + cidrsubnet() - Generamos CIDRs automáticamente
  public_subnet_cidrs = [
    for i in range(local.env_config.subnet_count) :
    cidrsubnet(var.vpc_cidr, 8, i)
    # Índices 0, 1, 2 → 10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24
  ]

  private_subnet_cidrs = [
    for i in range(local.env_config.subnet_count) :
    cidrsubnet(var.vpc_cidr, 8, i + 10)
    # Índices 10, 11, 12 → 10.0.10.0/24, 10.0.11.0/24, 10.0.12.0/24
  ]
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags                 = local.vpc_tags
}

resource "aws_subnet" "public" {
  # FUNCIÓN 5: length() - Creamos tantas subnets como CIDRs calculamos
  count = length(local.public_subnet_cidrs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = local.public_subnet_cidrs[count.index]
  availability_zone = element(var.availability_zones, count.index)
  # element() distribuye cíclicamente entre las AZs disponibles
  map_public_ip_on_launch = true

  tags = merge(local.common_tags, {
    # format() genera el nombre con número de dos dígitos: "01", "02", "03"
    Name = format("%s-%s-public-%02d", var.project, var.environment, count.index + 1)
    Tier = "public"
  })
}

resource "aws_subnet" "private" {
  count = length(local.private_subnet_cidrs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = local.private_subnet_cidrs[count.index]
  availability_zone = element(var.availability_zones, count.index)

  tags = merge(local.common_tags, {
    Name = format("%s-%s-private-%02d", var.project, var.environment, count.index + 1)
    Tier = "private"
  })
}
# network/outputs.tf

output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "IDs de las subnets públicas"
  value       = aws_subnet.public[*].id
  # [*] obtiene el atributo id de todos los elementos del array
}

output "private_subnet_ids" {
  description = "IDs de las subnets privadas"
  value       = aws_subnet.private[*].id
}

output "subnet_cidrs_debug" {
  description = "CIDRs calculados (útil para debugging)"
  value = {
    public  = local.public_subnet_cidrs
    private = local.private_subnet_cidrs
  }
}

  1. Errores Comunes al Usar Funciones

Error 1: Tipo de argumento incorrecto

# Problema: length() con un valor que no es colección
variable "my_var" {
  type    = string
  default = "hello"
}

locals {
  # ERROR: length() aplicado a un string devuelve el número de caracteres,
  # lo cual puede ser correcto, pero si esperas una lista y recibes un string,
  # el resultado no tendrá sentido
  count = length(var.my_var)  # Devuelve 5 (caracteres), no el número de elementos
}

# Solución: Asegúrate del tipo correcto
variable "my_list" {
  type    = list(string)
  default = ["a", "b", "c"]
}
locals {
  count = length(var.my_list)  # Devuelve 3 (elementos)
}

Error 2: cidrsubnet con bits insuficientes

# Problema: Pedir más subredes de las que permite el bloque CIDR
locals {
  # ERROR: Un /24 solo tiene 1 bit disponible para /25, no 8 bits para /32
  bad_subnet = cidrsubnet("10.0.0.0/24", 8, 0)
  # Error: prefix size must be no larger than 32

  # Para un /24, solo puedes añadir bits hasta /32
  # newbits puede ser como máximo 8 (24 + 8 = 32)
  good_subnet = cidrsubnet("10.0.0.0/24", 1, 0)  # → 10.0.0.0/25
}

Error 3: lookup() sin valor por defecto

# Problema: Si la clave no existe y no hay valor por defecto, Terraform falla
locals {
  # ERROR si terraform.workspace = "test" y "test" no está en el mapa
  config = lookup(var.env_config, terraform.workspace)
  # Error: lookup failed to find key "test"

  # Solución: Siempre proporciona un valor por defecto
  config = lookup(var.env_config, terraform.workspace, var.env_config["dev"])
}

Error 4: timestamp() causando cambios perpetuos

# Problema: timestamp() cambia en cada plan/apply
resource "aws_instance" "web" {
  tags = {
    LastUpdated = timestamp()
    # Esto causará que Terraform detecte un cambio en CADA plan,
    # ya que el timestamp siempre será diferente
  }
}

# Solución: Usar timestamps solo donde realmente tiene sentido
# o aceptar que el recurso tendrá cambios en cada apply

Resumen

En esta lección hemos explorado el amplio catálogo de funciones de Terraform:

  • Las funciones de string nos permiten manipular y formatear texto para generar nombres de recursos consistentes.
  • Las funciones de colección son las más utilizadas en la práctica para manipular listas y mapas de configuración.
  • cidrsubnet() y cidrhost() eliminan la necesidad de calcular rangos de IP manualmente.
  • templatefile() permite externalizar configuraciones complejas en archivos de plantilla reutilizables.
  • jsonencode() es esencial cuando trabajamos con políticas IAM y otras configuraciones en formato JSON.
  • Siempre podemos probar funciones en terraform console antes de incorporarlas al código.

En la siguiente lección estudiaremos los bloques dinámicos, que nos permitirán generar configuraciones repetitivas de forma elegante usando bucles dentro de nuestros recursos.

© Copyright 2026. Todos los derechos reservados