Burricornio Dev

Terraform, Terragrunt y LocalStack Implementación

Introducción

Este documento detalla nuestra implementación de infraestructura como código (IaC) usando Terraform, Terragrunt para gestión de entornos y LocalStack para pruebas locales.

  flowchart TD
    A[Terragrunt] --> B[Modules Terraform]
    B --> C[Dev Environment]
    B --> D[Prod Environment]
    C --> E[Minikube]
    D --> F[AWS Real]
    E --> G[Todo App]
    E --> H[RabbitMQ]
    E --> I[PostgreSQL]
    F --> J[Todo App]
    F --> K[AWS MQ]
    F --> L[RDS PostgreSQL]

Prerequisitos

Instalación de Dependencias

  1. Docker y Docker Compose:

  2. Terraform y Terragrunt:

    # Terraform
    curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
    sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
    sudo apt update && sudo apt install terraform
    
    # Terragrunt
    curl -L https://github.com/gruntwork-io/terragrunt/releases/download/v0.36.0/terragrunt_linux_amd64 -o terragrunt
    chmod +x terragrunt
    sudo mv terragrunt /usr/local/bin/
    
  3. AWS CLI (para LocalStack):

    pip install awscli-local
    

Configuración de LocalStack

Actualizamos docker-compose.localstack.yml para incluir LocalStack:

version: '3.8'

services:
  # Servicios existentes...
  
  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
      - "4510-4559:4510-4559"
    environment:
      - SERVICES=s3,cloudformation,mq,ec2,iam,rds
      - DEFAULT_REGION=eu-central-1
      - DEBUG=1
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
      - "./localstack_data:/tmp/localstack"

Nota Importante: La variable de entorno SERVICES es crucial. LocalStack solo inicia los servicios de AWS que se enumeran explícitamente en esta línea para ahorrar recursos. Si en el futuro añades un nuevo módulo de Terraform que utilice un servicio de AWS no listado aquí (ej. lambda, sqs), debes añadirlo a esta lista. De lo contrario, tus pruebas de Terraform fallarán. En LocalStack Community, no habilites eks porque no está soportado; las pruebas de EKS se hacen en AWS real.

Desarrollo Iterativo de Terraform con LocalStack

Este flujo de trabajo permite a los desarrolladores probar cambios en los módulos de Terraform para los entornos de nube (lab, prod) de forma local, rápida y sin costos, utilizando LocalStack como un clon de AWS.

El objetivo es redirigir temporalmente las llamadas de Terragrunt de un entorno (ej. lab) a nuestra instancia local de LocalStack.

Flujo de Trabajo Detallado

Paso 1: Iniciar LocalStack

Asegúrate de que LocalStack esté corriendo.

docker compose -f docker-compose.localstack.yml up -d

Paso 2: Crear un Archivo de Sobrescritura del Proveedor

Para “engañar” a Terragrunt, crea un archivo llamado _provider_override.tf en el directorio del componente que quieres probar (ej. infra/terraform/environments/lab/network).

Contenido para _provider_override.tf:

# Este archivo redirige las llamadas de AWS a LocalStack
provider "aws" {
  access_key                  = "test"
  secret_key                  = "test"
  region                      = "eu-central-1"
  s3_force_path_style         = true
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    ec2            = "http://localhost:4566"
    eks            = "http://localhost:4566"
    iam            = "http://localhost:4566"
    rds            = "http://localhost:4566"
    s3             = "http://localhost:4566"
    mq             = "http://localhost:4566"
    # Añadir más servicios según sea necesario
  }
}

Importante: Este archivo no debe ser añadido a Git. Puedes añadir **/*_provider_override.tf a tu archivo .gitignore global.

Paso 3: Desplegar en LocalStack

Navega al directorio donde creaste el archivo de sobreescritura y ejecuta apply.

cd infra/terraform/environments/lab/network
terragrunt apply -auto-approve

Terragrunt ahora aplicará la configuración del módulo de red en tu instancia local de LocalStack.

Paso 4: Inspeccionar los Recursos

Usa awslocal para verificar que los recursos se han creado correctamente.

# Listar VPCs creadas en LocalStack
awslocal ec2 describe-vpcs

# Listar subredes
awslocal ec2 describe-subnets
¿Qué es awscli-local (awslocal)?

awscli-local es un wrapper del AWS CLI que antepone automáticamente --endpoint-url http://localhost:4566 a los comandos del CLI, de modo que todas las peticiones se enrutan a LocalStack en lugar de AWS. Proporciona el binario awslocal que simplifica el uso del CLI con LocalStack.

  • Instalación (Python/pip):
    pip install awscli-local
    # Verificar
    awslocal --version
    
  • Alternativa sin instalación (contenedor con CLI de LocalStack):
    docker run --rm -it -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test \
      -e AWS_DEFAULT_REGION=eu-central-1 --network host localstack/aws-cli \
      awslocal ec2 describe-vpcs
    

Nota: Si no usas awslocal, puedes utilizar aws añadiendo --endpoint-url http://localhost:4566 en cada comando.

Comandos útiles para validar el módulo network
# VPCs (filtrar campos con jq)
awslocal ec2 describe-vpcs | jq '.Vpcs[] | {VpcId, CidrBlock, State}'

# Subredes
awslocal ec2 describe-subnets | jq '.Subnets[] | {SubnetId, VpcId, CidrBlock, AvailabilityZone, MapPublicIpOnLaunch}'

# Tablas de rutas
awslocal ec2 describe-route-tables | jq '.RouteTables[] | {RouteTableId, VpcId, Routes}'

# Internet Gateways
awslocal ec2 describe-internet-gateways | jq '.InternetGateways[] | {InternetGatewayId, Attachments}'

Si ejecutaste make test-infra, el script habrá generado infra-test-outputs.json en la raíz del repo con los outputs del módulo network (por ejemplo, vpc_id, public_subnet_ids, private_subnet_ids). Puedes cruzar estos valores con awslocal para validar:

# Extraer VpcId del output de Terraform
jq -r '.vpc_id.value' infra-test-outputs.json

# Verificar que exista esa VPC en LocalStack
VPC_ID=$(jq -r '.vpc_id.value' infra-test-outputs.json)
awslocal ec2 describe-vpcs --filters Name=vpc-id,Values="$VPC_ID" | jq '.Vpcs[] | {VpcId, State}'

# Verificar subredes públicas y privadas
jq -r '.public_subnet_ids.value[]' infra-test-outputs.json | xargs -I {} awslocal ec2 describe-subnets --subnet-ids {} | jq '.Subnets[] | {SubnetId, MapPublicIpOnLaunch}'
jq -r '.private_subnet_ids.value[]' infra-test-outputs.json | xargs -I {} awslocal ec2 describe-subnets --subnet-ids {} | jq '.Subnets[] | {SubnetId, MapPublicIpOnLaunch}'

Importante: En LocalStack Community, la API de EKS no está soportada (retorna 501). Comandos como awslocal eks list-clusters fallarán. Para validar el módulo EKS, usa terraform validate/plan con valores mock o valida en AWS real en entornos lab/prod.

Paso 5: Limpiar

Una vez que hayas terminado de probar, destruye los recursos y elimina el archivo de sobreescritura.

# Destruir los recursos en LocalStack
terragrunt destroy -auto-approve

# Eliminar el archivo de sobreescritura
rm _provider_override.tf

Redirección a LocalStack: Dos Métodos para Dos Contextos

Es importante entender que existen dos maneras de redirigir las llamadas de Terraform a LocalStack, cada una adaptada a su contexto:

Flujo de Trabajo Método de Redirección ¿Por qué?
Desarrollo Local Fichero _provider_override.tf Es un “truco” manual y explícito para que un desarrollador redirija el tráfico a su localhost.
Pipeline CI/CD Variable de Entorno AWS_ENDPOINT_URL Es el método estándar y automático para configurar el proveedor en un entorno desatendido como el CI/CD.

1. Desarrollo Local (_provider_override.tf)

En tu máquina local, el fichero _provider_override.tf es detectado automáticamente por Terraform y sus configuraciones tienen prioridad sobre cualquier otra. Es ideal para el desarrollo porque es explícito, temporal y no requiere modificar variables de entorno globales.

2. Pipeline de CI/CD (AWS_ENDPOINT_URL)

En un entorno automatizado como GitLab CI, el proveedor de AWS de Terraform está diseñado para detectar la variable de entorno AWS_ENDPOINT_URL. Cuando esta variable está presente, el proveedor redirige todas las llamadas a la API de AWS a esa URL. Este es el método preferido para la automatización, ya que no requiere modificar ningún fichero de código y se integra de forma nativa con los sistemas de CI/CD.

Diagrama de Flujos de Trabajo de Infraestructura

El siguiente diagrama visualiza los dos flujos principales para el desarrollo y despliegue de la infraestructura.

  graph TD
    subgraph "Flujo 1: Desarrollo Iterativo de Terraform (Local)"
        A[Desarrollador modifica un módulo, ej. 'lab/network'] --> B[Crea '_provider_override.tf']
        B --> C[terragrunt apply]
        C --> D{Recursos creados en LocalStack}
        D --> E[awslocal ec2 describe-vpcs]
        E --> F[terragrunt destroy]
        F --> G[Elimina '_provider_override.tf']
    end

    subgraph "Flujo 2: Pipeline de CI/CD (Automatizado)"
        H[Commit & Push a GitLab] --> I[Job 'infra_test' se ejecuta con LocalStack]
        I -- Falla --> M[Pipeline falla]
        I -- Éxito --> L[Merge a 'main']
        L --> N[Job 'infra_deploy_lab' se ejecuta]
        N --> O{Terragrunt aplica en AWS Lab}
        O -- Éxito --> P[Creación de Git Tag]
        P --> Q[Job 'infra_deploy_prod' se ejecuta]
        Q --> R{Terragrunt aplica en AWS Prod}
    end

CI/CD Integration (GitLab)

Ejemplo de .gitlab-ci.yml para pruebas de infraestructura:

stages:
  - test
  - deploy

infra_test:
  stage: test
  image: hashicorp/terraform:latest
  services:
    - name: localstack/localstack:latest
      alias: localstack
  variables:
    AWS_ACCESS_KEY_ID: "test"
    AWS_SECRET_ACCESS_KEY: "test"
    AWS_DEFAULT_REGION: "eu-central-1"
    # Redirigir las llamadas de AWS al servicio de LocalStack
    AWS_ENDPOINT_URL: "http://localstack:4566"
  script:
    - ./scripts/run-infra-tests.sh

infra_deploy_lab:
  stage: deploy
  image: hashicorp/terraform:latest
  environment: lab
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      changes:
        - infra/terraform/environments/lab/**/*
        - infra/terraform/modules/**/*
  script:
    - cd infra/terraform/environments/lab
    - terragrunt run-all apply -auto-approve

infra_deploy_prod:
  stage: deploy
  image: hashicorp/terraform:latest
  environment: prod
  rules:
    - if: '$CI_COMMIT_TAG'
      changes:
        - infra/terraform/environments/prod/**/*
        - infra/terraform/modules/**/*
  script:
    - cd infra/terraform/environments/prod
    - terragrunt run-all apply -auto-approve

Nota sobre la Eficiencia del Pipeline: La directiva rules:changes hace que nuestros jobs de despliegue sean mucho más inteligentes. El job infra_deploy_lab, por ejemplo, solo se ejecutará si se cumplen dos condiciones: el commit está en la rama main y se han modificado ficheros dentro del entorno lab o de los módulos compartidos. Esto evita despliegues innecesarios, ahorrando tiempo y reduciendo el riesgo de errores.

Gestión del Estado Remoto y Bootstrapping

La gestión del estado se ha simplificado y ahora sigue estas reglas:

  • El archivo raíz infra/terraform/terragrunt.hcl define el remote_state con backend s3 (bucket, región, lock table, etc.) para entornos reales (lab/prod).
  • En pruebas locales (LocalStack/CI), los componentes del entorno lab sobreescriben este backend a local mediante:
    • Un bloque remote_state { backend = "local" } en infra/terraform/environments/lab/*/terragrunt.hcl.
    • Un bloque generate "backend" que crea terragrunt_backend.tf con backend "local" {} evitando colisiones con archivos del módulo.

Ventajas de este enfoque:

  • No hace falta un componente backend separado ni gestión de dependencias para el bootstrapping.
  • Evita la condición de huevo y gallina en CI/LocalStack.
  • Los módulos de Terraform no declaran backends; Terragrunt los gestiona por entorno.

Recursos por Entorno

Recurso LocalStack (Dev) Lab (Staging) Prod (AWS)
S3 Buckets test-bucket app-staging app-prod
PostgreSQL (in Minikube) todo-staging-db todo-prod-db
RabbitMQ (in Minikube) todo-staging-queue todo-prod-queue
EKS - todo-lab-cluster todo-prod-cluster

Flujo de Trabajo de Desarrollo Local

El entorno de desarrollo local se centra en proveer un clúster de Kubernetes (kind) para desplegar y probar la aplicación. La responsabilidad está claramente separada:

  • Terraform/Terragrunt: Se encarga únicamente de provisionar y destruir el clúster de kind (make infra-up y make infra-down).
  • Ansible/Makefile: Se encarga de desplegar y configurar las dependencias de la aplicación (PostgreSQL, RabbitMQ) sobre el clúster ya existente (make deploy-services-local y make configure).
  • Helm/Makefile: Se encarga de desplegar la aplicación en sí (make app-deploy).

Alternativa actual: Clúster local con kind (solo Docker)

Hemos habilitado una alternativa más reproducible para el entorno local usando kind (Kubernetes in Docker) mediante un proveedor de Terraform. Esto evita depender de binarios locales externos y funciona en CI/DC siempre que haya Docker.

  • Proveedor: tehcyx/kind
  • Módulo: infra/terraform/modules/local-k8s
  • Entorno: infra/terraform/environments/dev/local-k8s

Provisionar el clúster

make infra-up

Este objetivo ejecuta Terragrunt para crear el recurso kind_cluster. El kubeconfig se genera en:

infra/terraform/modules/local-k8s/kubeconfig

Para usar kubectl con este clúster:

export KUBECONFIG=infra/terraform/modules/local-k8s/kubeconfig
kubectl cluster-info
kubectl get nodes

Notas tras aplicar (make infra-up)

Al finalizar make infra-up, verás una guía rápida en consola. Resumen:

# Kubeconfig local generado por Terraform
export KUBECONFIG=infra/terraform/modules/local-k8s/kubeconfig

# Validación rápida
kubectl cluster-info
kubectl get nodes

Destruir y limpiar completamente

El objetivo infra-down ha sido reforzado para limpiar residuos de kind y k3d incluso si las CLIs no están instaladas:

make infra-down

Este objetivo ejecuta:

  • terragrunt destroy del componente local-k8s.
  • Eliminación del clúster kind por CLI (si está disponible) o directamente vía Docker por label io.x-k8s.kind.cluster y la red kind.
  • Eliminación del kubeconfig generado en infra/terraform/modules/local-k8s/kubeconfig.
  • Eliminación de la red k3d-<cluster> si hubiese sido creada previamente.

Con esto, evitarás conflictos al cambiar entre motores locales o al reprovisionar el clúster con el mismo nombre.

Pasos del Flujo de Trabajo

  1. Provisionar el Clúster (una sola vez):

    make infra-up
    
  2. Desplegar los Servicios Base:

    make deploy-services-local
    
  3. Configurar los Servicios:

    make configure
    
  4. Desplegar la Aplicación:

    make app-deploy
    
  5. Destruir el Clúster: Cuando termines, puedes eliminar el clúster de Minikube.

    make infra-down
    

Testing de Recursos Kubernetes

Pruebas Locales

# Verificar despliegue
kubectl get deployment todo-backend

# Verificar logs
kubectl logs deployment/todo-backend

# Ejecutar prueba de integración
kubectl run test --image=curlimages/curl --rm -it -- \
  curl http://todo-backend:8000/health

Pruebas en Pipeline

kubernetes_test:
  stage: test
  image: bitnami/kubectl:latest
  script:
    - kubectl apply -f kubernetes/manifests/test-pod.yaml
    - kubectl wait --for=condition=complete job/test-job --timeout=60s
    - kubectl logs job/test-job

Convenciones de Nombrado

Seguimos estas convenciones para consistencia:

Tipo Recurso Convención Ejemplo
S3 <app>-<env>-<purpose> todo-app-dev-uploads
K8s <app>-<component> todo-backend
PostgreSQL <app>-<env>-<db> todo-dev-db

Troubleshooting Ampliado

Problema: Cambios no se reflejan en LocalStack

  • Solución:
    1. Reiniciar contenedor: docker compose restart localstack
    2. Limpiar volumenes: docker compose down -v
    3. Volver a aplicar: terragrunt apply -auto-approve

Problema: Timeouts en CI/CD

  • Solución:
    1. Aumentar timeout en pipeline
    2. Verificar disponibilidad de servicios
    3. Usar caché de dependencias

Problema: Diferencias entre LocalStack y AWS real

  • Solución:
    1. Usar versión más reciente de LocalStack
    2. Verificar soporte de servicios
    3. Implementar pruebas de integración reales en entorno lab

Limitaciones de LocalStack y Alcance de Pruebas

LocalStack Community no emula todos los servicios de AWS. En particular, el API de EKS devuelve 501 Not Implemented en la edición Community. Referencia: LocalStack Coverage.

  • Durante las pruebas automáticas locales y de CI, ejecutamos únicamente los componentes soportados por LocalStack. En este repositorio, eso significa:
    • lab/network: Sí se valida, planifica y aplica contra LocalStack.
    • lab/eks: Se excluye de las pruebas en LocalStack (o se usan entradas mock solo para validación). El despliegue real de EKS se prueba en entornos lab/prod con AWS real.

El script scripts/run-infra-tests.sh ya implementa estas decisiones:

  • Trazas detalladas: set -Eeuo pipefail, set -x con timestamps y trap de errores muestran la línea exacta donde se detiene.
  • Diagnóstico rápido: imprime versiones de terragrunt, terraform, docker y si existe aws CLI.
  • Ejecución acotada: ejecuta init, validate, apply y output exclusivamente dentro de infra/terraform/environments/lab/network.
  • Backend local para pruebas: se fuerza backend local mediante bloques generate y remote_state en lab para evitar dependencias de S3/Dynamo.

Ejemplo de CI actualizado (solo módulos soportados)

infra_test:
  stage: test
  image: hashicorp/terraform:latest
  services:
    - name: localstack/localstack:latest
      alias: localstack
  variables:
    AWS_ACCESS_KEY_ID: "test"
    AWS_SECRET_ACCESS_KEY: "test"
    AWS_DEFAULT_REGION: "eu-central-1"
    AWS_ENDPOINT_URL: "http://localstack:4566"
  script:
    # Ejecuta pruebas solo para 'lab/network' (LocalStack Community soporta EC2/VPC/Subnets)
    - cd infra/terraform/environments/lab/network
    - terragrunt init -input=false
    - terragrunt validate -input=false
    - terragrunt apply -auto-approve -input=false
    - terragrunt output -json > ../../../../infra-test-outputs.json
  artifacts:
    when: always
    paths:
      - infra-test-outputs.json
    expire_in: 1 week

Nota: Para lab/eks, use terragrunt validate/plan con valores mock si desea validar wiring, o pruebe el despliegue real en un entorno lab con AWS.

Prueba rápida con scripts/run-infra-tests.sh (Pipeline & Local)

Este script ejecuta pruebas rápidas de infraestructura contra LocalStack, pensado para:

  • Validación en CI/CD de los módulos soportados por LocalStack (network).
  • Reproducir el mismo flujo en local con un solo comando.

Ruta del script: scripts/run-infra-tests.sh

Prerrequisitos

  • Docker y Docker Compose (para levantar LocalStack).
  • Terraform y Terragrunt en el PATH.
  • Opcional: awscli-local (binario awslocal) para inspección manual de recursos (ver sección de inspección con awslocal).

Variables de entorno relevantes

  • AWS_ENDPOINT_URL (por defecto http://localhost:4566).
  • AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY (se usan valores dummy test/test).
  • AWS_DEFAULT_REGION (nos basamos en eu-central-1).

Alcance de las pruebas

  • Solo se ejecuta el componente infra/terraform/environments/lab/network.
  • LocalStack Community no soporta EKS; por tanto, EKS no se aplica en este flujo.

Ejecución

  • En local (recomendado):

    make test-infra
    

    Esto levantará LocalStack (vía docker compose), esperará a que esté listo y ejecutará init, validate, apply y output en lab/network.

  • En CI/CD (GitLab), el job infra_test de ejemplo llama a este script directamente:

    script:
      - ./scripts/run-infra-tests.sh
    

Salidas

  • El script genera infra-test-outputs.json en la raíz del repo con los outputs del módulo network (vpc_id, public_subnet_ids, private_subnet_ids, …). Puedes usar awslocal y jq para cruzar estos valores con los recursos en LocalStack.

Trazas y robustez

  • El script activa trazas detalladas con timestamps (set -Eeuo pipefail, set -x, trap de errores) y limpia recursivamente cachés .terragrunt-cache antes de ejecutar.

Limitaciones

  • EKS no está soportado en LocalStack Community (retorna 501). Para EKS, limitarse a validate/plan con valores mock en LocalStack o validar con AWS real en los entornos lab/prod.