IMPLANTAÇÃO/DEPLOY DE UMA APLICAÇÃO EM ANGULAR 8 UTILIZANDO UM WEB API EM ASP.NET CORE 2.2

Contextualizando o cenário da implantação

Ao iniciar este processo de implantação/deploy de uma aplicação web ASP.NET Core, fui ao longo de alguns dias, pelo método de tentativa e erros, descobrindo várias necessidades para que o backend e o frontend de uma aplicação funcionassem em uma máquina virtual (VM) do Microsoft Azure. Há muitos detalhes que o ASP.NET Core juntamente ao Angular exigem para funcionar conjuntamente, resolvi criar alguns tópicos, para que pudesse relembrar e replicar nos próximos projetos. Estes tópicos foram aumentando, assim como minha frustração de colocar esta aplicação online com estas tecnologias funcionando. Foi então que surgiu a ideia de escrever um post, objetivando organizar todas as tarefas realizadas de forma acessível a todas as pessoas, com o maior número de informações possíveis a este respeito num único lugar. Um artigo contendo as atividades básicas para iniciantes implantarem aplicações similares a esta.

Há muita documentação à ser lida para um detalhamento conceitual à respeito das práticas aqui executadas, não sendo objetivo deste post explicá-las, mas sim a identificação do que foi usado na prática. Ao longo deste post alguns links serão apresentados, para prover um maior respaldo técnico à medida que os problemas foram surgindo.


Este post não tem nada de especial e inventivo, acredito que seu mérito esteja no agrupamento de várias informações e testes à respeito das tecnologias citadas num único lugar.

Sou um desenvolvedor com alguns anos de experiência, não sendo da área de infraestratura. Por isto, alguns especialistas, DevOps podem “torcer o nariz” ao ver as medidas que foram adotadas, que seguem uma ordem cronológica à medida que os problemas foram surgindo. Embora eu já tivesse implantado diversas outras aplicações ASP.NET, substimei o processo de deploy do ASP.NET Core que é muito mais complexo, que as versões anteriores do ASP.NET. Acredito que hajam muitos desenvolvedores que assim como eu, querem e precisam saber como funciona de uma ponta até outra quando se trata de construir uma aplicação. Um grande problema para estes profissionais é saber onde inicia e termina as responsabilidades de configurações de cada implantação, o que pertence a instalação do backend e o que pertence ao frontend. Pois há diversos momentos onde não se sabe se o acerto do problema deve ser feito no backend ou no frontend via configuração do servidor web, neste caso o IIS. Isso acontece o tempo todo, sendo muito difícil de lidar, saber onde começa a origem do problema.

Da teoria a prática

Por ser uma aplicação relativamente simples, a estratégia adotada desde o início foi de usar a instalação mais simples possível para o deploy. E a medida que precisasse de alguma configuração extra, tomaria alguma atitude à partir da documentação da tecnologia e de opiniões de outros profissionais. Mas embora seja uma aplicação simples, este não é um “Hello World”, por isto vários foram os problemas e dúvidas com relação ao acesso a dados de uma forma segura através da UI, acesso ao banco de dados, autenticação de usuários, mas principalmente o fato de termos duas aplicações separadas conversando entre si. Portanto o processo iniciou-se com a publicação do web api e cópia dos arquivos publicados para o servidor Windows 2012 R2, assim como o build do Angular e a cópia dos packages para o mesmo servidor.

A aplicação web api (backend) foi implementada em ASP.NET Core 2.2, usando o EntityFramework Core 2.2.6 para a persitência dos dados, num banco SQL Server Express. Também foi implementado Json Web Tokens – JWT https://jwt.io/introduction/ para autenticação dos usuários com a api, assim como o uso do CORS https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS, que impede que uma outra página da Web não autorizada, faça solicitações para o domínio da página de nossa aplicação.

O frontend foi implementado usando o Angular 8.2.3, com rxjs 6.4.0 para a implementação dos observables para assinar/subscribe os endpoints (métodos GET, POST, PUT, DELETE, PATCH) do web api. Trata-se de uma aplicação simples implementada em typescript 3.5, com bootstrap 4.3.1 e outros plugins para interface (ngx-bootstrap, material angular, npmjs, etc), sem nem mesmo o uso de lazy loading para otimização do carregamento da aplicação. Para o deploy da interface Angular foi executado o comando, ng build –prod e a cópia de seus arquivos para o servidor, conforme recomendado pela documentação, https://angular.io/guide/deployment. O build do Angular empacota os arquivos em formatos otimizados, fazendo minificações nos arquivos, desabilitando checagens específicas do desenvolvimento a fim de dar mais rapidez a aplicação, entre outras coisas.

Para utilização de uma VM da nuvem do Microsoft Azure, deve-se antes de iniciar os testes, abrir duas portas para conexão com a aplicação. Uma porta para acesso ao Web Api e outra para a interface de usuário. Assim, deve-se criar dois pontos de extremidades, podendo-se criar filtros de rede ou uma sub-rede ou adaptador de rede. Podendo colocar filtros, que controlam o tráfego de entrada e de saída, em um grupo de segurança de rede anexado ao recurso que recebe o tráfego. Para saber mais como abrir portas para uma máquina virtual com o Portal do Azure:
https://docs.microsoft.com/pt-br/azure/virtual-machines/windows/nsg-quickstart-portal
Ou para mais detalhes sobre configurações de Pontos de Extremidade para VM do Azure:
https://docs.microsoft.com/pt-br/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-2.2#endpoint-configuration

Após estas definições com seu servidor, já com os arquivos do backend e frontend devidamente copiados para o servidor (em pastas separadas em qualquer lugar do servidor) é hora de criar um site para cada uma das aplicações. Não entraremos em detalhes sobre o processo de criação de website no IIS, bastando seguir as recomendações da documentação, https://support.microsoft.com/pt-br/help/323972/how-to-set-up-your-first-iis-web-site, uma vez que este procedimento não costuma ser fonte de problemas. Porém, após a criação dos dois sites no IIS 8, surgiu um problema relativo aos arquivos do frontend. Pois ao tentar realizar o carregamento do frontend no Google Chrome, a aplicação não renderiza a página inicial, exibindo a mensagem :

404 – File or directory not found. The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

Este erro é devido a ausência de um arquivo web.config para integrar a interface de usuário implementada em Angular ao servidor web. Em termos práticos, o web.config é o arquivo que realiza a configuração do IIS, para definições de parâmetros utilizados pela aplicação junto ao servidor web. O build do Angular não é direcionado para um servidor específico, no caso, o Internet Information Server – o IIS, pois uma mesma aplicação angular também pode ser implantada junto ao Apache, Nginx, Golang, Github Pages, etc. Por isto, após a geração do deploy deve-se criar manualmente um arquivo web.config para o frontend e colocá-lo na mesma pasta que contêm os arquivos do frontend.

AngularSemWebConfig
Figura 1: Visualização no Google Chrome da tela inicial da aplicação sem web.config

Segue abaixo, o código do web.config extraído da documentação do Angular para esta finalidade, https://angular.io/guide/deployment.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<system.webServer>
		<security>
			<authorization>
				<remove users="*" roles="" verbs="" />
				<add accessType="Allow" users="?" />
			</authorization>
		</security>
		<staticContent>
			<remove fileExtension=".woff" />
			<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
			<mimeMap fileExtension=".woff" mimeType="application/x-font-woff" />
		</staticContent>
		<rewrite>
			<rules>
				<rule name="Angular Routes" stopProcessing="true">
					<match url=".*" />
					<conditions logicalGrouping="MatchAll">
						<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
						<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
					</conditions>
					<action type="Rewrite" url="/index.html" />
				</rule>
			</rules>
		</rewrite>
	</system.webServer>
</configuration>

O arquivo web.config original fornecido na documentação estabelece algumas regras através das marcas para definir o comportamento das rotas no IIS. Isso é essencial para o aplicativo funcionar, porque reescreve os endereços URL das solicitações e suas respostas HTTP. Também foi adicionada ao web.config a tag no IIS para reconhecer arquivos com extensões .woff e .woff2, através da seção Existem outras maneiras de fazer essas opções, mas neste exemplo, usaremos esses recursos via código inserido no web.config.

UrlRewrite2
Figura 2: Seção URL Rewrite da aplicação angular no IIS

Ainda com relação ao deploy do frontend, há uma recomendação em alguns posts, referente à alteração do arquivo index.html gerado pelo build do Angular. O index.html possui uma tag que especifica o caminho base para resolver URLs relacionados ao aplicativo e ativos, arquivos estáticos, imagens, scripts e outros base href='/'. Este caminho pode ser alterado em alguns casos onde o ambiente de produção possui subpastas para instalação dentro de outras aplicações e outras especificidades. Há alguns exemplos onde a barra é removida da tag base. Em alguns momentos para buscar resolver problemas de funcionamento, esta tag teve seu valor alterado para outras possibilidades e a aplicação parou de ser renderizada. A barra indica a raiz da aplicação, para aplicações que não estão vinculadas a outras aplicações e não deve ser alterada nestes casos. Somente em situações específicas esta referência href deve ser alterada.

O backend e o CORS, um capítulo à parte

É importante saber à respeito de alguns conceitos à respeito do tipo da projeto do Web Api, para fazer as configurações corretas no deploy. Mais uma vez não vou entrar em muitos detalhes, pois a documentação do ASP.NET Core é muito didática. No caso do backend deste exemplo, trata-se FDD (implantação dependente de estrutura), onde o aplicativo é implantado usando a versão do .NET Core presente no Windows Server 2012 R2. Para obter mais informações a este respeito, https://dotnet.microsoft.com/download/dotnet-core/2.2 e para saber mais à respeito dos tipos de implantanção de serviços web api ASP.NET Core, https://docs.microsoft.com/pt-br/dotnet/core/deploying/.

O InProcess é o modelo de hospedagem do Web Api utilizado neste projeto, que já vem configurado pelo Visual Studio no template de criação, https://docs.microsoft.com/pt-br/aspnet/core/host-and-deploy/aspnet-core-module?view=aspnetcore-2.2

<PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>

Porém é importante saber algumas características deste tipo de hospedagem, para que não fique a percepção de que ele foi adotado apenas por ser um padrão de criação. Pois além disto ele é o ideal para arquitetura utilizada neste projeto por usar o servidor HTTP do IIS (IISHttpServer), que já está configurado, em vez do servidor Kestrel. O InProcess usa o CreateDefaultBuilder para chamar o método UseIIS() a fim de registrar o IISHttpServer. Dessa forma, a classe Program de inicialização do serviço, pode se manter como está, sem nenhuma configuração adicional. O trecho de código abaixo faz parte do arquivo Program.cs que está na raiz do projeto.

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

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}

Ao iniciar a execução da aplicação navegando por suas páginas HTML, o layout do código, a formatação e suas imagens são exibidas. Isto é um sinal de que a instalação do frontend está instalada corretamente. Porém, ao iniciar a utilização das funcionalidades que acessam o backend, no caso deste exemplo, na tela de login do sistema. Após a inserção do usuário e senha, ao chamar o endpoint do controller de autenticação de usuários, um erro é apresentado:

main-es2015.fbdfd9b26a7e658d49a6.js:1 Http failure response for http://server-sample.cloudapp.net:95/api/auth/login/: 0 Unknown Error.

Este é um tipo de erro de difícil identificação a princípio, uma vez que a própria mensagem no browser se refere a um Unknown Error. No caso deste exemplo ocorreu em função de uma incorreta configuração do CORS(Cross-Origin Resource Sharing), mas pode ocorrer também devido a falta de sua configuração.

UnknownError
Figura 3: UnKnown Error

Usando as ferramentas de análise de etapas do Resource Timing do Google Chrome é possível tentar realizar um diagnóstico referente ao problema. O Resource Timing possui fases que permitem visualizar a saída de um request do cliente, até seu recebimento e envio da resposta do servidor a cada request enviado. Também é possível receber diferentes indicadores de problemas de performance relacionados a esta tramitação dos dados, https://developers.google.com/web/tools/chrome-devtools/network/understanding-resource-timing.

São várias as fases que o Resource Timing apresenta a cada request, iniciando pelo enfileiramento dos requests (caso seja necessário), passando por ‘stalled/blocking’, ‘Proxy Negotiation’, ‘DNS Lookup’, ‘Connecting’, ‘SSL Request Sent’, ‘Waiting’ e ‘Downloading’ em que cada uma possui um tempo em milisegundos para sua execução. A análise do request do post gerado pelo login exibe apenas a fase stalled, sem mostrar nenhuma das outras fases, sinalizando a interrupção do processo de request logo no início da tentativa de conexão com o servidor. Quando o request é bem sucedido é apresentado um gráfico exibindo cada uma das fases descritas acima, com seus respectivos tempos de duração.

UnknownError2
Figura 4: Usando o Resource Timing

O CORS tem como função principal verificar se cada requisição realizada por intermédio de uma aplicação a um determinado recurso da internet, tem realmente permissão para fazê-la. Sem o CORS, ou outro dispositivo similar, seria possível que fosse construída uma aplicação para consumir recursos de quaisquer serviços implantados na rede, tais como bancos, lojas, orgãos governamentais. Bastando que para isto apenas soubesse a URI desejada para o acesso. É claro que há outras barreiras de segurança na rede, tais como o próprio uso de autenticação dos usuários. Podemos entender o CORS como uma primeira barreira de acesso realizada pelo browser, impedindo o acesso a domínios de rede não autorizados. Repetindo portanto, é o browser que executa o CORS. E é por isto que mesmo sem configurar o CORS, quando executa-se testes de conexão com os endpoints do backend via Postman o request funciona e pela execução via browser não funciona.

Numa aplicação onde não houvesse a separação do frontend com o backend, o CORS não seria necessário. Uma vez que as solicitações aos recursos estariam dentro do mesmo domínio, utilizando a mesma url base para toda a aplicação. A documentação a este respeito, https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS é muito mais rica que esta simples explicação e trás um nível de detalhamento muito maior é uma leitura recomendável.

No ASP.NET Core o CORS é configurado na classe Startup do projeto Web Api, na raiz do projeto. Há diversas formas de configurá-lo, basta realizar algumas consultas em algum buscador para verificar isto. A forma adotada neste projeto, foi a inserção do seguinte código no método ConfigureServices, juntamente aos outros códigos.

services.AddCors(options =>
{
options.AddPolicy(_AppCorsPolicy,
builder =>
{
builder.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod();
});
});

Que através da declaração e utilização da variável local, _AppCorsPolicy:

readonly string _AppCorsPolicy = "AppCorsPolicy";

pode ser repassada a extensão UseCors(), que está inserido no método Configure(IApplicationBuilder app, IHostingEnvironment env) da classe StartUp.

app.UseCors(_AppCorsPolicy);

Há uma questão relacionada ao uso das extensões WithOrigins e AllowAnyOriginsdo CORS. A extensão WithOrigins limita o acesso ao backend através da lista de opções configuradas e AllowAnyOrigins permite o acesso de qualquer origem, uma prática insegura. Na prática esta escolha não funciona em alguns casos, pois a especificação de AllowAnyOrigins juntamente a AllowCredentials retorna uma resposta inválida pelo browser. Há várias postagens que induzem a este tipo de erro, que aparentamente demonstram que estas duas extensões funcionam corretamente juntas. Mas AllowAnyOrigin afeta preflight requests e o cabeçalho/header Access-Control-Allow-Origin, conforme pode ser melhor observado em https://docs.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-2.2#preflight-requests. Portanto, espante a preguiça, mapeie as urls dos dispositivos que acessarão o backend e use a extensão WithOrigins.

No método Configure da classe StartUp também pode ser adicionado os métodos app.UseDefaultFiles() e app.UseStaticFiles() a fim de permitir ao IIS o acesso aos arquivos assets da interface de usuário através do backend. Verifique também se o CORS está instalado no site de sua aplicação no IIS, senão tiver instalado, instale-o.

CorsInstalledIIS
Figura 5: Instalação do CORS no site do web api (backend)

Há uma recomendação na documentação do ASP.NET Core sobre a possibilidade de uso do EnableCors decorator para os controllers da aplicação, https://docs.microsoft.com/pt-br/aspnet/core/security/cors?view=aspnetcore-2.2#enable-cors-with-attributes. Eu me assustpei quando vi esta recomendação na documentação, pois como não conseguia fazer a aplicação funcionar em produção, achei que teria que sair reescrevendo todos os meus controllers, mas isto não é obrigatório. Sua utilização permite o uso de várias políticas de uso do CORS. Nestes casos, sua implementação numa classe base para os controllers diminui bastante o trabalho. Basta usar o pacote Microsoft.AspNetCore.Cors e decore as classes desejadas.

MicrosoftAspNetCoreCors
Figura 6: Adicionando o Microsoft.AspNetCore.Cors ao projeto da web api

Não há nenhuma configuração no frontend referente ao CORS, todas as configurações são feitas no backend, que é o único responsável por barrar acessos não permitidos aos seus próprios recursos.

Neste ponto, algumas recomendações e dicas são importantes, dentro de um cenário de testes em que os problemas já duram algumas horas, ou até mesmo alguns dias. Por isto, resista em apenas compilar a aplicação e copiar apenas alguns arquivos para o servidor, sempre publique a aplicação e copie todos novamente. Há o caso por exemplo do arquivo APP_NAME.deps.json, que dependendo da alteração na propriedade launchSettings.json ou da classe Startup ele pode ser regerado pelo build, afetando a instalação caso não seja atualizado. A publicação da aplicação gera um arquivo web.config e vi diversas alterações que são sugeridas para alteração no web.config e até fiz algumas que resolveram alguns problemas. Mas há situações em que mudar o web.config só vai gerar problemas, deixe-o o mais enxuto possível. Por isto, não crie um web.config em sua app, deixe que o build faça isto pra você e vá adicionando as cláusulas exigidas pela sua aplicação. O build do Visual Studio parece ser muito competente em fazer isto, pelo menos para a minha aplicação que não tem tanta complexidade.

O seguinte código (copiado de alguma publicação na web, ele aparece bastante por aí) foi inserido em meu web.config:

<httpProtocol>
	<customHeaders>
		<add name="Access-Control-Allow-Origin" value="http://localhost:4200" />
		<add name="Access-Control-Allow-Credentials" value="true" >
		<add name="Access-Control-Allow-Headers" value="*" />
		<add name="Access-Control-Allow-Methods" value="*" />
	</customHeaders>
</httpProtocol>

Que na aplicação deste exemplo (não necessariamente isto vai ocorrer em outras aplicações com configurações diferentes para o IIS) retornou o seguinte erro:

Access to XMLHttpRequest at ‘http://server/api;auth/login/&#8217; from origin ‘http://localhost:4200&#8217; has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: The ‘Access-Control-Allow-Origin’ header contains multiple values ‘http://localhost:4200, http://localhost:4200/&#8217;, but only one is allowed.

Numa clara sinalização de que havia duas configurações distintas para o CORS, uma na aplicação (classe Startup) e outra no web.config. Ao retirar esta configuração do web.config, deixando apenas a configuração da classe Startup para o CORS a aplicação funciona, demonstrando que o CORS não precisa ser configurado em nenhum outro lugar. Caso resolva adotar uma configuração semelhante a que realizei, visualize no IIS o HTTP Response Header, ele deve estar vazio, conforme a figura abaixo:

HttpResponseHeaders
Figura 7: Http Response Header in IIS

Não há nenhum erro visualizado no console do Google Chrome que indique que o endereço da url esteja com uma barra a mais, ou algo similar. Os erros são bem genéricos e muitas vezes nos induzem a fazer mil outras coisas. Por isto verifique junto ao Postman, ou Fiddler a sua url, principalmente quando esta estiver sendo concatenada com variáveis de ambiente. Gastei algum tempo bastante significativo em função de um ponto e vírgula que digitei por acidente no lugar de uma barra.

Seguindo estas recomendações, uma aplicação de testes já poderia ter seus endpoints testados sem nenhum problema. Mas como já dissemos antes, esta é uma aplicação real, com acesso a banco de dados, com autenticação e autorização de usuários, com controle de log e uma arquitetura com várias ferramentas de terceiros. Por isto, após resolver as configurações do CORS, ainda restava o erro em que a descrição aparecia o termo ERR_UNSAFE_PORT. Este erro deve-se ao fato do Google Chrome reservar algumas portas para serviços especiais conforme a listagem neste link, https://chromium.googlesource.com/chromium/src.git/+/refs/heads/master/net/base/port_util.cc e eu acertei em cima de uma das portas que estão reservadas para estes serviços.

Foi uma experiência complexa, mas compensatória fazer este tipo de implantação. Como eu disse desde o início a pretensão era dar aos leitores interessados um norte para algum de seus problemas, fornecendo o maior número de informações possíveis através deste post. É claro que há muitas situações que não conseguimos cobrir com este post, mas fiquem a vontade em ir contribuindo com mais informações, através da correção ou adição de mais informações junto a estas. Este é apenas um pontapé inicial na tentativa de facilitar este tipo de trabalho.

Deixe um comentário