Esta entrada es para que puedas entrar de lleno al mundo de Docker, haré un ejemplo, pero no el típico donde levantas un contenedor por ejemplo .Net Core y correr una webpage que no hace uso de una base de datos, o correr un servidor de base de datos y lo compruebas con el SSMS haciendo consultas, esos ejemplos los haré en un próximo post donde explicaré los conceptos y ejemplos sencillitos.
Aquí abordaré la siguiente situación: Tienes una Web API real, con todo lo que puedas imaginar hecha en .Net Core y que hace uso de una base de datos SQL Server, cómo puedo contenerizar mi aplicación con Docker? Si te fijas bien un contenedor no basta, entonces cómo lo haces?
Hay dos formas, quizá más, pero las más usadas serían correr dos contenedores separados y configurar una red entre ellos para que se puedan comunicar, y la otra opción es hacer uso de un orquestador de ambientes como docker-compose, aquí aprenderás el método con docker-compose, empecemos entonces cracks!
Los requisitos para llevar a cabo este magnífico taller son:
- Tener una Web API application en .NET Core con base de datos (igual te daré una si no tienes)
- Docker desktop instalado
- Visual Studio Code instalado
- Conocimientos básicos en .Net Core
Recomendable, pero no imprescindible:
- Conocimientos básicos en docker
- Conocimientos básicos en Entity Framework
- Conocimientos básicos en Bases de datos SQL Server (aquí aprenderás SQL)
No te asustes si no cumples con todos los requisitos aquí planteados, igual vas a ir aprendiendo sobre la marcha, ya que todo lo que haremos aquí se explica a sí mismo (self-explanatory diría un gringo, en español no encuentro la palabra exacta 😁).
Conceptos básicos
Lo primero que debes saber es qué es Docker, pues bien, es una tecnología de código abierto cuya finalidad es automatizar el despliegue de aplicaciones dentro de sus propios entornos a los cuales llama contenedores, cada contenedor por ejemplo es un entorno configurable que puede proporcionarnos un sistema operativo o un servidor para una determinada finalidad.
En un ejemplo de la vida real, puedo tener un contenedor el cual sería mi servidor de base de datos SQL Server, sin tener que instalarlo en mi máquina real.
Esto quiere decir que Docker viene a reemplazar lo que serían las máquinas virtuales, y es más rápido que estas, además que puede automatizar tareas como vamos a ver a continuación.
Como puedes observar en la imagen un Contenedor hace uso de el sistema operativo del host o de tu máquina y sobre él arma uno o varios ambientes cada uno con sus propias librerías que dan compatibilidad y la posibilidad a tu aplicación de correr
Docker compose simplifica el uso de dockerfiles, estableciendo toda la "orquestación" de los ambientes necesarios para que nuestra aplicación funcione, usando la línea de comandos de docker según nosotros la configuremos y queramos en un archivo, el cual se irá ejecutando de forma secuencial.
Voy a escribir un artículo escribiendo precisamente los conceptos y tips básicos y más importantes acerca de Docker, en cuanto lo tenga listo lo pondré aquí. Lo que haremos hoy será un taller para ponernos manos a la obra con un caso real de una API hecha en .Net Core y su base de datos SQL Server, ya sabes que este blog es pragmático y que vamos a los casos reales, a la praxis, así que arrancamoooos. 🔥
Estableciendo ambiente
Vamos a trabajar con una API que está disponible en mi github desde este link: https://github.com/GeaSmart/LibraryNetCoreAPI
Podría ser cualquier otra, inclusive te animo a que practiques con tu propia API, si quieres saber cómo hacer una API, entra a esta entrada. Pa todo hay solución crack 😉.
Abriendo proyecto con VS Code
Lo primero será clonar el proyecto en alguna carpeta en mi caso lo haré en la carpeta DockerPractice
Clic derecho abrir en VS Code, o si no te aparece la opción abrir la carpeta desde VSC.
Una vez en VS Code abres una consola y clonas el proyecto ejecutando el comando
git clone https://github.com/GeaSmart/LibraryNetCoreAPI.git
Esta es una solución, es decir es un conjunto de proyectos agruados, en este caso son dos: LibraryNetCoreAPI y LibraryNetCoreAPI.Tests, el primero es el API que usaremos. El segundo es un proyecto de tests unitarios, así que podemos borrarlo, también podemos borrar los archivos que están fuera del proyecto:
En la consola debemos ubicarnos en el directorio raíz del proyecto, es decir la carpeta LibraryNetCoreAPI dentro de la que tiene el mismo nombre, para esto entonces ejecutamos:
cd LibraryNetCoreAPI/LibraryNetCoreAPI
Entonces tenemos masomenos esto (Ojo con la ruta del explorador y tambien con la ruta de la consola):
Probando la aplicacion
Sólo para probar, corremos la aplicación, esto lo hacemos ejecutando:
dotnet run
Genial!, si corre. 😎 Obviamente aún no tiene conexión aún a base de datos, pero lo resolveremos más adelante en docker.
Ahora ya podemos detener la ejecución desde la consola pulsando
CTRL + C
Configurar migración inicial
Crear la carpeta Models y en ella, crear la clase PrepareDb.cs
Lo que hará esta clase será inicializar la base de datos ejecutando una migración.
El proyecto hace uso de una base de datos SQL Server mediante la técnica Entity Framework Code First, por eso hace uso de migraciones. Mas info aquí.
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
namespace LibraryNetCoreAPI.Models
{
public static class PrepareDb
{
public static void Population(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.CreateScope()){
SeedData(serviceScope.ServiceProvider.GetService<ApplicationDBContext>());
}
}
public static void SeedData(ApplicationDBContext context){
System.Console.WriteLine("Applying initial migration..."); //para informarnos desde la consola
context.Database.Migrate();
System.Console.WriteLine("Initial migration (database) done!");
}
}
}
Ahora lo que sigue es llamar a la clase en el método Configure de la clase Startup
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//Initial migration call
PrepareDb.Population(app);
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
//Configuracion de middleware SWAGGER
app.UseSwagger();
app.UseSwaggerUI(config =>
{
config.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
config.SwaggerEndpoint("/swagger/v2/swagger.json", "v2");
config.RoutePrefix = ""; //para evitar problemas con la ruta en la que se lanza ...
}
);
}
Esto hará que al momento de iniciar la aplicación se ejecute una migración creando así la base de datos.
En producción deberías al menos agregar una lógica antes de ejecutar una migración al iniciar la aplicación ya que borraría la data que tenga tu base de datos, lo cual no deseas porque te quedarías sin chamba 🤣.
Finalmente actualiza la cadena de conexión tanto en appsettings.json y appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"connectionStrings": {
"defaultConnection": "Server=sql-server,1433;Initial Catalog=LibraryAPI;User ID=sa;Password=Aa123456!"
},
"JWTKey": "DHJ2416U534LV74CJ7HL54CN7LLXU3NH7L5LN478C63G4507K9890231NLHUICGLUGXPQWAPNPAQ737Y3A77A808LN0U681432",
"app-author": "Gerson Azabache M PRODUCTION",
"test": "valor desde appsettings.json",
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=c20a3c24-8569-450b-a300-768113e9020b;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/"
}
}
El el servidor le ponemos coma y un puerto ya que así lo vamos a configurar en docker después, de igual forma el nombre del servidor le puse sql-server ya que así llamaré el servicio de base de datos en docker más adelante, lo mismo con la contraseña del usuario sa, le puedes poner una contraseña cualquiera pero segura. Podrías cambiar estos parámetros, sólo ten en cuenta que tendrías que cambiarlo más adelante el docker también.
Orquestando los contenedores
Ya tenemos el proyecto preparado, lo que sigue es orquestar los ambientes para desplegar la aplicación mediante docker.
En el directorio raíz tienes que crear 3 archivos: Dockerfile, .dockerignore y docker-compose.yaml
Ten en cuenta: Dockerfile no tiene extensión y .dockerignore no tiene nombre, sólo extensión.
Para añadir los archivos en el directorio correcto, puedes hacerlo así:
La idea es que queden en la misma carpeta que el archivo con extensión .csproj
Los archivos tendrán el siguiente contenido:
Dockerfile
#Get .Net Core image
FROM mcr.microsoft.com/dotnet/sdk:5.0 as build
WORKDIR /app
#Copying csproj and restore
COPY *.csproj ./
RUN dotnet restore
#Copying the rest of the files and build
COPY . ./
RUN dotnet publish -c Release -o out
#Runtime
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build /app/out .
EXPOSE 80
ENTRYPOINT [ "dotnet" , "LibraryNetCoreAPI.dll" ]
.dockerignore
**/*.md
**/tests
#directorios comúnmente excluídos en proyectos .net, web o consola
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
docker-compose.yaml
version: '3'
services:
sql-server:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: "Aa123456!"
ports:
- "1433:1433"
api:
build: .
ports:
- "8080:80"
He resaltado los campos que es posible que debas personalizar: En el Dockerinfo pon en el ENTRYPOINT la dll de tu proyecto (el nombre de la dll es el mismo que el csproj), y en el docker-compose el nombre del servicio (en este caso sql-server) es el mismo que el servidor de la cadena de conexión del proyecto. La contraseña del usuario sa y el puerto debe ser el que tenga tu cadena de conexión de tu proyecto. Tenlo en cuenta 😉
Ahora tienes que abrir tu aplicación Docker Desktop.
Listo, ahora ya tenemos todos los archivos necesarios para que nuestra aplicación pueda correr en un contenedor docker que a su vez tendrá dos containers uno para el api y otro para la base de datos, cada uno con su respectiva imagen, esa es la belleza de usar docker compose.
Como te habrás dado cuenta, el uso de docker-compose permite automatizar tareas.
Genial entonces, sigamos!
Desplegando aplicación
Lo último que toca es hacer un build y luego ejecutar la orquestación, eso lo hacemos ejecutando los siguientes comandos:
docker-compose build
docker-compose up
Ojo con la consola, por ahí nota el "Initial migration (database) done!" son los mensajes que pusimos en el método SeedData, justamente para que nos oriente en consola, bien ahí!
Hasta ahora todo bien, yeah!
Probando la aplicación
En Docker desktop podemos ver cómo ya están funcionando nuestras imágenes (.Net core y SQL Server) y también nuestro set de containers:
En SSMS podemos comprobar que se nos ha creado nuestro servidor de base de datos y nos hemos podido conectar a él, con el puerto correspondiente y allí está la base de datos con todo y migraciones! genial! 😊
Ahora probaremos la aplicación, nos dirigimos a la dirección con el puerto especificado y genial, crack ya lo tienes!
Probaremos registrando un nuevo usuario
Perfecto hemos recibido una respuesta y un token, como era lo esperado para este API! 😀😀😀
Y si ejecutas una consulta hacia la base de datos, tienes al usuario recien creado! 🔥
Eso es todo crack! ahora toca que lo practiques!
Todo este proyecto con docker lo he subido a mi Github puedes checarlo aquí: https://github.com/GeaSmart/WebAPISQL-Docker
Si esta entrada te ha encantado crack, tanto como a mi, entonces que esperas, comparte! 😉💪
Hola. Excelente explicación. Tengo una consulta: ¿Es posible que una API dockerizada es decir dentro de un contenedor se conecte a una base de datos de un motos MSQL externo? Es decir que se conecte a un motor que no está en un contendor.
Gracias!
Hola Raúl, gracias por visitarme. Sobre tu consulta, sí es posible conectar aplicaciones para que se comuniquen tanto dentro de un container, entre dos containers o de un container hacia un servidor externo, esto sería mediante las networks de docker y la base de datos proveedora de los datos debería tener una IP a la cual hacer las peticiones. Saludos.