Implementación de Docker

Sugerencia

Incluso si está muy familiarizado con Docker o con Orleans, se recomienda, tal como lo haría con cualquier otra documentación de Orleans, que la lea hasta el final para evitar cualquier problema que pueda encontrar y que ya hayamos abordado.

Este artículo y los ejemplos que contiene son un trabajo en curso. Cualquier comentario, solicitud de incorporación de cambios o sugerencia es muy bienvenido.

Implementación de soluciones de Orleans en Docker

La implementación de Orleans en Docker puede ser complicada dada la forma en que se diseñaron las pilas de clústeres y los orquestadores de Docker. Lo más complicado es comprender el concepto de red superpuesta del modelo de redes de Kubernetes y Docker Swarm.

Los contenedores Docker y los modelos de red se diseñaron para ejecutar principalmente contenedores inmutables y sin estado. Por lo tanto, resulta muy sencillo poner en marcha un clúster que ejecuta aplicaciones node.js o Nginx. Sin embargo, si intenta usar algo más elaborado, como una aplicación distribuida o agrupada real (como las basadas en Orleans), a la larga tendrá problemas para configurarla. Es posible, pero no es tan sencillo como las aplicaciones basadas en Web.

La agrupación en clústeres de Docker consiste en reunir varios hosts para que funcionen como un grupo de recursos único, administrado mediante un orquestador de contenedores. Docker Inc. ofrece Swarm como su opción para la orquestación de contenedores, mientras que Google tiene Kubernetes (también conocido como K8s). Existen otros orquestadores, como DC/OS, Mesos, pero este documento se centrará en Swarm y K8s, que son los más usados.

La misma implementación y las mismas interfaces de grano que se ejecutan en cualquier lugar en el que Orleans ya es compatible se ejecutarán también en contenedores Docker. No se necesita ninguna consideración especial para poder ejecutar la aplicación en contenedores Docker.

Los conceptos que se describen aquí se pueden usar en los tipos .NET Core y .NET 4.6.1 de Orleans, pero para ilustrar la naturaleza multiplataforma de Docker y .NET Core, nos centraremos en un ejemplo en que se usa .NET Core. En este artículo, es posible que se proporcionen detalles específicos de la plataforma (Windows/Linux/OSX).

Requisitos previos

En este artículo, se supone que ya tiene instalados estos requisitos previos:

  • Docker: Docker4X tiene un instalador fácil de usar para las plataformas compatibles importantes. Contiene el motor de Docker y Docker Swarm.
  • Kubernetes (K8s): es la oferta de Google para la orquestación de contenedores. Contiene una guía para instalar Minikube (una implementación local de K8s) y kubectl junto con todas sus dependencias.
  • .NET: tipo multiplataforma de .NET
  • Visual Studio Code (VSCode): puede utilizar el IDE que quiera. VSCode es multiplataforma, por lo que lo usamos para garantizar que funcione en todas las plataformas. Una vez que instale VSCode, instale la extensión de C#.

Importante

No necesita tener instalado Kubernetes si no va a utilizarlo. El instalador de Docker4X ya incluye Swarm, por lo que no se necesita ninguna instalación adicional para usarlo.

Nota

En Windows, el instalador de Docker habilitará Hyper-V en el proceso de instalación. Como en este artículo y los ejemplos que aparecen en él se usa .NET Core, las imágenes de contenedor que se usan se basan en Windows Server NanoServer. Si no tiene previsto usar .NET Core y desea enfocarse en el marco completo de .NET 4.6.1, la imagen usada debería ser Windows Server Core y la versión 1.4 o superior de Orleans (que solo admite el marco completo de .NET).

Creación de una solución de Orleans

En las instrucciones siguientes, se muestra cómo crear una solución regular de Orleans con las herramientas dotnet nuevas.

Adapte los comandos para adecuarlos a su plataforma. Además, la estructura de directorios no es más que una sugerencia. Adáptela a sus necesidades.

mkdir Orleans-Docker
cd Orleans-Docker
dotnet new sln
mkdir -p src/OrleansSilo
mkdir -p src/OrleansClient
mkdir -p src/OrleansGrains
mkdir -p src/OrleansGrainInterfaces
dotnet new console -o src/OrleansSilo --framework netcoreapp1.1
dotnet new console -o src/OrleansClient --framework netcoreapp1.1
dotnet new classlib -o src/OrleansGrains --framework netstandard1.5
dotnet new classlib -o src/OrleansGrainInterfaces --framework netstandard1.5
dotnet sln add src/OrleansSilo/OrleansSilo.csproj
dotnet sln add src/OrleansClient/OrleansClient.csproj
dotnet sln add src/OrleansGrains/OrleansGrains.csproj
dotnet sln add src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj
dotnet add src/OrleansClient/OrleansClient.csproj reference src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj
dotnet add src/OrleansSilo/OrleansSilo.csproj reference src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj
dotnet add src/OrleansGrains/OrleansGrains.csproj reference src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj
dotnet add src/OrleansSilo/OrleansSilo.csproj reference src/OrleansGrains/OrleansGrains.csproj

Hasta ahora, solo hemos reutilizado código para crear la estructura de la solución y los proyectos, además de agregar referencias entre ellos. Nada distinto de un proyecto regular de Orleans.

En el momento de escribir este artículo, Orleans 2.0 (que es la única versión que admite .NET Core y multiplataforma) se encuentra en versión Technology Preview, por lo que los paquetes de NuGet se hospedan en una fuente MyGet y no se publican en la fuente oficial de NuGet.org. Para instalar los paquetes de NuGet de la versión preliminar, usaremos la CLI de dotnet y forzaremos la fuente de origen y la versión de MyGet:

dotnet add src/OrleansClient/OrleansClient.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet add src/OrleansGrainInterfaces/OrleansGrainInterfaces.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet add src/OrleansGrains/OrleansGrains.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet add src/OrleansSilo/OrleansSilo.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet add src/OrleansSilo/OrleansSilo.csproj package Microsoft.Orleans.OrleansRuntime -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000
dotnet restore

Ahora cuenta con todas las dependencias básicas para ejecutar una aplicación de Orleans sencilla. Observe que, hasta este momento, no ha cambiado nada respecto de la aplicación regular de Orleans. Ahora agregaremos código con el que podamos hacer algo.

Implementación de una aplicación de Orleans

Suponiendo que usa VSCode, ejecute code . en el directorio de la solución. Se abrirá el directorio en VSCode y se cargará la solución.

Esta es la estructura de la solución que acabamos de crear.

Visual Studio Code: Explorer with Program.cs selected.

También agregamos los archivos Program.cs, OrleansHostWrapper.cs, IGreetingGrain.cs y GreetingGrain.cs a las interfaces y a los proyectos de grano, respectivamente, y el código para esos archivos es el siguiente:

IGreetingGrain.cs:

using System;
using System.Threading.Tasks;
using Orleans;

namespace OrleansGrainInterfaces
{
    public interface IGreetingGrain : IGrainWithGuidKey
    {
        Task<string> SayHello(string name);
    }
}

GreetingGrain.cs:

using System;
using System.Threading.Tasks;
using OrleansGrainInterfaces;

namespace OrleansGrains
{
    public class GreetingGrain : Grain, IGreetingGrain
    {
        public Task<string> SayHello(string name)
        {
            return Task.FromResult($"Hello from Orleans, {name}");
        }
    }
}

OrleansHostWrapper.cs:

using System;
using System.NET;
using Orleans.Runtime;
using Orleans.Runtime.Configuration;
using Orleans.Runtime.Host;

namespace OrleansSilo;

public class OrleansHostWrapper
{
    private readonly SiloHost _siloHost;

    public OrleansHostWrapper(ClusterConfiguration config)
    {
        _siloHost = new SiloHost(Dns.GetHostName(), config);
        _siloHost.LoadOrleansConfig();
    }

    public int Run()
    {
        if (_siloHost is null)
        {
            return 1;
        }

        try
        {
            _siloHost.InitializeOrleansSilo();

            if (_siloHost.StartOrleansSilo())
            {
                Console.WriteLine(
                    $"Successfully started Orleans silo '{_siloHost.Name}' as a {_siloHost.Type} node.");
                return 0;
            }
            else
            {
                throw new OrleansException(
                    $"Failed to start Orleans silo '{_siloHost.Name}' as a {_siloHost.Type} node.");
            }
        }
        catch (Exception exc)
        {
            _siloHost.ReportStartupError(exc);
            Console.Error.WriteLine(exc);

            return 1;
        }
    }

    public int Stop()
    {
        if (_siloHost is not null)
        {
            try
            {
                _siloHost.StopOrleansSilo();
                _siloHost.Dispose();
                Console.WriteLine($"Orleans silo '{_siloHost.Name}' shutdown.");
            }
            catch (Exception exc)
            {
                siloHost.ReportStartupError(exc);
                Console.Error.WriteLine(exc);

                return 1;
            }
        }
        return 0;
    }
}

Program.cs (Silo):

using System;
using System.Collections.Generic;
using System.Linq;
using System.NET;
using System.Threading.Tasks;
using Orleans.Runtime.Configuration;

namespace OrleansSilo
{
    public class Program
    {
        private static OrleansHostWrapper s_hostWrapper;

        static async Task<int> Main(string[] args)
        {
            int exitCode = await InitializeOrleansAsync();

            Console.WriteLine("Press Enter to terminate...");
            Console.ReadLine();

            exitCode += ShutdownSilo();

            return exitCode;
        }

        private static int InitializeOrleansAsync()
        {
            var config = new ClusterConfiguration();
            config.Globals.DataConnectionString =
                "[AZURE STORAGE CONNECTION STRING HERE]";
            config.Globals.DeploymentId = "Orleans-Docker";
            config.Globals.LivenessType =
                GlobalConfiguration.LivenessProviderType.AzureTable;
            config.Globals.ReminderServiceType =
                GlobalConfiguration.ReminderServiceProviderType.AzureTable;
            config.Defaults.PropagateActivityId = true;
            config.Defaults.ProxyGatewayEndpoint =
                new IPEndPoint(IPAddress.Any, 10400);
            config.Defaults.Port = 10300;
            var ips = await Dns.GetHostAddressesAsync(Dns.GetHostName());
            config.Defaults.HostNameOrIPAddress =
                ips.FirstOrDefault()?.ToString();

            s_hostWrapper = new OrleansHostWrapper(config);
            return hostWrapper.Run();
        }

        static int ShutdownSilo() =>
            s_hostWrapper?.Stop() ?? 0;
    }
}

Program.cs (cliente):

using System;
using System.NET;
using System.Threading;
using System.Threading.Tasks;
using Orleans;
using Orleans.Runtime.Configuration;
using OrleansGrainInterfaces;

namespace OrleansClient
{
    class Program
    {
        private static IClusterClient s_client;
        private static bool s_running;

        static async Task Main(string[] args)
        {
            await InitializeOrleansAsync();

            Console.ReadLine();

            s_running = false;
        }

        static async Task InitializeOrleansAsync()
        {
            var config = new ClientConfiguration
            {
                DeploymentId = "Orleans-Docker";
                PropagateActivityId = true;
            };
            var hostEntry =
                await Dns.GetHostEntryAsync("orleans-silo");
            var ip = hostEntry.AddressList[0];
            config.Gateways.Add(new IPEndPoint(ip, 10400));

            Console.WriteLine("Initializing...");

            using client = new ClientBuilder().UseConfiguration(config).Build();
            await client.Connect();
            s_running = true;
            Console.WriteLine("Initialized!");

            var grain = client.GetGrain<IGreetingGrain>(Guid.Empty);

            while (s_running)
            {
                var response = await grain.SayHello("Gutemberg");
                Console.WriteLine($"[{DateTime.UtcNow}] - {response}");

                await Task.Delay(1000);
            }
        }
    }
}

Aquí no vamos a profundizar en la implementación de grano, ya que escapa del ámbito de este artículo. Consulte otra documentación relacionada. En esencia, esos archivos son una aplicación de Orleans mínima y esta será nuestro punto de partida para ir avanzando en el resto del artículo.

En este artículo, usamos el proveedor de pertenencia OrleansAzureUtils, pero puede utilizar cualquier otro proveedor que Orleans ya admita.

El archivo Dockerfile

Docker usa imágenes para crear el contenido. Si desea más información sobre cómo crear contenido propio, consulte la documentación de Docker. En este artículo, usaremos imágenes oficiales de Microsoft. Elija la imagen adecuada en función de las plataformas de destino y desarrollo. En este artículo, usamos microsoft/dotnet:1.1.2-sdk, una imagen basada en Linux. Por ejemplo, puede usar microsoft/dotnet:1.1.2-sdk-nanoserver para Windows. Elija una que se adapte a lo que necesita.

Nota para los usuarios de Windows: Tal como ya se mencionó, para ser multiplataforma, en este artículo usamos .NET Core y Orleans Technical Preview 2.0. Si quiere usar Docker en Windows con la versión totalmente publicada de Orleans 1.4 y versiones posteriores, debe usar las imágenes basadas en Windows Server Core, ya que las imágenes basadas en Linux y NanoServer solo admiten .NET Core.

Dockerfile.debug:

FROM microsoft/dotnet:1.1.2-sdk
ENV NUGET_XMLDOC_MODE skip
WORKDIR /vsdbg
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        unzip \
    && rm -rf /var/lib/apt/lists/* \
    && curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg
WORKDIR /app
ENTRYPOINT ["tail", "-f", "/dev/null"]

Básicamente, se usa este archivo Dockerfile para descargar e instalar el depurador de VSdbg, así como para iniciar un contenedor vacío y mantenerlo activo para siempre, a fin de que no sea necesario anularlo o quitarlo durante la depuración.

Ahora bien, la imagen es más pequeña para el entorno de producción, porque solo contiene el runtime de .NET Core y no todo el SDK. Además, el archivo dockerfile es un poco más sencillo:

Dockerfile:

FROM microsoft/dotnet:1.1.2-runtime
WORKDIR /app
ENTRYPOINT ["dotnet", "OrleansSilo.dll"]
COPY . /app

El archivo docker-compose

En esencia, el archivo docker-compose.yml define (dentro de un proyecto) un conjunto de servicios y sus dependencias en el nivel de servicio. Cada servicio contiene una o varias instancias de un contenedor determinado que se basa en las imágenes seleccionadas en el archivo Dockerfile. Puede encontrar más detalles sobre el archivo docker-compose en la documentación de docker-compose.

Para una implementación de Orleans, un caso de uso común es tener un archivo docker-compose.yml que contiene dos servicios, uno para Orleans Silo y el otro para Orleans Client. Client dependerá de Silo, lo que significa que solo se iniciará una vez que se active el servicio de Silo. Otro caso es agregar un almacenamiento, un servicio de base de datos o un contenedor, como SQL Server, que se debe iniciar antes que el cliente y el silo, por lo que ambos servicios dependerán de él.

Nota

Antes de seguir leyendo, tenga en cuenta que la sangríatiene un significado en los archivos docker-compose. Así es que, si tiene problemas, póngale atención.

A continuación, se muestra cómo describiremos los servicios para este artículo:

docker-compose.override.yml (depuración):

version: '3.1'

services:
  orleans-client:
    image: orleans-client:debug
    build:
      context: ./src/OrleansClient/bin/PublishOutput/
      dockerfile: Dockerfile.Debug
    volumes:
      - ./src/OrleansClient/bin/PublishOutput/:/app
      - ~/.nuget/packages:/root/.nuget/packages:ro
    depends_on:
      - orleans-silo
  orleans-silo:
    image: orleans-silo:debug
    build:
      context: ./src/OrleansSilo/bin/PublishOutput/
      dockerfile: Dockerfile.Debug
    volumes:
      - ./src/OrleansSilo/bin/PublishOutput/:/app
      - ~/.nuget/packages:/root/.nuget/packages:ro

docker-compose.yml (producción):

version: '3.1'

services:
  orleans-client:
    image: orleans-client
    depends_on:
      - orleans-silo
  orleans-silo:
    image: orleans-silo

En producción, no asignamos el directorio local ni tampoco tenemos la acción build:. Esto se debe a que, en el entorno de producción, las imágenes se deben crear e insertar en su propio Registro de Docker.

Todo junto

Ahora que tenemos todos los elementos móviles que se requieren para ejecutar la aplicación de Orleans, los uniremos a fin de poder ejecutar la solución de Orleans en Docker (¡por fin!).

Importante

Se deben ejecutar los comandos siguientes desde el directorio de la solución.

En primer lugar, nos aseguraremos de restaurar todos los paquetes de NuGet desde la solución. No tiene que hacerlo más de una vez. Solo tendrá que hacerlo de nuevo si cambia alguna dependencia de paquete en el proyecto.

dotnet restore

Ahora, compilaremos la solución mediante la CLI de dotnet como de costumbre y la publicaremos en un directorio de salida:

dotnet publish -o ./bin/PublishOutput

Sugerencia

Aquí usamos publish en lugar de build a fin de evitar problemas con los ensamblados que se cargaron dinámicamente en Orleans. Seguimos buscando una mejor solución para esto.

Una vez que se compila y publica la aplicación, debe crear las imágenes de Dockerfile. Este paso solo se hace una vez por cada proyecto y solo se debe repetir si cambia el archivo Dockerfile, docker-compose o si, por cualquier motivo, limpió el registro local de imágenes.

docker-compose build

Todas las imágenes que se usan en Dockerfile y docker-compose.yml se extraen del registro y se almacenan en caché en la máquina de desarrollo. Una vez que se crean las imágenes, ya está listo para empezar.

¡Comencemos!

# docker-compose up -d
Creating network "orleansdocker_default" with the default driver
Creating orleansdocker_orleans-silo_1 ...
Creating orleansdocker_orleans-silo_1 ... done
Creating orleansdocker_orleans-client_1 ...
Creating orleansdocker_orleans-client_1 ... done
#

Ahora, si ejecuta docker-compose ps, verá 2 contenedores en ejecución para el proyecto orleansdocker:

# docker-compose ps
             Name                     Command        State   Ports
------------------------------------------------------------------
orleansdocker_orleans-client_1   tail -f /dev/null   Up
orleansdocker_orleans-silo_1     tail -f /dev/null   Up

Nota

Si está en Windows y el contenedor utiliza una imagen de Windows como base, la columna Command mostrará el comando relativo de PowerShell a tail en sistemas *NIX, por lo que el contenedor mantendrá el mismo comportamiento.

Ahora que los contenedores están activos, no es necesario que los detenga cada vez que quiera iniciar la aplicación de Orleans. Todo lo que tiene que hacer es integrar el IDE para depurar la aplicación dentro del contenedor que se asignó anteriormente en docker-compose.yml.

Ampliación

Una vez que el proyecto de Compose esté en ejecución, puede escalar o reducir verticalmente la aplicación de manera sencilla con el comando docker-compose scale:

# docker-compose scale orleans-silo=15
Starting orleansdocker_orleans-silo_1 ... done
Creating orleansdocker_orleans-silo_2 ...
Creating orleansdocker_orleans-silo_3 ...
Creating orleansdocker_orleans-silo_4 ...
Creating orleansdocker_orleans-silo_5 ...
Creating orleansdocker_orleans-silo_6 ...
Creating orleansdocker_orleans-silo_7 ...
Creating orleansdocker_orleans-silo_8 ...
Creating orleansdocker_orleans-silo_9 ...
Creating orleansdocker_orleans-silo_10 ...
Creating orleansdocker_orleans-silo_11 ...
Creating orleansdocker_orleans-silo_12 ...
Creating orleansdocker_orleans-silo_13 ...
Creating orleansdocker_orleans-silo_14 ...
Creating orleansdocker_orleans-silo_15 ...
Creating orleansdocker_orleans-silo_6
Creating orleansdocker_orleans-silo_5
Creating orleansdocker_orleans-silo_3
Creating orleansdocker_orleans-silo_2
Creating orleansdocker_orleans-silo_4
Creating orleansdocker_orleans-silo_9
Creating orleansdocker_orleans-silo_7
Creating orleansdocker_orleans-silo_8
Creating orleansdocker_orleans-silo_10
Creating orleansdocker_orleans-silo_11
Creating orleansdocker_orleans-silo_15
Creating orleansdocker_orleans-silo_12
Creating orleansdocker_orleans-silo_14
Creating orleansdocker_orleans-silo_13

Después de unos segundos, verá que los servicios se escalan al número específico de instancias que solicitó.

# docker-compose ps
             Name                     Command        State   Ports
------------------------------------------------------------------
orleansdocker_orleans-client_1   tail -f /dev/null   Up
orleansdocker_orleans-silo_1     tail -f /dev/null   Up
orleansdocker_orleans-silo_10    tail -f /dev/null   Up
orleansdocker_orleans-silo_11    tail -f /dev/null   Up
orleansdocker_orleans-silo_12    tail -f /dev/null   Up
orleansdocker_orleans-silo_13    tail -f /dev/null   Up
orleansdocker_orleans-silo_14    tail -f /dev/null   Up
orleansdocker_orleans-silo_15    tail -f /dev/null   Up
orleansdocker_orleans-silo_2     tail -f /dev/null   Up
orleansdocker_orleans-silo_3     tail -f /dev/null   Up
orleansdocker_orleans-silo_4     tail -f /dev/null   Up
orleansdocker_orleans-silo_5     tail -f /dev/null   Up
orleansdocker_orleans-silo_6     tail -f /dev/null   Up
orleansdocker_orleans-silo_7     tail -f /dev/null   Up
orleansdocker_orleans-silo_8     tail -f /dev/null   Up
orleansdocker_orleans-silo_9     tail -f /dev/null   Up

Importante

La columna Command de esos ejemplos muestra el comando tail solo porque usamos el contenedor del depurador. Por ejemplo, si estuviésemos en el entorno de producción, mostraría dotnet OrleansSilo.dll.

Docker Swarm

La pila de agrupación en clústeres de Docker se denomina Swarm. Para más información, consulte Docker Swarm.

Si quiere ejecutar este artículo en un clúster de Swarm, no tiene trabajo adicional. Cuando ejecuta docker-compose up -d en un nodo de Swarm, se programarán contenedores en función de las reglas configuradas. Esto mismo se aplica a otros servicios basados en Swarm, como Docker Datacenter, Azure ACS (en modo Swarm) y AWS ECS Container Service. Lo único que tiene que hacer es implementar el clúster de Swarm antes de implementar la aplicación de Orleansdockerizada.

Nota

Si usa un motor de Docker con el modo Swarm que ya tiene compatibilidad con stack, deploy y compose v3, un mejor enfoque para implementar la solución sería docker stack deploy -c docker-compose.yml <name>. Tenga en cuenta que necesita el archivo de Compose v3 para admitir el motor de Docker y la mayoría de los servicios hospedados, como Azure y AWS, siguen usando motores v2 y anteriores.

Google Kubernetes (K8s)

Si tiene pensado usar Kubernetes para hospedar Orleans, hay disponible un proveedor de agrupación en clústeres mantenido por la comunidad en OrleansContrib\Orleans.Clustering.Kubernetes. Ahí también puede encontrar documentación y ejemplos de cómo hospedar Orleans en Kubernetes sin problemas mediante el proveedor.

Depuración de Orleans dentro de contenedores

Ahora que sabe cómo ejecutar Orleans en un contenedor desde cero, sería bueno aprovechar uno de los principios más importantes de Docker. los contenedores son inmutables. Y deben tener (casi) la misma imagen, dependencias y runtime en el entorno de desarrollo que en el de producción. Esto garantiza que la clásica afirmación "¡Pero si funciona en mi máquina!" nunca vuelva a repetirse. Para que eso sea posible, debe poder desarrollar dentro del contenedor y eso incluye tener un depurador asociado a la aplicación dentro del contenedor.

Hay varias herramientas para lograrlo. Después de evaluar varias, en el momento en que escribí este artículo, elegí una que se ve más sencilla y es menos intrusiva para la aplicación.

Como se mencionó anteriormente en este artículo, usamos VSCode para desarrollar el ejemplo, y aquí puede ver cómo asociar el depurador a la aplicación de Orleans dentro del contenedor.

En primer lugar, cambie dos archivos dentro del directorio .vscode en la solución:

tasks.json:

{
    "version": "0.1.0",
    "command": "dotnet",
    "isShellCommand": true,
    "args": [],
    "tasks": [
        {
            "taskName": "publish",
            "args": [
                "${workspaceRoot}/Orleans-Docker.sln", "-c", "Debug", "-o", "./bin/PublishOutput"
            ],
            "isBuildCommand": true,
            "problemMatcher": "$msCompile"
        }
    ]
}

Básicamente, lo que este archivo hace es indicar a VSCode que cada vez que compile el proyecto se ejecutará el comando publish como lo hicimos manualmente antes.

launch.json:

{
   "version": "0.2.0",
   "configurations": [
        {
            "name": "Silo",
            "type": "coreclr",
            "request": "launch",
            "cwd": "/app",
            "program": "/app/OrleansSilo.dll",
            "sourceFileMap": {
                "/app": "${workspaceRoot}/src/OrleansSilo"
            },
            "pipeTransport": {
                "debuggerPath": "/vsdbg/vsdbg",
                "pipeProgram": "/bin/bash",
                "pipeCwd": "${workspaceRoot}",
                "pipeArgs": [
                    "-c",
                    "docker exec -i orleansdocker_orleans-silo_1 /vsdbg/vsdbg --interpreter=vscode"
                ]
            }
        },
        {
            "name": "Client",
            "type": "coreclr",
            "request": "launch",
            "cwd": "/app",
            "program": "/app/OrleansClient.dll",
            "sourceFileMap": {
                "/app": "${workspaceRoot}/src/OrleansClient"
            },
            "pipeTransport": {
                "debuggerPath": "/vsdbg/vsdbg",
                "pipeProgram": "/bin/bash",
                "pipeCwd": "${workspaceRoot}",
                "pipeArgs": [
                    "-c",
                    "docker exec -i orleansdocker_orleans-client_1 /vsdbg/vsdbg --interpreter=vscode"
                ]
            }
        }
    ]
}

Ahora puede simplemente compilar la solución desde VSCode (que publicará) e iniciar tanto el servicio de Silo como el de Client. Enviará un comando docker exec al contenedor o a la instancia de servicio docker-compose que esté en ejecución para iniciar el depurador en la aplicación. Y listo. Ahora, el depurador está asociado al contenedor y lo usará como si fuese una aplicación de Orleans en ejecución en el entorno local. La diferencia ahora es que está dentro del contenedor y, una vez que haya terminado, podrá simplemente publicar el contenedor en el registro e insertarlo en los hosts de Docker en el entorno de producción.