Problema
En entornos de desarrollo y CI/CD que dependen de servicios AWS, es frecuente encontrarse con tres limitaciones:
- Costos – Ejecutar pruebas contra la nube real implica cargos por uso, lo que escala rápidamente con la cantidad de builds.
- Velocidad – Los recursos remotos añaden latencia; los pipelines se alargan y el feedback para los desarrolladores se retrasa.
- Cobertura – Herramientas gratuitas o de comunidad a menudo no implementan el conjunto completo de APIs, lo que obliga a escribir pruebas que sólo cubren un subconjunto de la funcionalidad real.
El patrón que se repite es la necesidad de un entorno AWS local que sea rápido, completo y sin costes ocultos, capaz de integrarse tanto en máquinas locales como en clústers Kubernetes usados por los pipelines. Cuando la solución no cumple con alguno de esos requisitos, los equipos terminan combinando varios emuladores, scripts de arranque y contenedores Docker, lo que genera complejidad y puntos de falla.
Causa
Las causas más habituales de este dolor de cabeza son:
- Dependencia de Docker‑in‑Docker: muchos emuladores se entregan como imágenes que requieren otro contenedor para ejecutarse, lo que complica la configuración del runner y aumenta el tiempo de arranque.
- Estado persistente no controlado: sin una forma de snapshot o restaurar un estado base, cada suite de pruebas debe recrear recursos (buckets, tablas, colas) desde cero, lo que consume minutos en cada ejecución.
- Falta de integración nativa con Kubernetes: cuando el pipeline ya corre en un clúster, lanzar un emulador como proceso dentro del pod del runner implica gestionar puertos, variables de entorno y, a veces, permisos de red.
- Limitaciones de la capa de emulación: versiones comunitarias de emuladores pueden no soportar servicios críticos como Lambda, EMR o Step Functions, obligando a usar la versión paga o a omitir pruebas.
Solución
Una arquitectura basada en un binario estático que expone todas las APIs de AWS compatibles y que pueda ejecutarse tanto en Docker como directamente en un pod de Kubernetes elimina la mayor parte de la fricción. El flujo típico es:
- Descarga del binario – Un único archivo ejecutable, sin dependencias externas (Python, Java, etc.).
- Modo efímero – Cada ejecución del pipeline arranca el emulador en modo
--ephemeral, garantizando un entorno limpio sin datos residuales. - Snapshots nombrados – Antes de la ejecución, se importa un snapshot pre‑seeded que contiene la infraestructura mínima (buckets, tablas, roles). Al finalizar, el emulador puede revertir a ese snapshot sin reiniciar el proceso.
- Despliegue Kubernetes‑native – El binario se empaqueta como una imagen de contenedor ligera y se lanza como un pod. En modo Lambda, cada función se ejecuta como un pod separado, lo que permite usar recursos del clúster (CPU, memoria) y aprovechar la política de autoscaling.
- Observabilidad integrada – Métricas Prometheus en
/metricsy logs estructurados en JSON facilitan la monitorización y el diagnóstico dentro del mismo pipeline.
Alternativas prácticas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Binario único + Docker | Muy fácil de integrar en runners que ya usan Docker. | Requiere Docker‑in‑Docker si el runner no permite montar contenedores. |
| Binario como sidecar en pod | Aprovecha la red del clúster, sin necesidad de DIND. | Necesita permisos de creación de pods (RBAC) y configuración de ServiceAccount. |
| Emulador basado en Python (Moto/LocalStack Community) | Amplia comunidad, fácil de extender. | No cubre todos los servicios, a veces lento, y depende de un intérprete. |
Cuándo aplicar esta solución
Aplica cuando:
- El pipeline se ejecuta en un runner con acceso a Docker o a un clúster Kubernetes.
- Necesitas probar más de la mitad de los servicios AWS (S3, DynamoDB, Lambda, SQS, etc.) sin incurrir en costos.
- Los tiempos de build son críticos y cada minuto de latencia impacta la entrega continua.
- Quieres reproducir entornos idénticos entre desarrolladores locales y CI sin mantener múltiples configuraciones.
No aplica si:
- Solo necesitas validar llamadas a un único servicio y la sobrecarga de un emulador completo no justifica su mantenimiento.
- El runner está estrictamente aislado sin capacidad de ejecutar contenedores o binarios externos (por ejemplo, entornos serverless de terceros).
- Requieres certificación de conformidad con la API de AWS que sólo la nube real garantiza.
Código
# Docker (modo efímero)
docker run --rm -p 4566:4566 \
-e JAISECLOUD_MODE=ephemeral \
rjaisval/jaiscloud-aws:latest
# Kubernetes (pod único)
kubectl run jaiscloud \
--image=rjaisval/jaiscloud-aws:latest \
--port=4566 \
--env="JAISECLOUD_MODE=ephemeral"
# Importar un snapshot llamado "baseline"
jaiscloud-cli snapshot import baseline
Verificación
- Conexión básica – Desde la máquina del runner, ejecuta
aws s3 ls --endpoint-url http://localhost:4566. Debería devolver la lista de buckets del snapshot. - Lambda – Despliega una función sencilla con
aws lambda create-function --function-name test --runtime python3.9 --handler handler.handler --zip-file fileb://func.zip --endpoint-url http://localhost:4566. Invócala y verifica que el log aparece en la salida JSON del contenedor. - Métricas – Accede a
http://localhost:4566/metricsy busca métricas comojaiscloud_requests_total. Un contador creciente indica que el emulador está procesando peticiones. - Snapshot revert – Después de crear recursos en la prueba, ejecuta
jaiscloud-cli snapshot revert baseline. Vuelve a listar los buckets; solo deben aparecer los definidos en el snapshot original.
Notas adicionales
- Persistencia de snapshots: guarda los archivos de snapshot en un bucket S3 interno o en un PVC del clúster para que estén disponibles entre builds.
- RBAC en Kubernetes: crea un
ServiceAccountcon permisocreate podssi vas a usar el modo Lambda que lanza pods por función. - Límites de recursos: aunque el emulador es ligero, asigna al menos 512 MiB de RAM y 0.5 CPU al pod para evitar cuellos de botella en pruebas de carga.
- Actualizaciones: al ser un binario estático, la actualización consiste en reemplazar la imagen del contenedor. Usa una política de rollout para evitar interrupciones en builds concurrentes.