In the demo, the web API app is replicated to four instances ( -- scale api=4
), and Nginx is served as a reverse proxy for the four api
services. When the client visits the https://localhost site, the four api
services handle the HTTP requests in a round-robin manner, as can be seen from the console logs and the Host Name
in the web page.
The complete solution is located in my GitHub repository. In this article, we will go over some implementation details.
We first create an ASP.NET Core Web API project MyWebApi
from the dotnet template. Since we will use Nginx for SSL termination, we don’t need to enable HTTPS support in our ASP.NET Core project.
The MyWebApi
app will be hosted by Kestrel which is sitting behind Nginx. HTTPS requests are proxied over HTTP, so the original scheme (HTTPS) and originating client IP address are lost. To remediate that, we can forward original scheme and client IP using forwarded headers by enabling the Forwarded Headers Middleware. Note that the Forwarded Headers Middleware should run before other middlewares. The following code snippet shows an example implementation.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseForwardedHeaders();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
In the code above, lines 4 to 7 configure the Forwarded Headers options. In the options, we can also restrict KnowProxies
and KnowNetworks
for security purposes when the app is in production. Line 12 calls the middleware to run the first in the pipeline, so that the forwarded headers information is available for other middlewares.
We can add some debugging code to check if everything works or not. The following code snippet sets up the application root page to show the host information, HTTP request scheme/path/headers, and the remote IP address.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGet("/", async context =>
{
context.Response.ContentType = "text/plain";
// Host info
var name = Dns.GetHostName(); // get container id
var ip = Dns.GetHostEntry(name).AddressList.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork);
Console.WriteLine($"Host Name: { Environment.MachineName} \t {name}\t {ip}");
await context.Response.WriteAsync($"Host Name: {Environment.MachineName}{Environment.NewLine}");
await context.Response.WriteAsync(Environment.NewLine);
// Request method, scheme, and path
await context.Response.WriteAsync($"Request Method: {context.Request.Method}{Environment.NewLine}");
await context.Response.WriteAsync($"Request Scheme: {context.Request.Scheme}{Environment.NewLine}");
await context.Response.WriteAsync($"Request URL: {context.Request.GetDisplayUrl()}{Environment.NewLine}");
await context.Response.WriteAsync($"Request Path: {context.Request.Path}{Environment.NewLine}");
// Headers
await context.Response.WriteAsync($"Request Headers:{Environment.NewLine}");
foreach (var (key, value) in context.Request.Headers)
{
await context.Response.WriteAsync($"\t {key}: {value}{Environment.NewLine}");
}
await context.Response.WriteAsync(Environment.NewLine);
// Connection: RemoteIp
await context.Response.WriteAsync($"Request Remote IP: {context.Connection.RemoteIpAddress}");
});
});
#nginx #dotnet #programming #docker