Micro service architecture using AWS
I’ve recently discovered the concept of « micro-services » architecture, but it is hard to find real life examples. This article gives some useful ideas, and I wanted to explore some ideas to expand it further.
What is a micro service?
I’d like to emphasize first that I’m pretty new to this concept and some of my assumptions may be completely wrong. Do not hesitate to leave some comments.
In a typical ZF2 application, you will have different modules. For instance, a User module, a Payment module, an Order module… This is already a good step to modularity. Those modules should be decoupled at the maximum. For instance, if an order is created, you will likely trigger an event « order.created ». Various listeners will have custom logic. For instance, the payment module may have a listener to create a payment using an API call to Stripe, the User module may have a listener that will reduce the customer’s balance, and so on…
Performance problems aside, this has some interesting issues.
The main one is that you will likely need to deploy the whole code each time you make a change. And because of the monolithic aspect of the application, changes applied to one module may actually affect another module. For instance, you could have a listener in the Payment module where a new listener stop propagation of the event. Without knowing, a change to one module have introduced a bug in another module that should be completely independent.
The solution to this problem is the micro-service approach.
In this approach, your main application block (like User module, Payment module, Order module…) are actually separate applications, developed, tested and deployed separately. Because they are completely independent, you are pretty sure that one change does not impact the other.
The main question is: how those services communicate?
There are two possible cases:
- When an operation must be done synchronously: for operations that need to be done at the same time, you would communicate through your own API. For instance, if creating an order must immediately create a payment, your order will make an API call that will be dispatched to the payment micro service.
- When an operation can be done asynchronously: actually, most operations can be done asynchronously. For instance, sending a confirmation email for the order, reducing the user balance… can be done asynchronously. To do that, the best way is to use events, that will be enqueued into a queue and handled asynchronously by a worker.
Implementation’s idea using AWS
Note: I have not implemented such a system in production.
People that know me know that I’m not a server person. I try to delegate as much of those tasks as possible. The good news is that I’m a big AWS user, and AWS offers many services that would allow us to create a nice micro-service approach. My idea is to use the following services:
- SNS and SQS : SNS is a notification service, while SQS is a queue system
- CloudFront : this is the Amazon’s CDN, and will be used to provide dispatching.
- Elastic Beanstalk: this is a service to easily deploy application, and automatically setting the auto-scaling group, the load balancer… it also provides a worker tier to automatically pull messages from a SQS queue.
Ok, the first step is heavily inspired by this article from Your Karma. Here is a view coming from their article that show how their architecture look like:
I want to give more details about this.
Actually, SNS is an Amazon service that allows to dispatch notifications to one or many subscribers. A subscriber may be an email, a phone number (to send an email) or… a SQS queue!
For instance, let’s say that we create a topic (you can imagine a SNS topic as an event), called « order.created ». We will then create one queue per micro-service (this is highly important, because SQS does not allow multiple consumers… this also allows to scale each service independently). Therefore, we will have one queue for the User micro-service, one queue for the Payment micro-service… and so on!
Then, we will subscribe each queue that is interested for the SNS event. In our case, we could subscribe the « mailer » queue (as we want to send an email as a response to this event), and the user queue (as we want to reduce the customer’s balance in response to this event).
You could automate this to automatically create queues, SNS topics… (this is what YourKarma seems to do).
This is the workflow in your code:
- An « order.created » event is triggered in your « Order » micro service.
- A message is created in this micro service, and sent to the SNS topic called « order.created ».
- SNS will automatically add a message to each SQS queue that is subscribed to this topic.
- Each queue will be handled independently by a worker.
This is really really nice, because you do not have to handle all of this yourself. Those two services are infinitely scalable, highly resilient and cheap.
The next step is to deploy this mess.
I’m a big fan of Elastic Beanstalk. This is a service from Amazon where you can easily deploy your code. You can even use Docker if you want.
What this service does is automatically creating the auto-scaling groups, the load balancer. It also adds various CloudWatch metrics to automatically monitor your application. Beanstalk takes care for yourself of removing unhealthy instances, adding new ones…
It also offers a worker tier. This works similarly, but the difference is that you attach a SQS queue to such an environment. Then, Elastic Beanstalk automatically pulls the messages for you from SQS, and send them to your application.
The idea is to actually have two environments per micro-service: one environment that act as a web-server (receiving API requests…), and another one that only handle SQS messages. This means that you can scale services independently. Some micro-services may have very intensive worker tasks (in which case you could pay for more powerful instances), while some other micro services may be web server intensive.
Obviously, this comes at a cost: a typical application would include a web server environment and one worker environment.
Using this approach, if we have 6 micro-services, this means 12 environments. Each web-facing environment, you’ll need to have one load balancer. And Amazon charges you for one load balancer.
Of course, this means that as the load is shared across multiple servers, you can have smaller servers on each micro-services, but still, this will highly increase the cost.
For instance, using only two servers (assuming m3.medium servers) - one worker and one web-server -, as well as one load balancer:
- Load balancer: 18$ / month
- Two m3.medium servers (assuming 1-year reservation) : 36$ * 2 = 72$
- Total cost: 90$ / month
Using a micro-service approach with 6 micro-services, it would means six worker servers, six web-servers servers and six load balancers:
- Load balancers: 18 * 6 = 108$
- 12 m3.medium servers (assuming 1-year reservation): 36 * 12 = 432$
- Total cost: 540$
Obviously, as we have less load, we could maybe use t2.medium instead of m3.medium, hence reducing the cost.
You could also argue that you could host all the micro-services in the same server. First, I’m not sure how to do that in Elastic Beanstalk (maybe the new Docker multi containers?), and it also reduces the advantage of being able to be scaled separately.
One question that arise in my head when I first read about this approach is: how the hell can I route this. The thing is that « api.mysite.com/users » must be dispatched to a completely different code than « api.mysite.com/orders ».
My initial idea was to use rewriting using Apache or nginx, and handle this at the server level.
But then I realized that if we have multiple load balancers, we could take advantage of another Amazon service: CloudFront.
CloudFront is a CDN, and since 2015, it offers all the tools you need to put CloudFront in front of your API, hence replacing in some cases something like Varnish.
As CloudFront is a CDN, this means that you can take advantage of it as a cache layer. CloudFront will also perform a lot of network optimization that, in many cases, speed up your API, even though you introduce a new layer in your architecture.
The nice thing is that CloudFront offers a feature called as « origin ». You can create as many origin as you want per CloudFront distribution. Then, you can create behaviour. A behaviour map a URL to a origin.
In our example:
- We would create one origin per load balancer. In other words, one origin per micro-service.
- Then, you will create multiple behaviour that map to one origin. In other words, you’ll define your routing in the behaviour. For instance, you could create a behaviour that will map « /users/ » to the user origin, and map « /users//orders » to the order origin, and so on for each of your routes.
Of course, that could also be automated to automatically create origin and behaviors.
Without this approach, you would likely need to create a « master API », that will act as an additional micro-service where all requests would be routed, and hence dispatched to other micro-services. By delegating this to CloudFront, we are removing one additional piece of software that we need to maintain, scale…
This approach seems to be really nice and scalable, however there are some things that are still highly unclear to me.
Performance: the fact that we have much smaller applications means better performance. In the context of a ZF2 application, this means: less enabled modules, less listeners, and ultimately better performance. However, the disadvantage is that now, micro services have to communicate through a REST API. Although servers are likely to be very near, the overhead introduced by one or multiple HTTP calls may degrade quite a lot performance.
Transactions: when using a monolithic application, it is easy to wrap a query around a transaction. If, at any stage of the process, something fails, it is quite easy to rollback the whole transaction. With the micro service approach, a first API call to another micro service may succeed, and the second one fail. How can this be handled elegantly?
Increase cost: for me, the main problem of this approach is the increase cost. The approach is appealing, but the cost make it nearly impossible for small companies. A typical application may have between 3 to 6 micro services. I’m still looking for a solution where I could deploy multiple applications to Elastic Beanstalk. But this will introduce a new problem: I won’t be able to use the CloudFront approach for routing.
Duplicated code: another big question for me is duplicated code. I’m a big fan of Doctrine 2. However, using such approach, this means that the « Order » micro-service won’t have access to any entities defined in the « User » micro-service. Having said that, how could we cleanly define associations in entities (as an order is likely linked to a user)? One idea I had is that, for smaller projects, I could create a shared module, that only defines entities (this is important, no services nor controller should be defined in this module). Obviously, this remove one of the main advantage of micro-service architecture, that allows each team to work completely independently, but for smaller project, this seems like a sane trade-off.
Ultimately, the question is: is it worthwhile? I love some the ideas, like using SNS and SQS to handle events, and being able to have smaller code base that is easier to maintain, with less potential side-effects. But this definitely seems harder to develop, especially when you’re alone, and I’m not sure this is worthwhile for small to medium projects.
Do not hesitate to post some ideas that could be useful!