Developing a simple API gateway in PHP and Lumen
The term “microservices” is on everyone’s lips today — suddenly they have become very trendy, and many companies announced transition to this architectural pattern without due research. Anyway, we will leave discussion of the pattern usefulness beyond this article.
Traditionally, there is an extra layer on top of microservice collection — a so-called API gateway that solves several problems (they will be listed later). At the time of this post, not many open source implementations exist so we decided to release our own in PHP with Lumen micro-framework (core of Laravel).
In this article we will prove how simple of a task it is for modern PHP!
What’s API gateway?
Speaking very briefly, API gateway is a smart proxy server between users and any number of API services, hence the name.
The need for this layer appears as soon as you start deploying microservices:
- A single endpoint address (URL) is much easier to remember and configure than many (Netflix has more than 600) individual API addresses;
- It makes sense to verify user credentials (usually a token) at the top level, once;
- Rate limiting makes sense at this level;
- The whole system becomes more flexible — you can change the internal structure on daily basis if you wish so. Supporting older API versions and schemas becomes trivial;
- You can cache or mutate responses;
- You can combine responses from different services for the sake of user’s convenience (or your front end developers).
There are more advantages of course — this is just a tip of the iceberg.
There is a nice free e-book from Nginx discussing microservices and API gateway — we advise you to read it if you are interested in this pattern.
As mentioned early, there aren’t many open source options, and those that exist lack some of the important features.
Why PHP and Lumen?
PHP 7 is a performant language, and frameworks such as Laravel and Symfony proven to the world that PHP can be both beautiful (expressive) and functional. Lumen, being a lightweight version of Laravel, is an ideal choice because we don’t need stuff like sessions, templates and other features of full stack applications.
Also, we just happen to have more experience with PHP and Lumen. But since we expect majority of future users to use our Docker image, language is not even important . It’s just a service layer that performs a simple role!
We propose the following architecture and terminology, to be used throughout the code to avoid any confusion:
The application is to be called Vrata (“Врата” literally means “gateway” in Russian, our CTO’s native language).
Right below the API gateway layer you place N microservices — backend APIs that respond to web-requests. Each service may consist of any number of instances, and our API gateway will pick a specific instance through the so-called service registry.
Further, each service hosts a certain amount of resources (in REST language), and then each resource can expose a number of possible actions. Rather simple and logical structure for any experienced REST programmer, eh?
We haven’t even started coding but we can already name a few requirements for our forthcoming product:
- Gateway must scale well horizontally, because we live in 2016 and things just scale horizontally in 2016. Therefore — no application state por favor;
- Gateway must be capable of combining queries and making asynchronous requests to microservices;
- Gateway must implementing rate limiting;
- Gateway must implement authentication. Traditionally, it is recommended that API gateways perform authentication, while underlying microservices are responsible for authorisation for their own resources;
- Gateway must be able to automatically import exposed resources from microservices. To start with, we picked Swagger as the most common API description format today;
- Gateway should be able to modify (mutate) microservice responses;
- And finally: gateway must run well directly from a Docker image, configured solely using environment variables. We do not want any additional repositories or deploy scripts or whatever!
We have to admit that most of the features are already up and running, and implementation process was a breeze. Like some people say, we live in very exciting times for software developers! It’s never been this easy to implement and deploy software.
Not much work on this front, we just installed Laravel Passport (took some magic to make it work with Lumen) and out of the box got the full set of all OAuth2 features including JWT. You can see my little Lumen Passport integration package on GitHub.
Routes and controller
All microservice routes are imported and saved in JSON format. Service provider takes care of mounting these routing during boot process:
We resolve a singleton instance of our route database (route registry) and pass application container (Lumen itself) to it so that it can add routes. Meanwhile in route registry class:
Very simple — we parse the routes one by one and we pass them to the container (Lumen application), everything is tied to the same controller. We also add OAuth2 token verification middleware and our helper middleware.
Now every exposed microservice route has a corresponding route on our API gateway. Moreover, aggregated (synthetic) routes are added as well, and everything goes to the same controller. Here is how that controller handles all GET requests:
We collect responses from microservices and then glue them together using array reducer. Moreover, we collect parameters and inject them when necessary (eg. first microservice may provide a parameter in response that will be used later with another microservice).
Guzzle was picked as a web client because it does async requests pretty well, plus it got some integration testing features shipped in the box.
There is already support for complex, aggregate queries — queries that correspond to multiple microservice requests. Eg. “give me that, and also that and a little bit of that”.
An example of an aggregated route configuration:
As you can see, not only aggregate routes are already available — they offer a decent set of features. You can mark underlying requests as critical or not, you can launch requests in parallel, you can use response of one microservice in the request to another microservice. Performance is nice too— mere 56 milliseconds for this test route (Lumen bootstrap time is around 20 ms, the rest is background requests launched in parallel).
This is the weakest part yet, with only one basic type of instance resolution available: DNS. Despite its apparent primitivity, it works just fine in AWS and Docker Cloud environment, where your cloud provider monitors your nodes’ health for you and provides you with a dynamic DNS record.
So currently Vrata just takes hostname of the service without asking questions — where there is a whole bunch of instances behind it or a single physical computer. But we will soon add support for the most popular service registry today — Consul.
The mission of a service registry is simple — maintain a table of functioning microservice instances and disclose this information whenever asked to. Both AWS and Docker Cloud are able to do this for you, providing you with a “magic» hostname that just always works.
When one talks about microservices, not mentioning Docker, one of the breakthrough technologies the last couple of years, is simply unforgivable. Microservices are often built and deployed as Docker images, it has become a standard practice, and so we quickly prepared a public image on Docker Hub.
A single command launched in the terminal of any OS X, Windows or Linux machine, and you get a working instance of Vrata:
$ Docker run -d -e GATEWAY_SERVICES=… -e GATEWAY_GLOBAL=… -e GATEWAY_ROUTES=… pwred/vrata
The entire configuration is passed via environment variables in JSON format. If you use Docker Cloud, you can deploy Vrata in a couple of clicks.
This API gateway is already deployed and being used at PoweredLocal. The whole code is open-source (MIT license) and available in our GitHub repo. Any contributions are wholeheartedly welcome.
Since aggregate queries really, really resemble GraphQL queries in structure, among the shortcoming features will be support for GraphQL queries.