Back-End

26 jul, 2018

.NET Core + Docker: executando uma Console Application como um serviço

Publicidade

Mesmo contando com templates para a geração de diferentes tipos de projetos, o .NET Core não dispõe de uma opção para a criação de aplicações que serão executadas em background como serviços.

Muitos desenvolvedores .NET já desenvolveram em algum momento soluções baseadas em Windows Services, utilizando para isto o próprio template oferecido pelo Visual Studio ou frameworks como o Topshelf (este último com suporte para implementações em Windows e até Mono). E o que fazer então ao se trabalhar com .NET Core?

Uma resposta a esta questão seria a criação de uma Console Application, com a execução da mesma acontecendo a partir de um container Docker. Temos assim a possibilidade de execução de tal projeto como um serviço, além de todas as vantagens oferecidas pelo Docker no que se refere ao gerenciamento de containers (execução, parada, reinício).

As próximas seções trazem um exemplo deste tipo de solução baseada em Docker, através de uma Console Application que verificará se uma lista de sites/hosts estão ativos e que gravará os resultados dessa checagem em um banco de dados do MongoDB.

Implementando a aplicação para testes

O projeto MonitoramentoAplicacoes será uma Console Application baseada no .NET Core 2.0, já estando inclusive disponível no GitHub:

Serão adicionados a esta aplicação os seguintes packages:

  • Microsoft.Extensions.Configuration.Json e Microsoft.Extensions.Options.ConfigurationExtensions, para a manipulação de itens de configuração;
  • MongoDB.Driver, empregado no acesso a uma base de log no MongoDB.

O arquivo appsettings.json conterá a seção ServiceConfigurations, na qual foram definidos os itens LogDatabase (banco de dados do MongoDB que armazenará dados de log), Hosts (lista de sites/hosts a serem verificados) e Intervalo (tempo em milissegundos entre cada checagem):

{
  "ServiceConfigurations": {
    "LogDatabase": "mongodb://localhost:17017",
    "Hosts": [ "siteinvalido.com.br", "github.com", "stackoverflow.com" ],
    "Intervalo": 30000
  }
}

A classe ServiceConfigurations será empregada na leitura das configurações especificadas em appsettings.json:

namespace MonitoramentoAplicacoes
{
    public class ServiceConfigurations
    {
        public string LogDatabase { get; set; }
        public string[] Hosts { get; set; }
        public int Intervalo { get; set; }
    }
}

Já o tipo ResultadoMonitoramento, conterá os dados de cada verificação de sites, representando os documentos a serem armazenados em uma coleção do MongoDB:

O valor do campo _id (classe ObjectId – namespace MongoDB.Bson) será gerado automaticamente pelo MongoDB;
O atributo BsonIgnoreIfNull (namespace MongoDB.Bson.Serialization.Attributes) associado à propriedade Exception fará com que este elemento não seja gravado na base, caso o mesmo esteja nulo.

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace MonitoramentoAplicacoes
{
    public class ResultadoMonitoramento
    {
        public ObjectId _id { get; set; }
        public string Horario { get; set; }
        public string Host { get; set; }
        public string Status { get; set; }
        [BsonIgnoreIfNull]
        public object Exception { get; set; }
    }
}

A classe DisponibilidadeRepository gravará na coleção Disponibilidade de um banco chamado DBMonitoramento os resultados da consulta a diferentes hosts:

using MongoDB.Driver;

namespace MonitoramentoAplicacoes
{
    public class DisponibilidadeRepository
    {
        private MongoClient _client;
        private IMongoDatabase _db;
        private IMongoCollection<ResultadoMonitoramento> _disponibilidade;

        public DisponibilidadeRepository(
            ServiceConfigurations configurations)
        {
            _client = new MongoClient(
                configurations.LogDatabase);
            _db = _client.GetDatabase("DBMonitoramento");
            _disponibilidade =
                _db.GetCollection<ResultadoMonitoramento>("Disponibilidade");
        }

        public void Incluir(
            ResultadoMonitoramento monitoramento)
        {
            _disponibilidade.InsertOne(monitoramento);
        }
    }
}

Por fim temos a implementação da classe Program:

  • Uma instância de ConfigurationBuilder (namespace Microsoft.Extensions.Configuration) será criada, a fim de possibilitar a leitura das configurações indicadas no arquivo appsettings.json;
  • Uma referência do tipo ServiceConfigurations será então gerada, com o seu preenchimento fazendo uso da instância obtida no passo anterior;
  • À instância da classe Timer (namespace System.Threading) será vinculada a execução do método TimerElapsed, com isto acontecendo a cada 30 segundos (tempo indicado em milissegundos no arquivo appsettings.json);
  • Quanto ao método TimerElapsed, esta operação utilizará uma instância da classe Ping (namespace System.Net.NetworkInformation) com a finalidade de determinar se um site/host está ou não no ar. O resultado desta ação será então gravado na base de dados DBMonitoramento;
  • O evento CancelKeyPress será finalmente configurado, empregando para isto uma instância de AutoResetEvent (namespace System.Threading) para controlar o término da thread principal da aplicação mediante as combinações de teclas Control + C ou Control + Break. O uso dos métodos Read da classe Console foi evitado, já que isto resultaria em erros durante a execução de um container.
using System;
using System.Threading;
using System.IO;
using System.Net.NetworkInformation;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;

namespace MonitoramentoAplicacoes
{
    class Program
    {
        private static Timer _timer;
        private static ServiceConfigurations _configurations;
        private static AutoResetEvent waitHandle = new AutoResetEvent(false);

        public static void TimerElapsed(object state)
        {
            DisponibilidadeRepository repository =
                new DisponibilidadeRepository(_configurations);

            foreach (string host in _configurations.Hosts)
            {
                Console.WriteLine(String.Empty);
                Console.WriteLine(
                    quot;Verificando a disponibilidade do host {host}");

                var resultado = new ResultadoMonitoramento();
                resultado.Horario =
                    DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                resultado.Host = host;

                // Verifica a disponibilidade efetuando um ping
                // no host que foi configurado em appsettings.json
                try
                {
                    using (Ping p = new Ping())
                    {
                        var resposta = p.Send(host);
                        resultado.Status = resposta.Status.ToString();
                    }
                }
                catch (Exception ex)
                {
                    resultado.Status = "Exception";
                    resultado.Exception = ex;
                }

                repository.Incluir(resultado);

                Console.WriteLine(
                    JsonConvert.SerializeObject(resultado));
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Carregando configurações...");

            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile(quot;appsettings.json");
            var configuration = builder.Build();

            _configurations = new ServiceConfigurations();
            new ConfigureFromConfigurationOptions<ServiceConfigurations>(
                configuration.GetSection("ServiceConfigurations"))
                    .Configure(_configurations);

            // Configura o timer para execução do ping e inicia
            // sua execução imediata
            _timer = new Timer(
                 callback: TimerElapsed,
                 state: null,
                 dueTime: 0,
                 period: _configurations.Intervalo);

            // Tratando o encerramento da aplicação com
            // Control + C ou Control + Break
            Console.CancelKeyPress += (o, e) =>
            {
                Console.WriteLine("Saindo...");

                // Libera a continuação da thread principal
                waitHandle.Set();
            };

            // Aguarda que o evento CancelKeyPress ocorra
            waitHandle.WaitOne();
        }
    }
}

O resultado da execução desta aplicação pode ser observado nas próximas imagens:

Executando a aplicação a partir de um container Docker

Para a execução do projeto MonitoramentoAplicacoes a partir de um container, será também utilizado o Docker Compose, tecnologia que possibilita a construção de soluções baseadas em múltiplos containers.

Maiores informações sobre o uso do Docker Compose podem ser encontradas no artigo a seguir:

O primeiro passo será a criação de um arquivo Dockerfile com as seguintes definições:

FROM microsoft/dotnet:2.0-sdk
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out
ENTRYPOINT ["dotnet", "out/MonitoramentoAplicacoes.dll"]

Já no arquivo docker-compose.yml estarão as instruções para a criação de um container que executará a Console Application (monitoramentoaplicacoes), além de uma dependência envolvendo o MongoDB (também a partir de um container chamado basemonitoramento na porta 57017). A network monitoramento-network foi especificada a fim de permitir a comunicação entre os dois containeres que integram esta solução:

version: '3'

services:
  monitoramento-aplicacoes:
    image: monitoramentoaplicacoes
    build:
      context: .
    networks:
      - monitoramento-network
    depends_on:
      - "basemonitoramento"

  basemonitoramento:
    image: mongo
    ports:
      - "57017:27017"
    networks:
      - monitoramento-network

networks: 
    monitoramento-network:
      driver: bridge

A definição de uma network/rede específica via Docker Compose permitirá ao projeto MonitoramentoAplicacoes referenciar diretamente o container do MongoDB, dispensando assim a aplicação da necessidade de indicar a porta em que a instância do banco de dados se encontra em execução. Com isso, o arquivo appsettings.json será modificado para que o item LogDatabase aponte para o container basemonitoramento:

{
  "ServiceConfigurations": {
    "LogDatabase": "mongodb://basemonitoramento",
    "Hosts": [ "siteinvalido.com.br", "github.com", "stackoverflow.com" ],
    "Intervalo": 30000
  }
}

Para criar a imagem da aplicação, a rede/network e os containers correspondentes, será executado o seguinte comando (através do PowerShell):

docker-compose up -d

O resultado desta ação está indicado nas imagens a seguir:

O comando docker network ls listará a rede monitoramento-network entre as networks disponíveis:

A imagem monitoramentoaplicacoes gerada via Docker Compose será listada ao executar o comando docker images:

A instrução docker-compose ps trará os containers gerados por meio do comando docker-compose up:

Os resultados dos testes verificando os sites/hosts estão nas próximas imagens (obtidas ao consultar os dados através do utilitário Robo 3T):

Referências