6 minutes read

POSTED Aug, 2020 dot IN Serverless

Working with Thundra in AWS SAM

Serkan Özal

Written by Serkan Özal


Founder and CTO of Thundra

 X

AWS SAM is rapidly growing in popularity. It’s a powerful tool for serverless development that integrates development, testing, and deployment strategies behind a comprehensive CLI, and it’s also a model of development that’s extensible and flexible. While AWS SAM does a lot to reduce configuration complexity, this doesn’t necessarily translate into greater platform observability. In this article we’ll walk through ways to close some of those observability holes with Thundra. We’ll implement a small serverless application to demonstrate the use of AWS SAM, then expand that application to include automated tracing and monitoring with Thundra.

Creating the Problem Space

We’ll be developing a basic calculator application, but this won’t devolve into long diatribes on Taylor series. Instead, we’ll focus on the four basic operations: addition, subtraction, multiplication, and division. We’ll have some basic error handling to watch for edge cases and bad values (dividing by 0, adding a string to an integer, etc.). We’ll develop and test locally using the AWS SAM CLI to power our debugging loop and explore the deployment possibilities the tool provides. Finally, we’ll work through integrating Thundra into the application.

Creating the Initial SAM Application

We’ll start with the most basic of commands, from the directory that will hold our project:

PS D:\dev\aws> sam init


This line walks you through the creation of your SAM application. It provides a template.yaml file, which you use to configure the environment surrounding your Lambda functions. Lambda functions all share similar function prototypes, receiving an “event” parameter and a “context” parameter on each invocation.

Once this step is completed, you’re ready to move on to implementing the calculator itself. You can choose from any of the template functions you like during initialization—there’s one for each major Lambda runtime. This article focuses on the NodeJS 12.x runtime.

Addition

We’ll start with the addition function, serverlessAddHandler. This function will take two operands and add them together. Below is the function code, which does as expected—reads two parameters from a body formatted as JSON, then returns the result of adding them together (formatted for API Gateway response):


/**
 * A Lambda function that adds two operands
 */
exports.serverlessAddHandler = async (event, context) => {
    // All log statements are written to CloudWatch
    var post_body = JSON.parse(event["body"])
    var lhs = parseInt(post_body["lhs"]);
    var rhs = parseInt(post_body["rhs"]);
    var result = lhs + rhs;
    return {
        "body": result,
        "statusCode": 200
    };
}

Figure 1: AWS Lambda addition function, Node.js

This function will be triggered via a POST request sent to API Gateway, with the URL for the function being set to “add.” We configure these resources in the template.yml file for the project as follows:


Resources:
  ...
  serverlessAddFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/serverless-add.serverlessAddHandler
      Runtime: nodejs12.x
      MemorySize: 128
      Timeout: 100
      Description: A Lambda function that adds two operands.
      Events:
        ServerlessAdd:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /add
            Method: post
      Policies:
        # Give Lambda basic execution Permission to the helloFromLambda
        - AWSLambdaBasicExecutionRole
 
Outputs:
  ServerlessAddFunction:
    Description: "ServerlessAdd Lambda Function ARN"
    Value: !GetAtt ServerlessAddFunction.Arn

Figure 2: Template.yml resource configuration for AWS Lambda addition

With these two files present, we’re now able to build our function using sam build. This converts our template.yml code into an executable and deployable unit. From this point, we can invoke our function locally using  sam local invoke serverlessAddFunction.


Subtraction

Like the addition function, the subtraction function, serverlessSubtractHandler, takes two inputs and returns the integer difference between them. Below are the code and configuration for the subtraction function:


/**
 * A Lambda function that subtracts two operands
 */
exports.serverlessSubtractHandler = async (event, context) => {
    // All log statements are written to CloudWatch
    var post_body = JSON.parse(event["body"])
    var lhs = parseInt(post_body["lhs"]);
    var rhs = parseInt(post_body["rhs"]);
    var result = lhs - rhs;
    return {
        "body": result,
        "statusCode": 200
    };
}

Figure 3: AWS Lambda subtraction function, Node.js


Resources:
  ...
  serverlessSubtractFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/serverless-subtract.serverlessSubtractHandler
      Runtime: nodejs12.x
      MemorySize: 128
      Timeout: 100
      Description: A Lambda function that subtracts two operands.
      Events:
        ServerlessSubtract:
          Type: Api 
          Properties:
            Path: /subtract
            Method: post
      Policies:
        # Give Lambda basic execution Permission to the function
        - AWSLambdaBasicExecutionRole
 
Outputs:
  ...
  ServerlessSubtractFunction:
    Description: "ServerlessSubtract Lambda Function ARN"
    Value: !GetAtt ServerlessSubtractFunction.Arn

Figure 4: Template.yml configuration for ServerlessSubtractFunction

We’re now free to build and invoke our function locally.


Multiply

The multiplication function, serverlessMultiplyHandler, like the subtraction function before it, takes two operands and returns the product between them. The code for multiplication is available below, along with the template.yml file. As you can see, we’re able to re-use a lot of the same configuration values to expand our function suite. This will allow us to build a more cohesive picture of our serverless application once we move on to logging.


/**
 * A Lambda function that multiplies two operands
 */
exports.serverlessMultiplyHandler = async (event, context) => {
    // All log statements are written to CloudWatch
    var post_body = JSON.parse(event["body"])
    var lhs = parseInt(post_body["lhs"]);
    var rhs = parseInt(post_body["rhs"]);
    var result = lhs * rhs;
    return {
        "body": result,
        "statusCode": 200
    };
}

Figure 5: AWS Lambda multiplication function, Node.js


Resources:
  ...
  serverlessMultiplyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/serverless-multiply.serverlessMultiplyHandler
      Runtime: nodejs12.x
      MemorySize: 128
      Timeout: 100
      Description: A Lambda function that multiplies two operands.
      Events:
        ServerlessMultiply:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /multiply
            Method: post
      Policies:
        # Give Lambda basic execution Permission to the function
        - AWSLambdaBasicExecutionRole
 
Outputs:
  ...
  ServerlessMultiplyFunction:
    Description: "ServerlessMultiply Lambda Function ARN"
    Value: !GetAtt ServerlessMultiplyFunction.Arn

Figure 6: AWS Lambda multiplication function configuration

Divide

The divide function, serverlessDivideHandler, adds a tiny bit of complexity. Whereas before we were largely unconcerned with the values presented (assuming the input was numerically formatted), with division we must be careful to avoid having a zero in the divisor. We’ll represent this with an early exit state in the code.


/**
 * A Lambda function that divides two operands
 */
exports.serverlessDivideHandler = async (event, context) => {
    // All log statements are written to CloudWatch
    var post_body = JSON.parse(event["body"])
    var lhs = parseInt(post_body["lhs"]);
    var rhs = parseInt(post_body["rhs"]);
    if(rhs == 0) {
        return {
            "body": "Error - divide by 0!",
            "statusCode": 403
        }
    }
    var result = lhs / rhs;
    return {
        "body": result,
        "statusCode": 200
    };
}

Figure 7: AWS Lambda division function, Node.js


Resources:
  ...
  serverlessDivideFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/serverless-divide.serverlessDivideHandler
      Runtime: nodejs12.x
      MemorySize: 128
      Timeout: 100
      Description: A Lambda function that divides two operands.
      Events:
        ServerlessDivide:
          Type: Api 
          Properties:
            Path: /divide
            Method: post
      Policies:
        # Give Lambda basic execution Permission to the function
        - AWSLambdaBasicExecutionRole
 
Outputs:
  ...
  ServerlessDivideFunction:
    Description: "ServerlessDivide Lambda Function ARN"
    Value: !GetAtt ServerlessDivideFunction.Arn

Figure 8: AWS Lambda division function configuration

Testing the Code

AWS SAM really begins to show its worth once we’re ready to begin testing and deployment. In addition to unit testing support in our language of choice, AWS SAM gives us a few helpful tools  to create a robust and repeatable testing and deployment process. The tools we’ll cover are local invocation, event generation, and guided deployment.

Local invocation is the ability to run a copy of your AWS Lambda code locally. Being able to run code in a familiar environment is a critical step in creating an efficient development pipeline. AWS SAM provides us with three tools to invoke Lambda functions locally: sam local invoke, sam local start-api, and sam local start-lambda. Use each in a way that makes sense for your function triggers. The local development loop for the functions above consisted of repeated invocations using Postman, with the Lambda functions running behind an HTTP API using sam local start-api.


Generating Test Events

The second feature that proves incredibly useful for acceptance testing is the ability to generate an AWS-formatted event locally using sam local generate-event …. This generates the text of an event, which you can save in a local JSON file for incorporation into your environment’s testing setup. This lets you easily replicate the messaging format of AWS resources without executing the resources themselves, saving you time and debugging effort that you’d normally use for discovering what the structure of each event represented.


Deployment

Once you’ve finished testing your code, AWS SAM can walk you through the cumbersome process of deploying your changes and configuring your application’s AWS resources. This can be done in a guided fashion, during which you’ll be presented with several configurable options to fine-tune your function’s deployment, or it can be done automatically with the default configured settings enabled. The approach you choose will depend largely on your infrastructure needs, but most people will have no problem simply running sam deploy --guided, which will generate and populate the needed AWS resources as it builds and deploys your application’s Node.js code.

Integrating Thundra

Now that we have a sizable application deploying through AWS SAM, it’s time to turn an eye to maintainability. One of the core issues with many serverless setups is observability. The ephemeral nature of serverless hardware, and in some cases the competing needs of AWS products themselves, conspire to make full observability across your serverless application a problem. Luckily, Thundra can step in as a robust and mature serverless observability solution, closing the critical holes left by CloudWatch and X-Ray.

To configure our AWS SAM application to incorporate Thundra, we’ll start with setting our API key in template.yml:


Globals:
  Function:
    Environment:
      Variables:
          thundra_apiKey: <your_api_key>

Figure 9: Thundra API key configuration

Next, we’ll need to add the Thundra layer to our Layers section in the Global settings:


Parameters:
  ThundraAWSAccountNo:
    Type: Number
    Default: 269863060030
 
  ThundraNodeLayerVersion:
    Type: Number
    Default: 62 # Or use any other version
 
Globals:
  Function:
  …
  Layers:
  - !Sub arn:aws:lambda:${AWS::Region}:${ThundraAWSAccountNo}:layer:thundra-lambda-node-layer:${ThundraNodeLayerVersion}

Figure 10: Thundra Lambda Layer configuration

Then, we’ll change the runtime of our serverless functions to provided:


Resources:
  ...
  serverlessAddFunction:
    Type: AWS::Serverless::Function
    Properties:
      ...
      Runtime: provided
      ...

Figure 11: Function runtime change

With these configurations completed, we can fully trace our serverless application. This lets us build more complex functionality into our calculator by letting us build a cohesive and comprehensive picture of our application’s behavior via Thundra’s dashboard. Thundra builds on the existing Lambda toolchain to close the holes in provider-driven observability, tying together all of the metrics we need into a single interface.

Tying It All Together

With the advent of AWS SAM, serverless development has really been showing its strength. For evidence, just consider that in this article we created a serverless calculator application and, with a few simple additions, fully instrumented the application using Thundra to drive analytics—all from a relatively small set of template and code files. This integration, which has full monitoring and debugging capabilities, can be used as a base for experimenting with some of Thundra’s more advanced features. With Thundra in the passenger seat, your serverless development will reach greater heights of complexity and accomplishment.