Saltar al contenido

Etiqueta: microsoft

Contenerización en .NET y Buenas Prácticas de Seguridad

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:

Resultado dotnet publish

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:

Cambiando csproj

Y el resultado obtenido sería el siguiente:

Resultado cambio de version a usar

También es posible usar imágenes base personalizadas, veamos como hacer esto, usando los siguientes comandos.

Indicando la imagen base

Siendo el resultado, el siguiente:

Resultado de indicar la imagen base

Adicionalmente, podemos realizar configuraciones adicionales a nuestra contenerización, mencionaremos algunas relevantes:

  • Apuntar a otras arquitecturas de SO:
Otras arquitecturas
  • Agregar labels a nuestro container:
Labels
  • Usar variables de entorno:
Variables de entorno
  • Indicar el puerto que vamos a usar y el protocolo:
Puerto y 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.

Archivo docker compose

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.
Archivos del proyecto

Si revisamos que contiene nuestro archivo ‘Dockerfile’.

Ejemplo dockerfile

Analicemos algunos puntos de nuestro archivo linea a linea:

  1. Le indicamos a Docker que  imagen base usar
  2. Se define la carpeta del directorio de trabajo.
  3. Copiamos el contenido de nuestro repositorio a la carpeta del directorio de trabajo que hemos definido.
  4. Ejecutamos el comando “dotnet build -o /app” para compilar nuestra solución.
  5. 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”.
  6. Establecemos un nuevo directorio de trabajo, a la carpeta “publish”.
  7. Seteamos una variable de entorno de nuestra imagen Docker, llamada  “ASPNETCORE_URLS” a cualquier dirección IP, con el puerto 80.
  8. Exponemos en nuestra imagen Docker el puerto 80 para que pueda ser consumido desde fuera del container.
  9. 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.

Tamaño de imágenes

Mejora en las imágenes

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.

Optimización del dockerfile

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.

Dockerfile con Multiple Stage

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).

Se genera un usuario especifico para nuestro docker

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.

Ejemplo dockerfile

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

Healtcheck en linux

Windows

Healtcheck en 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

Dockerizando una aplicación Net 5.0 – Part. II

Este post es una continuación de su versión anterior, en este nos enfocaremos en como configuramos nuestro ambiente de desarrollo con docker.

Retomemos en el código del proyecto.

Código del proyecto

Vamos a modificar nuestro «launchSetting.json», eliminemos los profiles que tenemos actualmente, creemos uno nuevo para manejar nuestra instancia en docker que tendrá la siguiente estructura.

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:61567/",
      "sslPort": 44304
    }
  },
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "Docker": {
      "commandName": "Docker",
      "launchBrowser": true,
      "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
      "publishAllPorts": true,
      "useSSL": true,
      "httpPort": 7550,
      "sslPort": 7551,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "SENTRY_CONNECTION": "",
        "DATABASE_CONNECTION": "",
        "JWT_KEY": "",
        "JWT_EXPIRATION": "",
        "URL_BASE_PATH": "",
        "AWS_ACCESS_KEY": "",
        "AWS_SECRET_KEY": "",
        "EMAIL_NO_REPLY": "",
        "ASPNETCORE_URLS": "http://+:8080;https://+:443"
      }
    }
  }
}
Nueva configuración en launchSettings.json

Lo que hemos hecho es crear un nuevo perfil, le indicamos el comando que va a ejecutar «docker», que debe abrir un navegador, la url que se va a abrir (esta como vemos se obtiene de variables, no valores en duro), un poco de dolor de cabeza me llevo descubrir el uso de «httpPort» y «sslPort», dado que estos dos nos permiten indicarle que puertos queremos que use. Esto ultimo parece algo menor, pero nos sirve cuando mapeamos en nuestro postman o en el front el puerto del backend, y que este sea consistente en distintos ambientes, de otro modo, nos asignarán un puerto aleatorio y esto nos generar tener que estar cambiando nuestra configuración en el front. Finalmente, acá también podemos indicarle las variables de entorno que vamos a usar dentro de nuestro dockerfile.

En nuestro ide, selecionamos «Docker» y ejecutamos el proyecto. Si es la primera ves que se ejecuta, se va a crear la imagen en base a nuestro dockerfile, luego de esto, el ide automaticamente va a crear una instancia en base a esta imagen. Veamos que comando es el que usa para ejecutar este comando.

Primero se hace el build:

docker build -f "C:\Users\walbe\Development\Simplicity\simplicity-api\dockerfile-dev" --force-rm -t simplicityserviceapi:dev --target base  --label "com.microsoft.created-by=visual-studio" --label "com.microsoft.visual-studio.project-name=Simplicity.Service.Api" "C:\Users\walbe\Development\Simplicity"
Docker build

Luego realizamos se realiza el run de nuestra imagen recién creada:

docker run -dt -v "C:\Users\walbe\vsdbg\vs2017u5:/remote_debugger:rw" -v "C:\Users\walbe\Development\Simplicity\simplicity-api\Simplicity.Service.Api:/app" -v "C:\Users\walbe\Development\Simplicity:/src/" -v "C:\Users\walbe\AppData\Roaming\Microsoft\UserSecrets:/root/.microsoft/usersecrets:ro" -v "C:\Users\walbe\AppData\Roaming\ASP.NET\Https:/root/.aspnet/https:ro" -v "C:\Users\walbe\.nuget\packages\:/root/.nuget/fallbackpackages2" -v "C:\Program Files\dotnet\sdk\NuGetFallbackFolder:/root/.nuget/fallbackpackages" -e "DOTNET_USE_POLLING_FILE_WATCHER=1" -e "ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS=true" -e "ASPNETCORE_ENVIRONMENT=Development" -e "ASPNETCORE_HTTPS_PORT=7551" -e "NUGET_PACKAGES=/root/.nuget/fallbackpackages2" -e "NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages;/root/.nuget/fallbackpackages2" -p 7550:8080 -p 7551:443 -P --name Simplicity.Service.Api_1 --entrypoint tail simplicityserviceapi:dev -f /dev/null
Docker run
docker run -dt 
-v "C:\Users\walbe\vsdbg\vs2017u5:/remote_debugger:rw" 
-v "C:\Users\walbe\Development\WebApplication1:/app" 
-v "C:\Users\walbe\Development\WebApplication1:/src/" 
-v "C:\Users\walbe\AppData\Roaming\Microsoft\UserSecrets:/root/.microsoft/usersecrets:ro" 
-v "C:\Users\walbe\AppData\Roaming\ASP.NET\Https:/root/.aspnet/https:ro" 
-v "C:\Users\walbe\.nuget\packages\:/root/.nuget/fallbackpackages2" 
-v "C:\Program Files\dotnet\sdk\NuGetFallbackFolder:/root/.nuget/fallbackpackages" 
-e "DOTNET_USE_POLLING_FILE_WATCHER=1" 
-e "ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS=true" 
-e "ASPNETCORE_ENVIRONMENT=Development" 
-e "ASPNETCORE_URLS=https://+:443;http://+:80" 
-e "NUGET_PACKAGES=/root/.nuget/fallbackpackages2" 
-e "NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages;/root/.nuget/fallbackpackages2" 

Con esto, ya tendriamos todo funcionando correctamente, les dejo un video de todo este proceso, donde podemos ver mejor como nuestra aplicación corre desde nuestra imagen docker y los cambios que realizamos se actualizan directamente.

Espero que les sirva, en el siguiente post explicare como realizar el deploy de una aplicación en NetCore o Net 5.0 en AWS Lambda.

Deja un comentario

Dockerizando una aplicación Net 5.0 – Part. I

Hola, espero que los que me lean a esta fecha estén bien. Hace ya un tiempo que no publicaba nada, principalmente se debe a la falta de tiempo que he tenido por el trabajo y otras complicaciones que empezaron a surgir como consecuencia de la pandemia. En estos meses he aprendido algunas cosas nuevas relacionadas con AWS, Google Cloud y otras cosas que por ahí alguien puede encontrar interesante.

Retomo las publicaciones con este post, por que les comento que en general, en AWS y GCloud no es tan común que sus servicios soporten la última versión de net que nos ofrece microsoft, en mi caso tenía que migrar algunos servicios de AWS a Google Cloud esta semana que pasó y me di con la sorpresa que app engine no soporta Net 5.0. Sin embargo, soportan docker así que fue la oportunidad perfecta para practicar un poco mis skills en docker que hace un tiempito no he podido jugar con esta herramienta.

Quiero que este sea el primer post, por que en algunos de los que tengo pensado más adelante, quiero usar net 5.0, y de este modo sabrán como manejarse con esta versión del framework. Tomemos como ejemplo una aplicación que tiene especial cariño para mi porque yo la inicie como un proyecto personal hace ya un par de años y poco a poco he ido añadiendole funcionalidades y cosas. Sobre la aplicación, puedo decir que usa EF, Clean architecture y que no voy a poder compartirles el código. Sin embargo, este post es para centrarnos en docker y como implementarlo en un ambiente de desarrollo y de producción.

Dicho esto, empecemos, en nuestro proyecto base, vamos a crear un archivo que llamaremos «dockerfile-dev» y otro «dockerfile-prod». Veamos cual es la estructura de nuestro proyecto desde visual studio y desde el explorador de windows.

Explorador de windows
Visual studio

Para este ejemplo, hemos creado los archivos de correspondientes de docker en la raiz del proyecto. haciendo un build de nuestro proyecto para ver si todo esta funcionando correctamente.

Build del proyecto

Además, es importante revisar el archivo «launchSetting.json» dentro de la carpeta «Properties» como saben este archivo te permite configurar los distintos ambientes en los que quisieras levantar tu aplicación localmente, acá manejaremos las variables de entorno que necesitemos, entre otras configuraciones.

Archivo launchSetting.json

Notemos, que por defecto el visual estudio, nos esta proporcionando ya algunos perfiles. Para nuestro caso, queremos poder levantar nuestra aplicación localmente dentro de una imagen docker. Esto último es un salvavidas en algunos casos, al empezar algún proyecto, hay un tiempo que puede ser menor o mayor para poder levantar las aplicaciones que se tengan, si estuvieran dockerizadas, sólo tendrías que clonar el proyecto, instalar docker, correr tu «docker build .» dentro de la carpeta donde estar el dockerfile y listo, puede iniciar el trabajo. Tenganlo en cuenta por si más adelante les puede servir.

Empecemos trabajando con el archivo «dockerfile-prod». Añadamos las siguientes lineas y luego detallaremos un poco que es lo que esperamos conseguir con estas.

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 8080

Estamos tomando como base de nuestra futura imagen docker la imagen del repositorio oficial de microsoft para Net 5.0. el «AS base» es un alias que le ponemos en nuestro dockerfile a esta imagen. La sentencia «WORKDIR /app» le indica docker cual es nuestro directorio de trabajo y finalmente el «EXPOSE 8080» le dice que vamos a exponer el puerto 8080 dentro de nuestro container.

ENV ASPNETCORE_ENVIRONMENT=${_ASPNETCORE_ENVIRONMENT}
ENV AWS_ACCESS_KEY=${_AWS_ACCESS_KEY}
ENV AWS_SECRET_KEY=${_AWS_SECRET_KEY}
ENV DATABASE_CONNECTION=${_DATABASE_CONNECTION}
ENV EMAIL_NO_REPLY=${_EMAIL_NO_REPLY}
ENV JWT_EXPIRATION=${_JWT_EXPIRATION}
ENV JWT_KEY=${_JWT_KEY}
ENV SENTRY_CONNECTION=${_SENTRY_CONNECTION}
ENV ASPNETCORE_URLS=${_ASPNETCORE_URLS}
ENV URL_BASE_PATH=${_URL_BASE_PATH}

Esta sección es basicamente para declarar variables de entorno, podemos ver que en este caso las variables hacen referencia a otras variables, esto lo dejo asi por que si esto va a ir a producción, hay un pipeline en el que se reemplazarán estas variables por motivos de seguridad y buenas prácticas.

RUN apt-get update
RUN apt-get install -y apt-utils
RUN apt-get install -y libgdiplus
RUN apt-get install -y libc6-dev 
RUN ln -s /usr/lib/libgdiplus.so/usr/lib/gdiplus.dll

Esta parte la añado por que es necesario para esta aplicación, basicamente instala en nuestro entorno una librería necesaria para generar archivos excel que por defecto no están en linux. Sin embargo, nos sirve para ver como correr comandos dentro de una imagen docker.

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ./ ./Simplicity/
RUN dotnet restore Simplicity\\Simplicity.Service.Api\\Simplicity.Service.Api.csproj

Acá vamos a tomar como base para hacer el build el sdk de microsoft de 5.0, le indicarmos cual va a ser nuestro directorio de trabajo para hacer nuestro build, copiamos todo el codigo que tengamos en la carpeta en la que esta ubicada nuestro archivo dockerfile con el siguiente comando «COPY ./ ./Simplicity/» en una carpeta que vamos a crear «Simplicity». Finalmente, en esta sección vamos a ejecutar el comando «dotnet restore» que si recuerdan la imagen de los archivos esta dentro de «Simplicity.Service.Api\\Simplicity.Service.Api.csproj» en nuestro caso, como le estamos añadiendo una carpeta más, termina siendo «Simplicity\\Simplicity.Service.Api\\Simplicity.Service.Api.csproj».

COPY . .
WORKDIR /src/Simplicity
RUN dotnet build Simplicity.Service.Api\\Simplicity.Service.Api.csproj -c Release -o /app/build

En esta parte, nuevamente copiamos los archivos al directorio actual, le indicamos a docker cual va a ser nuestro nuevo directorio de trabajo con «/src/Simplicity» y finalmente realizamos nuestro build, recordemos en este punto que estamos haciendo un release y le añadimos que el directorio del output «-o /app/build», consideremos que ahora no es necesario ponerle «Simplicity» al inicio por que ya lo pusimos en el workdir.

FROM build AS publish
RUN dotnet publish Simplicity.Service.Api\\Simplicity.Service.Api.csproj -c Release -o /app/publish

Ya estamos cerca de terminar, en esta parte vamos a hacer el publish con el típico comando «dotnet publish», tambien tenemos que indicar el output. Y finalmente, ya llegamos a la última parte de la configuración de nuestro dockerfile.

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Simplicity.Service.Api.dll"]

En este punto, indicamos nuestro directorio de trabajo, copiamos lo que tenemos en nuestra carpeta publish en una carpeta publish que vamos a crear en la carpeta «app» y con el comando «ENTRYPOINT» le indicamos a docker que comando queremos que ejecute, para nuestro caso es «dotnet» al archivo «Simplicity.Service.Api.dll». Realicemos el build de nuestro dockerfile.

docker build -t simplicity-service --file dockerfile-prod .
Realización del build del dockerfile
Realización del build
Listado de imágenes de docker

Ahora ejecutemos esta imagen docker y probemos el resultado. Notemos en el siguiente comando que le indicamos que corra en modo deamon con el «-dt» y también es necesario que le indiquemos el mapeo de los puertos de nuestra imagen docker con la máquina en la que se está ejecutando, esto lo conseguimos con «-p 4750:8080».

docker run -dt -p 7551:8080 cb831
Ejecutamos nuestra imagen docker
Listado de imagenes docker ejecutandose

Finalmente, ejecutemos en nuestro browser el localhost para el puerto 7551.

Ejecución de nuestra aplicación dentro de la imagen docker

Les comparto el dockerfile que hemos desarrollado completo.

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 8080

ENV ASPNETCORE_ENVIRONMENT=${_ASPNETCORE_ENVIRONMENT}
ENV AWS_ACCESS_KEY=${_AWS_ACCESS_KEY}
ENV AWS_SECRET_KEY=${_AWS_SECRET_KEY}
ENV DATABASE_CONNECTION=${_DATABASE_CONNECTION}
ENV EMAIL_NO_REPLY=${_EMAIL_NO_REPLY}
ENV JWT_EXPIRATION=${_JWT_EXPIRATION}
ENV JWT_KEY=${_JWT_KEY}
ENV SENTRY_CONNECTION=${_SENTRY_CONNECTION}
ENV ASPNETCORE_URLS=${_ASPNETCORE_URLS}
ENV URL_BASE_PATH=${_URL_BASE_PATH}

RUN apt-get update
RUN apt-get install -y apt-utils
RUN apt-get install -y libgdiplus
RUN apt-get install -y libc6-dev 
RUN ln -s /usr/lib/libgdiplus.so/usr/lib/gdiplus.dll

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ./ ./Simplicity/
RUN dotnet restore Simplicity\\Simplicity.Service.Api\\Simplicity.Service.Api.csproj

COPY . .
WORKDIR /src/Simplicity
RUN dotnet build Simplicity.Service.Api\\Simplicity.Service.Api.csproj -c Release -o /app/build

FROM build AS publish
RUN dotnet publish Simplicity.Service.Api\\Simplicity.Service.Api.csproj -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Simplicity.Service.Api.dll"]

La parte del post, en el que explico como levantar la aplicación para nuestro ambiente de desarrollo, debuggear y realizar cambios de nuestro desarrollor que se actualicen directamente en nuestra imagen docker la realizare en un siguiente post. Espero que lo que he desarrollado hasta acá les sirva. En unos días publico la segunda parte.

Deja un comentario

Single Responsibility Principle

Ya hace un tiempo llevo trabajando en una empresa en el sector financiero, en la cual como toda empresa de su envergadura el stack de tecnologías y aplicaciones que poseen es diverso. Y como comprenderan eso tarde o temprano nos llevaba tener que trabajar con legacy code, y las serie de retos que estos involucran. Más de una vez, he tenido que atajar métodos de más de 1000 líneas de código que tenían como objetivo extornar algún monto, grabar log, etc. Esto se vuelve un dolor se cabeza al querer realizar pruebas unitarias para que sean parte del pipeline que se ejecuta en un ambiente de integración y despliegue continuo.

Este problema pone en evidencia la importancia de desarrollar teniendo siempre en mente la utilización de patrones de diseño. Esto me lleva a tratar en este post el primero de los principios de SOLID, siendo los principios que lo componen no los únicos que existen, pero si un marco de buenas prácticas en el desarrollo de software que nos permite asegurar un nivel de calidad en lo que desarrollamos.

Single Responsability Principle representa la «S» en SOLID, este principio entre otras cosas nos indica que los métodos que desarrollamos deben poseer una sola reponsabilidad y por lo tanto un sólo motivo para cambiar. Cuando nos referimos a responsabilidad, esta puede tomar la forma de las siguiente maneras:

Tipos de responsabilidad
  • Grabar en una base de datos, archivo de texto o cualquier otro medio de persistencia que se desee.
  • Realizar un log del método que se está consumiendo y/o también de los errores que puedan ocurrir.
  • Validar la información ingresada al método.
  • Lógica de negocio.

En lineas generales, debemos buscar que el código que desarrollemos tenga el menor acoplamiento posible y la única manera de conseguirlo es definiendo adecuadamente las responsabilidades. Una buena manera de probar que tanto acoplamiento posee nuestro código es cuantos test unitarios es necesario desarrollar para poder coberturarlo al 100%, es muy usual que si nuestro método desarrollado tiene muchas responsabilidades, el poder realizarle pruebas unitarios se va a volver algo extremadamente completo y tedioso. Para poder probar este patrón de diseño, vamos a reformular código teniendo en cuenta las consideraciones antes mencionadas.

La aplicación que vamos a refactorizar nos permite obtener el cálculo de ratio para la realización de la contratación de una poliza, tenemos un método de más de 100 lineas de código que posee distintas responsabilidades, tal como podemos ver en la siguiente imagen:

Código a refactorizar
  1. Grabación del log cuando inicia la llamada al método.
  2. Lectura de un archivo de texto y conversión a un objeto.
  3. Validación de la data obtenida.
  4. Lógica de negocio.
  5. Persistencia de la información obtenida en un archivo de texto.
  6. Grabación del log cuando finaliza la llamada al método.

Como podemos ver, es necesario reformular el método para que cumpla con los requerimientos descritos por este patrón. Esto lo realizaremos extrayendo ciertas responsabilidades del método principal. El primero nos permitirá realizar el grabado de logs:

Método para grabar logs

Luego, desarrollaremos un método para leer el archivo de texto, el cual debe poder retornar la entidad que le pasemos, por lo tanto es debe ser genérico.

Método para leer de un archivo de texto y retornar una entidad

Además, es necesario extraer el grabado de la persistencia en un método, este también debe aceptar como variables de entradas genericos.

Grabado de la persistencia

Finalmente, quedando el método del siguiente modo:

Método refactorizado

Notese que también he separado en un método distinto las validaciones de las polizas de vida.

Espero que les ayude a entender un poco más este principio, así mismo comentarles que este es el primer post de los principios que componen SOLID. Como siempre, dejo el video del desarrollo de esta post y el link al repositorio en github.

REPO: GITHUB

2 comentarios

Llamar un Mock Object dos veces con distintos resultados

Recientemente en el trabajo nos enfrentamos a un escenario cuando estabamos creando los test unitarios de un proyecto. Usando Moq como librería para el mocking queriamos que al llamar a un mismo método en el repositorio obtengamos distintos resultados por cada llamada. Ya en un post anterior he tratado un tema similiar a este con test unitarios, pero en los escenarios más simples es sólo necesario realizar un llamado obtener el resultado desarrollar cierta lógica de negocio con este resultado y evaluar los aserciones en el test.

Me parecio interesante desarrollar un poco más este tema y por eso hago este post, en el les explicaré un poco sobre las distintas opciones que tenemos para poder obtener distintos resultados al llamar a mock object con Moq, siendo una de estas opciones «SetupSequence» que es un overload al método setup que existe en esta librería, lo que podría generar un problema es que este overload es relativamente reciente, dado que recién se a incorporado como parte de la librería el 20 de junio del 2019. Y la otra opción es el uso de una estructura de datos como las queue o colas.

Para el desarrollo del post, me valdré del mismo proyecto que trabajamos en el post pasado, simplemente he añadido nuevos método en la capa de aplicación y en el repositorio y nuevos test unitarios.

APLICACION

Nuevo método en la capa de aplicación

Este método, como ven simplemente llama dos veces al mismo método del repositorio, con distintos parametros de entrada y retorna la concatenación de ambos resultados.

REPOSITORIO

Nuevo método en la capa de repositorio

Como ven, en el repositorio también se ha creado un nuevo método acá se está intentando simular que se llega a una base de datos, pero por fines prácticos se está devolviendo el nombre completo para todas las llamadas. Veamos los nuevos test unitarios desarrollados.

TEST

Uso de SetupSequence

Como pueden ver, con el uso de «SetupSequence» es posible realizar más de «Returns» los cuales, se van devolviendo conforme se van llamando, de manera tal que en el primer llamado desde la capa de aplicación, se devuelve «walberth», y en el segundo «angela» y finalmente, se retorna la concatenación.

Uso de queue (Colas)

La otra forma de conseguir el mismo resultado, es valiendonos del uso de colas, como saben esta estructura de datos es del tipo FIFO (first in, first out), de modo que cada ves que llamemos a nuetro moq, y su objeto de retorno es del tipo Queue va a devolver los objetos de este modo.

Espero que les sirva de ayuda como hemos podido resolver este escenario, tal ves con un caso tan común, pero cuando ocurre puede que nos tome un par de horas ver la forma de darle solución. Como siempre, dejo el video del desarrollo de esta solución y los links a github.

REPO: GITHUB

Saludos.

1 comentario

Unit Test y Mocking en NetCore

Cuando empezamos a desarrollar, sea el lenguaje que nos adopte incialmente no solemos tener en consideración los test unitarios y mucho menos el mocking. Si nos dedicamos a este mundo del desarrollo, tarde o temprano vamos a tener que llegar a saber para que sirven y como implementarlos, y de esto de lo que busco escribir un poco hoy.

TEST UNITARIOS

Los test unitarios son secciones de código de escribimos, cuyo objetivo es probar el funcionamiento y funcionalidad de otros métodos que hayamos desarrollado. Es importante tener claro el concepto de atomicidad al querer realizar un test unitarios, dado que el método a probar que se ha desarrollado debemos intentar que posea una única responsabilidad, de manera tal que nuestros test unitarios puedan ser atómico.

MOCKING

Cuando nos referimos a mocking, debemos tener claro que se tiene que llevar a cabo con una prueba unitaria, y lo que nos permite es abstraer el método que queremos probar de otras dependencías que no se están probando. De tal manera, podemos aislar nuestro test unitario y conseguir que sean realmente atómicos.

En el ejemplo que llevaremos a cabo, explicare como implementar un test unitario en Net Core 2.2, usando una librería para poder aplicar mocking en nuestro test. Para conseguir esto, usaremos XUnit para el test unitaio y Moq para el mocking. Es importante recalcar que existen otras opciones y que los conceptos que se aplicarán en la demo, puedes tranquilamente extrapolarse a alguna otra librería que se quiera usar.

Para empezara crear nuestro proyecto, se tiene la siguiente estructura en un proyecto demo:

Distribución del proyecto demo
Nuget utilizados en el test unitario

La arquitectura no es lo importante en esta demo, pero basicamente tenemos una capa de presentación (aplicación consola), una capa de aplicación y una de repositorio, en esta última se debe tener acceso a la base de datos. Las pruebas se dividirán de la siguiente manera:

  1. Para explicar los test unitarios se tiene un método en la capa de aplicación un método que recibe el nombre y apellido de una persona y lo regresa concatenado, teniendo varientes si se ha ingreso el nombre o el apellido en vacio.
  2. Probaremos el mocking simulando que estamos accediendo al repositorio y desde este a una base de datos en donde obtenemos un listado de nombres de personas.
Capa de aplicación
Capa de repositorio

En el proyecto de test unitarios, se crearon los siguientes test unitarios:

  • El primero prueba el escenario ideal del método que desarrollamos, notemos que en la primera parte de nuestro test, estamos preparando la información, liego consumimos el método desarrollado y finalmente evaluamos el resultado con los assert que nos brinda xunit.
  • En el segundo test unitario, probamos el escenario en el que a nuestro método no se le proporcione la información necesaria. Se sigue el mismo formato con la organización de los pasos para las pruebas (preparar la información, llamar al método y finalmente evaluar los resultados).
  • Para realizar la prueba del mocking, es importante realizar la inyección de dependencia del repositorio en nuestra aplicación, dado que lo que buscamos con el mocking es poder simular el acceso en este repositorio, Luego, en el test creamos el mock con la interfaz de nuestro repositorio, le indicamos en «Setup» con una expresión lambda el método que vamos a probar, y en este punto podríamos proporcionale la información que sea necesaria para consumir este método de ser esto necesario y finalmente le indicamos lo que va a retornar, para conseguir esto hemos debido crear un objeto del mismo tipo que retorna nuestro método y indicarlo dentro de «Returns». Finalmente, le pasamos nuestro mock al construtor de nuestra aplicación y llamamos al método a probar.
Inyección de dependencia
Mocking usando moq en NetCore

Finalmente, corremos nuestros test unitarios y tenemos el siguiente resultado:

Como podemos ver, lo importante del test unitario es poder realizar pruebas atómicas del código que escribimos y con esta finalidad quitarle la responsabilidad como obtener objetos que escapan a lo que buscamos probar (como conexiones a base de datos o otros métodos). Y con esta finalidad nos apoyamos del mocking, existen muchas cosas más que se pueden llevar a cabo con Xunit, e incluso más con Moq, ambas librerías usadas para esta demo, Más adelante realizaré más post en los cuales detallo un poco las características de ambas, espero que encuentren útil el post, les dejo el video y el link de github del proyecto realizado.

REPO: GITHUB

Saludos.

Deja un comentario