---
name: terraform-experto
description: >
  Terraform: module design, remote state con S3+DynamoDB, workspaces, import,
  moved blocks, lifecycle rules, for_each y seguridad con checkov/tfsec. Cargar
  cuando se escriba infraestructura como código o se diseñen modules Terraform.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para Kubernetes manifiestos YAML o Helm charts — para Kubernetes cargar `kubernetes-orquestacion`."
  - "No cargar para Pulumi, CDK o CloudFormation — son herramientas IaC distintas con APIs y DSLs incompatibles con Terraform HCL."
  - "No cargar para Docker y containerización — para Docker cargar `contenedores-docker`."
  - "No cargar para recursos cloud específicos (políticas IAM de AWS, redes de Azure) sin contexto Terraform — para cloud sin IaC cargar `cloud-aws`, `azure-cloud` o `gcp-cloud`."
evolvable: true  # default para skill estandar
---
# Terraform Experto — IaC Moderno

Terraform es el estándar de IaC multi-cloud. Un módulo bien diseñado es pequeño,
reutilizable y con interfaz minimal. El estado remoto con locking es obligatorio
en equipos. Este skill cubre el diseño de módulos, estrategias de estado, patrones
de seguridad y el flujo correcto de trabajo.

## Cuándo NO cargar

- La tarea es Kubernetes YAML o Helm charts — cargar `kubernetes-orquestacion`.
- La herramienta IaC es Pulumi, AWS CDK o CloudFormation — tienen DSLs incompatibles con HCL.
- El trabajo es Docker y containerización — cargar `contenedores-docker`.
- La pregunta es sobre recursos cloud (IAM, networking) fuera del contexto de Terraform — cargar `cloud-aws`, `azure-cloud` o `gcp-cloud`.

## Cuándo cargar este skill

Invoca `Skill("terraform-experto")` cuando:

- Se escriban resources, modules o data sources de Terraform
- Se diseñe la estructura de módulos para un proyecto nuevo
- Se configure remote state con locking
- Se importe infraestructura existente con `terraform import` o import blocks
- Se evaluen estrategias de workspaces vs multiple state files
- Se configure checkov o tfsec para seguridad

## Conceptos clave

### Modules — la unidad de reutilización

Un módulo es un directorio con archivos `.tf`. El módulo raíz es el que se aplica
directamente. Los módulos hijo encapsulan recursos relacionados con una interfaz
de `variables.tf` (inputs) y `outputs.tf` (outputs). Un buen módulo tiene más
implementación que interfaz.

### Remote State con Locking

El estado de Terraform contiene la información de todos los recursos gestionados.
En equipos, debe almacenarse en backend remoto (S3, GCS, Terraform Cloud) con
locking habilitado para evitar applies concurrentes. DynamoDB como lock en AWS,
GCS nativo en GCP.

### for_each vs count

`count` genera recursos por índice numérico — frágil porque cambiar el orden
recrea recursos. `for_each` genera recursos por clave de mapa o set — estable
porque la clave identifica el recurso. Preferir `for_each` siempre que sea posible.

## Reglas obligatorias

### Terraform plan SIEMPRE antes de apply — nunca apply directo

```bash
# MAL — apply sin revisar el plan
terraform apply

# BIEN — plan con output a archivo, luego apply del mismo plan
terraform plan -out=tfplan
# Revisar el plan: terraform show -json tfplan | jq '.resource_changes[]'
terraform apply tfplan
```

**Por qué**: Un `terraform apply` sin plan puede destruir recursos si el estado
está desactualizado o si hay cambios de configuración inesperados.

### NUNCA hardcodear valores — usar variables con validación

```hcl
# MAL — valores hardcodeados no reutilizables
resource "aws_instance" "web" {
  instance_type = "t3.micro"
  ami           = "ami-0c55b159cbfafe1f0"
}

# BIEN — variables con validacion y descripcion
variable "instance_type" {
  type        = string
  description = "Tipo de instancia EC2"
  default     = "t3.micro"

  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "Solo se permiten instancias t3.micro, t3.small o t3.medium."
  }
}
```

### Modules con versión pinneada — nunca rangos abiertos

```hcl
# MAL — version no pinneada, puede romper en el proximo apply
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"  # cualquier 5.x puede incluir breaking changes
}

# BIEN — version exacta, actualizar conscientemente
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.3"
}
```

### prevent_destroy para recursos críticos de datos

```hcl
resource "aws_db_instance" "principal" {
  # ...configuracion...

  lifecycle {
    prevent_destroy = true  # falla el plan si algo intenta destruir esta BD
  }
}
```

## Patrones recomendados

### Estructura de módulo bien organizado

```
modulo-vpc/
├── main.tf          # Resources principales
├── variables.tf     # Inputs con descripcion y validacion
├── outputs.tf       # Outputs documentados
├── versions.tf      # required_providers y terraform version
└── README.md        # Generado con terraform-docs
```

```hcl
# versions.tf
terraform {
  required_version = ">= 1.6.0"

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

### Remote State con S3 + DynamoDB (AWS)

```hcl
# backend.tf
terraform {
  backend "s3" {
    bucket         = "mi-empresa-terraform-state"
    key            = "produccion/vpc/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true                    # cifrado en reposo OBLIGATORIO
    dynamodb_table = "terraform-state-locks" # locking
  }
}
```

### for_each para múltiples recursos similares

```hcl
variable "ambientes" {
  type = map(object({
    cidr_block   = string
    enable_nat   = bool
  }))
  default = {
    staging = { cidr_block = "10.1.0.0/16", enable_nat = false }
    prod    = { cidr_block = "10.0.0.0/16", enable_nat = true  }
  }
}

resource "aws_vpc" "ambientes" {
  for_each   = var.ambientes
  cidr_block = each.value.cidr_block

  tags = {
    Name     = "vpc-${each.key}"
    Ambiente = each.key
  }
}
```

### Import block (Terraform 1.5+) para infraestructura existente

```hcl
# import.tf — declarativo, incluible en el codigo
import {
  to = aws_s3_bucket.logs
  id = "mi-bucket-logs-existente"
}

resource "aws_s3_bucket" "logs" {
  bucket = "mi-bucket-logs-existente"
  # terraform genera el codigo inicial con: terraform plan -generate-config-out=generated.tf
}
```

### moved block para refactorizar sin destruir recursos

```hcl
# Al renombrar un resource o moverlo a un modulo
moved {
  from = aws_security_group.web
  to   = module.web_server.aws_security_group.this
}
```

### Dynamic blocks para configuración variable

```hcl
resource "aws_security_group" "web" {
  name = "sg-web"

  dynamic "ingress" {
    for_each = var.puertos_permitidos
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}
```

### Locals para expresiones reutilizables

```hcl
locals {
  tags_comunes = {
    proyecto    = var.nombre_proyecto
    ambiente    = var.ambiente
    gestionado  = "terraform"
    equipo      = var.equipo
  }

  prefijo = "${var.nombre_proyecto}-${var.ambiente}"
}

resource "aws_s3_bucket" "datos" {
  bucket = "${local.prefijo}-datos"
  tags   = local.tags_comunes
}
```

## Anti-patrones conocidos

### Estado local en equipo — siempre usar backend remoto

```hcl
# MAL — estado en disco local (default si no se configura backend)
# El estado se pierde si el disco falla y no se puede trabajar en equipo

# BIEN — siempre configurar backend remoto antes del primer apply
terraform {
  backend "s3" { ... }
}
```

### Módulos monolíticos con 500+ líneas

Un módulo con 500 resources mezcla responsabilidades y es imposible de reutilizar.
Dividir por dominio: modulo-red, modulo-compute, modulo-bd. La interfaz de cada
módulo debe ser pequeña comparada con su implementación.

### ignore_changes para ocultar drift — investigar la causa en su lugar

```hcl
# SOLO usar ignore_changes cuando el recurso se modifica por un proceso externo
# documentado. Nunca para ocultar conflictos sin entender la causa.
lifecycle {
  ignore_changes = [
    tags["LastModified"],  # etiqueta que AWS actualiza automaticamente
  ]
}
```

## Gotchas / Errores comunes no obvios

**`count` en lugar de `for_each` causa recreación de recursos si se elimina un elemento del medio de la lista**: `count = length(var.servidores)` crea recursos indexados `aws_instance.web[0]`, `[1]`, `[2]` — si se elimina `servidores[1]`, Terraform recrea `[1]` (con los valores de `[2]`) y destruye `[2]`. Causa: los recursos con `count` son identificados por índice numérico, no por valor. Fix: siempre usar `for_each = toset(var.servidores)` para colecciones — los recursos se identifican por la clave del mapa/set y la eliminación de un elemento no afecta a los demás.

**`terraform apply` en CI sin `-auto-approve` bloquea esperando input interactivo**: si el pipeline no pasa `-auto-approve`, Terraform espera `yes` del usuario — en CI no hay terminal interactivo y el job se congela hasta el timeout. Causa: comportamiento por defecto de Terraform para prevenir applies accidentales. Fix: en CI SIEMPRE usar `terraform apply -auto-approve tfplan` donde `tfplan` es el archivo de plan generado previamente con `terraform plan -out=tfplan` — nunca `terraform apply -auto-approve` sin plan previo.

**`terraform state` modificado manualmente (mv, rm) sin un `moved` block crea drift que otros membres del equipo no ven**: `terraform state mv` modifica el state file remoto pero no deja evidencia en el código HCL — otros miembros que ejecuten `terraform plan` ven diferencias inesperadas. Causa: el state file es el source of truth pero no está versionado junto con el código. Fix: para renombrar o mover recursos, usar siempre `moved {}` blocks en el HCL (Terraform 1.1+) que se commitean al repositorio — son la forma declarativa y rastreable de refactorizar recursos.

**`for_each` con un `map` que contiene valores de otro resource que no existe aún causa error de "value not known until apply"**: `for_each = var.ips_elasticas` donde `var.ips_elasticas` es un output de otro resource que se crea en el mismo apply lanza error porque el set de `for_each` debe ser conocido antes del apply. Causa: Terraform necesita conocer las claves del `for_each` en la fase de plan para saber cuántos recursos crear. Fix: usar valores estáticos como claves de `for_each` (strings conocidos) y referenciar el resource dinámico como atributo del objeto, no como clave.

## Checklist de verificación

- [ ] Plan revisado antes de cada apply
- [ ] Backend remoto con locking configurado desde el inicio
- [ ] Módulos con versión exacta pinneada
- [ ] Variables con `description` y `validation` donde aplica
- [ ] `prevent_destroy` en recursos críticos (BD, buckets con datos)
- [ ] `for_each` en lugar de `count` para recursos en colecciones
- [ ] Tags obligatorios definidos en `locals` y aplicados a todos los recursos
- [ ] checkov o tfsec ejecutado en CI sin errores HIGH/CRITICAL

## Referencias

- [Terraform Documentation](https://developer.hashicorp.com/terraform/docs)
- [terraform-aws-modules](https://registry.terraform.io/namespaces/terraform-aws-modules)
- [checkov](https://www.checkov.io)
- [tfsec](https://aquasecurity.github.io/tfsec)

---
*Skill creado con swl:crear-skill el 2026-03-31. Versión 1.0.0.*
