Building a full Node.js website in the FaaS world: Serverless Framework + AWS Lambda
A little bit of context
I am a Solution Architect at Biogen and my team (Biogen Healthcare Solutions) has the mission of developing digital services for patients and Healthcare Professionals (HCP). Early 2018 we started to develop a web platform called Neurodiem that intends to deliver scientific content to neurologists.
Beyond building an innovative service, it was important for us to use cutting-edge technologies. As we were creating the platform from scratch, and were already using AWS Lambda for small parts of another project, it was an opportunity to see if we could build a fully serverless platform. It seemed difficult to do it handling our AWS Lambda functions manually so we had to choose a framework to help us structure everything. At the time there were mainly four options: Serverless Framework, AWS SAM, Claudia.js and Apex. We eventually chose Serverless Framework since it was (and still is) the most liable and mature existing framework to build serverless applications.
The purpose of this post is to give our feedback about our usage of this framework on AWS (spoiler alert: we love it!). Before you read it, I would like to express a big thank you to our development team at Kaliop who implemented this architecture and helped us build a great product, and obviously helped me a lot to write this blog post.
What is Serverless?
As defined on Wikipedia, Serverless Computing (commonly called Serverless) is a “cloud-computing execution model in which the cloud provider runs the server, and dynamically manages the allocation of machine resources”. So Serverless doesn’t mean you don’t need servers to run your code, but that you don’t need to manage them anymore.
In the past, several concepts as SaaS (Software as a Service), MBaas (Mobile Backend as a Service) and IaaS (Infrastructure as a Service) have allowed tech teams to reduce significantly their codebase and their “sysadmin” work and replace them by third-party services. Yet, one concept was missing to have a full serverless architecture running: meet FaaS (Function as a Service).
A Function as a Service is a small unit of execution that runs a piece of code on demand. Once the function is triggered, the unit is started, the code is ran and the unit is shut down without you handle anything. This encourages you to split your codebase into small isolated stateless functions that run independently from each other.
Serverless on AWS
De facto, we can only deploy a serverless architecture thanks to one or several Cloud Computing Service Providers that will provide all the mechanisms required to handle the layers of service we want to delegate. The most famous providers of public cloud are Amazon Web Services (AWS), Microsoft Azure and Google Cloud Platform. AWS historically always had one step ahead in terms of variety and maturity of its services, and this is mainly the reason why we chose it over other providers.
What about Serverless Framework?
If you want to run a Lambda Function, and even a full serverless application, you don’t necessarily need to use a framework. You can manually create your lambdas in AWS console and edit the code online, or write your functions locally and deploy them with AWS CLI or AWS CloudFormation. However, Serverless Framework offers a normalized organization, built-in deployment methods and integrations with other AWS services that give a great comfort while you are developing your application. As it’s a cloud agnostic framework, it’s also true if you use Microsoft Azure Functions or Google Cloud Functions.
Quick overview of our architecture
To have a better sense of the context, here is an overview of our architecture. You can see how the lambdas fit into the whole architecture, being replicated on two availability zones and passing by API Gateway to be exposed on the internet (actually they are rather exposed to fastly which is our single entry point).
Our feedback after several months
As Serverless completely changes the paradigm compared to a monolithic architecture or even a Docker/Kubernetes micro-services architecture, it was quite a challenge to build an entire web platform with this new approach. During its construction, we identified good and bad sides both for development and run phases.
Let’s start with the run phase because it’s mainly why we wanted to use a serverless architecture in the first place.
It appeared clearly that handling the run phase with Serverless on AWS is much easier than with “more standard” architectures and infrastructures. As pretty much everything is managed by AWS itself, it reduces significantly our operation tasks and therefore it decreases a lot potential infrastructure issues and instabilities. Even if you can still develop and deploy a buggy application with Serverless, the fact that the functions are stateless forces us to have a clean conception and avoids common pitfalls. In addition, the big strength of AWS is its capability to scale your infrastructure on-demand very efficiently and safely. The only variable is the cost.
Cold starts and response time issues…
The major issue we have with our lambdas is the time they take to warm up. I explained above a Lambda is a unit of execution that switches on, runs the code and switches off. Actually it’s not completely true. If your lambda is triggered for the first time in a while, it’s started and then it stays on so it can execute itself right away if it’s triggered again. If it was not called for 15 minutes then it shuts down. The issue we see here is that if you don’t call a lambda very often, your lambda will take an extra time (~5 seconds) in order to warmup before running your code. This is what we call a “cold start” and it can have a major impact on your performance! Especially when you are building a platform that doesn’t have a very large target with a big traffic expected. The workaround we chose to do for the lambdas that we need to be the fastest as possible is a pre-warmup we frequently and automatically do so they are always ready to run their code. For more information about cold starts, I suggest you to read this very good article “Dealing with cold starts in AWS Lambda”. There’s also this useful article on how to warmup your lambdas with a Serverless Framework plugin.
…especially if you play with VPCs
As our users data are replicated in our managed Elasticsearch instance, we wanted it to be in a VPC (Virtual Private Cloud) so it wouldn’t be exposed publicly on the internet. The problem is when a lambda switches on and is configured to have access to Elasticsearch, you face again an extra-time so AWS can set up an ENI (Elastic Network Interface). This is mandatory for the lambda to communicate with your Elasticsearch instance (or whatever resource you have in your VPC). This issue penalized us a lot because it made the cold starts even longer. To know more about this, you may read this very complete article “How to manage Lambda VPC cold starts and deal with that killer latency”.
The deployment with Serverless Framework on AWS is very fast. The micro-services approach of Serverless allows us, compared to a monolithic application, to deploy only what we need, only what has been changed. However, you need to pay attention to the fact that if you have a mono-repository and a mono-stack (everything bundled and configured in one serverless.yml file) like we do, the more you have functions the more an unitary deployment (of one lambda) takes time. This organizational approach is very convenient while you are developing because each function has access to all the codebase. But it sort of breaks the concept of micro-services because it bundles all the code in each lambda, which is not optimal and slows down the deployment. I will come back in detail to this organizational issue in the last part of this article.
For that AWS Lambda is great because each lambda has automatically its logs landing on AWS Cloudwatch. It makes the monitoring much more easier with zero effort to set it up. The tool itself is pretty powerful because you can quickly search in all your logs and you can set up alarms on various metrics such as errors, invocations, duration, etc. Personally we rather use Site24x7 that gathers all our monitoring across several projects and it’s plugged to our AWS account so we can set up alarms the same way. Here is our Site24x7 Availability Summary Report for all our AWS resources on the last 30 days:
Our overall feeling about using Serverless Framework during our developments is very positive, despite the fact there are some pain points to keep in mind.
Ease of use
First of all, Serverless Framework is very easy to use on a local machine. It is very quick to install a full working stack on a new development machine. It takes about 5 minutes without the external dependencies and if you already have Docker installed, and about 20 minutes if we count everything (installation of Docker, set up of all credentials for all third-party services, etc.). As in addition it is very stable from one machine to another, we didn’t lose much time on installation issues and we could onboard new developers very easily.
Offline vs Online
When you are developing on Serverless Framework, you have to keep in mind that its usage on a local development machine is not exactly the same than online on AWS. It sometimes need small adjustments to make it work the same way. Also, although the tools simulating API Gateway and Lambda work like a charm, we were not totally satisfied by the other ones so for a part of them we had to use real online services on a AWS sandbox. It’s not a big constraint but it means we didn’t manage to obtain a development stack working completely offline.
Serverless Framework vs AWS SAM
When we started our developments, we asked ourselves if we were going to use Serverless Framework or AWS Serverless Application Model (AWS SAM), both offering the possibility to build and deploy serverless applications on AWS. The big advantage of AWS SAM is obviously a better integration of Amazon services, where Serverless Framework has well integrated some services (ex: S3) but much less other ones (ex: Cognito).
On another hand, Serverless Framework has two major positive points:
- it’s a little bit older and at the time we started to build our product it was more mature and more used than AWS SAM. For instance in 2018 npm serverless package counts 3M+ downloads in 2018 whereas aws-sam-local package (the package to run AWS SAM locally) has 160K downloads.
- it’s agnostic, meaning if tomorrow we decide to use other providers’ services (ex: Microsoft Azure, Google Cloud Platform, etc.) partially or exclusively, we still can with this framework. It’s very useful if one day we want to fully migrate to another provider, or more likely if we want to have a hybrid architecture using services of several providers.
Impacts on technical choices
As after all a serverless architecture is a micro-services architecture, it inherits of its big advantages. One of them is that we can totally write our Lambda functions in different languages, using the most adapted one for what we want to achieve. This has two positive impacts on our technical choices:
- we are not bound for life with a language and we will be able to migrate more easily our code function by function, where a monolithic application only allows you a one-shot migration. It’s also a good way to test new languages on a small part of the codebase and keep the tech team up to date. This is what the big tech companies often do.
- we are not constrained by limitations of one language in terms of offered possibilities, tooling, packages and community.
Pay attention to the size of your packages
A little thorn we had in our side is that a Lambda function has a limited size so we had to be careful with the packages we use. Indeed, we wanted to use Puppeteer to generated PDF but it made the lambda too big (> 50MB once compressed) so we couldn’t deploy it. Therefore we decided to use wkhtmltopdf instead.
Impacts on organizational choices
You can organize your serverless project in many ways. They all have pros and cons and the best choice will depend on the typology or your project. The only big constraint to keep in mind is that you can’t have more than 200 resources on one AWS CloudFormation stack. As one Lambda requires several resources to run, you can reach this limit very (very) quickly.
Mono-repo / Mono-stack
This is what we have chosen. You have everything in one repository and you have only one serverless configuration.
- very easy when you start, just like a monolithic application
- very convenient for development because each function has access to the whole codebase
- as I said you break the micro-services concept. You still can have your application cut into functional pieces but it’s purely virtual because at the end everything is bundled together
- your deployment time (and even response time) increases with your codebase growing
- you reach very quickly the 200 resources limit we talked about
Mono-repo / Multi-stack
It’s almost the same thing but instead of having one configuration file, you split your repository in several folders which all have a configuration file and will have a dedicated CloudFormation stack. Depending on what granularity you apply to this cutting, it can be seen as a macro or micro services architecture.
- you’re forced to cut your project into functional pieces, and this time it’s a real cutting even for deployment
- you’re not bothered with the CloudFormation limit
- you keep your response and deployment time constant
- a little more tedious to maintain all the stacks
- your functional pieces can still share code together but it requires a very clean organization with a central library or external dependencies
Instead of having everything in one repository, you cut your project in several repositories, each repository having one or several stacks.
- same pros than Mono-Repo / Multi-stack but in this case it really separates your functional pieces and you can be sure they are all independent from each other
- much more tedious to work with and maintain
- you have to handle the compatibility and retro-compatibility between the different services since they don’t have the same versioning
Our choice, and our plan
We started to reconsider our choice of a Mono-repo/Mono-stack organization when we reached the 200 resources limit of CloudFormation. Eventually we stuck to it because we found a workaround using Serverless Framework plugin that automatically splits your stack into nested stacks. As each nested stack has a 200 resources limit and you can have up to 200 stacks, you are in peace for a project of reasonable size. Yet if your project is growing, we don’t recommend to use the same workaround because it brought other issues. Indeed with nested stacks we reached API limits so we used Serverless DependsOn Plugin in addition, and now we are currently facing circular dependencies issues due to this plugin.
So basically we already know we will have to move very soon to one of the two other organizations. The too multiple interdependencies, the too many workarounds to keep a mono-stack and the fact that functions size, response time and deployment time are all increasing make this choice clearly not sustainable. We will probably adopt the Mono-repo/Multi-stack one which seems to combine the advantages of the two others and which will be probably enough for the size of our project. For more information about this topic, I suggest you read these interesting articles: Organizing Serverless Projects and Lessons Learned Building a Large Serverless Project on AWS.
This architecture is pretty profitable because everything is on-demand and we don’t keep a resource longer than we need. In this article AWS Lambda Pricing in Context — A Comparison to EC2, the author explains that for a low-traffic platform Lambda is 3 to 10 times less expensive than the smallest and less expensive EC2 instances. And for a high-traffic platform the EC2 instances would need to receive constantly a large amount of requests to make them more profitable than the lambdas, without mentioning the fact that an EC2 requires much more maintenance tasks (which can increase the overall cost a lot!) and doesn’t come automatically with clustering, autoscaling and load balancing.
For now we are not regretting our choice of using Serverless Framework on AWS. Even if it was quite a challenge to build a product with a serverless architecture — especially for a web platform since the majority of the use cases we had seen were with APIs and/or pure backend processing — it turns that it was a judicious choice because so far we are not struggling at all with production management and monitoring, and there was no major blocking point during our developments. There’s clearly a learning curve to completely understand how everything work together with this kind of architecture, but passed that it’s very pleasant for the developers. We already know that with our growing codebase we will must refactor and rearrange things, especially to have faster deployments and less dependencies, but it’s the everyday life of every IT project, isn’t it?
Thank you for reading, don’t hesitate to comment or contact me if you have any remark, question or suggestion. I will be pleased to have your point of view and exchange with you.