Looking for Senior AWS Serverless Architects & Engineers?
Let's TalkThis article is part of a series:
- Part One — review your existing application (Wed. 09/18)
- Part Two — break apart your application (Fri. 09/20)
- Part Three — lift-and-shift (Wed. 09/25)
- Part Four — choose a deployment framework (Fri. 09/27)
- Part Five — build a strong foundation (Fri. 10/11)
In this series, we take a look at what areas you need to think about when making a “serverless migration”. First, we will touch on what you should review about your existing application, move into breaking your application apart, choose a cloud provider, choose a deployment framework, and how to build a strong foundation.
In the last article, we reviewed our existing application. Now let’s look at how to break apart our application.
Break apart your application
Once you’ve identified the areas of your application which need to change. We need to take a look at how our application is architected.
If you’re already using a micro-service architecture than things become a lot easier because you already had to move away from a monolithic architecture. However, if you’re not working with micro-services then we need to start from the top.
First, we need to look at how to effectively break apart your application code and because I’m writing this around lunchtime, we are going to use pizza 🍕 as our running analogy. Our goal is to go from one large pizza with no slices to a large pizza with multiple slices.
Each slice of pizza will represent one cloud function. Each pizza will represent one logical grouping of functionality.
The CRUD example
For example, let’s imagine that your application handles CRUD (Create, Read, Update, Delete) functionality for /users.
- Create a user — POST /users
- Get a user — GET /users/:id
- Get all users — GET /users
- Update a user — PUT /users/:id
- Delete a user — DELETE /users/:id
What is our logical grouping?
The logical grouping would be the entire /users functionality. We now have identified our pizza 🍕.
How many cloud functions are in the grouping?
Each CRUD endpoint will route to a different cloud function which handles that specific piece of functionality. Resulting in five slices of highly modular pizza 🍕. Yum?
Five separate cloud functions, is that a good idea?
When you break things apart to that degree the organizational overhead increases dramatically. You also are more susceptible to “cold starts” which happen when your cloud function first receives a request and hasn’t had any requests for an extended period of time (e.g. 3–10 minutes, it varies).
Since you will have five cloud functions, each time you receive that first request you will have to deal with another “cold start” which will result in a slower response.
If you’re making an enterprise shift to serverless from a monolithic architecture then you’re going to want to be more hesitant when breaking things apart to this degree. As you will quickly go from one server hosting your entire application to hundreds of cloud functions, depending on the size and level of functionality.
When would I suggest this approach?
I would only suggest this approach if you were looking to optimize your existing serverless environment and already had a lot of experience in-house to support this. As each extra cloud function, requires it’s own permissions and you can imagine how bloated your automation can quickly become.
If your business is looking for a high degree of security, then having granular permissions on each individual piece of functionality (e.g. create user) is a possibility when you break things apart as described above.
What’s an alternative, how should I handle this?
If you’re looking for an alternative to make the migration smoother with less overhead and less “cold start” issues. You can think about the logical groupings differently. For example, let’s imagine the entire application as the pizza 🍕 and make each slice of pizza 🍕 a subset of functionality (e.g. /users CRUD).
- The pizza — Entire application
- Slice of pizza — Logical grouping of similar functionality (e.g. /users CRUD)
Now if we take the example above and define it under our new classification we are left with a single cloud function.
- Only need to worry about “cold start” on a single cloud function
- Only need to manage a single cloud function
- Less bloated automation
- Easier to understand
If we then add more functionality in the future like some new API endpoints for /schedules that also has full CRUD functionality similar to /users. We are going to have two cloud functions.
Leaving us with a nicely broken apart and optimized micro-service architecture running as cloud functions. This level of modularity will keep us focused on the migration and avoid the worst outcome, pre-optimization.
The 30+ minute process example
Let’s imagine that you have an API which runs twice a day and updates your entire database with new data from a third-party source. Here are the steps:
- API is invoked by a crontab
- API sends a request to the backend server
- Backend server makes a request to the third party API continuously until all data is received
- Backend server writes to your own database until all data is written
We can do better than this and one potential way would be through leveraging SQS to operate as our “worker”. For simplicity, let’s imagine that the third-party API can give us back the total number of records in less than a second. Let’s also imagine that step #3 has parameters called “start” and “end” which we can use like this, ?start=0&end=100000. Every 100,000 records will take 1 minute to process.
Let’s update the list.
Part one:
- A server invokes the API with a crontab
- API sends a request to the backend server handling our API code
- Backend server makes a request to the third-party API for the total number of records
- Backend server splits the record total into 100,000 chunks and creates a URL, https://third-party-api.com/api?start=0&end=100000
- Backend server sends the URL to SQS as a message
Part two:
- SQS invokes a cloud function one-by-one for each message that hits the queue
- Cloud function unpacks the message containing the URL to make the third-party API request
- Cloud function makes the third-party API request and receives the chunk of data
- Cloud function writes the data to the database
Let’s optimize even further. In part one, we were still using a “crontab” and a backend server. Now that we have broken this apart, we can make part one also run off a cloud function and we can do so using another cloud function event. The event is called an AWS CloudWatch Rule, this will give us the ability to replicate the crontab but instead invoke our cloud function and make our entire setup “serverless”.
Part one (updated):
- CloudWatch Rule invokes the cloud function
- Cloud function makes a request to the third-party API for the total number of records
- Cloud function splits the record total into 100,000 chunks and creates a URL, https://third-party-api.com/api?start=0&end=100000
- Cloud function sends the URL to SQS as a message
Now as you can see we’ve eliminated potentially two servers. The first was running our crontabs and the second was hosting our API. Now we have cloud functions with per-second-billing that can scale horizontally with a queue which invokes a worker cloud function based on the number of records to process and store! Now we’re cooking 🥘
We also simulated the crontab using a fully managed service by AWS meaning less overhead and less raw cost.
Review
In this article, we covered two use-cases, one where you’re building an API and trying to determine how best to break apart your API to mesh with cloud functions. The second, covering how to take a not-fit-for-serverless long-running process and make it work anyways.
In the next article, we are going to talk about choosing a deployment framework because once you know how your architecture will look, you need to automate it.