In this tutorial I will be demonstrating a Hello-World Python Serverless Application using AWS SAM (Serverless Application Model). SAM is a open-source framework that allows you to build serverless applications on the AWS Cloud.

Serverless might familiar to most, but if you still need to wrap your head around serverless, im my mind I see it as a management construct. Serverless applications still runs on a server (that AWS manages), but you dont manage the server as AWS let you focus less on the server management and more on your applications. AWS manages the servers, patch management, orchestration etc, so all you have to worry about is your application and deploying your application to the serverless framework of your choice.

bekker-clothing-developer-tshirts

Amazon Serverless Application Model (SAM)

SAM allows you to build and deploy serverless applications at a really easy way. SAM generates the template definition and teh code of your language choice that will essentially deploy a cloudformation stack. I have never been a fan of writing cloudformation templates, but SAM makes it really easy defining a blueprint for your application.

Hello World with SAM

Let's use AWS SAM to deploy a "hello world" application in Python that will use the requests library which we will make use of to let our lambda function make a HTTP request to a external source and return the content on the response when we make a GET request to our API Gateway's resource.

Installing SAM

I am using a Mac, so I will be using homebrew to install aws sam, for anything other than Mac, have a look at their documentation. Note that you will also need Docker as a requirement before continuing.

$ brew tap aws/tap
$ brew install aws-sam-cli

Ensure that SAM is installed by checking the version:

$ sam --version
SAM CLI, version 0.18.0

Next I will provision a S3 bucket where we will host our deployment packages:

$ aws s3 mb --region=eu-west-1 s3://ruanbekker-sam-demo
make_bucket: ruanbekker-sam-demo

Initialize a Python Project

Im a python fanatic, so I will be generating a python project:

$ sam init --runtime python3.7
[+] Initializing project structure...

Project generated: ./sam-app

Steps you can take next within the project folder
===================================================
[*] Invoke Function: sam local invoke HelloWorldFunction --event event.json
[*] Start API Gateway locally: sam local start-api

Read sam-app/README.md for further instructions

[*] Project initialization is now complete

Hop onto the generated directory:

$ cd sam-app

In this directory we will have the python code, sample event and requirements file should we want to install external dependencies.

Let's invoke our function locally using docker:

$ sam local invoke HelloWorldFunction --event event.json

2019-07-27 21:29:32 Invoking app.lambda_handler (python3.7)
2019-07-27 21:29:32 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:python3.7 Docker container image......
2019-07-27 21:29:36 Mounting /Users/ruan/workspace/aws-sam-hello-world/sam-app/hello_world as /var/task:ro,delegated inside runtime container
START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72	Duration: 3.74 ms	Billed Duration: 100 ms	Memory Size: 128 MB	Max Memory Used: 22 MB
{"statusCode":200,"body":"{\"message\": \"hello, world!\"}"}

As you can see it suceeded. Let's have a peek through the content that we have in our generated project path.

For our requirements:

$ cat hello_world/requirements.txt
requests

Our python code, in this scenario we will edit our hello world application to make a HTTP request to get the external ip and return it in our response:

$ cat hello_world/app.py
import json
import requests

def lambda_handler(event, context):
    try:
        ipraw = requests.get("http://ip.ruanbekker.com")
        ip = ipraw.text.strip('\r\n')
    except requests.RequestException as e:
        # Send some context about this error to Lambda Logs
        print(e)
        raise e

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello, world!",
            "location": ip
        }),
    }

Serverless Deployment

After we have edit our lambda function, we need to run a build, then we can invoke our function locally:

$ sam build
$ sam local invoke HelloWorldFunction --event event.json

2019-07-27 21:46:43 Invoking app.lambda_handler (python3.7)
2019-07-27 21:46:43 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:python3.7 Docker container image......
2019-07-27 21:46:47 Mounting /Users/ruan/workspace/aws-sam-hello-world/sam-app/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container
START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72	Duration: 318.21 ms	Billed Duration: 400 ms	Memory Size: 128 MB	Max Memory Used: 28 MB
{"statusCode":200,"body":"{\"message\": \"hello, world!\", \"location\": \"155.x.x.x\"}"}

Now that we can see that our application is working locally, let's ship our deployment package to Amazon S3:

$ sam package --output-template packaged.yaml --s3-bucket ruanbekker-sam-demo

Uploading to 1c51ba365f5e21101b5f477462ce576a  558445 / 558445.0  (100.00%)
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/ruan/workspace/aws-sam-hello-world/sam-app/packaged.yaml --stack-name <YOUR STACK NAME>

Now that our deployment package is on S3, we can use SAM to deploy the stack, making use of cloudformation:

$ sam deploy --template-file packaged.yaml --capabilities CAPABILITY_IAM --stack-name aws-sam-getting-started

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - aws-sam-getting-started

Getting Deployment Status

We can get deployment status from Cloudformation, by describing the stack name:

$ aws cloudformation describe-stacks --stack-name aws-sam-getting-started
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:eu-west-1:xxxx:stack/aws-sam-getting-started/xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx",
            "StackName": "aws-sam-getting-started",
            "ChangeSetId": "arn:aws:cloudformation:eu-west-1:xx:changeSet/awscli-cloudformation-package-deploy-xxxxxxxx/xx-xxxx-xxxx-xx-xx",
            "Description": "sam-app\nSample SAM Template for sam-app\n",
            "CreationTime": "2019-07-27T19:53:19.027Z",
            "LastUpdatedTime": "2019-07-27T19:53:24.683Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Outputs": [
                {
                    "OutputKey": "HelloWorldFunctionIamRole",
                    "OutputValue": "arn:aws:iam::xxxxxxxxx:role/aws-sam-getting-started-HelloWorldFunctionRole-1UWL8N0S5XAJG",
                    "Description": "Implicit IAM Role created for Hello World function"
                },
                {
                    "OutputKey": "HelloWorldApi",
                    "OutputValue": "https://xxxxxx.execute-api.eu-west-1.amazonaws.com/Prod/hello/",
                    "Description": "API Gateway endpoint URL for Prod stage for Hello World function"
                },
                {
                    "OutputKey": "HelloWorldFunction",
                    "OutputValue": "arn:aws:lambda:eu-west-1:xxxxxx:function:aws-sam-getting-started-HelloWorldFunction-1DOKOJ2LYU65M",
                    "Description": "Hello World Lambda Function ARN"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}

We can get the API Gateway URL, by redirecting the output to jq:

$ aws cloudformation \
  describe-stacks \
  --stack-name aws-sam-getting-started | jq -r '.Stacks[].Outputs[] | select(.OutputKey == "HelloWorldApi") | .OutputValue'

https://xxxx.execute-api.eu-west-1.amazonaws.com/Prod/hello/

Test the Deployment:

Test the deployment by making a HTTP request to the API Gateway's resource:

$ curl https://xxxx.execute-api.eu-west-1.amazonaws.com/Prod/hello/
{"message": "hello, world!", "location": "63.35.183.90"}

Updating the Code

Let's update the code by introducing a change to return a random name:

$ cat hello_world/app.py
import json
import requests
import random

def get_name():
    names = ["ruan", "james", "frank", "sam"]
    return random.choice(names)

def lambda_handler(event, context):
    try:
        ipraw = requests.get("http://ip.ruanbekker.com")
        ip = ipraw.text.strip('\r\n')
        random_name = get_name()
    except requests.RequestException as e:
        print(e)
        raise e

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello, world!",
            "location": ip,
            "name": random_name
        }),
    }

After the code has been edited, run a build:

$ sam build
2019-07-27 22:10:13 Building resource 'HelloWorldFunction'
2019-07-27 22:10:13 Running PythonPipBuilder:ResolveDependencies
2019-07-27 22:10:14 Running PythonPipBuilder:CopySource

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 <yourbucket>

Test the changes locally:

$ sam local invoke HelloWorldFunction --event event.json

2019-07-27 22:10:32 Invoking app.lambda_handler (python3.7)
2019-07-27 22:10:32 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:python3.7 Docker container image......
2019-07-27 22:10:36 Mounting /Users/ruan/workspace/aws-sam-hello-world/sam-app/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container
START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72	Duration: 318.29 ms	Billed Duration: 400 ms	Memory Size: 128 MB	Max Memory Used: 28 MB

{"statusCode":200,"body":"{\"message\": \"hello, world!\", \"location\": \"155.xx.xx.xx\", \"name\": \"james\"}"}

As we can see the changes works as expected, we can publish the deployment package to S3:

$ sam package --output-template packaged.yaml --s3-bucket ruanbekker-sam-demo

Uploading to f4ae23d85db08ef91b049f316b1f62b9  558515 / 558515.0  (100.00%)
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/ruan/workspace/aws-sam-hello-world/sam-app/packaged.yaml --stack-name <YOUR STACK NAME>

Deploy the stack with the latest deployment package:

$ sam deploy --template-file packaged.yaml --capabilities CAPABILITY_IAM --stack-name aws-sam-getting-started

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - aws-sam-getting-started

After the deployment has completed, make a HTTP request against the API Gateway Endpoint:

$ curl https://xxxx.execute-api.eu-west-1.amazonaws.com/Prod/hello/
{"message": "hello, world!", "location": "18.202.242.52", "name": "frank"}

Extra: Local API

We can also use SAM to start a local API to test our changes, to do that, start the local API:

$ sam local start-api

2019-07-27 22:21:28 Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
2019-07-27 22:21:28 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
2019-07-27 22:21:28  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

As you can see SAM creates a listening port on localhost to accept requests for interacting with our codebase. Let's switch to a new terminal and make a HTTP request:

$ curl http://127.0.0.1:3000/hello
{"message": "hello, world!", "location": "155.xx.xx.xx", "name": "frank"}

When we switch to the terminal where we started the api, we can see the logs:

2019-07-27 22:22:18 Invoking app.lambda_handler (python3.7)
2019-07-27 22:22:18 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:python3.7 Docker container image......
2019-07-27 22:22:22 Mounting /Users/ruan/workspace/aws-sam-hello-world/sam-app/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container
START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72	Duration: 321.09 ms	Billed Duration: 400 ms	Memory Size: 128 MB	Max Memory Used: 29 MB
2019-07-27 22:22:24 No Content-Type given. Defaulting to 'application/json'.
2019-07-27 22:22:24 127.0.0.1 - - [27/Jul/2019 22:22:24] "GET /hello HTTP/1.1" 200 -

Cleanup

Once we are done, we can delete the cloudformation stack to remove all our resources:

$ aws cloudformation delete-stack --stack-name aws-sam-getting-started

Resources

Thank You

If you enjoyed this post and you would like to hear more, check out my website at ruan.dev or follow me on Twitter: @ruanbekker