EnvoyBrokerLab: laboratorio local para entender Envoy, control planes y aprovisionamiento asíncrono
Hace un tiempo me quedé dando vueltas con una idea: cuando hablamos de plataformas internas, gateways o infraestructura de tráfico, muchas veces nos enfocamos demasiado en el proxy.
Envoy, NGINX, HAProxy o cualquier otro componente de data plane suelen llevarse toda la atención. Pero en una arquitectura real, el proxy es solo una parte del sistema.
Lo interesante está en todo lo que lo rodea:
Intent
-> Desired State
-> Queue
-> Worker
-> Reconciliation
-> Control Plane
-> Data Plane
Con esa idea armé EnvoyBrokerLab, un laboratorio local para entender cómo una plataforma podría aprovisionar rutas HTTP de forma asíncrona y actualizar Envoy sin editar su configuración manualmente.
El proyecto simula una plataforma interna de tráfico. La idea es que los equipos de desarrollo no tengan que configurar directamente componentes de infraestructura. En lugar de eso, consumen una API interna que abstrae el aprovisionamiento y la operación de una capacidad concreta.
En este caso, esa capacidad es el aprovisionamiento de rutas HTTP.
La idea del proyecto
La idea principal es simple:
En vez de editar manualmente la configuración del proxy, un usuario declara una intención mediante una API. La plataforma guarda ese estado deseado, procesa la operación de forma asíncrona y reconcilia la configuración del data plane.
Por ejemplo, un usuario podría pedir una ruta así:
{
"host": "demo.local",
"prefix": "/",
"target": "service-a"
}
Esa request no modifica Envoy directamente.
En cambio, pasa por una serie de componentes que representan una arquitectura más cercana a una plataforma real:
- El Broker API recibe la solicitud.
- La ruta se guarda como estado deseado en PostgreSQL.
- Se publica una operación en RabbitMQ.
- Un Worker consume esa operación.
- El Control Plane lee el estado deseado.
- El Control Plane genera configuración dinámica para Envoy.
- Envoy actualiza su comportamiento.
- El tráfico empieza a llegar al backend correspondiente.
Ese flujo me parece mucho más interesante que simplemente levantar un proxy con una configuración estática.
Nota de inspiración
Este proyecto nació a partir del análisis de una charla técnica de Atlassian sobre arquitectura de plataformas internas para manejo de tráfico, Service Brokers, Envoy y control planes.
TrafficForge no es una implementación oficial ni una réplica exacta de esa arquitectura. Es una maqueta local y simplificada creada con fines educativos para entender los patrones detrás de este tipo de plataformas.
Referencia:
- Atlassian. Charla técnica sobre arquitectura de plataforma, Service Broker y Envoy. YouTube
Arquitectura general
La arquitectura de alto nivel del laboratorio queda así:
┌──────────────────────┐
│ Developer / Consumer │
│ curl / Postman │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Service Broker API │
│ FastAPI │
└──────────┬───────────┘
│
├─────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ PostgreSQL │ │ RabbitMQ │
│ Desired State │ │ Async Operations │
└──────────────────┘ └─────────┬────────┘
│
▼
┌──────────────────┐
│ Worker │
│ Reconciler Job │
└─────────┬────────┘
│
▼
┌──────────────────┐
│ Control Plane │
│ Envoy Config Gen │
└─────────┬────────┘
│
▼
┌──────────────────┐
│ Envoy │
│ Data Plane │
└─────────┬────────┘
│
┌──────────────────┴──────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ service-a │ │ service-b │
└──────────────┘ └──────────────┘
También se puede ver como un flujo más compacto:
Developer / Consumer
|
v
Service Broker API
|
+----------------+
| |
v v
PostgreSQL RabbitMQ
Desired State Async Operations
|
v
Worker
|
v
Control Plane
|
v
Envoy
|
+----------+----------+
v v
service-a service-b
La demo está organizada con componentes separados para:
brokercontrol-planeworkerdbenvoy- backends de prueba
La composición es intencionalmente simple, pero alcanza para mostrar varios conceptos de platform engineering:
- una API de entrada;
- un store de estado deseado;
- una cola de operaciones;
- un worker reconciliador;
- un control plane;
- un data plane;
- servicios backend para validar el routing.
Broker API: recibir intención, no ejecutar todo
El Broker API es el punto de entrada público de la plataforma.
Su responsabilidad principal es transformar una solicitud del usuario en estado de plataforma y en una operación asíncrona.
No configura Envoy directamente.
Esto es importante porque una plataforma real debería evitar acoplar la API expuesta a los usuarios con el mecanismo interno de ejecución.
En este laboratorio, el Broker hace algo más parecido a lo que haría una plataforma interna:
- expone una API de aprovisionamiento de rutas;
- valida el input;
- guarda el estado deseado de la ruta en PostgreSQL;
- crea un registro de operación;
- publica un mensaje de operación en RabbitMQ;
- devuelve una respuesta
acceptedal usuario.
Un ejemplo de request:
curl -X POST http://localhost:8080/routes \
-H "Content-Type: application/json" \
-d '{
"host": "demo.local",
"prefix": "/",
"target": "service-a"
}'
Una respuesta esperada sería algo como esto:
{
"status": "accepted",
"operation_id": 1,
"route_id": 1,
"message": "Operation enqueued"
}
Ese detalle es importante: la operación fue aceptada, no necesariamente completada.
Esto representa un patrón muy común en infraestructura: separar la aceptación de una solicitud de su ejecución real.
PostgreSQL como desired state store
PostgreSQL cumple el rol de state store.
Guarda tanto el estado deseado como el estado de las operaciones.
En vez de depender solamente de requests transitorias, la plataforma guarda qué rutas deberían existir.
Conceptualmente:
routes = lo que debería existir
operations = lo que se está procesando
Esto permite diferenciar dos cosas que suelen mezclarse:
- la intención declarada por el usuario;
- el estado de ejecución de esa intención.
La tabla routes
La tabla routes representa lo que la plataforma quiere que exista.
Por ejemplo:
demo.local / -> service-a
Eso es desired state.
No significa solamente “alguien hizo una request”, sino que el sistema guarda una declaración de estado deseado: para el host demo.local y el prefijo /, el tráfico debería ir hacia service-a.
La tabla operations
La tabla operations representa los cambios que están siendo procesados.
Por ejemplo:
operation_id=1
operation_type=upsert-route
status=completed
Esto da una forma simple de auditoría.
Permite saber qué se pidió, qué operación se creó, en qué estado quedó y si terminó correctamente o falló.
Un ciclo simple de operación podría verse así:
pending -> processing -> completed
O, si algo falla:
pending -> processing -> failed
Este modelo abre la puerta a ideas más interesantes, como:
- reconciliación periódica;
- auditoría;
- reintentos;
- rollback;
- recuperación de operaciones fallidas;
- regeneración de configuración;
- detección de drift.
RabbitMQ para desacoplar la ejecución
RabbitMQ aparece como cola de operaciones asíncronas.
El Broker no llama directamente al Control Plane para modificar Envoy. En su lugar, publica un mensaje y deja que otro componente procese el cambio.
El flujo queda así:
Broker API
-> PostgreSQL
-> RabbitMQ
-> Worker
-> Control Plane
-> Envoy
Esto tiene varias ventajas:
- la API puede responder rápido;
- hay mejor aislamiento ante fallos;
- el Worker puede procesar operaciones a su ritmo;
- se puede escalar el procesamiento;
- se puede manejar backpressure;
- se pueden agregar retries;
- se puede implementar una dead-letter queue;
- se desacopla la capa pública de la capa de ejecución;
- el comportamiento se parece más al de una plataforma cloud real.
Para una demo local, RabbitMQ es práctico porque además permite inspeccionar visualmente las colas desde su UI de management.
En una implementación de cloud provider, este rol podría estar ocupado por otras tecnologías:
- AWS SQS;
- Google Pub/Sub;
- Azure Service Bus;
- Kafka;
- NATS;
- RabbitMQ.
En este laboratorio, RabbitMQ funciona bien porque es simple, visual y fácil de inspeccionar en Docker Compose.
Worker: el puente entre la cola y la reconciliación
El Worker es el componente de ejecución.
Consume los mensajes publicados en RabbitMQ y dispara la reconciliación.
Sus responsabilidades son:
- consumir operaciones pendientes;
- marcar operaciones como
processing; - llamar al Control Plane;
- marcar operaciones como
completedofailed; - actualizar el estado de la ruta cuando corresponde.
El Worker actúa como puente entre la intención asíncrona y la reconciliación real de la plataforma.
En una versión más completa, este componente podría manejar:
- retries con backoff;
- errores transitorios;
- idempotencia;
- métricas;
- trazas distribuidas;
- dead-letter queues;
- control de concurrencia.
Pero incluso en esta versión simple, el Worker ayuda a mostrar una separación importante: la API registra intención; el Worker ejecuta.
Control Plane: traducir intención en configuración
El Control Plane es una de las partes más importantes del proyecto.
Su responsabilidad es traducir el estado deseado en configuración para Envoy.
No recibe requests de aprovisionamiento directamente desde el usuario. En lugar de eso, lee el estado deseado desde PostgreSQL y genera los archivos de configuración dinámica que Envoy necesita para rutear tráfico.
Sus responsabilidades son:
- leer rutas activas;
- generar configuración LDS para Envoy;
- generar configuración CDS para Envoy;
- escribir archivos de configuración dinámica de forma atómica;
- exponer un endpoint de reconciliación.
La traducción conceptual sería esta:
{
"host": "demo.local",
"prefix": "/",
"target": "service-a"
}
se convierte en:
Requests con Host demo.local y path / deben ir al cluster service-a.
En esta implementación, el Control Plane genera archivos dinámicos para Envoy.
Es una forma simple de entender el patrón sin implementar todavía un servidor xDS gRPC completo.
Más adelante, una evolución natural sería reemplazar esa configuración vía filesystem por un verdadero xDS server.
Envoy como Data Plane
Envoy queda como Data Plane.
Esto significa que no decide qué rutas deberían existir. No conoce la lógica de negocio de la plataforma. No procesa operaciones. No guarda estado deseado.
Envoy se encarga de lo que tiene que hacer bien:
- recibir tráfico HTTP;
- escuchar en el puerto
10000; - matchear requests por header
Hosty path prefix; - seleccionar el cluster configurado;
- reenviar la request al backend correcto;
- exponer endpoints de administración en el puerto
9901.
En el laboratorio hay dos servicios simples:
service-a
service-b
Cada uno responde con su nombre, el path de la request y el header Host.
Son intencionalmente simples porque su propósito no es simular una aplicación real, sino validar el comportamiento de routing.
Envoy no decide qué rutas deberían existir. Solo aplica la configuración generada por el Control Plane.
Backend services
Los backend services son servidores HTTP simples.
Su objetivo es validar que Envoy esté enviando el tráfico al destino correcto.
Los backends disponibles son:
service-aservice-b
Una respuesta esperada de uno de estos servicios podría verse así:
{
"service": "service-a",
"path": "/",
"host_header": "demo.local"
}
Esto permite probar rápidamente si la ruta configurada está funcionando como se espera.
Ciclo de vida de una request de aprovisionamiento
El flujo de aprovisionamiento completo se puede ver así:
1. El usuario envía POST /routes al Broker API.
2. El Broker valida la request.
3. El Broker inserta o actualiza la ruta en PostgreSQL.
4. El Broker crea un registro de operación.
5. El Broker publica un mensaje en RabbitMQ.
6. El Worker consume el mensaje.
7. El Worker marca la operación como processing.
8. El Worker llama a Control Plane /reconcile.
9. El Control Plane lee las rutas activas desde PostgreSQL.
10. El Control Plane genera configuración dinámica para Envoy.
11. Envoy detecta la configuración actualizada.
12. El Worker marca la operación como completed.
13. El tráfico empieza a rutear hacia el backend seleccionado.
Lo interesante es que no hay una modificación directa desde la API hacia Envoy.
La API recibe intención. La plataforma persiste estado. La cola desacopla ejecución. El Worker procesa. El Control Plane reconcilia. Envoy aplica.
Flujo de tráfico en runtime
Una vez que la ruta ya fue aprovisionada, el flujo del tráfico HTTP es distinto del flujo de aprovisionamiento.
El runtime traffic flow sería:
1. El cliente envía una request a Envoy.
2. Envoy recibe la request en el puerto 10000.
3. Envoy revisa el header Host.
4. Envoy matchea el prefijo de la ruta.
5. Envoy selecciona el cluster configurado.
6. Envoy reenvía la request al backend service.
7. El backend service responde.
8. Envoy devuelve la respuesta al cliente.
Ejemplo:
curl -H "Host: demo.local" http://localhost:10000/
Resultado esperado si la ruta apunta a service-a:
{
"service": "service-a",
"path": "/",
"host_header": "demo.local"
}
Esta separación entre flujo de aprovisionamiento y flujo de tráfico es importante.
El control plane decide y configura.
El data plane atiende tráfico.
Modelo de desired state
EnvoyBrokerLab usa un modelo de desired state.
El estado deseado se guarda en PostgreSQL. El Control Plane reconcilia ese estado y lo convierte en configuración para Envoy.
Este enfoque se parece al modelo de controllers en Kubernetes.
El sistema no se limita a ejecutar comandos imperativos. Guarda qué debería existir y después intenta mover el sistema real hacia ese estado.
Una forma simple de pensarlo:
Estado deseado:
demo.local / -> service-a
Estado real:
Envoy tiene una ruta cargada hacia service-a
Reconciliación:
comparar, generar config y aplicar
Este patrón permite futuras mejoras como:
- reconciliación periódica;
- detección de drift;
- recuperación de operaciones fallidas;
- regeneración de configuración;
- auditoría de rutas;
- soporte de rollback.
Modelo de aprovisionamiento asíncrono
El proyecto evita intencionalmente el aprovisionamiento síncrono directo.
El Broker API no actualiza Envoy en la misma request.
En lugar de eso:
Broker API
-> PostgreSQL
-> RabbitMQ
-> Worker
-> Control Plane
-> Envoy
Este patrón es más realista para plataformas de infraestructura porque las operaciones reales pueden involucrar:
- APIs cloud;
- retries;
- sistemas distribuidos;
- consistencia eventual;
- fallos parciales;
- múltiples componentes;
- dependencias externas.
El modelo asíncrono permite que la API siga respondiendo rápido mientras la plataforma procesa los cambios por detrás.
La respuesta accepted no significa “ya está aplicado”. Significa “la plataforma aceptó la intención y creó una operación para procesarla”.
Separación entre Control Plane y Data Plane
La distinción entre Control Plane y Data Plane es central en este proyecto.
Control Plane
El Control Plane es responsable de decidir y configurar.
En este laboratorio, incluye:
- Broker API;
- PostgreSQL;
- RabbitMQ;
- Worker;
- Control Plane service.
Es la parte que recibe intención, guarda estado, procesa operaciones y genera configuración.
Data Plane
El Data Plane es responsable de manejar tráfico real.
En este laboratorio, incluye:
- Envoy.
Envoy recibe requests, matchea rutas y reenvía tráfico.
Esta separación permite que el data plane se mantenga enfocado en performance y manejo de tráfico, mientras el control plane se ocupa de estado, políticas y generación de configuración.
Por qué Envoy
Envoy es un proxy L7 moderno usado en networking cloud-native, plataformas de ingress, API gateways y service meshes.
Es útil para este proyecto porque soporta configuración dinámica y permite separar su configuración inicial de bootstrap de su configuración de runtime.
En este laboratorio, Envoy representa el data plane.
El punto importante no es solamente que Envoy pueda proxyear tráfico, sino que puede ser controlado por un control plane externo.
Ese patrón aparece en muchas arquitecturas modernas:
Control Plane
|
v
Envoy
La configuración no tiene por qué editarse manualmente cada vez. Puede ser generada por otro sistema a partir de estado deseado.
Por qué RabbitMQ
RabbitMQ se usa para modelar workflows asíncronos de infraestructura.
En una plataforma real, muchas operaciones no son instantáneas. Pueden fallar, tardar, depender de servicios externos o necesitar reintentos.
RabbitMQ ayuda a representar ese comportamiento sin hacer el laboratorio demasiado complejo.
Sus ventajas en esta demo son:
- desacopla el Broker del Worker;
- permite procesar operaciones de forma asíncrona;
- facilita el manejo futuro de retries;
- abre la puerta a dead-letter queues;
- permite inspeccionar mensajes desde la UI de management;
- funciona muy bien en un entorno local con Docker Compose.
Por qué PostgreSQL
PostgreSQL ofrece un state store simple y familiar.
En una plataforma real, este rol podría ocuparlo otra base o sistema de almacenamiento, por ejemplo:
- DynamoDB;
- PostgreSQL;
- MySQL;
- etcd;
- CockroachDB;
- Cloud Spanner.
En esta demo, PostgreSQL alcanza para representar dos cosas importantes:
- desired state;
- historial o estado de operaciones.
No hace falta algo más sofisticado para entender el patrón.
Probando el flujo localmente
Para levantar todo el entorno:
docker compose up --build
La demo expone varios servicios locales:
| Componente | URL o puerto |
|---|---|
| Broker API | localhost:8080 |
| Control Plane | localhost:8090 |
| Envoy Proxy | localhost:10000 |
| Envoy Admin | localhost:9901 |
| RabbitMQ Management | localhost:15672 |
| service-a | backend interno |
| service-b | backend interno |
Antes de crear una ruta, se puede probar Envoy:
curl -H "Host: demo.local" http://localhost:10000/
La respuesta esperada es algo como:
No route provisioned yet
Después se crea una ruta hacia service-a:
curl -X POST http://localhost:8080/routes \
-H "Content-Type: application/json" \
-d '{
"host": "demo.local",
"prefix": "/",
"target": "service-a"
}'
La respuesta esperada:
{
"status": "accepted",
"operation_id": 1,
"route_id": 1,
"message": "Operation enqueued"
}
Luego se prueba el tráfico a través de Envoy:
curl -H "Host: demo.local" http://localhost:10000/
La respuesta debería indicar que llegó a service-a:
{
"service": "service-a",
"path": "/",
"host_header": "demo.local"
}
Ahora se puede cambiar dinámicamente la ruta hacia service-b:
curl -X POST http://localhost:8080/routes \
-H "Content-Type: application/json" \
-d '{
"host": "demo.local",
"prefix": "/",
"target": "service-b"
}'
Al volver a probar:
curl -H "Host: demo.local" http://localhost:10000/
el tráfico debería llegar ahora a service-b.
Lo interesante es que no se editó manualmente la configuración de Envoy. La ruta cambió a través del flujo de plataforma.
Trade-offs de la arquitectura
Como todo laboratorio, EnvoyBrokerLab tiene decisiones que ayudan a aprender el patrón, pero también limitaciones importantes.
Fortalezas
Algunas fortalezas del enfoque:
- separación clara de responsabilidades;
- buen valor educativo;
- fácil de correr localmente;
- demuestra conceptos reales de platform engineering;
- usa procesamiento asíncrono de operaciones;
- evita acoplar directamente la API con el proxy;
- hace explícito el desired state;
- provee auditabilidad básica mediante registros de operaciones;
- permite entender el rol de cada componente sin depender de Kubernetes.
Debilidades
También hay varias cosas que no están cubiertas:
- no hay autenticación;
- no hay autorización;
- no hay RBAC;
- no hay TLS;
- no hay un servidor xDS gRPC real;
- no hay retries;
- no hay dead-letter queue;
- no hay validación avanzada de configuración antes de aplicar;
- no hay rollback;
- no hay stack de observabilidad;
- hay una sola instancia de Envoy;
- no está pensado para producción.
Y está bien que sea así.
El objetivo no es construir una plataforma productiva, sino bajar a tierra los conceptos principales.
Lo que me interesaba entender
Este lab no busca ser productivo.
No tiene autenticación, autorización, TLS, RBAC, validación avanzada de configuración, rollback, observabilidad completa ni un xDS server real.
El objetivo era ordenar algunos conceptos que aparecen mucho cuando se habla de platform engineering, service meshes, gateways o infraestructura cloud:
- qué significa separar control plane y data plane;
- por qué conviene guardar desired state;
- cómo una queue ayuda a desacoplar operaciones;
- qué rol cumple un Worker;
- cómo una API puede representar intención y no ejecución directa;
- por qué el proxy no es toda la plataforma;
- cómo una configuración dinámica puede generarse desde estado persistido.
La principal idea que me llevo es esta:
Una plataforma de tráfico no es solamente el proxy. Es el sistema que recibe intención, guarda estado, procesa cambios, reconcilia configuración y recién después actualiza el data plane.
Camino de evolución
Una evolución natural del proyecto podría verse así:
v0.1 - Configuración dinámica local basada en filesystem
v0.2 - Agregar dead-letter queue
v0.3 - Agregar idempotency keys
v0.4 - Agregar métricas con Prometheus
v0.5 - Agregar trazas con OpenTelemetry
v0.6 - Agregar un xDS gRPC server real
v0.7 - Agregar múltiples instancias de Envoy
v0.8 - Agregar TLS y rate limiting
v0.9 - Agregar manifests de Kubernetes
v1.0 - Agregar integración con Gateway API
De todas esas mejoras, las más interesantes para una próxima iteración serían observabilidad y xDS.
Observabilidad porque ayudaría a ver el recorrido completo de una operación:
POST /routes
-> DB insert
-> RabbitMQ publish
-> Worker consume
-> Control Plane reconcile
-> Envoy config update
Y xDS porque acercaría el proyecto a una arquitectura más realista de control plane para Envoy.
Posibles mejoras
Más allá del roadmap, hay varias mejoras concretas que se podrían sumar:
- agregar una dead-letter queue;
- implementar retries con exponential backoff;
- agregar idempotency keys;
- sumar autenticación;
- sumar autorización;
- agregar RBAC;
- validar la configuración antes de aplicarla;
- mantener versiones anteriores para rollback;
- exponer métricas con Prometheus;
- agregar tracing con OpenTelemetry;
- implementar un xDS gRPC server real;
- desplegarlo en Kubernetes;
- integrarlo con Gateway API;
- explorar un flujo GitOps con Argo CD;
- soportar múltiples instancias de Envoy;
- agregar TLS;
- agregar rate limiting;
- mejorar la auditoría de operaciones;
- detectar drift entre estado deseado y estado real.
Cierre
EnvoyBrokerLab me sirvió como excusa para ordenar varios conceptos que a veces se estudian por separado:
- APIs internas;
- colas;
- estado deseado;
- workers;
- reconciliación;
- control planes;
- data planes;
- configuración dinámica de proxies;
- flujos asíncronos de infraestructura.
Vistos juntos, empiezan a formar una idea mucho más clara de cómo puede construirse una plataforma interna para manejar infraestructura de tráfico.
No es una implementación productiva, ni pretende serlo.
Es un laboratorio para entender el patrón.
El aprendizaje principal es simple:
El proxy es solo una parte de la arquitectura. El verdadero valor está en el control plane que gobierna cómo se configura ese proxy.
Resumen rápido
- EnvoyBrokerLab simula una plataforma interna de tráfico HTTP.
- El usuario declara intención mediante una API, no editando Envoy manualmente.
- PostgreSQL guarda el desired state y el estado de operaciones.
- RabbitMQ desacopla la recepción de requests de la ejecución real.
- El Worker consume operaciones y dispara reconciliación.
- El Control Plane traduce estado deseado en configuración dinámica para Envoy.
- Envoy funciona como Data Plane y solo se ocupa de manejar tráfico.
- La arquitectura permite entender patrones usados en platform engineering, gateways y service meshes.
- El proyecto no es productivo: es un laboratorio para estudiar separación de responsabilidades, reconciliación y aprovisionamiento asíncrono.