TPI · Tema 01 · Integración de microservicios

Microservicios aislados, plataforma compartida

Cómo llevar el stack completo —13 micros + gateway, hoy en un mismo compose con redes Docker— a una mesh Tailscale donde cada micro corre aislado, en su propia máquina, y sigue siendo parte del sistema: se registra en Eureka, el gateway le enruta y recibe identidad propagada sin publicar un solo puerto.

transporte WireGuard L3 direcciones 100.64.0.0/10 nombres MagicDNS patrón sidecar + network_mode micro desafiospracticos-service puertos publicados 0
02

El diagrama objetivo: hoy, todo en redes Docker

Un solo compose, una sola máquina: el borde en edge_net/gateway_net, la infraestructura compartida y los 13 micros en backend_net, y una red por grupo que solo conecta a cada micro con su propia BD.

edge_net (bridge) gateway_net (bridge interna) backend_net (bridge interna) el gateway también está en backend_net Cliente final fuera del host Página web Angular · tu máquina nginx web :80 / :443 nginx reverse proxy :8081 api-gateway :8000 · enruta /api/* proxy_pass 1º registry (Redis) :6379 fallback → Eureka :8761 Eureka registro y heartbeat · :8761 registry (Redis) consulta primaria del gateway · :6379 MinIO S3 interno · :9000 Kafka asíncrono · :9092 los 13 se registran · heartbeat usuarios_net usuarios :3007 usuarios_db PostgreSQL · 5432 auth_net auth :3013 auth_db PostgreSQL · 5432 cursos_net cursos :3001 cursos_db PostgreSQL · 5432 roadmap_net roadmap :3010 roadmap_db PostgreSQL · 5432 teorico_net desafio-teorico :3004 teorico_db PostgreSQL · 5432 practico_net desafio-practico :3005 practico_db PostgreSQL · 5432 sandbox_net sandbox :3008 sandbox_jobs Redis · 6379 motor_net motor-desafios :3009 motor_db PostgreSQL · 5432 llm_net llm :3003 llm_db PostgreSQL · 5432 chat_net notificaciones :3002 chat_store MongoDB · 27017 mercado_net mercado :3011 mercado_db PostgreSQL · 5432 banco_net banco :3012 banco_db PostgreSQL · 5432 backoffice_net backoffice :3006 backoffice_db ClickHouse · 8123
borde: cliente → nginx → gateway, única puerta real descubrimiento: gateway → registry Redis (1º) · fallback → Eureka async: sandbox → MinIO · sandbox → Kafka (ejecucion.terminada) → notificaciones red por grupo: micro ↔ su BD

Membresías dobles: nginx RP en edge_net + gateway_net · gateway en gateway_net + backend_net. El gateway no consulta MinIO: descubre por el registry (Redis) y cae a Eureka. Ninguna BD sale de su red de grupo.

03

El problema: una red compartida por todos

La topología Docker resolvió el borde y el descubrimiento, pero ata el sistema a una máquina: compartir red es compartir host.

01 · Escala

No cruza máquinas

Los bridges Docker son locales al host. Sumar un equipo significa darle acceso a la misma máquina o armar túneles a mano.

Un solo compose es también un solo punto de despliegue y de falla.

02 · Blast radius

Los micros se ven entre sí

backend_net es una red común: cualquier contenedor alcanza a los otros trece por nombre o por IP.

Un micro comprometido observa el resto del stack.

03 · Identidad

ports: publicados = spoofeables

Si el puerto de un micro queda expuesto, cualquiera llega directo e inventa los headers X-*.

El borde único deja de ser único.

host A · un compose nginx + gateway Eureka + Kafka MinIO los 13 micros + sus BDs bridge backend_net: solo existe dentro de este host IPs 172.x, no ruteables desde afuera host B · otro equipo su micro en su compose su propia BD su propio bridge, su propio rango 172.x nadie ve al otro: no hay red en común sin ruta no hay red L3 común entre hosts
La causa de fondo

El contrato del sistema asume que el gateway es la única puerta: ningún puerto de micro sale del host. Las redes Docker lo cumplen dentro de una máquina; entre equipos, no existe.

04

La idea: cambiar el transporte, no la arquitectura

Mismo sistema, otra red.

redes Docker locales · L2, un host, IPs 172.x tailnet Tailscale · L3 WireGuard entre nodos, IPs 100.x + MagicDNS
Cliente final nginx web api-gateway lb://desafiospracticos-service micro · :8086 :443 /api/* Eureka tailnet 100.x
Eureka

Mismo rol: registro

Sigue siendo el catálogo de quién está vivo y en qué dirección. Solo cambia la dirección anunciada: la IP del tailnet, nunca la 172.x del bridge.

API Gateway

Misma puerta única

Valida identidad y rutea /api/* por lb://<serviceId>. Necesita ruta de salida al tailnet: sidecar propio o Tailscale a nivel host.

Identidad

Mismos contratos

Headers X-*, allowlist, catálogo de scopes y JWKS siempre por el gateway. Nada de esto cambia con la mesh.

En una línea

Lo único que cambia es cómo viaja el paquete: de un bridge local a una red privada punto a punto entre máquinas distintas.

05

El patrón sidecar: el micro hereda la red

El micro no habla Tailscale: comparte el namespace de red de un sidecar que sí lo habla. Nada de ports: publicados — el único camino de entrada es la tailnet.

su propio compose · sin ports publicados sidecar Tailscale · modo kernel TS_USERSPACE=false · /dev/net/tun · NET_ADMIN + NET_RAW tailscale0 · 100.x.y.z · nombre MagicDNS network_mode: "service:<sidecar>" desafiospracticos-service :8086 app · :8087 management · no valida JWT hereda IP 100.x · resolv.conf → 100.100.100.100 su base de datos (privada) queda dentro del compose · nunca sale a la tailnet nodos de la tailnet · tag:plataforma api-gateway :8080 lb://desafiospracticos-service · resuelve por Eureka Eureka :8761 registra la IP 100.x · el gateway la resuelve Kafka :9092 async MySQL :3306 datos de plataforma TCP :8086 por la tailnet registro + heartbeat sin puertos publicados: a :8086 solo se llega por la tailnet
tráfico de negocio por el gateway registro en el service registry frontera del compose: BD privada, micro alcanzable solo por la mesh
Por qué modo kernel y no userspace

En userspace Tailscale no crea interfaz: expone un proxy SOCKS que solo sirve al sidecar, así que compartir el namespace no daría conectividad. El modo kernel exige /dev/net/tun y las capabilities NET_ADMIN + NET_RAW.

06

ANTES vs DESPUÉS: misma plataforma, otra topología

Los roles no se mueven: se mueve la red. Lo que era un compose con redes compartidas pasa a ser un conjunto de nodos que se ven por la tailnet.

ANTES · redes Docker un solo compose · una sola máquina nginx web nginx RP api-gateway Eureka Kafka MinIO infra compartida: registro, bus y storage sobre la misma red interna backend_net · red compartida: cada micro ve a los otros arriba micro · abajo su BD 13 micros, cada uno con su BD — pero todos en el mismo cable el gateway llega por red interna · los puertos publicados son spoofeables un host, un punto de despliegue y de falla VS DESPUÉS · tailnet Tailscale cada micro en su compose · la plataforma como nodos de la tailnet plataforma · api-gateway + Eureka + Kafka + MySQL tag:plataforma · accesibles solo dentro del tailnet TAILNET WireGuard · MagicDNS equipo A compose + sidecar su BD privada equipo B compose + sidecar su BD privada desafiospracticos-service 100.x · :8086 · sin ports en su propio compose con sidecar tu próximo micro mismo patrón compose + sidecar + allowlist nadie comparte red Docker · las BDs no salen a la tailnet
1 · Transporte

de bridge Docker local a tailnet L3

2 · Dirección

de IP 172.x a IP 100.x por MagicDNS

3 · Aislamiento

de un compose para todo a uno por equipo

4 · Borde

el gateway suma ruta al tailnet

El único cambio de rol: el gateway necesita ruta de salida al tailnet para iniciar conexiones hacia 100.64.0.0/10.

07

El patrón repetido: cada equipo con su compose

No hay una red que compartir ni una máquina que ceder: cada equipo conecta su micro con su propio sidecar y todos se encuentran en la tailnet.

TAILNET WireGuard · MagicDNS 100.64.0.0/10 plataforma · api-gateway + Eureka + Kafka + MySQL · tag:plataforma equipo A su compose + sidecar su micro y su BD equipo B su compose + sidecar su micro y su BD desafiospracticos-service 100.x · :8086 · sin ports serviceId en la allowlist + scope · ya integrado tu próximo micro mismo patrón, mismo gateway no hace falta tocar la red de nadie todos se ven por IP o nombre del tailnet · nadie comparte red Docker
Sumar un equipo

Un serviceId único terminado en -service, su sidecar y su compose. Sin tocar la infraestructura de los demás.

Lo que hay que pedir

Alta en la allowlist del gateway y, si usa scopes propios, alta en el catálogo y credencial de servicio.

Lo que no se comparte

Red, base de datos, volúmenes, CI ni credenciales: cada equipo despliega cuando quiere, en su máquina.

08

Qué se comparte y qué no

Compartir la plataforma no es compartir la infraestructura: lo común son los contratos, y eso es lo único que hay que negociar entre equipos.

Se comparte · contratos

Lo que hay que negociar

  • Registro Eureka: serviceId, estado y dirección alcanzable.
  • Allowlist del gateway: sin alta, todo da 404 por diseño.
  • Catálogo de scopes y credencial de servicio.
  • Headers X-* y el prefijo /api/<segmento>.
  • ACLs del tailnet: quién habla con quién y por qué puerto.
No se comparte · infraestructura

Lo que cada equipo mantiene

  • Red Docker: ya no hay bridge común entre equipos.
  • Bases de datos y volúmenes: privados de cada compose.
  • CI/CD y ritmo de despliegue.
  • Credenciales y secretos: por entorno, nunca al repo.
  • Operación del sidecar: auth key efímera y rotación.
La prueba del contrato

Sumar un micro solo toca allowlist, scopes y ACLs. Si hiciera falta tocar la red de otro equipo, no sería un contrato: sería acoplamiento.

1 · serviceId

único y terminado en -service

2 · Allowlist

alta en el gateway: sin esto, 404

3 · Scopes

catálogo + credencial de servicio

4 · Registro

Eureka anunciando la IP 100.x

5 · ACL

deny por defecto

09

Seguridad del tailnet: grants con deny por defecto

La tailnet por defecto deja pasar todo: sin reglas, “sin ports:” no protege. Se usan ACLs de Tailscale — la key del array es grants, no acls — con tags por rol y reglas explícitas: los micros solo salen al Gateway y a Kafka.

tag:plataforma api-gateway :8080 única puerta: valida e inyecta identidad entra al micro solo por el puerto de app Eureka :8761 fallback de descubrimiento del gateway los micros no lo consultan directo tag:kafka Kafka :9092 eventos asíncronos accept :8080 accept :8086 accept :9092 tag:microservicio desafiospracticos-service :8086 app · :8087 management solo sale al gateway y a Kafka users-service :8082 jamás expuesto ni forwardeado el JWKS va por el gateway micro ↔ micro: bloqueado por deny-by-default cualquier nodo sin tag: deny por defecto validado con tests automáticos dentro del mismo policy file
{
  "tagOwners": {
    "tag:plataforma":    ["autogroup:admin"],
    "tag:microservicio": ["autogroup:admin"],
    "tag:kafka":         ["autogroup:admin"]
  },
  "grants": [ // deny-by-default: solo estas reglas
    { "src": ["tag:microservicio"], "dst": ["tag:plataforma"],
      "ip": ["tcp:8080"] },
    { "src": ["tag:microservicio"], "dst": ["tag:kafka"],
      "ip": ["tcp:9092"] },
    { "src": ["tag:plataforma"], "dst": ["tag:microservicio"],
      "ip": ["tcp:8086"] }
  ],
  "tests": [ // micro ↔ micro debe quedar denegado
    { "src": "tag:microservicio",
      "accept": ["tag:plataforma:8080", "tag:kafka:9092"],
      "deny":   ["tag:microservicio:8086"] }
  ]
}
Deny-by-default, no confianza en el código

El bloqueo micro ↔ micro ocurre a nivel de red (WireGuard): no depende de que el micro respete la regla.

JWKS: /.well-known/jwks.json siempre por el gateway; nunca forward directo a :8082 · tests en el mismo policy file.

10

Verificación real: desafiospracticos-service

Lo observado sobre el servicio ya integrado: registro, ruta pública por el gateway, diagnóstico, canario de firma y ACL del tailnet activa.

PruebaEsperadoObtenido
Registro en Eureka UP anunciando la IP del tailnet UP · IP 100.x (no 172.x)
GET /api/desafiospracticos/public/ping 200 con X-Request-Id 200 · header presente
GET /api/desafiospracticos/diagnostico gateway 200 · registro UP {gatewayAlcanzable: 200, eurekaRegistrado: UP}
JWKS por el gateway keys=1 · kids=[dev] keys=1 · kids=[dev]
ACL del tailnet deny por defecto ACTIVA · deny-by-default validado
100.xIP anunciada en Eureka — nunca la 172.x del bridge
200GET /api/desafiospracticos/public/ping con X-Request-Id
200 / UPdiagnostico: gatewayAlcanzable / eurekaRegistrado
keys=1JWKS por el gateway · kids=[dev]
ACL del tailnet: activa

Ya no es un pendiente: el tailnet opera con deny por defecto. La verificación negativa (nc -z <ip-micro> 8086 desde otro nodo) quedó bloqueada a nivel de red.

Regla de oro para diagnosticar

404 falta allowlist · 401 sin identidad o sesión vencida · 503 el gateway no llega a ninguna instancia (es red, no registro) · 403 invalid-audience el token es para otro destino.

Registro, ruteo e identidad funcionando de punta a punta. La ACL del tailnet está activa y validada: deny por defecto en toda la red.

11

Cierre: el gateway rutea a un micro sin puertos

El gateway rutea a un micro que no publica puertos.

Qué se logró

La integración, de punta a punta

  • desafiospracticos-service registrado UP anunciando IP 100.x.
  • Ruta pública por el gateway, con X-Request-Id de ida y vuelta.
  • Diagnóstico en verde: {gatewayAlcanzable: 200, eurekaRegistrado: UP}.
  • JWKS por el gateway: keys=1 · kids=[dev].
  • Cero puertos publicados: el micro solo vive en la tailnet.
Para sumarte

Tres cosas, en este orden

  1. serviceId en la allowlist

    Único y terminado en -service. Sin esto: 404 por diseño.

  2. Scope en el catálogo

    Si consumís o exponés scopes propios: alta en el catálogo y credencial de servicio.

  3. Tu propio sidecar

    Compose con sidecar Tailscale, sin ports:, registrando la IP 100.x.

ACL del tailnet: activa

La mesh ya opera con deny por defecto: solo tag:plataforma entra al puerto de app de los micros.

Automatizado: skill de conexión a Tailscale

Sumar un micro a la tailnet quedó empaquetado como skill del agente (sidecar kernel + auth key efímera + registro con IP 100.x): de 0 a 100 en el primer intento, sin fallos.