Introducción

Probar el código de infraestructura es una práctica que durante años fue ignorada o considerada secundaria. Sin embargo, a medida que la infraestructura como código (IaC) ha madurado, la industria ha llegado a una conclusión clara: la infraestructura sin pruebas es infraestructura que fallará en el peor momento posible.

Un error en el código de Terraform puede resultar en la destrucción accidental de bases de datos de producción, la apertura de puertos de seguridad no deseados, o el aprovisionamiento incorrecto de recursos que genera costos inesperados. Las pruebas automatizadas son la red de seguridad que previene estos escenarios.

En este módulo explorarás la pirámide de testing para Terraform, desde las validaciones más rápidas hasta las pruebas de integración completas.


¿Por qué Probar el Código de Infraestructura?

  • Detectar errores temprano: Un error de sintaxis o de lógica detectado localmente es mucho más barato que uno detectado en producción
  • Garantizar conformidad de seguridad: Las pruebas pueden verificar que no se crean recursos con configuraciones inseguras
  • Documentación ejecutable: Las pruebas describen el comportamiento esperado de la infraestructura
  • Confianza para refactorizar: Con pruebas, puedes reorganizar módulos sin miedo a romper el comportamiento
  • Validación continua: En un pipeline CI/CD, las pruebas se ejecutan automáticamente en cada cambio
  • Cumplimiento normativo: Las organizaciones reguladas necesitan demostrar que sus controles de infraestructura son verificados

Niveles de Testing para Terraform

La pirámide de testing de infraestructura tiene cuatro niveles, de menor a mayor costo y tiempo de ejecución:

                      /\
                     /  \
                    /    \
                   / E2E  \          Minutos a horas
                  /  Tests \         Costo: Alto
                 /──────────\
                /Integration \       Minutos
               /    Tests    \       Costo: Medio-Alto
              /────────────────\
             /   Unit Tests     \    Segundos a minutos
            / (terraform test)  \    Costo: Bajo
           /────────────────────\
          /   Static Analysis    \   Milisegundos
         / (fmt, validate, lint) \   Costo: Muy bajo
        /──────────────────────────\

Nivel 1: Análisis Estático (Linting)

No crea ningún recurso real. Analiza el código sin ejecutarlo.

  • Verifica formato del código
  • Detecta errores de sintaxis
  • Identifica patrones problemáticos o inseguros
  • Tiempo de ejecución: segundos

Nivel 2: Pruebas Unitarias

Verifica la lógica de la configuración sin crear recursos reales (o con recursos mínimos/mocked).

  • Valida que las variables producen los valores esperados
  • Verifica que los locals calculan correctamente
  • Comprueba condiciones y validaciones
  • Tiempo de ejecución: segundos a pocos minutos

Nivel 3: Pruebas de Integración

Crea recursos reales en un entorno de pruebas y verifica su funcionamiento.

  • Despliega la infraestructura completa en AWS/Azure/GCP de prueba
  • Ejecuta verificaciones sobre los recursos creados
  • Destruye todo al finalizar
  • Tiempo de ejecución: minutos (5-30 minutos típicamente)

Nivel 4: Pruebas End-to-End

Verifica el sistema completo funcionando de forma integrada.

  • Despliega la infraestructura completa
  • Despliega la aplicación sobre ella
  • Ejecuta pruebas funcionales de la aplicación
  • Tiempo de ejecución: 30 minutos a varias horas

Herramientas de Testing para Terraform

Tabla Comparativa de Herramientas

Herramienta Nivel Lenguaje Crea recursos reales Requiere credenciales de nube Velocidad
terraform fmt Estático HCL No No Instantánea
terraform validate Estático HCL No No Segundos
tflint Estático HCL No Opcional Segundos
checkov Estático (seguridad) Python No No Segundos
terrascan Estático (seguridad) HCL No No Segundos
terraform test Unitario/Integración HCL (.tftest.hcl) Opcional Si (para integración) Segundos a minutos
Terratest Integración/E2E Go Si Si Minutos

Herramienta 1: terraform fmt

terraform fmt formatea el código de Terraform según el estilo oficial. Aunque no es estrictamente una herramienta de pruebas, asegurar que el código está bien formateado es el primer paso del análisis estático.

# Formatear todos los archivos .tf del directorio actual y subdirectorios
terraform fmt -recursive

# Solo verificar si hay archivos que necesitan formato (útil en CI/CD)
# Devuelve código de salida 0 si todo está formateado, 1 si hay cambios pendientes
terraform fmt -check -recursive

# Ver qué cambios haría sin aplicarlos (modo diff)
terraform fmt -diff -check

# Ejemplo de output cuando hay problemas de formato:
# variables.tf
# main.tf
# (devuelve código 1, lo que hace fallar el pipeline de CI)
# ANTES de terraform fmt (formato incorrecto):
resource "aws_instance" "web" {
ami="ami-0c55b159cbfafe1f0"
  instance_type    =   "t3.micro"
tags={Name="mi-instancia",Environment="dev"}
}

# DESPUÉS de terraform fmt (formato correcto):
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
  tags = {
    Name        = "mi-instancia"
    Environment = "dev"
  }
}

Herramienta 2: terraform validate

terraform validate verifica la sintaxis y la coherencia interna de los archivos de configuración sin acceder a ninguna API de proveedor.

# Ejecutar validación en el directorio actual
terraform validate

# Output exitoso:
# Success! The configuration is valid.

# Output con error:
# ╷
# │ Error: Reference to undeclared resource
# │
# │   on main.tf line 15, in resource "aws_instance" "web":
# │    15:   subnet_id = aws_subnet.public.id
# │
# │ A managed resource "aws_subnet" "public" has not been declared in the root module.
# ╵

# IMPORTANTE: terraform validate necesita que se haya ejecutado "terraform init" primero
terraform init -backend=false  # Inicializar sin configurar el backend (útil en CI)
terraform validate

Qué detecta terraform validate

  • Referencias a recursos que no existen
  • Tipos de variables incorrectos
  • Argumentos desconocidos en recursos
  • Bloques requeridos que faltan
  • Expresiones inválidas

Qué NO detecta terraform validate

  • Recursos que ya existen y causarán conflicto
  • Cuotas de la nube excedidas
  • Permisos insuficientes
  • Problemas de configuración específicos del proveedor

Herramienta 3: tflint

tflint es un linter avanzado que detecta problemas que terraform validate no puede encontrar, como tipos de instancia no válidos en AWS o reglas de nomenclatura personalizadas.

# Instalación
brew install tflint  # macOS
# o usando el instalador oficial:
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

# Inicializar tflint con los plugins necesarios
tflint --init

# Ejecutar tflint
tflint

# Ejecutar de forma recursiva en subdirectorios
tflint --recursive
# .tflint.hcl - Archivo de configuración de tflint

config {
  # Habilitar el plugin de AWS para reglas específicas de ese proveedor
  plugin "aws" {
    enabled = true
    version = "0.27.0"
    source  = "github.com/terraform-linters/tflint-ruleset-aws"
  }
}

# Regla: tipos de instancia EC2 inválidos
rule "aws_instance_invalid_type" {
  enabled = true
}

# Regla: AMIs deprecadas o incorrectas
rule "aws_instance_invalid_ami" {
  enabled = true
}

# Regla: variables sin descripción
rule "terraform_documented_variables" {
  enabled = true
}

# Regla: outputs sin descripción
rule "terraform_documented_outputs" {
  enabled = true
}

# Regla: nombres de recursos que no siguen snake_case
rule "terraform_naming_convention" {
  enabled = true

  resource {
    format = "snake_case"
  }

  variable {
    format = "snake_case"
  }

  output {
    format = "snake_case"
  }
}
# Output de tflint cuando encuentra un problema:
# 2 issue(s) found:
#
# Warning: "t2.micro" is previous generation instance type. (aws_instance_invalid_type)
#
#   on main.tf line 5:
#    5:   instance_type = "t2.micro"
#
# Notice: `description` attribute is required (terraform_documented_variables)
#
#   on variables.tf line 1:
#    1: variable "region" {

Herramienta 4: checkov

checkov es una herramienta de análisis estático de seguridad que verifica las configuraciones de Terraform contra más de 1.000 políticas de seguridad predefinidas (CIS Benchmarks, NIST, SOC2, etc.).

# Instalación
pip install checkov

# Escanear el directorio actual
checkov -d .

# Escanear y generar reporte en formato JUnit (para CI/CD)
checkov -d . --output junitxml > checkov-report.xml

# Escanear y exportar en múltiples formatos
checkov -d . --output cli --output json --output-file-path .

# Omitir checks específicos (cuando hay falsos positivos o excepciones documentadas)
checkov -d . --skip-check CKV_AWS_79,CKV_AWS_8
# Ejemplo de output de checkov:
# Passed checks: 15, Failed checks: 3, Skipped checks: 0

# Check: CKV_AWS_79: "Ensure Instance Metadata Service Version 1 is not enabled"
# FAILED for resource: aws_instance.web_server
# File: /main.tf:10-25

# Check: CKV_AWS_8: "Ensure all data stored in the Launch configuration or instance is securely encrypted"
# FAILED for resource: aws_instance.web_server
# File: /main.tf:10-25

# Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access"
# FAILED for resource: aws_s3_bucket.public_assets
# File: /storage.tf:5-15

Cómo corregir los findings de checkov:

# ANTES (inseguro - falla checkov):
resource "aws_instance" "web_server" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = "t3.micro"
  # Sin cifrado de volumen raíz
  # Sin deshabilitar IMDSv1
}

# DESPUÉS (seguro - pasa checkov):
resource "aws_instance" "web_server" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = "t3.micro"

  # Deshabilitar IMDSv1 (solo permitir IMDSv2)
  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"  # Requiere IMDSv2
    http_put_response_hop_limit = 1           # Limita el alcance del token
  }

  # Cifrar el volumen raíz
  root_block_device {
    encrypted   = true          # Cifrado con la clave por defecto de AWS KMS
    volume_type = "gp3"         # Tipo de volumen moderno y eficiente
    volume_size = 20            # Tamaño en GB
  }
}

Herramienta 5: terrascan

terrascan es similar a checkov pero con enfoque en compliance y políticas empresariales. Soporta políticas escritas en Rego (el lenguaje de OPA - Open Policy Agent).

# Instalación
brew install terrascan  # macOS
# o descarga desde: https://github.com/tenable/terrascan/releases

# Escanear el directorio actual con las reglas de AWS
terrascan scan -t aws

# Escanear con reglas de múltiples proveedores
terrascan scan -t aws -t k8s

# Escanear en formato JSON para integración con CI/CD
terrascan scan -t aws -o json

# Ver todas las políticas disponibles
terrascan list policies -t aws

Herramienta 6: terraform test (Nativo, Terraform v1.6+)

Desde la versión 1.6, Terraform incluye un framework de testing nativo que permite escribir pruebas en HCL. Esta es la herramienta recomendada para pruebas unitarias y de integración.

Estructura de un archivo de prueba

Los archivos de prueba tienen la extensión .tftest.hcl y se colocan en el mismo directorio del módulo o en un subdirectorio tests/:

mi-modulo/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
    ├── unit_tests.tftest.hcl
    └── integration_tests.tftest.hcl

Ejecutar las pruebas

# Ejecutar todos los archivos .tftest.hcl en el directorio actual
terraform test

# Ejecutar solo un archivo de prueba específico
terraform test -filter=tests/unit_tests.tftest.hcl

# Ejecutar con output detallado
terraform test -verbose

Ejemplo Completo de Archivo .tftest.hcl

# tests/unit_tests.tftest.hcl
# Pruebas unitarias para el módulo de networking

# ============================================================
# BLOQUE run: Define una prueba individual
# ============================================================

# PRUEBA 1: Verificar que el módulo aplica sin errores con valores por defecto
run "valid_configuration_with_defaults" {
  # command = plan  => Solo ejecuta "terraform plan" (sin crear recursos reales)
  # command = apply => Crea recursos reales (para pruebas de integración)
  command = plan

  # Valores de variables a usar en esta prueba
  variables {
    environment = "test"
    vpc_cidr    = "10.0.0.0/16"
    region      = "us-east-1"
  }

  # assert: Condiciones que deben ser verdaderas para que la prueba pase
  assert {
    # condition: La expresión que se evalúa (debe ser booleana)
    condition = output.vpc_cidr == "10.0.0.0/16"

    # error_message: Mensaje que se muestra si la condición es falsa
    error_message = "El CIDR de la VPC debe coincidir con el valor de entrada"
  }

  assert {
    # Verificar que el nombre del recurso sigue la convención esperada
    condition     = aws_vpc.main.tags["Environment"] == "test"
    error_message = "La etiqueta Environment debe ser 'test'"
  }
}

# PRUEBA 2: Verificar el comportamiento con diferentes entornos
run "production_environment_naming" {
  command = plan

  variables {
    environment = "prod"
    vpc_cidr    = "10.1.0.0/16"
    region      = "us-east-1"
  }

  assert {
    # Verificar que el nombre del recurso incluye el entorno
    condition     = can(regex("prod", aws_vpc.main.tags["Name"]))
    error_message = "El nombre de la VPC debe contener 'prod' en entorno de producción"
  }
}

# PRUEBA 3: Verificar que se rechaza un CIDR inválido
run "invalid_vpc_cidr_validation" {
  command = plan

  variables {
    environment = "dev"
    vpc_cidr    = "256.0.0.0/16"  # CIDR inválido
    region      = "us-east-1"
  }

  # expect_failures: Lista de recursos/variables que SE ESPERA que fallen
  # Útil para probar que las validaciones funcionan correctamente
  expect_failures = [
    var.vpc_cidr,  # Se espera que esta variable falle la validación
  ]
}
# tests/integration_tests.tftest.hcl
# Pruebas de integración: CREAN RECURSOS REALES en AWS
# ADVERTENCIA: Estas pruebas tienen costo en la nube y toman más tiempo

# Configuración del provider para las pruebas de integración
# Terraform test permite sobreescribir la configuración del provider
provider "aws" {
  region = "us-east-1"
  # Las credenciales se toman de las variables de entorno AWS_*
}

# PRUEBA DE INTEGRACIÓN 1: Verificar que la VPC se crea correctamente
run "vpc_is_created_with_correct_configuration" {
  command = apply  # Crea recursos reales

  variables {
    environment = "tftest"    # Prefijo específico para recursos de prueba
    vpc_cidr    = "10.99.0.0/16"  # CIDR reservado para pruebas
    region      = "us-east-1"
  }

  assert {
    # Verificar que el output contiene un ID de VPC válido
    condition     = can(regex("^vpc-", output.vpc_id))
    error_message = "El ID de la VPC debe comenzar con 'vpc-'"
  }

  assert {
    # Verificar que el estado de la VPC es 'available'
    condition     = aws_vpc.main.state == "available"
    error_message = "La VPC debe estar en estado 'available' después de crearse"
  }
}
# NOTA: terraform test destruye automáticamente los recursos creados con
# command = apply al finalizar cada bloque "run"

# PRUEBA DE INTEGRACIÓN 2: Verificar conectividad entre subnets
run "subnets_are_in_different_availability_zones" {
  command = apply

  variables {
    environment         = "tftest"
    vpc_cidr            = "10.99.0.0/16"
    region              = "us-east-1"
    availability_zones  = ["us-east-1a", "us-east-1b"]
  }

  assert {
    condition = (
      aws_subnet.public[0].availability_zone != aws_subnet.public[1].availability_zone
    )
    error_message = "Las subnets públicas deben estar en diferentes zonas de disponibilidad"
  }
}

Escribir Pruebas Efectivas con terraform test

Guía de qué verificar en cada tipo de módulo:

# tests/networking_module_tests.tftest.hcl

# PATRONES COMUNES EN PRUEBAS DE TERRAFORM:

# Patrón 1: Verificar valores de outputs
run "outputs_have_correct_format" {
  command = plan
  variables {
    environment = "test"
    vpc_cidr    = "10.0.0.0/16"
  }

  # can() evalúa si una expresión se ejecuta sin error
  # regex() devuelve error si no hay coincidencia, lo que hace can() = false
  assert {
    condition     = can(regex("^10\\.", output.vpc_cidr))
    error_message = "El CIDR debe pertenecer al bloque 10.x.x.x"
  }
}

# Patrón 2: Verificar etiquetas de recursos
run "all_resources_have_required_tags" {
  command = plan
  variables {
    environment = "prod"
    vpc_cidr    = "10.0.0.0/16"
  }

  assert {
    # contains() verifica si un mapa tiene una clave específica
    condition     = contains(keys(aws_vpc.main.tags), "Environment")
    error_message = "Todos los recursos deben tener la etiqueta 'Environment'"
  }

  assert {
    condition     = contains(keys(aws_vpc.main.tags), "ManagedBy")
    error_message = "Todos los recursos deben tener la etiqueta 'ManagedBy'"
  }
}

# Patrón 3: Verificar que no hay recursos públicamente accesibles por defecto
run "no_public_access_by_default" {
  command = plan
  variables {
    environment        = "test"
    vpc_cidr           = "10.0.0.0/16"
    enable_nat_gateway = false
  }

  assert {
    condition     = aws_subnet.private.map_public_ip_on_launch == false
    error_message = "Las subnets privadas no deben asignar IPs públicas automáticamente"
  }
}

# Patrón 4: Verificar conteos de recursos
run "correct_number_of_subnets_created" {
  command = plan
  variables {
    environment        = "test"
    vpc_cidr           = "10.0.0.0/16"
    availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
  }

  assert {
    # length() cuenta el número de elementos en una lista o mapa
    condition     = length(aws_subnet.public) == 3
    error_message = "Deben crearse 3 subnets públicas, una por zona de disponibilidad"
  }
}

Integración de Tests en el Pipeline CI/CD

# .github/workflows/terraform-tests.yml
# Pipeline de GitHub Actions para ejecutar pruebas de Terraform

name: Terraform Tests

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  # ============================================================
  # JOB 1: Análisis estático (rápido, sin credenciales de nube)
  # ============================================================
  static-analysis:
    name: Static Analysis
    runs-on: ubuntu-latest

    steps:
      - name: Checkout código
        uses: actions/checkout@v4

      - name: Configurar Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.7.0"

      - name: Inicializar Terraform (sin backend)
        run: terraform init -backend=false

      - name: Verificar formato (terraform fmt)
        run: terraform fmt -check -recursive
        # Falla si algún archivo no está formateado correctamente

      - name: Validar sintaxis (terraform validate)
        run: terraform validate

      - name: Instalar tflint
        uses: terraform-linters/setup-tflint@v4
        with:
          tflint_version: v0.50.0

      - name: Inicializar tflint
        run: tflint --init

      - name: Ejecutar tflint
        run: tflint --recursive

      - name: Instalar checkov
        run: pip install checkov

      - name: Ejecutar checkov
        run: |
          checkov -d . \
            --output cli \
            --output junitxml \
            --output-file-path . \
            --skip-check CKV_AWS_79  # Excluir si hay razón documentada
        # El resultado XML se puede publicar como reporte en GitHub

  # ============================================================
  # JOB 2: Pruebas unitarias con terraform test
  # ============================================================
  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: static-analysis  # Solo ejecutar si el análisis estático pasa

    steps:
      - name: Checkout código
        uses: actions/checkout@v4

      - name: Configurar Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.7.0"

      - name: Inicializar Terraform
        run: terraform init -backend=false

      - name: Ejecutar pruebas unitarias
        run: terraform test -filter=tests/unit_tests.tftest.hcl
        # Las pruebas unitarias usan command=plan, no necesitan credenciales de nube

  # ============================================================
  # JOB 3: Pruebas de integración (solo en merge a main)
  # ============================================================
  integration-tests:
    name: Integration Tests
    runs-on: ubuntu-latest
    needs: unit-tests
    # Solo ejecutar en merge a main, no en cada PR (costo en la nube)
    if: github.ref == 'refs/heads/main'

    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_TEST }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_TEST }}
      AWS_DEFAULT_REGION: us-east-1

    steps:
      - name: Checkout código
        uses: actions/checkout@v4

      - name: Configurar Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.7.0"

      - name: Inicializar Terraform
        run: terraform init

      - name: Ejecutar pruebas de integración
        run: terraform test -filter=tests/integration_tests.tftest.hcl
        # terraform test destruye los recursos automáticamente al finalizar

Ejercicio: Escribir Pruebas para un Módulo de Terraform

Descripción del módulo a probar

Tienes el siguiente módulo que crea un bucket S3 seguro:

# modules/s3-secure-bucket/main.tf

variable "bucket_name" {
  description = "Nombre del bucket S3"
  type        = string
}

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

variable "enable_versioning" {
  description = "Habilitar versionado del bucket"
  type        = bool
  default     = true

  validation {
    condition     = var.enable_versioning == true || var.environment == "dev"
    error_message = "El versionado solo puede deshabilitarse en entorno dev."
  }
}

resource "aws_s3_bucket" "main" {
  bucket = "${var.bucket_name}-${var.environment}"

  tags = {
    Name        = "${var.bucket_name}-${var.environment}"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "main" {
  bucket = aws_s3_bucket.main.id

  versioning_configuration {
    status = var.enable_versioning ? "Enabled" : "Suspended"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
  bucket = aws_s3_bucket.main.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

output "bucket_id" {
  description = "ID del bucket S3"
  value       = aws_s3_bucket.main.id
}

output "bucket_arn" {
  description = "ARN del bucket S3"
  value       = aws_s3_bucket.main.arn
}

Tu tarea: Escribe un archivo .tftest.hcl que verifique:

  1. El nombre del bucket incluye el entorno
  2. El versionado está habilitado por defecto
  3. Se rechaza deshabilitar el versionado en producción
  4. La encriptación está configurada

Solución

# modules/s3-secure-bucket/tests/bucket_tests.tftest.hcl

# PRUEBA 1: El nombre del bucket incluye el entorno
run "bucket_name_includes_environment" {
  command = plan

  variables {
    bucket_name       = "mi-app-datos"
    environment       = "prod"
    enable_versioning = true
  }

  assert {
    # El ID del bucket debe contener el nombre y el entorno concatenados
    condition     = aws_s3_bucket.main.bucket == "mi-app-datos-prod"
    error_message = "El nombre del bucket debe ser '{bucket_name}-{environment}'"
  }

  assert {
    # La etiqueta Name debe coincidir con el nombre del bucket
    condition     = aws_s3_bucket.main.tags["Name"] == "mi-app-datos-prod"
    error_message = "La etiqueta Name debe coincidir con el nombre del bucket"
  }
}

# PRUEBA 2: El versionado está habilitado por defecto
run "versioning_enabled_by_default" {
  command = plan

  variables {
    bucket_name = "mi-bucket"
    environment = "staging"
    # No especificamos enable_versioning, debe usar el valor por defecto (true)
  }

  assert {
    condition = (
      aws_s3_bucket_versioning.main.versioning_configuration[0].status == "Enabled"
    )
    error_message = "El versionado debe estar habilitado por defecto"
  }
}

# PRUEBA 3: Se rechaza deshabilitar el versionado en producción
run "cannot_disable_versioning_in_production" {
  command = plan

  variables {
    bucket_name       = "mi-bucket"
    environment       = "prod"
    enable_versioning = false  # Debe fallar la validación
  }

  # Esperamos que la variable falle su validación
  expect_failures = [
    var.enable_versioning,
  ]
}

# PRUEBA 4: Se permite deshabilitar el versionado en dev
run "can_disable_versioning_in_dev" {
  command = plan

  variables {
    bucket_name       = "mi-bucket"
    environment       = "dev"
    enable_versioning = false  # Permitido en dev
  }

  assert {
    condition = (
      aws_s3_bucket_versioning.main.versioning_configuration[0].status == "Suspended"
    )
    error_message = "El versionado debe estar suspendido cuando enable_versioning=false"
  }
}

# PRUEBA 5: La encriptación está siempre configurada
run "encryption_is_always_configured" {
  command = plan

  variables {
    bucket_name = "mi-bucket"
    environment = "prod"
  }

  assert {
    condition = (
      aws_s3_bucket_server_side_encryption_configuration.main
        .rule[0]
        .apply_server_side_encryption_by_default[0]
        .sse_algorithm == "AES256"
    )
    error_message = "La encriptación AES256 debe estar siempre configurada"
  }
}

# PRUEBA 6: Las etiquetas requeridas están presentes
run "required_tags_are_present" {
  command = plan

  variables {
    bucket_name = "mi-bucket"
    environment = "prod"
  }

  assert {
    condition     = contains(keys(aws_s3_bucket.main.tags), "Environment")
    error_message = "La etiqueta 'Environment' es requerida en todos los buckets"
  }

  assert {
    condition     = contains(keys(aws_s3_bucket.main.tags), "ManagedBy")
    error_message = "La etiqueta 'ManagedBy' es requerida en todos los buckets"
  }

  assert {
    condition     = aws_s3_bucket.main.tags["ManagedBy"] == "terraform"
    error_message = "El valor de 'ManagedBy' debe ser 'terraform'"
  }
}

Ejecutar las pruebas:

# Navegar al módulo
cd modules/s3-secure-bucket

# Inicializar (necesario aunque no haya backend)
terraform init -backend=false

# Ejecutar todas las pruebas
terraform test

# Output esperado:
# tests/bucket_tests.tftest.hcl... in progress
#   run "bucket_name_includes_environment"... pass
#   run "versioning_enabled_by_default"... pass
#   run "cannot_disable_versioning_in_production"... pass
#   run "can_disable_versioning_in_dev"... pass
#   run "encryption_is_always_configured"... pass
#   run "required_tags_are_present"... pass
# tests/bucket_tests.tftest.hcl... tearing down
# tests/bucket_tests.tftest.hcl... pass
#
# Success! 6 passed, 0 failed.

Errores Comunes en Testing de Terraform

  1. Solo hacer terraform validate y creer que es suficiente: validate solo comprueba la sintaxis. No detecta problemas de seguridad, tipos de instancia inválidos o configuraciones subóptimas.

  2. No ejecutar pruebas en CI/CD: Las pruebas que solo se ejecutan "a veces, localmente" no son pruebas reales. Deben estar en el pipeline.

  3. Pruebas de integración que no limpian recursos: Si las pruebas de integración fallan a mitad y no hay limpieza, quedan recursos huérfanos que generan costos. terraform test maneja esto automáticamente, pero con Terratest hay que implementarlo manualmente con defer.

  4. No usar command = plan cuando es suficiente: Las pruebas que no necesitan recursos reales deben usar command = plan. Es más rápido y no tiene costo en la nube.

  5. Probar demasiado en las pruebas unitarias: Las pruebas unitarias deben verificar la lógica del módulo, no el comportamiento de la nube. Para eso están las pruebas de integración.

  6. No incluir expect_failures: Las pruebas de validaciones son tan importantes como las pruebas de casos correctos. Si no pruebas que las validaciones rechazan datos incorrectos, no sabes si realmente funcionan.


Resumen

En este módulo has aprendido:

  • Por qué las pruebas de infraestructura son indispensables para proyectos profesionales
  • Los cuatro niveles de testing: análisis estático, pruebas unitarias, integración y end-to-end
  • Las herramientas principales del ecosistema: terraform fmt, terraform validate, tflint, checkov, terrascan y terraform test
  • Cómo escribir archivos .tftest.hcl con bloques run, assert y expect_failures
  • Cómo integrar todas las pruebas en un pipeline de GitHub Actions
  • Un ejercicio completo con pruebas para un módulo S3 real

En el siguiente módulo aprenderás las mejores prácticas de seguridad en Terraform, incluyendo gestión de credenciales, cifrado del estado, variables sensibles y herramientas para auditar la seguridad de tus configuraciones.

© Copyright 2026. Todos los derechos reservados