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.
- ¿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:
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
localspara 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.
- Cómo Probar Funciones con
terraform console
terraform consoleAntes 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.
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
> exitConsejo: Usa
terraform consolesiempre 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.
- 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 |
- 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"
}
- 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
}
- 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"]
}
- 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
}
}
- 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
}
}
- La Función
templatefile()
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
Ejemplo completo: Configuración de Nginx
# 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 }
}))
}
- Funciones de Red:
cidrsubnet() y cidrhost()
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)
}
- 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
}
}
- 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 applyResumen
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()ycidrhost()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 consoleantes 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.
Curso de Terraform
Módulo 1: Introducción a Terraform
- ¿Qué es Terraform?
- Instalando Terraform
- Conceptos Básicos de Terraform
- Primera Configuración de Terraform
Módulo 2: Lenguaje de Configuración de Terraform
Módulo 3: Gestión del Estado
Módulo 4: Módulos de Terraform
Módulo 5: Aprovisionamiento de Recursos
- Conceptos Básicos de Aprovisionamiento
- Aprovisionamiento de Recursos AWS
- Aprovisionamiento de Recursos Azure
- Aprovisionamiento de Recursos GCP
Módulo 6: Funcionalidades Avanzadas de Terraform
Módulo 7: Mejores Prácticas de Terraform
- Organización del Código
- Control de Versiones
- Pruebas del Código de Terraform
- Mejores Prácticas de Seguridad
Módulo 8: Terraform en CI/CD
- Integración de Terraform con CI/CD
- Automatización de Terraform con Jenkins
- Uso de Terraform con GitHub Actions
- Terraform Cloud y Enterprise
