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. Debe ser revisado, probado y versionado igual 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 estructura un proceso de revisión de código específicamente para configuraciones de Terraform, CloudFormation, Pulumi o Ansible.

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 | `terraform destroy -target` o eliminación de stack | 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

Las revisiones de infraestructura difieren de las de código de aplicación porque el radio de explosión es mayor. Un bug en código de aplicación afecta un pod; un bug en Terraform puede eliminar una base de datos o exponerla a internet. La plantilla impone análisis estático (chequeos automatizados), revisión de seguridad (juicio humano) y preparación operacional (¿puedes ejecutarlo y recuperarte de él?). El plan de rollback no es negociable: cada cambio de infraestructura debe ser reversible dentro del RTO del servicio que soporta.

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

  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 revision:

# .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\`\`\``
            });

Politica personalizada de Checkov para reglas especificas de la organizacion

Define politicas personalizadas que hagan cumplir los estandares de tu organizacion:

# 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 modulos 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)
}

Automatizacion de deteccion de drift

Programar verificaciones regulares de drift y alertar cuando la infraestructura diverge del codigo:

import subprocess
import json
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", "-chdir=f{self.terraform_dir}/{env}", "init", "-input=false"],
                check=True, capture_output=True
            )
            subprocess.run(
                ["terraform", "-chdir=f{self.terraform_dir}/{env}", "refresh", "-input=false"],
                check=True, capture_output=True
            )
            plan = subprocess.run(
                ["terraform", "-chdir=f{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)

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.