El Imperativo de las Pruebas de Infraestructura como Código

La Infraestructura como Código (IaC) ha revolucionado cómo aprovisionamos y gestionamos la infraestructura, pero con gran poder viene gran responsabilidad. Sin pruebas adecuadas, IaC puede propagar configuraciones erróneas a escala, llevando a vulnerabilidades de seguridad, violaciones de cumplimiento y cortes catastróficos. Las pruebas de IaC ya no son opcionales: son un requisito crítico para mantener una infraestructura confiable, segura y conforme.

La complejidad de los entornos cloud modernos demanda estrategias de prueba sofisticadas que van más allá de la simple validación de sintaxis. Necesitamos verificar configuraciones de seguridad, asegurar el cumplimiento de políticas organizacionales, validar integraciones entre servicios y confirmar que nuestra infraestructura se comporta correctamente bajo varias condiciones. Este enfoque integral de las pruebas de IaC previene errores costosos y acelera la entrega mientras mantiene la calidad.

Pirámide de Pruebas para Infraestructura como Código

Análisis Estático y Linting

La base de las pruebas de IaC comienza con el análisis estático. Estas pruebas se ejecutan rápidamente y detectan problemas comunes temprano en el ciclo de desarrollo:

# terraform/modules/web-server/variables.tf
variable "instance_type" {
  description = "Tipo de instancia EC2"
  type        = string
  default     = "t3.micro"

  validation {
    condition = contains([
      "t3.micro",
      "t3.small",
      "t3.medium",
      "t3.large"
    ], var.instance_type)
    error_message = "El tipo de instancia debe ser uno de los tamaños aprobados."
  }
}

variable "subnet_ids" {
  description = "Lista de IDs de subnet para el despliegue"
  type        = list(string)

  validation {
    condition = length(var.subnet_ids) >= 2
    error_message = "Se requieren al menos 2 subnets para alta disponibilidad."
  }
}

variable "environment" {
  description = "Nombre del entorno"
  type        = string

  validation {
    condition = can(regex("^(dev|staging|prod)$", var.environment))
    error_message = "El entorno debe ser dev, staging o prod."
  }
}

Pipeline de Validación Terraform

# .github/workflows/terraform-validation.yml
name: Pipeline de Validación Terraform

on:
  pull_request:
    paths:
      - 'terraform/**'
      - '.github/workflows/terraform-validation.yml'

jobs:
  validate:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Configurar Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.5.0

      - name: Verificación de Formato Terraform
        run: |
          terraform fmt -check -recursive terraform/

      - name: TFLint - Linter de Terraform
        run: |
          curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

          # Configurar TFLint
          cat > .tflint.hcl <<EOF
          config {
            module = true
            force = false
          }

          plugin "aws" {
            enabled = true
            version = "0.24.0"
            source  = "github.com/terraform-linters/tflint-ruleset-aws"
          }

          rule "aws_instance_invalid_type" {
            enabled = true
          }

          rule "aws_resource_missing_tags" {
            enabled = true
            tags = ["Environment", "Owner", "CostCenter"]
          }
          EOF

          tflint --init
          tflint --recursive

      - name: Escaneo de Seguridad con Checkov
        run: |
          pip install checkov
          checkov -d terraform/ --framework terraform --output json --output-file-path checkov-report.json

          # Parsear y fallar en problemas de alta severidad
          python3 <<EOF
          import json
          with open('checkov-report.json', 'r') as f:
              report = json.load(f)

          failed_checks = report.get('summary', {}).get('failed', 0)
          if failed_checks > 0:
              print(f"Se encontraron {failed_checks} problemas de seguridad")
              for check in report.get('results', {}).get('failed_checks', []):
                  print(f"  - {check['check_id']}: {check['check_name']}")
              exit(1)
          EOF

      - name: Validar Terraform
        run: |
          for dir in terraform/environments/*/; do
            echo "Validando $dir"
            cd $dir
            terraform init -backend=false
            terraform validate
            cd -
          done

      - name: Generar Documentación
        run: |
          # Instalar terraform-docs
          curl -sSLo terraform-docs.tar.gz https://github.com/terraform-docs/terraform-docs/releases/download/v0.16.0/terraform-docs-v0.16.0-linux-amd64.tar.gz
          tar -xzf terraform-docs.tar.gz
          chmod +x terraform-docs

          # Generar docs para cada módulo
          for module in terraform/modules/*/; do
            ./terraform-docs markdown table --output-file README.md --output-mode inject $module
          done

Pruebas Unitarias con Terratest

Implementación Integral de Terratest

// test/terraform_aws_vpc_test.go
package test

import (
    "fmt"
    "strings"
    "testing"
    "time"

    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/gruntwork-io/terratest/modules/random"
    "github.com/gruntwork-io/terratest/modules/retry"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestTerraformAwsVpc(t *testing.T) {
    t.Parallel()

    // Generar identificadores únicos
    uniqueId := random.UniqueId()
    region := "us-east-1"
    vpcCidr := "10.0.0.0/16"

    terraformOptions := &terraform.Options{
        TerraformDir: "../terraform/modules/vpc",

        Vars: map[string]interface{}{
            "vpc_cidr":     vpcCidr,
            "environment":  "test",
            "name":        fmt.Sprintf("test-vpc-%s", uniqueId),
            "region":      region,
            "enable_nat":  true,
            "single_nat":  false,
        },

        EnvVars: map[string]string{
            "AWS_DEFAULT_REGION": region,
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Validar salidas
    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId)

    publicSubnetIds := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
    privateSubnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")

    assert.Equal(t, 3, len(publicSubnetIds))
    assert.Equal(t, 3, len(privateSubnetIds))

    // Verificar configuración VPC
    vpc := aws.GetVpcById(t, vpcId, region)
    assert.Equal(t, vpcCidr, vpc.CidrBlock)
    assert.True(t, vpc.EnableDnsSupport)
    assert.True(t, vpc.EnableDnsHostnames)

    // Probar configuraciones de subnet
    for _, subnetId := range publicSubnetIds {
        subnet := aws.GetSubnetById(t, subnetId, region)
        assert.True(t, subnet.MapPublicIpOnLaunch)
        assert.Contains(t, subnet.AvailabilityZone, region)
    }

    for _, subnetId := range privateSubnetIds {
        subnet := aws.GetSubnetById(t, subnetId, region)
        assert.False(t, subnet.MapPublicIpOnLaunch)

        // Verificar conectividad NAT gateway
        routeTable := aws.GetRouteTableForSubnet(t, subnet, region)
        hasNatRoute := false
        for _, route := range routeTable.Routes {
            if route.DestinationCidrBlock == "0.0.0.0/0" && route.NatGatewayId != "" {
                hasNatRoute = true
                break
            }
        }
        assert.True(t, hasNatRoute, "La subnet privada debe tener ruta NAT gateway")
    }

    // Probar ACLs de red
    testNetworkAcls(t, vpcId, region)

    // Probar grupos de seguridad
    testSecurityGroups(t, terraformOptions, region)
}

func testNetworkAcls(t *testing.T, vpcId string, region string) {
    nacls := aws.GetNetworkAclsForVpc(t, vpcId, region)

    for _, nacl := range nacls {
        // Verificar reglas de entrada
        for _, rule := range nacl.IngressRules {
            if rule.RuleNumber == 100 {
                assert.Equal(t, "tcp", rule.Protocol)
                assert.Equal(t, "0.0.0.0/0", rule.CidrBlock)
            }
        }

        // Verificar reglas de salida
        for _, rule := range nacl.EgressRules {
            if rule.RuleNumber == 100 {
                assert.Equal(t, "-1", rule.Protocol) // Todos los protocolos
                assert.Equal(t, "0.0.0.0/0", rule.CidrBlock)
            }
        }
    }
}

func TestTerraformAwsEcs(t *testing.T) {
    t.Parallel()

    terraformOptions := &terraform.Options{
        TerraformDir: "../terraform/modules/ecs-cluster",

        Vars: map[string]interface{}{
            "cluster_name":         fmt.Sprintf("test-cluster-%s", random.UniqueId()),
            "capacity_providers":   []string{"FARGATE", "FARGATE_SPOT"},
            "container_insights":   true,
            "enable_execute_command": true,
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Probar creación del cluster
    clusterArn := terraform.Output(t, terraformOptions, "cluster_arn")
    assert.Contains(t, clusterArn, "cluster/test-cluster")

    // Probar despliegue del servicio
    deployTestService(t, terraformOptions)
}

func deployTestService(t *testing.T, terraformOptions *terraform.Options) {
    serviceOptions := &terraform.Options{
        TerraformDir: "../terraform/modules/ecs-service",

        Vars: map[string]interface{}{
            "cluster_id":     terraform.Output(t, terraformOptions, "cluster_id"),
            "service_name":   "test-service",
            "task_cpu":       256,
            "task_memory":    512,
            "desired_count":  2,
            "container_port": 8080,
        },
    }

    defer terraform.Destroy(t, serviceOptions)
    terraform.InitAndApply(t, serviceOptions)

    // Esperar a que el servicio se estabilice
    retry.DoWithRetry(t, "Esperar servicio ECS", 30, 10*time.Second, func() (string, error) {
        // Verificar estado del servicio
        return "", nil
    })
}

Pruebas de Ansible con Molecule

Configuración de Pruebas con Molecule

# molecule/default/molecule.yml
---
dependency:
  name: galaxy

driver:
  name: docker

platforms:
  - name: ubuntu-2204
    image: ubuntu:22.04
    pre_build_image: false
    dockerfile: Dockerfile.j2
    privileged: true
    command: /lib/systemd/systemd
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    tmpfs:
      - /tmp
      - /run
    capabilities:
      - SYS_ADMIN

  - name: centos-8
    image: centos:8
    pre_build_image: false
    dockerfile: Dockerfile.j2
    privileged: true
    command: /usr/sbin/init
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    tmpfs:
      - /tmp
      - /run

provisioner:
  name: ansible
  config_options:
    defaults:
      callback_whitelist: profile_tasks
      fact_caching: jsonfile
      fact_caching_connection: /tmp/ansible_cache
  inventory:
    host_vars:
      ubuntu-2204:
        ansible_python_interpreter: /usr/bin/python3
  lint:
    name: ansible-lint
  playbooks:
    prepare: prepare.yml
    converge: converge.yml
    verify: verify.yml

verifier:
  name: testinfra
  options:
    verbose: true
  lint:
    name: flake8

scenario:
  name: default
  test_sequence:
    - dependency
    - lint
    - cleanup
    - destroy
    - syntax
    - create
    - prepare
    - converge
    - idempotence
    - side_effect
    - verify
    - cleanup
    - destroy

Pruebas de Playbook de Ansible

# molecule/default/tests/test_nginx.py
import os
import pytest

import testinfra.utils.ansible_runner

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')


def test_nginx_instalado(host):
    """Probar que nginx está instalado"""
    nginx = host.package('nginx')
    assert nginx.is_installed
    assert nginx.version.startswith('1.')


def test_servicio_nginx(host):
    """Probar que el servicio nginx está ejecutándose y habilitado"""
    nginx = host.service('nginx')
    assert nginx.is_running
    assert nginx.is_enabled


def test_configuracion_nginx(host):
    """Probar configuración de nginx"""
    config = host.file('/etc/nginx/nginx.conf')
    assert config.exists
    assert config.is_file
    assert config.user == 'root'
    assert config.group == 'root'
    assert oct(config.mode) == '0o644'

    # Validar sintaxis de configuración
    cmd = host.run('nginx -t')
    assert cmd.rc == 0
    assert 'syntax is ok' in cmd.stderr
    assert 'test is successful' in cmd.stderr


def test_puertos_nginx(host):
    """Probar que nginx escucha en los puertos esperados"""
    assert host.socket('tcp://0.0.0.0:80').is_listening

    # Probar SSL si está configurado
    if host.file('/etc/nginx/sites-enabled/ssl').exists:
        assert host.socket('tcp://0.0.0.0:443').is_listening


def test_procesos_nginx(host):
    """Probar que los procesos nginx están ejecutándose"""
    master = host.process.filter(comm='nginx', user='root')
    assert len(master) == 1

    workers = host.process.filter(comm='nginx', user='www-data')
    assert len(workers) >= 1


def test_archivos_log_nginx(host):
    """Probar que los archivos de log se crean con permisos correctos"""
    access_log = host.file('/var/log/nginx/access.log')
    error_log = host.file('/var/log/nginx/error.log')

    for log in [access_log, error_log]:
        assert log.exists
        assert log.is_file
        assert log.user == 'www-data'
        assert oct(log.mode) == '0o640'


def test_cabeceras_seguridad_nginx(host):
    """Probar que las cabeceras de seguridad están establecidas"""
    response = host.run('curl -I http://localhost')
    assert response.rc == 0

    headers = response.stdout
    assert 'X-Frame-Options: SAMEORIGIN' in headers
    assert 'X-Content-Type-Options: nosniff' in headers
    assert 'X-XSS-Protection: 1; mode=block' in headers


@pytest.mark.parametrize('site', [
    'default',
    'app.example.com'
])
def test_sitios_nginx(host, site):
    """Probar configuraciones de sitios nginx"""
    site_config = host.file(f'/etc/nginx/sites-available/{site}')
    assert site_config.exists

    site_enabled = host.file(f'/etc/nginx/sites-enabled/{site}')
    assert site_enabled.exists
    assert site_enabled.is_symlink
    assert site_enabled.linked_to == f'/etc/nginx/sites-available/{site}'

Políticas como Código con Open Policy Agent

Políticas OPA para Terraform

# policies/terraform/security.rego
package terraform.security

import future.keywords.if
import future.keywords.in

default allow = false

# Denegar buckets S3 públicos
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket_public_access_block"
    resource.change.after.block_public_acls == false
    msg := sprintf("El bucket S3 %s debe bloquear ACLs públicas", [resource.address])
}

# Requerir encriptación para instancias RDS
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_db_instance"
    not resource.change.after.storage_encrypted
    msg := sprintf("La instancia RDS %s debe tener almacenamiento encriptado", [resource.address])
}

# Hacer cumplir requisitos de etiquetado
required_tags := ["Environment", "Owner", "CostCenter", "Project"]

deny[msg] {
    resource := input.resource_changes[_]
    required_tag := required_tags[_]
    not resource.change.after.tags[required_tag]
    msg := sprintf("Al recurso %s le falta la etiqueta requerida: %s", [resource.address, required_tag])
}

# Validación de reglas de grupo de seguridad
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_security_group_rule"
    resource.change.after.type == "ingress"
    resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
    resource.change.after.from_port == 22
    msg := sprintf("La regla del grupo de seguridad %s permite SSH desde cualquier lugar", [resource.address])
}

# Restricciones de tipo de instancia
allowed_instance_types := [
    "t3.micro", "t3.small", "t3.medium",
    "m5.large", "m5.xlarge"
]

deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"
    not resource.change.after.instance_type in allowed_instance_types
    msg := sprintf("La instancia %s usa un tipo no aprobado: %s", [
        resource.address,
        resource.change.after.instance_type
    ])
}

# Requisitos de segmentación de red
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"
    resource.change.after.associate_public_ip_address == true
    contains(resource.change.after.tags.Environment, "prod")
    msg := sprintf("La instancia de producción %s no puede tener IP pública", [resource.address])
}

Políticas Sentinel para Terraform Cloud

# policies/control-costos.sentinel
import "tfplan/v2" as tfplan
import "decimal"

# Umbral máximo de costo mensual
max_costo_mensual = decimal.new(1000)

# Calcular costo mensual estimado
costo_estimado = decimal.new(tfplan.cost_estimate.total_monthly_cost)

# Regla principal
main = rule {
    costo_estimado.less_than_or_equal_to(max_costo_mensual)
}

# Límites de costo de instancia por tipo
limites_costo_instancia = {
    "t3.micro":  50,
    "t3.small":  100,
    "t3.medium": 200,
    "t3.large":  300,
    "m5.large":  400,
    "m5.xlarge": 800,
}

# Validar costos individuales de instancias
validar_costos_instancias = func() {
    validado = true

    for tfplan.resource_changes as _, rc {
        if rc.type is "aws_instance" and rc.change.actions contains "create" {
            tipo_instancia = rc.change.after.instance_type

            if tipo_instancia in keys(limites_costo_instancia) {
                # Estimar costo por hora (simplificado)
                costo_hora = limites_costo_instancia[tipo_instancia] / 730

                if costo_hora > limites_costo_instancia[tipo_instancia] / 730 {
                    print("La instancia", rc.address, "excede el límite de costo")
                    validado = false
                }
            }
        }
    }

    return validado
}

# Verificaciones de cumplimiento
regla_cumplimiento = rule {
    validar_costos_instancias()
}

Pruebas de Integración con Kitchen-Terraform

Configuración de Kitchen

# .kitchen.yml
---
driver:
  name: terraform
  root_module_directory: test/fixtures/wrapper
  command_timeout: 1800

provisioner:
  name: terraform

verifier:
  name: terraform
  systems:
    - name: default
      backend: ssh
      hosts_output: public_ip
      user: ubuntu
      key_files:
        - test/assets/id_rsa

    - name: aws
      backend: awspec

platforms:
  - name: ubuntu-2204
    driver:
      variables:
        platform: ubuntu-2204

  - name: amazon-linux-2
    driver:
      variables:
        platform: amazon-linux-2

suites:
  - name: default
    driver:
      variables:
        instance_count: 2
        enable_monitoring: true
    verifier:
      inspec_tests:
        - test/integration/default
    lifecycle:
      pre_converge:
        - local: echo "Ejecutando tareas pre-convergencia"
      post_converge:
        - local: echo "Ejecutando tareas post-convergencia"

Pruebas de Integración con InSpec

# test/integration/default/controls/infrastructure.rb
control 'infraestructura-01' do
  title 'Verificar Configuración VPC'
  desc 'Asegurar que la VPC está configurada correctamente'

  describe aws_vpc(vpc_id: attribute('vpc_id')) do
    it { should exist }
    its('cidr_block') { should eq '10.0.0.0/16' }
    its('state') { should eq 'available' }
    its('enable_dns_support') { should eq true }
    its('enable_dns_hostnames') { should eq true }
  end
end

control 'infraestructura-02' do
  title 'Verificar Grupos de Seguridad'
  desc 'Asegurar que los grupos de seguridad están configurados de forma segura'

  aws_security_groups.where(vpc_id: attribute('vpc_id')).entries.each do |sg|
    describe aws_security_group(group_id: sg.group_id) do
      it { should_not allow_ingress_from_internet_on_port(22) }
      it { should_not allow_ingress_from_internet_on_port(3389) }

      # Matchers personalizados
      its('ingress_rules') { should_not include(
        from_port: 0,
        to_port: 65535,
        cidr_blocks: ['0.0.0.0/0']
      )}
    end
  end
end

control 'infraestructura-03' do
  title 'Verificar Instancias EC2'
  desc 'Asegurar que las instancias EC2 cumplen los requisitos de seguridad'

  aws_ec2_instances.where(vpc_id: attribute('vpc_id')).entries.each do |instance|
    describe aws_ec2_instance(instance_id: instance.instance_id) do
      it { should be_running }
      it { should have_encrypted_root_volume }
      its('monitoring_state') { should eq 'enabled' }
      its('instance_type') { should be_in %w[t3.micro t3.small t3.medium] }

      # Verificar que IMDSv2 es requerido
      its('metadata_options.http_tokens') { should eq 'required' }
      its('metadata_options.http_endpoint') { should eq 'enabled' }
    end
  end
end

control 'infraestructura-04' do
  title 'Verificar Buckets S3'
  desc 'Asegurar que los buckets S3 son seguros'

  aws_s3_buckets.entries.each do |bucket|
    describe aws_s3_bucket(bucket_name: bucket.name) do
      it { should have_default_encryption_enabled }
      it { should have_versioning_enabled }
      it { should_not be_public }

      its('bucket_acl.grants') { should_not include(
        grantee: { type: 'Group', uri: 'http://acs.amazonaws.com/groups/global/AllUsers' }
      )}
    end
  end
end

control 'infraestructura-05' do
  title 'Verificar Instancias RDS'
  desc 'Asegurar que las instancias RDS están configuradas de forma segura'

  aws_rds_instances.entries.each do |db|
    describe aws_rds_instance(db_instance_identifier: db.db_instance_identifier) do
      it { should be_encrypted }
      it { should have_automated_backups_enabled }
      its('backup_retention_period') { should be >= 7 }
      its('multi_az') { should eq true }
      its('publicly_accessible') { should eq false }
      its('deletion_protection') { should eq true }
    end
  end
end

Pipeline de Cumplimiento Continuo

Pipeline CI/CD Completo para IaC

# .github/workflows/iac-pipeline.yml
name: Pipeline de Infraestructura como Código

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'  # Verificación diaria de cumplimiento

env:
  TF_VERSION: '1.5.0'
  ANSIBLE_VERSION: '2.15.0'
  AWS_DEFAULT_REGION: 'us-east-1'

jobs:
  analisis-estatico:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Configurar Herramientas
        run: |
          # Instalar herramientas requeridas
          pip install checkov ansible-lint yamllint

          # Instalar tflint
          curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

          # Instalar tfsec
          curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash

      - name: YAML Lint
        run: yamllint -c .yamllint .

      - name: Ansible Lint
        run: ansible-lint ansible/

      - name: Escaneo de Seguridad Terraform
        run: |
          tfsec terraform/ --format json --out tfsec-report.json
          checkov -d terraform/ --output json --output-file-path checkov-report.json

      - name: Subir Reportes de Seguridad
        uses: actions/upload-artifact@v3
        with:
          name: reportes-seguridad
          path: |
            tfsec-report.json
            checkov-report.json

  pruebas-unitarias:
    runs-on: ubuntu-latest
    needs: analisis-estatico

    strategy:
      matrix:
        suite_pruebas: [vpc, ecs, rds, s3]

    steps:
      - uses: actions/checkout@v3

      - name: Configurar Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.20'

      - name: Ejecutar Terratest
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          cd test
          go mod download
          go test -v -timeout 30m -run Test${{ matrix.suite_pruebas }}

  validacion-politicas:
    runs-on: ubuntu-latest
    needs: analisis-estatico

    steps:
      - uses: actions/checkout@v3

      - name: Configurar OPA
        run: |
          curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
          chmod +x opa
          sudo mv opa /usr/local/bin/

      - name: Plan Terraform
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          cd terraform/environments/dev
          terraform init
          terraform plan -out=tfplan.binary
          terraform show -json tfplan.binary > tfplan.json

      - name: Verificación de Políticas OPA
        run: |
          opa eval -d policies/ -i terraform/environments/dev/tfplan.json \
            "data.terraform.security.deny[_]" --format pretty

          # Fallar si se violan políticas
          VIOLACIONES=$(opa eval -d policies/ -i terraform/environments/dev/tfplan.json \
            "data.terraform.security.deny" --format json | jq '.result[0].expressions[0].value | length')

          if [ "$VIOLACIONES" -gt 0 ]; then
            echo "¡Se encontraron violaciones de política!"
            exit 1
          fi

  pruebas-integracion:
    runs-on: ubuntu-latest
    needs: [pruebas-unitarias, validacion-politicas]
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v3

      - name: Configurar Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'
          bundler-cache: true

      - name: Ejecutar Pruebas Kitchen
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          bundle exec kitchen test --parallel

  escaneo-cumplimiento:
    runs-on: ubuntu-latest
    needs: pruebas-integracion

    steps:
      - uses: actions/checkout@v3

      - name: Ejecutar Cumplimiento InSpec
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          # Instalar InSpec
          curl https://omnitruck.chef.io/install.sh | sudo bash -s -- -P inspec

          # Ejecutar perfiles de cumplimiento
          inspec exec compliance/ --reporter json:compliance-report.json

      - name: Generar Reporte de Cumplimiento
        run: |
          python3 scripts/generar_reporte_cumplimiento.py \
            --input compliance-report.json \
            --output reporte-cumplimiento.html

      - name: Subir Reporte de Cumplimiento
        uses: actions/upload-artifact@v3
        with:
          name: reporte-cumplimiento
          path: reporte-cumplimiento.html

Conclusión

Las pruebas de Infraestructura como Código no se tratan solo de prevenir cortes, sino de construir confianza en nuestros cambios de infraestructura, asegurar seguridad y cumplimiento, y permitir despliegues rápidos y seguros. Las estrategias de pruebas integrales presentadas aquí forman un marco robusto que captura problemas en cada nivel, desde errores de sintaxis hasta problemas complejos de integración.

La combinación de análisis estático, pruebas unitarias, pruebas de integración y validación de políticas crea una red de seguridad que permite a los equipos moverse rápido sin romper cosas. Al implementar estas prácticas, las organizaciones pueden lograr el santo grial de la gestión de infraestructura: infraestructura auto-documentada, auto-validada y auto-reparable que escala de manera confiable.

Recuerda que las pruebas de IaC son una disciplina en evolución. A medida que tu infraestructura crece en complejidad, también deberían hacerlo tus estrategias de prueba. Comienza con lo básico—linting y validación—luego agrega gradualmente capas de pruebas más sofisticadas. La inversión en pruebas integrales de IaC rinde dividendos en incidentes reducidos, despliegues más rápidos y mayor confianza del equipo.