Key Takeaways
- What we fundamentally do in terms of creating an API Gateway for the WSO2 API Manager is to create Ballerina services that correspond to the APIs published on the API Manager.
- A Request Filter has access to the incoming request data. And therefore it can be used to inspect its contents for validations, copying and pushing them over to other systems or even for modifying the original request.
- By default a client application calling a secured Service on the Gateway needs to send a token (JWT) signed by a STS (Security Token Service) that is trusted by the Gateway. The request should comply with the OAuth Bearer profile.
- Ballerina Streams allows you to build streaming queries for performing projections, filtering, windows, stream joins and patterns on data that you receive on an event stream.
- In many Microservice architectures users would need to generate container runtimes of the API Gateway. Ballerina makes this simple by allowing us to annotate the service with the relevant Docker and Kubernetes annotations.
A modern API is a well-defined and easy to understand network accessible function that caters for a particular business requirement. An API Gateway is a layer in an architecture pattern that allows the business API to focus on the business functionality while taking care of the request dispatching, policy enforcement, protocol translations and analytics.
This article is about how we built an API Gateway in Ballerina for the WSO2 API Manager. WSO2 API Manager is an Open Source Full Lifecycle API Management solution. It hosts capabilities to design and document APIs and publish them with various kinds of policies. It also has a developer portal which is used by application developers to discover and subscribe to APIs. Its security component provides facilities for client applications to obtain tokens to access APIs. The Ballerina based API Gateway acts as the enforcement point of the policies attached to APIs by the API design and discovery components.
Generating Ballerina Services from APIs in API Manager
Ballerina is a programming language designed to make integration simple and agile. As such it contains all constructs typically required in the integration domain such as Services, Endpoints, Circuit Breakers and a lot more. What we fundamentally do in terms of creating an API Gateway for the WSO2 API Manager is to create Ballerina services that correspond to the APIs published on the API Manager. Since API Manager defines the API definitions (resource paths, verbs, etc) and the target endpoint details of each API, we have developed a tool which connects to the API Manager via its REST interfaces and converts the API definitions into Ballerina source code based on a set of templates.
The same process applies to the policies defined on the API Manager as well. All policies, such as the quota policy enforcements defined on the API Manager are converted to Ballerina source code.
The following diagram depicts this process of code generation.
The generated source code is organized into a package so that the Ballerina compiler can generate a single binary as an executable artifact.
In the sections to follow we will look at the language constructs and concepts we used in Ballerina to make it work as an API Gateway. I will be explaining why we used each construct and how we have used them with code examples.
Ballerina as a simple proxy
An API on an API Gateway is in essence a proxy which sits in between the client application and the target API. Its core responsibility is to intercept requests from the client application to the target API and ensure appropriate policies are enforced on them. The following is a simple example of a Ballerina service that acts as a proxy to a target API.
import ballerina/http;
import ballerina/log;
//The Target Endpoint
endpoint http:Client targetEndpoint {
url: "https://api.pizzastore.com/pizzashack/v1"
};
//This service is accessible at /pizzashack/1.0.0 on port 9090
@http:ServiceConfig {
basePath: "/pizzashack/1.0.0"
}
service<http:Service> passthrough bind { port: 9090 } {
@http:ResourceConfig {
methods:["GET"],
path: "/menu"
}
passthrough(endpoint caller, http:Request req) {
//Forward the client request to the /menu resource of the Target
//Endpoint.
var clientResponse = targetEndpoint->forward("/menu", req);
//Check if the response from the target Endpoint was a success or not.
match clientResponse {
http:Response res => {
caller->respond(res) but { error e =>
log:printError("Error sending response", err = e) };
}
error err => {
http:Response res = new;
res.statusCode = 500;
res.setPayload(err.message);
caller->respond(res) but { error e =>
log:printError("Error sending response", err = e) };
}
}
}
}
The above is a simple Ballerina service which listens for requests on port 9090. This service is exposed on the path (basepath) /pizzashack/1.0.0
and has a resource definition to accept GET
requests on subpath /menu
. All GET
requests on /menu
are forwarded to the target endpoint https://api.pizzastore.com/pizzashack/v1/menu.
Request filters
Now that we have successfully created a simple passthrough proxy, the next step would be to see how we can make the Gateway enforce various QoS on API requests such as authentication, authorization, rate limiting and analytics.
If you look at the service definition above, you would notice that the service “binds” to a port (9090). What actually happens in here is that the service actually binds to the default HTTP Listener in Ballerina on port 9090. Ballerina allows us to define our own Listener for the service to bind to. Each listener can have its own set of request filters. A Request Filter has access to the incoming request data. And therefore it can be used to inspect its contents for validations, copying and pushing them over to other systems or even for modifying the original request. We have chosen to implement a custom Listener to which we can bind our services to since we can then engage our custom request filters to the Listener. The following is the interface definition of our Listener.
public type APIGatewayListener object {
public {
EndpointConfiguration config;
http:Listener httpListener;
}
new () {
httpListener = new;
}
public function init(EndpointConfiguration config);
public function initEndpoint() returns (error);
public function register(typedesc serviceType);
public function start();
public function getCallerActions() returns (http:Connection);
public function stop();
};
As you can see a Listener requires a list of functions to be implemented such as for initializing, starting the listener, stopping the listener, etc. I haven’t included the implementations of the particular functions since it is too much detail at this point.
The following is the signature of the Authentication Request Filter. The code within the filterRequest
function performs authentication of the calling client application and propagates the state of the Filter to the downstream filters by returning a FilterResult
object from the function.
public type AuthnFilter object {
public function filterRequest (http:Request request, http:FilterContext
context) returns http:FilterResult {
Now that we’ve seen how to declare our own Listener and implement a Request Filter, the next step is to go ahead and declare our Listener as an Endpoint
and engage all our Request Filters in the preferred order. The following is how it is done.
import wso2/gateway;
AuthnFilter authnFilter;
OAuthzFilter authzFilter;
RateLimitFilter rateLimitFilter;
AnalyticsFilter analyticsFilter;
ExtensionFilter extensionFilter;
endpoint gateway:APIGatewayListener apiListener {
port:9095,
filters:[authnFilter, authzFilter, rateLimitFilter,
analyticsFilter, extensionFilter]
};
We have imported the wso2/gateway
package since our custom APIGatewayListener
resides within that particular package. We have then declared all of our Request Filters which perform various operations on the incoming request. Finally we declare the endpoint with the default port and all the filters in the order in which they are to be executed.
Once we have the endpoint (listener and filters) declared, the final step is to bind this endpoint to our proxy service. In the example service used in the previous section, we bound the service to the default HTTP listener on port 9090. Now that we have a fully functional endpoint which performs all of the QoS on API requests expected by an API Gateway we need to change our service definition as below.
//This service is accessible at /pizzashack/1.0.0 on port 9095
@http:ServiceConfig {
basePath: "/pizzashack/1.0.0"
}
service<http:Service> passthrough bind apiListener {
As you can see, instead of saying bind {port : 9090}
we now say bind apiListener. This makes our service listen on the new endpoint we declared and hence all requests that are served by this particular service will now have to go through our Request Filters which perform all functions expected by an API Gateway.
Securing a Service over OAuth
Authentication
Ballerina allows us to secure its services over OAuth and Basic Authentication. The default mode of securing services on the API Gateway is OAuth since it has established itself as the de facto standard for securing REST APIs. You can find an example of securing services on the ballerina.io website.
First you need to specify the authentication providers at the listener definition as follows.
http:AuthProvider jwtAuthProvider = {
scheme:"jwt",
issuer:"ballerina",
audience: "ballerina.io",
certificateAlias: "ballerina",
trustStore: {
path: "${ballerina.home}/bre/security/ballerinaTruststore.p12",
password: "ballerina"
}
};
endpoint gateway:APIGatewayListener apiListener {
port:9095,
filters: ....,
authProviders:[jwtAuthProvider]
};
This means that whatever requests being received on our listener on port 9095 should be run via the jwtAuthProvider
which validates whether the incoming requests contain an http header named ‘Authorization’ bearing a trusted JWT string (more on the JWT later). Once that part is done, following is how a service can be secured in Ballerina.
//This service is accessible at /pizzashack/1.0.0 on port 9090
@http:ServiceConfig {
basePath: "/pizzashack/1.0.0",
authConfig: {
authentication: { enabled: true }
}
}
The authConfig
is actually optional since our Listener Endpoint which the service binds to enables security by default. I have mentioned it in here for clarity.
Authorization
Authorization is enabled per each operation of the service using scopes. A scope defines a claim the token that comes along with the incoming request needs to bear in order for the request to be granted access to the resource/operation. Following is how we can specify scopes for a resource/operation.
@http:ResourceConfig {
methods:["GET"],
path: "/menu",
authConfig: {
scopes: ["list_menu"]
}
}
passthrough(endpoint caller, http:Request req) {
The above declares that the token coming along with the incoming request needs to bear a scope named as “list_menu
” to be granted access to the resource.
By default a client application calling a secured Service on the Gateway needs to send a token (JWT) signed by a STS (Security Token Service) that is trusted by the Gateway. The request should comply with the OAuth Bearer profile. Which basically says the token should be sent in an http header named “Authorization” and its value should be in the form of “Bearer $token
”. The “trust” factor is based on whether or not the Gateway has access to the public certificate of the STS. A JWT is a 3 part string delimited by a ‘.’ (dot) character. Each part has to be base64 encoded. The 3 parts being the JWT header, JWT payload and JWT signature. The following is what a JWT payload looks like
{
"sub": "ballerina",
"iss": "ballerina",
"exp": 2818415019,
"iat": 1524575019,
"jti": "f5aded50585c46f2b8ca233d0c2a3c9d",
"aud": [
"ballerina",
"ballerina.org",
"ballerina.io"
],
"scope": "list_menu"
}
As you can see, the JWT needs to bear the scope claim with the list of scopes it bears. The access to the resource would be granted if the scope claim contains a scope that the resource expects.
Ballerina Streams for Rate Limiting Policy Enforcement
API Manager allows you to enforce rate limiting policies against APIs. The policies are defined on the administrative portal of the API Manager. A rate limit policy defines request quotas based on number of requests or data bandwidth per defined time unit. For example, the policy “Silver” would allow an API to be accessed at 2000 request per minute by an Application. We have used Streams in Ballerina to achieve this functionality. It allows you to build streaming queries for performing projections, filtering, windows, stream joins and patterns on data that you receive on an event stream.
An event is generated out of each message successfully processed by the Service. This event is then put into a stream, named as the requestStream
as shown below. The RequestStreamDTO
object contains all of the information required by the policy to make its decisions. Some of these information are derived from the http request itself and some are inferred by the upstream filters such as the authentication filter (ex: who is accessing my service).
public stream<RequestStreamDTO> requestStream;
public function publishNonThrottleEvent(RequestStreamDTO request) {
requestStream.publish(request);
}
Each rate limiting policy (Silver) is modeled as a Ballerina function which runs a busy loop listening on the requestStream
. Upon receiving an event on the stream, we execute a query on the data received via the event to check if it meets the criteria defined in the policy. The following is an example of such a function which checks if more than 2000 requests are being received via a given application within a time window of 1 minute.
function initSubscriptionSilverPolicy() {
stream<gateway:GlobalThrottleStreamDTO> resultStream;
stream<gateway:EligibilityStreamDTO> eligibilityStream;
forever {
from gateway:requestStream
select messageID, (subscriptionTier == "Silver") as isEligible,
subscriptionKey as throttleKey
=> (gateway:EligibilityStreamDTO[] counts) {
eligibilityStream.publish(counts);
}
from eligibilityStream
throttler:timeBatch(60000, 0)
where isEligible == true
select throttleKey, count(messageID) >= 2000 as isThrottled,
expiryTimeStamp
group by throttleKey
=> (gateway:GlobalThrottleStreamDTO[] counts) {
resultStream.publish(counts);
}
If a quota breach is identified, an event is created and pushed into a resultStream
. The RateLimitFilter
on the Endpoint listens on this resultStream
and blocks off requests if they have been identified as being received from an Application that has breached its quota.
Ballerina File API for Aggregating Analytics Data
The API Gateway is also expected to feed data into an Analytics engine for generating business insights in the form of reports. Data retrieved via all successful requests are batched into a file periodically for a given time window. Since the Gateway can host more than one service it is possible that multiple services would attempt to write data to the same file concurrently, causing potential write locks on the file for long periods of time. We have used Ballerina streams to avoid this situation. This guarantees that there is only one thread in the system that writes data to the file sequentially and also ensures that file writes happen asynchronously to the request processing thread. The following is a code block from the AnalyticsFilter
which writes events to a stream.
public function filterRequest(http:Request request, http:FilterContext context) returns http:FilterResult {
http:FilterResult requestFilterResult;
AnalyticsRequestStream requestStream = generateRequestEvent(request,
context);
EventDTO eventDto = generateEventFromRequest(requestStream);
eventStream.publish(eventDto);
requestFilterResult = { canProceed: true, statusCode: 200, message:
"Analytics filter processed." };
return requestFilterResult;
}
The function that’s subscribed to the AnalyticsRequestStream
uses the Ballerina Character I/O API to write this data to file. Following is the block of code that does that.
io:ByteChannel channel = io:openFile("api-usage-data.dat", io:APPEND);
io:CharacterChannel charChannel = new(channel, "UTF-8");
try {
io:println("writing to events to a file");
match charChannel.write(getEventData(eventDTO),0) {
int numberOfCharsWritten => {
io:println(" No of characters written : " +
numberOfCharsWritten);
}
error err => {
throw err;
}
}
} finally {
match charChannel.close() {
error sourceCloseError => {
io:println("Error occured while closing the channel: " +
sourceCloseError.message);
}
() => {
io:println("Source channel closed successfully.");
}
}
}
Upon completion of each time window the current file is rotated (renamed) and saved in the file system. A periodic task then uploads these files into the Analytics engine for further processing. We use Ballerina Task Timers to achieve this functionality. Ballerina’s support for multipart file upload allows us to upload files to the Analytics engine.
Generating Deployable Artifacts for Docker and Kubernetes
In many Microservice architectures users would need to generate container runtimes of the API Gateway. Ballerina makes this simple by allowing us to annotate the service with the relevant Docker and Kubernetes annotations. See the Ballerina GitHub repo for an example. As I have mentioned above, we have a tool that generates the Ballerina source files that correspond the APIs defined on the API Manager. If a user of the Gateway requires docker images or the k8s artifacts of the gateway to be generated at the build phase (which we will talk about in the next section), we get a certain set of inputs at the source generation phase and generate the Ballerina source files with the relevant Docker and/or Kubernetes annotations.
Let us take a look at the Docker related annotations in here. You can find more detail about the Kubernetes annotations from the samples in the Ballerina GitHub repo. In order to map the Listening port of the gateway to the same port on the host machine running the docker container, the endpoint needs to be annotated as below.
@docker:Expose{}
endpoint gateway:APIGatewayListener apiListener {
port:9095,
filters:[authnFilter, authzFilter, rateLimitFilter, analyticsFilter, extensionFilter]
};
The service then needs to be annotated with the relevant docker image and docker registry details.
@docker:Config {
registry:"private.docker.gateway.com",
name:"passthrough",
tag:"v1.0"
}
service<http:Service> passthrough bind apiListener {
Once we generate the Ballerina sources with the relevant annotations, our build process would generate the Docker image which we can then run on a container.
Build and Run the Gateway
Now that we have a good understanding of how the source code is generated, the next step is to look at how we compile and run the Gateway. The standard Ballerina build command executed on the generated package would give you an executable artifact which can be run through the Ballerina run command. However, this requires that you have Ballerina installed on whatever the environment you choose to run the Gateway on. Since it is possible that you perform the code generation from a development environment and run the Gateway process elsewhere, we have built a tool that wraps around the Ballerina build process to embed the Ballerina runtime and the executable archive that is generated by the Ballerina compiler into a single .zip unit. This unit is generally less than 50 megabytes in distribution size (depending on the number of services you compile) and it contains a) the Ballerina Runtime (known as the BRE), b) the compiled binary and c) a bash script to start the gateway process. For deploying on standard VMs, this distribution can be decompressed and run by executing the bash script within it.
If the generated source code contains the @Docker or @Kubernetees annotations, the output of the Build is different. In the case of docker, the build command automatically generates the docker image of the Gateway and in the case of kubernetes, it generates the kubernetes artifacts so that these can be used easily within deployment automation environments.
About the Author
Nuwan Dias works as a Director at WSO2, the largest Open Source integration provider. He is a member of the architecture team at WSO2 which spearheads the strategy and design of WSO2 products. Nuwan specializes on APIs and API Management. He spends most of his time working closely with the Engineering team in WSO2 which is responsible for the research and development of the WSO2 API Manager. Nuwan is a member of the Open API Security Group and has spoken at numerous conferences around the world on the topics of APIs, Integration, Security and Microservices.