Trabajando con AKS Devspaces

AKS Devspaces es una característica que Microsoft está promocionando bastante en sus charlas: casi en cualquier evento donde se hable de AKS se demuestra, ni que sea brevemente, Devspaces.

Pero… qué es y lo más importante: ¿Para qué sirve y como se usa?

Devspaces es una tecnología diseñada para facilitar las pruebas de desarrollo en entornos multi-contenedor. Es decir, desarrollamos una solución compuesta de varios contenedores y queremos poder depurar o probar nuevas versiones de un contenedor sin afectar al resto.

Sin Devspaces hay varias opciones que podríamos usar:

  1. Ejecutar el «docker-compose.dcproj» desde Visual Studio: con esto obtenemos depuración multi-contenedor a costa de tener todo en una solución (sln) de Visual Studio. Esta opción es la única que permite escenarios de depuración multi-contenedor.
  2. Ejecutar todos los contenedores en local. Ya sea con compose o si queremos un escenario más «realista» usar Minikube o el Kubernetes de Docker Desktop. El problema principal es que debo instalar y ejecutar todo el sistema en mi máquina de desarrollo. Es posible depurar un contenedor, aunque hay que recurrir a técnicas un poco complejas.
  3. Tener un namespace de Kubernetes para mi solo y en este namespace tener instalado todo el sistema (o parte de él). Esta opción funciona, pero complica el proceso de pruebas: cada vez que desarrolles debes generar la imagen, subirla al registro y desplegarla en el clúster. Además, la depuración es casi imposible.

Devspaces intenta ofrecer una solución. Actualmente (y desde hace bastante tiempo) está en previewEso significa que en algunos escenarios la experiencia flaquea un poco, pero voy a intentar contaros qué podemos hacer a día de hoy.

Para probar devspaces necesitais un clúster de AKS creado en una región compatible con Devspaces (p. ej. east us) y con Http Application Routing habilitado.

Configurar el clúster para Devspaces

Para poder usar devspaces lo primero es configurar el clúster. Esto es porque para que Devspaces funcione deben agregarse varios elementos al clúster. Para ello debemos teclear:

az aks use-dev-spaces -g <grupo-recursos> -n <nombre-aks>

Esto (después de un rato) nos habilitará Devspaces en el clúster y además si es la primera vez que lo usamos nos instalará la CLI de Devspaces (azds). Durante este proceso nos preguntará por un Devspace a usar de forma predeterminada. Vamos a decirle default.

A nivel de Kubernetes eso nos creará un namespace nuevo llamado azds con toda la infraestructura propia de Devspaces.

Ejecución del comando az aks use-dev-spaces

Preparando los proyectos para Devspaces

Para poder desplegar un proyecto en Devspaces dicho proyecto debe estar preparado y para ello necesitamos básicamente tres cosas:

  • El fichero de configuración de Devspaces (azds.yaml)
  • Un Dockerfile especial, usado por Devspaces (Dockerfile.develop)
  • El chart de Helm para desplegar el proyecto

Esos tres elementos son por proyecto (en nuestro caso pues los tendremos tanto para la web como para la API). Por suerte para crearlos podemos usar la herramienta de línea de comandos «azds». Solo debes situarte en cada uno de los csprojs y teclear «azds prep –public»

Nota: El parámetro «–public» es para indicar que queremos acceso desde el exterior (eso nos creará un recurso ingress asociado).

Este comando no instala nada en el clúster, solo nos crea el Dockerfile.Develop, el chart de Helm y el fichero de configuración azds.yaml:

Ficheros creados por azds

Por supuesto esos ficheros son una plantilla que podemos retocar (y deberemos hacerlo especialmente el fichero Dockerfile.develop y el chart de Helm). Luego veremos porqué necesitamos este Dockerfile.develop.

Instalando la versión «base»

Vale, el escenario que vamos a cubrir va a ser el de que tenemos dos contenedores (una web y una api) y nosotros somos desarrolladores de la API. Queremos poder probar (y depurar) los cambios de la API, pero en un escenario end-to-end (es decir usando la web).

Para ello, lo primero es tener en un devspace «la versión base»: eso es, la versión que va a ser compartida por todos los desarrolladores. En este devspace va a estar desplegado todo el proyecto (en nuestro caso la web y la api).

Ahora que tenemos los proyectos preparados ya los podemos desplegar en el «devspaces» base (que es default). Para ello simplemente nos vamos a ambos proyectos y tecleamos:

azds up -d

Eso hace muchas cosas:

  1. Sincroniza los ficheros locales con los del clúster
  2. Instala el chart de Helm para crear los objetos de Kubernetes
  3. Crea la imagen (usando Dockerfile.develop)
  4. Nos da una URL pública (si teníamos acceso desde el exterior)

Salida del comando azds up

Una vez hemos ejecutado «azds up -d» en todos los proyectos, ya tenemos la versión base de la aplicación instalada en el devspace default:

Objetos k8s (servicios, deployments, ingresses) creados

También puedes usar el comando «azds list-up» que te dirá lo que se está ejecutando y que está controlado por devspaces:

Salida del comando azds list-up

Arreglando la API

Eres un desarrollador nuevo y te han asignado un error en la API. Y es que parece que la web siempre muestra el mismo valor (42) recibido de la API:

Error en la web (siempre muestra 42)

Teóricamente este valor debería ser distinto cada vez. Vale, vale, vale… este es un escenario tan simple que no requeriría el uso de devspaces (vamos, bastaría con depurar la API en local y depurarla con llamdas usando curl o similar), pero bueno…

Lo que vamos a ver es como:

  1. Como nuevo desarrollador puedes desplegar solo la API de una forma privada para tí
  2. Como puedes usar tu versión de la API y a la vez ejectuar la versión «base» de la web

Desplegar tu versión de la API

Para ello lo que necesitas es un devspace nuevo: si en default está la versión base, en el devspace nuevo que crees para tí, estará tu versión de la API.

Para ello, vete al directorio donde está la API y crea el nuevo devspace mediante el comando:

azds space select --name eiximenis

(Sustituye eiximenis por el nombre que quieras, claro). Es importante que cuando te pregunte por el Devspace padre selecciones «default». ¿Por qué? Porque de ese modo establecemos la relación de que para todos aquellos contenedores que no estén en eiximenis se ejecute la versión de «default».

A nivel de Kubernetes se ha creado un namespace nuevo (eiximenis) que actualmente está vacío. Pero no solo ha ocurrido eso: se han creado nuevas entradas de DNS para los servicios que había en el devspace base:

Nuevas entradas DNS (ingresses) creadas

Observa como el nombre es <nombre-devspace>.s.<dns-base>.

Si ahora accedo a http://eiximenis.s.webclient.82gwcx5dsw.eus.azds.io/ veré igualmente la web, pero ahora estoy accediendo a través de mi devspace nuevo (eiximenis). Lo que pasa es que como no hay nada en este devspace y su padre era default, se ejecuta todo lo que hay en default.

Probando la API

No vamos a ver como depurar el código porque eso depende de la herramienta que uses (Visual Studio o Visual Studio Code), pero veamos como sería el flujo.

Recuerda que estamos situados en el directorio de la API, así que ahora usamos azds up -d para instalar la API en nuestro devspace nuevo.  En este momento tengo la API instalada en mi devspace (aunque es la misma versión que la «base» que está en default).

Ahora uso Code o cualquier editor y abro el código de la API. Como soy un desarrollador Ninja Master Senior, intuyo que el error estará en /Controllers/RandomController.cs:

[HttpGet("value")]
public ActionResult<int> Get()
{
    return 42;
}

Ajá… he encontrado el error! Ahora simplemente lo corrijo:

[HttpGet("value")]
public ActionResult<int> Get()
{
    return new Random().Next(1,101);
}

Una vez corregido vuelvo a lanzar «azds up -d» y eso me subirá la nueva versión a AKS. Ahora ya puedo probar la web pero desde mi devspace. Y el resultado es… (redoble de tambores)… EL MISMO QUE ANTES. La web sigue mostrando siempre el valor de 42!

No obstante si pruebas la API con curl verás que el resultado es distinto cada vez. ¿Qué está ocurriendo?

Error de la web aunque la API va bien

El header de Devspaces

Lo que está ocurriendo es lo siguiente:

  1. Entras a la web a través de la URL del devspace eiximenis (la que empieza por eiximenis.s).
  2. La web no está en dicho devspace, pero como es hijo de «default» se ejecuta la web de default
  3. La web de default llama a la API, y se ejecuta la API… de default, no del devspace eiximenis.

Y es que nos hemos olvidado de una cosa: de una cabecera HTTP. Pero para entenderlo retrocedamos un poco. Si observas como son los recursos ingress de devspaces verás lo siguiente:

Ingress de azds con la anotación de ingress.class

Observa que los ingress tienen la anotación «kubernetes.io/ingress.class: traefik-azds«. ¿Y que narices es traefik-adzs? Pues nada más ni nada menos que el controlador ingress de Devspaces. Si miras en el namespace de azds verás el servicio LoadBalancer asociado.

Traefik es un proxy inverso que puede ser configurado como controlador ingress. Lo que nos interesa a nosotros es que cuando usemos un devspace xyz, traefik añadirá una cabecera «azds-route-as» con el valor del devspace. Nosotros debemos propagar esta cabecera en las llamadas HTTP que hagamos.

¡Y ese es el problema! Como la web no propaga esta cabecera, cuando desde la web invocamos a la API, se ejecuta la API del mismo devspace que la web (en este caso default).

Vamos ahora a modificar la web. Para ello, lo primero es volver a situarnos en el devspace «base» (default) usando:

azds space select --name default

Ahora modificamos la web para propagar la cabecera. Por suerte dado que la web usaba IHttpClientFactory (como estoy seguro hacen todos tus nuevos desarrollos, ¿verdad?) este cambio es muy sencillo y en un solo punto. Básicamente en Startup, modifico:

services.AddHttpClient("api").ConfigureHttpClient(c =>
{
    c.BaseAddress = new Uri(Configuration["ApiUrl"], UriKind.Absolute);
});

por:

services.AddHttpContextAccessor();
services.AddTransient<DevspacesMessageHandler>();
services.AddHttpClient("api").ConfigureHttpClient(c =>
{
    c.BaseAddress = new Uri(Configuration["ApiUrl"], UriKind.Absolute);
}).AddHttpMessageHandler<DevspacesMessageHandler>();

Vale, vale, eso no es todo… tengo que implementar la clase DevpsacesMessageHandler:

public class DevspacesMessageHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public DevspacesMessageHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var req = _httpContextAccessor.HttpContext.Request;

        if (req.Headers.ContainsKey("azds-route-as"))
        {
            request.Headers.Add("azds-route-as", req.Headers["azds-route-as"] as IEnumerable<string>);
        }
        return base.SendAsync(request, cancellationToken);
    }
}

Una vez hechos esos cambios, publico la nueva versión «base» de la web usando «azds up -d».

Ahora ya podemos volver a situarnos en el devspace eiximenis:

azds space select --name eiximenis

Ok, si ahora accedes a la web usando la URL del devspace eiximenis verás como la web llama a la versión de la API que está en el devspace eiximenis, pero que si accedes mediante la URL del devspace default (sin eiximenis.s delante) verás como se usa la versión «base» de la API (y recibimos siempre 42):

Web llamando a la versión de la API correcta

¡Y listos! Con eso hemos visto como usar devspaces. Nos ha faltado por ver como depurar, pero eso depende de la herramienta y haría el post más largo de lo que ya es.

Así, que en resumen:

  1. Tenemos una versión «base» de todos los servicios
  2. En mi devspace tengo solo el/los servicios con los que estoy trabajando
  3. Debo usar siempre la URL asociada con mi devspace (la que empieza por <nombre-devspace>.s
  4. Debo acordarme de pasar la cabecera azds-route-as
  5. En todo momento, mediante azds, trabajo en un devspace. Con «azds space select –name <nombre>» selecciono con que devspace trabajo
  6. Con «azds up» instalo el servicio en el devspace en el que esté trabajando
  7. Con «azds down» elimino el servicio de dicho devspace

Ay sí, que me olvido: el fichero Dockerfile.develop

Antes he comentado que veríamos el por qué del fichero «Dockerfile.develop». Bien, este es el Dockerfile que se usa para construir el contenedor que se despliega en devspaces. En devspaces, dado que es un entorno de desarrollo y depuración  (por más AKS que sea), no desplegamos los contenedores finales si no unos de desarrollo.

Esos contenedores básicamente, parten de la imagen del SDK y contienen el código fuente y ejecutan un «dotnet run» como punto de entrada. Cada vez que usamos «adzs up», se copia el código fuente al contenedor de desarrollo y se ejecuta el «dotnet run». Esto, el tener el código fuente en el contenedor, es lo que habilita los escenarios de depuración.

Ahora sí… como introducción no ha estado mal, ¿verdad?

Os dejo un .zip con el ejemplo finalhttps://1drv.ms/u/s!Asa-selZwiFlg_t_BFZaTiiDbmLByQ

¡Espero que os haya sido interesante!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *