This post is part of a series
- Serverless Patterns and Best Practices for AWS - Part 1 Design (this)
- Serverless Patterns and Best Practices for AWS - Part 2 Build (stay tuned)
- Serverless Patterns and Best Practices for AWS - Part 2 Deploy and monitor (stay tuned)
One of the more recent developments in cloud computing and products is that of Serverless functions. This article is the first of a three-part series which will discuss some of the best practices and considerations when building Serverless applications, in particular, Amazon Web Service's (AWS) function-as-a-service, Lambda. The series is a collection of design, development and deployment considerations that have been used on a number of our deployments here in Platform Engineering. It is not meant to detail each aspect of building and deploying Serverless applications, however it will highlight some of the most important design questions which need answering before delving into that journey.
This post’s main goal is to guide future Serverless designs and implementations, and also encourage community feedback through comments. We have based the parts of this post on different phases of the project lifecycle, design, build, deploy and monitor. This first post will focus on the design, while the second post will explore the build/deploy/monitor phases.
Questions Need Answers
Before starting on any design, there are a number of questions which need to be answered. Below we highlight some of the key items you will need to address in order to produce a successful design and implementation.
What are my limits?
It is important to understand what your limits are before you even start designing. Knowing allowed function memory allocation, function timeouts and the number of concurrent executions accepted are important considerations when designing your serverless solution. These limits are constantly changing, so for AWS Lambda you should consult the latest AWS documentation.
What should your function do?
Ideally, functions should only focus on executing specific, intended logic; everything else should be delegated to other services and leverage frameworks. For example, you can use the AWS API gateway to expose your function as a REST API while leveraging the input validation feature. This ensures your API gateway performs input validation through JSON schemas, freeing up logic from unneeded input validation operations.
To VPC or not to VPC?
Where should you deploy your function? Functions can be deployed inside a Virtual Private Cloud (VPC) or outside, however for the sake of security consciousness you should deploy your functions inside a VPC. This is a must when you need to access private resources within your VPC and will, most certainly, put your InfoSec team at ease. We'd like to mention, though, that AWS does do a good job of securing your environment even without the use of VPCs.
What are the implications of running your functions within a VPC? The main drawback is slower function start-times as an Elastic Network Interface (ENI) will need to be created and assigned to that instance. This will also consume one of the IP addresses in your VPC CIDR range, which might be a problem when your Lambda function scales.
What is my function’s purpose?
As touched on earlier, your function should always serve a single purpose, and it should be simple, quick and easy to update. With that said, how small should your function be? Having a more granular function may cause performance issues as you increase the number of cold starts, as well as increasing the chance of hitting your account’s concurrent Lambda execution limit. Consequently, rules will need to be set in regards to what each function should handle.
A good example of balancing granular services with flexible, reusable logic is supporting HTTP resources. A single function can be built for each resource (for instance, /sales) which accepts multiple HTTP actions. Your handler method would receive various actions (GET, POST, DELETE, etc) and route them to the correct private method. In turn, this makes unit testing easier, and decreases cold-start times. General-function services—such as renaming a file or triggering other services—are also good contenders for Serverless functions.
What are my function privileges?
Lambda functions can do many things and access many services, which is why it is important to limit their access. Using Function Granularity, the security mechanisms around your function should adopt the principle of least privilege. Similarly, ensure that any IAM roles and/or policies assigned to the function follow the sample principle
Should I use Lambda layers?
Lambda layers (a ZIP archive containing libraries, custom runtimes or additional dependencies) can be used to pull in additional code to your function. This allows reuse of shared logic and components without having to include the shared component in each function's base code, promoting reusability and ease of code maintenance.
While layers can provide great benefit, there are caveats. Here are few important things to consider:
- The layer size is counted against the total Lambda function size
- Only a maximum of five layers can be added to one function
- When a layer is updated, any function which references the layer must update the reference to the layer’s version and be redeployed
When a layer (or its version) is deleted, functions which depend on it will not be able to be updated until the reference to the missing layer/version is resolved.
Should my function talk to other functions?
In the event your function will be invoked by multiple consumers, it is preferable to expose it through an API Gateway. This will leverage the Gateway’s capabilities in addition to enforcing policies, that is the recommended way to expose your functions.
You should consider the latency that will be added to the function through multiple function hops (in addition to any cold-start issues). One solution is to create a library or a layer that can be included in the functions that need that feature. This way you create a reusable asset which avoids network latency. A key consideration when building reusable assets is future use cases, which will make it easier and more beneficial to expose as an API later on.
It is possible, however, to create a cross-dependency between two functions in some use cases when the second function provides one of the following benefits:
- An anti-corruption layer between your services and an external system
- Cross-cutting requirements, such as logging and authorisation
- Providing common functionality, e.g. validation
How do you manage datastore connections?
One of the key attractions of Serverless functions is scalability. By nature, multiple instances of a function can be near-linearly scaled. One limit on linear scaling is connection to datastores.
Managing connections can be a time/resource-consuming task, and you'll have to be creative in your code to efficiently handle the connections. This involves thinking about connection pooling, data caching and other techniques, especially when you need to scale. If connections to data sources are required, consider using a data source management module rather than direct connections to backend databases, for example.
What concerns surround code dependencies?
Like many other programming paradigms, limiting the use of third-party libraries provides a more secure and leaner function. It reduces risk introduced through third-party code and libraries, and also produces a smaller codebase footprint. Of course, this needs to be weighed against requirements, but it is an important item to consider during design.
How should my function be invoked?
As AWS Lambda is event driven, care needs to be taken when designing functions’ purposes and how they are invoked. There is an array of options to choose from, such as message queues, notifications, inbound HTTP listeners, and ALB invocations. Depending on the use case, the decision may be obvious, although it’s important to consider your options. For example, should you simply use an API Gateway or ALB? Will the function be invoked synchronously or asynchronously? In addition, AWS now also provides an eventbridge for many different event sources.
Serverless functions provide great benefit in modern Cloud computing, offering granular, independent and lightweight functionality. Like all technologies, however, they require considerations to be taken when designing them in order to avoid pitfalls and harness strengths. In this article we tried to cover some questions that need to be asked before you start designing Serverless apps. From function purpose to shared dependencies, data source bottlenecks to function invocation, careful thought is necessary to leverage the true power of this new technology. In the next article we will be covering some guidelines around building the Serverless functions designed while asking the questions outlined here. If you have any questions or comments, please add them using the form below or reach out via Twitter/LinkedIn.