Project Tye (simplemente Tye de ahora en adelante) es un proyecto experimental del equipo de NetCore pensado para ayudarnos en el desarrollo de apliaciones de (micro)servicios basadas en NetCore. Aunque podríamos llegar a usar Tye sin netcore, no es para lo que está concebida: NO es una herramienta de propósito general. Es una herramienta pensada para desarrolladores en netcore.
Funcionalidades de Tye
Es complicado definir qué es exactamente Tye, ya que anda a medio camino entre un orquestador básico, un ejecutor de aplicaciones, un control plane básico y un gestor de despliegues. Quizá sea más sencillo poner una lista de funcionalidades de Tye:
- Ejecuta con un solo comando (
tye run
) tu aplicación entera de microservicios, incluyendo las dependencias. - Ofrece un pequeño portal des de el cual se puede ver los servicios ejecutándose y sus logs
- Reinicia automáticamente los servicios que den error
- Ofrece un sistema de service discovery
- Mantiene 1 o más réplicas de cada servicio
- Colecciona métricas de los servicios
- Es capaz de generar los manifiestos de Kubernetes necesarios y desplegarlos en cualquier clúster que tengas.
- Tiene algunas integraciones con otros productos (p. ej. con Dapr)
Analicemos todos esos puntos, empezando por el primero…
Ejecutar tu aplicación con un solo comando (o tye vs compose)
No es ningún secreto que con compose también puedo levantar toda mi aplicación y sus dependencias simplemente usando (docker-compose up
). Qué ofrece, entonces, Tye que no me de compose?
La respuesta es que compose es de uso genérico, que ejecuta cargas de trabajo de contenedores y las orquesta levemente. Tye, por otro lado, está pensado para ejecutar aplicaciones hechas en netcore. La principal diferencia entre un docker-compose up
y un tye run
es que en el segundo los proyectos netcore se ejecutan directamente con dotnet run
. A cada proceso de netcore, tye le asigna puertos aleatorios (en lugar de los tradicionales http/5000 y https/5001).
Al igual que Compose, Tye usa un fichero YAML, pero con su propio formato:
|
|
Es parecido a Compose, pero con la salvedad de que en Tye los servicios no tienen porque ser contenedores. En este caso tenemos dos servicios, uno de los cuales es un proyecto de netcore (ese se ejecutará en local). El otro es un contenedor de node. Cuando uso contenedores a Tye le puedo pasar o bien una imagen (aunque en este caso siempre hace docker pull
por lo que si es una imagen solo local no funcionará), o bien un Dockerfile y entonces Tye construye la imagen automáticamente. Cuando son procesos no netcore, debo indicarle a Tye un binding, que es un puerto de mi máquina host que se enlazará al (mismo) puerto del contenedor. Esos bindings además, se incluyen en el sistema de service discovery.
Al igual que compose, Tye puede levantar infraestructura adicional. Esa infraestructura por lo general son contenedores docker, pero (a diferencia de compose) pueden ser también directamente ejecutables. Eso plantea una duda: si mi código son procesos que corren directamente en mi máquina, pero la infraestructura adicional pueden ser contenedores Docker… ¿cómo se comunican?
Por un lado los contenedores Docker de infraestructura (pongamos un SQL Server p. ej.), exponen los puertos en localhost
. Ahí es Docker quien se encarga de todo, tye no hace nada. Si tienes un SQL Server ejecutándose en tu aplicación tye, este estará en localhost:1433
y tu código accederá a él usando esa dirección. Pero, ¿qué ocurre si es un contenedo el que debe comunicarse con tu proceso dotnet?
Imagina que tienes un microservicio hecho en nodejs, y el resto son de netcore. Cuando levantes con Tye la aplicación, Tye no sabe ejecutar nodejs, así que ese microservicio lo debes tener como imagen Docker. En este momento, tienes un contenedor (que ejecuta el microservicio de nodejs) y varios procesos locales que ejecutan tus servicios netcore. El contenedor se expone en localhost:xxxx
, por lo que la comunicación desde netcore hacia el contenedor está, como ya hemos visto, asegurada. El problema es si el contenedor nodejs debe abrir una conexión contra nuestros microservicios netcore. Nuestros procesos netcore exponen puertos en localhost
(Tye se encarga de eso), pero ese localhost
no es el mismo localhost
del contenedor. ¿Así pues, cómo puede comunicarse el microservicio de nodejs que se ejecuta bajo un contenedor con el servicio netcore que se ejecuta en local?
Bien, tengo un cliente de nodejs y un servidor de netocore para ver ese caso. Lanzo la aplicación con tye run
y accedo al dashboard de tye que se encuentra habitualmente en (http://localhost:8000
) y veo que efectivamente la aplicación consta de un proyecto (el de netcore) y un contenedor (el de nodejs):
Comuniación entre servicios
A diferencia de Compose (donde todo son contenedores), al usar Tye, tenemos una mezcla de procesos locales y de contenedores. En nuestro ejemplo netcoresvc
es un proceso local, mientras que nodeclient
es un contenedor. Acceder desde el proceso local (net core) al contenedor (nodejs) es sencillo, ya que este último expone los puertos en localhost
a través de Docker, por lo que, el proceso local solo debe acceder a localhost:xxxx
y allí estará el contenedor de nodejs escuchando. Ahí Tye no hace realmente nada, es Docker quien se encarga de todo.
Más divertido es el escenario inverso: acceder desde un contenedor a un proceso local. En este caso el proceso local expone también los puertos en localhost
, pero ¡ojo! ese localhost es la propia máquina, el host de Docker (recuerda que ahora se trata de un proceso local), por lo tanto si el contenedor accede a localhost:yyyy
, no encontrará nadie, ya que para un contenedor localhost
es sí mismo (y no el host de Docker). ¿Entonces, como habilita Tye esa comunicación?
La realidad ahí, me resulta un poco confusa, porque me da la sensación que Tye usa dos mecanismos distintos, o dicho de otro modo: habilita un mecanismo pero termina usando otro. Deja que te lo cuente y quedará más claro. El mecanismo que Tye habilita para permitir acceder desde un contenedor a un proceso local es que por cada proceso local, crea un contenedor asociado que hace de proxy. Esos proxies los monta en la misma red de Docker donde están el resto de contenedores, de forma que se ven entre ellos. Así, cuando un contenedor debe comunicarse con un proceso local, se comunica realmente con el contenedor proxy (controlado por Tye) y este simplemente reenvía la petición al proceso local que tenga asociado.
En nuestro ejemplo si ejecutas tye run
verás en los logs que salen por pantalla, las siguientes líneas:
Running container netcoresvc-proxy_6bff71f4-e with ID 175cf67562d0
Running docker command network connect tye_network_6b903a53-e netcoresvc-proxy_6bff71f4-e --alias netcoresvc
Running container nodeclient_83750d08-3 with ID 9aea73dbd9c0
Running docker command network connect tye_network_6b903a53-e nodeclient_83750d08-3 --alias nodeclient
Las primeras dos líneas muestran como Tye pone en marcha el proxy y lo enlaza a la red (en mi caso tye_network_6b903a53-e
) con el alias netcoresvc
. Las dos úlimas muestran como Tye ejecuta el contenedor de node y lo enlaza a la misma red bajo el alias nodeclient
. Si lanzo un docker ps
tengo lo siguiente:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9aea73dbd9c0 nodeclient "docker-entrypoint.s…" 4 minutes ago Up 4 minutes 0.0.0.0:3000->3000/tcp nodeclient_83750d08-3
175cf67562d0 mcr.microsoft.com/dotnet/core/sdk:3.1 "dotnet Microsoft.Ty…" 4 minutes ago Up 4 minutes netcoresvc-proxy_6bff71f4-e
Por un lado el contenedor de node, por otro lado el proxy. Voy ahora a ejecutar una sesión interactiva mediante un contenedor de busybox enlazado a esa misma red (docker run -it --network tye_network_6b903a53-e busybox /bin/sh
). Si ahora lanzas un ping netcoresvc
verás como nos responde una IP, que es, precisamente la IP del proxy. Y puedes verificar con un wget -qO- http://netcoresvc/
como obtienes la respuesta del proceso local. Por lo tanto, el contenedor de proxy reenvía la petición al proceso local. Pero, la realidad es que Tye parece usar otra estrategia para comunicar los contenedores con los procesos locales.
Service Discovery
Uno de los servicios que ofrece Tye es un service discovery, es decir le podemos preguntar a Tye las URLs donde residen los distintos servicios (sean procesos locales o contenedores). Eso es necesario porque Tye elige puertos aleatorios para los procesos locales, por lo que, a priori, no sabemos en que URL se ejecutarán. Ese service discovery funciona “al estilo Kubernetes”, es decir, Tye inyecta variables de entorno (con un determinado patrón) que contienen las URLs de los distintos servicios. De este modo, si quiero saber la URL por donde escucha por HTTP un servicio en concreto, solo debo consultar la variable de entorno correspondiente. El nombre de la variable de entorno se calcula a partir del nombre del servicio (el definido en tye.yaml
) y el binding que pidamos (el binding significa si queremos la URL de HTTP, la de HTTPS o cualquier otro protocolo como gRPC). Dado que, por lo general, en .NET Core las variables de entorno se leen a través de IConfiguration existe un paquete NuGet que ofrece un método de extensión sobre IConfiguration
para pedir la URL de un servicio.
Pues, si analizamos las variables de entorno que nos ha inyectado Tye en el contenedor nodeclient
podremos hacernos una idea de como espera Tye que accedamos al servicio de .NET (proceso local). Para verlo he abierto una sesión interactiva con el contenedor de node y he lanzado un env | grep NETCORESVC
y ese ha sido el resultado:
SERVICE__NETCORESVC__HOST=host.docker.internal
SERVICE__NETCORESVC__PROTOCOL=http
SERVICE__NETCORESVC__PORT=59587
SERVICE__NETCORESVC__HTTPS__HOST=host.docker.internal
SERVICE__NETCORESVC__HTTPS__PROTOCOL=https
SERVICE__NETCORESVC__HTTPS__PORT=59588
NETCORESVC_SERVICE_HOST=host.docker.internal
NETCORESVC_SERVICE_PROTOCOL=http
NETCORESVC_SERVICE_PORT=59587
NETCORESVC_HTTPS_SERVICE_HOST=host.docker.internal
NETCORESVC_HTTPS_SERVICE_PROTOCOL=https
NETCORESVC_HTTPS_SERVICE_PORT=59588
Lo primero que puedes observar es que los valores están como repetidos (hay el doble de variables de entorno de las que se necesitarían). Eso es porque Tye crea variables de entorno en el formato Kubernetes (las que empiezan por NETCORESVC_
y luego crea las variables de entorno en su propio formato (las que empiezan por SERVICE__
). En todo caso lo que me interesa es que veas el valor del host, que es host.docker.internal
. Recuerda que esas variables de entorno son las que ha inyectado Tye al contenedor de node, por lo que Tye “espera” que el contenedor de node acceda al servicio de netcore (proceso local) a través de http://host.docker.internal:59587
. Este host host.docker.internal
es un host que existe solo dentro de las redes de Docker y que representan a la propia máquina Host. Así, de ese modo el contenedor accede al puerto 59587 del host directamente, sin pasar por proxy alguno.
Investigando el contenedor proxy
Tye monta ese contenedor de proxy (aunque luego parece que no es necesario para permitir la comunicación entre contenedores y procesos locales), pero… ¿lo usa para algo más? Pues la verdad es que el código de este contenedor es bastante sencillo. Parece que consta solo de un Program.cs
y realmente parece que solo hace de proxy. He hecho algunas pruebas y efectivamente cuando, desde un contenedor accedo al servicio local usando el DNS de netcoresvc
el proxy muestra logs de que redirige la conexión:
dbug: Microsoft.Tye.Proxy.Program[0]
Attempting to connect to netcoresvc-proxy_acbe28d0-2 listening on 80:62577
dbug: Microsoft.Tye.Proxy.Program[0]
Successfully connected to netcoresvc-proxy_acbe28d0-2 listening on 80:62577
dbug: Microsoft.Tye.Proxy.Program[0]
Proxying traffic to netcoresvc-proxy_acbe28d0-2 80:62577
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[6]
Connection id "0HM38GQ6IUJ8O" received FIN.
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[7]
Connection id "0HM38GQ6IUJ8O" sending FIN because: "The Socket transport's send loop completed gracefully."
Observa que el proxy usa los puertos 80 y también el 443 y enruta tanto HTTP como HTTPS. Para configurarse este contendor usa determinadas variables de entorno (que le suministra Tye):
CONTAINER_HOST
: DNS del host de Docker (o seahost.docker.internal
al menos en Docker Destkop bajo Windows)PROXY_PORT
: Puertos locales (del contenedor) y remotos (del host) a mapear. Así, si el proceso local de netcore escucha en el59587
(HTTP) y59588
(HTTPS) esta variable valdrá:80:59587;443:59588
.APP_INSTANCE
: Parece contener el nombre del propio proxy, usado en los logs.
En resumen, el proxy está ahí y funciona perfectamente, por eso no entiendo porque Tye no lo usa en su sistema de service discovery. He puesto una issue en el repo, preguntando precisamente eso.
Añadiendo réplicas
Una de las características de Tye es que hace súper sencillo el añadir réplicas de tus servicios y así probar en desarrollo escenarios concurrentes. He modificado el fichero tye.yaml
añadiendo replicas: 3
al servicio netcoresvc
para ver que es lo que ocurre. Por un lado, efectivamente ahora tengo 3 réplicas del proceso (cada una de ellas escuchando en sus par de puertos de mi máquina (un puerto para HTTP, otro para HTTPS)). Ahora bien, en el dashboard Tye solo me muestra un par de puertos:
En la imagen he puesto el dashboard de Tye y parte de los logs. Observa como en el dashboard solo vemos dos puertos, pero es que encima ninguno de esos dos puertos se corresponde a los puertos reales de los 3 procesos. Eso es porque en este caso, el propio Tye hace de proxy y me ofrece un endpoint único para las 3 réplicas:
C:\>curl http://localhost:62838
Hello World from UFOBOT-V4 (2dc8cd39-a99e-4f4d-9cc7-f6df0bdf8792)
C:\>curl http://localhost:62838
Hello World from UFOBOT-V4 (da9e1ac7-183e-47bc-8f5e-22d9c7c463f8)
C:\>curl http://localhost:62838
Hello World from UFOBOT-V4 (2daab067-1bc3-4d14-833f-a241afd88c53)
C:\>curl http://localhost:62838
Hello World from UFOBOT-V4 (2dc8cd39-a99e-4f4d-9cc7-f6df0bdf8792)
El proceso netcoresvc
simplemente imprime el nombre de la máquina y un Guid único. Se puede ver como parece que Tye usa un round robin y va repartiendo las peticiones entre las 3 réplicas. Por supuesto yo puedo acceder directamente a una réplica si sé su puerto (que sale en los logs). Teniendo réplicas, Tye configura el contenedor de proxy de la siguiente manera:
PROXY_PORT
:80:62838;443:62842
CONTAINER_HOST
:host.docker.internal
Nada inesperado: el contenedor de proxy queda enlazado al par de puertos que controla Tye, por lo que las llamadas a través del contenedor de proxy también serán balanceadas entre las instancias.
Y, como ya te debes imaginar, al contenedor de node, Tye le pasa la siguiente configuración:
SERVICE__NETCORESVC__PROTOCOL=http
SERVICE__NETCORESVC__HOST=host.docker.internal
SERVICE__NETCORESVC__PORT=62838
SERVICE__NETCORESVC__HTTPS__PROTOCOL=https
SERVICE__NETCORESVC__HTTPS__HOST=host.docker.internal
SERVICE__NETCORESVC__HTTPS__PORT=62842
Lo mismo que en el caso anterior. Si usamos esas variables, el contenedor de node accede directamente a la máquina Host, a los puertos controlados por Tye y por lo tanto las llamadas serán balanceadas.
Replicas de contenedores
El escenario anterior era cuando las réplicas eran de los procesos locales (netcore). Pero también podemos usar réplicas en los contenedores (en este ejemplo replicar el contenedor de nodejs). En este caso Tye levanta un contenedor por cada réplica y los puertos se mapean a puertos elegidos por Tye. En mi ejemplo, el binding del contenedor de node es sobre el puerto 3000
y este es el puerto que se abría en mi máquina. Ahora bien, cuando tenemos réplicas eso cambia:
C:\>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
569f6ad05339 nodeclient "docker-entrypoint.s…" 35 seconds ago Up 34 seconds 3000/tcp, 0.0.0.0:51478->51478/tcp nodeclient_036f6f79-2
4bf332f84a23 nodeclient "docker-entrypoint.s…" 35 seconds ago Up 34 seconds 3000/tcp, 0.0.0.0:51477->51477/tcp nodeclient_fea63638-d
1d911b66007e nodeclient "docker-entrypoint.s…" 35 seconds ago Up 35 seconds 3000/tcp, 0.0.0.0:51476->51476/tcp nodeclient_92aba817-4
En el log de tye run
verás un mensaje parecido al siguiente:
Mapping external port 3000 to internal port(s) 51476, 51477, 51478 for nodeclient binding null
El problema ahora es el siguiente: Tye sabe que hay un binding del contenedor de node, sobre el puerto 3000. Cuando solo había una réplica, Tye se limita a enlazar el puerto 3000 de mi máquina (el valor especificado en el binding), al puerto 3000 del contenedor, y por lo tanto accediendo a localhost:3000
, ahí estaba el contenedor de node. Ahora con réplicas Tye ya no puede hacer eso, ya que no puede abrir N veces el puerto 3000. Por lo tanto lo que Tye hace es mapear un puerto al azar por cada réplica (en mi caso son esos 51478
, 51477
y 51476
) del contenedor al mismo puerto local de la máquina. Además de eso, Tye usa el propio proxy que él contiene para enlazar el puerto 3000 de mi máquina a esos 3 puertos. Es decir, llamando a localhost:3000
desde mi máquina, la petición será interceptada por Tye y redirigida a cualquiera de esos 3 puertos y por lo tanto a cualquiera de las 3 réplicas del contenedor de node.
Por lo tanto, Tye hace su trabajo magistralmente, el problema está en mi código de nodejs. Dicho código abre siempre el puerto del contenedor especificado en la variable de entorno PORT
. En el fichero tye.yaml
siempre le especifico 3000
, por lo que cada réplica abre su puerto 3000, pero Tye no hace nada con ese puerto del contenedor. En el caso de proyectos netcore eso no ocurría, porque Kestrel usa por defecto la variable de entorno ASPNETCORE_URLS
para abrir sus puertos y Tye, como entiende de netcore, modifica esa variable de entorno para cada réplica.
¿Puedo, de algún modo desde el contenedor de node saber qué puerto me ha sido asignado? Pues sí, porque Tye me inyecta esa configuración en la variable PROXY_PORT
que tiene la forma <container-port>:<host-port>
. Así me basta el siguiente código en mi contenedor node:
|
|
Con esa salida, ahora cada réplica de mi contenedor abre el puerto correspondiente (si existe la variable PROXY_PORT
que nos pasa Tye usa dicha variable y en caso de no existir hace fallback a la variable PORT
). Ahora sí que la magia es completa:
- Llamando a
localhost:3000
me responde una de las réplicas (la que Tye elija, que parece que hace round robin) - Llamando a
localhost:51478
me responde la primera réplica (ya que es su propio puerto). Si usolocalhost:51477
me responde siempre la segunda y si usolocalhost:51476
siempre la tercera. Pero recuerda que esos puertos propios de cada réplica cambian a cada ejecución, el puerto importante es el definido en el binding, eso es el3000
. Ese es el puerto que Tye inyecta en las variable de entorno de los otros procesos, por si esos deben llamar al contenedor de node:
SERVICE__NODECLIENT__HOST=host.docker.internal
SERVICE__NODECLIENT__PORT=3000
Bindings y cadenas de conexión
Un escenario típico en Tye es levantar la infrastructura usando contenedores. Ya levantes un Redis, un Mongo, un SQL Server o un RabbitMQ al final necesitarás algún tipo de cadena de conexión para conectarte con él. Tye es agnóstico de la cadena de conexión que definas, pero tiene algunas pequeñas ayudas. Por ejemplo, si quisieras levantar un SQL Server, tendrías lo siguiente en tu tye.yaml
:
|
|
Observa la definición del binding. Por un lado indicamos que el contenedor escucha en el 1433
, por lo que al levantar eso, en localhost:1433
estará escuchando nuestro servidor. Pero el binding define también una cadena de conexión. Esta cadena de conexión me la inyectará Tye en los demás servicios (como variable de entorno):
CONNECTIONSTRINGS__SQLSERVER=Server=host.docker.internal,1433;User Id=sa;Password=Pass@word1
Que la variable de entorno tenga ese nombre (CONNECTIONSTRINGS__SQLSERVER
) no es casual, eso nos permite leerla fácilmente desde netcore usando GetConnectionString. Desde cualquier otro servicio puedo leerla sabiendo que el nombre es ese (CONNECTIONSTRINGS__<NOMBRE-SERVICIO>
). Observa además como en la variable de entorno inyectada se ha sustituído ${host}
por el host del servicio y ${port}
por el puerto.
Integración con Dapr
¿Conoces Dapr? Tengo pendiente ir hablando de él en el blog, pero se trata de un runtime de ejecución de aplicaciones distribuídas que permite desarrollar de forma portable e independiente de la tecnología ciertos aspectos de aplicaciones distribuídas. Dapr usa el patrón sidecar, por lo que hay un proceso de Dapr ejecutándose y asignado a cada servicio. Este proceso de Dapr se comunica mediante el servicio usando HTTP o gRPC y es el que le da acceso a las funcionalidades ofrecidas por Dapr. En Kubernetes eso se traduce en que cada pod termina ejecutando dos contenedores (el del servicio y el de Dapr). En local se traduce en que para iniciar tu aplicación debes hacerlo a través del ejecutable de dapr (pones en marcha dapr y dapr pone en marcha tu proceso). Eso entra en contradicción con Tye, ya que para empezar, en el caso de netcore en Tye no le indicamos procesos si no simples proyectos (Tye los compila y los lanza). Por suerte Tye se integra con Dapr, por lo que sin tener que hacer apenas nada, podemos indicar a Tye que lance nuestros proyectos netcore usando Dapr.
A grandes rasgos basta con añadir lo siguiente al fichero tye.yaml
:
|
|
Con eso añadimos la extensión de dapr y le indicamos en qué directorio están los YAMLs que definen los componentes que Dapr debe usar. Los componentes en Dapr son la infraestructura subyacente que Dapr usa. Por ejemplo, si queremos pub/sub bajo Dapr, entonces necesitamos alguna infraestructura (p. ej. un Redis, o un SQS o un Service Bus). Qué infraestructura usar y su configuración se define en esos YAML. Luego Dapr nos abstrae completamente (ya que pasamos a usar la API de Dapr, no la de la infraestructura). Un tema a tener importante es que Tye solo lanza nuestros procesos, no se encarga de que la infraestructura necesaria se esté ejecutando en mi máquina. Si es necesario debo añadir dicha infraestructura en mi fichero tye.yaml
o bien tenerla en marcha ya en mi máquina.
¡Y poco más! Lo dejo por aquí por el momento. Espero que os haya sido interesante y que os animéis a probar Tye, porque yo pienso que es una herramienta con un futuro prometedor. Y si, en un futuro, Visual Studio entiende de Tye y nos ofrece una experiencia de F5 similar a la que ofrece con Compose, ¡entonces ya sería la bomba!
En otro post veremos como desplegar a Kubernetes desde Tye :)
Saludos!