1. Overview

In our previous article, we already implemented a full stack serverless application on AWS, using API Gateway for REST endpoints, AWS Lambda for business logic, as well as a DynamoDB as a database.

However, the deployment consists of many manual steps, which might get unhandy with growing complexity and with the number of environments.

In this tutorial now, we’ll discuss how to use the AWS Serverless Application Model (SAM), which enables a template-based description and automated deployment of serverless applications on AWS.

In detail, we’ll have a look at the following topics:

  • Basics of the Serverless Application Model (SAM), as well as of the underlying CloudFormation
  • Definition of a Serverless Application, using the SAM template syntax
  • Automated deployment of the application, using the CloudFormation CLI

2. Basics

As discussed previously, AWS enables us to implement entirely serverless applications, by using API Gateway, Lambda functions, and DynamoDB. Undoubtedly, that offers already many advantages for performance, cost, and scalability.

However, the downside is, that we need a lot of manual steps in the AWS Console at the moment, like creating each function, uploading code, creating the DynamoDB table, creating IAM roles, creating API and API structure, etc.

For complex applications and with multiple environments like test, staging, and production, that effort multiplies quickly.

This is where CloudFormation for applications on AWS in general, and Serverless Application Model (SAM) specifically for serverless applications, comes into play.

2.1. AWS CloudFormation

CloudFormation is an AWS service for the automatic provisioning of AWS infrastructure resources. A user defines all required resources in a blueprint (called template), and AWS takes care of the provisioning and configuration.

The following terms and concepts are essential for understanding CloudFormation and SAM:

A template is a description of an application, how it should be structured at runtime. We can define a set of required resources, as well as how these resources shall be configured. CloudFormation provides a common language for defining templates, supporting JSON and YAML as a format.

Resources are the building blocks in CloudFormation. A resource can be anything, like a RestApi, a Stage of a RestApi, a Batch Job, a DynamoDB table, an EC2 instance, a network interface, an IAM role, and many more. The official documentation currently lists about 300 resource types for CloudFormation.

A stack is the instantiation of a template. CloudFormation takes care of provisioning and configuration the stack.

2.2. Serverless Application Model (SAM)

As so often, the use of powerful tools can get very complex and unhandy, which is also the case for CloudFormation.

That is why Amazon introduced the Serverless Application Model (SAM). SAM started with the claim to provide a clean and straightforward syntax for defining serverless applications. Currently, it has only three resource types, which are Lambda functions, DynamoDB tables, and APIs.

SAM is based on the CloudFormation template syntax, so we can define our template using the simple SAM syntax, and CloudFormation will further process that template.

More details are available at the official GitHub repository as well as within the AWS documentation.

3. Prerequisites

For the following tutorial, we’ll need an AWS account. A free tier account should be sufficient.

Besides that, we need to have the AWS CLI installed.

Finally, we need an S3 Bucket in our region, which can be created via the AWS CLI with the following command:

$>aws s3 mb s3://baeldung-sam-bucket

While the tutorial uses baeldung-sam-bucket in the following, be aware that bucket names must be unique, so you have to choose your name.

As a demo application, we’ll use the code from Using AWS Lambda with API Gateway.

4. Creating the Template

In this section, we’ll create our SAM template.

We’ll first have a look at the overall structure, before defining the individual resources.

4.1. Structure of the Template

First, let’s have a look at the overall structure of our template:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Baeldung Serverless Application Model example
 
Resources:
  PersonTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      # Define table properties here
  StorePersonFunction:
    Type: AWS::Serverless::Function
    Properties:
      # Define function properties here
  GetPersonByHTTPParamFunction:
    Type: AWS::Serverless::Function
    Properties:
      # Define function properties here
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      # Define API properties here

As we can see, the template consists of a header and a body:

The header specifies the version of the CloudFormation template (AWSTemplateFormatVersion) as well as the version of our SAM template (Transform). We can also specify a Description.

The body consists of a set of resources: each resource has a name, a resource Type, and a set of Properties.

The SAM specification currently supports three types: AWS::Serverless::ApiAWS::Serverless::Function as well as AWS::Serverless::SimpleTable.

As we want to deploy our example application, we have to define one SimpleTable, two Functions, as well as one Api in our template-body.

4.2. DynamoDB Table Definition

Let’s define our DynamoDB table now:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Baeldung Serverless Application Model example
 
Resources:
  PersonTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
          Name: id
          Type: Number
      TableName: Person

We only need to define two properties for our SimpleTable: the table name, as well as a primary key, which is called id and has the type Number in our case.

A full list of supported SimpleTable properties can be found in the official specification.

Note: As we only want to access the table using the primary key, the AWS::Serverless::SimpleTable is sufficient for us. For more complex requirements, the native CloudFormation type AWS::DynamoDB::Table can be used instead.

4.3. Definition of the Lambda Functions

Next, let’s define our two functions:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Baeldung Serverless Application Model example
 
Resources:
  StorePersonFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.baeldung.lambda.apigateway.APIDemoHandler::handleRequest
      Runtime: java8
      Timeout: 15
      MemorySize: 512
      CodeUri: ../target/aws-lambda-0.1.0-SNAPSHOT.jar
      Policies: DynamoDBCrudPolicy
      Environment:
        Variables:
          TABLE_NAME: !Ref PersonTable
      Events:
        StoreApi:
          Type: Api
            Properties:
              Path: /persons
              Method: PUT
              RestApiId:
                Ref: MyApi
  GetPersonByHTTPParamFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.baeldung.lambda.apigateway.APIDemoHandler::handleGetByParam
      Runtime: java8
      Timeout: 15
      MemorySize: 512
      CodeUri: ../target/aws-lambda-0.1.0-SNAPSHOT.jar
      Policies: DynamoDBReadPolicy
      Environment:
        Variables:
          TABLE_NAME: !Ref PersonTable
      Events:
        GetByPathApi:
          Type: Api
            Properties:
              Path: /persons/{id}
              Method: GET
              RestApiId:
                Ref: MyApi
        GetByQueryApi:
          Type: Api
            Properties:
              Path: /persons
              Method: GET
              RestApiId:
                Ref: MyApi

As we can see, each function has the same properties:

Handler defines the logic of the function. As we are using Java, it is the class name including the package, in connection with the method name.

Runtime defines how the function was implemented, which is Java 8 in our case.

Timeout defines how long the execution of the code may take at most before AWS terminates the execution.

MemorySize defines the size of the assigned memory in MB. It’s important to know, that AWS assigns CPU resources proportionally to MemorySize. So in the case of a CPU-intensive function, it might be required to increase MemorySize, even if the function doesn’t need that much memory.

CodeUri defines the location of the function code. It currently references the target folder in our local workspace. When we upload our function later using CloudFormation, we’ll get an updated file with a reference to an S3 object.

Policies can hold a set of AWS-managed IAM policies or SAM-specific policy templates. We use the SAM-specific policies DynamoDBCrudPolicy for the StorePersonFunction and DynamoDBReadPolicy for GetPersonByPathParamFunction and GetPersonByQueryParamFunction.

Environment defines environment properties at runtime. We use an environment variable for holding the name of our DynamoDB table.

Events can hold a set of AWS events, which shall be able to trigger the function. In our case, we define an Event of type Api. The unique combination of path, an HTTP Method, and a RestApiId links the function to a method of our API, which we’ll define in the next section.

A full list of supported Function properties can be found in the official specification.

4.4. API Definition as Swagger File

After defining DynamoDB table and functions, we can now define the API.

The first possibility is to define our API inline using the Swagger format:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Baeldung Serverless Application Model example
 
Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: test
      EndpointConfiguration: REGIONAL
      DefinitionBody:
        swagger: "2.0"
        info:
          title: "TestAPI"
        paths:
          /persons:
            get:
              parameters:
              - name: "id"
                in: "query"
                required: true
                type: "string"
              x-amazon-apigateway-request-validator: "Validate query string parameters and\
                \ headers"
              x-amazon-apigateway-integration:
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetPersonByHTTPParamFunction.Arn}/invocations
                responses: {}
                httpMethod: "POST"
                type: "aws_proxy"
            put:
              x-amazon-apigateway-integration:
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${StorePersonFunction.Arn}/invocations
                responses: {}
                httpMethod: "POST"
                type: "aws_proxy"
          /persons/{id}:
            get:
              parameters:
              - name: "id"
                in: "path"
                required: true
                type: "string"
              responses: {}
              x-amazon-apigateway-integration:
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetPersonByHTTPParamFunction.Arn}/invocations
                responses: {}
                httpMethod: "POST"
                type: "aws_proxy"
        x-amazon-apigateway-request-validators:
          Validate query string parameters and headers:
            validateRequestParameters: true
            validateRequestBody: false

Our Api has three properties: StageNamedefines the stage of the API, EndpointConfiguration defines whether the API is regional or edge-optimized, and DefinitionBody contains the actual structure of the API.

In the DefinitionBody, we define three parameters: the swagger version as “2.0”, the info:title: as “TestAPI”, as well as a set of paths.

As we can see, the paths represent the API structure, which we had to define manually before. The paths in Swagger are equivalent to the resources in the AWS Console. Just like that, each path can have one or more HTTP verbs, which are equivalent to the methods in the AWS Console.

Each method can have one or more parameters as well as a request validator.

The most exciting part is the attribute x-amazon-apigateway-integration, which is an AWS-specific extension to Swagger:

uri specifies which Lambda function shall be invoked.

responses specify rules how to transform the responses returned by the function. As we are using Lambda Proxy Integration, we don’t need any specific rule.

type defines that we want to use Lambda Proxy Integration, and thereby we have to set httpMethod to “POST”, as this is what Lambda functions expect.

A full list of supported Api properties can be found in the official specification.

4.5. Implicit API Definition

A second option is to define the API implicitly within the Function resources:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Baeldung Serverless Application Model Example with Implicit API Definition
 
Globals:
  Api:
    EndpointConfiguration: REGIONAL
    Name: "TestAPI"
 
Resources:
  StorePersonFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.baeldung.lambda.apigateway.APIDemoHandler::handleRequest
      Runtime: java8
      Timeout: 15
      MemorySize: 512
      CodeUri: ../target/aws-lambda-0.1.0-SNAPSHOT.jar
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref PersonTable
      Environment:
        Variables:
          TABLE_NAME: !Ref PersonTable
      Events:
        StoreApi:
          Type: Api
          Properties:
            Path: /persons
            Method: PUT
  GetPersonByHTTPParamFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.baeldung.lambda.apigateway.APIDemoHandler::handleGetByParam
      Runtime: java8
      Timeout: 15
      MemorySize: 512
      CodeUri: ../target/aws-lambda-0.1.0-SNAPSHOT.jar
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref PersonTable
      Environment:
        Variables:
          TABLE_NAME: !Ref PersonTable
      Events:
        GetByPathApi:
          Type: Api
          Properties:
            Path: /persons/{id}
            Method: GET
        GetByQueryApi:
          Type: Api
          Properties:
            Path: /persons
            Method: GET

As we can see, our template is slightly different now: There is no AWS::Serverless::Api resource anymore.

However, CloudFormation takes the Events attributes of type Api as an implicit definition and creates an API anyway. As soon as we test our application, we’ll see that it behaves the same as when defining the API explicitly using Swagger.

Besides, there is a Globals section, where we can define the name of our API, as well as that our endpoint shall be regional.

Only one limitation occurs: when defining the API implicitly, we are not able to set a stage name. This is why AWS will create a stage called Prod in any case.

5. Deployment and Test

After creating the template, we can now proceed with deployment and testing.

For this, we’ll upload our function code to S3 before triggering the actual deployment.

In the end, we can test our application using any HTTP client.

5.1. Code Upload to S3

In a first step, we have to upload the function code to S3.

We can do that by calling CloudFormation via the AWS CLI:

$> aws cloudformation package --template-file ./sam-templates/template.yml --s3-bucket baeldung-sam-bucket --output-template-file ./sam-templates/packaged-template.yml

With this command, we trigger CloudFormation to take the function code specified in CodeUri: and to upload it to S3. CloudFormation will create a packaged-template.yml file, which has the same content, except that CodeUri: now points to the S3 object.

Let’s take a look at the CLI output:

Uploading to 4b445c195c24d05d8a9eee4cd07f34d0 92702076 / 92702076.0 (100.00%)
Successfully packaged artifacts and wrote output template to file packaged-template.yml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file c:\zz_workspace\tutorials\aws-lambda\sam-templates\packaged-template.yml --stack-name <YOUR STACK NAME>

5.2. Deployment

Now, we can trigger the actual deployment:

$> aws cloudformation deploy --template-file ./sam-templates/packaged-template.yml --stack-name baeldung-sam-stack  --capabilities CAPABILITY_IAM

As our stack also needs IAM roles (like the functions’ roles for accessing our DynamoDB table), we must explicitly acknowledge that by specifying the –capabilities parameter.

And the CLI output should look like:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - baeldung-sam-stack

5.3. Deployment Review

After the deployment, we can review the result:

$> aws cloudformation describe-stack-resources --stack-name baeldung-sam-stack

CloudFormation will list all resources, which are part of our stack.

5.4. Test

Finally, we can test our application using any HTTP client.

Let’s see some sample cURL commands we can use for these tests.

StorePersonFunction:

$> curl -X PUT 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons' \
   -H 'content-type: application/json' \
   -d '{"id": 1, "name": "John Doe"}'

GetPersonByPathParamFunction:

$> curl -X GET 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons/1' \
   -H 'content-type: application/json'

GetPersonByQueryParamFunction:

$> curl -X GET 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons?id=1' \
   -H 'content-type: application/json'

5.5. Clean Up

In the end, we can clean up by removing the stack and all included resources:

aws cloudformation delete-stack --stack-name baeldung-sam-stack

6. Conclusion

In this article, we had a look at the AWS Serverless Application Model (SAM), which enables a template-based description and automated deployment of serverless applications on AWS.

In detail, we discussed the following topics:

  • Basics of the Serverless Application Model (SAM), as well as the underlying CloudFormation
  • Definition of a Serverless Application, using the SAM template syntax
  • Automated deployment of the application, using the CloudFormation CLI

As usual, all the code for this article is available over on GitHub.