The AWS Serverless Application Model (SAM) is an exciting technology stack to start building applications upon. You can write and deploy large web and/or event-based apps and services without worrying about up-front costs, scalability, and infrastructure.
SAM apps are defined by a YAML template that you deploy using AWS CloudFormation. While basic applications are easy to define, things become more complex as you begin to add in more advanced features like Authorizers and Binary data support (more on those later).
In this post, I’m going to walk through a simple Serverless Hello World app.
In a later post, we will then extend this to include token authentication and serving image files.
You can learn more about SAM here at the project’s GitHub page.
A Basic SAM App
My approach to serverless applications is to contain all of the templates and function code within a single repository. The SAM template remains at the top level with each Lambda function within it’s own sub-directory.
Here is the beginning structure of our Hello World app’s repository:
First, let’s look at the contents of our template.yaml file:
The SAM template does a lot of behind-the-scenes magic for us when we go to deploy with CloudFormation. Note the Transform key at the top with the AWS::Serverless-2016-10-31 value. This is an instruction that tells CloudFormation to take the template content and translate it into native CloudFormation. Our templates would be much, much larger and more complex if we had to write them in native CloudFormation objects.
Moving on to Resources, this is where we define the Lambda functions that will perform the work for our app. There one function for our app, HelloWorldFunc (the key is just a label for the resource), with the Type is AWS::Serverless::Function.
Under the Properties of our function we further define it and how it is invoked. The CodeUri key is the path to the code that makes up the function. In this case it is a relative path to our /HelloWorldFunc/ directory (it must be a directory and not a file).
The Handler is two parts: filename.function. Within /HelloWorldFunc/ we have a single Python file: lambda_function.py. In that file is the function that will be executed when the file is run in Lambda: lambda_handler().
You don’t need to name your files and functions this way. Your handler value just needs to point to the right file and function within that file.
Set the Runtime value to the language you are using for the Lambda function. In this case it is python3.6, but Lambda supports a number of languages, and because they are all independent you can mix-and-match across multiple functions (e.g. one can be Python and the other nodeJS).
Events define how the function is invoked. There are a lot of different types of events you can use as a part of the SAM spec, but here we have defined an API operation. GetApi is the label for this event (you can define as many events as you want), the Type is Api which will be through API Gateway, and for the API’s properties the Path is “/” (or the root of the domain) and the Method is get (standard HTTP methods).
Nothing else needs to be defined. I’ll explain why in the “Package & Deploy” section.
Now, let’s look at the code in /HelloWorldFunc/lambda_function.py:
It’s not a straightforward print(“Hello World”) as you might have been expecting. The integration between API Gateway and Lambda uses what’s called Lambda Proxy. When the API Gateway invokes the Lambda function it expects back a dictionary object that provides what the HTTP response to the initiating client should be.
- isBase64Encoded is a boolean flag to tell API Gateway if your response body is a bas64 encoded binary blob. You’ll set this to True if you’re returning a file that will need to be base64 encoded into the body.
- statusCode is the HTTP code to responde with. 2xx for success, 4xx for an error due to the client request.
- body is the content you are returning. It must be a string and not any other data type. If you’re returning binary content (e.g. a file), you will need to base63 encode it, put that string into this body key and set isBase64Encoded to True.
- headers must be set here with at least the Content-Type of the response body. You can also set whatever other headers you want.
Package and Deploy
Using the AWS CLI, we can easily package our Lambda functions, upload them to S3, update the template, and deploy using CloudFormation in two steps.
First, we will need to run the package command from the repository’s directory:
- –template-file is the filename of the SAM template.
- –s3-bucket is the name of an S3 bucket you have access and can upload files to.
- –output-template-file is a filename you provide for an updated SAM template that will be generated by this command. If you don’t specify this key, the template data will be sent to STDOUT.
Packaging converts the code for the Lambda functions into zip files that CloudFormation will use when creating the AWS objects. In the template example above, the CodeUri key is a directory path. Everything within that directory will be put into the zipped artifact for the function and uploaded to the specified S3 bucket.
The CodeUri must be either a directory path or an S3 location. In a later post, I’ll talk about deployment workflows where you may need to handle the packaging on your own and then update your template.
Our newly created deployment.yaml file will contain the S3 locations of the zipped artifacts for each function in your SAM template in their CodeUri key – replacing the directory paths in the source template.
We can now deploy our packaged serverless application:
- –template-file is the filename of our deployment template.
- –stack-name is the name we are going to give to the CloudFormation stack that will be created.
- –capabilities CAPABILITY_IAM is granting CloudFormation permissions to generate IAM roles and permissions.
Whenever you execute a CloudFormation template (SAM is an extension to CloudFormation) you are creating a stack. The stack is the deployed application according to the template for the given name.
The first time you run this command, CloudFormation is going to create a brand new stack and all of the resources you’ve defined. The next time you run your template and specify this stack name it will update all of the stack’s resources according to whatever changes you have defined from the last run to the current run. This include the creation, updating, and deletion of resources. CloudFormation handles it all for you.
We can give a different name for the stack and run the template again to create a second running instance of our serverless application. This is an important feature of using CloudFormation: you can re-use the same template multiple times for completely different stacks. This means you can deploy to an entire test/staging environment before deploying to your production environment using the same template.
You should see the following output as the stack is created:
You can log into your AWS console and navigate to the CloudFormation page (be sure you’re in the right region!) to see everything that has been created for this stack:
Click on Resources tab and you will be able to navigate to the page for each resources that was created as a part of the stack. Go to the API Gateway, click on Stages and the Prod stage. You’ll be presented with the default generated URL.
Go there and you should see:
And there we have the Serverless Hello World app.
There is, of course, so much more that can be done through the Serverless Application Model, and many more AWS services to tie into!
The next article I write will cover extending our Hello World app with a Swagger definition to lock it behind basic token authentication. From there, we will continue to build upon that swagger definition to enable binary data support and serve images from an S3 bucket!
You can find the templates and code shown in this post here on my GitHub: