Problema

Los sistemas de orquestación de flujos (Airflow, Prefect, etc.) suelen requerir que cada worker tenga acceso directo a la base de datos central para leer el estado de los jobs y actualizar sus resultados. En entornos con nodos en la periferia –por ejemplo, un Raspberry Pi en el garaje, una VM detrás de NAT en la casa de los padres o un cluster de GPU aislado en una red de alta seguridad– esa arquitectura obliga a abrir puertos, crear VPNs o exponer credenciales de base de datos. Cada vez que el acceso a la DB falla, los workers se quedan colgados, pierden tareas o, en el peor de los casos, se pierde la trazabilidad completa del flujo. El síntoma típico es “el worker no puede conectarse a Postgres” o “el scheduler se queda sin heartbeat”. El reto es mantener la flexibilidad de ejecutar tareas en contenedores, procesos o pods sin comprometer la seguridad ni la disponibilidad del backend.

Causa

  1. Conexión directa a la base de datos
    Los workers tradicionales usan JDBC/ODBC para consultar el estado. Cuando el nodo está detrás de NAT o sin ruta directa, la única solución es exponer la DB mediante VPN o reglas de firewall, lo que aumenta la superficie de ataque.

  2. Acoplamiento de cola y repositorio
    En versiones anteriores de algunos motores, la cola de ejecución y el repositorio de metadatos estaban ligados a la misma tecnología (por ejemplo, Kafka + Elasticsearch). Cambiar solo la cola implicaba reescribir gran parte del código, generando bugs duplicados.

  3. Estado persistente en el worker
    Al guardar credenciales y estado localmente, cualquier robo de hardware expone información sensible y dificulta la rotación de claves.

  4. Escalado rígido
    Añadir un nuevo nodo implica replicar la configuración de la base de datos, lo que complica el despliegue en clusters heterogéneos (ARM vs x86) o en entornos de bajo consumo.

Solución

Separar el plano de control del plano de ejecución. Un controller (o “control plane”) centraliza la base de datos y la cola, mientras que los workers son completamente stateless y se comunican con el controller mediante una única conexión saliente gRPC protegida con mTLS. Este patrón elimina la necesidad de abrir puertos inbound, permite que los workers vivan detrás de NAT y reduce la carga de gestión de credenciales.

Pasos generales:

  1. Desplegar el controller

    • Usa Docker Compose o Helm para lanzar una instancia con Postgres (o MySQL) y la cola elegida (por ejemplo, RabbitMQ o una tabla de tareas en la BD).
    • Habilita la generación automática de certificados mTLS (Kestra incluye scripts para crear una CA interna).
  2. Configurar los workers

    • En cada nodo, instala únicamente el runtime de Kestra (no la base de datos).
    • Apunta la variable KESTRA_CONTROLLER_URL al endpoint del controller y monta los certificados generados.
    • Selecciona el runner que necesites: Docker socket, proceso local o pod de Kubernetes, mediante la clave runner.type en el YAML del flujo.
  3. Definir flujos en YAML

    • Mantén los archivos de flujo bajo control de versiones (Git).
    • Usa trigger para cron, webhook, MQTT o Kafka según convenga; los triggers se evalúan en el controller, por lo que la latencia es mínima.
  4. Aislar plugins

    • Configura KESTRA_PLUGINS_AUTO_INSTALL_ENABLED=true para que el controller descargue plugins bajo demanda.
    • En entornos con ancho de banda limitado, pre‑descarga los plugins críticos y desactiva la auto‑instalación.
  5. Implementar cuotas y políticas de seguridad

    • Define quota.maxExecutions por namespace para evitar que un flujo mal configurado consuma todos los recursos.
    • Usa credential.store para que las credenciales se almacenen en el controller y nunca viajen al worker.

Este enfoque funciona tanto en despliegues monolíticos (Docker run simple) como en clusters Kubernetes, y permite mezclar arquitecturas ARM y x86 sin modificar los flujos.

Cuándo aplicar esta solución

  • Entornos con nodos aislados: homelabs, sucursales, dispositivos IoT, o cualquier worker detrás de NAT.
  • Requerimientos de seguridad estricta: cuando no se pueden exponer puertos de base de datos ni mantener credenciales en los workers.
  • Escalado horizontal frecuente: cuando se añaden o retiran workers dinámicamente y se necesita que el proceso sea “plug‑and‑play”.
  • Arquitecturas heterogéneas: mezclas de ARM y x86, o despliegues mixtos Docker/Kubernetes.

No es necesario si todos los workers están en la misma red privada y la exposición de la base de datos no representa riesgo; en ese caso la arquitectura monolítica puede ser más simple.

Código

# 1️⃣ Controller con Docker Compose (Postgres + Kestra)
cat > docker-compose.yml <<'EOF'
version: "3.8"
services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: kestra
      POSTGRES_PASSWORD: kestra_pass
      POSTGRES_DB: kestra
    volumes:
      - pg_data:/var/lib/postgresql/data

  kestra-controller:
    image: kestra/kestra:2.0-lts
    command: server controller
    environment:
      KESTRA_DATABASE_URL: jdbc:postgresql://postgres:5432/kestra
      KESTRA_DATABASE_USERNAME: kestra
      KESTRA_DATABASE_PASSWORD: kestra_pass
      KESTRA_CONTROLLER_GRPC_TLS_ENABLED: "true"
      KESTRA_CONTROLLER_GRPC_TLS_CA_CERT: /certs/ca.crt
      KESTRA_CONTROLLER_GRPC_TLS_CERT: /certs/controller.crt
      KESTRA_CONTROLLER_GRPC_TLS_KEY: /certs/controller.key
    volumes:
      - ./certs:/certs:ro
    ports:
      - "8080:8080"
      - "50051:50051"   # gRPC
volumes:
  pg_data:
EOF

docker compose up -d

# 2️⃣ Worker (Docker socket runner) en cualquier host
docker run -d --name kestra-worker \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v ./certs:/certs:ro \
  -e KESTRA_CONTROLLER_URL=grpc://<controller_ip>:50051 \
  -e KESTRA_CONTROLLER_GRPC_TLS_CA_CERT=/certs/ca.crt \
  -e KESTRA_CONTROLLER_GRPC_TLS_CERT=/certs/worker.crt \
  -e KESTRA_CONTROLLER_GRPC_TLS_KEY=/certs/worker.key \
  kestra/kestra:2.0-lts server worker

Verificación

  1. Accede a http://<controller_ip>:8080 y verifica que el UI muestra el nodo controller activo.
  2. En la UI, crea un flujo sencillo con un script que imprima date.
  3. Ejecuta el flujo manualmente; la ejecución debe aparecer en la lista de workers y el contenedor Docker del worker debe iniciar y terminar sin errores.
  4. Revisa los logs del worker (docker logs kestra-worker) para confirmar que la conexión gRPC se estableció con éxito y que no aparecen mensajes de “failed to connect to database”.
  5. Simula una caída de red del worker (desconecta la interfaz) y observa que el controller marca la ejecución como “lost” y la re‑intenta cuando el worker vuelve a conectarse.

Notas adicionales

  • Rotación de certificados: Kestra permite recargar los certificados sin reiniciar el proceso; basta con reemplazar los archivos bajo /certs y enviar SIGHUP al proceso.
  • Persistencia de plugins: si el worker está en un nodo con espacio limitado, monta un volumen compartido (/app/plugins) para que los plugins descargados se reutilicen entre reinicios.
  • Escalado en Kubernetes: usa el chart oficial y habilita controller.grpc.tls.enabled=true. El Deployment del worker puede escalar con replicas y el Service ClusterIP mantiene la comunicación outbound.
  • Monitoreo: expón los endpoints /metrics de Prometheus tanto en controller como en workers; la métrica kestra_worker_connected ayuda a detectar workers desconectados.
  • Backup de la BD: programa un pg_dump periódico y almacénalo en S3/MinIO; la restauración es tan simple como volver a levantar el contenedor Postgres con el dump.

Con esta arquitectura, los flujos de trabajo quedan aislados del entorno de red, los workers pueden desplegarse en cualquier hardware y la gestión de credenciales se centraliza, reduciendo la superficie de ataque y simplificando el escalado.