In Part 1, I provided an overview of FaaS defining what it is as well as discussing some of the advantages and disadvantages of the architecture. In this part, I will discuss applying some microservice patterns & best practices to FaaS with a sample use case implementation using AWS Lambda.
Microservice Patterns & Best Practices
There are several microservice patterns and best practices available to implement a good architecture and the list below is by no means complete. I will discuss how the example use case (Flight Monitoring and Event Notifications) can implement these patterns and best practices to a FaaS architecture.
- Gateway Routing - Route requests to multiple services using a single endpoint. This pattern is useful when you wish to expose multiple services on a single endpoint and route to the appropriate service based on the request
- Separate Data Store for each Microservice (Function) - Each function owns and maintains its data store with no sharing across functions
- Command Query Responsibility Segregation (CQRS) - Utilizing event sourcing to act as a command to trigger another action
- Stateless - Treat functions as replaceable entities of a group. If one goes down, it can quickly come up and reconstitute itself. Functions are "cattle" not "pets"
- Model Services and APIs based on Domain-driven Design (DDD) - Services should be modeled around the business domain. APIs should embrace modeling specs such as Open API (Swagger)
- Failure Isolation - Adopt isolation of failure using independent functions. This includes asynchronous communications, loose coupling (high cohesion), event-driven and self-contained
- Good CI/CD Practices - Independently deploy, update, scale & replace any function.
- Throttling - The ability to throttle request bursts to ensure stability.
Use Case - Flight Monitoring and Event Notifications
A client sells airline insurance for tickets purchased on public websites. They want to implement a cloud-based solution that will automatically detect if a flight cancellation or delay has occurred and if so, send the customer an SMS message notifying them of the reason and status of the flight interruption.
There are 3 workflows for this use case:
- HTTP POST for flights. This adds flight metadata to the database which includes flight information and passenger email address and cell number
- HTTP POST flight notification. This is the API to notify the system of a flight event such as a delay or cancellation. All passengers who are affected by the event are sent an SMS (Text) message
- HTTP GET Alerts. This queries alerts issued from flight event notifications rong> Alerts. This queries alerts issued from flight event notifications
Several architectural options were considered in developing this solution. The following are the key drivers in selecting the solution:
- Data Source - Two choices. Traditional RDMS such as MySQL via RDS or a NO-SQL using the AWS DynamoDB. The RDS was selected because it is a more mainstream data store, has backup capability, and data integrity to name a few. Also using an RDS implementation presented a more complex set of considerations to challenge FaaS adaptability which is discussed in Part 3
- Model-Based - Follow a DDD driven approach for building services and APIs. Swagger (OpenAPI spec) was used to describe and build the APIs
- Event-Driven - Use of message bus (SNS) to follow a more CQRS pattern and to enhance loose coupling with high cohesion
- API Gateway - The use of an API Gateway to provide throttling, access (API key), validation and governance to separate the API model from the functional execution (Lambda)
- CI/CD - Fully automate green-field deployment and breakdown of application and infrastructure
Figure 1 below illustrates the implemented architecture. The architecture consists of the following major components:
VPC - A VPC was created because in order to make the RDS instance secure (not accessible from the internet) you need to put it in a VPC with the appropriate security groups, roles, and subnets.
RDS - The RDS contains a MySQL instance to store flights and alerts data.
Lambda Functions - There are 3 Lambda functions. Two in the VPC and one outside the VPC. The two Lambdas inside the VPC are there because, in order to directly access the flights and alerts data, they need to be in the same VPC as RDS. This means the Lambdas no longer have access to the internet.
- Flights Lambda (Inside VPC) - Handles all flight-related functionality
- Alerts Lambda (Inside VPC) - Handles alerts-related functionality
- SMS Lambda (Outside VPC) - Handles alert notification and sends texts (SMS)
NAT and Internet Gateway - The Alerts Lambda is inside the VPC and it needs public access to communicate with the SNS service. NAT provides this capability. The Internet Gateway provides access to the Bastion Server so the deployment script can create tables in the RDS instance (SSH Tunneling).
Note: NAT can be expensive. AWS provides an alternative to a NAT for specific AWS services via a VPC Endpoint (VPCE) which is more cost effective. At the time of the development, only S3 and DynamoDB VPCEs were available. Since then AWS has added an SNS VPCE. For this use case, a NAT would not be needed if the SNS VPCE was available.
Bastion Server - The Bastion Server was created strictly to provide SSH Tunneling to allow the deploy script to create tables in RDS. After the tables are created, the script provides options to either keep the server active, idle, or deletes it.
API Gateway - The gateway provides API management and access to internal Lambdas. APIs were modeled using a Swagger configuration template.
S3 - S3 was used to store configuration files needed by the Lambdas such as JDBC settings. The data was encrypted without public access.
CloudWatch - Provides logging for Lambdas. It also "pings" each Lambda every 20 minutes to keep it "warm" to reduce cold start latency. ( More on keeping Lambdas warm in Part 3).
SNS Service - SNS is the message bus for inter-lambda communications. When an alert needs to be sent, the Alerts Lambda publishes an alerts message to the alerts SNS topic. The SMS Lambda subscribes to that same topic and receives the message to process.
SMS Service - SMS takes the constructed text from the SMS Lambda and sends it to all affected customers.
Figure 1: Implemented Architecture
How it Was Built - Tools & Frameworks
As mentioned in Part1, there are many FaaS based languages you can use depending upon the Cloud platform support. For this use, case I chose Java because that is the language I am most comfortable with.
You first need an AWS account to deploy the Lambdas. Credentials with AWS environment variables need to be set according to AWS requirements. Installed software requirements:
- Java 8
- Latest AWS CLI installed (version>=1.11.37)
- Node.js 4+ (To install Serverless Framework)
- Serverless Framework with plugins installed
AWS provides a Java SDK for all of their APIs. I created a Maven project and used the Intellij IDE to code the project. The setup was designed so each function is treated as a separate module so they can be developed and deployed independently. I also created modules for components that could be shared across functions (modules) such as common utilities and JDBC logic. Each function is compiled into an executable jar with all of its dependencies included.
To execute, AWS just requires your Java function to implement the RequestHandler interface to handle events. The rest of the code is your business logic and parsing any of context and event-related parameters passed to the function.
The Serverless Framework allows you to deploy your Lambda functions (utilizing the function jars you created) and configure the API Gateway. The framework also includes some plugins to simplify the deploy and configurations. All of the framework capabilities/configurations are expressed in a YAML file.
I organized the Serverless YAML files by "service" domain. As service is analogous to a Microservice which can have one-to-many APIs.
This framework was very important in not only simplifying the configuration/deployment but was also a huge time saver. You can deploy or remove the entire FaaS stack with a single command line (sls deploy or sls remove). That is very cool.
Swagger (Open API)
The OpenAPI (a.k.a. Swagger) is a spec to model and define your REST APIs. It is widely adopted and many Cloud providers utilize it to help define and configure their own API Gateways. Like with the Serverless Framework, the Swagger spec can be expressed in either JSON or YAML formats. Swagger provided the API definitions as well as the resource models and API validation criteria.
After I modeled the APIs via Swagger, it was just a cut & paste into the Serverless YAML file. Note: There are some initiatives in the Serverless community to more tightly integrate Swagger to just reference the Swagger file vs. a cut & paste effort.
Mustache is a cross-language logic-less templating framework. It was used to populate configuration files (e.g., Serverless file and JDBC property file) with dynamic data during deployment. For example, a Lambda function inside a VPC needs subnet ids, security group information to configure the function. This information comes from the upstream deployment step via the CloudFormation template.
CloudFormation is the AWS "infrastructure as code" service to help you model and set up your AWS resource infrastructure such as the VPC, roles, security groups, SNS Topics, Bastion Server, etc. Like with Swagger, you express the template capabilities in either a JSON or YAML format.
To execute the infrastructure creation/destruction an executable java application was made utilizing the AWS SDK and CloudFormation template. In addition to setting up the required Cloud infrastructure, it also created the RDS instance tables (via Bastion server) and deployed some property files to S3. It did everything except deploy/configure the Lambda Functions and API Gateway.
The deployment scripts are Bash code calling the deploy jar and in sequence executing the different Serverless Framework configuration files. Each script required only a few parameters to execute. There is one script to create the entire stack and another to tear it down.
In starting out on building and deploying the use case, I wanted to make sure I applied Microservices patterns and best practices for a FaaS architecture. Below are some of the key concepts applied.
Gateway Routing - The API Gateway was used to handle multiple services using a single endpoint. There were two services, flights/alerts and messaging.
Separate Data Store for each Microservice (Function) - Each service had their database tables for them alone to manage.
Command Query Responsibility Segregation (CQRS) - When a notification was posted, alerts triggered an SNS event to send out text messages.
Model Services and APIs based on Domain-driven Design (DDD) - Utilized Swagger to model APIs.
Good CI/CD Practices - Both the architecture and development model supported independent deployment as well as a fully automated creation/destruction of the stack.
For the most part, FaaS can subscribe to a Microservices centric architecture, but it does have some caveats.
In Part 3 I will discuss the lessons learned and conclusion about FaaS.