Back-End

27 set, 2018

Asp .Net Core 2.0 + Docker + SQL Server 2017 – Primeira aplicação conteinerizada

Publicidade

Oi, galera!

Resolvi dedicar um artigo para todos os desenvolvedores .NET que já conhecem Docker mas ainda não criaram sua primeira aplicação .NET Core conteinerizada.

Desenvolvi uma WebAPI c/ Docker que você pode pegar aqui.

Docker na Prática

Na utilização de Docker em cenários reais é bem incomum um contexto onde seja necessário a utilização de apenas um container. É por isso que neste exemplo já estou trazendo uma aplicação multi-container. Teremos uma imagem de banco de dados SQL Server 2017, onde será utilizada a imagem oficial disponibilizada pela Microsoft no Docker Hub e utilizaremos também uma imagem personalizada para hospedar a WebAPI em ASP .NET Core. Cada imagem será hospedada em um container diferente.

Iniciando com Docker

Para você que esta iniciando, existem algumas maneiras de se hospedar uma aplicação ASP .NET Core em contêineres. O modo mais tradicional, porém não o mais simples, é o de escrever sua própria imagem, o que recomendo fazer depois que já tiver algum tipo de experiência prática.

Enquanto está começando, você pode tirar proveito das ferramentas do Visual Studio que permitem que você adicione suporte ao Docker de um modo bem simples.

Utilizando as ferramentas de suporte ao Docker do Visual Studio, você pode adicionar suporte ao Docker para um projeto já existente ou para um novo projeto. Para ambos os casos o Visual Studio vai te fornecer um template pronto de uma imagem Docker.

Se você vai iniciar um projeto WebAPI do zero e quer adicionar suporte ao Docker, basta clicar na caixinha “Enable Docker Support“.

Porém, se o seu projeto de WebAPI já existe e você quer adicionar suporte ao Docker, clique com o botão direito no projeto > “Add” > “Docker Support“.

Em seguida você verá que um arquivo Dockerfile foi adicionado na raiz da WebAPI. E na raiz da solução serão adicionados os arquivos do Docker Compose.

Orquestrando contêineres

Se você já realizou o download do código disponibilizado, você vai encontrar o controlador ClientController onde estão todos os endpoints que serão expostos no container dockersql.application.webapi.

 /// <summary>
    /// Client Controller
    /// </summary>
    [Route("api/[controller]")]
    public class ClientController : Controller
    {
        private readonly IClientApplicationService _clientApplicationService;

        /// <summary>
        /// Client Controller Constructor
        /// </summary>
        /// <param name="clientApplicationService"></param>
        public ClientController(IClientApplicationService clientApplicationService)
        {
            _clientApplicationService = clientApplicationService;
        }

        /// <summary>
        /// Seleciona um cliente pelo id
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        [HttpGet("{clientId}", Name = nameof(GetClientByIdAsync))]
        [ProducesResponseType(200)]
        [ProducesResponseType(404)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> GetClientByIdAsync([FromRoute]int clientId)
        {
            var clientResponse = await _clientApplicationService.GetByIdAsync(clientId);
            if (clientResponse == null)
                return NotFound();

            return Ok(clientResponse);
        }

        /// <summary>
        /// Registra um cliente
        /// </summary>
        /// <param name="clientRequest"></param>
        /// <returns></returns>
        [HttpPost]
        [ProducesResponseType(201)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> RegisterClientAsync([FromBody]ClientRequest clientRequest)
        {
            var clientResponse = await _clientApplicationService.AddAsync(clientRequest);
            return Created(new Uri(quot;{Request.Path.Value}/{nameof(GetClientByIdAsync)}/{clientResponse.Id}"), clientResponse);
        }

        /// <summary>
        /// Atualiza as informações de um cliente
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="clientRequest"></param>
        /// <returns></returns>
        [HttpPut("{clientId}")]
        [ProducesResponseType(202)]
        [ProducesResponseType(500)]
        [ProducesResponseType(404)]
        public async Task<IActionResult> UpdateClientAsync([FromRoute]int clientId, [FromBody]ClientRequest clientRequest)
        {
            if (!await ClientExists(clientId))
                return NotFound();

            return Accepted(await _clientApplicationService.UpdateAsync(clientId, clientRequest));
        }

        /// <summary>
        /// Deleta um cliente
        /// </summary>
        /// <param name="clientId"></param>
        /// <returns></returns>
        [HttpDelete("{clientId}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        [ProducesResponseType(404)]
        public async Task<IActionResult> RemoveClientAsync([FromRoute]int clientId)
        {
            if (!await ClientExists(clientId))
                return NotFound();

            await _clientApplicationService.DeleteAsync(clientId);
            return Ok();
        }

        private async Task<bool> ClientExists(int clientId)
        {
            var clientResponse = await _clientApplicationService.GetByIdAsync(clientId);
            return clientResponse != null;
        }
    }

E o mais interessante é que a aplicação está hospedada no container dockersql.application.webapi, porém, o banco de dados está hospedado no container dockersql.database. Sendo assim, você pode estar se perguntando como o container da aplicação fará a persistência de dados, já que estão em contêineres diferentes. E a resposta está aqui: o acesso será feito via IP.

  "ConnectionStrings": {
    "Default": "Data Source=172.31.132.33,1401;Initial Catalog=DockerSQL;User ID=sa;Password=123Aa321"
  }

O endereço de IP na string de conexão deve ser o IP da sua máquina juntamente com a porta exposta no container.

Avançando, faremos o seguinte: vamos explorar todas especificações no arquivo docker-compose.yml. Neste arquivo é onde acontece toda magia da orquestração de contêineres.

version: '3.6'

networks: 
  dockersql-network:
    driver: bridge

services:
  dockersql.application.webapi:
    container_name: dockersql.application.webapi
    image: dockersql.application.webapi
    build:
      context: .
      dockerfile: Source/Application/DockerSQL.Application.WebAPI/Dockerfile
    networks:
      - dockersql-network
    depends_on:
      - sql.database

  sql.database:
    container_name: dockersql.database
    image: microsoft/mssql-server-linux:2017-latest
    networks:
      - dockersql-network

Na primeira linha do arquivo é onde é feita a especificação da versão do Docker Compose a ser utilizada.

Após a declaração da versão, pode-se ver a criação de uma network, onde na linha quatro ocorre a especificação do nome da network e na linha cinco a especificação de qual driver será utilizado para criação da network. O propósito de uma network, como o próprio nome já diz, é estabelecer uma rede comum a todos os containeres da aplicação.

Se quiser saber mais sobre docker networks, acesse este link.

3. Após a declaração da network, entramos na seção de services. Vamos por partes:

3.1. O parâmetro container_name, nada mais é do que um parâmetro opcional para especificar o nome do seu container. Quando não especificado o docker gerará um nome aleatório.

3.2. O parâmetro image é o nome da sua imagem, sendo que – vale muita atenção aqui – caso o Docker não encontre a imagem especificada localmente, ele automaticamente irá a procurar no Docker Hub. Perceba que se a imagem estiver localizada no Docker Hub, não é necessário configurar os parâmetros de build. Caso contrário, é necessário especificar o contexto (context) e o caminho do Dockerfile (dockerfile).

3.3. O parâmetro networks especifica qual a rede customizada a ser utilizada pelo container.

3.4. O parâmetro depends_on permite especificar quais as dependências de um determinado container. Contextualizado, muitas vezes, antes de subir um container, pode ser necessário subir outro container como, por exemplo, no caso da nossa aplicação DockerSQL, não faz sentido subir o container dockersql.application.webapi sem antes ter subido o container dockersql.database, pois de nada adianta uma aplicação sem banco de dados no ar.

3.5. Por último, temos a especificação do container de banco de dados SQL Server 2017 juntamente com as configurações de nome da imagem, nome do container e rede. Parâmetros que acabamos de ver logo acima.

Você pode verificar o documento oficial da Microsoft sobre Docker + SQL Server 2017 aqui.

É muito comum que tenhamos alguns arquivos de Docker Compose Override, principalmente por ambiente. E nestes arquivos existe a possibilidade de sobrescrita de configurações dos containeres, parâmetros como ambiente de desenvolvimento, porta externa e interna do container e até mesmo usuário e senha no caso de um container de banco de dados. Segue o exemplo do docker-compose.override.yml.

version: '3.6'

services:
  dockersql.application.webapi:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "9091:80"
  
  sql.database:
    environment:
      - SA_PASSWORD=123Aa321
      - ACCEPT_EULA=Y
    ports:
      - "1401:1433"

Você pode verificar mais exemplos de Docker Compose Override aqui.

Por hoje é só, pessoal! Espero ter ajudado, e que de agora em diante você não crie mais aplicações fora de conteineres! Grande abraço e obrigado pela leitura!

Quer saber mais sobre docker, docker compose, .net core, testes unitários e de integração? Baixe meu e-book free: https://kenerry.com.br