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
- 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.
- Falta de automatización de CI/CD – Sin un pipeline que ejecute
terraform applyy despliegue la aplicación, cada cambio requiere intervención manual, lo que genera errores de sincronización. - Exposición insegura de la terminal – Usar
ttydsin un certificado TLS o sin control de dominio genera vulnerabilidades y problemas de acceso. - 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.
- 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:
-
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_instanceynode_groupcon escalado automático.
-
GitHub Actions como orquestador
- Workflow que ejecuta
terraform init,planyapplyen la ramamain. - 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.
- Workflow que ejecuta
-
Kubernetes para el sandbox
- Deploy de
ttydcomoDeploymentcon unServicetipoLoadBalancer. - Anotaciones de Ingress para ACM y dominio personalizado.
- ConfigMap que monta la imagen Docker del proyecto y define el comando de arranque.
- Deploy de
-
Control de costes
- Habilita
cluster_autoscalerycluster_autoscaleren los node groups. - Usa etiquetas de coste (
CostCenter,Owner) para filtrar en AWS Billing. - Configura
aws_budgetvía Terraform para recibir alertas cuando el gasto supere un umbral.
- Habilita
Flujo de trabajo recomendado
- Commit → GitHub Actions ejecuta plan → Revisor aprueba → apply crea/actualiza infraestructura.
- 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. - Programación de destrucción mediante un workflow cron que corre
terraform destroy -target=module.ekscuando no haya usuarios activos (puedes consultar CloudWatch metricActiveConnectionsdel 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
- Ejecuta
terraform applyy confirma que se crean VPC, subredes y el clúster EKS. - En la consola de AWS, verifica que el LoadBalancer tiene una IP pública y que el certificado ACM está asociado al dominio.
- Accede a
https://sandbox.tu-dominio.comy comprueba que la terminalttydmuestra un prompt de Bash. - Usa
kubectl get pods -n defaultpara asegurarte de que el pod está en estadoRunning. - Revisa la factura de AWS al día siguiente; si el gasto supera el umbral, ajusta
desired_capacityo el cron de destroy.
Notas adicionales
- Persistencia de datos:
ttydno guarda el estado del shell. Si necesitas mantener archivos entre sesiones, monta unPersistentVolumeClaimcon 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
replicasy habilita un Ingress consessionAffinity: 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