Artículo original

https://blogs.msdn.microsoft.com/appconsult/2018/10/24/deploy-a-docker-multi-container-application-on-azure-web-apps/

Introducción

 En la última publicación de mi serie sobre Docker hemos visto cómo, gracias a Docker Compose, es fácil implementar una aplicación compuesta por varios componentes que se ejecutan en diferentes contenedores.

Gracias a Docker Compose podemos ejecutar o detener todos los contenedores que componen nuestra aplicación al mismo tiempo con un solo comando; Además, la herramienta se encarga de crear un puente de red dedicado, de modo que podamos ponerlos en comunicación fácilmente utilizando la dirección IP o el nombre DNS asignado.

 La mayor ventaja de este enfoque (y, en general, de los contenedores Docker) es que podemos tomar esta arquitectura y desplegarla en cualquier máquina que aloja el servicio Docker y asegurarnos de que siempre se ejecutará como se espera.

Sin embargo, hasta la última publicación, siempre hemos implementado nuestras soluciones basadas en contenedores en nuestra máquina local. Fue genial para propósitos de prueba y depuración, pero no es un escenario realista. Una aplicación real se ejecutaría en un servidor real, de modo que cualquier usuario pueda acceder a ella.

¿Y qué mejor lugar para alojar nuestras aplicaciones que la nube? Azure es una combinación perfecta para soluciones basadas en contenedores, ya que puede escalar rápidamente según la carga de trabajo.

Como hemos visto hasta ahora, Docker es solo un servicio que puede ejecutarse en cualquier máquina con Windows o Linux. Un enfoque simple para alojar nuestra aplicación sería crear una máquina virtual de Linux en Azure, instalar Docker e invocar nuestro comando Docker Compose. Sin embargo, esta solución tiene algunas desventajas. Desde que creamos la máquina virtual, somos responsables de ello. Debemos mantenerlo actualizado y actualizado con las últimas funciones de seguridad, etc.

¿Sería mucho mejor si pudiéramos usar una solución PaaS (Plataforma como servicio)? ¡Bienvenido Azure Web Apps!

Usando Azure Web Apps con Docker

Azure Web Apps es un servicio de Azure, que forma parte del reino del Servicio de aplicaciones de Azure, que puede alojar cualquier tipo de aplicación web. Es un servicio de PaaS, lo que significa que podemos centrarnos en el alojamiento de aplicaciones web, sin preocuparnos por el hardware y la configuración que se encuentran debajo. Podemos elegir la configuración de la máquina (según la carga de trabajo), podemos configurar la plataforma de la aplicación (por ejemplo, al elegir qué versión de .NET, PHP o Java queremos aprovechar), pero no somos responsables de mantener La plataforma de subrayado. No tenemos que cuidar de mantener el sistema operativo o los marcos actualizados.

Desde hace algún tiempo, el Servicio de aplicaciones de Azure ha agregado soporte para máquinas Linux, lo que significa que podemos alojar aplicaciones web en una máquina que ejecuta Linux en lugar de Windows. Con esta característica también obtenemos soporte completo de Docker, para un solo contenedor o para múltiples contenedores.

Esto significa que, al usar las mismas imágenes de Docker y componer el archivo que hemos creado para implementar nuestra solución localmente, podemos implementar nuestra aplicación también en Azure y obtener de inmediato todos los beneficios ofrecidos por esta poderosa plataforma.

¡Empecemos!

Primero, vaya al portal de Azure e inicie sesión con su cuenta. Elija Crear un recurso y, de la lista, elija Aplicación Web.

Se le pedirá que configure su aplicación web:

  • El nombre de la aplicación es un identificador único para la aplicación. También será el prefijo del dominio predeterminado para acceder a esta aplicación desde Internet. Por ejemplo, si lo llama MyFirstDockerApp, podrá acceder a él utilizando https://myfirstdockerapp.azurewebsites.net
  • La suscripción de Azure, en caso de tener uno o más.
  • El grupo de recursos, que es una forma de agrupar todos los servicios requeridos por la aplicación web para funcionar. Sugiero crear uno nuevo dedicado, para que en una etapa posterior pueda eliminar o mover fácilmente todos los recursos.
  • El sistema operativo. Elige Linux.
  • El plan / ubicación del Servicio de Aplicaciones. Utilice esta opción para elegir el plan de Servicio de aplicaciones que desea usar. Un plan de Servicio de aplicaciones es una combinación de ubicación (en la región de Azure en la que desea implementar la aplicación) y el nivel de precios (que refleja las características de hardware de la máquina de alojamiento). En la parte superior notará que los niveles de precios se dividen en tres categorías diferentes: Dev / Test, Production y Isolated. Ya que estamos en la fase de prueba, siéntase libre de elegir uno de los planes de la primera opción, que son gratuitos o muy económicos.

Hemos dejado una opción atrás, porque es la crítica que necesitamos para nuestra carga de trabajo. En Publicar, puede elegir Código o Docker Image. Una vez que elija la segunda opción, verá en la parte inferior un nuevo campo llamado Configurar contenedor. Al hacer clic en él, expandirá, a la derecha, un nuevo panel donde se especificará la configuración de sus contenedores:

Tienes tres opciones:

  • Se puede usar un solo contenedor para implementar una sola aplicación que se ejecuta en un solo contenedor
  • Docker Compose se puede utilizar para implementar una solución basada en múltiples contenedores utilizando el formato de archivo Docker Compose.
  • Kubernetes logra el mismo objetivo de la opción anterior, pero utilizando el formato de archivo Kubernetes para describir los contenedores. En caso de que nunca hayas oído hablar de ello, Kubernetes es una tecnología muy popular para organizar contenedores creados por Google.

 Independientemente de la opción, podrá utilizar diferentes fuentes de imagen:

  • Quickstart permite implementar rápidamente soluciones de muestra para probar la plataforma del Servicio de aplicaciones de Azure.
  • Azure Container Registry es una plataforma provista por Azure para alojar sus propias imágenes. Piense en ello como su propio repositorio personal de Docker Hub.
  • Docker Hub es el repositorio oficial de Docker que hemos aprendido a usar en las publicaciones anteriores. Desde aquí puede extraer todas las imágenes públicas publicadas por desarrolladores y empresas de todo el mundo.
  • El Registro privado es la opción para elegir en caso de que esté alojando sus imágenes en su propio repositorio privado.

 Nuestro escenario es una solución de múltiples contenedores basada en imágenes provenientes de Docker Hub. Si recuerda una de las publicaciones anteriores, hemos enviado las imágenes que hemos creado para nuestra aplicación web y nuestra API web a Docker Hub.

Vamos a elegir Docker Compose y luego a Docker Hub como fuente de imagen. Hemos publicado nuestras imágenes como públicas, así que deje el acceso Público como repositorio. En el campo Archivo de configuración presione Buscar y busque el archivo docker-compose.yml que hemos incorporado en una publicación anterior.

Verá, en el área de Configuración, el contenido del archivo YML.

Presione Aplicar y será redirigido a la página de configuración de la aplicación web. Esta vez, el campo Configurar contenedor debe informarse como Completo. Presione Crear en la parte inferior de la sección para iniciar la creación de la aplicación web.

La aplicación web ahora se implementará y, bajo el capó, Azure obtendrá las imágenes que hemos especificado en el archivo Docker Compose (qmatteoq \ testwebapp, qmatteoq \ testwebapi y redis) y girará un contenedor para cada una de ellas. Azure es lo suficientemente inteligente como para entender que el servicio llamado web es el único para el que estamos exponiendo el puerto 80. Como tal, este contenedor será el expuesto directamente a través del puerto 80 de la URL que se ha asignado a nuestro Servicio de aplicaciones.

Todo es increíble ... más o menos

 Vamos a darle una oportunidad a nuestro despliegue. Abra el navegador y apúntelo a la URL que se le asignó a su servicio de aplicaciones. Deberías ver esto:

Bueno, esto no es una agradable sorpresa. Algo malo está sucediendo. Específicamente, según el seguimiento de la pila, parece que nuestra aplicación web no se puede comunicar con la API web. El método GetStringAsync () del HttpClient, que estamos usando para recuperar la lista de publicaciones del blog de la API web, está fallando.

¿Es posible obtener más detalles? Claro, pero primero tenemos que habilitar algunos registros. Vaya a la sección Registros de diagnóstico de su aplicación web y enciéndala seleccionando Sistema de archivos.

Ahora intente volver a cargar el sitio web para reproducir el error. Ahora deberíamos tener algún registro en el sistema de archivos de la aplicación web. La forma más fácil de acceder a ellos es elegir Herramientas avanzadas en la sección Herramientas de desarrollo de la aplicación web.

Presione el botón Ir para abrir Kudu, que es una interfaz web de diagnóstico para su aplicación. Normalmente, Kudu está disponible en la misma URL de la aplicación principal, pero con el prefijo scm entre el nombre de la aplicación y el dominio. Por ejemplo, si su aplicación está alojada en https://mydockerwebapp.azurewebsites.net, la interfaz de Kudu estará disponible en https://mydockerwebapp.scm.azurewebsites.net.

 

Como puede ver en la interfaz web, una de las API REST disponibles se denomina registros de Docker actual:

Haga clic en el botón Descargar como zip para descargar un archivo comprimido, que contendrá varios archivos de registro: uno genérico para el servicio Docker y uno específico para cada contenedor que se haya creado.

Comencemos por abrir el relacionado con la aplicación web, que termina con el sufijo web_docker.

Este es el error que vemos:

2018-10-25T10:54:59.901632419Z       An unhandled exception has occurred while executing the request.
2018-10-25T10:54:59.901706119Z System.Net.Http.HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).
2018-10-25T10:54:59.901714719Z    at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
2018-10-25T10:54:59.901719519Z    at System.Net.Http.HttpClient.GetStringAsyncCore(Task`1 getTask)
2018-10-25T10:54:59.901774720Z    at TestWebApp.Pages.IndexModel.OnGetAsync() in /src/TestWebApp/Pages/Index.cshtml.cs:line 18
2018-10-25T10:54:59.901782320Z    at Microsoft.AspNetCore.Mvc.RazorPages.Internal.ExecutorFactory.NonGenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments)
2018-10-25T10:54:59.901786720Z    at Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker.InvokeHandlerMethodAsync()
2018-10-25T10:54:59.901859320Z    at Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker.InvokeNextPageFilterAsync()
2018-10-25T10:54:59.901867120Z    at Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
2018-10-25T10:54:59.901911520Z    at Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
2018-10-25T10:54:59.901927920Z    at Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker.InvokeInnerFilterAsync()
2018-10-25T10:54:59.901932220Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
2018-10-25T10:54:59.902024521Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
2018-10-25T10:54:59.902032421Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
2018-10-25T10:54:59.902095221Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
2018-10-25T10:54:59.902103021Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
2018-10-25T10:54:59.902107221Z    at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
2018-10-25T10:54:59.902159422Z    at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
2018-10-25T10:54:59.902174122Z    at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
2018-10-25T10:54:59.930285062Z       Request finished in 85.9709ms 500 text/html; charset=utf-8

Esta información no es realmente útil porque es el mismo error que vemos en el seguimiento de la pila de la página web. La API web no puede devolver la lista de publicaciones del blog a la aplicación web. Así que tal vez algo malo está sucediendo con la API web en sí. Echemos un vistazo al registro con el sufijo newsfeed_docker.

2018-10-25T10:54:59.881446518Z       An unhandled exception has occurred while executing the request.
2018-10-25T10:54:59.881562818Z StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). UnableToConnect on rediscache:6379/Interactive, Initializing, last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, unanswered-write: 783901s ago, keep-alive: 60s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, global: 53s ago
2018-10-25T10:54:59.881571918Z    at StackExchange.Redis.ConnectionMultiplexer.ConnectImplAsync(Object configuration, TextWriter log) in C:\projects\stackexchange-redis\src\StackExchange.Redis\ConnectionMultiplexer.cs:line820
2018-10-25T10:54:59.881632919Z    at TestWebApi.Controllers.NewsController.Get() in /app/Controllers/NewsController.cs:line 22
2018-10-25T10:54:59.881640719Z    at lambda_method(Closure , Object )
2018-10-25T10:54:59.881644719Z    at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()
2018-10-25T10:54:59.881696719Z    at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
2018-10-25T10:54:59.881704419Z    at System.Threading.Tasks.ValueTask`1.get_Result()
2018-10-25T10:54:59.881762619Z    at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()
2018-10-25T10:54:59.881781119Z    at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
2018-10-25T10:54:59.881785419Z    at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
2018-10-25T10:54:59.881846920Z    at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
2018-10-25T10:54:59.881854320Z    at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
2018-10-25T10:54:59.881937520Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
2018-10-25T10:54:59.881944820Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
2018-10-25T10:54:59.881948520Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
2018-10-25T10:54:59.882008221Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
2018-10-25T10:54:59.882015221Z    at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
2018-10-25T10:54:59.882018921Z    at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
2018-10-25T10:54:59.882073921Z    at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
2018-10-25T10:54:59.913042876Z       Request finished in 66.4934ms 500 text/html; charset=utf-8

¡Ahora estamos recibiendo un error mucho más significativo! Como puede ver en la descripción, el problema parece ser la conexión entre la API web y el caché de Redis. La conexión con el caché de Redis para recuperar la lista de publicaciones en caché está fallando.

¿Que esta pasando aqui? Este problema me llevó un tiempo descifrarlo porque la misma arquitectura de múltiples contenedores funcionaba bien en mi máquina local. Cuando implementa una solución utilizando Docker Compose, gracias al puente dedicado, todos los contenedores están alojados en la misma red, por lo que pueden comunicarse fácilmente entre sí utilizando su dirección IP o nombre DNS. Como tal, al principio no tenía idea de por qué la API web no podía comunicarse con el caché de Redis.

Podemos simplificar la comunicación entre varios contenedores utilizando su nombre. El mismo concepto se aplicó durante la fase de implementación, cuando hemos establecido la propiedad container_name en el archivo Docker Compose.

Esta es la configuración que hemos especificado para el contenedor de caché Redis:

redis:
  image: redis
  container_name: rediscache

Dado que hemos especificado rediscache como nombre del contenedor, hemos configurado la API web para que se conecte a la memoria caché de Redis usando este nombre. Este es un extracto del código que escribimos anteriormente en el controlador de la API web:

[Route("api/[controller]")]
[ApiController]
public class NewsController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public async Task<ActionResult<IEnumerable<string>>> Get()
    {
        ConnectionMultiplexer connection = await ConnectionMultiplexer.ConnectAsync("rediscache");
        var db = connection.GetDatabase();
 
        //code to parse the XML
    }
}

La conexión al contenedor rediscache, realizada por el método ConnectAsync () de la clase ConnectionMultiplexer, está fallando. La razón es que Azure ignora la propiedad container_name incluida en el archivo Docker Compose, pero establece el nombre del contenedor simplemente usando el nombre del servicio.

En nuestro archivo Docker Compose, el nombre que hemos configurado para el servicio simplemente se vuelve a borrar, por lo que debemos cambiar el código de la API web para usar el nuevo punto final:

[Route("api/[controller]")]
[ApiController]
public class NewsController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public async Task<ActionResult<IEnumerable<string>>> Get()
    {
        ConnectionMultiplexer connection = await ConnectionMultiplexer.ConnectAsync("redis");
        var db = connection.GetDatabase();
 
        //code to parse the XML
    }
}

Después de haber aplicado esta solución, necesitamos crear una nueva imagen y empujarla nuevamente a Docker Hub. Al principio, ejecute el siguiente comando desde la carpeta que contiene la API web:

docker build --rm -t qmatteoq/testwebapi:latest .

Luego empújelo a Docker Hub usando el siguiente comando:

docker push qmatteoq/testwebapi

Una vez hecho esto, vuelva al portal de Azure y, en la aplicación web, elija la sección de configuración del contenedor. Esto mostrará el mismo panel que ha utilizado durante la creación de la aplicación web para configurar su solución basada en Docker. Presione Buscar y busque nuevamente el archivo Docker Compose, luego presione Guardar al final de la página. Esto activará un nuevo despliegue de la solución. Azure extraerá las imágenes actualizadas del concentrador Docker y hará girar un nuevo conjunto de contenedores.

Si hizo todo bien, ahora si intenta volver a golpear la URL de su aplicación web, debería ver la aplicación en funcionamiento:

Y si lo actualizamos, debería ver el cambio del primer elemento en la lista de publicaciones del blog, para demostrar que la lista de publicaciones se recuperó de la memoria caché de Redis y no se volvió a descargar del RSS original.

Siguientes Pasos

En esta publicación, hemos aprendido cómo podemos usar los Servicios de aplicaciones de Azure para implementar nuestra aplicación de múltiples contenedores en la nube, de modo que también se pueda acceder a ella fácilmente desde otros usuarios, se pueda escalar fácilmente en caso de grandes cargas de trabajo, etc. Se realizó utilizando un servicio de PaaS, lo que significa que no debemos preocuparnos por mantener el hardware, la máquina virtual, el sistema operativo o el servicio Docker. Solo debemos enfocarnos en nuestra aplicación.

Sin embargo, para lograr este objetivo, en esta publicación hemos realizado muchas tareas manuales. Tuvimos que volver a publicar manualmente nuestra imagen una vez que la hemos construido; tuvimos que realizar manualmente una nueva implementación del archivo Docker Compose para permitir que Azure elija la nueva versión de la imagen que hemos publicado en Docker Hub; etc.

 En la próxima publicación veremos cómo podemos automatizar este proceso gracias a Azure DevOps y Docker Hub.