Problema

Muchas veces los ingenieros necesitan un entorno de desarrollo que sea reproducible, aislado y accesible sin depender de una máquina local. La solución típica es lanzar una VM o usar Docker, pero esas opciones no escalan cuando se quiere compartir el entorno con varios usuarios o integrarlo en pipelines CI/CD. El patrón recurrente es: provisionar infraestructura en la nube (VPC, clúster Kubernetes, registro de imágenes) y exponer una terminal web que permita a cualquier persona trabajar desde el navegador, todo bajo control de código. El reto está en combinar Terraform, GitHub Actions y Kubernetes de forma que la infraestructura sea declarativa, los despliegues sean automáticos y el coste sea predecible.

Causa

  1. Fragmentación de la configuración – Cuando la red, el clúster y los recursos de CI se definen en archivos diferentes, es fácil perder consistencia y crear dependencias ocultas.
  2. Falta de automatización de CI/CD – Sin un pipeline que ejecute terraform apply y despliegue la aplicación, cada cambio requiere intervención manual, lo que genera errores de sincronización.
  3. Exposición insegura de la terminal – Usar ttyd sin un certificado TLS o sin control de dominio genera vulnerabilidades y problemas de acceso.
  4. Costes inesperados – Un clúster EKS activo 24/7 genera facturas altas; la ausencia de políticas de auto‑escalado o de destrucción automática deja recursos encendidos sin necesidad.
  5. Gestión de imágenes – Subir imágenes a ECR sin versionado o sin limpieza periódica ocupa espacio y aumenta el gasto.

Solución

Una arquitectura declarativa que cubra los cuatro pilares del problema:

  1. Terraform como única fuente de verdad

    • Define VPC, subredes, EKS, ECR, Route53 y ACM en módulos reutilizables.
    • Usa lifecycle { prevent_destroy = false } solo donde sea crítico.
    • Añade variables para habilitar spot_instance y node_group con escalado automático.
  2. GitHub Actions como orquestador

    • Workflow que ejecuta terraform init, plan y apply en la rama main.
    • Paso de plan como artefacto para revisión manual antes del apply.
    • Job de destroy programado (por ejemplo, a medianoche) para evitar costes cuando el sandbox no se usa.
  3. Kubernetes para el sandbox

    • Deploy de ttyd como Deployment con un Service tipo LoadBalancer.
    • Anotaciones de Ingress para ACM y dominio personalizado.
    • ConfigMap que monta la imagen Docker del proyecto y define el comando de arranque.
  4. Control de costes

    • Habilita cluster_autoscaler y cluster_autoscaler en los node groups.
    • Usa etiquetas de coste (CostCenter, Owner) para filtrar en AWS Billing.
    • Configura aws_budget vía Terraform para recibir alertas cuando el gasto supere un umbral.

Flujo de trabajo recomendado

  1. Commit → GitHub Actions ejecuta plan → Revisor aprueba → apply crea/actualiza infraestructura.
  2. Pull request que modifica el Dockerfile o el manifiesto de Kubernetes dispara un build en GitHub Actions, sube la imagen a ECR y actualiza el Deployment con kubectl set image.
  3. Programación de destrucción mediante un workflow cron que corre terraform destroy -target=module.eks cuando no haya usuarios activos (puedes consultar CloudWatch metric ActiveConnections del LoadBalancer).

Cuándo aplicar esta solución

  • Necesitas un entorno de desarrollo que pueda ser accedido desde cualquier navegador sin instalar clientes SSH.
  • El proyecto requiere que varios colaboradores prueben cambios simultáneamente y que el entorno sea idéntico a producción.
  • Quieres que la infraestructura sea versionada y reproducible, y que los cambios pasen por revisión.
  • El presupuesto es limitado y necesitas mecanismos automáticos para apagar recursos fuera de horario.

No aplicar si el equipo solo necesita una VM ligera o si la aplicación no depende de Kubernetes; en esos casos Terraform + EC2 puede ser más simple.

Código

# terraform/main.tf – módulo principal
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  name   = "${var.project_name}-vpc"
  cidr   = "10.0.0.0/16"
  azs    = ["${var.aws_region}a", "${var.aws_region}b"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
}

module "eks" {
  source          = "terraform-aws-modules/eks/aws"
  cluster_name    = "${var.project_name}-eks"
  cluster_version = "1.30"
  subnets         = module.vpc.private_subnets
  vpc_id          = module.vpc.vpc_id

  node_groups = {
    dev = {
      desired_capacity = 2
      max_capacity     = 4
      min_capacity     = 1
      instance_type    = "t3.medium"
      spot_price       = "0.02"
      labels = {
        role = "dev"
      }
    }
  }

  enable_irsa = true
}
# .github/workflows/ci-cd.yml
name: CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 0 * * *'   # destroy nightly

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.9.0
      - name: Terraform Init
        run: terraform init
      - name: Terraform Plan
        id: plan
        run: terraform plan -out=tfplan
      - name: Upload Plan
        uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: tfplan
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve tfplan

  build-image:
    needs: terraform
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v2
      - name: Build & Push
        run: |
          IMAGE_TAG=${{ github.sha }}
          docker build -t ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPO }}:$IMAGE_TAG .
          docker push ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPO }}:$IMAGE_TAG
      - name: Update Deployment
        run: |
          kubectl set image deployment/ttyd ttyd=${{ env.ECR_REGISTRY }}/${{ env.ECR_REPO }}:$IMAGE_TAG --record
# k8s/ttyd-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ttyd
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ttyd
  template:
    metadata:
      labels:
        app: ttyd
    spec:
      containers:
        - name: ttyd
          image: <ECR_URI>:<TAG>
          args: ["bash"]
          ports:
            - containerPort: 7681
---
apiVersion: v1
kind: Service
metadata:
  name: ttyd-svc
spec:
  type: LoadBalancer
  ports:
    - port: 443
      targetPort: 7681
  selector:
    app: ttyd

Verificación

  1. Ejecuta terraform apply y confirma que se crean VPC, subredes y el clúster EKS.
  2. En la consola de AWS, verifica que el LoadBalancer tiene una IP pública y que el certificado ACM está asociado al dominio.
  3. Accede a https://sandbox.tu-dominio.com y comprueba que la terminal ttyd muestra un prompt de Bash.
  4. Usa kubectl get pods -n default para asegurarte de que el pod está en estado Running.
  5. Revisa la factura de AWS al día siguiente; si el gasto supera el umbral, ajusta desired_capacity o el cron de destroy.

Notas adicionales

  • Persistencia de datos: ttyd no guarda el estado del shell. Si necesitas mantener archivos entre sesiones, monta un PersistentVolumeClaim con EFS.
  • Seguridad: Configura IAM OIDC y usa ServiceAccount con permisos mínimos para que el pod pueda leer imágenes de ECR.
  • Escalado de usuarios: Si varios usuarios acceden simultáneamente, aumenta replicas y habilita un Ingress con sessionAffinity: ClientIP.
  • Coste de EKS: El control de nodos spot reduce el gasto en un 70 % en la mayoría de regiones; sin embargo, ten en cuenta que los nodos spot pueden ser interrumpidos, por lo que los pods deben tener podDisruptionBudget.
  • Limpieza de imágenes: Programa un workflow que ej