En este post, vamos a centrarnos en como contenerizar una aplicación en .NET, primero revisaremos que novedades tenemos con NET 8 y luego veremos un enfoque más clásico usando Docker. Adicionalmente, veremos que buenas prácticas es recomendable seguir y que consideraciones respecto a la seguridad debemos de tener en cuenta cuando lo realizamos.
CONTENERIZACIÓN EN NET 8
A partir de NET 7 y consecuentemente en NET 8, tenemos la posibilidad de contenerizar nuestras aplicaciones directamente desde nuestra solución (.csproj), sin necesidad de usar un “dockerfile” como seguramente hemos usado ampliamente si queríamos conseguir este resultado con anterioridad. Esto gracias al uso de la librería “Microsoft.NET.Build.Containers”, esta mejora nos permitirá estandarizar como generamos los containers para nuestras aplicaciones.
Veamos como conseguimos realizar esto. Una ves que tengamos nuestra aplicación lista para contenerizar, usamos el siguiente comando.
dotnet publish /p:PublishProfile=DefaultContainer
El resultado de este comando termina siendo algo parecido a este mensaje:
Notemos que, por default, esta contenerización esta utilizando la siguiente imagen ‘mcr.microsoft.com/dotnet/aspnet:8.0’. Como veremos más adelante, es importante tener control sobre que imagen base vamos a usar, veamos como podemos cambiar este valor.
En el ‘.csproj’, añadimos:
Y el resultado obtenido sería el siguiente:
También es posible usar imágenes base personalizadas, veamos como hacer esto, usando los siguientes comandos.
Siendo el resultado, el siguiente:
Adicionalmente, podemos realizar configuraciones adicionales a nuestra contenerización, mencionaremos algunas relevantes:
- Apuntar a otras arquitecturas de SO:
- Agregar labels a nuestro container:
- Usar variables de entorno:
- Indicar el puerto que vamos a usar y el protocolo:
Se tienen más opciones de configuración, para lo cual, es recomendable referirnos a la documentación oficial.
CONTENERIZACIÓN USANDO UN DOCKER
Partamos usando un proyecto común usando por el mismo repositorio de Docker como ejemplo de contenerización:
git clone https://github.com/dockersamples/student-record-management
Al clonar este repositorio, revisemos rápidamente el Docker compose file.
Como podemos ver, para levantar la aplicación, necesitamos a su vez levantar tres componentes:
- Una base de datos (postgres).
- Un administrador de esta base de datos (adminer).
- Nuestra aplicación (web ´student-record-management’). Se hace referencia al archivo ‘Dockerfile’ que tenemos en nuestra carpeta root de nuestra aplicación clonada.
Si revisamos que contiene nuestro archivo ‘Dockerfile’.
Analicemos algunos puntos de nuestro archivo linea a linea:
- Le indicamos a Docker que imagen base usar
- Se define la carpeta del directorio de trabajo.
- Copiamos el contenido de nuestro repositorio a la carpeta del directorio de trabajo que hemos definido.
- Ejecutamos el comando “dotnet build -o /app” para compilar nuestra solución.
- Ejecutamos el comando “dotnet publish -o /publish” para generar una publicación de nuestra solución y establecermos la ruta de salida de en la carpeta “publish”.
- Establecemos un nuevo directorio de trabajo, a la carpeta “publish”.
- Seteamos una variable de entorno de nuestra imagen Docker, llamada “ASPNETCORE_URLS” a cualquier dirección IP, con el puerto 80.
- Exponemos en nuestra imagen Docker el puerto 80 para que pueda ser consumido desde fuera del container.
- Finalmente, levantamos nuestra aplicación.
Como pueden ver, con estos simples pasos ya tenemos una aplicación completa corriendo en un ambiente aislado, que nos asegura independencia y control sobre que ocurre en nuestra imagen.
Sin embargo, que funcione, no nos asegura que estamos cumpliendo con buenas prácticas, el primer punto y que debería resultar el más evidente que podríamos mejorar es la imagen sobre la cual corremos toda nuestra aplicación, dado que estamos usando el SDK completo de .NET (versión 6.0.0, que pasa alrededor de 760 MB). Veamos cuales son los puntos (buenas prácticas) que debemos considerar.
ESCOGER LA IMAGEN DE DOCKER CORRECTA
Las más evidente – espero – a simple vista, podemos guiarnos del repositorio oficial de Microsoft, teniendo en cuenta que se tienen imágenes para desarrollo y para producción, entre las cuales para producción hay imágenes con el runtime de .NET y optimizaciones que nos pueden servir, adicionalmente a esto, se pueden elegir imágenes basadas en SO Linux o Windows también.
Considerando esto, en la última .Net conf 2023, tuvimos una actualización con las imágenes base que tenemos disponibles para desplegar nuestros containers.
OPTIMIZAR NUESTRO DOCKERFILE PARA COMPILAR NUESTRO PROYECTO
En el mundo de .NET, cuando se buscan restaurar las dependencias que pueden tener nuestro proyecto, requerimos de un archivo tipo “.csproj”, “.sln” o un “nuget.config”. Podríamos aprovechar estas características, para hace run restore de nuestra imagen al inicio y permitir que Docker realice un cache de esta información y de este optimizar el tiempo construcción de nuestra imagen. Veamos en una imagen, como conseguimos esto.
Al copiar primero a nuestro directorio de trabajo todos los archivos “.csproj” que tenga nuestra solución y hacerle un “restore”, estamos consiguiendo, optimizando como Docker construye nuestra imagen.
COMPILAR NUESTRA IMAGEN CON MULTIPLES ETAPAS
En el ejemplo que desarrollamos, estábamos usando el SDK completo para compilar nuestra aplicación (y este generaría que nuestra imagen Docker, pesará como mínimo 760 MB).
El motivo por el que se puede estar haciendo esto, es porque para generar el compilado de nuestra solución, se pueden requerir características no son propias del runtime, estas pueden usarse en la ejecución de los test unitarios, entre otros posibles usos.
Sin embargo, una ves que hemos generado el build usando estas características del SDK, es posible generar nuestro publish de otra imagen de Docker, una más acorde a la optimización que buscamos de nuestras imágenes, vemos un ejemplo de esto.
Esta segunda imagen, está optimizada para producción y es sobre está imagen que generamos nuestra imagen de Docker final, copiando el compilado que hemos obtenido de la anterior. No analizaremos el paso a paso, porque lo hicimos líneas arriba, pero el resultado final de usar ambas imágenes para generar nuestro archivo Docker, es que la imagen final de nuestra aplicación tendrá aproximadamente 240 MB.
PREFERIR USAR TAGS ESPECIFICOS DE IMÁGENES DOCKER, SOBRE LOS “LATEST”
Si hemos usado Docker, sabemos que en las imágenes se generan tenemos la posibilidad de indicar un tag en específico. Y a su vez, cuando usamos imágenes de terceros, podemos indicar un tag en específico a usar. Este último punto es importante siempre tenerlo en cuenta, dado que, si no lo hacemos, por defecto se usa el tag “latest”, lo cual significa que cuando se construya nuestra imagen Docker, buscará la última versión reciente, y esto en muchos casos implica cambios que pueden romper nuestro build o tener comportamientos inesperados.
EJECUTAR NUESTRO PROCESO EN DOCKER, NO COMO UN USUARIO ROOT
Recordemos que al final de cuentas, una imagen Docker corre bajo un sistema operativo, por lo que el manejo de usuarios y los permisos que estos tienen es un punto fundamental que considerar cuando queremos evitar alguna posible brecha de seguridad. Teniendo esto en mente, recordemos que en Docker tenemos la posibilidad de usar el comando “USER” para poder modificar el usuario por defecto que usa nuestro Docker file (root).
Como se puede ver en el dockerfile, creamos el usuario en el SO con el comando “RUN” y luego hacer el cambio para ejecutar los procesos con un usuario distinto usando el comando “USER”.
USAR “.dockerignore” PARA MANTENER LIMPIA NUESTRA IMAGEN
Muy parecido a su uso en GIT, con esto, buscamos obviar que se consideren en nuestro build información que no nos sirve, por ejemplo, los archivos de compilación local que tenemos en nuestro proyecto, archivos con un READMNE, etc. Veamos un ejemplo que de podríamos encontrar en este archivo.
USO DE HEALTH CHECKS
El comando “HEALTHCHECKS” nos permite indicarle a Docker como está corriendo nuestro servicio, ya sea por si por algún error de codificación se cayó y no puede procesar solicitudes. De este modo, podemos gestionar mejor nuestros contenedores. Adicionalmente, este punto es relevante cuando usamos orquestadores como Kubernetes, dado que le damos visibilidad de que es lo que está pasando con nuestro container. Para implementar los health checks, tenemos que considerar si nuestra imagen va a correr sobre un SO del tipo Linux o Windows. Linux
Windows
Con esto llegamos al final del artículo sobre contenerización. Quedan varios puntos en el tintero, espero que les haya servidor este overview sobre algunas consideraciones y sigamos mejorando en nuestra contenerización de aplicaciones en .NET.
Deja un comentario