An Introduction to AWS Serverless Application Model

[*]

In this blog, the AWS Serverless Application Model (AWS SAM) is introduced. You will learn what SAM is and where it can be used for. You will also get your hands dirty, because you will create an AWS SAM project from scratch. Enjoy!

1. Introduction

AWS SAM is, according to the official documentation, a framework that you can use to build serverless applications on AWS . A serverless application consists of several lambda’s and other resources working together. In order to define all of these components, a SAM template is used. This SAM template is used to deploy the application to AWS. During deployment, the SAM template is converted to a CloudFormation template which will actually create the resources defined in the SAM template. An introduction to AWS CloudFormation is given in a previous post. SAM can be seen as a CloudFormation++ where less configuration is needed in the template itself. Eg the Events property in a SAM template will create an API gateway for the Lambda but with less yaml than you would need in a CloudFormation template. You will see for yourself later on in this post. Another big advantage of SAM is that you can easily test it locally on your dev machine.

Enough talking, let’s get started! In a previous post, a Java Lambda with an API gateway was created. You will use the code of that Lambda for deploying the application using SAM. The code for the Lambda is available at GitHub. The resulting SAM template and Lambda used in this blog, is also available at GitHub.

The step-by-step guide is inspired by the AWS Getting Started tutorial. However, in this post you will use a Java Lambda instead of Python and you will see what you need to change to the Lambda created in a previous post in order to let it work with SAM.

A prerequisite is that you have the SAM CLI installed, instructions can be found here.

2. Creating the SAM Application

The easiest way to get started, is to start from an example application.

Choose for AWS Quick Start Template.

Which template source would you like to use?
    1 - AWS Quick Start Templates
    2 - Custom Template Location
Choice: 1

Choose for package type Zip.

What package type would you like to use?
    1 - Zip (artifact is a zip uploaded to S3)  
    2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Choose Java11.

Which runtime would you like to use?
    1 - nodejs14.x
    2 - python3.8
    3 - ruby2.7
    4 - go1.x
    5 - java11
    6 - dotnetcore3.1
    7 - nodejs12.x
    8 - nodejs10.x
    9 - python3.7
    10 - python3.6
    11 - python2.7
    12 - ruby2.5
    13 - java8.al2
    14 - java8
    15 - dotnetcore2.1
Runtime: 5

Choose Maven as build tool.

Which dependency manager would you like to use?
    1 - maven
    2 - gradle
Dependency manager: 1

Choose myawssamplanet as project name.

Project name [sam-app]: myawssamplanet

Choose the Hello World Example: Maven.

Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates
 
AWS quick start application templates:
    1 - Hello World Example: Maven
    2 - EventBridge Hello World: Maven
    3 - EventBridge App from scratch (100+ Event Schemas): Maven
    4 - Step Functions Sample App (Stock Trader): Maven
Template selection: 1

The SAM project is created.

-----------------------
Generating application:
-----------------------
Name: myawssamplanet
Runtime: java11
Dependency Manager: maven
Application Template: hello-world
Output Directory: .
 
Next steps can be found in the README file at ./myawssamplanet/README.md

The generated application has the following contents:

  • events directory: contains an event.json file which can be used to test the lambda locally;
  • HelloWorldFunction: the sample application, you will replace this one;
  • README.md: a readme file containing instructions what you can do with the sample application;
  • template.yaml: this is the SAM template which contains the configuration of the resources that need to be created (similar as a CloudFormation template).

3. Customize SAM Project

In this section, you will customize the generated application in order to use the Java Lambda created in a previous post. Create a directory MyAwsSamFunction next to the HelloWorldFunction and copy the contents of the Java Lambda in this directory. Remove the HelloWorldFunction directory.

Change the template.yaml file in order that the references to the HelloWorldFunction are replaced with references to the MyAwsSamFunction. The resulting file is the following:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  myawssamplanet
 
  Sample SAM Template for myawssamplanet
 
Globals:
  Function:
    Timeout: 20
 
Resources:
  MyAwsSamFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: MyAwsSamFunction
      Handler: com.mydeveloperplanet.myawslambdajavaplanet.LambdaJava::handleRequest
      Runtime: java11
      MemorySize: 512
      Environment:
        Variables:
          PARAM1: VALUE
      Events:
        MyAwsLambdaJavaGet:
          Type: Api
          Properties:
            Path: /myawslambdajava
            Method: get
 
Outputs:
  MyAwsSamApi:
    Description: "API Gateway endpoint URL for Prod stage for LambdaJava function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/myawslambdajava/"
  MyAwsSamFunction:
    Description: "LambdaJava Lambda Function ARN"
    Value: !GetAtt MyAwsSamFunction.Arn
  MyAwsSamFunctionIamRole:
    Description: "Implicit IAM Role created for LambdaJava function"
    Value: !GetAtt MyAwsSamFunctionRole.Arn

In the Resources section, the Lambda is defined and a MyAwsLambdaJavaGet Event of type Api. The latter will create an API Gateway containing a Rest resource where the Lambda can be invoked by means of a GET request.

A complete overview of the different sections is described here.

You also need to change the LambdaJava class. The current implementation makes us of the Lambda Integration type in the API Gateway. However, by default, SAM will make use of the Lambda Proxy Integration type. The difference is that the payload is sent differently from API Gateway to the Lambda function. If you do not change this, an Internal Server error will be sent as a response when you invoke the Rest resource. The current implementation is the following:

public class LambdaJava implements RequestHandler<Map<String, String>, String> {
    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
 
    @Override
    public String handleRequest(Map<String, String> event, Context context) {
        LambdaLogger logger = context.getLogger();
        String response = "Version 1";
        ...
        return response;
    }
}

Instead, you need to implement the handleRequest which takes an APIGatewayProxyRequestEvent object. You also need to replace GSON.toJson(input) with input.getBody() and the response needs be formatted a little bit different. Besides that, also a null-check needs to be added in case the body is empty, otherwise a NullPointerException will be thrown.

public class LambdaJava implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
 
    @Override
    public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
        LambdaLogger logger = context.getLogger();
 
        // log execution details
        logger.log("ENVIRONMENT VARIABLES: " + GSON.toJson(System.getenv()) + System.lineSeparator());
        logger.log("CONTEXT: " + GSON.toJson(context) + System.lineSeparator());
        // process event
        logger.log("EVENT: " + input.getBody() + System.lineSeparator());
        logger.log("EVENT TYPE: " + input.getClass() + System.lineSeparator());
 
        if (input.getBody() != null) {
                // Parse JSON into an object
                Car car = GSON.fromJson(input.getBody(), Car.class);
                logger.log("Car brand: " + car.getBrand() + System.lineSeparator());
                logger.log("Car type: " + car.getType() + System.lineSeparator());
        }
 
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/text");
        headers.put("X-Custom-Header", "application/text");
 
        APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
                .withHeaders(headers);
        return response
                .withStatusCode(200)
                .withBody("Version 1");
 
    }
}

4. Build and Deploy

Now it is time to build and deploy SAM. Navigate to the directory where the template.yaml exists and execute the build command:

$ sam build
Building codeuri: /home/../myawssamplanet/MyAwsSamFunction runtime: java11 metadata: {} architecture: x86_64 functions: ['MyAwsSamFunction']
Running JavaMavenWorkflow:CopySource
Running JavaMavenWorkflow:MavenBuild
Running JavaMavenWorkflow:MavenCopyDependency
Running JavaMavenWorkflow:MavenCopyArtifacts
 
Build Succeeded
 
Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml
 
Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guided

After building the application, you deploy SAM. Choose myawssamplanet as stack name for CloudFormation.

$ sam deploy --guided
 
Configuring SAM deploy
======================
 
    Looking for config file [samconfig.toml] :  Not found
 
    Setting default arguments for 'sam deploy'
    =========================================
    Stack Name [sam-app]: myawssamplanet

Leave the default of the AWS Region and enter y in order to confirm changes before deploy.

AWS Region [eu-west-3]: 
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y

Choose Y to allow role creation.

#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: Y

Do not disable rollback.

#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: N

The API will be publicly available, so choose y.

MyAwsSamFunction may not have authorization defined, Is this okay? [y/N]: y

Save the arguments to a configuration file.

Save arguments to configuration file [Y/n]: Y

Leave the defaults for the configuration file name and for the configuration environment.

SAM configuration file [samconfig.toml]: 
SAM configuration environment [default]:

The CloudFormation stacks are created. Take a look at AWS CloudFormation in the AWS Management console.

Choose y to deploy the application (at the bottom of the output below).

    Looking for resources needed for deployment:
    Creating the required resources...
    Successfully created!
     Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-o08u00g288r9
     A different default S3 bucket can be set in samconfig.toml
 
    Saved arguments to config file
    Running 'sam deploy' for future deployments will use the parameters saved above.
    The above parameters can be changed by modifying samconfig.toml
    Learn more about samconfig.toml syntax at 
    https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
 
Uploading to myawssamplanet/8e80317f02e0baccb04761d59791c6ad  2667728 / 2667728  (100.00%)
 
    Deploying with following values
    ===============================
    Stack name                   : myawssamplanet
    Region                       : eu-west-3
    Confirm changeset            : True
    Disable rollback             : False
    Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-o08u00g288r9
    Capabilities                 : ["CAPABILITY_IAM"]
    Parameter overrides          : {}
    Signing Profiles             : {}
 
Initiating deployment
=====================
Uploading to myawssamplanet/a0f8a035e37699e9be00c92f8d0d10fe.template  1308 / 1308  (100.00%)
 
Waiting for changeset to be created..
 
CloudFormation stack changeset
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                               LogicalResourceId                       ResourceType                            Replacement                           
-------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                   MyAwsSamFunctionMyAwsLambdaJavaGetPer   AWS::Lambda::Permission                 N/A                                  
                                        missionProd                                                                                                           
+ Add                                   MyAwsSamFunctionRole                    AWS::IAM::Role                          N/A                                  
+ Add                                   MyAwsSamFunction                        AWS::Lambda::Function                   N/A                                  
+ Add                                   ServerlessRestApiDeployment4aeac94236   AWS::ApiGateway::Deployment             N/A                                  
+ Add                                   ServerlessRestApiProdStage              AWS::ApiGateway::Stage                  N/A                                  
+ Add                                   ServerlessRestApi                       AWS::ApiGateway::RestApi                N/A                                  
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 
Changeset created successfully. arn:aws:cloudformation:eu-west-3:<account ID>:changeSet/samcli-deploy1643551688/4b3d5e55-adc8-44d7-8011-e8b1e17d9b9f
 
 
Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

All the resources are created by the CloudFormation stack.

2022-01-30 15:11:17 - Waiting for stack create/update to complete
 
CloudFormation events from stack operations
-------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                          ResourceType                            LogicalResourceId                       ResourceStatusReason                  
-------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                      AWS::IAM::Role                          MyAwsSamFunctionRole                    -                                     
CREATE_IN_PROGRESS                      AWS::IAM::Role                          MyAwsSamFunctionRole                    Resource creation Initiated           
CREATE_COMPLETE                         AWS::IAM::Role                          MyAwsSamFunctionRole                    -                                     
CREATE_IN_PROGRESS                      AWS::Lambda::Function                   MyAwsSamFunction                        -                                     
CREATE_IN_PROGRESS                      AWS::Lambda::Function                   MyAwsSamFunction                        Resource creation Initiated           
CREATE_COMPLETE                         AWS::Lambda::Function                   MyAwsSamFunction                        -                                     
CREATE_IN_PROGRESS                      AWS::ApiGateway::RestApi                ServerlessRestApi                       -                                     
CREATE_COMPLETE                         AWS::ApiGateway::RestApi                ServerlessRestApi                       -                                     
CREATE_IN_PROGRESS                      AWS::ApiGateway::RestApi                ServerlessRestApi                       Resource creation Initiated           
CREATE_IN_PROGRESS                      AWS::Lambda::Permission                 MyAwsSamFunctionMyAwsLambdaJavaGetPer   -                                     
                                                                                missionProd                                                                   
CREATE_IN_PROGRESS                      AWS::ApiGateway::Deployment             ServerlessRestApiDeployment4aeac94236   -                                     
CREATE_IN_PROGRESS                      AWS::Lambda::Permission                 MyAwsSamFunctionMyAwsLambdaJavaGetPer   Resource creation Initiated           
                                                                                missionProd                                                                   
CREATE_IN_PROGRESS                      AWS::ApiGateway::Deployment             ServerlessRestApiDeployment4aeac94236   Resource creation Initiated           
CREATE_COMPLETE                         AWS::ApiGateway::Deployment             ServerlessRestApiDeployment4aeac94236   -                                     
CREATE_IN_PROGRESS                      AWS::ApiGateway::Stage                  ServerlessRestApiProdStage              -                                     
CREATE_IN_PROGRESS                      AWS::ApiGateway::Stage                  ServerlessRestApiProdStage              Resource creation Initiated           
CREATE_COMPLETE                         AWS::ApiGateway::Stage                  ServerlessRestApiProdStage              -                                     
CREATE_COMPLETE                         AWS::Lambda::Permission                 MyAwsSamFunctionMyAwsLambdaJavaGetPer   -                                     
                                                                                missionProd                                                                   
CREATE_COMPLETE                         AWS::CloudFormation::Stack              myawssamplanet                          -                                     
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 
CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                     
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 MyAwsSamFunction                                                                                                                        
Description         LambdaJava Lambda Function ARN                                                                                                          
Value               arn:aws:lambda:eu-west-3::function:myawssamplanet-MyAwsSamFunction-H9zkMo3NEWsn                                             
 
Key                 MyAwsSamApi                                                                                                                             
Description         API Gateway endpoint URL for Prod stage for LambdaJava function                                                                        
Value               https://5w9uji7dag.execute-api.eu-west-3.amazonaws.com/Prod/myawslambdajava/                                                           
 
Key                 MyAwsSamFunctionIamRole                                                                                                                 
Description         Implicit IAM Role created for LambdaJava function                                                                                      
Value               arn:aws:iam:::role/myawssamplanet-MyAwsSamFunctionRole-XNX3QIEXXG93                                                        
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 
Successfully created/updated stack - myawssamplanet in eu-west-3

Navigate to the API Gateway service and you notice that the API Gateway is created.

Navigate to the Lambda service and you notice that the Lambda is created.

The public URL for the API Gateway is shown in the console output. Try to access the URL. As expected, ‘Version 1’ is returned.

$ curl https://5w9uji7dag.execute-api.eu-west-3.amazonaws.com/Prod/myawslambdajava/
Version 1

5. Test Locally

One of the main advantages of using SAM, is that you can test it locally. You can test the application including an API Gateway by using the local start-api command.

$ sam local start-api
Mounting MyAwsSamFunction at http://127.0.0.1:3000/myawslambdajava [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2022-01-30 15:35:39  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

Open a new Terminal and try to invoke the API Gateway resource.

$ curl http://127.0.0.1:3000/myawslambdajava
Version 1

It might take some seconds before the response is returned. In the meanwhile, the terminal where you started the API, outputs the serverlogging. Here you notice that a Docker container is started.

It is also possible to invoke the Lambda directly by simulating the event which is normally sent from the API Gateway to the Lambda. This is where the event.json file comes in handy.

$ sam local invoke "MyAwsSamPlanet" -e events/event.json

6. Add Post Request

The Lambda which is used, actually takes a JSON body, converts JSON objects into Java objects and prints it to the logging. Add a MyAwsLambdaPost request to the template.yaml in order to test this behaviour.

Events:
  MyAwsLambdaJavaGet:
    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: /myawslambdajava
      Method: get
  MyAwsLambdaJavaPost:
    Type: Api
    Properties:
      Path: /myawslambdajava
      Method: post

Again, build and deploy SAM. Wait until the deployment has been finished and POST a request.

$ curl -i -X 'POST' 
>    'https://5w9uji7dag.execute-api.eu-west-3.amazonaws.com/Prod/myawslambdajava/' 
>    -H 'accept: application/json' 
>    -H 'Content-Type: application/json' 
>    -d '{
>    "brand": "FORD",
>    "type": "Kuga"
>  }'
HTTP/2 200 
content-type: application/text
content-length: 9
date: Sun, 30 Jan 2022 14:54:22 GMT
x-amzn-requestid: b9855126-b51e-4dc9-abea-97dee77b5168
x-amz-apigw-id: Mw74sG6AiGYFoRg=
x-custom-header: application/text
x-amzn-trace-id: Root=1-61f6a69d-02ca6ba1368061e864cb7d6f;Sampled=0
x-cache: Miss from cloudfront
via: 1.1 415e8d76bf2c69e5e03b89ba8461cd7e.cloudfront.net (CloudFront)
x-amz-cf-pop: AMS50-C1
x-amz-cf-id: q1c-8WZOIqEhllZhGBAFQP8auu54fX7E3B41S2DcZNhr-HxV5JZLSQ==
 
Version 1

Navigate to the Lambda service and navigate to the Monitor tab. From here, you can open the CloudWatch logging for the Lambda. The logging shows you that the event is received in the Lambda and converted to a Java object.

EVENT: 
{
    "brand": "FORD",
    "type": "Kuga"
}
 
EVENT TYPE: class com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
Car brand: FORD
Car type: Kuga

7. Unit Test

For completeness, also a unit test is added to the git repository. It is not further explained in this blog, but the test can be viewed at GitHub. It is important to add the junit dependency to the pom.

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
    <scope>test</scope>
</dependency>

You also need to ensure that the context and the logger are not null. It is not a very beautiful unit test, but it just works 🙂 .

8. Cleanup

As always, it is a good practice to clean up all the resources you have created after you are done experimenting with AWS services. Because CloudFormation is used, it is quite easy to delete all the resources by means of a single command.

$ aws cloudformation delete-stack --stack-name myawssamplanet --region eu-west-3

You can follow the progress in CloudFormation.

SAM also created a S3 bucket, this one is not automatically deleted. Navigate to the S3 service. Select the created bucket and click the Empty button. Follow the instructions and click the Delete button in order to remove the S3 bucket.

Now you can also delete the aws-sam-cli-managed-default stack which was created.

$ aws cloudformation delete-stack --stack-name aws-sam-cli-managed-default --region eu-west-3

9. Conclusion

In this blog, you got an introduction of the Serverless Application Model of AWS. You also got your hands dirty and created by means of a SAM template an API Gateway and Java Lambda. You learn how to build, deploy and test the application remotely and locally.

.

Leave a Comment