Introducción
Los bloques resource y data son los pilares centrales de cualquier configuración de Terraform. Los recursos definen la infraestructura que queremos crear y gestionar, mientras que los data sources nos permiten leer información de infraestructura existente sin modificarla. Dominar estos dos conceptos, junto con los meta-argumentos que los potencian, te permitirá escribir configuraciones de Terraform profesionales y flexibles.
El bloque resource: sintaxis completa
Estructura básica
resource "tipo_proveedor_recurso" "nombre_local" {
# Argumentos específicos del recurso
argumento_1 = valor_1
argumento_2 = valor_2
# Bloques anidados dentro del recurso
bloque_anidado {
sub_argumento = valor
}
}El tipo del recurso siempre tiene la forma proveedor_tiporecurso, por ejemplo:
aws_instance(AWS + instancia EC2)azurerm_virtual_machine(Azure + máquina virtual)google_compute_instance(Google Cloud + instancia compute)
Ejemplo detallado de un recurso
resource "aws_instance" "servidor_web" {
# --- Configuración de hardware ---
ami = "ami-0c55b159cbfafe1f0" # ID de la imagen de sistema operativo
instance_type = "t3.micro" # Tipo de instancia: 2 vCPUs, 1GB RAM
# --- Configuración de red ---
subnet_id = aws_subnet.publica.id # Subnet donde se coloca
vpc_security_group_ids = [aws_security_group.web.id] # Grupos de seguridad
associate_public_ip_address = true # Asignar IP pública automáticamente
# --- Configuración de almacenamiento ---
root_block_device {
volume_type = "gp3" # Tipo de volumen: SSD de propósito general v3
volume_size = 20 # Tamaño en GB
encrypted = true # Cifrado en reposo habilitado
}
# --- Script de inicialización (se ejecuta al arrancar la instancia) ---
user_data = <<-EOF
#!/bin/bash
# Actualizar paquetes del sistema
apt-get update -y
# Instalar el servidor web nginx
apt-get install -y nginx
# Iniciar nginx y habilitarlo para que arranque con el sistema
systemctl start nginx
systemctl enable nginx
EOF
# --- Etiquetas (metadata) ---
tags = {
Name = "servidor-web-principal"
Entorno = "produccion"
Gestionado = "terraform"
}
}Meta-argumentos de recursos
Los meta-argumentos son argumentos especiales que Terraform entiende para todos los tipos de recursos, independientemente del proveedor. Son:
| Meta-argumento | Propósito |
|---|---|
depends_on |
Define dependencias explícitas entre recursos |
count |
Crea múltiples instancias del mismo recurso |
for_each |
Crea recursos a partir de un mapa o set |
provider |
Especifica qué configuración de proveedor usar |
lifecycle |
Controla el comportamiento durante el ciclo de vida |
Meta-argumento depends_on
Normalmente Terraform infiere automáticamente el orden de creación analizando las referencias entre recursos. Pero a veces hay dependencias implícitas que Terraform no puede detectar. En esos casos usamos depends_on.
resource "aws_iam_role_policy" "politica_acceso_s3" {
name = "acceso-s3"
role = aws_iam_role.ec2_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "*"
}]
})
}
resource "aws_instance" "servidor" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
iam_instance_profile = aws_iam_instance_profile.perfil.name
# La instancia necesita la política aplicada ANTES de arrancar
# aunque no hay referencia directa a "politica_acceso_s3"
depends_on = [
aws_iam_role_policy.politica_acceso_s3
]
}Regla de oro: Usa depends_on solo cuando sea absolutamente necesario. La mayoría de las veces las referencias implícitas son suficientes y más limpias.
Meta-argumento count
count permite crear múltiples instancias del mismo bloque de recurso. Terraform crea count copias, cada una accesible como recurso[índice].
variable "num_servidores" {
type = number
default = 3
}
# Con count = 3, Terraform crea tres instancias EC2
resource "aws_instance" "servidores" {
count = var.num_servidores # Crea 3 instancias
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
# count.index es el índice actual (0, 1, 2, ...)
# Lo usamos para dar nombres únicos a cada instancia
tags = {
Name = "servidor-${count.index + 1}"
# count.index empieza en 0, sumamos 1 para nombres más naturales
# Resultado: "servidor-1", "servidor-2", "servidor-3"
}
}
# Acceder a instancias individuales por índice
output "ip_primer_servidor" {
value = aws_instance.servidores[0].public_ip # Primera instancia (índice 0)
}
output "ip_segundo_servidor" {
value = aws_instance.servidores[1].public_ip # Segunda instancia (índice 1)
}
# Obtener una lista de todos los IPs usando [*] (splat expression)
output "todas_las_ips" {
value = aws_instance.servidores[*].public_ip
# Resultado: ["1.2.3.4", "5.6.7.8", "9.10.11.12"]
}Distribución en múltiples zonas con count
variable "zonas" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Creamos tantas subnets como zonas de disponibilidad haya
resource "aws_subnet" "publicas" {
count = length(var.zonas) # Una subnet por zona
vpc_id = aws_vpc.principal.id
# cidrsubnet calcula un sub-bloque CIDR único para cada subnet
# "10.0.0.0/16", 8 bits adicionales, índice -> genera 10.0.0.0/24, 10.0.1.0/24, etc.
cidr_block = cidrsubnet("10.0.0.0/16", 8, count.index)
availability_zone = var.zonas[count.index] # Zona correspondiente al índice
tags = {
Name = "subnet-publica-${var.zonas[count.index]}"
}
}Limitaciones de count
El principal problema de count es que los recursos se identifican por índice numérico. Si eliminas un elemento del medio de la lista, Terraform puede querer recrear recursos innecesariamente.
# PROBLEMA con count: si la lista cambia, los índices cambian
variable "nombres_servidores" {
default = ["web-1", "web-2", "web-3"]
}
resource "aws_instance" "web" {
count = length(var.nombres_servidores)
# [0] = web-1, [1] = web-2, [2] = web-3
# Si eliminamos "web-2", Terraform ve:
# [0] = web-1 (sin cambio)
# [1] = web-3 (antes era web-2 -> Terraform modifica o recrea!)
# Esto puede causar destrucciones innecesarias
}Para estos casos, for_each es mejor opción.
Meta-argumento for_each
for_each crea recursos a partir de un mapa o un set de strings. Cada recurso se identifica por su clave, lo que lo hace mucho más robusto que count cuando la colección puede cambiar.
for_each con un map
variable "instancias" {
type = map(object({
tipo = string
zona = string
}))
default = {
"web-1" = { tipo = "t2.micro", zona = "us-east-1a" }
"web-2" = { tipo = "t2.small", zona = "us-east-1b" }
"api-1" = { tipo = "t3.medium", zona = "us-east-1a" }
}
}
resource "aws_instance" "servidores" {
# for_each recibe el mapa completo
for_each = var.instancias
ami = "ami-0c55b159cbfafe1f0"
# each.value contiene el valor del elemento actual del mapa
# each.key contiene la clave del elemento actual ("web-1", "web-2", etc.)
instance_type = each.value.tipo
availability_zone = each.value.zona
tags = {
# each.key es el nombre del servidor: "web-1", "web-2", "api-1"
Name = each.key
}
}
# Acceder a un servidor específico por su clave
output "ip_web_1" {
value = aws_instance.servidores["web-1"].public_ip
}
# Obtener todos los IPs en un mapa (clave -> IP)
output "todas_las_ips" {
value = { for k, v in aws_instance.servidores : k => v.public_ip }
# Resultado: { "web-1" = "1.2.3.4", "web-2" = "5.6.7.8", "api-1" = "9.10.11.12" }
}for_each con un set de strings
variable "buckets_s3" {
type = set(string) # Set: sin duplicados, sin orden garantizado
default = ["logs-app", "backups-db", "assets-web"]
}
resource "aws_s3_bucket" "buckets" {
for_each = var.buckets_s3 # Un bucket por elemento del set
# each.key es el elemento del set (en sets, key y value son iguales)
bucket = each.key # "logs-app", "backups-db", "assets-web"
tags = {
Name = each.key
}
}
output "arns_buckets" {
description = "ARNs de todos los buckets creados"
value = { for k, v in aws_s3_bucket.buckets : k => v.arn }
}Comparación count vs for_each
| Característica | count |
for_each |
|---|---|---|
| Identifica recursos por | Índice numérico (0, 1, 2...) | Clave string |
| Acceso al elemento actual | count.index |
each.key y each.value |
| Tipo de colección aceptada | Cualquier número | map o set(string) |
| Impacto al eliminar elementos | Puede recrear recursos innecesariamente | Solo afecta el elemento eliminado |
| Cuándo usar | Recursos idénticos, conteo simple | Recursos con configuración por clave |
| Salida en state | recurso[0], recurso[1] |
recurso["clave"] |
Bloque lifecycle
El bloque lifecycle controla cómo Terraform maneja los cambios en el ciclo de vida de un recurso. Se coloca dentro del bloque resource.
resource "aws_instance" "produccion" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.large"
lifecycle {
# create_before_destroy: primero crea el nuevo recurso y luego destruye el viejo
# Útil para recursos que no pueden tener downtime (como balanceadores de carga)
create_before_destroy = true
# prevent_destroy: bloquea la destrucción accidental del recurso
# Si alguien ejecuta "terraform destroy" en este recurso, Terraform dará error
prevent_destroy = true
# ignore_changes: ignora cambios en ciertos atributos durante "terraform apply"
# Útil cuando el recurso se modifica fuera de Terraform (cambios manuales)
ignore_changes = [
# Ignoramos el tag "LastModified" que se actualiza externamente
tags["LastModified"],
# Ignoramos cambios en la AMI (para no recrear si se actualiza la imagen)
ami,
]
}
tags = {
Name = "servidor-produccion"
}
}Ejemplo práctico de cada opción lifecycle
# --- create_before_destroy ---
# Caso de uso: balanceador de carga (no podemos tener downtime)
resource "aws_lb" "principal" {
name = "lb-produccion"
internal = false
load_balancer_type = "application"
subnets = aws_subnet.publicas[*].id
lifecycle {
create_before_destroy = true
# Si necesitamos cambiar algo que requiere recrear el LB:
# 1. Terraform crea el nuevo LB
# 2. Actualiza todas las referencias al nuevo LB
# 3. Destruye el LB antiguo
# -> Sin downtime durante el cambio
}
}
# --- prevent_destroy ---
# Caso de uso: base de datos de producción (datos críticos)
resource "aws_db_instance" "produccion" {
identifier = "db-produccion"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 100
lifecycle {
prevent_destroy = true
# Si alguien ejecuta "terraform destroy" o elimina este bloque del código,
# Terraform dará error:
# Error: Instance cannot be destroyed
# on main.tf line X, in resource "aws_db_instance" "produccion":
# prevent_destroy = true
# Esta salvaguarda evita borrado accidental de datos de producción
}
}
# --- ignore_changes ---
# Caso de uso: grupo de autoescalado donde el número de instancias
# lo gestiona AWS (no queremos que Terraform lo sobreescriba)
resource "aws_autoscaling_group" "app" {
name = "asg-app"
min_size = 2
max_size = 10
desired_capacity = 2 # Valor inicial, puede cambiar por autoescalado
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
lifecycle {
ignore_changes = [
# Ignoramos desired_capacity porque AWS lo ajusta automáticamente
# Si no lo ignoramos, Terraform lo resetearía a 2 en cada apply
desired_capacity,
]
}
}El bloque data: leer datos existentes
Los data sources permiten leer información de recursos que ya existen (ya sea creados por otro proceso de Terraform, manualmente, o por otro equipo) sin modificarlos.
Sintaxis básica
data "tipo_proveedor_dato" "nombre_local" {
# Argumentos para filtrar/identificar el dato que queremos leer
argumento_filtro = valor
}
# Usar el dato leído
data.tipo_proveedor_dato.nombre_local.atributoEjemplos de data sources
# Leer la AMI de Ubuntu más reciente (en lugar de hardcodear el ID)
data "aws_ami" "ubuntu" {
most_recent = true # Queremos la versión más reciente
owners = ["099720109477"] # Cuenta oficial de Canonical (Ubuntu)
# Filtros para encontrar la AMI exacta que queremos
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
# El asterisco * es un comodín que coincide con cualquier texto
}
filter {
name = "virtualization-type"
values = ["hvm"] # Solo AMIs con virtualización HVM (más modernas)
}
}
# Ahora usamos el dato en lugar de hardcodear el ID de la AMI
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id # ID dinámico obtenido del data source
instance_type = "t2.micro"
tags = {
Name = "servidor-web"
# También podemos mostrar la versión de la AMI en los tags
AMIName = data.aws_ami.ubuntu.name
}
}
# Leer información de la VPC actual (útil cuando la VPC la creó otro equipo)
data "aws_vpc" "seleccionada" {
# Filtramos por tags para encontrar la VPC correcta
tags = {
Entorno = "produccion"
Equipo = "red"
}
}
# Leer las subnets de esa VPC
data "aws_subnets" "privadas" {
# Filtramos por VPC y por el tag que identifica subnets privadas
filter {
name = "vpc-id"
values = [data.aws_vpc.seleccionada.id]
}
tags = {
Tipo = "privada"
}
}
# Usar los datos leídos para crear instancias en las subnets existentes
resource "aws_instance" "app" {
count = 2
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
# Distribuimos las instancias entre las subnets privadas existentes
subnet_id = data.aws_subnets.privadas.ids[count.index]
}Datos del proveedor y cuenta
# Obtener información de la cuenta y región actual de AWS
data "aws_caller_identity" "actual" {}
# No necesita argumentos: obtiene la info de quien ejecuta Terraform
data "aws_region" "actual" {}
# Usar los datos para construir ARNs dinámicamente
resource "aws_iam_policy" "acceso_bucket" {
name = "acceso-mi-bucket"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:*"]
# Construimos el ARN del bucket usando la cuenta y región actuales
# Así el código funciona en cualquier cuenta/región sin cambios
Resource = "arn:aws:s3:::mi-bucket-${data.aws_caller_identity.actual.account_id}"
}]
})
}
output "id_cuenta_aws" {
value = data.aws_caller_identity.actual.account_id
}
output "region_actual" {
value = data.aws_region.actual.name
}Diferencia entre resource y data con tabla comparativa
| Característica | resource |
data |
|---|---|---|
| Propósito | Crear/modificar/destruir infraestructura | Leer información existente |
| Gestión del ciclo de vida | Sí: create, update, destroy | No: solo lectura |
| Aparece en el plan | Sí: como "add", "change" o "destroy" | Sí: como "data source refresh" |
| Se almacena en el estado | Sí | Sí (temporalmente, se refresca) |
| Sintaxis | resource "tipo" "nombre" |
data "tipo" "nombre" |
| Acceso | tipo_recurso.nombre.atributo |
data.tipo.nombre.atributo |
| Cuándo usar | Cuando Terraform debe CREAR el recurso | Cuando el recurso YA EXISTE |
| Riesgo si se borra | El recurso real se destruye | Nada: solo se deja de leer |
Ejemplos de código detallados: combinando resource y data
Escenario real: aplicación que usa red existente
# =============================================
# Infraestructura de red (creada por otro equipo)
# La leemos con data sources
# =============================================
# Leer la VPC de producción existente
data "aws_vpc" "produccion" {
filter {
name = "tag:Name"
values = ["vpc-produccion"]
}
}
# Leer las subnets privadas de esa VPC
data "aws_subnets" "privadas" {
filter {
name = "vpc-id"
values = [data.aws_vpc.produccion.id]
}
filter {
name = "tag:Tipo"
values = ["privada"]
}
}
# Leer el security group de aplicaciones existente
data "aws_security_group" "app" {
vpc_id = data.aws_vpc.produccion.id
tags = {
Name = "sg-aplicaciones"
}
}
# =============================================
# Infraestructura de la aplicación
# La CREAMOS con resource
# =============================================
# Security group adicional específico para esta app
resource "aws_security_group" "mi_app" {
name = "sg-mi-app"
description = "Security group para mi-aplicacion"
# Usamos la VPC leída con el data source
vpc_id = data.aws_vpc.produccion.id
ingress {
description = "HTTP interno"
from_port = 8080
to_port = 8080
protocol = "tcp"
# Solo permitimos tráfico desde dentro de la VPC
cidr_blocks = [data.aws_vpc.produccion.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1" # -1 = todos los protocolos
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "sg-mi-app"
}
}
# Instancias de la aplicación en las subnets existentes
resource "aws_instance" "app" {
count = length(data.aws_subnets.privadas.ids) # Una instancia por subnet
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.small"
# Colocamos cada instancia en una subnet privada diferente
subnet_id = data.aws_subnets.privadas.ids[count.index]
# Asignamos ambos security groups: el existente y el nuevo
vpc_security_group_ids = [
data.aws_security_group.app.id, # Security group existente (leído)
aws_security_group.mi_app.id # Security group nuevo (creado)
]
tags = {
Name = "mi-app-${count.index + 1}"
Subnet = data.aws_subnets.privadas.ids[count.index]
}
}Ejercicio práctico
Enunciado
Crea una configuración Terraform que:
- Use
countpara crear 3 instancias EC2 con nombres únicos (servidor-1,servidor-2,servidor-3). - Use
for_eachpara crear 3 buckets S3 con diferentes configuraciones:logs,backupsyassets. - Añada un bloque
lifecycleconprevent_destroy = truesolo al bucket debackups. - Use un
data sourcepara leer la región actual de AWS y usarla en los nombres de los buckets. - Defina outputs para las IPs de los servidores y los ARNs de los buckets.
Solución detallada
# =============================================
# Archivo: variables.tf
# =============================================
variable "num_servidores" {
type = number
description = "Número de servidores EC2 a crear"
default = 3
}
variable "ami_id" {
type = string
description = "ID de la AMI para las instancias EC2"
default = "ami-0c55b159cbfafe1f0" # Ubuntu 20.04 en us-east-1
}
variable "tipo_instancia" {
type = string
description = "Tipo de instancia EC2"
default = "t2.micro"
}
# =============================================
# Archivo: main.tf
# =============================================
# Data source para obtener la región actual dinámicamente
# Así no necesitamos hardcodear "us-east-1" en ningún lado
data "aws_region" "actual" {}
# Data source para obtener la cuenta actual
data "aws_caller_identity" "actual" {}
# ------ PARTE 1: count para instancias EC2 ------
resource "aws_instance" "servidores" {
# Crea 'num_servidores' instancias (3 por defecto)
count = var.num_servidores
ami = var.ami_id
instance_type = var.tipo_instancia
tags = {
# count.index va de 0 a (count-1), sumamos 1 para nombres naturales
Name = "servidor-${count.index + 1}"
Numero = count.index + 1
Total = var.num_servidores
Gestionado = "terraform"
}
}
# ------ PARTE 2: for_each para buckets S3 ------
# Definimos la configuración de cada bucket
locals {
# Mapa con la configuración de cada bucket
# La clave es el nombre lógico; el valor es la configuración
config_buckets = {
"logs" = {
descripcion = "Almacenamiento de logs de la aplicacion"
es_privado = true
evitar_borrado = false # Los logs no son tan críticos
}
"backups" = {
descripcion = "Backups de bases de datos"
es_privado = true
evitar_borrado = true # LOS BACKUPS SON CRÍTICOS: no borrar
}
"assets" = {
descripcion = "Archivos estáticos públicos (imágenes, CSS, JS)"
es_privado = false # Los assets son públicos
evitar_borrado = false
}
}
# Construimos el nombre del bucket con la región (de la data source)
# Resultado: "mi-empresa-logs-us-east-1", "mi-empresa-backups-us-east-1", etc.
# Los nombres de S3 deben ser únicos globalmente, incluir la región ayuda
prefijo_bucket = "mi-empresa-${data.aws_region.actual.name}"
}
# Creamos los buckets usando for_each con el mapa de configuración
resource "aws_s3_bucket" "buckets" {
# for_each itera sobre el mapa: each.key = "logs"/"backups"/"assets"
for_each = local.config_buckets
# Nombre único: prefijo + tipo de bucket
bucket = "${local.prefijo_bucket}-${each.key}"
# Resultado: "mi-empresa-us-east-1-logs", "mi-empresa-us-east-1-backups", etc.
tags = {
Name = each.key # "logs", "backups" o "assets"
Descripcion = each.value.descripcion # Descripción del bucket
Privado = each.value.es_privado ? "si" : "no"
Cuenta = data.aws_caller_identity.actual.account_id
}
}
# ------ PARTE 3: lifecycle con prevent_destroy ------
# Para aplicar lifecycle solo al bucket de backups, necesitamos un recurso separado
# (no podemos poner lifecycle condicional dentro de for_each de forma directa)
# Alternativa: usar un recurso separado para el bucket de backups con lifecycle
resource "aws_s3_bucket" "backups_protegido" {
bucket = "${local.prefijo_bucket}-backups-criticos"
tags = {
Name = "backups-criticos"
Descripcion = "Backups críticos de bases de datos (PROTEGIDO)"
Privado = "si"
}
# El lifecycle se aplica a ESTE recurso específico
lifecycle {
# Terraform dará error si alguien intenta destruir este bucket
prevent_destroy = true
}
}
# ------ Configuración de acceso público en los buckets ------
# El bucket de assets es público; los demás son privados
resource "aws_s3_bucket_public_access_block" "buckets" {
for_each = local.config_buckets
bucket = aws_s3_bucket.buckets[each.key].id
# Si es privado, bloqueamos TODO el acceso público
block_public_acls = each.value.es_privado
block_public_policy = each.value.es_privado
ignore_public_acls = each.value.es_privado
restrict_public_buckets = each.value.es_privado
}
# =============================================
# Archivo: outputs.tf
# =============================================
# Output 1: IPs privadas de los servidores (lista)
output "ips_privadas_servidores" {
description = "Lista de IPs privadas de los servidores EC2"
# [*] es la splat expression: obtiene el atributo de todos los elementos count
value = aws_instance.servidores[*].private_ip
# Ejemplo: ["10.0.1.10", "10.0.1.11", "10.0.1.12"]
}
# Output 2: IPs públicas de los servidores (lista)
output "ips_publicas_servidores" {
description = "Lista de IPs públicas de los servidores EC2"
value = aws_instance.servidores[*].public_ip
}
# Output 3: Mapa de nombre -> ID de instancia
output "ids_servidores" {
description = "Mapa de nombre de servidor a su ID"
# Usamos una expresión for para construir el mapa
value = {
for i, servidor in aws_instance.servidores :
"servidor-${i + 1}" => servidor.id
}
# Ejemplo: { "servidor-1" = "i-abc123", "servidor-2" = "i-def456" }
}
# Output 4: ARNs de todos los buckets S3
output "arns_buckets" {
description = "Mapa de nombre de bucket a su ARN"
value = {
for k, v in aws_s3_bucket.buckets : k => v.arn
}
# Ejemplo: { "logs" = "arn:aws:s3:::mi-empresa-us-east-1-logs", ... }
}
# Output 5: Nombres de todos los buckets creados
output "nombres_buckets" {
description = "Lista con los nombres de todos los buckets S3 creados"
value = [for k, v in aws_s3_bucket.buckets : v.bucket]
}
# Output 6: ARN del bucket de backups protegido
output "arn_backups_protegido" {
description = "ARN del bucket de backups con protect_destroy habilitado"
value = aws_s3_bucket.backups_protegido.arn
}
# Output 7: Información de la región y cuenta
output "info_cuenta" {
description = "Información de la cuenta y región de AWS"
value = {
region = data.aws_region.actual.name
cuenta = data.aws_caller_identity.actual.account_id
}
}Para probar el ejercicio:
# 1. Inicializar el directorio
terraform init
# 2. Revisar el plan de creación
terraform plan
# El plan mostrará:
# + aws_instance.servidores[0] (crear servidor-1)
# + aws_instance.servidores[1] (crear servidor-2)
# + aws_instance.servidores[2] (crear servidor-3)
# + aws_s3_bucket.buckets["logs"] (crear bucket logs)
# + aws_s3_bucket.buckets["backups"] (crear bucket backups)
# + aws_s3_bucket.buckets["assets"] (crear bucket assets)
# + aws_s3_bucket.backups_protegido (crear bucket backups protegido)
# 3. Aplicar los cambios
terraform apply
# 4. Consultar los outputs
terraform output ips_privadas_servidores
terraform output arns_buckets
terraform output -json # Todos los outputs en JSONErrores comunes
Error 1: Dependencias circulares
# INCORRECTO: A depende de B y B depende de A -> Terraform no puede resolverlo
resource "aws_security_group" "sg_a" {
vpc_id = aws_vpc.mi_vpc.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.sg_b.id] # A depende de B
}
}
resource "aws_security_group" "sg_b" {
vpc_id = aws_vpc.mi_vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.sg_a.id] # B depende de A -> ERROR!
}
}
# CORRECTO: Separar las reglas en recursos aws_security_group_rule
resource "aws_security_group" "sg_a" {
vpc_id = aws_vpc.mi_vpc.id
# Sin referencias a sg_b aquí
}
resource "aws_security_group" "sg_b" {
vpc_id = aws_vpc.mi_vpc.id
# Sin referencias a sg_a aquí
}
# Las reglas que crean la dependencia se añaden después
resource "aws_security_group_rule" "a_permite_b" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
security_group_id = aws_security_group.sg_a.id
source_security_group_id = aws_security_group.sg_b.id
}Error 2: count y for_each en el mismo recurso
# INCORRECTO: no puedes usar count y for_each juntos
resource "aws_instance" "web" {
count = 3
for_each = var.instancias # Error: solo uno de los dos
}Error 3: for_each con lista en lugar de map o set
# INCORRECTO: for_each no acepta listas directamente
variable "nombres" {
default = ["web", "api", "db"]
}
resource "aws_instance" "servidores" {
for_each = var.nombres # Error: debe ser map o set, no list
}
# CORRECTO: convertir la lista a set
resource "aws_instance" "servidores" {
for_each = toset(var.nombres) # Convertimos a set
}Error 4: prevent_destroy no funciona con terraform destroy -target
# IMPORTANTE: prevent_destroy evita "terraform destroy" completo
# pero NO bloquea la eliminación del recurso del estado con terraform state rm
# Solo bloquea si el plan de Terraform intenta destruirlo
# Si necesitas destruir a pesar del prevent_destroy, debes:
# 1. Eliminar o comentar el bloque lifecycle en el código
# 2. Ejecutar terraform apply (para que el cambio entre en efecto)
# 3. Luego terraform destroyResumen
En este módulo has aprendido los conceptos fundamentales de los bloques resource y data:
- El bloque
resourcecrea y gestiona infraestructura. Su tipo tiene la formaproveedor_tiporecursoy necesita un nombre local único. - El meta-argumento
depends_ondefine dependencias explícitas cuando Terraform no puede inferirlas automáticamente. - El meta-argumento
countcrea múltiples instancias identificadas por índice numérico; ideal para recursos idénticos. - El meta-argumento
for_eachcrea recursos a partir de mapas o sets, identificados por clave string; más robusto ante cambios en la colección. - El bloque
lifecyclecontrola el comportamiento en operaciones críticas:create_before_destroypara cero downtime,prevent_destroypara proteger recursos críticos, eignore_changespara ignorar cambios externos. - El bloque
datalee información de recursos existentes sin modificarlos; esencial para integrar con infraestructura existente.
Preparación para el siguiente módulo
En el siguiente módulo estudiaremos los Proveedores (Providers) en profundidad: cómo configurarlos, versionarlos, usar múltiples providers simultáneamente y gestionar la autenticación. Los providers son la capa que conecta Terraform con los servicios reales (AWS, Azure, GCP, etc.), y entenderlos bien es crucial para trabajar en entornos multi-cloud.
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
