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::Api, AWS::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.