Contextualizing the Deployment Scenario
When starting this deploying process, an ASP.NET Core web application with Angular app, I went over a few days through the trial and error method, discovering several things that a backend and frontend need to work together in a virtual machine (Microsoft Azure Virtual Machine). There are many details required to deploy these two technologies ASP.NET Core and the Angular. Because of this, I decided to create some topics, so that I could remember and replicate in the next projects. These topics were increasing, as well as my frustration with these technologies working. It was then that I came up with the idea of writing a post, aiming to organize all the tasks performed in a way accessible to all people, with as much information as possible in this regard in one place. This is an article containing the basic activities for beginners (like me) to deploy applications similar to this one.
There is a lot of documentation to be read for a conceptual understanding regarding the practices performed here, not being the objective of this post to explain them. Throughout this post, some links will be presented, to provide greater technical support, as problems have arisen.
Quote:
This post has nothing special and inventive, I believe that its merit lies in the grouping of various information and tests regarding the technologies mentioned in one place.
I am a developer with some years of experience, but I am not in the infrastructure area. For this reason, some specialists, DevOps can “wrinkle their nose” when seeing the measures that have been adopted, which follow a chronological order as the problems arose. Although I had already deployed several other ASP.NET applications, I underestimate the ASP.NET Core deployment process, which is much more complex than the ASP.NET old versions. I believe that there are many developers who, like me, want and need to know how it works from end to end when it comes to building an application. A big problem for these professionals is knowing what configuration responsibilities belong to the backend installation and what belongs to the frontend. There are several times when it is not known whether the problem should be fixed on the backend or on the frontend via web server configuration, in this case IIS. This happens all the time, being very difficult to deal with, to know where the source of the problem begins.
From Theory to Practice
This is a relatively simple application, the strategy adopted from the beginning was to use the simplest installation possible for the deployment. And as I needed some extra configuration, I would take some action based on the technology documentation and the opinions of other professionals. But although it is a simple application, this is not a “Hello World
”, so there were several problems and doubts regarding access to data in a secure way through the UI, access to the database, user authentication, but mainly the fact that we have two separate applications talking to each other. So the process started with the publication of the web API and copy of the published files to the Windows 2012 R2 server, as well as the Angular build and the copy of the packages to the same server.
The web API application (backend) was implemented in ASP.NET Core 2.2, using EntityFramework Core 2.2.6 for data persistence, in a SQL Server Express database. Json Web Tokens - JWT was also implemented to authenticate users with the API, as well as the use of CORS, which prevents another unauthorized Web page from making requests for the domain of our application pages.
The frontend was implemented using Angular 8.2.3, with rxjs 6.4.0 for the implementation of observables to subscribe the endpoints (GET
, POST
, PUT
, DELETE
, PATCH
methods) of the web API. It's a simple application implemented in typescript 3.5, with 4.3.1 bootstrap and other interface plugins (ngx-bootstrap, Angular material, npmjs, etc.), without even using lazy loading to optimize the application loading. To deploy the Angular interface, the command, ng build --prod and the copy of its files to the server were executed, as recommended by the documentation. The Angular build packages files in optimized formats, making minifications to the files, disabling specific development checks in order to speed up the application, among other things.
To use a VM from the Microsoft Azure cloud, before starting the tests, open two ports to connect to the application. One port for accessing the web API and another for the user interface. Thus, you must create two end points, and you can create network filters or a subnet or network adapter. You can place filters, which control incoming and outgoing traffic, in a network security group attached to the resource that receives the traffic. To learn more about opening ports for a virtual machine with the Azure Portal:
Or for more details on Azure VM Endpoint configurations:
After these settings with your server, with the backend and frontend files duly copied to the server (in separate folders anywhere on the server), it is time to create a website for each of the applications. We will not go into details about the website creation process in IIS, just follow the recommendations in the documentation, this procedure is not usually a source of problems. However, after the creation of the two sites in IIS 8, a problem arose regarding the frontend files. Because when trying to load the frontend in Google Chrome, the application does not render the home page, displaying the message:
404 - File or directory not found. The resource you are looking for might have been removed,
had its name changed, or is temporarily unavailable.
This error refers to the lack of web.config to integrate the user interface implemented in Angular to the web server. In practical terms, web.config is the file that performs the IIS configuration, for parameter definitions used by the application with the web server. The build of Angular is not directed to a specific server, in this case, Internet Information Server - IIS, since that same angular application can also be deployed with Apache, Nginx, Golang, Github Pages, etc. For this reason, after generating the deploy, you must manually create a web.config file for the frontend and place it in the same folder that contains the frontend files.
Figure 1: Google Chrome view of the home screen of the application without web.config
Below is the web.config code extracted from Angular's documentation for this purpose.
<configuration>
<system.webserver>
<security>
<authorization>
<remove roles="" users="*" verbs="">
<add accesstype="Allow" users="?">
</add></remove></authorization>
</security>
<staticcontent>
<remove fileextension=".woff">
<mimemap fileextension=".woff2" mimetype="application/font-woff2">
<mimemap fileextension=".woff" mimetype="application/x-font-woff">
</mimemap></mimemap></remove></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">
</add></add></conditions>
<action type="Rewrite" url="/index.html">
</action></match></rule>
</rules>
</rewrite>
</system.webserver>
</configuration>
(*)The first line of this code was commented by CodeProject editor itself.
The original web.config file provided in the documentation establishes some rules through the <rewrite>
and <rules>
tags, to define the behavior of routes within IIS. This is essential for the application to work, because by rewriting the URL addresses of the requests and their HTTP responses, it has also been added to web.config the tag <staticContent>
to IIS recognize files with .woff and .woff2 extensions, through the section. There are other ways to make these options, but in this example, we will use these resources via code inserted in web.config.
Figure 2: Rewrite URL section of the Angular application in the IIS
Still talking about the frontend deployment, there is a recommendation in some posts regarding the alteration of the index.html file generated by the Angular build. The index.html has a tag that specifies the base path to resolve URLs related to the application and assets, static files, images, scripts and others base href='/'
. This path can be changed in some cases where the production environment has subfolders for installation within other applications and other specificities. There are some examples where the slash is removed from the base tag. In some moments to try solving problems loading pages, I changed this tag to try other possibilities and the application stopped fully. The slash indicates the root of the application, for applications that are not linked to other applications and should not be changed in these cases. Only in specific situations, this tag should be changed.
The Backend and CORS, a Separate Chapter
It's important to know about some concepts regarding the type of Web API project, to make the correct settings in the deploy. Once again, I will not go into too much detail, as the ASP.NET Core documentation is very didactic. In case of the backend of this example, it is FDD (structure-dependent deployment), where the application is deployed using the .NET Core version present in Windows Server 2012 R2. For more information in this regard, check out this link and to know more about the deployment types of ASP.NET Core API web services, click on this link.
The InProcess
is the Web API hosting model used in this project, which is already configured by Visual Studio in the creation template.
<propertygroup>
<targetframework>netcoreapp2.2</targetframework>
<aspnetcorehostingmodel>InProcess</aspnetcorehostingmodel>
</propertygroup>
However, it is important to know some characteristics of this type of hosting, so that there is no perception that it was adopted just because it is a project template of Visual Studio. The InProcess
is ideal for the architecture used in this project because it uses the IIS HTTP server (IISHttpServer
), which is already configured, instead of the Kestrel server. It uses CreateDefaultBuilder
to call the UseIIS()
method to register the IISHttpServer
. In this way, the Program
startup class of the service can remain as is, without any additional configuration, being ready to use in production enviroment. The code snippet below is part of the Program.cs file that is at the root of the project.
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup Startup();
}
By starting running the application by browsing its HTML pages, the code layout, formatting and images are displayed. This is a sign that the frontend installation is installed correctly. However, when starting to use the features that access the backend, in the case of this example, the login page. Inserting the user and password that call the endpoint of the authentication controller in the backend, an error is presented:
main-es2015.fbdfd9b26a7e658d49a6.js:1 Http failure response for
http://server-sample.cloudapp.net:95/api/auth/login/: 0 Unknown Error.
This error type is difficult to identify at first, since the message in the browser itself refers to an Unknown Error. In this example, it occurred due to an incorrect configuration of CORS (Cross-Origin Resource Sharing), but it can also occur due to the lack of its configuration.
Figure 3: Unknown Error
Using the analysis tools of Google Chrome throughout Resource Timing, it is possible to try to diagnose the problem. The Resource Timing display phases that allow viewing a client request, until its receipt and sending the server's response to each sent request. It's also possible to receive different indicators of performance problems related to this processing of data. You can take a look at this link.
There are several phases that Resource Timing presents at each request, starting by 'queuing requests' (if necessary), going through 'stalled/blocking', 'Proxy Negotiation', 'DNS Lookup', 'Connecting', 'SSL Request Sent', 'Waiting and Downloading' presenting the time in milliseconds for the execution for each one. The analysis of the post request generated by the login request ends out in the first 'stalled' phase, without showing any other phases, signalling the interruption of the request process to the server. When the request is successful, a graph would be presented showing each of the phases described above, with their respective duration times.
Figure 4: Using the Resource Timing
CORS has the main function verifying each request made through an application to a specific internet resource (a backend endpoint), validating if the request is allowed or not. Without CORS, or another similar device, it would be possible to make an application to consume resources from any services deployed on the network, such as banks, stores, government agencies. Needing to know only the desired URI for access. Of course, there are other security barriers on the network, such as the user authentication itself. We can understand CORS as a first access barrier, performed by the browser to prevent access to unauthorized network domains. Again, it is the browser that runs CORS. Because this is even without configuring CORS, when running connection tests for backend endpoints via Postman or other tool, the request works and by executing via the browser, it does not work.
In an application without separation between the frontend and the backend, CORS wouldn't be necessary. Because the requests for resources would be within the same domain, using the same base URL for the entire application, there are no locks. The documentation in this regard, https://developer.mozilla.org/en-US/docs/ Web / HTTP / CORS is much richer than this simple explanation and brings a higher level of detail, hence it is recommended to read it.
In ASP.NET Core, CORS is configured in the Startup
class of the web API project, at the project root. There are several ways to configure it, just make some queries in some search engine to check this. The form adopted in this project was the insertion of the following code in the ConfigureServices
method, together with the other codes.
services.AddCors(options =>
{
options.AddPolicy(_AppCorsPolicy,
builder =>
{
builder.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
That is done by declaring and using the local variable, _AppCorsPolicy
:
readonly string _AppCorsPolicy = "AppCorsPolicy";
_AppCorsPolicy
can be assigned to the UseCors()
extension, which is inserted in the Configure
method (IApplicationBuilder
app, IHostingEnvironment
env) of the Startup
class.
app.UseCors(_AppCorsPolicy);
There is an issue regarding the use of the CORS WithOrigins
and AllowAnyOrigins
extensions. The WithOrigins
extension limits access to the backend through the list of configured options and AllowAnyOrigins
allows access from any origin, an insecure practice. In practice, this choice does not work in some cases, as the specification of AllowAnyOrigins
together with AllowCredentials
returns an invalid response by the browser. There are several posts that induce this type of error, which apparently demonstrate that these two extensions work correctly together. But AllowAnyOrigin
affects preflight requests and the Access-Control-Allow-Origin header / header, as best seen here. So chase away laziness, map the URLs of the devices that will access the backend and use the WithOrigins
extension.
In the Configure
method of the Startup
class, the app.UseDefaultFiles()
and app.UseStaticFiles()
methods can also be added to allow IIS access to the user interface assets files through the backend. Also, check if CORS is installed on the website of your application in IIS, if not installed, install it.
Figure 5: CORS on the Web API IIS Site (backend)
There is a recommendation in the ASP.NET Core documentation on the possibility of using the EnableCors decorator for the application's controllers, check out this link. I was scared when I saw this recommendation in the documentation, because as I was unable to make the application work in production, I thought I would have to rewrite all my controllers, but this is not mandatory. Its use allows the use of several CORS usage policies. In this case, its implementation in a base class for controllers that reduces work a lot. Just use the Microsoft.AspNetCore.Cors
package and decorate the desired classes.
Figure 6: Adding the Microsoft.AspNetCore.Cors to the backend
Quote:
There is no configuration on the frontend regarding CORS, all configurations are made on the backend, which is solely responsible for barring unauthorized access to its own resource.
At this point, some recommendations and tips are important, within a testing scenario where problems have lasted for a few hours, or even a few days. For this reason, you should be to resist in just build the application and copying only a few files to the server, always publish the application and copy them all again.. There is the case of the APP_NAME.deps.json file which depending on the change in the launchSettings.json property or the Startup
class it can be regenerated by the build, affecting the installation if it is not updated. By publishing the application project, it generates a web.config file. I saw several changes that are suggested to change in web.config and I even made some that solved some problems. But there are situations in which changing web.config will only cause problems, make it as dry as possible. So, don't create a web.config in your app, let the publish do this for you and go added the clauses required by your application. The Visual Studio Publish seems to be very competent in doing this, at least for my application that is not so complex.
The code snippet following (copied from some publication on the web) was inserted in my 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="*">
</add></add></add></add></customheaders>
</httpprotocol>
That in the application of this example (not necessarily, this will occur in other applications with different configurations for IIS) returned the following error:
Access to XMLHttpRequest at 'http://server/api;auth/login/' from origin
'http://localhost:4200' 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/', but only one is allowed.
Clarifying that there were two different configurations for the CORS, one in the app (Startup
Class) and another in web.config. When removing this configuration from web.config, leaving only the configuration of the Startup
class to CORS, the application works, demonstrating that CORS doesn't need to be configured anywhere else. If you decide to adopt a configuration similar to this, view the HTTP Response Header in IIS, it must be empty, as shown in the figure below:
Figure 7: HTTP Response Header in IIS
There is no error seen on the Google Chrome console that indicates that the URL address has an extra slash, or something similar. Mistakes are very generic and often lead us to do a thousand other things. So check with Postman, or Fiddler, for your URL, especially when it is being concatenated with environment variables. I spent quite a bit of time because of a semicolon I accidentally typed in instead of a slash.
Following these recommendations, a test application could have its endpoints tested without any problems. But as we said before, this is a real application, with access to a database, with user authentication and authorization, with log control and an architecture with several third-party tools. For this reason, after resolving the CORS settings, there was still an error in which the description appeared with the term ERR_UNSAFE_PORT
. This error is due to the fact that Google Chrome reserves some ports for special services as listed in this link and I got it right over one of the ports that are reserved for these services.
It was a slightly difficult experience but is rewarding to do this implantation type. As I said from the beginning, the intention was to give interested readers a guide to some of their problems, providing as much information as possible through this post. Of course, there are many situations that we were unable to cover with this post, but feel free to contribute more information, by correcting or adding more information to this post. This is just a kick-start in an attempt to facilitate this kind of work.
History
- 21st January, 2020: Initial version