Deploy a Serverless API on AWS, Azure and GCP

Using Serverless, Terraform and k6 for load testing

Aurélien BETTINI
8 min readMay 30, 2023

Tldr

In this article, I’ve created a serverless Express API and deployed it on AWS, Azure, and GCP to evaluate each platform’s performance. After conducting load tests using K6, I found that Azure and AWS performed comparably well (slightly better for Azure), while GCP lagged due to significant cold starts (up to 5 seconds when speaking to a Database). I concluded that understanding and managing function cold starts is crucial for enhancing API response time. My recommendation is to leverage serverless services’ benefits across all platforms, keeping in mind these performance considerations.

Introduction

While creating an Express API for a personal project, I wondered if it was possible to deploy it on multiple Clouds. Having the flexibility to link my website to whichever platform offers the best service for my project would be advantageous. For example, choosing Azure if I wanted to access Azure OpenAI Service.

Usually, when creating an API, I’m using AWS Lambda and other services like Api Gateway, Dynamodb. But am I missing something on Azure and GCP?

In this article, I will create a simple Express API, deploy it on multiple cloud providers, using Serverless services, and evaluate their performance.

Let’s get started !

The goal

  1. Let’s create a Serverless Express API that handles simple use cases, such as retrieving and pushing data to a database.
  2. Deploy the API to: AWS, Azure, GCP
  3. Test the APIs using K6: Conduct load testing with K6 and analyse the results. We want this API to be used with a website, so latency and performances are key as our users can’t wait 10 seconds for a response.

Why serverless?

With a serverless approach, your resources scale according to demand. You simply write your API functions, and AWS, GCP, or Azure will handle the rest, whether you have a few requests a day or millions. You only pay for what you use, and it typically costs around $0.5 per million requests, which is quite affordable.

Additionally, most cloud providers offer generous free tiers so you can test your API without worrying about costs:

  • AWS Lambda: 1 million free requests/month
  • GCP: 2 million free requests/month
  • Azure: 1 million free requests/month

As long as you don’t receive a significant amount of traffic, you won’t have to worry about costs.

NOTE: If you follow the code examples in this article, you should not incur any charges. However, it’s always a good idea to set up a cost alarm on your cloud account as a precaution.

The API

We are building a website where people can subscribe to our Newsletter.

Example I’ve created using Nextjs and tailwindcss

And we also have a dashboard !

We have 2 endpoints to create:

  • /feed : GET request. User data feed.
  • /newsletter: POST request. The user subscribe to our fabulous newsletter.

For these use cases, we need to store and retrieve user data. To accomplish this, let’s associate our APIs with the NoSQL databases available on each cloud platform, such as AWS Lambda with DynamoDB, Azure Functions with CosmosDB, etc.

Why NoSQL? 💴 Free tier and performance !

Here’s the Express API code

router.get('/feed', (req, res) => {
const feed = [{
id: '81614',
likes: '29',
replies: '11',
views: '2.7k',
author: {
name: 'Aurelien Bettini',
imageUrl:
'<https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80>',
href: '#',
},
date: 'March 20 at 11:43 AM',
datetime: '2023-03-20T11:43:00',
href: '#',
title: 'Pain au Chocolat or Croissant: Which is the better choice?',
body: "<p>The age-old debate between pain au chocolat and croissant has been dividing pastry lovers for generations. Both are delicious French pastries with flaky, buttery layers that melt in your mouth, but the inclusion of chocolate in the pain au chocolat adds an extra element of indulgence.</p> <p>Ultimately, the choice between these two delightful pastries comes down to personal preference, but it's worth considering the following factors when making your decision…</p>" ,
}]
res.send(feed);
});
# This part change depending on the CSP, as we are using a different backend
router.post("/newsletter", async (req, res) => {
const { email } = req.body;
try {
// Query to check if the email already exists
// Get email from Dynamodb/CosmosDB/Firestore
{...}
if (items.length > 0) {
res.status(400).json({ error: "Email already exists" });
} else {
const item = {
id: email,
email: email,
};

// Push to Database
{...}
res.json({ message: "Subscription created successfully" });
}
} catch (error) {
// console.log(error);
res.status(500).json({ error: "Could not create the user" });
}
});

I’m not requesting the backend on the /feed endpoint, just to compare the performances with and without the databases.

API deployment

The code: https://github.com/aubettini/blogs/tree/main/express-api-on-aws-azure-gcp

I won’t go into details here, as you can find the README in the GitHub repository. The main point to note is that each serverless function receives a different payload depending on the cloud platform, which is why I sometimes used different Node.js packages. However, the Express API portion remains the same.

Here are some remarks (keep in mind that I have more experience with AWS):

Deployment on AWS

I used the Serverless Framework to deploy an API Gateway + Lambda containing the Express API. For this, I had to use the serverless-http package. Otherwise, no major issues.

Deployment on GCP

I used the Serverless Framework to deploy a Cloud Function + HTTP endpoint. I faced a minor authorization issue, which I resolved by opening the endpoint publicly and adding the AllUsers permission.

Also, I couldn’t select the Cloud Function version (1st or 2nd) with the Serverless framework, so I’ve just manually cloned my first function to upgrade the version.

Deployment on Azure

I intended to use the Serverless Framework, but I encountered difficulties. Instead, I resorted to using the VSCode Azure Functions App extension to deploy from Vscode. It’s a shame that I couldn’t stick with the Serverless Framework 😔

Database deployment

I used Terraform for this step ✌️

Load testing

To test the load, I used K6. Why K6? because we can create scripts in JavaScript and generate dynamic payloads for the /newsletter endpoint.

Location parameter

All functions and databases are deployed in the same location, and I selected North Virginia for each cloud platform:

GCP: us-east4

AWS: us-east-1

Azure: East US

To performance some latency/load testing, I’ve created a VM on AWS to run K6.

Since all the datacenter are located in Virginia, the latency between them is pretty low (<2ms according to this article).

We can subtract 4ms (round trip) to the results to avoid giving an advantage to AWS 😄.

Function parameters

Usually, the CPU used is defined by the memory allocated.

AWS: 256MB

GCP: 256 MB

Azure: 1.5 Gb Max. You can’t select this parameter: https://learn.microsoft.com/en-us/azure/azure-functions/functions-scale#service-limits.

AWS and GCP are pretty similar in terms of configuration compared to Azure. You can read this great article explaining the differences: https://iamondemand.com/blog/aws-lambda-vs-azure-functions-ten-major-differences/. Note that the cost is looks pretty similar on all the Cloud Privider.

K6 parameters

We are using 10 Virtual User with 200 Iterations. It’s 200 requests in total, spread across 10 VU.

Results (/feed API)

For this endpoint, we don’t request the database just to have a baseline.

Azure is performing pretty well. Let’s have a look at the median.

Hmm… We see a different story here. It appears that GCP has some very slow requests, which drag the mean down. To confirm this, let’s examine those slow requests, which represent the function cold starts.

The maximum cold start for the Azure functions is 800ms seconds, compared to 2.06/2.17 seconds for GCP’s 1st/2nd generation. The difference is huge!

Also, let’s see the results when the functions are WARM, meaning we shouldn’t have a lot of COLD starts:

We can acknowledge that function cold start play a huge part on the overall results. We can confirm this by looking at the concurrency (I was only able to find this information for AWS and GCP).

Left: Number of AWS Lambda instances — Right: GCP function instances (2nd version)

Using the same load test, AWS Lambda has a maximum of 10 concurrent executions, while GCP has 20. This explains the higher average request time for GCP, knowing the slow cold start.

Results (/newsletter API) function + databases

On average, the AWS lambda and Azure performed pretty well (slightly better for Azure) compared to GCP. The difference is actually significant(>100%).

And it seems that the cold starts is again responsible for this.

You can find all the results on this spreadsheet:

https://docs.google.com/spreadsheets/d/1K4ln_7ZPjwCL73OF0_-O21LktDQ2TJTo6ob3pCkwlWE/edit?usp=sharing

Conclusion

On the deployment side, it was quite fun to deploy the same Serverless application on multiple Clouds. You can definitely transfer your knowledges from one to another. GCP and AWS looks really similar and deploying the app on both was pretty fast. Azure, on the other end, has different concepts, and I got stuck for 1 day before having a successful deployment (Learning curve I guess).

Regarding the results, they speak for themself: Be careful with the cold start !

Your “unlucky” users can’t wait 1–6 seconds before getting a result which is just not possible when creating a website.

However, you can mitigate this problem by keeping your functions warm (using Cloudwatch on AWS for example). But that’s for another post!

Don’t hesitate to drop a comment if you have any remarks or tips.

--

--

Aurélien BETTINI

Cloud Engineer @ Hedge fund. Bringing ideas to life is my passion.