¿Qué demonios es Docker y Docker-Compose? y cómo Dockerizar Dotnet Core WebApi y SQL Server en un ambiente de desarrollo ideal
Tags: docker, dotnet, containers, csharp
Published: 2020-06-15 00:55:12

A menudo que avanzamos en esta extensa curva de aprendizaje en el mundo de la ingeniería y desarrollo de software, nos encontramos con problemas que debemos resolver, algunos muy típicos y otros no tanto. En este post me enfocaré en abordar algunos problemas relacionados al ciclo de vida del desarrollo del software como:
- ¿Por qué nadie usa la versión estable de DotNet? - **El arquitecto**
- En mi máquina funcionaba. - **El desarrollador**
- Copie los archivos a donde dijiste, si no funciona es tu problema - **Operaciones**

Vamos a centrarnos en una de las tantas tecnologías de moda en la industria del software llamada Docker.

## Que abordaremos en este post
>- ¿Qué demonios es Docker?
>- Máquinas Virtuales
>- Contenedores de Docker
>- Docker-Compose 
>- Utilizar Docker-Compose para crear un ambiente Multi-Contenedor con ASP.NET Core y SQL Server

## Requisitos
>- Docker-Desktop
>- Visual Studio 2019

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/74jl7m7yta7v91ivkooq.jpg)
* * *
# Docker
Una plataforma abierta para desarrollar, enviar y ejecutar aplicaciones que te permite empaquetar tu proyecto con todas sus dependencias únicamente necesarias en un simple binario, de forma totalmente aislada al resto de aplicaciones que puedan convivir en el mismo host. 
Docker es muy útil para desarrolladores y administradores de sistemas para compilar, ejecutar y compartir aplicaciones en contenedores. 
Los contenedores no son nuevos el concepto data desde 1979 y aunque Docker como tal salió en 2013 hoy en día sigue siendo tendencia y su uso para implementar fácilmente aplicaciones es totalmente genial.
El concepto de utilizar contenedores es cada vez más popular debido a que nos ofrecen múltiples beneficios y características como las siguientes:

- **Flexibles:** Incluso las aplicaciones más complejas se pueden contenerizar.

- **Ligeros:** Los contenedores aprovechan y comparten el Kernel del host, haciéndolos mucho más eficientes en términos de recursos del sistema que las máquinas virtuales.

- **Portables:** Puedes compilar localmente, implementar en la nube y ejecutar en cualquier lugar.

- **Acoplados holgadamente:** Los contenedores son altamente autosuficientes y encapsulados, lo que te permite reemplazar o actualizar uno sin interrumpir a otros.

- **Escalables:** Puedes aumentar y distribuir automáticamente réplicas de contenedor en un centro de datos.

- **Seguros:** Los contenedores aplican restricciones y aislamientos agresivos a los procesos sin ninguna configuración necesaria por parte del usuario.

> **Nota:** Los contenedores y las máquinas virtuales a menudo son muy comparados debido a que son tecnologías que nos ofrecen un mecanismo de virtualización, aunque nos pueden servir para lo mismo presentan algunas notables diferencias, analicemos cada uno por separado y al final tendremos una conclusión sobre un contenedor versus una máquina virtual.

## *Máquinas Virtuales (VM)*
Una VM nos provee de un sistema operativo completo funcionando de manera aislada sobre otro sistema operativo completo.
La tecnología de VMs permite compartir hardware entre varios sistemas operativos al mismo tiempo.
A continuación un esquema simplificado de arquitectura de VMs:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qaoc2bmxqd3bmi41limo.PNG)
>  **Léelo de abajo a arriba**

Es lógico pensar que tras bambalinas siempre tiene que existir infraestructura que lo soporte todo, esta puede ser tu computadora personal para desarrollo, un servidor onpremise en tu data center o algún servicio de infraestructura en la nube IaaS que ofrecen distintos proveedores como Azure, AWS, Google Cloud, Digital Ocean, etc, para esto existen múltiples opciones pero al final se trata siempre de "infraestructura": máquinas físicas sobre las que se ejecutan las VMs.
No obstante para que las máquinas virtuales puedan ejecutarse es necesario otro componente importante que está por encima del S.O. llamado hipervisor, un software especializado para exponer los recursos de hardware del host, de modo que puedan ser utilizados por otros sistemas operativos, obviamente esto incluye CPUs, memoria, tarjeta de red, espacio de almacenamiento en disco y el resto del hardware. 
En conclusión la tarea del hypervisor es engañar a un sistema operativo convencional para que crea que se está ejecutando sobre una máquina física. 
Los hipervisores que probablemente conozcas porque son muy comunes y son los que personalmente conozco:
  - VirtualBox
  - Hyper-V 
  - VMWare

* * *

## *Contenedores de Docker*
Los contenedores por su parte tienen un enfoque totalmente distinto al de las VMs. Si bien es cierto tratan también de aislar aplicaciones y de generar un entorno replicable, portable y estable para la ejecución de procesos, esto gracias a los namespaces y control groups de Linux.
Los contenedores en lugar de ejecutar un sistema operativo completo lo que hacen es compartir los recursos del propio sistema operativo "host" sobre el que se ejecutan.

A continuación el esquema equivalente al de VMs, para el caso de contenedores:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pj2klcqff58o8525jyel.PNG)

Visualmente podemos notar que solo desaparece la capa del sistema operativo huésped (Guest Host), y se sustituye el hipervisor por el motor de Docker, sin embargo las diferencias son grandes.
Docker se encarga de ejecutar y gestionar los contenedores, pero en lugar de exponer los diferentes recursos de hardware del host, lo que hace es compartir entre todos los contenedores ese hardware, optimizando su uso y eliminando la necesidad de tener n cantidad de sistemas operativos separados para conseguir el aislamiento y garantizar el mismo comportamiento de las aplicaciones contenerizadas en diferentes ambientes.

Otro tema muy importante sobre Docker es su funcionamiento con base en imágenes que se pueden reutilizar entre varias aplicaciones y/o contenedores. Cada una de esas imágenes se puede asimilar como una "capa" que se puede superponer a otras para formar un sistema de archivos que combina todas las capas necesarias.
 
> **Ejemplo:** Una capa puede contener las bibliotecas o el runtime que necesitamos utilizar, para este ejemplo será el SDK de Dotnet Core, otra con bibliotecas determinadas de las que hace uso nuestra aplicación por ejemplo Entity Framework, Serilog, JWT, etc, y otra capa final con el código fuente de nuestra aplicación. La combinación de todas estas capas nos da como resultado una nueva imagen, única de nuestra aplicación, y con esta nueva y única imagen podemos instanciar y crear uno o varios contenedores.
***
> **Nota Importante:** Las VMs y los contenedores son tecnologías que persiguen un fin similar, pero con distintos enfoques.

# Docker-Compose
Es una herramienta para definir y ejecutar aplicaciones Docker multicontenedor que permite simplificar el uso de Docker a partir de archivos YAML, de está forma es mas sencillo crear contendores que se relacionen entre sí, conectarlos, habilitar puertos, volumenes, etc. Nos permite lanzar un solo comando para crear e iniciar todos los servicios desde su configuración(YAML), esto significa que puedes crear diferentes contenedores y al mismo tiempo diferentes servicios en cada contenedor, integrarlos a un volumen común e iniciarlos y/o apagarlos, etc. Este es un componente fundamental para poder construir aplicaciones y microservicios.
Docker-Compose funciona en todos los entornos:  production, staging, development, testing, así como flujos de trabajo basados en Continuous Integration(CI).
Si quieres conocer más acerca de Docker-Compose ve a la documentación oficial https://docs.docker.com/compose/#features

> **Nota:** En este Post utilizaremos docker-compose para lanzar dos contenedores. Uno que contenga todas las dependencias para empaquetar una solución ASP.NET Core WebApi y otro contenedor que contenga las dependencias necesarias para nuestra Base de Datos SQL Server. Mediante docker-compose haremos el enlace de comunicación entre ambos contenedores.

Estoy seguro que hasta este punto ya entendemos lo básico de cómo funcionan ambas tecnologías.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4ibvujpmazv4g24t3bkq.jpg)
* * *

Vamos a centrarnos en crear un entorno ideal de desarrollo con tecnologías Microsoft (Dotnet Core y SQL Server) sobre Docker aprovechando la facilidad que nos proporciona docker-compose sobre el Engine de Docker para realizar tareas programáticamente.

## Utilizando Docker-Compose para crear un ambiente Multi-Contenedor con ASP.NET Core y SQL Server
Inicialmente vamos a necesitar una solución ASP.NET Core que vamos a empaquetar en un contenedor de docker, utilizaremos la siguiente solución perteneciente a un Post anterior.
{% link https://dev.to/ebarrioscode/base-de-datos-en-memoria-con-asp-net-core-3-0-mvc-entity-framework-core-3950 %}
> **Nota:** Esta solución utiliza una base de datos SQL Server In Memory, en este caso cambiaremos esa característica y vamos a montar SQL Server en un Contenedor de Docker.

Una vez que hemos abierto nuestra solución WebApi en Visual Studio 2019, vamos a situarnos en el proyecto WebApi y vamos a agregar la Compatibilidad con el Orquestador de Contenedores.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ptuk6viqv8ucxmthriep.png)
* * *

Seleccionamos Docker-Compose
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/5ryu6y5b52e6wuinqb7q.PNG)
* * *

Tras pulsar el botón Aceptar empezará el proceso de creación del proyecto Docker-Compose en nuestra solución de Visual Studio, se agregará un archivo llamado Dockerfile en el proyecto WebApi y en segundo plano se hará pull de las imágenes de Docker necesarias para empaquetar nuestra solución.

Examinaremos el nuevo proyecto Docker-Compose recientemente creado.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pccqmb7f6z0ak2h757ri.PNG)
* * *
> **Nota:** Este proyecto consta de 3 archivos.

**.dockerignore** - Se encarga de ignorar configuraciones o compilados y files que utiliza o crea Docker en tiempo de compilación y/o ejecución que son propios de cada ambiente y que no son necesarios al momento de hacer push al registry de Docker para publicar una imagen, el .dockerignore es similar al uso del .gitignore de repositorios Git.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/si0a2myw9kav9b7gnfp7.PNG)
* * *

**docker-compose.yml** - Es un archivo YAML donde definimos los servicios, redes, volumes y todo lo necesario para crear un ambiente con base en contenedores. Se encarga de buscar instrucciones para ejecutarlas, estas instrucciones contienen toda la configuración que será aplicada a cada contenedor iniciado por ese servicio. 
Las instrucciones del docker-compose.yml son equivalentes a pasarle parámetros al comando **docker run**, de la misma manera las definiciones de las redes y volumes serían semejantes a los comandos **docker network create** y **docker volume create**.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ewpmnjd1n1vi3h0m3q5j.PNG)
* * *

A continuación una descripción simple de las principales instrucciones contenidas en el archivo docker-compose.yml

- **version:** Los archivos docker-compose.yml son versionados y es importante indicar la versión a utilizar.

- **services:** Indica los servicios a utilizar, podemos anidar n cantidad de servicios a esta instrucción, cada servicio puede tener cualquier nombre pero como buena práctica lo mejor es dar nombres explícitos. Para nuestro ejemplo utilizaremos dos servicios que representan un WebApi de dotnet core y una base de datos Sql Server. 

- **dockerizingwebapi:** Es el nombre de nuestro primer servicio y hace referencia a nuestro WebApi.

- **container_name:** Permite establecer un nombre para un contenedor al que hace referencia el servicio.

- **image:** Permite tagear la imagen que se creará para instanciar el contenedor en el que estará montado el servicio. Es equivalente a utilizar el comando **docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]**.

- **build:** Se utilizar para indicar el contexto e indicar la ruta del archivo Dockerfile para construir el contenedor del servicio. Posteriormente veremos que es un Dockerfile.      

- **depends_on:** Se utiliza para establecer la dependencia y comunicación con otros servicios, en este caso el servicio dockerizingwebapi necesita comunicarse con el servicio database que se encuentra definido posteriormente.

- **database:** Es el nombre de nuestro segundo servicio y hace referencia a la base de datos sql server.    

- **ports:** Se utiliza para exponer los puertos necesarios desde el contenedor.

- **environment:** Permite establecer variables de entorno durante el ciclo de vida del contenedor.
* * *

**docker-compose.override.yml** - Expone configuraciones específicas para el entorno de desarrollo. Crea algunas variables de entorno, expone algunos puertos del host y monta algunos volumes.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/weti2kwwnc96cber49ql.PNG)
* * *

> **Nota:** Docker-Compose nos permite utilizar múltiples archivos de configuración y personalizar nuestra aplicación con base en Compose para diferentes ambientes o flujos de trabajo con el fin de compartir configuración común.
* * *
> **Ejemplo:** Podríamos crear un archivo **docker-compose.prod.yml** y que exponga configuraciones específicas para un entorno productivo. Lo mismo podríamos hacer con otros ambientes como Staging, Testing, Production, etc. Con esto podemos aprovechar extender esta funcionalidad para replicar entornos y que cada entorno contenga configuraciones propias.

## Dockerfile
Para crear contenedores de Docker podemos utilizar varios comandos, descargar imágenes del Registry de Docker con **docker pull** seguidamente utilizar **docker run**, y múltiples acciones que podemos realizar con más comandos de Docker. 
Un Dockerfile es un archivo de texto plano que contiene todos los comandos de Docker que podríamos ejecutar desde la línea de comandos para crear una imagen y seguidamente instanciar un contenedor con base en esa imagen. Docker nos permite construir imágenes automáticamente ejecutando las instrucciones definidas en un Dockerfile.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/g0tpoq9z5g7b4t924qca.PNG)
* * *
> **Nota:** El Dockerfile que crea la plantilla de Visual Studio por defecto viene preparado con multi-stage-builds para poder declarar múltiples **FROM** en el archivo Dockerfile y garantizar una adecuada optimización de nuestra imagen final de Docker, de esta manera tendremos una imagen limpia con base en otras imágenes de Docker.

Podemos observar que en segundo plano se está haciendo un pull de las imágenes definidas en las instrucciones **FROM** del Dockerfile, esto hace referencia a dos imágenes de los repositorios de Microsoft. 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/tjot8ln43bin4p05pazp.PNG)
* * *

Ahora crearemos las migraciones para poder crear la estructura de la base de datos dentro del contenedor e inicializar las tablas de la base de datos, para esto tendremos que ejecutar migraciones como lo hacemos normalmente en la consola de administración de paquetes nuget.
Seleccionamos el proyecto WebApi como proyecto de inicio y ejecutamos el comando **Add-Migration** [nombre_de_la_migración]. Lo anterior creará el Directorio Migrations y las migraciones representadas como clases C#.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jcg525ylbbv5ayctgr55.PNG)
* * *

Crearemos una clase static y la nombraremos DbInitializer.cs. Esta clase tendrá un método static que recibe como parámetro el contexto de datos que utilizaremos para crear datos iniciales posteriormente a crear las tablas.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/3ed5shaeulgxkd1hvd8l.PNG)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zraxeiug6unbzv7np2cc.PNG)
* * *
> **Nota:** Debemos volver a seleccionar como proyecto de inicio Docker-Compose ya que en este caso no haremos un **Update-Database** en la consola de administración de paquetes de Nuget de VS, a continuación definiremos un método en la clase Startup.cs que haga esa tarea automáticamente.

Llamaremos a este método UpdateDatabase, lo utilizaremos para aplicar las migraciones en el contenedor y crear datos de inicio en las tablas correspondientes a nuestros modelos en la base de datos dentro del contenedor.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/kq20et07q2d5jzotilyu.PNG)
* * *

Invocaremos al método anterior en el método Configure() de la misma clase Startup.cs.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/iat4kyxb6fzk8e1vzrg6.PNG)
* * *

Ahora modificaremos la implementación de la inyección de la dependencia de nuestro contexto de datos Entity Framework Core. Reemplazaremos el método UseInMemmoryDatabase por el método UseSqlServer, le pasaremos la cadena de conexión definida en el archivo appsettings.json y haremos un Replace pasando el valor de un EndPoint definido como variable de entorno del contenedor en el archivo docker-compose.override.yml.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w4qpb30wnx6pn2dvxcew.PNG)
* * *

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/q3wv9o1e655atsg2527e.PNG)
* * *

Todo está listo es hora de probar, vamos a seleccionar nuestro proyecto Docker-Compose como proyecto de inicio, seguidamente ejecutamos la solución desde Visual Studio. Notaremos que Visual Studio se encarga de lanzar los comandos de Docker y Docker-Compose para utilizar todas las configuraciones.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/og5qln0vccrar4ovywqs.PNG)
* * *

Ahora iremos a Postman y lanzamos una petición HTTP Get a la url api/Albumes.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/md44penev96dj3tiu05x.PNG)
* * *
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zwaqgzujh6co9d4mm63m.jpg)
* * *

Podemos aprovechar las herramientas que nos provee Visual Studio 2019 para obtener información sobre nuestros contenedores.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7al4nhrkgvulvyetjovw.PNG)
> Como puedes notar en contenedores de Solución tenemos dos contenedores, el primero que contiene un WebApi y el siguiente una Base de datos Sql Server.
* * *

También podemos obtener información de nuestros contenedores que están en ejecución lanzando el comando **docker ps** en cualquier terminal, utilizaré PowerShell.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z2snw185ozuc9arz0xwo.PNG)
> **Nota:** Con docker ps obtendremos información de contenedores en ejecución, información como el ID de cada Contenedor, la imagen que utilizó cada contenedor para crearse, los puertos que expone cada contendor.
* * *

**Conclusiones**
- Contenedores VS Máquinas Virtuales: Sus diferencias son enormes, tanto en la teoría como en la práctica y es que los contenedores comparten el mismo sistema operativo con el host, conteniendo solo lo estrictamente necesario para ejecutar una Aplicación, Base de Datos, Web Service, un Sistema Operativo, lo que sea que tu quieras empaquetar dentro de un contenedor, mientras que las máquinas virtuales incorporan un sistema operativo completo. Esto afecta directamente y enormemente el rendimiento: al haber menos capas entre el metal (infraestructura) y la aplicación; se ganan milisegundos de latencia preciada. El tiempo de arranque de un contenedor Docker es increíblemente instantáneo.

- Los contenedores permiten desplegar aplicaciones rápidamente, arrancarlas, detenerlas más rápido y aprovechar mejor los recursos de hardware. 

- En cuanto a la utilización de Docker con tecnologías .NET, Visual Studio nos ayuda a ser ágiles en muchos aspectos. Todo lo realizado en este Post puedes hacerlo sin Visual Studio, lanzando los comandos de Docker en cualquier terminal como PowerShell, CMD, Git bash, etc y utilizando cualquier editor de código para modificar tus archivos.

- La función principal de Docker-Compose es la creación de infraestructura basada en microservicios, es decir, los contenedores y los vínculos entre ellos. No obstante esta herramienta es capaz de hacer mucho más, como: 

*Crear Imágenes, si está definido en el Dockerfile*
```
docker-compose build
```

*Escalar contenedores de forma sencilla*
```
docker-compose scale SERVICE=5
```

*Se pueden volver a ejecutar los contenedores que se han detenido*
```
docker-compose up --no-recreate
```

- Docker nos ayuda a crear imágenes y contenedores para nuestras aplicaciones y poder ser más eficientes.

- En fin hemos visto y definido qué es y para que nos ayuda Docker y Docker-Compose, la importancia que tiene hoy en día para la implementación de infraestructuras simples que pueden ser para ambientes de desarrollo u otras más complejas que necesitan de alguna forma rápida poder crecer y ser escalables sin que se tengan que hacer tantos procesos manuales.
* * *

**Link al respositorio en GitHub**
{% github EbarriosCode/Docker-Compose-DevEnvironment-WebApi_3.1-with-SQLServer %}

**Referencias**
- https://docs.docker.com/get-started/overview/
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/about/containers-vs-vm
- https://docs.docker.com/compose/extends/
- https://docs.docker.com/compose/aspnet-mssql-compose/