AWS SAM (Serverless Application Model)

Following excerpt from AWS Developer site:

The AWS Serverless Application Model (AWS SAM) is an open-source framework that you can use to build serverless applications on AWS.

serverless application is a combination of Lambda functions, event sources, and other resources that work together to perform tasks. Note that a serverless application is more than just a Lambda function—it can include additional resources such as APIs, databases, and event source mappings.

You can use AWS SAM to define your serverless applications. AWS SAM consists of the following components:

  • AWS SAM template specification.You use this specification to define your serverless application. It provides you with a simple and clean syntax to describe the functions, APIs, permissions, configurations, and events that make up a serverless application. You use an AWS SAM template file to operate on a single, deployable, versioned entity that’s your serverless application. For the full AWS SAM template specification, see AWS Serverless Application Model Specification.
  • AWS SAM command line interface (AWS SAM CLI). You use this tool to build serverless applications that are defined by AWS SAM templates. The CLI provides commands that enable you to verify that AWS SAM template files are written according to the specification, invoke Lambda functions locally, step-through debug Lambda functions, package and deploy serverless applications to the AWS Cloud, and so on. For details about how to use the AWS SAM CLI, including the full AWS SAM CLI Command Reference, see AWS SAM CLI.

AWS SAM is a higher-level abstraction of AWS CloudFormation that simplifies serverless application development. AWS SAM template files are AWS CloudFormation template files with a few additional resource types defined that are specific to serverless applications—such as API Gateway endpoints and Lambda functions. This means that AWS SAM supports the full suite of resources, intrinsic functions, and other template features that are available in AWS CloudFormation.

In order to use AWS SAM it must be installed on the client. This can be done here:
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html

 

SAM Templates

SAM is driven by templates. The format of an AWS SAM template closely follows the format of an AWS CloudFormation template, which is described in Template Anatomy in the AWS CloudFormation User Guide.

The primary differences between AWS SAM templates and AWS CloudFormation templates are the following:

  • Transform declaration. The declaration Transform: AWS::Serverless-2016-10-31 is required for AWS SAM templates. This declaration identifies an AWS CloudFormation template as an AWS SAM template. For more information about transforms, see Transform in the AWS CloudFormation User Guide.
  • Globals section. The Globals section is unique to AWS SAM. It defines properties that are common to all your serverless functions and APIs. All the AWS::Serverless::Function, AWS::Serverless::Api, and AWS::Serverless::SimpleTable resources inherit the properties that are defined in the Globals section. For more information about the Globals section, see Globals Section of the Template in the AWS Serverless Application Model Developer Guide.
  • Resources section. In AWS SAM templates the Resources section can contain a combination of AWS CloudFormation resources and AWS SAM resources. For more information about AWS CloudFormation resources, see AWS Resource and Property Types Reference in the AWS CloudFormation User Guide. For more information about AWS SAM resources see AWS SAM Resource and Property Reference in the AWS Serverless Application Model Developer Guide.

Sample template format shown below (YAML).

Transform: AWS::Serverless-2016-10-31

Globals:
  set of globals

Description:
  String

Metadata:
  template metadata

Parameters:
  set of parameters

Mappings:
  set of mappings

Conditions:
  set of conditions

Resources:
  set of resources

Outputs:
  set of outputs

 

 

 

Quick Command Reference

 

Sample commands:

jlee:$ aws s3 mb s3://samtest
make_bucket: samtest

jlee$ sam build
2019-04-19 10:28:16 Building resource 'samtest'
2019-04-19 10:28:16 Running NodejsNpmBuilder:NpmPack
2019-04-19 10:28:16 Running NodejsNpmBuilder:CopyNpmrc
2019-04-19 10:28:16 Running NodejsNpmBuilder:CopySource
2019-04-19 10:28:16 Running NodejsNpmBuilder:NpmInstall
2019-04-19 10:28:18 Running NodejsNpmBuilder:CleanUpNpmrc

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Package: sam package --s3-bucket 
    
jlee$ sam package --template-file template.yaml --output-template-file packaged.yaml --s3-bucket samtest

Successfully packaged artifacts and wrote output template to file packaged.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /Users/jlee/packaged.yaml --stack-name 

jlee$ sam deploy --template-file packaged.yaml --capabilities CAPABILITY_IAM --stack-name samtest

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

Some other things to note

 

Sample Project

This is a small SAM app with a front end SPA (single page app) powered by Angular, and backend using API Gateway and Lambda. Both sides use SAM for build and deployment. The app allows users to view EC2 instances and either shut them down or turn them on. For simplicity, some things have been hard coded – such as regions and instance types (using tags).

The backend has two api endpoints, one for getting a list of EC2 instances. This is the ec2monitorGet app.

let response;

exports.lambdaHandler = async (event, context) => {
    let AWS = require('aws-sdk');
    AWS.config.region = 'us-west-2';
    let ec2 = new AWS.EC2({ apiVersion: '2016-11-15' });

    response = {
        'statusCode': 200
    };

    try {
        if (event.httpMethod && event.httpMethod == 'GET') {
            let params = {
                Filters: [
                    {
                        Name: "tag:autoshutdown",
                        Values: [
                            "true"
                        ]
                    }
                ]
            };

            let data = await ec2.describeInstances(params).promise();
            let instances = [];

            if (data.Reservations.length > 0) {
                for (r in data.Reservations) {
                    if (data.Reservations[r].Instances.length > 0) {
                        for (i in data.Reservations[r].Instances) {
                            let nametag = data.Reservations[r].Instances[i].Tags.filter(function (tag) {
                                return tag.Key == 'Name';
                            });
                            let instance = {
                                'name': nametag.length > 0 ? nametag[0].Value : 'Unknown',
                                'instanceid': data.Reservations[r].Instances[i].InstanceId,
                                'state': data.Reservations[r].Instances[i].State.Name
                            };
                            instances.push(instance);
                        }
                    }
                }
            }
            
            let body = {
                message: instances.length + ' instances found',
                instances: instances
            }
            response['body'] = JSON.stringify(body);
        } else {
            response['body'] = "Invalid HTTP Method";
        }
    } catch (err) {
        console.log(err);
        return err;
    }

    return response;
};

 

The second api is for posting commands, to either shut down or start up an instance. This is the ec2monitorPost app.

let response;

exports.lambdaHandler = async (event, context) => {
    let AWS = require('aws-sdk');
    AWS.config.region = 'us-west-2';
    let ec2 = new AWS.EC2({ apiVersion: '2016-11-15' });

    response = {
        'statusCode': 200
    };

    try {
        if (event.httpMethod && event.httpMethod == 'POST') {
            let body = JSON.parse(event.body);
            let params = {
                Filters: [
                    {
                        Name: "tag:autoshutdown",
                        Values: [
                            "true"
                        ]
                    }
                ]
            };

            let data = await ec2.describeInstances(params).promise();

            if (data.Reservations.length > 0) {
                for (r in data.Reservations) {
                    if (data.Reservations[r].Instances.length > 0) {
                        for (i in data.Reservations[r].Instances) {
                            // TODO
                            if (body.action == 'stop') {
                                response['body'] = 'Stopping EC2 Instance: ' + body.instanceid;
                            } else if (body.action == 'start') {
                                response['body'] = 'Starting EC2 Instance: ' + body.instanceid;
                            } else {
                                response['body'] = 'Error - no action given for: ' + body.instanceid;
                            }
                        }
                    }
                }
            }
        } else {
            response['body'] = "Invalid HTTP Method";
        }
    } catch (err) {
        console.log(err);
        return err;
    }

    return response;
};

 

Note that for the code examples above I’m using Nodejs v8. In this version we have the new async/await abilities. The AWSSDK works with this version by offering ‘promise()’ methods we can call for each of the APIs. This new feature makes coding a bit cleaner without having to write out callback functions. Also, this gives us simplified try/catch statement usage for handling any of the errors that may come up during the API calls.

Another thing to note is regarding global variables in the template file. We can set globals for things like lambda function timeout, which by default is 3 seconds but since in this example we’re dealing with EC2 instructions I’ve increased it to 10 seconds. Other things we can configure are API Gateway CORS. Below is example of the template file used by SAM.

BBBBBBBBB

 

As shown in the previous section, I use the SAM CLI and run the build, package and deploy commands to get the backend deployed. Once the backend is deployed, I can deploy the frontend. For this example app my frontend is an Angular app that will be deployed on S3 as a static website. I use the AWS CLI to setup the bucket and deploy the front end as shown below.

 

jlee:$ aws s3 mb s3://samtest-web

jlee:$ aws s3 cp samtest/ s3://samtest-web --recursive --exclude 'node_modules/*'
upload: samtest/favicon.ico to s3://samtest-web/favicon.ico
upload: samtest/index.html to s3://samtest-web/index.html
upload: samtest/runtime.js to s3://samtest-web/runtime.js
upload: samtest/main.js.map to s3://samtest-web/main.js.map
upload: samtest/runtime.js.map to s3://samtest-web/runtime.js.map
upload: samtest/es2015-polyfills.js.map to s3://samtest-web/es2015-polyfills.js.map
upload: samtest/polyfills.js.map to s3://samtest-web/polyfills.js.map
upload: samtest/es2015-polyfills.js to s3://samtest-web/es2015-polyfills.js
upload: samtest/styles.js to s3://samtest-web/styles.js
upload: samtest/polyfills.js to s3://samtest-web/polyfills.js
upload: samtest/main.js to s3://samtest-web/main.js 
upload: samtest/styles.js.map to s3://samtest-web/styles.js.map
upload: samtest/vendor.js.map to s3://samtest-web/vendor.js.map
upload: samtest/vendor.js to s3://samtest-web/vendor.js

jlee:$ aws s3 ls s3://samtest-web
2019-04-23 10:18:47     290826 es2015-polyfills.js
2019-04-23 10:18:47     211178 es2015-polyfills.js.map
2019-04-23 10:18:47       5430 favicon.ico
2019-04-23 10:18:47        743 index.html
2019-04-23 10:18:47      20127 main.js
2019-04-23 10:18:47      14017 main.js.map
2019-04-23 10:18:47     241644 polyfills.js
2019-04-23 10:18:47     240220 polyfills.js.map
2019-04-23 10:18:47       6224 runtime.js
2019-04-23 10:18:47       6214 runtime.js.map
2019-04-23 10:18:47     183880 styles.js
2019-04-23 10:18:47     196554 styles.js.map
2019-04-23 10:18:47    7095571 vendor.js
2019-04-23 10:18:47    7432815 vendor.js.map

jlee:$ aws s3 website s3://samtest-web/ --index-document index.html --error-document index.html

jlee:$ aws s3api put-bucket-policy --bucket samtest-web --policy file://s3policy.json

jlee:$ ng build --prod                                                 
Date: 2019-04-23T23:54:33.813Z
Hash: 1aaaab5c8973af42f3fe
Time: 32122ms
chunk {0} runtime.26209474bfa8dc87a77c.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.c5dd28b362270c767b34.js (es2015-polyfills) 56.4 kB [initial] [rendered]
chunk {2} main.71f1b500ef528a11da6b.js (main) 526 kB [initial] [rendered]
chunk {3} polyfills.8bbb231b43165d65d357.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.7775c3807c26fe8ac360.css (styles) 61.2 kB [initial] [rendered]

jlee$ aws s3 cp dist/samtest/ s3://samtest-web --recursive --exclude 'node_modules/*'
upload: dist/samtest/runtime.26209474bfa8dc87a77c.js to s3://samtest-web/runtime.26209474bfa8dc87a77c.js
upload: dist/samtest/index.html to s3://samtest-web/index.html
upload: dist/samtest/favicon.ico to s3://samtest-web/favicon.ico
upload: dist/samtest/polyfills.8bbb231b43165d65d357.js to s3://samtest-web/polyfills.8bbb231b43165d65d357.js
upload: dist/samtest/3rdpartylicenses.txt to s3://samtest-web/3rdpartylicenses.txt
upload: dist/samtest/main.71f1b500ef528a11da6b.js to s3://samtest-web/main.71f1b500ef528a11da6b.js
upload: dist/samtest/es2015-polyfills.c5dd28b362270c767b34.js to s3://samtest-web/es2015-polyfills.c5dd28b362270c767b34.js
upload: dist/samtest/styles.7775c3807c26fe8ac360.css to s3://samtest-web/styles.7775c3807c26fe8ac360.css

I can take all the above steps and write them into a script. I can then use this script as part of my CI/CD to automate the deployment process for both frontend and backend.

 

References

AWS SAM
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html

AWSLABS SAM Repository
https://github.com/awslabs/serverless-application-model

Using SAM and the SAM Repositories
https://docs.aws.amazon.com/serverlessrepo/latest/devguide/using-aws-sam.html

NodeJS v8 with AWSSDKs?
https://serverless.com/blog/common-node8-mistakes-in-lambda/

Example lessons on SAM
https://github.com/lucpod/ticketless/tree/master/lessons

Sample Lambda Function shutting down EC2s
https://github.com/geekmuse/iot-button-ec2-controller/blob/master/index.js