Saltar al contenido

EnvoyBrokerLab: laboratorio local para entender Envoy, control planes y aprovisionamiento asíncrono

Augusto Mancuso
Published date:

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:

  1. El Broker API recibe la solicitud.
  2. La ruta se guarda como estado deseado en PostgreSQL.
  3. Se publica una operación en RabbitMQ.
  4. Un Worker consume esa operación.
  5. El Control Plane lee el estado deseado.
  6. El Control Plane genera configuración dinámica para Envoy.
  7. Envoy actualiza su comportamiento.
  8. 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:


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:

La composición es intencionalmente simple, pero alcanza para mostrar varios conceptos de platform engineering:


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:

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 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:


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:

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:

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:

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:

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:

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:

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:

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:


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:

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:

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 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:


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:

En esta demo, PostgreSQL alcanza para representar dos cosas importantes:

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:

ComponenteURL o puerto
Broker APIlocalhost:8080
Control Planelocalhost:8090
Envoy Proxylocalhost:10000
Envoy Adminlocalhost:9901
RabbitMQ Managementlocalhost:15672
service-abackend interno
service-bbackend 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:

Debilidades

También hay varias cosas que no están cubiertas:

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:

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:


Cierre

EnvoyBrokerLab me sirvió como excusa para ordenar varios conceptos que a veces se estudian por separado:

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

Next
Kubernetes desde cero: componentes, nodos e interfaces principales