Building Microservices On .NET Core – Part 4 Building API Gateways With Ocelot
This is the fourth article in our series about building microservices on .NET Core. In the first article we introduced the series and prepared the plan: business case and solution architecture. In the second article we described how you can structure internal architecture of one microservice using CQRS pattern and MediatR library. In third article we described the importance of service discovery in microservice based architecture and presented practical implementation with Eureka.
In this article we are going to focus on another fundamental concepts of microservice based architecture – api gateways.
Source code for complete solution can be found on our GitHub.
What is API Gateway
One of the advantages of microservice based approach is that you can compose big system out of smaller services, each responsible for one business capability. This approach, when applied to big and complex domains like e-commerce, insurance or finance, results in solution made of several to tens microservices. Taking into account that this landscape is dynamic, new instances of services are started when workload increases, new services are added, some services are split into multiple ones, you can imagine how hard it would be if you would like to access each service directly from your client application.
API Gateway pattern tries to resolve problem of accessing individual services from client applications, by adding a single point of interaction between client application and backend services. API Gateway works as a facade that hides complexity of the underlying system from its clients.
API Gateway is another microservice running in front of your backend services and exposing only operations needed by given client.
API Gateway can do more than just routing requests from client application into proper backend services. However you should be careful not to introduce business and process logic that may result in overambitious api gateways issue.
Apart from routing API Gateways are usually responsible for security. We usually do not allow unauthenticated and unauthorized calls to get through gateway, so it is gateway responsibility to check if required security tokens are present, valid and contain required claims.
Next thing is handling CORS. API Gateway must be prepared to be accessed from web browsers running single page applications from different origin than API-Gateway.
API Gateways are often responsible for request and response transformation like adding headers, changing request formats to translate between data representations used by the client and by the server.
Last but not least API Gateway can be used to change communication protocols. For example you can expose your services as HTTP REST on API Gateway, while these calls are translated by API Gateway into gRPC for example.
In our IT company it is common practise to build separate API Gateways for each type of client application. For example if we have microservices based system for insurance we would build: a separate gateway for insurance agents portal, a separate gateway for back-office application, a separate gateway for bank-insurance integration, a separate gateway for end customers mobile application.
Building API Gateway with Ocelot
There are many solutions for building API Gateways in the Java land, but when I was searching for solutions in the .NET space the only viable solution, apart from building your own from scratch, is Ocelot. It is a very interesting and powerful project, used even in Microsoft official samples.
Let’s implement API Gateway for our sample insurance sales portal using Ocelot.
Getting started
We start with empty ASP.NET Core web application. All we need is Program.cs and appsettings.json files.
We start by adding Ocelot to our project using nuget.
Install-Package Ocelot
In our project we also use Ocelot service discovery and cache features, so we need to add two more NuGet packages: Ocelot.Provider.Eureka and Ocelot.Cache.CacheManager. Finally our solution should look like on the picture below.
In next step we need to add ocelot.json file which will host our Ocelot gateway configuration.
Now we can modify Program.cs to properly bootstrap all required services including Ocelot.
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.UseUrls("http://localhost:8099")
.ConfigureAppConfiguration((hostingContext, config) =>
{
config
.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true,
true)
.AddJsonFile("ocelot.json", false, false)
.AddEnvironmentVariables();
})
.ConfigureServices(s =>
{
s.AddOcelot().AddEureka().AddCacheManager(x => x.WithDictionaryHandle());
})
.Configure(a =>
{
a.UseOcelot().Wait();
})
.Build();
}
}
The most important parts here are: adding ocelot.json configuration file, adding Ocelot service with Eureka and Cache manager support.
If you remember from previous part of this series we use Eureka as service registry and discovery mechanism. Here we want to take advantage of it and tell Ocelot to resolve downstream services urls from Eureka instead of hard-coding it.
We are also using caching support in Ocelot to present how you can configure api gateway to cache same slowly changing data.
In order for all of this to work we must now fill configuration files properly.
Let’s start with appsettings.json, where we add Eureka configuration.
{
"spring": {
"application": { "name": "Agent-Portal-Api-Gateway" }
},
"eureka": {
"client": {
"serviceUrl": "http://localhost:8761/eureka/",
"shouldRegisterWithEureka": false,
"validateCertificates": false
}
}
}
Now it’s time to have a work on ocelot.json – central configuration part of our api gateway. ocelot.json consist of two main sections: ReRoutes and GlobalConfiguration.
ReRoutes defines routes – maps endpoints exposed by api gateway to backend services. As part of this mapping security, caching and transformations can also be defined.
GlobalConfiguration defines global setting for the whole api gateway.
Let’s start with GlobalConfiguration:
"GlobalConfiguration": {
"RequestIdKey": "OcRequestId",
"AdministrationPath": "/administration",
"UseServiceDiscovery" : true,
"ServiceDiscoveryProvider": { "Type": "Eureka", "Host" : "localhost", "Port" : "8761"}
}
Key things here are: enabling service discovery and pointing to right Eureka instance.
Now we can define routes. Let’s define our first route that will map request coming to api-gateway as HTTP GET for /Products/{code} to downstream service ProductService that exposes product data as HTTP GET [serviceHost:port]/api/Products/{code}.
"ReRoutes": [
{
"DownstreamPathTemplate": "/api/Products/{everything}",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/Products/{everything}",
"ServiceName": "ProductService",
"UpstreamHttpMethod": [ "Get" ]
}
]
DownstreamPathTemplate specifies backend service url, UpstreamPathTemplate specifies url that is exposed by api gateway, Downstream and Upstream Schema specifies schema, ServiceName specifies name under which downstream service is registered in Eureka.
Let’s see another example. This time we will configure offer creation service, which is exposed by PolicyService as HTTP POST [serviceHost:port]/api/Offer
{
"DownstreamPathTemplate": "/api/Offer",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/Offers",
"ServiceName": "PolicyService",
"UpstreamHttpMethod": [ "Post" ]
}
Advanced features of Ocelot
Cors
This is not related to Ocelot per se, but it is often required to support cross origin request at api gateway layer. We need to modify our Program.cs. “In ConfigureServices() we need to add”:
s.AddCors();
In Configure() method we need to add:
a.UseCors(b => b
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
);
Security
Next we will add JWT token based security to our api gateway. This way unauthenticated request won’t pass through our api gateway.
In our BuildWebHost method we need to add a key we will use for JWT validation. In real world application you should store this key in a secure secret store, but for demonstration purposes let’s just create a variable.
var key = Encoding.ASCII.GetBytes("THIS_IS_A_RANDOM_SECRET_2e7a1e80-16ee-4e52-b5c6-5e8892453459");
Now we need to setup security in ConfigureService():
s.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer("ApiSecurity", x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
With this settings we can now come back to ocelot.json and define security requirements for our routes.
In our case we require that user is authenticated and token contains claim userType with value SALESMAN.
Let’s see how this can be configured:
{
"DownstreamPathTemplate": "/api/Products",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/Products",
"ServiceName": "ProductService",
"UpstreamHttpMethod": [ "Get" ],
"FileCacheOptions": { "TtlSeconds": 15 },
"AuthenticationOptions": {
"AuthenticationProviderKey": "ApiSecurity",
"AllowedScopes": []
},
"RouteClaimsRequirement": {
"userType" : "SALESMAN"
}
}
We added AuthenticationOptions section to link authentication mechanism defined in Program.cs with Ocelot and then we specified in RouteClaimsRequirement which claim with which value must be provided in order for the request to be passed to backend service.
Service Discovery
We’ve already presented usage of Eureka for service discovery. You don’t have to use service discovery and can map upstream request to backend services using hard coded urls, but this will remove many advantages of microservice based architecture and make your deployment and operations very complex, as you have to keep in sync your backend microservices urls with ocelot config.
Apart from Eureka Ocelot supports other service discovery mechanism: Consul and Kubernetes.
You can read more about this subject in Ocelot service discovery documentation.
Load Balancing
Ocelot provides built-in load balancer which can be configured per each route. There are four types of it available: least connection, round robin, cookie sticky session, first available service.
You can read more about in in Ocelot documentation.
Caching
Ocelot provides out of the box simple caching implementation. Once you include Ocelot.Cache.CacheManager package and activate it
s.AddOcelot()
.AddCacheManager(x => { x.WithDictionaryHandle(); })
You can configure caching for each route. Let’s for example add caching to route that fetches product definition with given product code:
{
"DownstreamPathTemplate": "/api/Products/{everything}",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/Products/{everything}",
"ServiceName": "ProductService",
"UpstreamHttpMethod": [ "Get" ],
"FileCacheOptions": { "TtlSeconds": 15 },
"AuthenticationOptions": {
"AuthenticationProviderKey": "ApiSecurity",
"AllowedScopes": []
},
"RouteClaimsRequirement": {
"userType" : "SALESMAN"
}
}
This configuration tells Ocelot to cache result of given request for 15 seconds.
Ocelot also gives you ability to plug-in your own caching opening possibilities to extend simple cache with more robust options like Redis or memcache.
You can read more about it in Ocelot caching documentation.
Rate Limiting
Ocelot supports rate limiting. This feature helps you protect downstream services from overloading. As usual you can configure rate limiting per route basis. In order to enable rate limiting you need to add the following json to your route:
"RateLimitOptions": {
"ClientWhitelist": [],
"EnableRateLimiting": true,
"Period": "1s",
"PeriodTimespan": 1,
"Limit": 1
}
ClientWhiteList lets you specify which clients should not be limited, EnableRateLimiting enables rate limiting, Period configures period of time to which limit applies (can be specified in seconds, minutes, hours or days), Limit configures number of requests permitted in given period. If in given Period client exceeds number of request specified in Limit then they have to wait PeriodTimespan before another request is passed to downstream service.
Transformation
Ocelot allows us to configure header and claims transformation. You can add headers to request and response. Apart from static values you can also use placeholders: {RemoteIpAddress} client IP address, {BaseUrl} ocelot base url, {DownstreamBaseUrl} downstream service base url and {TraceId} Butterfly trace id (if you use Butterfly distributed tracing). You can also find and replace header values.
Ocelot also allows you to access claims and transform them into headers, query string params or other claims. This is very useful when you need to pass information about authorized user to backend service. As always you specify these transformations per route basis.
In the example below you can see how you can extract sub claim and put it in CustomerId header.
"AddHeadersToRequest": {
"CustomerId": "Claims[sub] > value[1] > |"
}
You can read more about this topic in Ocelot header transformation documentation and Ocelot claim transformation documentation.
Summary
Ocelot offers us feature rich api gateway implementation with almost no coding required. Most of the work you have to perform is related to properly defining routes between exposed api gateway endpoints and backend services urls. You can easily add authentication and authorization support and caching.
Apart from features described in this post, Ocelot also supports request aggregation, logging, web sockets, distributed tracing with Butterfly project and delegating handlers.
You can check out complete solution source code at: https://github.com/asc-lab/dotnetcore-microservices-poc.