Tip

How to use microservices to manage serverless APIs

Monolithic applications may be a little bit heavy for development of serverless APIs, but microservices can make all the difference. Here's how to make it work.

As organizations of all types continue to kick their own API development into high gear, some developers may find their old monolithic services are simply too heavy to provision using something like AWS CloudFormation. This is a sign it's time to break your service into multiple smaller services that don't contain large numbers of API endpoints. Let's review how and why microservices can help you create and manage serverless APIs. 

The Serverless Framework error

Serverless Framework is a popular API creation and management tool on AWS Lambda. It connects to API Gateway and Lambda through a CloudFront template, which makes it easy to write an application once and then release it to staging and production environments. But Serverless Framework can display an error that can scare developers into thinking everything they're doing is wrong:

The CloudFormation template is invalid: Template format error: Number of resources, 201, is greater than maximum allowed, 200

This error message can confuse developers who don't have over 200 API endpoints. For example, a user can have about 50 API endpoints and still encounter this message.

This misunderstanding occurs because resource does not mean API endpoint. An API endpoint might look like a single resource while it is in Serverless Framework, but once compiled to the CloudFormation template, that endpoint may be split into three or four different resources: one resource for the API Gateway connection, one for the Lambda function, one for an associated identity and access management role and possibly another if there are Simple Queue Service or Simple Notification Service topics also associated with the API endpoint.

There's no way to break down the endpoints of these serverless APIs or split them into multiple stages of deployment, so developers are essentially stuck re-architecting their API.

Splitting the serverless monolith into microservices

Knowing that a single CloudFormation stack can't really contain more than a few API endpoints, developers must plan to split that service into multiple services. Many developers do this already, so why not just split into microservices and keep everything separate? Developers can even split out common tasks, such as user authentication, into entirely separate Lambda functions that can be called by any of the separately created microservices.   

For example, a company may have one serverless service that fetches a license by an ID, a second that updates a license by an ID and a third that fetches account information. All of these services share the common task of authenticating and validating user permissions for API access.

Since each of these serverless APIs is a completely separate serverless service, they each create their own CloudFormation stack and API Gateway. As a result, the developers can't simply create an authorizer function in one of them and share them across each of their microservices. Instead, they split that authorizer function out into its own microservice.

In this example, the authorizer function actually sits in its own separate repository, and it isn't even included with its own serverless service. It's simply a Lambda function that builds using CodeBuild with a very basic buildspec.yml configuration:

version: 0.2

phases:

  install:

    commands:

      # Pre-install dependencies and environment config

      - apt-get update

      - apt-get install -y ssh

      # Download SSH keys from S3

      - aws s3 sync s3://company-config/home/ ~/

      - chmod 0600 ~/.ssh/*

      - npm install npm@6 --global

      # Install dependencies needed for running tests

      - npm install

  pre_build:

    commands:

      # Discover and run unit tests in the 'tests' directory

      - npm test

  build:

    commands:

      # Build the js files

      - ./node_modules/.bin/tsc

      # Zip everything up excluding the node_modules/@types directory

      -

        zip -r archive.zip

        *.js src/*.js node_modules

        -x "node_modules/@types/*"

      # Use the AWS CLI directly to upload our lambda function

      -

        aws lambda update-function-code

        --function-name authenticate

        --zip-file fileb://archive.zip

        --publish > ./resp.json   

      # Tag the new version

      -

        aws lambda update-alias

        --function-name authenticate

        --name $BUILD_ENV

        —function-version

        $(echo "console.log(require('./resp.json').Version)"|node)

This basic auto-build script lets you avoid the unnecessary overhead of using a framework, like Serverless or Apex, and lets you link directly to the Lambda function from your other microservices. By using AWS Command Line directly, you're able to upload a new version of your microservice and tag it based on the build environment, or even tag specific releases by version if you choose. Because the AWS serverless APIs allow you to invoke a Lambda function by a specific alias, you can also maintain a separate version of your code for development, staging and production by setting the Qualifier property on lambda.invoke:

      const lambda_resp = await lambda.invoke({

         FunctionName: 'authenticate',

         Qualifier: process.env.BUILD_ENV,

         Payload: JSON.stringify(event),

      }).promise();

Take note that this method does not automatically manage the IAM role and environment variables. The IAM roles should not change frequently for most microservices, so this hopefully shouldn't cause too much of an issue.

Putting all of your microservices under one domain

Now that you have your separate serverless services to fetch licenses, update licenses and fetch user information, the next challenge is to expose all of those services to a single API domain. Using the Serverless Framework plug-in serverless-domain-manager, you can configure a custom domain that references each of your microservices under a different base path. Here's a licenses service example:

custom:

  customDomain:

    domainName: api.example.com

    basePath: licenses

Unfortunately, the basePath cannot contain slashes, so developers must use separate domain names for each build environment by adding in the stage to the domainName:

domainName: api-${opt:stage, self:provider.stage}.example.com

In this example, each stage will fall under its own separate domain, such as api-dev.example.com/licenses. To update a license, however, the API can't live under the same base path, which means the list, fetch and update operations all must exist under the same serverless service -- if you use the Serverless Framework. Alternatively, you can create each microservice as a simple Lambda function with qualifiers and create an API Gateway manually to link to them. By creating the API Gateway separately from the Lambda function, you can more easily deploy updates to individual microservices and then link them all under a single API Gateway and custom domain.

Serverless Framework is the glue for your microservices

The problem with Serverless Framework is that it doesn't simplify the process of splitting Lambda functions into separate microservices that deploy individually. Whenever you execute sls deploy, it bundles all the code in the current directory and uploads it as a CloudFormation stack. This means that every Lambda function in that service uses the same code base with a different entry point. That configuration violates the core idea of microservices, which is to have each microservice run isolated and independent of each other.

It's also troublesome to split each microservice into its own serverless service, as you can't have multiple microservices exist under a single API Gateway base path. To use serverless APIs with microservices, you should keep each API Gateway separate from each other and produce a separate CloudFormation Stack for every microservice. This is not ideal and makes you question using Serverless Framework with microservices at all. However, the better approach is to use Serverless Framework to deploy the "glue code" which connects each of the deployed microservices separately under one single API Gateway.

Dig Deeper on Enterprise architecture management