Diseñando y Desarrollando Aplicaciones .NET Multi-Contenedor y Basadas en Microservicios

Visión

Desarrollar aplicaciones contenerizadas de microservicios significa que está creando aplicaciones multi-contenedor. Sin embargo, una aplicación de multi-contenedores también podría ser más simple, por ejemplo, una aplicación de tres capas y se podría construir sin usar una arquitectura de microservicios.

Anteriormente planteamos la pregunta "¿Es necesario usar Docker para construir con una arquitectura de microservicio?" La respuesta es un claro no. Docker es un habilitador y puede proporcionar beneficios significativos, pero los contenedores y Docker no son un requisito obligatorio para los microservicios. Como ejemplo, puede crear una aplicación basada en microservicios con o sin Docker cuando utilice Azure Service Fabric, que soporta microservicios que se ejecutan como procesos simples o como contenedores Docker.

Sin embargo, si sabe cómo diseñar y desarrollar una aplicación basada en microservicios, basada también en contenedores Docker, podrá diseñar y desarrollar cualquier otro modelo de aplicación más simple. Por ejemplo, puede diseñar una aplicación de tres capas que también requiera un enfoque de multi-contenedor. Por eso, y debido a que las arquitecturas de microservicio son una tendencia importante dentro del mundo de los contenedores, esta sección se centra en la implementación de una arquitectura de microservicio que utiliza contenedores Docker.

Diseñando una aplicación orientada a microservicios

Esta sección se enfoca en desarrollar una aplicación empresarial hipotética del lado del servidor.

Especificaciones de la aplicación

La aplicación hipotética maneja las peticiones ejecutando la lógica de negocios, accediendo a bases de datos y luego devolviendo respuestas HTML, JSON o XML. Diremos que la aplicación debe soportar una variedad de clientes, incluidos los navegadores de escritorio que ejecutan Single Page Applications (SPA), aplicaciones web tradicionales, aplicaciones web móviles y aplicaciones móviles nativas. La aplicación también podría exponer una API para que la consumieran terceros. También debería ser capaz de integrar sus microservicios o aplicaciones externas de forma asíncrona, por lo que ese enfoque ayudará a la resiliencia de los microservicios en el caso de fallas parciales.

La aplicación tendrá estos tipos de componentes:

La aplicación deberá tener alta escalabilidad y permitir que los subsistemas verticales escalen de forma autónoma, porque ciertos subsistemas requerirán más escalabilidad que otros.

Se debe poder desplegar la aplicación en múltiples entornos de infraestructura (múltiples nubes públicas y locales) y, preferiblemente, debe ser multiplataforma, capaz de pasar de Linux a Windows (o viceversa) fácilmente.

Contexto del equipo de desarrollo

También suponemos lo siguiente en cuanto al proceso de desarrollo

Seleccionando una arquitectura

¿Cuál debería ser la arquitectura para desplegar de la aplicación? Las especificaciones para la aplicación, junto con el contexto de desarrollo, sugieren encarecidamente que debe diseñar la aplicación descomponiéndola en subsistemas autónomos, en forma de microservicios y contenedores colaborativos, donde cada microservicio sea un contenedor.

En este enfoque, cada servicio (contenedor) implementa un conjunto de funciones cohesivas y estrechamente relacionadas. Por ejemplo, una aplicación puede consistir en servicios tales como el servicio de catálogo, el servicio de pedidos, el servicio del carrito de compras, el servicio de perfil de usuario, etc.

Los microservicios se comunican usando protocolos como HTTP (REST), pero también de forma asíncrona (por ejemplo, usando AMQP) siempre que sea posible, especialmente cuando se propagan actualizaciones con eventos de integración.

Los microservicios se desarrollan y despliegan como contenedores de forma independiente. Esto significa que un equipo puede desarrollar y desplegar un microservicio sin afectar a otros subsistemas.

Cada microservicio tiene su propia base de datos, lo que le permite estar totalmente desacoplado de otros microservicios. Cuando es necesario, la consistencia entre bases de datos de diferentes microservicios se logra usando eventos de integración de nivel de aplicación (a través de un bus de eventos lógicos), tal como se maneja en Command and Query Responsibility Segregation (CQRS). Por eso, las restricciones del negocio deben adecuarse a la consistencia eventual los microservicios y sus bases de datos.

eShopOnContainers: Una aplicación de referencia en .NET Core basada en microservicios y desplegada en contenedores

Para que se pueda enfocar en la arquitectura y las tecnologías, en lugar de pensar en un dominio de negocio hipotético que quizás no conozca, hemos seleccionado un dominio de negocio bien conocido, específicamente, una aplicación simplificada de comercio electrónico (e-shop) que presenta un catálogo de productos, toma pedidos de los clientes, verifica el inventario y realiza otras funciones del negocio. El código fuente de la aplicación basada en contenedores está disponible en el repositorio eShopOnContainers en GitHub.

La aplicación está compuesta por varios subsistemas, que incluyen varias opciones de interfaz de usuario de la tienda (una aplicación web y una aplicación móvil nativa), junto con los microservicios y contenedores de servicios de back-end para todas las operaciones del lado del servidor. La Figura 6-1 muestra la arquitectura de la aplicación de referencia.

Image

Figura 6-1. La arquitectura del entorno de desarrollo para la aplicación de referencia eShopOnContainers

Entorno de hosting. En la Figura 6-1, verá varios contenedores desplegados dentro de un solo host Docker. Ese sería el caso cuando se implementa en un host Docker único con el comando docker-compose up. Sin embargo, si está usando un orquestador o cluster de contenedores, cada contenedor podría estar ejecutándose en un host (nodo) diferente y cualquier nodo podría estar ejecutando cualquier cantidad de contenedores, como explicamos anteriormente en la sección de arquitectura.

Arquitectura de comunicación. La aplicación eShopOnContainers usa dos tipos de comunicación, según el tipo de acción (consultas versus actualizaciones y transacciones):

La aplicación se despliega como un conjunto de microservicios en forma de contenedores. Las aplicaciones cliente pueden comunicarse con esos contenedores y también puede haber comunicación entre los microservicios. Como se mencionó, esta arquitectura inicial está usando comunicación directa de cliente a microservicio, lo que significa que una aplicación cliente puede realizar solicitudes a cada uno de los microservicios directamente. Cada microservicio tiene un endpoint público como https://servicename.applicationname.companyname. Si es necesario, cada microservicio puede usar un puerto TCP diferente. En producción, esa URL se correlacionaría con el balanceador de carga de los microservicios, que distribuye las solicitudes entre las instancias disponibles del microservicio.

Nota importante sobre API Gateway versus comunicación directa en eShopOnContainers. Como se explicó en la sección de arquitectura de esta guía, la arquitectura de comunicación directa de cliente a microservicio puede tener inconvenientes cuando se construye una aplicación grande y compleja basada en microservicios. Pero puede ser suficientemente bueno para una aplicación pequeña, como eShopOnContainers, donde el objetivo es enfocarse en una aplicación basada en contenedores Docker más simple y no queríamos crear una API Gateway monolítica, que pudiese impactar la autonomía de desarrollo de los microservicios.

Pero, si va a diseñar una aplicación grande basada en microservicios con docenas de microservicios, le recomendamos encarecidamente que considere el patrón API Gateway, como explicamos en la sección de arquitectura.

Se podría refactorizar esta decisión arquitectónica de una vez, pensando en aplicaciones para producción y fachadas especialmente diseñadas para clientes remotos. Tener múltiples API Gateways personalizados, dependiendo del dispositivo cliente puede proporcionar beneficios, como se mencionó en la sección de arquitectura, pero, como también se mencionó, las API Gateways grandes y monolíticas que podrían matar la autonomía de desarrollo de sus microservicios.

Soberanía de datos por microservicio

En la aplicación de ejemplo, cada microservicio posee su propia base de datos o fuente de datos, aunque todas (las que son SQL Server) están en el mismo contenedor. Esta decisión de diseño se hizo sólo para facilitar que un desarrollador obtenga el código de GitHub, lo clone y lo abra en Visual Studio o Visual Studio Code. Por otro lado, esto hace que sea fácil compilar las imágenes Docker personalizadas utilizando .NET Core CLI y Docker CLI y luego desplegarlas y ejecutarlas en un entorno de desarrollo Docker. De cualquier manera, el uso de contenedores para bases de datos permite a los desarrolladores crear e implementar en cuestión de minutos sin tener que preparar una base de datos externa o cualquier otra fuente de datos con dependencias duras en la infraestructura (tanto en la nube o como on-premises).

En un entorno de producción real, para lograr alta disponibilidad y escalabilidad, las bases de datos deberían estar en servidores dedicados en la nube o en servidores on-premises, pero no en contenedores.

Por lo tanto, las unidades de despliegue para microservicios (e incluso para las bases de datos en esta aplicación) son contenedores Docker y la aplicación de referencia es una aplicación de varios contenedores que sigue los principios de los microservicios.

Recursos adicionales

Beneficios de una solución basada en microservicios

Una solución basada en microservicios como esta tiene muchos beneficios:

Cada microservicio es relativamente pequeño, fácil de administrar y evolucionar. Específicamente:

Es posible escalar áreas individuales de la aplicación. Por ejemplo, es posible que el servicio de catálogo o el servicio del carrito de compras se deban escalar, pero no el proceso de pedidos. Una infraestructura de microservicios será mucho más eficiente en el uso recursos, cuando se escala, que una arquitectura monolítica.

Puede dividir el trabajo de desarrollo entre múltiples equipos. Cada servicio puede ser propiedad de un solo equipo de desarrollo. Cada equipo puede administrar, desarrollar, desplegar y escalar su servicio independientemente del resto de los equipos.

Los problemas están más aislados. Si hay un problema en un servicio, sólo ese servicio se ve afectado inicialmente (excepto cuando se usa un diseño incorrecto, con dependencias directas entre microservicios) y otros servicios pueden seguir atendiendo las solicitudes. Por el contrario, si un componente funciona mal en una arquitectura de despliegue monolítico puede colapsar todo el sistema, especialmente cuando involucra recursos del entorno, como una fuga de memoria. Además, cuando se resuelve un problema en un microservicio, puede desplegar sólo el microservicio afectado sin afectar el resto de la aplicación.

Puede usar las últimas tecnologías. Como puede comenzar a desarrollar servicios de forma independiente y ejecutarlos uno al lado del otro (gracias a los contenedores y .NET Core), puede comenzar a utilizar las últimas tecnologías y frameworks más rápidamente en lugar de quedarse atrapado en una stack o framework más antiguo para toda la aplicación.

Inconvenientes de una solución basada en microservicios

Una solución basada en microservicios como esta también tiene algunos inconvenientes:

Aplicación distribuida. Hacer una aplicación distribuida agrega complejidad para los desarrolladores cuando diseñan y construyen los servicios. Por ejemplo, los desarrolladores deben implementar la comunicación entre servicios utilizando protocolos como HTTP o AMPQ, lo que agrega complejidad para las pruebas y el manejo de excepciones. También agrega latencia al sistema.

Complejidad de despliegue. Una aplicación que tiene docenas de tipos de microservicios y necesita alta escalabilidad (necesita poder crear muchas instancias por servicio y balancear esos servicios entre muchos hosts) implica un alto grado de complejidad de despliegue para la gestión de TI e infraestructura. Si no está utilizando una infraestructura orientada a microservicios (como un orquestador y programador), esa complejidad adicional puede requerir mucho más esfuerzo de desarrollo que la aplicación del negocio en sí misma.

Transacciones atómicas. generalmente no son posibles las transacciones atómicas entre múltiples microservicios. Los requisitos del negocio deben manejar la consistencia eventual entre múltiples microservicios.

Mayores necesidades de recursos globales (memoria total, unidades y recursos de red para todos los servidores o hosts). En muchos casos, cuando reemplaza una aplicación monolítica con un enfoque de microservicios, la cantidad de recursos globales que necesita inicialmente la nueva aplicación, será mayor que las de la aplicación monolítica original. Esto se debe a que el mayor grado de granularidad y los servicios distribuidos requieren más recursos globales. Sin embargo, dado el bajo costo de los recursos en general y el beneficio de poder escalar sólo ciertas áreas de la aplicación, en comparación con los costes a largo plazo cuando se desarrollan aplicaciones monolíticas, el mayor uso de recursos suele representar un buen compromiso

Problemas con la comunicación directa de cliente a microservicio. Cuando la aplicación es grande, con docenas de microservicios, existen desafíos y limitaciones si la aplicación requiere comunicaciones directas de cliente a microservicio. Un problema es la posible falta de correspondencia entre las necesidades del cliente y las API expuestas por cada uno de los microservicios. En ciertos casos, es posible que la aplicación cliente tenga que realizar muchas solicitudes por separado para componer la interfaz de usuario, que puede ser ineficiente a través de Internet y no sería práctico en una red móvil. Por lo tanto, se deben minimizar las peticiones de la aplicación cliente al back-end.

Otro problema con las comunicaciones directas de cliente a microservicio es que algunos microservicios podrían estar usando protocolos que no son compatibles con la Web. Un servicio podría usar un protocolo binario, mientras que otro podría usar mensajes AMQP. Esos protocolos no son compatibles con un firewall y es mejor usarlos sólo internamente. Por lo general, una aplicación debe usar protocolos como HTTP y WebSockets para la comunicación fuera del firewall.

Otro inconveniente más con este enfoque directo de cliente a servicio es que hace que sea difícil refactorizar los contratos para esos microservicios. Con el tiempo, los desarrolladores pueden querer cambiar la forma en que el sistema se divide en servicios. Por ejemplo, podrían fusionar dos servicios o dividir un servicio en dos o más servicios. Sin embargo, si los clientes se comunican directamente con los servicios, realizar este tipo de refactorización puede romper la compatibilidad con las aplicaciones del cliente.

Como se mencionó en la sección de arquitectura, cuando diseñe y construya una aplicación compleja basada en microservicios, podría considerar el uso de API Gateways múltiples de grano fino en lugar de una comunicación simple y directa de cliente a microservicio.

Partición de los microservicios. Finalmente, sin importar qué enfoque elija para su arquitectura de microservicios, otro desafío es decidir cómo particionar una aplicación completa en múltiples microservicios. Como se señala en la sección de arquitectura de la guía, hay varias técnicas y enfoques que puede usar. Básicamente, debe identificar las áreas de la aplicación que están desacopladas de otras y que tienen un bajo número de dependencias fuertes. En muchos casos, esto se corresponde con la partición de los servicios por caso de uso. Por ejemplo, en nuestra aplicación de e-shop, tenemos un servicio de pedidos que es responsable de toda la lógica del negocio relacionada con el proceso de pedido. También tenemos el servicio de catálogo y el servicio de carrito de compras que implementa otras capacidades. Idealmente, cada servicio debe tener sólo un pequeño conjunto de responsabilidades. Esto es similar al principio de responsabilidad única (SRP) aplicado a las clases, que establece que una clase sólo debe tener un motivo para cambiar. Pero en este caso, se trata de microservicios, por lo que el alcance será mayor que el de una sola clase. Más que nada, un microservicio tiene que ser completamente autónomo, de principio a fin, incluida la responsabilidad de sus propias fuentes de datos.

Arquitectura externa versus arquitectura interna y patrones de diseño

La arquitectura externa es la arquitectura de microservicios compuesta por servicios múltiples, siguiendo los principios descritos en la sección de arquitectura de esta guía. Sin embargo, dependiendo de la naturaleza de cada microservicio, e independientemente de la arquitectura de microservicio de alto nivel que elija, es común y a veces recomendable tener diferentes arquitecturas internas, cada una basada en patrones diferentes, para diferentes microservicios. Los microservicios incluso pueden usar tecnologías y lenguajes de programación diferentes. La Figura 6-2 ilustra esta diversidad.

Image

Figura 6-2. Diferencias entre arquitectura y diseño internos y externos

Por ejemplo, en nuestro ejemplo de eShopOnContainers, los microservicios de catálogo, carrito de compras y perfil de usuario son simples (básicamente, subsistemas CRUD – Create, Read, Update, Delete – Crear, Leer, Actualizar, Eliminar). Por lo tanto, su arquitectura y diseño interno es sencillo. Sin embargo, es posible que tenga otros microservicios, como el microservicio de pedidos, que es más complejo y representa reglas del negocio en constante cambio, con un alto grado de complejidad del dominio. En casos como estos, es posible que desee implementar patrones más avanzados dentro de un microservicio particular, como los definidos con los enfoques de diseño orientados por el dominio (DDD), como lo estamos haciendo en el microservicio de pedidos de eShopOnContainers. (Revisaremos estos patrones de DDD posteriormente, en la sección que explica la implementación del microservicio de pedidos).

Otra razón para una tecnología diferente por microservicio podría ser la naturaleza de cada microservicio. Por ejemplo, podría ser mejor usar un lenguaje de programación funcional como F #, o incluso un lenguaje como R si está apuntando a dominios de IA (Inteligencia Artificial) y de machine learning (aprendizaje de máquinas), en lugar de un lenguaje de programación orientado a objetos como C#.

La conclusión es que cada microservicio puede tener una arquitectura interna diferente basada en diferentes patrones de diseño. No todos los microservicios se tienen qué implementar usando patrones avanzados de DDD, porque eso sería exagerado. Del mismo modo, los microservicios complejos, con una lógica del negocio en cambio constante, no deberían implementarse como componentes CRUD, porque podría terminar con código de baja calidad.

El nuevo mundo: múltiples patrones arquitectónicos y servicios políglotas

Hay muchos patrones arquitectónicos usados por los arquitectos y desarrolladores de software. A continuación, se muestran algunos de ellos (mezcla de estilos y patrones de arquitectura):

También puede crear microservicios con muchas tecnologías y lenguajes, como ASP.NET Core Web API, NancyFx, ASP.NET Core SignalR (disponible con .NET Core 2), F#, Node.js, Python, Java, C++, GoLang y más.

El punto importante es que ningún patrón o estilo de arquitectura, ni ninguna tecnología en particular, son adecuados para todas las situaciones. La Figura 6-3 muestra algunos enfoques y tecnologías (aunque no en un orden particular) que se podrían usar en diferentes microservicios.

Image

Figura 6-3. El mundo de múltiples patrones arquitectónicos y microservicios políglotas

Como se muestra en la Figura 6-3, en aplicaciones compuestas por muchos microservicios (Bounded Context en terminología DDD, o simplemente "subsistemas" como microservicios autónomos), puede implementar cada microservicio de una forma diferente. Cada uno puede tener un patrón arquitectónico diferente y utilizar diferentes lenguajes y bases de datos, dependiendo de la naturaleza de la aplicación, los requisitos del negocio y las prioridades. En algunos casos, los microservicios pueden ser similares. Pero eso no es frecuente, ya que los límites y requisitos de cada subsistema suelen ser diferentes.

Por ejemplo, para una aplicación CRUD simple de mantenimiento, podría no tener sentido diseñar e implementar patrones DDD. Pero para su dominio o negocio principal, se podrían necesitar patrones más avanzados para abordar la complejidad del negocio y manejar reglas en cambio constante.

Especialmente cuando maneje aplicaciones grandes, compuestas por múltiples subsistemas, no debe aplicar una arquitectura única de nivel superior, basada en un patrón de arquitectura único. Por ejemplo, CQRS no tiene sentido como una arquitectura de nivel superior para una aplicación completa, pero podría ser útil para un conjunto específico de servicios.

No hay una solución universal o patrón de arquitectura correcto para todos los casos. No puede tener "un patrón de arquitectura que gobierne todo". Dependiendo de las prioridades de cada microservicio, debe elegir un enfoque más adecuado para cada uno, como se explica en las siguientes secciones.

Creando un microservicio CRUD simple para datos

Esta sección describe cómo crear un microservicio simple que realice operaciones de creación, lectura, actualización y eliminación (CRUD) en una fuente de datos.

Diseñando un microservicio CRUD simple

Desde el punto de vista del diseño, este tipo de microservicios contenerizados es muy simple. Tal vez el problema a resolver sea simple, o tal vez la implementación sea sólo una prueba de concepto.

Image

Figura 6-4. Diseño interno para microservicios CRUD sencillos

Un ejemplo de este tipo es el microservicio de catálogo de la aplicación eShopOnContainers. Este tipo de servicio implementa toda su funcionalidad en un proyecto ASP.NET Web API único que incluye clases para su modelo de datos, su lógica del negocio y su lógica de acceso a datos. También almacena sus datos relacionados en una base de datos que se ejecuta en SQL Server (como otro contenedor para propósitos de desarrollo y prueba), pero también podría ser cualquier servidor SQL Server normal, como se muestra en la Figura 6-5.

Image

Figura 6-5. Diseño de un microservicio simple tipo CRUD/orientado a datos

Cuando está desarrollando este tipo de servicio, solo necesita ASP.NET Core y una API de acceso a datos o ORM como Entity Framework Core. También puede generar metadatos de Swagger automáticamente a través de Swashbuckle, para proporcionar una descripción de lo que ofrece su servicio, como se explica en la siguiente sección.

Tenga en cuenta que ejecutar un servidor de base de datos como SQL Server dentro de un contenedor Docker es adecuado para entornos de desarrollo, porque puede tener todas sus dependencias en funcionamiento sin necesidad de preparar una base de datos en la nube o en las instalaciones internas. Esto es muy conveniente para ejecutar pruebas de integración. Sin embargo, para entornos de producción, no se recomienda hacerlo, ya que normalmente no se obtiene alta disponibilidad con ese enfoque. Para un entorno de producción en Azure, se recomienda usar Azure SQL DB o cualquier otra tecnología de base de datos que pueda proporcionar alta disponibilidad y alta escalabilidad. Por ejemplo, para un enfoque NoSQL, puede elegir DocumentDB.

Finalmente, al editar los ficheros de metadatos Dockerfile y docker-compose.yml, puede configurar cómo se creará la imagen de este contenedor: qué imagen base usará, además de configuraciones de diseño como nombres internos y externos y puertos TCP.

Implementando un microservicio CRUD simple con ASP.NET Core

Para implementar un microservicio CRUD simple usando .NET Core y Visual Studio, se comienza creando un proyecto simple ASP.NET Core API Web (apuntando a .NET Core para que pueda correr en un host Docker de Linux), como se muestra en la Figura 6- 6.

Image

Figura 6-6. Creando un proyecto ASP.NET Core Web API en Visual Studio

Después de crear el proyecto, puede implementar sus controladores MVC como lo haría en cualquier otro proyecto de API Web, utilizando la API de Entity Framework o cualquier otra. En un nuevo proyecto API Web, puede ver que la única dependencia que tiene en ese microservicio es ASP.NET Core. Internamente, dentro de la dependencia Microsoft.AspNetCore.All, se hace referencia a Entity Framework y muchos otros paquetes NuGet de .NET Core, como se muestra en la Figura 6-7.

Image

Figura 6-7. Dependencias en un microservicio API Web CRUD simple

Implementando servicios CRUD API Web con Entity Framework Core

Entity Framework (EF) Core es una versión ligera, extensible y multiplataforma de la popular tecnología de acceso a datos de Entity Framework. EF Core es un mapeador objeto-relacional (ORM) que permite a los desarrolladores de .NET trabajar con una base de datos usando objetos .NET.

El microservicio de catálogo usa EF y el proveedor de SQL Server porque su base de datos se ejecuta en un contenedor con la imagen Docker de SQL Server para Linux. Sin embargo, la base de datos podría implementarse en cualquier Servidor SQL, como Windows local o Azure SQL DB. Lo único que tendría que cambiar es la cadena de conexión en el microservicio ASP.NET Web API.

El modelo de datos

Con EF Core, el acceso a los datos se realiza mediante el uso de un modelo. Un modelo se compone de clases de entidades (del modelo de dominio) y un contexto derivado de una clase base de EF (DbContext), que representa una sesión con la base de datos, que le permite consultar y guardar datos. Puede generar un modelo a partir de una base de datos existente, codificar manualmente un modelo para que coincida con su base de datos o usar migraciones EF para crear una base de datos a partir de su modelo, usando el enfoque code first (que facilita la evolución de la base de datos a medida que su modelo cambia con el tiempo). Para el microservicio de catálogo estamos utilizando el último enfoque. Puede ver un ejemplo de la clase CatalogItem en el siguiente ejemplo de código, que es una clase simple (POCO – Plain Old CLR Object), que corresponde a la entidad del modelo de dominio.

Public class CatalogItem

{

public int Id { get; set; }

public string Name { get; set; }

public string Description { get; set; }

public decimal Price { get; set; }

public string PictureFileName { get; set; }

public string PictureUri { get; set; }

public int CatalogTypeId { get; set; }

public CatalogType CatalogType { get; set; }

public int CatalogBrandId { get; set; }

public CatalogBrand CatalogBrand { get; set; }

public int AvailableStock { get; set; }

public int RestockThreshold { get; set; }

public int MaxStockThreshold { get; set; }

 

public bool OnReorder { get; set; }

public CatalogItem() { }

// Additional code ...

}

También necesita un DbContext que represente una sesión con la base de datos. Para el microservicio de catálogo, la clase CatalogContext deriva de la clase base DbContext, como se muestra en el siguiente ejemplo:

public class CatalogContext : DbContext

{

public CatalogContext(DbContextOptions<CatalogContext> options) : base(options)

{

}

public DbSet<CatalogItem> CatalogItems { get; set; }

public DbSet<CatalogBrand> CatalogBrands { get; set; }

public DbSet<CatalogType> CatalogTypes { get; set; }

// Additional code ...

}

Puede tener implementaciones adicionales de DbContext. Por ejemplo, en el microservicio Catalog.API, hay un segundo DbContext llamado CatalogContextSeed donde se cargan automáticamente los datos de ejemplo la primera vez que intenta acceder a la base de datos. Este método es útil para datos de demostración.

Dentro del DbContext, utilice el método OnModelCreating para personalizar el mapeo objeto/base de datos de las entidades del modelo de dominio y otros puntos de extensibilidad de EF.

Consultando datos desde controladores API Web

Las instancias de las clases de entidades normalmente se recuperan de la base de datos utilizando Language Integrated Query (LINQ), como se muestra en el siguiente ejemplo:

[Route("api/v1/[controller]")]

public class CatalogController : ControllerBase

{

private readonly CatalogContext _catalogContext;

private readonly CatalogSettings _settings;

private readonly ICatalogIntegrationEventService

_catalogIntegrationEventService;

public CatalogController(CatalogContext context,

IOptionsSnapshot<CatalogSettings> settings,

ICatalogIntegrationEventService

catalogIntegrationEventService)

{

_catalogContext = context ?? throw new

ArgumentNullException(nameof(context));

_catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService));

_settings = settings.Value;

((DbContext)context).ChangeTracker.QueryTrackingBehavior =

QueryTrackingBehavior.NoTracking;

}

// GET api/v1/[controller]/items[?pageSize=3&pageIndex=10]

[HttpGet]

[Route("[action]")]

[ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>),

(int)HttpStatusCode.OK)]

public async Task<IActionResult> Items([FromQuery]int pageSize = 10,

[FromQuery]int pageIndex = 0)

{

var totalItems = await _catalogContext.CatalogItems

.LongCountAsync();

 

var itemsOnPage = await _catalogContext.CatalogItems

.OrderBy(c => c.Name)

.Skip(pageSize * pageIndex)

.Take(pageSize)

.ToListAsync();

 

itemsOnPage = ChangeUriPlaceholder(itemsOnPage);

var model = new PaginatedItemsViewModel<CatalogItem>(

pageIndex, pageSize, totalItems, itemsOnPage);

 

return Ok(model);

}

//...

}

Guardar datos

Los datos se crean, eliminan y modifican en la base de datos usando instancias de las clases de entidad. Podría agregar código como el siguiente ejemplo (con datos simulados, en este caso) a los controladores de su API web.

var catalogItem = new CatalogItem() {CatalogTypeId=2, CatalogBrandId=2,

Name="Roslyn T-Shirt", Price = 12};

_context.Catalog.Add(catalogItem);

_context.SaveChanges();

 

Inyección de dependencias en ASP.NET Core y controladores API Web

En ASP.NET Core puede usar Inyección de Dependencias (DI - Dependency Injection) desde el principio. No es necesario configurar un contenedor de inversión de control (IoC) de terceros, aunque puede conectar su contenedor de IoC preferido a la infraestructura ASP.NET Core si lo desea. En este caso, significa que puede inyectar directamente el DBContext de EF requerido o repositorios adicionales a través del constructor del controlador.

En el ejemplo anterior de la clase CatalogController, estamos inyectando un objeto de tipo CatalogContext más otros objetos a través del constructor CatalogController().

El registro de la clase DbContext en el contenedor IoC del servicio, es una configuración fundamental del proyecto de la API web. Normalmente se hace en la clase Startup llamando al método services.AddDbContext<DbContext>() dentro del método ConfigureServices(), como se muestra en el siguiente ejemplo:

public void ConfigureServices(IServiceCollection services)

{

// Additional code…

 

services.AddDbContext<CatalogContext>(options =>

{

options.UseSqlServer(Configuration["ConnectionString"],

sqlServerOptionsAction: sqlOptions =>

{

sqlOptions.

MigrationsAssembly(

typeof(Startup).

GetTypeInfo().

Assembly.

GetName().Name);

//Configuring Connection Resiliency:

sqlOptions.

EnableRetryOnFailure(maxRetryCount: 5,

maxRetryDelay: TimeSpan.FromSeconds(30),

errorNumbersToAdd: null);

});

// Changing default behavior when client evaluation occurs to throw.

// Default in EFCore would be to log warning when client evaluation is done.

options.ConfigureWarnings(warnings => warnings.Throw(

RelationalEventId.QueryClientEvaluationWarning));

});

//...

}

Recursos adicionales

La cadena de conexión a la base de datos y las variables de entorno usadas por los contenedores Docker

Puede usar la configuración de ASP.NET Core y agregar una propiedad ConnectionString a su fichero settings.json como se muestra en el siguiente ejemplo:

{

"ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=

Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word",

"ExternalCatalogBaseUrl": "http://localhost:5101",

"Logging": {

"IncludeScopes": false,

"LogLevel": {

"Default": "Debug",

"System": "Information",

"Microsoft": "Information"

}

}

}

 

El fichero settings.json puede tener valores predeterminados para la propiedad ConnectionString o para cualquier otra propiedad. Sin embargo, esas propiedades serán reemplazadas al usar Docker, por los valores de las variables de entorno que especifique en el fichero docker-compose.override.yml.

Desde los ficheros docker-compose.yml o docker-compose.override.yml, puede inicializar esas variables de entorno para que Docker las configure como variables de entorno del sistema operativo para usted, como se muestra en el siguiente fichero docker-compose.override.yml (la cadena de conexión y otras líneas largas se pliegan a la línea siguiente en este ejemplo, pero no debe hacerlo en su fichero, es decir, la cadena de conexión es una sola línea larga).

# docker-compose.override.yml

catalog.api:

environment:

- ConnectionString=Server=sql.data;

Database=Microsoft.eShopOnContainers.Services.CatalogDb;

User Id=sa;Password=Pass@word

# Additional environment variables for this service

ports:

- "5101:80"

 

Los ficheros docker-compose.yml a nivel de solución no sólo son más flexibles que los ficheros de configuración en el nivel de proyecto o microservicio, sino que también son más seguros. Tenga en cuenta que las imágenes Docker que compila por microservicio no contienen los ficheros docker-compose.yml, sólo ficheros binarios y ficheros de configuración para cada microservicio, incluido el fichero Dockerfile. Pero los ficheros docker-compose.yml y docker-compose.override.yml no se incluyen en la imagen Docker, no se despliegan junto con su aplicación, sólo se usan en el momento del despliegue.

Por lo tanto, poner valores de variables de entorno en los ficheros docker-compose.yml (incluso sin encriptar los valores) es más seguro que ponerlos en los ficheros de configuración de .NET regulares que se despliegan con su código.

Finalmente, puede obtener el valor configurado de su código usando Configuration["ConnectionString"], como se muestra en el método ConfigureServices() en un ejemplo de código anterior.

Sin embargo, para entornos de producción, es posible que desee explorar otras formas de almacenar esos datos secretos, como las cadenas de conexión. Por lo general, eso será administrado por el orquestador elegido, como puede hacer con la administración de secretos de Docker Swarm.

Implementar versionamiento en ASP.NET Web APIs

A medida que cambian los requisitos del negocio, se pueden agregar nuevas colecciones de recursos, las relaciones entre los recursos pueden cambiar y la estructura de los datos en los recursos podría modificarse. La actualización de una API Web para gestionar nuevos requisitos es un proceso relativamente sencillo, pero debe tener en cuenta los efectos que dichos cambios tendrán en las aplicaciones cliente que consumen la API Web. Aunque el desarrollador que diseña e implementa una API Web tiene control total sobre esa API, el desarrollador no tiene el mismo grado de control sobre las aplicaciones cliente, que podrían ser creadas por organizaciones de terceros que operan de forma remota.

El versionamiento permite que una API Web indique las características y los recursos que expone. Una aplicación cliente puede enviar peticiones a una versión específica de una característica o recurso. Existen varios enfoques para implementar el versionamiento:

El versionamiento en el query string o URI son los más simples de implementar. El versionamiento en los encabezados es un buen enfoque. Sin embargo, este último no es tan explícito y sencillo como los otros dos. Dado que el versionamiento de URL es el más simple y explícito, es el que se usa en la aplicación de ejemplo eShopOnContainers.

Con el versionamiento por URI, como en la aplicación de ejemplo eShopOnContainers, cada vez que modifique la API web o cambie el esquema de recursos, debe cambiar el número de versión al URI para cada recurso. Los URI existentes deben continuar funcionando como antes, devolviendo recursos que se ajusten al esquema que coincida con la versión solicitada.

Como se muestra en el siguiente ejemplo, la versión se puede establecer utilizando el atributo Route en el controlador de la API Web, lo que hace que la versión sea explícita en el URI (v1 en este caso).

[Route("api/v1/[controller]")]

public class CatalogController : ControllerBase

{

// Implementation ...

 

Este mecanismo de versionamiento es simple y depende de que el servidor enrute la petición al endpoint apropiado. Sin embargo, para un versionamiento más sofisticado, además de ser el mejor al usar REST, debe usar enlaces e implementar HATEOAS (Hypertext as the Engine of Application State).

Recursos adicionales

http://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx

https://docs.microsoft.com/azure/architecture/best-practices/api-design#versioning-a-restful-web-api

https://www.infoq.com/articles/roy-fielding-on-versioning

Generando metadata de descripción de Swagger para su API Web ASP.NET Core

Swagger es un framework open source muy utilizado y respaldado por un gran ecosistema de herramientas que le ayuda a diseñar, crear, documentar y consumir sus API RESTful. Se está convirtiendo en el estándar para el dominio de descripción de metadatos de APIs. Debería incluir metadatos de descripción de Swagger con cualquier tipo de servicio de microservicio, ya sean orientados a datos o microservicios más avanzados orientados por el dominio (como se explica en la siguiente sección).

El corazón de Swagger es la especificación Swagger, que es metadata de la descripción de un API en un fichero JSON o YAML. La especificación crea el contrato RESTful para su API, que detalla todos sus recursos y operaciones en un formato legible tanto por humanos como por programas, para facilitar el desarrollo, descubrimiento e integración de la API.

La especificación Swagger es la base de la especificación OpenAPI (OAS) y se desarrolla en una comunidad abierta, transparente y colaborativa, para estandarizar la forma en que se definen las interfaces RESTful.

La especificación define la estructura de cómo se puede descubrir un servicio y cómo se entienden sus capacidades. Para obtener más información, incluido un editor web y ejemplos de especificaciones de Swagger de compañías como Spotify, Uber, Slack y Microsoft, visite el sitio de Swagger (http://swagger.io).

¿Por qué usar Swagger?

Las razones principales para generar metadatos de Swagger para sus API son las siguientes.

Facilita que otros productos consuman e integren automáticamente sus API. Docenas de productos y herramientas comerciales, y muchas librerías y frameworks soportan Swagger. Microsoft tiene productos y herramientas de alto nivel que pueden consumir automáticamente APIs basadas en Swagger, como las siguientes:

Permite generar automáticamente documentación de la API. Cuando crea API RESTful de gran escala, como en aplicaciones complejas basadas en microservicios, necesita manejar muchos endpoints con diferentes modelos de datos utilizados en las payloads (datos incluídos en) las peticiones y respuestas. Tener la documentación adecuada y tener un explorador de API robusto, como el que se obtiene con Swagger, es clave para el éxito de su API y la adopción por parte de los desarrolladores.

Los metadatos de Swagger son los que usan Microsoft Flow, PowerApps y Azure Logic Apps para comprender cómo usar las API y conectarse a ellas.

Cómo automatizar la generación de la metadata de Swagger para un API con el paquete NuGet Swashbuckle

Generar metadatos Swagger manualmente (en un fichero JSON o YAML) puede ser un trabajo tedioso. Sin embargo, puede automatizar el descubrimiento de la API de los servicios de ASP.NET Web API, usando el paquete NuGet Swashbuckle para generar dinámicamente los metadatos Swagger de la API.

Swashbuckle genera automáticamente los metadatos Swagger para sus proyectos ASP.NET Web API. Soporta los proyectos ASP.NET Core Web API y los ASP.NET Web API del framework tradicional y cualquier otra variante, como las Azure API App, las Azure Mobile App, los microservicios de Azure Service Fabric basados en ASP.NET. También soporta APIs Web simples implementada en contenedores, como en la aplicación de referencia.

Swashbuckle combina el API Explorer y Swagger en swagger-ui para proporcionar una experiencia completa en documentación y descubrimiento para los consumidores de su API. Además de su generador de metadatos Swagger, Swashbuckle también contiene una versión integrada de swagger-ui, que funcionará automáticamente una vez que se instale Swashbuckle.

Esto significa que puede complementar su API con una interfaz de usuario agradable, para ayudar a los desarrolladores a descubrir y usar su API. Requiere una cantidad muy pequeña de código y de mantenimiento porque se genera automáticamente, lo que le permite concentrarse en crear su API. El resultado del explorador de APIs se parece al de la Figura 6-8.

Image

Figura 6-8. Explorador de APIs de Swashbuckle, basado en metadata Swagger del microservicio de catálogo de eShopOnContainers

Sin embargo, el explorador de APIs no es lo más importante. Una vez que tiene una API web que se puede describir con los metadatos Swagger, su API se puede usar sin problemas desde las herramientas basadas en Swagger, incluidos los generadores de código de cliente que se pueden orientar a muchas plataformas. Por ejemplo, como se mencionó antes, AutoRest genera automáticamente clases de cliente .NET. Pero también están disponibles herramientas adicionales como swagger-codegen, que permiten la generación de código de librerías de cliente API, stubs de servidor y documentación automáticamente.

Actualmente, Swashbuckle consta de dos paquetes internos de NuGet bajo el metapaquete de alto nivel Swashbuckle.Swashbuckle.AspNetCoreSwaggerGen para aplicaciones ASP.NET Core.

Después de haber agregado la dependencia a estos paquetes NuGet en su proyecto de API Web, necesitará configurar Swagger en la clase Startup, como se muestra a continuación:

public class Startup

{

public IConfigurationRoot Configuration { get; }

 

// Other startup code...

 

public void ConfigureServices(IServiceCollection services)

{

// Other ConfigureServices() code...

// Add framework services.

services.AddSwaggerGen(options =>

{

options.DescribeAllEnumsAsStrings();

options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info

{

Title = "eShopOnContainers - Catalog HTTP API",

Version = "v1",

Description = "The Catalog Microservice HTTP API",

TermsOfService = "Terms Of Service"

});

});

// Other ConfigureServices() code...

}

 

public void Configure(IApplicationBuilder app,

IHostingEnvironment env,

ILoggerFactory loggerFactory)

{

// Other Configure() code...

// ...

app.UseSwagger()

.UseSwaggerUI(c =>

{

c.SwaggerEndpoint("/swagger/v1/swagger.json", "Catalog.API V1");

});

}

}

Una vez hecho esto, puede iniciar su aplicación y explorar los siguientes endpoints de JSON y la interfaz de usuario de Swagger, usando URLs como estas:

http://<your-root-url>/swagger/v1/swagger.json

http://<your-root-url>/swagger/

 

Anteriormente pudo ver la interfaz de usuario generada por Swashbuckle para una URL como http://<your-root-url>/swagger. En la Figura 6-9 también puede ver cómo puede probar cualquier método API.

Image

Figura 6-9. Probando el método Catalog/Item del API con Swashbuckle UI

La figura 6-10 muestra los metadatos Swagger JSON generados a partir del microservicio eShopOnContainers (que es lo que las herramientas usan por debajo) cuando hace la petición a <su-url-raíz>/swagger/v1/swagger.json usando Postman.

Image

Figura 6-10. JSON de la metadara Swagger

Es así de simple. Y como se genera automáticamente, los metadatos de Swagger se ampliarán cuando agregue más funcionalidades a su API.

Recursos adicionales

Definiendo su aplicación multi-contenedor con docker-compose.yml

En esta guía, se presentó el fichero docker-compose.yml en la sección Paso 4. Definir los servicios en docker-compose.yml al construir aplicaciones multi-contenedor. Sin embargo, hay otras formas de usar los ficheros docker-compose que vale la pena explorar con más detalle.

Por ejemplo, puede describir explícitamente cómo desea desplegar su aplicación de varios contenedores en el fichero docker-compose.yml. Opcionalmente, también puede describir cómo va a construir sus imágenes Docker personalizadas. (También se pueden construir imágenes Docker personalizadas con Docker CLI).

Básicamente, usted define cada uno de los contenedores que desea desplegar, más ciertas características para el despliegue de cada uno. Una vez que tenga un fichero con la descripción del despliegue multi-contenedor, puede implementar toda la solución en una sola acción orquestada por el comando de la CLI docker-compose up, o puede desplegarla de forma transparente desde Visual Studio. De lo contrario, necesitaría usar la CLI de Docker para desplegar contenedor por contenedor en varios pasos, mediante el uso del comando docker run desde la interfaz de comandos. Por lo tanto, cada servicio definido en docker-compose.yml debe especificar exactamente una imagen o build. Otros parámetros son opcionales y análogos a los correspondientes del comando docker run.

El siguiente código YAML es la definición de un posible fichero global docker-compose.yml único, para el ejemplo eShopOnContainers. Este no es el fichero real de docker-compose de eShopOnContainers, es sólo una versión simplificada y consolidada en un único fichero, aunque esta no es la mejor manera de trabajar con ficheros docker-compose, como se explicará más adelante.

version: '2'

services:

webmvc:

image: eshop/webmvc

environment:

- CatalogUrl=http://catalog.api

- OrderingUrl=http://ordering.api

- BasketUrl=http://basket.api

ports:

- "5100:80"

depends_on:

- catalog.api

- ordering.api

- basket.api

catalog.api:

image: eshop/catalog.api

environment:

- ConnectionString=Server=sql.data;Initial Catalog=CatalogData;

User Id=sa;Password=your@password

expose:

- "80"

ports:

- "5101:80"

#extra hosts can be used for standalone SQL Server or services at the dev PC

extra_hosts:

- "CESARDLSURFBOOK:10.0.75.1"

depends_on:

- sql.data

ordering.api:

image: eshop/ordering.api

environment:

- ConnectionString=Server=sql.data;Database=Services.OrderingDb;

User Id=sa;Password=your@password

ports:

- "5102:80"

#extra hosts can be used for standalone SQL Server or services at the dev PC

extra_hosts:

- "CESARDLSURFBOOK:10.0.75.1"

depends_on:

- sql.data

basket.api:

image: eshop/basket.api

environment:

- ConnectionString=sql.data

ports:

- "5103:80"

depends_on:

- sql.data

sql.data:

environment:

- SA_PASSWORD=your@password

- ACCEPT_EULA=Y

ports:

- "5434:1433"

basket.data:

image: redis

El parámetro o nodo raíz en este fichero es services. Bajo ese nodo se definen los servicios que desea desplegar y ejecutar con comando docker-compose up o cuando despliega desde Visual Studio, usando ese fichero docker-compose.yml. En este caso, el fichero docker-compose.yml tiene definidos varios servicios, como se describen en la siguiente tabla.

Image

Un contenedor para un servicio API Web sencillo

Enfocándonos en el contenedor del microservicio catalog.api, podemos ver que tiene una definición sencilla:

catalog.api:

image: eshop/catalog.api

environment:

- ConnectionString=Server=catalog.data;Initial Catalog=CatalogData;

User Id=sa;Password=your@password

expose:

- "80"

ports:

- "5101:80"

 

#extra hosts can be used for standalone SQL Server or services at the dev PC

extra_hosts:

- "CESARDLSURFBOOK:10.0.75.1"

 

depends_on:

- sql.data

 

Este servicio contenerizado tiene la siguiente configuración básica:

Como la cadena de conexión está definida por una variable de entorno, puede establecer esa variable a través de un mecanismo diferente y en un momento diferente. Por ejemplo, podría establecer una cadena de conexión diferente al implementar en producción en los hosts finales o hacerlo desde sus procesos de CI/CD en VSTS o su sistema DevOps preferido.

También hay otras configuraciones más avanzadas del docker-compose.yml que revisaremos más adelante.

Usando ficheros docker-compose para ejecutar en múltiples entornos

Los ficheros docker-compose.yml son ficheros de configuración y se pueden utilizar en muchas infraestructuras que entienden ese formato. La herramienta más sencilla es el comando docker-compose, pero otras como los orquestadores (por ejemplo, Docker Swarm) también entienden ese fichero.

Por lo tanto, al usar el comando docker-compose, puede apuntar a los siguientes escenarios principales.

Entornos de desarrollo

Cuando desarrolla aplicaciones, es importante poder ejecutar una aplicación en un entorno de desarrollo aislado. Puede usar el comando docker-compose de la CLI, para crear ese entorno o usar Visual Studio, que usa docker-compose por debajo.

El fichero docker-compose.yml le permite configurar y documentar todas las dependencias de servicios de su aplicación (otros servicios, caché, bases de datos, colas, etc.). Con el comando CLI docker-compose, puede crear e iniciar uno o más contenedores para cada dependencia con un solo comando (docker-compose up).

Los ficheros docker-compose.yml son interpretados por el motor Docker, pero también sirven para documentar la composición de su aplicación multi-contenedor.

Entornos de pruebas

Una parte importante de cualquier proceso de despliegue o integración continuos (CD/CI) son las pruebas unitarias y de integración. Estas pruebas automatizadas requieren un entorno aislado para que no se vean afectadas por los usuarios o cualquier otro cambio en los datos de la aplicación.

Con docker-compose puede crear y eliminar ese entorno aislado muy fácilmente, con unos pocos comandos desde la interfaz de comandos del sistema o con scripts batch, como los siguientes comandos:

docker-compose up -d

./run_unit_tests

docker-compose down

 

Entornos de producción

También puede usar Compose para desplegar en un motor Docker remoto. Un caso típico es implementar en una instancia única del host Docker (como un servidor o una máquina virtual de producción preparados con Docker Machine). Pero también podría ser un cluster Docker Swarm, porque los clusters también son compatibles con los ficheros docker-compose.yml.

Si está utilizando cualquier otro orquestador (Azure Service Fabric, Mesos DC/OS, Kubernetes, etc.), es posible que necesite agregar ajustes de configuración de metadatos y configuración como los de docker-compose.yml, pero en el formato requerido por el orquestador.

En cualquier caso, docker-compose es una herramienta conveniente y un formato de metadatos para flujos de trabajo de desarrollo, prueba y producción, aunque el flujo de trabajo de producción puede variar según el orquestador que esté utilizando.

Usando múltiples ficheros docker-compose para manejar varios entornos

Al apuntar a diferentes entornos, debe usar varios ficheros compose. Esto le permite crear múltiples variantes de configuración dependiendo del entorno.

Reemplazando el fichero base docker-compose

Podría usar un fichero docker-compose.yml único, como en los ejemplos simplificados que se muestran en las secciones anteriores. Sin embargo, no se recomienda eso para la mayoría de las aplicaciones.

Docker Compose, por defecto, lee dos ficheros, un docker-compose.yml y un fichero opcional docker-compose.override.yml. Como se muestra en la Figura 6-11, cuando está usando Visual Studio y habilita la compatibilidad con Docker

Image

Figura 6-11. Ficheros de docker-compose en Visual Studio 2017

Puede editar los ficheros docker-compose con cualquier editor, como Visual Studio Code o Sublime, y ejecutar la aplicación con el comando docker-compose up.

Por convención, el fichero docker-compose.yml contiene su configuración base y otros parámetros estáticos. Eso significa que no debería cambiar la configuración del servicio según el entorno de donde se despliegue.

El fichero docker-compose.override.yml, como su nombre lo sugiere, contiene parámetros que reemplazan la configuración base, como los que dependen del entorno de despliegue. También puede tener múltiples ficheros de reemplazo con diferentes nombres. Los ficheros de reemplazo generalmente contienen información adicional que necesita la aplicación, pero específica para un entorno o para un despliegue.

Apuntando a múltiples entornos

Un caso de uso típico es cuando define múltiples ficheros de composición para apuntar a múltiples entornos, como producción, pre-producción (staging), integración continua (CI) o desarrollo. Para soportar estas diferencias, puede dividir su configuración de composición en varios ficheros, como se muestra en la figura 6-12.

Image

Figura 6-12. Múltiples ficheros docker-compose pueden reemplazar valores del docker-compose.yml base

Comience con el fichero base docker-compose.yml. Este fichero base debe contener la configuración básica o estática, que no cambia según el entorno. Por ejemplo, eShopOnContainers tiene el siguiente fichero docker-compose.yml (simplificado con menos servicios) como fichero base.

#docker-compose.yml (Base)

version: '3'

services:

basket.api:

image: eshop/basket.api:${TAG:-latest}

build:

context: ./src/Services/Basket/Basket.API

dockerfile: Dockerfile

depends_on:

- basket.data

- identity.api

- rabbitmq

 

catalog.api:

image: eshop/catalog.api:${TAG:-latest}

build:

context: ./src/Services/Catalog/Catalog.API

dockerfile: Dockerfile

depends_on:

- sql.data

- rabbitmq

 

marketing.api:

image: eshop/marketing.api:${TAG:-latest}

build:

context: ./src/Services/Marketing/Marketing.API

dockerfile: Dockerfile

depends_on:

- sql.data

- nosql.data

- identity.api

- rabbitmq

 

webmvc:

image: eshop/webmvc:${TAG:-latest}

build:

context: ./src/Web/WebMVC

dockerfile: Dockerfile

depends_on:

- catalog.api

- ordering.api

- identity.api

- basket.api

- marketing.api

 

sql.data:

image: microsoft/mssql-server-linux:2017-latest

 

nosql.data:

image: mongo

 

basket.data:

image: redis

rabbitmq:

image: rabbitmq:3-management

Como ya dijimos, los parámetros en el fichero base docker-compose.yml no deberían cambiar por los diferentes entornos de despliegue.

Si, por ejemplo, se fija en la definición del servicio webmvc, puede ver cómo esa información es muy similar sin importar el entorno donde se despliegue. Usted tiene la siguiente información:

Puede tener una configuración adicional, pero el punto importante es que en el fichero base docker-compose.yml, sólo debe establecer la configuración que es común a todos los entornos. Luego, en docker-compose.override.yml o ficheros similares para producción o staging, debe colocar la configuración específica para cada entorno.

Por lo general, docker-compose.override.yml se usa para el entorno de desarrollo, como en el ejemplo siguiente de eShopOnContainers:

#docker-compose.override.yml (Extended config for DEVELOPMENT env.)

version: '3'

 

services:

# Simplified number of services here:

basket.api:

environment:

- ASPNETCORE_ENVIRONMENT=Development

- ASPNETCORE_URLS=http://0.0.0.0:80

- ConnectionString=${ESHOP_AZURE_REDIS_BASKET_DB:-basket.data}

- identityUrl=http://identity.api

- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105

- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}

- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}

- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}

- AzureServiceBusEnabled=False

- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}

- OrchestratorType=${ORCHESTRATOR_TYPE}

- UseLoadTest=${USE_LOADTEST:-False}

 

ports:

- "5103:80"

 

catalog.api:

environment:

- ASPNETCORE_ENVIRONMENT=Development

- ASPNETCORE_URLS=http://0.0.0.0:80

- ConnectionString=${ESHOP_AZURE_CATALOG_DB:-Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word}

- PicBaseUrl=${ESHOP_AZURE_STORAGE_CATALOG_URL:-http://localhost:5101/api/v1/catalog/items/[0]/pic/}

- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}

- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}

- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}

- AzureStorageAccountName=${ESHOP_AZURE_STORAGE_CATALOG_NAME}

- AzureStorageAccountKey=${ESHOP_AZURE_STORAGE_CATALOG_KEY}

- UseCustomizationData=True

- AzureServiceBusEnabled=False

- AzureStorageEnabled=False

- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}

- OrchestratorType=${ORCHESTRATOR_TYPE}

ports:

- "5101:80"

 

marketing.api:

environment:

- ASPNETCORE_ENVIRONMENT=Development

- ASPNETCORE_URLS=http://0.0.0.0:80

- ConnectionString=${ESHOP_AZURE_MARKETING_DB:-Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word}

- MongoConnectionString=${ESHOP_AZURE_COSMOSDB:-mongodb://nosql.data}

- MongoDatabase=MarketingDb

- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}

- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}

- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}

- identityUrl=http://identity.api

- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105

- CampaignDetailFunctionUri=${ESHOP_AZUREFUNC_CAMPAIGN_DETAILS_URI}

- PicBaseUrl=${ESHOP_AZURE_STORAGE_MARKETING_URL:-http://localhost:5110/api/v1/campaigns/[0]/pic/}

- AzureStorageAccountName=${ESHOP_AZURE_STORAGE_MARKETING_NAME}

- AzureStorageAccountKey=${ESHOP_AZURE_STORAGE_MARKETING_KEY}

- AzureServiceBusEnabled=False

- AzureStorageEnabled=False

- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}

- OrchestratorType=${ORCHESTRATOR_TYPE}

- UseLoadTest=${USE_LOADTEST:-False}

ports:

- "5110:80"

 

webmvc:

environment:

- ASPNETCORE_ENVIRONMENT=Development

- ASPNETCORE_URLS=http://0.0.0.0:80

- CatalogUrl=http://catalog.api

- OrderingUrl=http://ordering.api

- BasketUrl=http://basket.api

- LocationsUrl=http://locations.api

- IdentityUrl=http://10.0.75.1:5105

- MarketingUrl=http://marketing.api

- CatalogUrlHC=http://catalog.api/hc

- OrderingUrlHC=http://ordering.api/hc

- IdentityUrlHC=http://identity.api/hc

- BasketUrlHC=http://basket.api/hc

- MarketingUrlHC=http://marketing.api/hc

- PaymentUrlHC=http://payment.api/hc

- UseCustomizationData=True

- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}

- OrchestratorType=${ORCHESTRATOR_TYPE}

- UseLoadTest=${USE_LOADTEST:-False}

ports:

- "5100:80"

sql.data:

environment:

- MSSQL_SA_PASSWORD=Pass@word

- ACCEPT_EULA=Y

- MSSQL_PID=Developer

ports:

- "5433:1433"

nosql.data:

ports:

- "27017:27017"

basket.data:

ports:

- "6379:6379"

rabbitmq:

ports:

- "15672:15672"

- "5672:5672"

En este ejemplo, la configuración de reemplazo del desarrollo expone algunos puertos al host, define variables de entorno con URLs de redireccionamiento y especifica cadenas de conexión para el entorno de desarrollo. Estas configuraciones son sólo para el entorno de desarrollo.

Cuando ejecuta docker-compose up (o ejecuta la aplicación desde Visual Studio), el comando lee los reemplazos automáticamente, como si fusionara ambos ficheros.

Supongamos que quiere otro fichero Compose para el entorno de producción, con diferentes valores de configuración, puertos o cadenas de conexión. Puede crear otro fichero de reemplazo, como el fichero docker-compose.prod.yml, con diferentes configuraciones y variables de entorno. Ese fichero puede almacenarse en un repositorio Git diferente o ser administrado y asegurado por un equipo diferente.

Cómo desplegar con un fichero de reemplazo específico

Para utilizar varios ficheros de reemplazo, o un fichero de reemplazo con otro nombre, puede usar la opción -f con el comando docker-compose y especificar los ficheros. Compose combina los ficheros en el orden en que se especifican en la línea de comandos. El siguiente ejemplo muestra cómo desplegar con ficheros de reemplazo.

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

 

Usando variables de entorno en ficheros docker-compose

Es conveniente, especialmente en entornos de producción, poder obtener parámetros de configuración a partir de variables de entorno, como hemos mostrado en ejemplos anteriores. Puede hacer referencia a una variable de entorno en sus ficheros docker-compose utilizando la sintaxis ${MY_VAR}. La siguiente línea de un fichero docker-compose.prod.yml muestra cómo hacer referencia al valor de una variable de entorno.

IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105

 

Las variables de entorno se crean e inicializan de diferentes maneras, dependiendo del entorno de su host (Linux, Windows, cluster de la nube, etc.). Sin embargo, un enfoque conveniente es usar un fichero “.env”. Los ficheros docker-compose admiten la declaración de variables de entorno por defecto en el fichero “.env”. Esos son los valores por defecto para las variables de entorno, pero pueden ser reemplazados por los valores que haya definido en cada uno de sus entornos (sistema operativo host o variables de entorno de su cluster). Coloque el fichero “.env” en la carpeta donde se ejecuta el comando docker-compose.

El siguiente ejemplo muestra el contenido de un fichero “.env”, como el de la aplicación eShopOnContainers.

# .env file

ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost

ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP=10.121.122.92

 

Docker-compose espera que cada línea en un fichero “.env” tenga el formato <variable>=<valor>.

Tenga en cuenta que los valores establecidos en el entorno de ejecución siempre reemplazan los valores definidos dentro del fichero “.env”. De manera similar, los valores pasados a través de los argumentos de la línea de comando, también reemplazan los valores por defecto establecidos en el fichero “.env”.

Recursos adicionales

Construyendo imágenes Docker optimizadas para ASP.NET Core

Si está explorando acerca de Docker y .NET Core en la Internet, encontrará ficheros Dockerfiles que muestran lo fácil que es construir una imagen Docker al copiar su fuente en un contenedor. Estos ejemplos sugieren que, al usar una configuración simple, puede tener una imagen Docker con el entorno empaquetado con su aplicación. El siguiente ejemplo muestra un fichero Dockerfile simple dentro de esta línea de pensamiento.

FROM microsoft/dotnet

WORKDIR /app

ENV ASPNETCORE_URLS http://+:80

EXPOSE 80

COPY . .

RUN dotnet restore

ENTRYPOINT ["dotnet", "run"]

 

Un Dockerfile como este funcionará. Sin embargo, puede optimizar sustancialmente sus imágenes, especialmente sus imágenes de producción.

En el modelo de contenedores y microservicios, está constantemente iniciando contenedores. La forma típica de usar contenedores no es reiniciando un contenedor suspendido, porque el contenedor es desechable. Los orquestadores (como Docker Swarm, Kubernetes, DC/OS o Azure Service Fabric) simplemente crean nuevas instancias de imágenes. Lo que esto significa es que tendría que optimizar, precompilando la aplicación durante la construcción, para que el proceso de creación de instancias sea más rápido. Cuando se inicia el contenedor, debería estar listo para funcionar. No debería restaurar y compilar en tiempo de ejecución, utilizando los comandos dotnet restore y dotnet build de la CLI de dotnet, como se muestra en muchos artículos de blog sobre .NET Core y Docker.

El equipo de .NET ha estado haciendo un trabajo importante para optimizar .NET Core y ASP.NET Core para correr en contenedores. No sólo .NET Core es un framework liviano con poco uso de memoria; el equipo se ha enfocado en el rendimiento de inicio y ha producido algunas imágenes Docker optimizadas, como la imagen microsoft/aspnetcore disponible en Docker Hub, en comparación con las imágenes normales de microsoft/dotnet o microsoft/nanoserver. La imagen de microsoft/aspnetcore proporciona una configuración automática de aspnetcore_urls para el puerto 80 y el caché de ensamblados pre-ngend. Ambas configuraciones dan como resultado un inicio más rápido.

Recursos adicionales