Saltar al contenido principal

Día 27 - CI/CD para Terraform con LocalStack

🚀 CI/CD para Terraform con GitHub Actions y LocalStack

¡Hoy automatizamos nuestros despliegues de Terraform con LocalStack!
Aprenderemos a crear pipelines de CI/CD con GitHub Actions para deployar nuestra infraestructura usando LocalStack como simulador de AWS, incluyendo S3 para el backend de estado remoto de Terraform.


🤔 ¿Qué es LocalStack y S3?

🐳 LocalStack - El Simulador de AWS

LocalStack es una plataforma que simula los servicios de AWS en tu máquina local, permitiendo desarrollar y probar aplicaciones cloud sin necesidad de conectarse a AWS real.

¿Por qué usar LocalStack?

  • 💰 Sin costos: No pagas por recursos AWS durante desarrollo
  • 🚀 Desarrollo rápido: Testing instantáneo sin latencia de red
  • 🔒 Privacidad: Datos sensibles nunca salen de tu máquina
  • 🧪 Testing seguro: Experimenta sin riesgo de afectar producción
  • 📦 Portabilidad: Mismo ambiente en cualquier máquina

Servicios AWS que simula LocalStack:

✅ S3          # Almacenamiento de objetos
✅ EC2 # Máquinas virtuales
✅ Lambda # Funciones serverless
✅ DynamoDB # Base de datos NoSQL
✅ CloudFormation # Infrastructure as Code
✅ IAM # Gestión de identidades
✅ SQS/SNS # Colas y notificaciones
✅ API Gateway # APIs REST
... y muchos más

🪣 Amazon S3 - Simple Storage Service

Amazon S3 es el servicio de almacenamiento de objetos de AWS, diseñado para almacenar y recuperar cualquier cantidad de datos desde cualquier lugar.

Características principales de S3:

  • 📁 Buckets: Contenedores para organizar objetos
  • 🔄 Versionado: Control de versiones de archivos
  • 🔐 Seguridad: Cifrado y control de acceso granular
  • 📊 Durabilidad: 99.999999999% (11 9's) de durabilidad
  • 🌍 Escalabilidad: Almacenamiento prácticamente ilimitado

¿Por qué S3 para Terraform Backend?

# Beneficios de usar S3 como backend remoto:
✅ Estado compartido entre el equipo
✅ Versionado automático del estado
✅ Bloqueo de estado (con DynamoDB)
✅ Cifrado del archivo de estado
✅ Backup y recuperación automática
✅ Acceso desde CI/CD pipelines

🔄 LocalStack + S3 + Terraform = ❤️

La combinación perfecta para desarrollo:

# Flujo de trabajo:
1️⃣ LocalStack simula S3 localmente
2️⃣ Terraform usa S3 (LocalStack) como backend
3️⃣ Estado remoto compartido sin AWS real
4️⃣ CI/CD testing sin costos
5️⃣ Misma configuración para dev/staging/prod

Comparación: AWS Real vs LocalStack

AspectoAWS RealLocalStack
Costo💰 Pago por uso🆓 Gratis
Velocidad🌐 Latencia de red⚡ Local (ms)
Datos☁️ En la nube🏠 En tu máquina
Producción✅ Ideal❌ Solo desarrollo
Configuración🔑 Credenciales reales🔓 Credenciales test
Persistencia♾️ Permanente🔄 Configurable

🤔 ¿Por qué CI/CD para Infrastructure as Code?

Problemas del Despliegue Manual

Errores humanos en comandos
Inconsistencias entre ambientes
Falta de trazabilidad de cambios
Despliegues lentos y propensos a fallos
Sin rollback automático

Beneficios del CI/CD con LocalStack

Validación automática antes del despliegue
Despliegues consistentes y repetibles
Historial completo de cambios
Rollback rápido en caso de problemas
Múltiples ambientes gestionados igual
Testing local sin costos de AWS
Backend S3 remoto para estado de Terraform


🏗️ Estructura del Proyecto CI/CD con LocalStack

terraform-cicd-localstack/
├── .github/
│ └── workflows/
│ ├── terraform-ci.yml # Pipeline de validación
│ ├── terraform-cd.yml # Pipeline de despliegue
│ └── terraform-destroy.yml # Pipeline de limpieza
├── docker-compose.localstack.yml # LocalStack setup
├── scripts/
│ ├── setup-localstack.sh # Script de configuración
│ └── wait-for-localstack.sh # Script de espera
├── environments/
│ ├── dev.tfvars
│ ├── staging.tfvars
│ └── prod.tfvars
├── modules/
│ ├── docker-webapp/
│ └── s3-backend/ # Módulo para S3 backend
├── backend.tf # Configuración de backend S3
├── main.tf
├── variables.tf
├── outputs.tf
└── README.md

🐳 Configuración de LocalStack

docker-compose.localstack.yml

version: '3.8'

services:
localstack:
image: localstack/localstack:3.0
container_name: terraform-localstack
ports:
- "4566:4566" # LocalStack endpoint
- "4510-4559:4510-4559" # External services port range
environment:
# Servicios AWS a emular
- SERVICES=s3,ec2,iam,lambda,cloudformation,logs,events
# Configuración
- DEBUG=1
- DATA_DIR=/var/lib/localstack/data
- DOCKER_HOST=unix:///var/run/docker.sock
- HOST_TMP_FOLDER=${TMPDIR:-/tmp}/localstack
# Persistencia de datos
- PERSISTENCE=1
volumes:
- "${TMPDIR:-/tmp}/localstack:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
networks:
- terraform-network

networks:
terraform-network:
driver: bridge

Scripts de Configuración

scripts/setup-localstack.sh

#!/bin/bash
set -e

echo "🚀 Configurando LocalStack para Terraform..."

# Variables
LOCALSTACK_ENDPOINT="http://localhost:4566"
BUCKET_NAME="terraform-state-roxs"
AWS_DEFAULT_REGION="us-east-1"

# Configurar AWS CLI para LocalStack
export AWS_ACCESS_KEY_ID="test"
export AWS_SECRET_ACCESS_KEY="test"
export AWS_DEFAULT_REGION="us-east-1"
export AWS_ENDPOINT_URL="http://localhost:4566"

echo "⏳ Esperando a que LocalStack esté listo..."
./scripts/wait-for-localstack.sh

echo "📦 Creando bucket S3 para estado de Terraform..."
aws --endpoint-url=$LOCALSTACK_ENDPOINT s3 mb s3://$BUCKET_NAME || echo "Bucket ya existe"

echo "🔒 Habilitando versionado en el bucket..."
aws --endpoint-url=$LOCALSTACK_ENDPOINT s3api put-bucket-versioning \
--bucket $BUCKET_NAME \
--versioning-configuration Status=Enabled

echo "📋 Listando buckets disponibles..."
aws --endpoint-url=$LOCALSTACK_ENDPOINT s3 ls

echo "✅ LocalStack configurado correctamente!"

scripts/wait-for-localstack.sh

#!/bin/bash
set -e

LOCALSTACK_ENDPOINT="http://localhost:4566"
MAX_ATTEMPTS=30
ATTEMPT=1

echo "⏳ Esperando a que LocalStack esté disponible..."

while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
echo "Intento $ATTEMPT/$MAX_ATTEMPTS..."

if curl -s "$LOCALSTACK_ENDPOINT/_localstack/health" > /dev/null; then
echo "✅ LocalStack está listo!"
exit 0
fi

echo "😴 LocalStack no está listo aún, esperando 5 segundos..."
sleep 5
ATTEMPT=$((ATTEMPT + 1))
done

echo "❌ LocalStack no está disponible después de $MAX_ATTEMPTS intentos"
exit 1

🔍 Pipeline de CI (Continuous Integration) con LocalStack

El pipeline de CI valida nuestro código antes de que se fusione a la rama principal, usando LocalStack para simular AWS.

.github/workflows/terraform-ci.yml

name: Terraform CI - Validación con LocalStack

# Cuándo ejecutar el pipeline
on:
pull_request:
branches: [main]
paths:
- '**.tf'
- '**.tfvars'
- '.github/workflows/**'
- 'docker-compose.localstack.yml'

# Variables globales
env:
TF_VERSION: 1.6.0
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1

jobs:
# Job 1: Validación básica
validate:
name: Validar Terraform
runs-on: ubuntu-latest

services:
localstack:
image: localstack/localstack:3.0
ports:
- 4566:4566
env:
SERVICES: s3,ec2,iam,lambda,cloudformation,logs,events
DEBUG: 1
DATA_DIR: /var/lib/localstack/data
PERSISTENCE: 1
options: >-
--health-cmd "curl -f http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
# Paso 1: Descargar código
- name: Checkout código
uses: actions/checkout@v4

# Paso 2: Configurar permisos para scripts
- name: Configurar permisos de scripts
run: |
chmod +x scripts/setup-localstack.sh
chmod +x scripts/wait-for-localstack.sh

# Paso 3: Instalar Terraform y AWS CLI
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Setup AWS CLI
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

# Paso 4: Configurar LocalStack
- name: Configurar LocalStack
run: |
export AWS_ENDPOINT_URL=http://localhost:4566
./scripts/setup-localstack.sh

# Paso 5: Verificar formato
- name: Verificar formato de código
run: terraform fmt -check -recursive

# Paso 6: Inicializar Terraform con backend S3
- name: Terraform Init
run: |
terraform init \
-backend-config="endpoint=http://localhost:4566" \
-backend-config="access_key=test" \
-backend-config="secret_key=test"
env:
AWS_ENDPOINT_URL: http://localhost:4566

# Paso 7: Validar sintaxis
- name: Validar sintaxis
run: terraform validate

# Job 2: Generar plan para cada ambiente con LocalStack
plan:
name: Plan para Ambientes (LocalStack)
runs-on: ubuntu-latest
needs: validate
strategy:
matrix:
environment: [dev, staging]

services:
localstack:
image: localstack/localstack:3.0
ports:
- 4566:4566
env:
SERVICES: s3,ec2,iam,lambda,cloudformation,logs,events
DEBUG: 1
DATA_DIR: /var/lib/localstack/data
PERSISTENCE: 1
options: >-
--health-cmd "curl -f http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5

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

- name: Configurar permisos de scripts
run: |
chmod +x scripts/setup-localstack.sh
chmod +x scripts/wait-for-localstack.sh

- name: Setup Terraform y AWS CLI
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Setup AWS CLI
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

- name: Configurar LocalStack
run: |
export AWS_ENDPOINT_URL=http://localhost:4566
./scripts/setup-localstack.sh

- name: Terraform Init
run: |
terraform init \
-backend-config="endpoint=http://localhost:4566" \
-backend-config="access_key=test" \
-backend-config="secret_key=test"
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Seleccionar workspace
run: |
terraform workspace select ${{ matrix.environment }} || \
terraform workspace new ${{ matrix.environment }}
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Generar plan
run: |
terraform plan \
-var-file="environments/${{ matrix.environment }}.tfvars" \
-out="${{ matrix.environment }}.tfplan"
env:
AWS_ENDPOINT_URL: http://localhost:4566
TF_VAR_database_password: "ci-test-password-${{ matrix.environment }}"

# Verificar recursos en LocalStack
- name: Verificar recursos en LocalStack
run: |
echo "📊 Verificando recursos creados en LocalStack..."
aws --endpoint-url=http://localhost:4566 s3 ls
aws --endpoint-url=http://localhost:4566 s3 ls s3://terraform-state-roxs

# Guardar el plan para usar en CD
- name: Subir plan
uses: actions/upload-artifact@v3
with:
name: tfplan-${{ matrix.environment }}
path: ${{ matrix.environment }}.tfplan
retention-days: 1

# Job 3: Comentar el plan en el PR
comment-plan:
name: Comentar Plan en PR
runs-on: ubuntu-latest
needs: plan
if: github.event_name == 'pull_request'

services:
localstack:
image: localstack/localstack:3.0
ports:
- 4566:4566
env:
SERVICES: s3,ec2,iam,lambda,cloudformation,logs,events
DEBUG: 1
options: >-
--health-cmd "curl -f http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5

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

- name: Configurar permisos de scripts
run: |
chmod +x scripts/setup-localstack.sh
chmod +x scripts/wait-for-localstack.sh

- name: Setup Terraform y AWS CLI
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Setup AWS CLI
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

- name: Configurar LocalStack
run: |
export AWS_ENDPOINT_URL=http://localhost:4566
./scripts/setup-localstack.sh

- name: Terraform Init
run: |
terraform init \
-backend-config="endpoint=http://localhost:4566" \
-backend-config="access_key=test" \
-backend-config="secret_key=test"
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Descargar plan de dev
uses: actions/download-artifact@v3
with:
name: tfplan-dev

- name: Mostrar plan en comentario
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');

// Generar el plan en formato legible
const { execSync } = require('child_process');
const planOutput = execSync('terraform show -no-color dev.tfplan', {
encoding: 'utf8',
env: { ...process.env, AWS_ENDPOINT_URL: 'http://localhost:4566' }
});

// Crear comentario
const output = `## 📋 Plan de Terraform (Dev) con LocalStack

<details>
<summary>Ver cambios propuestos</summary>

\`\`\`
${planOutput}
\`\`\`

</details>

**✅ Validado con LocalStack**
**📦 Backend:** S3 (terraform-state-roxs)
**🌍 Ambiente:** Desarrollo
`;

// Publicar comentario
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});

🚀 Pipeline de CD (Continuous Deployment) con LocalStack

El pipeline de CD despliega automáticamente usando LocalStack cuando hay cambios en la rama principal.

.github/workflows/terraform-cd.yml

name: Terraform CD - Despliegue con LocalStack

# Cuándo ejecutar
on:
push:
branches: [main]
paths:
- '**.tf'
- '**.tfvars'
- 'docker-compose.localstack.yml'
# También permitir ejecución manual
workflow_dispatch:
inputs:
environment:
description: 'Ambiente a desplegar'
required: true
default: 'dev'
type: choice
options:
- dev
- staging
- prod
action:
description: 'Acción a realizar'
required: true
default: 'apply'
type: choice
options:
- plan
- apply
- destroy

env:
TF_VERSION: 1.6.0
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1

jobs:
# Despliegue a Desarrollo (automático)
deploy-dev:
name: 🔧 Desplegar a Desarrollo (LocalStack)
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: development
url: http://localhost:8080

services:
localstack:
image: localstack/localstack:3.0
ports:
- 4566:4566
env:
SERVICES: s3,ec2,iam,lambda,cloudformation,logs,events
DEBUG: 1
DATA_DIR: /var/lib/localstack/data
PERSISTENCE: 1
options: >-
--health-cmd "curl -f http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5

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

- name: Configurar permisos de scripts
run: |
chmod +x scripts/setup-localstack.sh
chmod +x scripts/wait-for-localstack.sh

- name: Setup Terraform y herramientas
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Setup Docker
uses: docker/setup-buildx-action@v3

- name: Setup AWS CLI
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

- name: Configurar LocalStack y S3 Backend
run: |
export AWS_ENDPOINT_URL=http://localhost:4566
./scripts/setup-localstack.sh

- name: Terraform Init con S3 Backend
run: |
terraform init \
-backend-config="endpoint=http://localhost:4566" \
-backend-config="access_key=test" \
-backend-config="secret_key=test"
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Seleccionar workspace dev
run: terraform workspace select dev || terraform workspace new dev
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Aplicar cambios a dev
run: |
terraform apply \
-var-file="environments/dev.tfvars" \
-auto-approve
env:
AWS_ENDPOINT_URL: http://localhost:4566
TF_VAR_database_password: ${{ secrets.DEV_DB_PASSWORD || 'dev-default-password' }}

- name: Verificar despliegue en LocalStack
run: |
echo "🎉 Despliegue a desarrollo completado!"
echo "📊 Obteniendo información del despliegue..."
terraform output

echo "📦 Verificando recursos en LocalStack S3..."
aws --endpoint-url=http://localhost:4566 s3 ls s3://terraform-state-roxs/workspaces/dev/

echo "🔍 Estado actual de Terraform..."
aws --endpoint-url=http://localhost:4566 s3 cp s3://terraform-state-roxs/workspaces/dev/terraform.tfstate - | head -20

# Despliegue a Staging (con aprobación)
deploy-staging:
name: 🧪 Desplegar a Staging (LocalStack)
runs-on: ubuntu-latest
needs: deploy-dev
if: github.ref == 'refs/heads/main'
environment:
name: staging
url: http://localhost:8081

services:
localstack:
image: localstack/localstack:3.0
ports:
- 4566:4566
env:
SERVICES: s3,ec2,iam,lambda,cloudformation,logs,events
DEBUG: 1
DATA_DIR: /var/lib/localstack/data
PERSISTENCE: 1
options: >-
--health-cmd "curl -f http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5

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

- name: Configurar permisos de scripts
run: |
chmod +x scripts/setup-localstack.sh
chmod +x scripts/wait-for-localstack.sh

- name: Setup Terraform y herramientas
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Setup Docker
uses: docker/setup-buildx-action@v3

- name: Setup AWS CLI
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

- name: Configurar LocalStack y S3 Backend
run: |
export AWS_ENDPOINT_URL=http://localhost:4566
./scripts/setup-localstack.sh

- name: Terraform Init con S3 Backend
run: |
terraform init \
-backend-config="endpoint=http://localhost:4566" \
-backend-config="access_key=test" \
-backend-config="secret_key=test"
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Seleccionar workspace staging
run: terraform workspace select staging || terraform workspace new staging
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Aplicar cambios a staging
run: |
terraform apply \
-var-file="environments/staging.tfvars" \
-auto-approve
env:
AWS_ENDPOINT_URL: http://localhost:4566
TF_VAR_database_password: ${{ secrets.STAGING_DB_PASSWORD || 'staging-default-password' }}

- name: Verificar aplicación en staging
run: |
echo "⏳ Esperando a que los servicios estén listos..."
sleep 30

echo "🧪 Verificando que la aplicación responda..."
curl -f http://localhost:8081 || echo "⚠️ Aplicación no responde aún"

echo "📦 Estado en LocalStack S3..."
aws --endpoint-url=http://localhost:4566 s3 ls s3://terraform-state-roxs/workspaces/staging/

# Despliegue a Producción (solo manual)
deploy-prod:
name: 🏭 Desplegar a Producción (LocalStack)
runs-on: ubuntu-latest
if: github.event.inputs.environment == 'prod'
environment:
name: production
url: http://production.roxs-voting.com

services:
localstack:
image: localstack/localstack:3.0
ports:
- 4566:4566
env:
SERVICES: s3,ec2,iam,lambda,cloudformation,logs,events
DEBUG: 1
DATA_DIR: /var/lib/localstack/data
PERSISTENCE: 1
options: >-
--health-cmd "curl -f http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5

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

- name: Configurar permisos de scripts
run: |
chmod +x scripts/setup-localstack.sh
chmod +x scripts/wait-for-localstack.sh

- name: Setup Terraform y herramientas
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Setup AWS CLI
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

- name: Configurar LocalStack y S3 Backend
run: |
export AWS_ENDPOINT_URL=http://localhost:4566
./scripts/setup-localstack.sh

- name: Terraform Init con S3 Backend
run: |
terraform init \
-backend-config="endpoint=http://localhost:4566" \
-backend-config="access_key=test" \
-backend-config="secret_key=test"
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Seleccionar workspace prod
run: terraform workspace select prod || terraform workspace new prod
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Plan para producción
run: |
terraform plan \
-var-file="environments/prod.tfvars" \
-out=prod.tfplan
env:
AWS_ENDPOINT_URL: http://localhost:4566
TF_VAR_database_password: ${{ secrets.PROD_DB_PASSWORD || 'prod-secure-password' }}

- name: Mostrar plan
run: terraform show -no-color prod.tfplan

- name: Aplicar a producción
if: github.event.inputs.action == 'apply'
run: terraform apply -auto-approve prod.tfplan
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Verificar producción
if: github.event.inputs.action == 'apply'
run: |
echo "🎉 ¡Despliegue a producción completado!"
terraform output

echo "📦 Verificando estado en LocalStack S3..."
aws --endpoint-url=http://localhost:4566 s3 ls s3://terraform-state-roxs/workspaces/prod/

- name: Notificar equipo
if: always()
run: |
echo "📢 Notificando al equipo sobre el despliegue..."
echo "🎯 Ambiente: Producción"
echo "📊 Acción: ${{ github.event.inputs.action }}"
echo "🔍 Estado del workflow: ${{ job.status }}"
# Aquí puedes agregar notificaciones a Slack, Teams, etc.

🧹 Pipeline de Limpieza con LocalStack

Para destruir recursos cuando ya no los necesites.

.github/workflows/terraform-destroy.yml

name: Terraform Destroy - Limpieza con LocalStack

# Solo ejecución manual
on:
workflow_dispatch:
inputs:
environment:
description: 'Ambiente a destruir'
required: true
type: choice
options:
- dev
- staging
- prod
confirm:
description: 'Escriba "DESTROY" para confirmar'
required: true
type: string

env:
TF_VERSION: 1.6.0
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1

jobs:
destroy:
name: 🗑️ Destruir Infraestructura (LocalStack)
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}

services:
localstack:
image: localstack/localstack:3.0
ports:
- 4566:4566
env:
SERVICES: s3,ec2,iam,lambda,cloudformation,logs,events
DEBUG: 1
DATA_DIR: /var/lib/localstack/data
PERSISTENCE: 1
options: >-
--health-cmd "curl -f http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Validar confirmación
run: |
if [ "${{ github.event.inputs.confirm }}" != "DESTROY" ]; then
echo "❌ Confirmación incorrecta. Debe escribir 'DESTROY'"
exit 1
fi
echo "✅ Confirmación válida para ambiente: ${{ github.event.inputs.environment }}"

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

- name: Configurar permisos de scripts
run: |
chmod +x scripts/setup-localstack.sh
chmod +x scripts/wait-for-localstack.sh

- name: Setup Terraform y AWS CLI
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Setup AWS CLI
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

- name: Configurar LocalStack y S3 Backend
run: |
export AWS_ENDPOINT_URL=http://localhost:4566
./scripts/setup-localstack.sh

- name: Terraform Init con S3 Backend
run: |
terraform init \
-backend-config="endpoint=http://localhost:4566" \
-backend-config="access_key=test" \
-backend-config="secret_key=test"
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Seleccionar workspace
run: terraform workspace select ${{ github.event.inputs.environment }}
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Verificar estado actual
run: |
echo "📋 Estado actual del ambiente ${{ github.event.inputs.environment }}:"
terraform show

echo "📦 Archivos en S3 antes de destruir:"
aws --endpoint-url=http://localhost:4566 s3 ls s3://terraform-state-roxs/workspaces/${{ github.event.inputs.environment }}/ || echo "No hay archivos"

- name: Plan destroy
run: |
terraform plan -destroy \
-var-file="environments/${{ github.event.inputs.environment }}.tfvars" \
-out=destroy.tfplan
env:
AWS_ENDPOINT_URL: http://localhost:4566
TF_VAR_database_password: "dummy-password-for-destroy"

- name: Mostrar qué se va a destruir
run: terraform show -no-color destroy.tfplan

- name: Destruir infraestructura
run: terraform apply -auto-approve destroy.tfplan
env:
AWS_ENDPOINT_URL: http://localhost:4566

- name: Confirmar destrucción
run: |
echo "🗑️ Infraestructura de ${{ github.event.inputs.environment }} destruida"
echo "📊 Estado final:"
terraform show

echo "📦 Verificando limpieza en S3:"
aws --endpoint-url=http://localhost:4566 s3 ls s3://terraform-state-roxs/workspaces/${{ github.event.inputs.environment }}/ || echo "✅ Workspace limpio"

- name: Limpiar workspace (opcional)
if: github.event.inputs.environment != 'prod'
run: |
echo "🧹 Eliminando workspace de ${{ github.event.inputs.environment }}..."
terraform workspace select default
terraform workspace delete ${{ github.event.inputs.environment }} || echo "Workspace ya eliminado"
env:
AWS_ENDPOINT_URL: http://localhost:4566

🔐 Configuración de Secrets y Variables

Para que los pipelines funcionen con LocalStack, necesitas configurar secrets en GitHub:

En tu repositorio GitHub:

  1. Ve a SettingsSecrets and variablesActions
  2. Agregar estos secrets:
# Passwords de base de datos por ambiente
DEV_DB_PASSWORD=dev_password_123
STAGING_DB_PASSWORD=staging_password_456
PROD_DB_PASSWORD=super_secure_prod_password_789

# Credenciales LocalStack (opcionales, ya que usa 'test')
LOCALSTACK_ACCESS_KEY=test
LOCALSTACK_SECRET_KEY=test

# Si usas Docker Registry privado
DOCKER_USERNAME=tu_usuario
DOCKER_PASSWORD=tu_password

# Para notificaciones (opcional)
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...

# Variables específicas de LocalStack
LOCALSTACK_ENDPOINT=http://localhost:4566
AWS_DEFAULT_REGION=us-east-1

Variables de Ambiente en GitHub Actions

# En cada workflow, definir:
env:
TF_VERSION: 1.6.0
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
AWS_ENDPOINT_URL: http://localhost:4566

🎯 Configuración de Environments

GitHub permite configurar protection rules por ambiente:

1. Crear Environments

En SettingsEnvironments, crear:

  • development (sin restricciones)
  • staging (con reviewers opcionales)
  • production (con reviewers obligatorios)

2. Protection Rules para Producción

# Configuración recomendada para production
Required reviewers: 2
Deployment branches: main only
Wait timer: 0 minutes

📁 Archivos de Variables por Ambiente con LocalStack

environments/dev.tfvars

# Desarrollo - Recursos mínimos con LocalStack
app_name = "roxs-voting-dev"
replica_count = 1
memory_limit = 256

external_ports = {
vote = 8080
result = 3000
}

enable_monitoring = false
backup_enabled = false

# Configuración específica de LocalStack
aws_region = "us-east-1"
localstack_endpoint = "http://localhost:4566"
s3_bucket_suffix = "dev"

environments/staging.tfvars

# Staging - Configuración intermedia con LocalStack
app_name = "roxs-voting-staging"
replica_count = 2
memory_limit = 512

external_ports = {
vote = 8081
result = 3001
}

enable_monitoring = true
backup_enabled = true

# Configuración específica de LocalStack
aws_region = "us-east-1"
localstack_endpoint = "http://localhost:4566"
s3_bucket_suffix = "staging"

environments/prod.tfvars

# Producción - Máximos recursos con LocalStack
app_name = "roxs-voting-prod"
replica_count = 3
memory_limit = 1024

external_ports = {
vote = 80
result = 3000
}

enable_monitoring = true
backup_enabled = true

# Configuración específica de LocalStack
aws_region = "us-east-1"
localstack_endpoint = "http://localhost:4566"
s3_bucket_suffix = "prod"

🔄 Flujo de Trabajo Completo con LocalStack

1. Desarrollo Local

# Iniciar LocalStack
docker-compose -f docker-compose.localstack.yml up -d

# Configurar LocalStack
./scripts/setup-localstack.sh

# Crear feature branch
git checkout -b feature/nueva-funcionalidad

# Hacer cambios en Terraform
# ... editar archivos ...

# Validar localmente con LocalStack
export AWS_ENDPOINT_URL=http://localhost:4566
terraform fmt
terraform validate
terraform init -backend-config="endpoint=http://localhost:4566"
terraform plan

# Commit y push
git add .
git commit -m "feat: agregar nueva funcionalidad"
git push origin feature/nueva-funcionalidad

2. Pull Request con Validación LocalStack

# Crear PR en GitHub
# ↓
# Se ejecuta automáticamente el pipeline CI con LocalStack
# ↓
# Revisar el plan de Terraform en los comentarios
# ↓
# Verificar que todos los recursos se crean en LocalStack
# ↓
# Aprobar y mergear el PR

3. Despliegue Automático

# Al hacer merge a main:
# ↓
# Se ejecuta pipeline CD automáticamente con LocalStack
# ↓
# Despliega a DEV → luego a STAGING (usando LocalStack)
# ↓
# Para PROD: ejecutar manualmente el workflow

4. Verificación con LocalStack

# Verificar que todo funciona en LocalStack
curl http://localhost:8080 # dev
curl http://localhost:8081 # staging

# Verificar estado en S3 LocalStack
aws --endpoint-url=http://localhost:4566 s3 ls s3://terraform-state-roxs/

# Ver logs en GitHub Actions
# Verificar outputs de Terraform

🛠️ Comandos Útiles para CI/CD con LocalStack

LocalStack Commands

# Iniciar LocalStack localmente
docker-compose -f docker-compose.localstack.yml up -d

# Verificar estado de LocalStack
curl http://localhost:4566/_localstack/health

# Listar servicios disponibles
curl http://localhost:4566/_localstack/info

# Ver buckets S3 en LocalStack
aws --endpoint-url=http://localhost:4566 s3 ls

# Verificar contenido del bucket de estado
aws --endpoint-url=http://localhost:4566 s3 ls s3://terraform-state-roxs/ --recursive

Ver estado de workflows

# Usando GitHub CLI
gh workflow list
gh workflow view terraform-ci.yml
gh run list --workflow=terraform-cd.yml

# Ver logs de un run específico
gh run view 1234567890 --log

Ejecutar workflows manualmente

# Desplegar a staging con LocalStack
gh workflow run terraform-cd.yml -f environment=staging -f action=apply

# Destruir ambiente de dev en LocalStack
gh workflow run terraform-destroy.yml -f environment=dev -f confirm=DESTROY

Debugging de problemas con LocalStack

# Ver último run fallido
gh run list --limit 1 --status failure

# Verificar LocalStack logs
docker logs terraform-localstack

# Verificar conectividad a LocalStack
curl -f http://localhost:4566/_localstack/health

# Verificar secrets configurados
gh secret list

🧪 Testing del Pipeline con LocalStack

Crear un PR de Prueba

# 1. Iniciar LocalStack localmente
docker-compose -f docker-compose.localstack.yml up -d

# 2. Crear cambio pequeño
echo "# Test change for LocalStack" >> README.md
git add README.md
git commit -m "test: pipeline CI/CD con LocalStack"
git push origin feature/test-pipeline-localstack

# 3. Crear PR y verificar que:
# ✅ Se ejecuta el pipeline CI con LocalStack
# ✅ LocalStack services están healthy
# ✅ Se crea bucket S3 para estado
# ✅ Aparece comentario con el plan
# ✅ Todos los checks pasan

# 4. Mergear y verificar que:
# ✅ Se ejecuta pipeline CD con LocalStack
# ✅ Se despliega a dev y staging usando LocalStack
# ✅ Estado se almacena en S3 LocalStack
# ✅ La aplicación funciona

Verificar Recursos en LocalStack

# Verificar que los recursos están siendo creados
aws --endpoint-url=http://localhost:4566 s3 ls
aws --endpoint-url=http://localhost:4566 ec2 describe-instances
aws --endpoint-url=http://localhost:4566 iam list-roles

# Verificar el estado de Terraform en S3
aws --endpoint-url=http://localhost:4566 s3 cp s3://terraform-state-roxs/workspaces/dev/terraform.tfstate - | jq '.'

💡 Mejores Prácticas con LocalStack

1. Protección de Ramas

# En Settings → Branches → main
✅ Require pull request reviews
✅ Require status checks to pass
✅ Require branches to be up to date
✅ Include administrators

2. LocalStack Configuration

# ✅ Usar persistencia para mantener datos entre reinicios
# ✅ Configurar servicios específicos que necesitas
# ✅ Usar health checks en los pipelines
# ✅ Verificar connectivity antes de usar
# ❌ No usar LocalStack para producción real

3. Secrets Management

# ✅ Usar secrets de GitHub para passwords reales
# ✅ Usar credenciales 'test' para LocalStack
# ✅ Separar configuración por ambiente
# ❌ Nunca hardcodear secrets en código

4. Environments Strategy

# ✅ dev → automático con LocalStack, sin restricciones
# ✅ staging → automático con LocalStack, con reviewers opcionales
# ✅ prod → manual, considerar AWS real para producción

5. Monitoring y Debugging

# Configurar verificaciones para:
- LocalStack health status
- S3 bucket accessibility
- Terraform state integrity
- Pipeline performance

🚨 Troubleshooting Común con LocalStack

1. Error: "LocalStack not ready"

# Solución: Verificar health check
curl http://localhost:4566/_localstack/health

# Incrementar tiempo de espera
./scripts/wait-for-localstack.sh

2. Error: "S3 bucket not found"

# Verificar que el bucket existe
aws --endpoint-url=http://localhost:4566 s3 ls

# Recrear bucket si es necesario
aws --endpoint-url=http://localhost:4566 s3 mb s3://terraform-state-roxs

3. Error: "Backend initialization failed"

# Verificar configuración de backend
terraform init -backend-config="endpoint=http://localhost:4566" \
-backend-config="access_key=test" \
-backend-config="secret_key=test"

4. Error: "Workspace doesn't exist"

# Solución: Crear workspace automáticamente
terraform workspace select $ENV || terraform workspace new $ENV

5. Pipeline muy lento con LocalStack

# Optimización: Cache de Terraform y LocalStack data
- name: Cache LocalStack data
uses: actions/cache@v3
with:
path: /tmp/localstack
key: ${{ runner.os }}-localstack-${{ hashFiles('docker-compose.localstack.yml') }}

- name: Cache Terraform
uses: actions/cache@v3
with:
path: ~/.terraform.d/plugin-cache
key: ${{ runner.os }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}

6. Error: "Port already in use"

# Verificar puertos en uso
lsof -i :4566

# Parar contenedores conflictivos
docker stop $(docker ps -q --filter "publish=4566")

✅ ¿Qué Aprendiste Hoy?

CI/CD fundamentals para Infrastructure as Code con LocalStack
LocalStack integration para simular AWS en pipelines
S3 backend configuration para estado remoto de Terraform
GitHub Actions con servicios de LocalStack
Pipelines de validación (CI) usando LocalStack
Pipelines de despliegue (CD) automáticos con LocalStack
Backend S3 remoto para múltiples workspaces
Protection rules y environments en GitHub
Secrets management con LocalStack
Troubleshooting de problemas específicos de LocalStack
Testing y debugging de infraestructura local


🔮 ¿Qué Sigue Mañana?

Mañana en el Día 28 - el gran final - tendremos:

  • Estrategia para abordar el proyecto final con LocalStack
  • Metodología de implementación híbrida (LocalStack + AWS)
  • Planificación del desafío roxs-voting-app con CI/CD completo
  • Consolidación de todos los conceptos DevOps + LocalStack

📚 Recursos Adicionales


💬 Comparte tu progreso en la comunidad con el hashtag #DevOpsConRoxs

¡Excelente trabajo automatizando tus despliegues con CI/CD y LocalStack! 🚀🎉