How can you architect Serverless SaaS Applications on AWS?
In my previous blog post I listed 6 key themes that separates successful SaaS vendors from the rest. In this post I dive more deeply into one of the themes discussed in the previous blog post, namely serverless and microservices.
One of the great innovations of public cloud computing in recent years has been the advent of serverless computing. Serverless computing allows you to focus on writing and deploying software components without the need for focusing on the underlying infrastructure. The software components, often called functions, are executed based on a defined set of events and compute resources are consumed based on the usage during execution. AWS Lambda [1] was the first publicly available serverless computing offering. AWS Lambda supports natively Java, Go, PowerShell, Node.js, C#, Python, and Ruby runtimes. It also provides AWS Lambda layers or runtimes which allows you to use any additional programming languages to author your functions.
In this blog post I discuss some of the key considerations you need to make when designing a serverless SaaS architecture and give an overview of an example serverless microservice architecture for SaaS applications on AWS.
Benefits of serverless
When working with SaaS providers or independent software vendors (ISVs) wanting to transform their product into SaaS we typically advise them to design their application architecture using microservices and serverless capabilities. This offers a number of advantages:
- You focus on value adding activities like writing code instead of designing and managing infrastructure
- You speed up development time and simplify development of new functionality by breaking it down into small pieces of functionality
- You optimise infrastructure cost by consuming only the computing resources required to run the code with 100ms billing - you don’t pay for the idle time
- You get the benefits of autoscaling of infrastructure resources
There are also drawbacks of going serverless. Testing, especially integration testing, becomes more difficult as the units of integration become smaller. It is also often difficult to identify security vulnerabilities with traditional security tooling and debug your application in the serverless approach. Lastly, for many organisations moving to serverless requires a complete paradigm shift. You need to upskill your developers, architects and security teams to think and operate in the new environment.
Defining the microservices
When designing the architecture one of the first things you need to do is to define the granularity of your microservices and functions. By making your functions too large, including lots of functionality into the same function, you lose some of the flexibility and speed of development. It also makes it harder to debug your functions and makes your software less fault tolerant. By making your functions small enough you enhance the fault tolerance of your application. You can design your application in a way that if one function fails the others can mostly still remain operational. On the other hand, making your functions too small you increase complexity and make it more difficult to understand and manage the overall architecture. There’s a middle ground that depends a lot on the software and functionality you are building. For example, if you want to provide tiering of functionality for your customers, so that some functionality is available only for certain subscription tier, you should decouple such functionality into separate microservices. Whatever granularity you choose, it is important to make sure that the microservices are loosely coupled to allow them to be developed and deployed independently of the other microservices.
Adding tenant context
The second consideration is specific to the SaaS model. When building SaaS applications you need to be able to do tenant isolation, tenant management, tenant metering and monitoring. You need to be able to identify and authenticate tenants and offer different tenants different sets of functionality based on their subscription tier. You should also make sure the performance is fairly distributed among tenants and monitor the usage to identify upsell opportunities and gain valuable insights about usage patterns. On AWS you can implement all this with the help of Amazon Cognito [2]. You can use Cognito to manage user identities and to inject user context into the different layers of your application stack.
Example architecture
Our simplified example is a serverless architecture for a SaaS application. The example uses S3 buckets for static web content, API Gateway for REST API, Cognito for user management and authentication, Lambda for serverless microservices, Amazon Aurora Serverless for SQL database and DynamoDB for NoSQL database.
Each of the Lambda functions can themselves trigger additional Lambdas. It is therefore easy to design even quite complex applications using simple functions. However we advise our customers to avoid so-called serverless monoliths and instead design their lambda functions to be as independent as possible. The best practice is to adopt an event-driven approach where each Lambda function is independent from each other and triggered by events. Lambda functions can then emit events to e.g. Amazon SNS, to trigger other functions. You can also use AWS Step Functions to coordinate the Lambda functions [3].
There are couple of strategies you can take when designing your Lambda functions:
- Create one Lambda function per microservice, where each microservice is a unit that is able to work in isolation
- For each microservice, create one Lambda function that handles all of the HTTP methods POST, GET, PUT, DELETE, etc…
- For each HTTP function create a separate Lambda function
By choosing one of the strategies above you limit the complexity of your architecture. However, if you choose to have one Lambda function per microservice you need to make sure your microservices are quite granular.
Our example architecture uses Cognito to make sure that each layer is aware of the tenant context. This allows you to offer the right functionality through the API and execute Lambda functions in a tenant specific context.
Next steps
One of the most important factors of a successful SaaS architecture is how you enable multitenancy and implement tenant isolation. In our example above we implicitly assumed that we have the same instances of the APIs, functions and databases for all the tenants and tenant isolation is handled using tenant context through Cognito. There are other ways to handle tenant isolation. However, going through the different options deserves its own blog post. So stay tuned.
References:
[1] https://aws.amazon.com/lambda/
Get in Touch.
Let’s discuss how we can help with your cloud journey. Our experts are standing by to talk about your migration, modernisation, development and skills challenges.