Abril de 2019

Volume 34 – Número 4

[Pontos de dados]

O EF Core em um Aplicativo Docker em Contêiner

Por Julie Lerman

Julie LermanPassei muito tempo examinando o Entity Framework e o EF Core, e também trabalhando com o Docker. Você viu provas disso em várias colunas anteriores. Mas, até agora, ainda não trabalhei com eles em conjunto. Como as ferramentas do Docker para Visual Studio 2017 já existem há algum tempo, imaginei que seria fácil chegar lá. Mas não foi. Talvez isso tenha ocorrido porque gosto de saber o que está acontecendo nos bastidores e quero conhecer e entender minhas opções. Seja como for, acabei examinando informações de postagens em blogs, artigos, problemas do GitHub e a documentação da Microsoft antes de conseguir alcançar meu objetivo inicial. O que desejo é tornar mais fácil para os leitores desta coluna encontrarem o caminho (e evitarem alguns dos obstáculos com os quais me deparei) consolidando tudo em um único local.

Vou me concentrar no Visual Studio 2017, ou seja, no Windows, portanto, você terá que verificar se está com o Docker Desktop for Windows instalado (dockr.ly/2tEQgR4) e se o configurou para usar contêineres do Linux (o padrão). Isso também requer que o Hyper-V esteja habilitado no seu computador, mas o instalador o alertará se necessário. Se você estiver trabalhando no Visual Studio Code (independentemente do sistema operacional), há muitas extensões para o trabalho com o Docker diretamente do IDE.

Criando o Projeto

Comecei minha jornada com uma API simples do ASP.NET Core. As etapas para a configuração de um novo projeto que fique igual ao meu são: Novo Projeto |.NET Core | Aplicativo Web ASP.NET Core. Na página de escolha do tipo de aplicativo, selecione API. Verifique se a opção Habilitar Suporte do Docker está marcada (Figura 1). Deixe a configuração do sistema operacional no Linux. Os contêineres do Windows são maiores e mais complicados e suas opções de hospedagem ainda são muito limitadas. Aprendi isso da forma mais difícil.

Configurando o Novo Projeto de API do ASP.NET Core
Figura 1 Configurando o Novo Projeto de API do ASP.NET Core

Já que você habilitou o suporte do Docker, verá um Dockerfile no novo projeto. O Dockerfile fornece instruções ao mecanismo do Docker para a criação de imagens e a execução de um contêiner com base na imagem final. Executar um contêiner é semelhante a instanciar um objeto a partir de uma classe. A Figura 2 mostra o Dockerfile criado pelo modelo. (É bom ressaltar que estou usando o Visual Studio 2017 versão 15.9.7 com o .NET Core 2.2 instalado no meu computador. À medida que as ferramentas do Docker evoluem, o mesmo ocorre com o Dockerfile.)

Figura 2 O Dockerfile padrão criado pelo modelo do projeto

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY ["DataApi/DataApi.csproj", "DataApi/"]
RUN dotnet restore "DataApi/DataApi.csproj"
COPY . .
WORKDIR "/src/DataApi"
RUN dotnet build "DataApi.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "DataApi.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "DataApi.dll"]

A primeira instrução identifica a imagem base usada para criar as imagens subsequentes e o contêiner de seu aplicativo é especificado como:

microsoft/dotnet:2.2-aspnetcore-runtime

Em seguida, uma imagem de build será criada com base na imagem base. A imagem de build é exclusivamente para a criação do aplicativo, portanto, também precisa do SDK. Vários comandos são executados na imagem de build para a inserção do código de seu projeto na imagem e para a restauração dos pacotes necessários antes da construção da imagem.

A próxima imagem criada será usada para publicação; ela se baseia na imagem de build. Para essa imagem, o Docker executará dotnet publish e criará uma pasta com os ativos mínimos necessários para executar o aplicativo.

A imagem final não precisa do SDK e é criada a partir da imagem base. Todos os ativos de publicação serão copiados para essa imagem e um Ponto de entrada será identificado — ou seja, o que deve acontecer quando essa imagem for executada.

Por padrão, as ferramentas do Visual Studio executam apenas o primeiro estágio da configuração de depuração, ignorando as imagens de publicação e final, mas para uma configuração de versão o Dockerfile inteiro é usado.

Os diversos conjuntos de builds são chamados de builds multiestágio, com cada etapa com foco em uma tarefa diferente. O interessante é que cada comando executado em uma imagem, como os seis comandos na imagem de build, faz com que o Docker crie uma nova camada da imagem. A documentação da Microsoft sobre arquitetura para aplicativos .NET em contêineres localizada em bit.ly/2TeCbIu faz um ótimo trabalho ao explicar o Dockerfile linha a linha e descrever como ele ficou mais eficiente com os builds multiestágio.

Por enquanto, deixarei o Dockerfile como padrão.

Depurando o Controlador Padrão no Docker

Antes de passar para a depuração do Docker, verificarei o aplicativo fazendo a depuração no perfil auto-hospedado do ASP.NET Core (que usa o Kestrel, um servidor Web multiplataforma para ASP.NET Core). Certifique-se de que o botão Iniciar Depuração (seta verde na barra de ferramentas) esteja configurado para fazer a execução usando o perfil correspondente ao nome do seu projeto — no meu caso é DataAPI.

Em seguida, execute o aplicativo. O navegador deve ser aberto apontando para a URL http://localhost:5000/api/values e exibindo os resultados do método do controlador padrão (“value1,” “value2”). Bem, agora você sabe que o aplicativo está funcionando e é hora de testá-lo no Docker. Interrompa o aplicativo e altere o perfil de depuração para o Docker. Se o Docker for Windows estiver sendo executado (com as configurações apropriadas mencionadas no início deste artigo), ele executará o Dockerfile. Se você nunca tiver extraído as imagens referenciadas, inicialmente o mecanismo do Docker as trará do Docker hub. A extração das imagens pode levar alguns minutos. Você pode acompanhar o progresso na janela de saída do build. Em seguida, o Docker criará todas as imagens seguindo as etapas no Dockerfile, mas não recompilará as que não tiverem mudado desde a última execução. A etapa final será executada pelas Ferramentas do Visual Studio para Docker, que chamarão o build do docker e, em seguida, executarão o comando docker run para iniciar o contêiner. Como resultado, uma nova janela (ou guia) do navegador será aberta com a mesma saída de antes, mas a URL será diferente porque estará vindo de dentro da imagem do Docker que está expondo essa porta. No meu caso, seria http://172.26.137.194/api/values. Configurações alternativas farão com que o navegador seja iniciado usando http://localhost:hostPort.

Se o Visual Studio não conseguir executar o Docker

Encontrei dois problemas que inicialmente impediram o Docker de compilar as imagens. Na primeira vez que tentei fazer a depuração no Visual Studio tendo como destino o Docker, recebi a mensagem "Ocorreu um erro ao tentar executar o contêiner do Docker". O erro referenciava a linha 256 de container.targets. Isso não era informativo nem útil até eu perceber mais tarde que poderia ter visto os detalhes do erro na saída do build. Após testar várias coisas (inclusive pesquisar muito na Internet, mas sem ter examinado a janela de saída do build), acabei tentando extrair uma imagem a partir da CLI do Docker, que me solicitou que fizesse logon no Docker, mesmo já o tendo feito no aplicativo do Docker Desktop. Após fazer isso, consegui depurar no Visual Studio 2017. Posteriormente, o logoff na CLI do Docker não atrapalhou e ainda pude depurar. Não tenho certeza de qual é a relação entre as duas ações. No entanto, quando desinstalei totalmente o Docker Desktop for Windows e o reinstalei, fui forçada, novamente, a fazer logon por meio da CLI do Docker para poder executar meu aplicativo. De acordo com um problema exibido pelo GitHub em bit.ly/2Vxhsx4, parece que isso ocorreu porque fiz logon no Docker for Windows usando o meu endereço de email e não meu nome de logon.

Esse mesmo erro ocorreu uma vez quando desabilitei o Hyper-V. Resolvi o problema reativando o Hyper-V e reiniciando o computador. (Para quem ficou curioso, precisei executar uma máquina virtual no VirtualBox para uma tarefa sem qualquer relação e desabilitar Hyper-V, que não pode estar ativo no VirtualBox).

O que o Mecanismo do Docker Está Gerenciando?

Como resultado da primeira execução desse aplicativo, o mecanismo do Docker extraiu do Docker hub (hub.docker.com) as duas imagens mencionadas e fez o seu rastreamento. Mas o Dockerfile também criou outras imagens que ele armazenou em cache. A execução de imagens do Docker na linha de comando revelou a imagem docker4w usada pelo próprio Docker for Windows, uma imagem aspnetcore-runtime extraída do Docker Hub e a imagem dataapi:dev, que foi criada pela compilação do Dockerfile — ou seja, a imagem a partir da qual seu aplicativo está sendo executado. Se você executar imagens do Docker -a para exibir imagens ocultas, verá mais duas imagens (as sem tags) que são as imagens intermediárias de compilação e publicação criadas pelo Dockerfile, conforme mostrado na Figura 3. Você não verá nada referente à imagem do SDK, de acordo com Glenn Condron da Microsoft, "devido a uma peculiaridade no modo como o build multiestágio do Docker funciona".

Imagens expostas e ocultas do Docker após a execução da API
Figura 3 Imagens expostas e ocultas do Docker após execução da API

Você pode examinar ainda mais detalhes de uma imagem usando o comando:

docker image inspect [imageid]

Mas e os contêineres? O comando docker ps revela o contêiner criado pelas ferramentas do Docker para Visual Studio 2017 chamando docker run (com parâmetros) na imagem de desenvolvimento. Empilhei os resultados na Figura 4 para que você possa ver todas as colunas. Não há contêineres ocultos.

Contêiner do Docker criado pela execução do aplicativo a partir da depuração do Visual Studio 2017
Figura 4 Contêiner do Docker criado pela execução do aplicativo a partir da depuração do Visual Studio 2017

Configurando a API de dados

Agora, vamos transformar o que obtivemos em uma API de dados usando o EF Core como mecanismo de persistência de dados. O modelo é simplista com o objetivo de focar o uso da conteinerização e seu impacto sobre a fonte de dados do EF Core.

Comece adicionando uma classe chamada Magazine.cs:

public class Magazine
{
  public int MagazineId { get; set; }
  public string Name { get; set; }
}

Em seguida, você precisa instalar três pacotes NuGet diferentes. Já que vou mostrar a diferença entre usar um banco de dados SQLite independente e um banco de dados SQL Server, adicione tanto o pacote Microsoft.EntityFrameworkCore.Sqlite quanto o pacote Microsoft.EntityFrameworkCore.SqlServer ao projeto. Você também executará as migrações do EF Core, portanto, o terceiro pacote a ser instalado é o Microsoft.EntityFrameworkCore.Design.

Agora, deixarei que as ferramentas criem um controlador e um DbContext para a API. Caso isso seja novidade para você, aqui estão as etapas:

  • Clique com o botão direito na pasta Controllers no Gerenciador de Soluções.
  • Selecione Adicionar | Controlador | Controlador de API com ações, usando o Entity Framework.
  • Selecione a classe Magazine como a classe de Modelo.
  • Clique no sinal de adição ao lado da classe de contexto de dados, altere a parte realçada do nome para Mag, para que passe a ser [YourApp]. Models.MagContext, e depois clique em Adicionar.
  • Deixe o nome do controlador padrão como MagazinesController.
  • Clique em Add.

Quando terminar, você terá uma nova pasta Data com a classe MagContext, e a pasta Controllers terá um novo arquivo MagazineController.cs.

Agora farei com que o EF Core propague o banco de dados com três revistas usando a propagação baseada em DbContext sobre a qual escrevi na minha coluna de agosto de 2018 (msdn.com/magazine/mt829703). Adicione este método a MagContext.cs:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Magazine>().HasData(
    new Magazine { MagazineId = 1, Name = "MSDN Magazine" },
    new Magazine { MagazineId = 2, Name = "Docker Magazine" },
    new Magazine { MagazineId = 3, Name = "EFCore Magazine" }
  );
 }

Configurando o Banco de Dados

Para criar o banco de dados, preciso especificar o provedor e a cadeia de conexão, além de criar e executar uma migração. Quero trilhar um caminho familiar, logo, começarei definindo SQL Server LocalDB como destino e especificando a cadeia de conexão no arquivo appsettings.json.

Quando você abrir appsettings.json, verá que ele já contém uma cadeia de conexão, que foi criada pela ferramenta do controlador quando a deixei definir o arquivo MagContext. Ainda que os provedores do SQL Server e do SQLite estejam instalados, parece que o padrão é o provedor do SQL Server. Isso se mostrou verdadeiro em testes subsequentes. Prefiro um nome de minha preferência para a cadeia de conexão e para o banco de dados, portanto, substituí a cadeia de conexão MagContext por MagsConnectionMssql e adicionei meu nome de banco de dados preferido, DP0419Mags:

"ConnectionStrings": {
    "MagsConnectionMssql":
      "Server=(localdb)\\mssqllocaldb;Database=DP0419Mags;Trusted_Connection=True;"
  }

No arquivo startup.cs do aplicativo, que inclui um método ConfigureServices, as ferramentas também inseriram um código para configurar o DbContext. Altere o nome MagContext de sua cadeia de conexão para que coincida com o novo nome:

services.AddDbContext<MagContext>(options =>
  options.UseSqlServer(Configuration.GetConnectionString(  "MagsConnectionMssql")));

Agora posso usar as migrações do EF Core para criar a primeira migração e, como estou no Visual Studio, posso fazê-lo usando os comandos do PowerShell no Console do Gerenciador de Pacotes:

add-migration initMssql

Migrando o Banco de Dados

Esse comando criou um arquivo de migração, mas não vou criar meu banco de dados usando os comandos de migração — quando eu implantar meu aplicativo, não quero ter que executar comandos de migração para criar ou atualizar o banco de dados. Em vez disso, usarei o método Database.Migrate do EF Core. É uma decisão importante a de onde esse método lógico entrará em seu aplicativo. Você precisa que ele seja executado quando o aplicativo for iniciado. Muitas pessoas acham que esse momento seria quando o arquivo startup.cs for executado, mas a equipe do ASP.NET Core recomenda colocar o código de inicialização do aplicativo no arquivo program.cs, que é o ponto de partida verdadeiro de um aplicativo ASP.NET Core. Mas, como em qualquer decisão, poderá haver fatores que afetarão essa orientação.

O método Main padrão do programa chama o método CreateWebHostBuilder do ASP.NET Core, que realiza diversas tarefas em seu nome, e, em seguida, chama mais dois métodos — Build e Run:

public static void Main(string[] args)
{
  CreateWebHostBuilder(args).Build().Run();
}

Preciso migrar o banco de dados depois de Build e antes de Run. Para fazê-lo, criei um método de extensão para ler as informações do provedor de serviço definidas em startup.cs, que vai descobrir a configuração de DbContext. Em seguida, o método chama Database.Migrate no contexto. Adaptei o código (e as diretrizes do membro da equipe do EF Core, Brice Lambson) exibido no problema do GitHub em bit.ly/2T19cbY para criar o método de extensão de IWebHost mostrado na Figura 5. O método foi projetado para receber um tipo DbContext genérico.

Figura 5 Método de Extensão de IWebHost

public static IWebHost MigrateDatabase<T>(this IWebHost webHost) where T : DbContext
{
  using (var scope = webHost.Services.CreateScope())
  {
    var services = scope.ServiceProvider;
    try
    {
      var db = services.GetRequiredService<T>();
      db.Database.Migrate();
    }
    catch (Exception ex)
    {
      var logger = services.GetRequiredService<ILogger<Program>>();
      logger.LogError(ex, "An error occurred while migrating the database.");
    }
  }
  return webHost;
}

Em seguida, modifiquei o método Main para chamar MigrateDatabase para MagContext entre Build e Run:

CreateWebHostBuilder(args).Build().MigrateDatabase<MagContext>().Run();

Conforme você adiciona todo o código novo, o Visual Studio deverá solicitar que use as instruções para Microsoft.EntityFrameworkCore, Microsoft.Extensions.DependencyInjection e o namespace de sua classe MagContext.

Agora, o banco de dados será migrado (ou até mesmo criado) conforme necessário no tempo de execução.

Uma última etapa antes da depuração é informar ao ASP.NET Core que, ao iniciar, é necessário apontar para o controlador de revistas e não para o controlador de valores. Você pode fazer isso no arquivo launchsettings.json, alterando as instâncias de launchUrl de api/values para api/Magazines.

Executando a API de Dados no Kestrel e Depois no Docker

Como fiz para o controlador de valores, inicialmente vou testar o esquema no servidor auto-hospedado usando o perfil do projeto (por exemplo, DataAPI) em vez do perfil do Docker. Já que o banco de dados ainda não existe, a migração criará o novo banco de dados, o que significa que haverá uma pequena demora, porque o SQL Server, e até mesmo o LocalDB, tem muito trabalho a fazer. Mas o banco de dados será criado e propagado e, em seguida, o método de controlador padrão lerá e exibirá as três revistas no navegador em localhost:5000/carros/api/Magazines.

Agora vamos testar com o Docker. Altere o perfil de depuração para o do Docker e execute-o novamente. Oh! Não!  Quando o navegador é aberto, ele exibe uma SQLException, com os detalhes explicando que TryGetConnection falhou.

O que está acontecendo aqui? O aplicativo está procurando o SQL Server (definido como "(localdb) \\mssqllocaldb" na cadeia de conexão) dentro do contêiner do Docker que está sendo executado. Mas LocalDB está instalado no meu computador e não dentro do contêiner. Ainda que essa seja uma opção comum para a preparação para um banco de dados SQL Server quando você estiver em um ambiente de desenvolvimento, ela não funcionará tão facilmente quando tiver como destino contêineres do Docker.

Isso significa que tenho mais trabalho a fazer — e possivelmente você terá mais dúvidas. Eu certamente tive.

Desvio para um Caminho Mais Fácil

Há alternativas muito boas, como usar o SQL Server para Linux em outro contêiner do Docker ou definir como destino um banco de dados do SQL Azure. Examinarei essas soluções nos próximos artigos mas, primeiro, quero que você veja uma solução rápida em que o servidor de banco de dados está realmente dentro do contêiner e sua API será executada com êxito. É possível fazer isso facilmente com o SQLite, que é um banco de dados independente.

Você já deve estar com o pacote Microsoft.EntityFramework.SQLite instalado. As dependências desse pacote NuGet forçarão os componentes de tempo de execução do SQLite a instalar a imagem em que o aplicativo é compilado.

Adicione uma nova cadeia de conexão chamada MagsConnectionSqlite ao arquivo appsettings.json. Especifiquei o nome do arquivo como DP0419Mags.db:

"ConnectionStrings": {
    "MagsConnectionMssql":
      "Server=(localdb)\\mssqllocaldb;Database=DP0419Mags;Trusted_Connection=True;",
    "MagsConnectionSqlite": "Filename=DP0419Mags.db;"
  }

Na inicialização, altere o provedor do DbContext para SQLite com o novo nome de cadeia de conexão:

services.AddDbContext<MagContext>(options =>
  options.UseSqlite(Configuration.GetConnectionString(  "MagsConnectionSqlite")));

O arquivo de migração que você criou é específico do provedor do SQL Server, portanto, é preciso substituí-lo. Exclua a pasta Migrations inteira e execute Add-Migration initSqlite no Console do Gerenciador de Pacotes para recriar a pasta junto com os arquivos de migração e de instantâneo.

Você pode executar esse comando no servidor interno se quiser ver o arquivo que é criado, ou pode simplesmente começar a depurar isto no Docker. O novo banco de dados SQLite será criado muito rapidamente quando o comando de migração for chamado e, então, o navegador exibirá as três revistas novamente. Observe que o endereço IP da URL será o que você viu anteriormente ao executar o controlador de valores no Docker. No meu caso, o endereço é http://172.26.137.194/api/Magazines. Agora, o SQLite e a API estão sendo executados dentro do contêiner do Docker.

Em breve: uma solução mais amigável para a produção

Embora usar o banco de dados SQLite certamente simplifique a tarefa de permitir que o EF Core crie um banco de dados dentro do mesmo contêiner que está executando o aplicativo, provavelmente não é assim que você vai querer implantar sua API na produção. Uma das vantagens dos contêineres é que você pode expressar a separação de conceitos empregando e coordenando vários contêineres.

No caso dessa pequena solução, talvez o SQLite servisse. Mas para suas soluções do mundo real, você deve usar outras opções. Se pensarmos no SQL Server, uma opção seria definir como destino um Banco de Dados SQL do Microsoft Azure. Com essa opção, independentemente de onde estiver executando o aplicativo (no computador de desenvolvimento, no IIS, em um contêiner do Docker, em um contêiner do Docker na nuvem), você terá certeza de sempre estar apontando para um banco de dados ou servidor de banco de dados consistente dependendo de seus requisitos. Outro caminho é se beneficiar dos servidores de banco de dados em contêineres, como o SQL Server para Linux, como escrevi em uma coluna anterior (msdn.com/magazine/mt784660). Os microsserviços introduzem outra camada de soluções possíveis, já que a orientação é que haja um único banco de dados por microsserviço. Você também poderia gerenciá-los facilmente em contêineres. Há um livro excelente (e gratuito) da Microsoft sobre arquitetura de aplicativos .NET para microsserviços em contêineres localizado em bit.ly/2NsfYBt.

Nas próximas colunas, examinarei algumas dessas soluções conforme mostro como definir como destino o SQL Azure ou um SQL Server em contêiner; como gerenciar cadeias de conexão e proteger as credenciais usando variáveis de ambiente do Docker; e como permitir que o EF Core descubra as cadeias de conexão no tempo de design usando comandos de migrações e no tempo de execução de dentro do contêiner do Docker. Mesmo com minha experiência já existente com o Docker e o EF Core, eu passei por muitas curvas de aprendizado aperfeiçoando os detalhes dessas soluções e estou ansiosa para compartilhá-los todos com vocês.


Julie Lerman é Diretora Regional da Microsoft, MVP da Microsoft, coach e consultora de equipes de software e reside nas colinas de Vermont. Você pode encontrá-la em apresentações sobre acesso de dados ou sobre outros tópicos em grupos de usuários e conferências em todo o mundo. Ela escreve no blog thedatafarm.com/blog e é autora do “Programming Entity Framework”, bem como de uma edição do Code First e do DbContext, todos da O'Reilly Media. Siga-a no Twitter: @julielerman e confira seus cursos da Pluralsight em bit.ly/PS-Julie.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Glenn Condron, Steven Green, Mike Morton