StackPractices
intermediate Por Mathias Paulenko

Plantilla de Revisión de Infrastructure as Code

Una plantilla para revisar código de infraestructura Terraform y CloudFormation.

Temas: devops

Visión General

El código de infraestructura es software, y merece la misma disciplina de revisión que el código de aplicación — un solo security group mal configurado o una política IAM demasiado permisiva puede exponer todo tu entorno. Esta plantilla te da un proceso de revisión estructurado para cambios de Terraform, CloudFormation, Pulumi o Ansible, para que nada peligroso se cuele un viernes por la tarde.

Cuándo Usar

Usa este recurso cuando:

  • Agregues un nuevo módulo de Terraform o stack de CloudFormation a producción
  • Revises pull requests que modifican infraestructura
  • Audites código de infraestructura existente por problemas de seguridad o costos

Solución

# Revisión de Infrastructure as Code: `<Módulo / Stack>`

## 1. Metadatos del Cambio

| Campo | Valor |
|-------|-------|
| Módulo / Stack | `nombre` |
| Herramienta | `Terraform / CloudFormation / Pulumi / Ansible` |
| Entorno | `dev / staging / prod` |
| Ticket | `JIRA-1234` |
| Autor | `@autor` |
| Revisor | `@revisor` |
| Nivel de Riesgo | `Bajo / Medio / Alto / Crítico` |

## 2. Análisis Estático

- [ ] `terraform validate` o `cfn-lint` pasa sin errores
- [ ] `terraform plan` o `change set` ha sido revisado para eliminaciones inesperadas
- [ ] Escaneo de seguridad (Checkov, tfsec, cfn-nag) tiene cero hallazgos HIGH/CRITICAL
- [ ] Estimación de costo proporcionada para nuevos recursos (Infracost o manual)
- [ ] Bloqueo de archivo de estado configurado para Terraform
- [ ] Configuración de backend usa un store de estado remoto y cifrado

## 3. Revisión de Seguridad

| Check | Aprobado / Fallido | Notas |
|-------|-------------------|-------|
| Sin secretos hardcodeados en código o variables | | |
| Roles IAM / RBAC de mínimo privilegio | | |
| Security groups restringen ingress a CIDRs conocidos | | |
| Cifrado en reposo habilitado para almacenamiento | | |
| Cifrado en tránsito forzado (TLS 1.2+) | | |
| Acceso público deshabilitado por defecto | | |
| Logging habilitado para todos los data planes | | |
| WAF / protección DDoS para endpoints públicos | | |

## 4. Confiabilidad y Operaciones

| Check | Aprobado / Fallido | Notas |
|-------|-------------------|-------|
| Límites / cuotas de recursos verificados | | |
| Health checks y auto-recovery configurados | | |
| Redundancia multi-AZ o multi-región donde se requiere | | |
| Política de backup / snapshot definida | | |
| Monitoreo y alertas incluidos | | |
| Shutdown graceful / draining para servicios stateful | | |
| Idempotencia verificada: re-ejecución no produce cambios | | |

## 5. Costo y Eficiencia

| Check | Aprobado / Fallido | Notas |
|-------|-------------------|-------|
| Instancias right-sized (no default / máximo) | | |
| Capacidad reservada o savings plans considerados | | |
| Recursos no utilizados removidos en este cambio | | |
| Políticas de ciclo de vida de almacenamiento definidas | | |
| Costos de transferencia de datos estimados | | |

## 6. Documentación

- [ ] README actualizado con inputs, outputs y ejemplo de uso
- [ ] Architecture Decision Record (ADR) incluido para cambios importantes
- [ ] Runbook actualizado para nuevos procedimientos operacionales
- [ ] Playbooks de alertas de on-call cubren nuevas señales de monitoreo

## 7. Plan de Rollback

| Escenario | Acción de Rollback | Tiempo para Completar |
|-----------|-------------------|----------------------|
| Falla de despliegue | Re-aplicar el estado anterior o revertir el merge — evita `destroy -target` en recursos stateful | 15 min |
| Regresión de rendimiento | Revertir a imagen anterior / escalar arriba | 10 min |
| Incidente de seguridad | Deshabilitar acceso público + revocar claves | 5 min |

Explicación

Revisar infraestructura no es lo mismo que revisar código de aplicación — el radio de explosión es mayor. Un bug en código de aplicación afecta un pod; un bug en Terraform puede borrar una base de datos o dejarla expuesta en internet. Por eso la plantilla divide la revisión en tres pasadas: análisis estático (los chequeos automatizados), revisión de seguridad (criterio humano) y preparación operacional (¿puedes ejecutarlo y recuperarte cuando falle?). El plan de rollback tampoco es opcional — cada cambio de infra tiene que ser reversible dentro del RTO del servicio que soporta.

El flujo es este:

flowchart diagram: PR abierto

Cómo Adoptar Esta Plantilla

No pegues la checklist entera en cada PR y lo des por hecho — calibra el nivel de riesgo. Un módulo nuevo o cualquier cosa que toque estado de prod merece la revisión completa de principio a fin. Un cambio de tags o un ajuste fuera de prod solo necesita la sección de análisis estático más una pasada rápida de seguridad; una revisión pesada para trivialidades solo enseña a la gente a aprobar sin mirar.

Un despliegue que funciona: mete primero el análisis estático y el escaneo de seguridad en el CI, construye el hábito de revisar el plan durante un par de semanas, y después haz obligatoria la checklist completa para cambios de producción. Si tu equipo ya usa un proceso de revisión genérico (mira la checklist de code review), mantén las dos — aquella caza bugs de lógica, esta caza errores de radio de explosión.

Variantes

HerramientaAnálisis EstáticoEscaneo de SeguridadGestión de Estado
Terraformterraform validate, fmtCheckov, tfsec, TerrascanBackend S3 remoto + bloqueo
CloudFormationcfn-lint, cfn-guardcfn-nag, CheckovStack sets + drift detection
Pulumipulumi previewCheckovEstado Pulumi Cloud
Ansibleansible-lint, syntax-checkRoles de hardening de AnsibleGit + AWX / Tower

Lo que funciona

Algunas prácticas que hacen que estas revisiones funcionen en equipos reales:

  1. Ejecuta análisis estático en CI/CD antes de que un humano vea el pull request
  2. Requiere dos aprobaciones para cambios de infraestructura en producción, no una
  3. Revisa el diff de terraform plan, no solo el código; los planes revelan cambios destructivos
  4. Separa archivos de estado por entorno; nunca compartas estado de prod y dev
  5. Usa versionado de módulos; fija versiones de provider y módulo para evitar actualizaciones sorpresa

Errores Comunes

  1. Revisar solo el diff de código e ignorar la salida de terraform plan
  2. Hardcodear secretos en lugar de usar un gestor de secretos (Vault, AWS Secrets Manager)
  3. Usar count o for_each en recursos stateful sin considerar pérdida de datos al destruir
  4. Olvidar actualizar documentación cuando la infraestructura cambia
  5. Ejecutar terraform apply localmente en lugar de a través de un pipeline CI/CD con logging de auditoría

Soluciones Avanzadas

Escaneo automatizado de IaC con Checkov en CI/CD

Integra el escaneo de infraestructura en tu pipeline para detectar problemas antes de la revisión:

# .github/workflows/terraform-review.yml
name: Terraform Security Scan
on:
  pull_request:
    paths:
      - "terraform/**"
      - "infrastructure/**"

jobs:
  checkov-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          framework: terraform
          output_format: sarif
          output_file_path: results.sarif
          soft_fail: false
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

  terraform-plan-review:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-tf
          aws-region: us-east-1
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.7.0"
      - name: Terraform Init
        run: terraform -chdir=terraform/ init -input=false
      - name: Terraform Plan
        run: terraform -chdir=terraform/ plan -input=false -out=tfplan
      - name: Post Plan to PR
        uses: actions/github-script@v7
        with:
          script: |
            const { execSync } = require('child_process');
            const planOutput = execSync('terraform -chdir=terraform/ show -no-color tfplan').toString();
            const truncated = planOutput.substring(0, 50000);
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
              body: `## Terraform Plan\n\n\`\`\`diff\n${truncated}\n\`\`\``
            });

Política personalizada de Checkov para reglas específicas de la organización

Define políticas personalizadas que hagan cumplir los estándares de tu organización:

# checkov_custom_policies/aws_require_tags.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories

class RequireCostCenterTag(BaseResourceCheck):
    def __init__(self):
        name = "Ensure all resources have a CostCenter tag"
        check_id = "CKV_AWS_CUSTOM_1"
        supported_resources = ["aws_*"]
        categories = [CheckCategories.CONVENTION]
        super().__init__(name=name, id=check_id, categories=categories,
                         supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        tags = conf.get("tags", [{}])[0]
        if "CostCenter" in tags and "Environment" in tags:
            return CheckResult.PASSED
        return CheckResult.FAILED

check = RequireCostCenterTag()
# Run custom policies alongside built-in checks
checkov -d terraform/ \
  --external-checks-dir checkov_custom_policies/ \
  --framework terraform \
  --output cli \
  --soft-fail false

Pruebas de módulos de Terraform con Terratest

Valida que tu infraestructura realmente funciona escribiendo pruebas automatizadas:

// test/infrastructure_test.go
package test

import (
    "testing"
    "time"

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

func TestVpcModule(t *testing.T) {
    terraformOptions := &terraform.Options{
        TerraformDir: "../terraform/modules/vpc",
        Vars: map[string]interface{}{
            "environment":    "test",
            "vpc_cidr":       "10.0.0.0/16",
            "enable_nat":     true,
        },
    }

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

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

    subnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
    assert.Equal(t, 2, len(subnetIds))

    // Verify NAT gateway was created
    natGatewayId := terraform.Output(t, terraformOptions, "nat_gateway_id")
    assert.NotEmpty(t, natGatewayId)

    // Verify VPC has DNS support
    aws.AssertVpcDnsSupportEnabled(t, "us-east-1", vpcId)
}

Automatización de detección de drift

Programa verificaciones regulares de drift y alerta cuando la infraestructura diverge del código:

import subprocess
import smtplib
from email.mime.text import MIMEText
from typing import List, Dict

class DriftDetector:
    def __init__(self, terraform_dir: str, environments: List[str]):
        self.terraform_dir = terraform_dir
        self.environments = environments

    def check_drift(self, env: str) -> Dict:
        """Check for drift in a specific environment."""
        result = {"environment": env, "drifted": False, "resources": []}

        try:
            subprocess.run(
                ["terraform", f"-chdir={self.terraform_dir}/{env}", "init", "-input=false"],
                check=True, capture_output=True
            )
            subprocess.run(
                ["terraform", f"-chdir={self.terraform_dir}/{env}", "refresh", "-input=false"],
                check=True, capture_output=True
            )
            plan = subprocess.run(
                ["terraform", f"-chdir={self.terraform_dir}/{env}", "plan", "-detailed-exitcode", "-input=false"],
                capture_output=True, text=True
            )

            if plan.returncode == 2:
                result["drifted"] = True
                result["resources"] = self._parse_drifted_resources(plan.stdout)
        except subprocess.CalledProcessError as e:
            result["error"] = str(e)

        return result

    def _parse_drifted_resources(self, plan_output: str) -> List[str]:
        """Extract resource addresses from plan output."""
        resources = []
        for line in plan_output.split("\n"):
            if line.startswith("  # ") and "will be" in line:
                resources.append(line.strip())
        return resources

    def send_alert(self, drift_results: List[Dict]) -> None:
        """Send email alert if drift is detected."""
        drifted_envs = [r for r in drift_results if r["drifted"]]
        if not drifted_envs:
            return

        body = "Drift detected in the following environments:\n\n"
        for env in drifted_envs:
            body += f"Environment: {env['environment']}\n"
            for resource in env["resources"]:
                body += f"  - {resource}\n"
            body += "\n"

        msg = MIMEText(body)
        msg["Subject"] = "[ALERT] Terraform Drift Detected"
        msg["From"] = "infra-alerts@example.com"
        msg["To"] = "platform-team@example.com"

        with smtplib.SMTP("smtp.example.com", 587) as server:
            server.send_message(msg)

# Example usage
detector = DriftDetector("terraform/environments", ["prod", "staging", "dev"])
results = [detector.check_drift(env) for env in detector.environments]
detector.send_alert(results)

Lectura Adicional

Preguntas frecuentes

¿Los cambios de infraestructura deberían requerir la misma aprobación que los despliegues de aplicación?

A menudo deberían requerir más escrutinio. Los cambios de aplicación se pueden revertir con un despliegue; los cambios de infraestructura pueden destruir datos. Considera un flujo de aprobación separado para infraestructura de producción, o requiere la firma de un ingeniero senior.

¿Cómo reviso un módulo de Terraform grande sin perder detalles?

Divide la revisión en capas: primero análisis estático y revisión de plan, luego chequeos de seguridad, luego preparación operacional. No intentes revisar todo a la vez. Usa un checklist (como esta plantilla) para que ninguna categoría sea omitida.

¿Qué es drift detection y por qué importa?

El drift ocurre cuando alguien cambia infraestructura fuera de IaC (ej. por consola). Herramientas como Terraform refresh, AWS Config o CloudFormation drift detection identifican estos cambios. Revisa reportes de drift regularmente; de lo contrario tu código y la realidad divergen, haciendo futuros cambios peligrosos.

¿Qué herramientas de escaneo uso para cada herramienta de IaC?

Para Terraform, Checkov o tfsec. Para CloudFormation, cfn-lint junto con cfn-nag o cfn-guard. Para Ansible, ansible-lint. Elijas lo que elijas, conéctalo al CI para que corra en cada pull request — un escaneo que depende de que alguien se acuerde de ejecutarlo deja de ocurrir en un mes.

¿Quién debería aprobar cambios de infraestructura en producción?

Al menos una persona que no escribió el código, y una segunda aprobación para todo lo que toque estado de prod, IAM o red. La checklist cubre lo mecánico; quien aprueba necesita contexto suficiente para cazar lo que las herramientas no ven.