AWS News Blog
New – AWS SAM Local (Beta) – Build and Test Serverless Applications Locally
|
Update (January 25, 2023) – Remove reference to non-existent S3 bucket.
Today we’re releasing a beta of a new tool, SAM Local, that makes it easy to build and test your serverless applications locally. In this post we’ll use SAM local to build, debug, and deploy a quick application that allows us to vote on tabs or spaces by curling an endpoint. AWS introduced Serverless Application Model (SAM) last year to make it easier for developers to deploy serverless applications. If you’re not already familiar with SAM my colleague Orr wrote a great post on how to use SAM that you can read in about 5 minutes. At it’s core, SAM is a powerful open source specification built on AWS CloudFormation that makes it easy to keep your serverless infrastructure as code – and they have the cutest mascot.
SAM Local takes all the good parts of SAM and brings them to your local machine.
- It lets you develop and test your AWS Lambda functions locally with
sam local
and Docker. - It lets you simulate function invocations from known event sources like Amazon Simple Storage Service (Amazon S3), Amazon DynamoDB, Amazon Kinesis, Amazon Simple Notification Service (Amazon SNS), and more.
- It lets you start a local Amazon API Gateway from a SAM template, and quickly iterate on your functions with hot-reloading.
- It lets you quickly validate your SAM template and even integrate that validation with linters or IDEs
- It provides interactive debugging support for your Lambda functions
There are a couple of ways to install SAM Local but the easiest is through NPM. A quick npm install -g aws-sam-local
should get us going but if you want the latest version you can always install straight from the source: go get github.com/awslabs/aws-sam-local
(this will create a binary named aws-sam-local, not sam).
I like to vote on things so let’s write a quick SAM application to vote on Spaces versus Tabs. We’ll use a very simple, but powerful, architecture of API Gateway fronting a Lambda function and we’ll store our results in DynamoDB. In the end a user should be able to curl our API curl https://SOMEURL/ -d '{"vote": "spaces"}'
and get back the number of votes.
Let’s start by writing a simple SAM template.yaml:
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
VotesTable:
Type: "AWS::Serverless::SimpleTable"
VoteSpacesTabs:
Type: "AWS::Serverless::Function"
Properties:
Runtime: python3.6
Handler: lambda_function.lambda_handler
Policies: AmazonDynamoDBFullAccess
Environment:
Variables:
TABLE_NAME: !Ref VotesTable
Events:
Vote:
Type: Api
Properties:
Path: /
Method: post
So we create a DynamoDB table that we expose to our Lambda function through an environment variable called TABLE_NAME
.
To test that this template is valid I’ll go ahead and call sam validate
to make sure I haven’t fat-fingered anything. It returns Valid!
so let’s go ahead and get to work on our Lambda function.
import os
import os
import json
import boto3
votes_table = boto3.resource('dynamodb').Table(os.getenv('TABLE_NAME'))
def lambda_handler(event, context):
print(event)
if event['httpMethod'] == 'GET':
resp = votes_table.scan()
return {'body': json.dumps({item['id']: int(item['votes']) for item in resp['Items']})}
elif event['httpMethod'] == 'POST':
try:
body = json.loads(event['body'])
except:
return {'statusCode': 400, 'body': 'malformed json input'}
if 'vote' not in body:
return {'statusCode': 400, 'body': 'missing vote in request body'}
if body['vote'] not in ['spaces', 'tabs']:
return {'statusCode': 400, 'body': 'vote value must be "spaces" or "tabs"'}
resp = votes_table.update_item(
Key={'id': body['vote']},
UpdateExpression='ADD votes :incr',
ExpressionAttributeValues={':incr': 1},
ReturnValues='ALL_NEW'
)
return {'body': "{} now has {} votes".format(body['vote'], resp['Attributes']['votes'])}
So let’s test this locally. I’ll need to create a real DynamoDB database to talk to and I’ll need to provide the name of that database through the enviornment variable TABLE_NAME
. I could do that with an env.json
file or I can just pass it on the command line. First, I can call:
$ echo '{"httpMethod": "POST", "body": "{\"vote\": \"spaces\"}"}' |\
TABLE_NAME="vote-spaces-tabs" sam local invoke "VoteSpacesTabs"
to test the Lambda – it returns the number of votes for spaces so theoritically everything is working. Typing all of that out is a pain so I could generate a sample event with sam local generate-event api
and pass that in to the local invocation. Far easier than all of that is just running our API locally. Let’s do that: sam local start-api
. Now I can curl my local endpoints to test everything out.
I’ll run the command: $ curl -d '{"vote": "tabs"}' http://127.0.0.1:3000/
and it returns: “tabs now has 12 votes”. Now, of course I did not write this function perfectly on my first try. I edited and saved several times. One of the benefits of hot-reloading is that as I change the function I don’t have to do any additional work to test the new function. This makes iterative development vastly easier.
Let’s say we don’t want to deal with accessing a real DynamoDB database over the network though. What are our options? Well we can download DynamoDB Local and launch it with java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb
. Then we can have our Lambda function use the AWS_SAM_LOCAL
environment variable to make some decisions about how to behave. Let’s modify our function a bit:
import os
import json
import boto3
if os.getenv("AWS_SAM_LOCAL"):
votes_table = boto3.resource(
'dynamodb',
endpoint_url="http://docker.for.mac.localhost:8000/"
).Table("spaces-tabs-votes")
else:
votes_table = boto3.resource('dynamodb').Table(os.getenv('TABLE_NAME'))
Now we’re using a local endpoint to connect to our local database which makes working without wifi a little easier.
**UPDATE 2018-08-27**: The DynamoDB team released a docker container for running DynamoDB local so this is even easier than what I described above. I simply run docker run -p 8000:8000 amazon/dynamodb-local
rather than the JAR.
SAM local even supports interactive debugging! In Java and Node.js I can just pass the -d
flag and a port to immediately enable the debugger. For Python I could use a library like import epdb; epdb.serve()
and connect that way. Then we can call sam local invoke -d 8080 "VoteSpacesTabs"
and our function will pause execution waiting for you to step through with the debugger.
Alright, I think we’ve got everything working so let’s deploy this!
First I’ll call the sam package
command which is just an alias for aws cloudformation package
and then I’ll use the result of that command to sam deploy
.
$ sam package --template-file template.yaml --s3-bucket MYAWESOMEBUCKET --output-template-file package.yaml
Uploading to 144e47a4a08f8338faae894afe7563c3 90570 / 90570.0 (100.00%)
Successfully packaged artifacts and wrote output template to file package.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file package.yaml --stack-name
$ sam deploy --template-file package.yaml --stack-name VoteForSpaces --capabilities CAPABILITY_IAM
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - VoteForSpaces
Which brings us to our API:
.
I’m going to hop over into the production stage and add some rate limiting in case you guys start voting a lot – but otherwise we’ve taken our local work and deployed it to the cloud without much effort at all. I always enjoy it when things work on the first deploy!
Here’s what it looks like:
We hope that SAM Local makes it easier for you to test, debug, and deploy your serverless apps. We have a CONTRIBUTING.md guide and we welcome pull requests. Please tweet at us to let us know what cool things you build. You can see our What’s New post here and the documentation is live here.
– Randall