Introducción

Cuando terraform validate no detecta problemas pero terraform apply falla de formas inesperadas, o cuando un plan no hace lo que esperas, necesitas herramientas de depuración más potentes. Terraform ofrece un conjunto completo de mecanismos para inspeccionar su comportamiento interno, probar expresiones y rastrear la comunicación con los providers. En esta lección dominarás estas herramientas para convertirte en un diagnosticador eficiente.


  1. Herramientas de Depuración Disponibles

Terraform proporciona las siguientes herramientas principales para depuración:

  • TF_LOG: Activa logs detallados del comportamiento interno de Terraform
  • TF_LOG_PATH: Redirige los logs a un archivo en lugar de la terminal
  • TF_LOG_CORE y TF_LOG_PROVIDER: Logs separados para el núcleo y los providers
  • terraform console: REPL interactivo para evaluar expresiones HCL
  • terraform validate: Valida la configuración sin conectarse a la nube
  • terraform plan -json: Plan en formato JSON para análisis detallado
  • terraform providers: Muestra y verifica la configuración de providers
  • Outputs temporales: Técnica para exponer valores intermedios durante el desarrollo

  1. La Variable de Entorno TF_LOG

TF_LOG es la herramienta de depuración más poderosa de Terraform. Activa un sistema de logging que registra todo lo que Terraform hace internamente, incluyendo las llamadas a la API del proveedor de nube.

Niveles de log disponibles

Nivel Descripción Cuándo usarlo
ERROR Solo errores críticos Para silenciar la mayoría de logs pero capturar fallos
WARN Advertencias y errores Cuando quieres ver posibles problemas sin todo el ruido
INFO Información general del flujo Seguimiento del progreso de operaciones
DEBUG Información detallada de depuración Investigar comportamientos inesperados
TRACE Máximo nivel de detalle (todo) Depuración profunda, incluye llamadas HTTP completas
JSON Logs en formato JSON estructurado Análisis programático de logs

Activar TF_LOG

# En Linux/macOS - nivel DEBUG
export TF_LOG=DEBUG
terraform plan

# En Linux/macOS - nivel TRACE (máximo detalle)
export TF_LOG=TRACE
terraform apply

# En Windows PowerShell
$env:TF_LOG = "DEBUG"
terraform plan

# Para desactivar los logs
unset TF_LOG        # Linux/macOS
Remove-Item Env:TF_LOG  # PowerShell

Ejemplo de salida con TF_LOG=DEBUG

2024-01-15T10:30:00.123Z [DEBUG] provider.terraform-provider-aws:
  Starting aws provider version 5.31.0 PID=12345

2024-01-15T10:30:00.456Z [DEBUG] provider.terraform-provider-aws:
  AWS Auth: Retrieving credentials using AssumeRole

2024-01-15T10:30:01.789Z [DEBUG] provider.terraform-provider-aws:
  HTTP Request: PUT https://s3.amazonaws.com/my-bucket
  Headers: {"Content-Type": ["application/xml"]}

2024-01-15T10:30:02.012Z [DEBUG] provider.terraform-provider-aws:
  HTTP Response: 403 Forbidden
  Body: <?xml version="1.0" encoding="UTF-8"?>
  <Error><Code>AccessDenied</Code>
  <Message>Access Denied</Message></Error>

En este ejemplo, los logs de DEBUG revelan que el problema es un 403 Forbidden al intentar crear el bucket S3, lo que indica falta de permisos.


  1. TF_LOG_PATH: Guardar Logs en Archivo

Cuando los logs son muy extensos (especialmente con nivel TRACE), es más práctico guardarlos en un archivo para analizarlos con calma.

# Guardar logs en un archivo mientras se muestra la salida normal en terminal
export TF_LOG=DEBUG
export TF_LOG_PATH=./terraform-debug.log
terraform apply

# Ver el archivo de logs
less ./terraform-debug.log

# Buscar errores específicos en el log
grep -i "error\|failed\|denied" ./terraform-debug.log

# Filtrar solo las llamadas HTTP
grep "HTTP Request\|HTTP Response" ./terraform-debug.log

Consejo: Rotar logs entre sesiones

# Crear un log con timestamp para no sobreescribir anteriores
export TF_LOG_PATH="./logs/terraform-$(date +%Y%m%d-%H%M%S).log"
mkdir -p ./logs
terraform apply

  1. TF_LOG_CORE y TF_LOG_PROVIDER: Logs Separados

Desde Terraform 0.15+, puedes separar los logs del núcleo de Terraform de los logs de los providers. Esto es muy útil para aislar si un problema está en la lógica de Terraform o en el provider específico.

# Solo logs del núcleo de Terraform (planificación, estado, etc.)
export TF_LOG_CORE=DEBUG
export TF_LOG_PROVIDER=OFF  # Silencia logs del provider
terraform plan

# Solo logs del provider (llamadas a la API de AWS, Azure, etc.)
export TF_LOG_CORE=OFF
export TF_LOG_PROVIDER=DEBUG
terraform apply

# Logs separados en archivos diferentes
export TF_LOG_CORE=DEBUG
export TF_LOG_PROVIDER=TRACE
# Nota: TF_LOG_PATH afecta a ambos; para separarlos completamente
# se recomienda ejecutar con uno activo a la vez

Cuándo usar cada uno:

  • TF_LOG_CORE=DEBUG: Cuando el problema parece estar en la lógica del plan, dependencias entre recursos, o el manejo del estado.
  • TF_LOG_PROVIDER=TRACE: Cuando el problema está en la comunicación con la API de AWS/Azure/GCP, problemas de autenticación o respuestas inesperadas de la API.

  1. Analizar Logs de Debug: Qué Buscar

Los logs de Terraform son extensos. Saber dónde mirar te ahorra horas de análisis.

Patrones importantes en los logs

# Buscar errores HTTP
grep "StatusCode\|status_code\|HTTP/1" terraform-debug.log

# Buscar problemas de autenticación
grep -i "auth\|credential\|token\|denied\|unauthorized" terraform-debug.log

# Buscar qué recursos se están procesando
grep "aws_\|azurerm_\|google_" terraform-debug.log | grep "Refreshing\|Creating\|Updating"

# Ver la secuencia de operaciones
grep "\[DEBUG\]\|\[INFO\]\|\[ERROR\]" terraform-debug.log | head -50

Anatomía de un log de TRACE

Con TF_LOG=TRACE verás las llamadas HTTP completas:

2024-01-15T10:30:00.000Z [TRACE] provider.terraform-provider-aws:
  Request Details:
  ---[ REQUEST ]----------------------------
  PUT /my-terraform-state-bucket HTTP/1.1
  Host: s3.us-east-1.amazonaws.com
  Authorization: AWS4-HMAC-SHA256 Credential=AKIA.../20240115/us-east-1/s3/aws4_request
  Content-Type: application/xml

  ---[ RESPONSE ]---------------------------
  HTTP/1.1 200 OK
  x-amz-request-id: ABC123DEF456
  ETag: "d41d8cd98f00b204e9800998ecf8427e"

  1. terraform console: Probar Expresiones Interactivamente

terraform console es un REPL (Read-Eval-Print Loop) que te permite evaluar expresiones HCL en el contexto de tu configuración actual y el estado de Terraform. Es invaluable para:

  • Probar funciones HCL antes de usarlas en el código
  • Inspeccionar valores del estado actual
  • Depurar expresiones complejas

Uso básico

# Iniciar la consola (requiere inicialización previa con terraform init)
terraform console
# Dentro de terraform console:

# Probar funciones de string
> upper("hello world")
"HELLO WORLD"

> replace("ami-123456", "ami-", "")
"123456"

# Probar funciones de lista
> length(["a", "b", "c"])
3

> element(["us-east-1a", "us-east-1b", "us-east-1c"], 1)
"us-east-1b"

# Probar funciones de mapa
> lookup({dev = "t2.micro", prod = "t2.large"}, "prod", "t2.micro")
"t2.large"

# Verificar expresiones condicionales
> var.environment == "prod" ? "t2.large" : "t2.micro"
# (Requiere que var.environment esté definida)

Inspeccionar el estado actual

Si ya hay recursos creados en el estado:

terraform console
# Ver atributos de un recurso existente en el estado
> aws_vpc.main.id
"vpc-0a1b2c3d4e5f67890"

> aws_vpc.main.cidr_block
"10.0.0.0/16"

# Ver outputs definidos
> output.vpc_id
"vpc-0a1b2c3d4e5f67890"

# Iterar sobre recursos creados con count
> aws_subnet.public[0].id
"subnet-0123456789abcdef0"

> [for s in aws_subnet.public : s.id]
[
  "subnet-0123456789abcdef0",
  "subnet-0fedcba9876543210",
]

Depurar expresiones complejas paso a paso

# Supón que tienes esta variable en tu configuración:
# variable "instance_types" {
#   default = {
#     dev  = "t2.micro"
#     stg  = "t2.small"
#     prod = "t2.large"
#   }
# }

# En terraform console:
> var.instance_types
{
  "dev" = "t2.micro"
  "prod" = "t2.large"
  "stg" = "t2.small"
}

# Probar el acceso a un elemento
> var.instance_types["dev"]
"t2.micro"

# Si hay un problema con una clave que no existe:
> var.instance_types["qa"]
# Esto daría un error que puedes ver antes de aplicarlo

  1. Outputs Temporales para Depuración

Una técnica efectiva es añadir outputs temporales para exponer valores intermedios que no son visibles de otra forma durante el terraform plan.

# En outputs.tf - Outputs temporales de depuración

# Ver qué AMI está seleccionando el data source
output "debug_ami_id" {
  value       = data.aws_ami.amazon_linux.id
  description = "DEBUG: AMI seleccionada por el data source"
}

# Ver las AZs disponibles
output "debug_availability_zones" {
  value       = data.aws_availability_zones.available.names
  description = "DEBUG: Lista de AZs disponibles"
}

# Ver la resolución de una expresión compleja
output "debug_subnet_ids" {
  value = {
    for idx, subnet in aws_subnet.public :
    "subnet-${idx}" => {
      id   = subnet.id
      cidr = subnet.cidr_block
      az   = subnet.availability_zone
    }
  }
  description = "DEBUG: Mapa detallado de subnets creadas"
}
# Ejecutar plan para ver los valores que tendrían esos outputs
terraform plan

# O aplicar y ver los valores reales
terraform apply

# Una vez resuelto el problema, eliminar los outputs de debug
# o marcarlos para que no se muestren en la salida normal:
# Output de debug que no contamina la salida de apply
output "debug_ami_id" {
  value     = data.aws_ami.amazon_linux.id
  sensitive = true  # Oculta el valor en la salida, pero sigue siendo accesible
}

  1. Inspeccionar el Plan Detallado con -json

# Generar el plan y guardarlo
terraform plan -out=debug.tfplan

# Convertir el plan a JSON y analizarlo
terraform show -json debug.tfplan > debug-plan.json

# Ver la estructura del plan
cat debug-plan.json | jq '.'

# Ver solo los cambios planeados
cat debug-plan.json | jq '.resource_changes[] | {address: .address, action: .change.actions}'

# Ver los valores antes y después de un recurso específico
cat debug-plan.json | jq '.resource_changes[] | select(.address == "aws_instance.web") | .change'

# Ver las variables usadas en el plan
cat debug-plan.json | jq '.variables'

Ejemplo de salida JSON útil:

{
  "resource_changes": [
    {
      "address": "aws_instance.web",
      "type": "aws_instance",
      "name": "web",
      "change": {
        "actions": ["update"],
        "before": {
          "instance_type": "t2.micro",
          "tags": {"Name": "web-server"}
        },
        "after": {
          "instance_type": "t2.small",
          "tags": {"Name": "web-server"}
        },
        "after_unknown": {}
      }
    }
  ]
}

  1. terraform providers: Verificar Configuración de Providers

# Mostrar los providers requeridos y sus versiones
terraform providers

# Salida típica:
# Providers required by configuration:
# .
# ├── provider[registry.terraform.io/hashicorp/aws] ~> 5.0
# └── provider[registry.terraform.io/hashicorp/random] ~> 3.0

# Ver el árbol de providers incluyendo módulos
terraform providers tree  # (disponible en versiones recientes)

# Bloquear las versiones actuales en .terraform.lock.hcl
terraform providers lock

# Actualizar el lockfile para una plataforma específica
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64

  1. Tabla de Comandos de Diagnóstico

Comando Descripción Cuándo usarlo
terraform validate Valida sintaxis y referencias sin conectarse a la nube Primera verificación antes de cualquier operación
terraform fmt -check Verifica el formato del código sin modificarlo En pipelines de CI/CD
terraform plan -detailed-exitcode Código de salida 0=sin cambios, 1=error, 2=hay cambios En automatización para detectar drift
terraform plan -json Plan en formato JSON para análisis programático Analizar cambios complejos o en scripts
terraform show Muestra el estado actual o un plan guardado Inspeccionar lo que Terraform sabe de la infraestructura
terraform state list Lista todos los recursos en el estado Ver qué recursos gestiona Terraform
terraform state show <recurso> Muestra todos los atributos de un recurso del estado Inspeccionar valores específicos de un recurso
terraform providers Lista providers requeridos y sus versiones Verificar que los providers correctos están configurados
terraform console REPL interactivo para evaluar expresiones Probar funciones y expresiones HCL antes de usarlas
TF_LOG=DEBUG terraform apply Logs detallados de la operación Investigar fallos no explicados por los mensajes de error

  1. Depuración de Módulos: Conocer el Origen de los Errores

Los errores en módulos tienen una localización más compleja porque muestran la ruta completa:

╷
│ Error: Unsupported argument
│
│   on .terraform/modules/vpc/main.tf line 45, in resource "aws_vpc" "this":
│   45:   enable_classiclink = var.enable_classiclink
│
│ An argument named "enable_classiclink" is not expected here.
╵

Interpretación: El error está dentro del módulo vpc (en .terraform/modules/vpc/main.tf), no en tu código. Esto indica que la versión del módulo que estás usando es incompatible con la versión del provider.

Cómo encontrar el origen:

# Ver qué módulos están descargados y sus versiones
terraform providers

# Ver la fuente de un módulo en particular
cat .terraform/modules/modules.json | jq '.Modules[] | select(.Key == "vpc")'

# Ver las dependencias del módulo
cat .terraform/modules/vpc/.terraform.lock.hcl

Solución típica: Actualizar o fijar la versión del módulo en source:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.2"  # Fijar versión compatible con tu provider
  # ...
}

  1. Ejemplo Completo de Sesión de Depuración

Supongamos que ejecutas terraform apply y obtienes un error de AccessDenied. Aquí está la sesión completa de depuración:

Paso 1: Reproducir con logs DEBUG

export TF_LOG=DEBUG
export TF_LOG_PATH=./debug-session.log
terraform apply 2>&1 | tee apply-output.log

Paso 2: Analizar el log

# Buscar el error específico
grep -n "AccessDenied\|403\|Forbidden" ./debug-session.log

# Salida:
# 1523: [DEBUG] HTTP Response: 403 Forbidden
# 1524: [DEBUG] Body: {"Code":"AccessDenied","Message":"User: arn:aws:iam::123456789:user/terraform is not authorized to perform: s3:CreateBucket"}

Paso 3: Identificar el recurso y la acción

# Ver las líneas alrededor del error para contexto
sed -n '1515,1535p' ./debug-session.log

Paso 4: Verificar las credenciales actuales

# Fuera de Terraform, verificar qué usuario/rol está activo
aws sts get-caller-identity

# Salida:
# {
#     "UserId": "AIDA...",
#     "Account": "123456789",
#     "Arn": "arn:aws:iam::123456789:user/terraform"
# }

# Verificar los permisos de ese usuario en AWS
aws iam list-attached-user-policies --user-name terraform

Paso 5: Usar terraform console para verificar la configuración

terraform console
# Verificar que la región está correcta
> var.aws_region
"us-east-1"

# Verificar el nombre del bucket (por si ya existe)
> aws_s3_bucket.terraform_state.bucket
# Si el recurso no existe aún:
# Error: A managed resource "aws_s3_bucket" "terraform_state" has not been declared

Paso 6: Corregir y verificar

# Después de añadir los permisos s3:CreateBucket al usuario IAM
unset TF_LOG
unset TF_LOG_PATH

# Validar de nuevo
terraform validate

# Aplicar de nuevo
terraform apply

  1. Ejercicio: Depurar una Configuración que Falla

Tienes la siguiente configuración que falla al ejecutar terraform apply:

# variables.tf
variable "environment" {
  type = string
}

variable "instance_count" {
  type    = number
  default = 2
}

# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-22.04-amd64-server-*"]
  }
}

locals {
  instance_types = {
    dev  = "t2.micro"
    prod = "t2.large"
  }

  # Esta expresión puede fallar - ¿por qué?
  selected_type = local.instance_types[var.environment]
}

resource "aws_instance" "app" {
  count = var.instance_count

  ami           = data.aws_ami.ubuntu.id
  instance_type = local.selected_type

  tags = {
    Name        = "app-${var.environment}-${count.index}"
    Environment = var.environment
  }
}

output "instance_ids" {
  value = aws_instance.app[*].id
}

output "instance_ips" {
  # Esta expresión también puede tener un problema
  value = [for inst in aws_instance.app : inst.public_ip if inst.public_ip != ""]
}

El error al ejecutar:

terraform apply -var="environment=staging"
╷
│ Error: Invalid index
│
│   on main.tf line 38, in locals:
│   38:   selected_type = local.instance_types[var.environment]
│     ├────────────────
│     │ local.instance_types is object with 2 attributes
│     │ var.environment is "staging"
│
│ The given key does not identify an element in this collection value.
╵

Tu tarea:

  1. Usa terraform console para investigar el problema.
  2. Identifica todos los posibles fallos en la configuración.
  3. Corrige la configuración para que sea robusta.

  1. Solución Paso a Paso

Paso 1: Investigar con terraform console

# Primero, ejecutar terraform init si no está hecho
terraform init

# Abrir la consola con la variable problemática
terraform console -var="environment=staging"
# Investigar el mapa
> local.instance_types
{
  "dev" = "t2.micro"
  "prod" = "t2.large"
}

# El problema es evidente: "staging" no existe en el mapa
# Probar la función lookup con valor por defecto
> lookup(local.instance_types, "staging", "t2.micro")
"t2.micro"

# Probar con contains para validar
> contains(keys(local.instance_types), "staging")
false

# Verificar los valores válidos
> keys(local.instance_types)
tolist([
  "dev",
  "prod",
])

Paso 2: Identificar todos los problemas

Problema 1: La expresión local.instance_types[var.environment] falla si environment no es dev o prod. No hay valor por defecto ni validación.

Problema 2: La variable environment no tiene validación de valores permitidos.

Problema 3: El output instance_ips filtra por public_ip != "" pero en instancias sin IP pública, este valor puede ser null, no "".

Paso 3: Configuración corregida

# variables.tf - CORREGIDO
variable "environment" {
  type        = string
  description = "Entorno de despliegue (dev, stg, prod)"

  # Validación para rechazar valores no soportados
  validation {
    condition     = contains(["dev", "stg", "prod"], var.environment)
    error_message = "El entorno debe ser 'dev', 'stg' o 'prod'."
  }
}

variable "instance_count" {
  type    = number
  default = 2

  validation {
    condition     = var.instance_count > 0 && var.instance_count <= 10
    error_message = "El número de instancias debe estar entre 1 y 10."
  }
}

# main.tf - CORREGIDO
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-22.04-amd64-server-*"]
  }
}

locals {
  instance_types = {
    dev  = "t2.micro"
    stg  = "t2.small"   # Añadido stg
    prod = "t2.large"
  }

  # Corrección 1: lookup con valor por defecto
  selected_type = lookup(local.instance_types, var.environment, "t2.micro")
}

resource "aws_instance" "app" {
  count = var.instance_count

  ami           = data.aws_ami.ubuntu.id
  instance_type = local.selected_type

  tags = {
    Name        = "app-${var.environment}-${count.index}"
    Environment = var.environment
  }
}

output "instance_ids" {
  value = aws_instance.app[*].id
}

# Corrección 2: usar != null en lugar de != ""
output "instance_ips" {
  value = [for inst in aws_instance.app : inst.public_ip if inst.public_ip != null]
}

Verificar la corrección:

# Probar con un entorno válido
terraform plan -var="environment=stg"

# Probar que la validación funciona con valor inválido
terraform plan -var="environment=qa"
# Debe mostrar:
# │ Error: Invalid value for variable
# │   var.environment is "qa"
# │ El entorno debe ser 'dev', 'stg' o 'prod'.

  1. Consejos Avanzados de Depuración

Consejo 1: Usar TF_CLI_ARGS para flags persistentes

# En lugar de escribir -var-file=... en cada comando
export TF_CLI_ARGS_plan="-var-file=dev.tfvars"
export TF_CLI_ARGS_apply="-var-file=dev.tfvars"

# Ahora terraform plan usará automáticamente -var-file=dev.tfvars
terraform plan

Consejo 2: Aislar el recurso problemático con -target

# Aplicar solo un recurso específico para depurar
terraform plan -target=aws_instance.web
terraform apply -target=aws_instance.web

Advertencia: -target es una herramienta de depuración, no debe usarse en producción de forma regular, ya que puede dejar el estado inconsistente.

Consejo 3: Usar terraform state show para inspeccionar el estado

# Ver todos los atributos de un recurso específico en el estado
terraform state show aws_instance.web

# Salida:
# resource "aws_instance" "web" {
#   ami                    = "ami-0c55b159cbfafe1f0"
#   instance_type          = "t2.micro"
#   private_ip             = "10.0.1.5"
#   public_ip              = "54.123.45.67"
#   ...
# }

Consejo 4: Comparar plan anterior con el actual

# Guardar plan actual
terraform plan -out=before.tfplan
terraform show -json before.tfplan > before.json

# Después de cambios, generar nuevo plan
terraform plan -out=after.tfplan
terraform show -json after.tfplan > after.json

# Comparar (requiere jq instalado)
diff <(jq '.resource_changes' before.json) <(jq '.resource_changes' after.json)

Resumen

En esta lección has aprendido a usar las herramientas de depuración de Terraform de forma sistemática:

  • TF_LOG con sus cinco niveles (TRACE, DEBUG, INFO, WARN, ERROR) para obtener visibilidad completa del comportamiento interno de Terraform.
  • TF_LOG_PATH para guardar logs extensos en archivos analizables con grep y otras herramientas.
  • TF_LOG_CORE y TF_LOG_PROVIDER para aislar si el problema está en el núcleo de Terraform o en el provider específico.
  • terraform console como REPL interactivo para probar expresiones HCL antes de aplicarlas y para inspeccionar el estado.
  • Outputs temporales como técnica para exponer valores intermedios durante el desarrollo.
  • terraform plan -json para analizar cambios programáticamente.
  • Un flujo de depuración completo aplicado a un caso real de AccessDenied.

En la siguiente lección abordaremos los problemas de gestión del estado, que son a menudo los más delicados y que requieren mayor precaución en equipos de trabajo: estados corruptos, drift, recursos huérfanos y bloqueos indefinidos.

© Copyright 2026. Todos los derechos reservados