In Progress
Unit 1, Lesson 1
In Progress

Serverless Ruby with AWS SAM

A lot of introductions to “serverless” computing show you how to put some code in the cloud as a one-off using a web console. But once you start developing serverless code in earnest, you need a testable local dev environment and repeatable deployment automation. In today’s episode you’ll learn how to use the AWS Serverless Application Model to robustly deploy Ruby code to the cloud.

Video transcript & code

So-called “serverless” computing is a paradigm that abstracts away all the usual concerns around provisioning and scaling computing infrastructure. In the best case, serverless lets us focus our attention on the bare essentials of an application service: reacting to a request or event, and computing a response.

In episode 567, guest chef Brittany Martin showed us how to run a Ruby service on AWS Lambda. In that video, we saw how to use the AWS web console to deploy a Ruby service from a zip file.

The web console is a great way to get to know Lambda and get a feel for its capabilities. But clicking around a web UI is not a repeatable or testable workflow. For robust delivery of code, we need something we can automate.

So today, I’m going to show you how to deploy a Ruby service to Lambda using SAM: The AWS Serverless Application Model. SAM provides a set of tools for declaring the configuration of a Lambda-based application, testing it, and deploying it from the command-line. It’s not the only serverless framework out there, but it’s the one with Amazon’s support behind it. So if you’re building on (or thinking of building on) Amazon’s serverless infrastructure, it’s worth learning.

I’m going to make the assumption here that you’ve already installed the base AWS CLI tool, you’ve authenticated with AWS credentials, and you’ve installed the aws-sam-cli. I used pip (the python package installer) to install the sam cli, but there are also some operating-specific packages.

Let’s create a simple dice-roller app for role-playing games.

We start a new SAM application with sam init

The tool asks us what source of template we’d like to use. We want to use one of the AWS-provided quick-start templates.

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

Next it asks us which language runtime we want to build on. We choose Ruby!

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

Next up it asks us for a name. Since we’re going to make an app for rolling dice, let’s call it “roller”.

Project name [sam-app]: roller

Cloning app templates from https://github.com/awslabs/aws-sam-cli-app-templates.git

-----------------------
Generating application:
-----------------------
Name: roller
Runtime: ruby2.7
Dependency Manager: bundler
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./roller/README.md

At this point, SAM starts generating a project for us. It helpfully lets us know the public Github repo it’s using as a template source.

Once it’s done, we can see that it has generated some directories and files.

Of particular note is the file hello_world/app.rb. This is the entry point that SAM generated for our Lambda function.

It contains a plain old Ruby method named lambda_handler, and right now all it does is return a hello message.

Let’s test this out. We can run sam local start-api to start up a local HTTP server that embeds our function.

With this running, we can trigger the function with an HTTP request, and see a response.

Now that we’ve confirmed we can exercise our app, let’s make it do something a little bit more interesting than say “hello”. Let’s rewrite the lambda_handler method to implement rolling dice.

Our new code starts by extracting a dice specification out of the HTTP query parameters. AWS arranges for these parameters to be passed into our handler inside the event object.

We parse the number of dice and how many sides each die should have into integer values.

Then we simulate a roll of the requested size of dice the requested number of times, and sum the result.

Finally, we return a response that informs the client of the dice roll result, as both an explanatory message and a numeric result value.

require 'json'

def lambda_handler(event:, context:)
  dice_spec = event.fetch("queryStringParameters").fetch("dice").to_s
  matches = /(?<dice_count>\d+)d(?<dice_faces>\d+)/.match(dice_spec)
  dice_count = matches["dice_count"]).to_i
  dice_faces = matches["dice_faces"]).to_i
  result = dice_count.times.collect{rand(dice_faces)}.sum
  {
    statusCode: 200,
    body: {
      message: "I rolled #{dice_count} #{dice_faces}-sided dice and got #{result}",
      result: result 
    }.to_json
  }
end

OK, let’s try out our changes.

Let’s ask it to roll two 6-sided dice.

Success! We see the response our code was supposed to generate.

Let’s try some more… say, 3 8-sided dice…

Or 1 100-sided die.

Looking good! Let’s deploy this thing to the world!

Well, except the URL path still says “hello”. Let’s get away from the generated “hello world” naming.

First we’ll rename the hello directory to roller. Technically we could skip this, but it bothers me that the directory for our dice-roller code is called “hello”.

Then we open up the template.yaml file. This file is the central point of configuration in a SAM-based project.

There are a bunch of “hello world”-based names in here that sam init generated. We’re just going to methodically update each of them to dice-rolling terminology, including the various descriptions.

Down at the bottom of the file, one of the sections we updated has to do with an API gateway definition.

Something that’s important to keep in mind about AWS Lambda functions is that, by themselves, they don’t know anything about HTTP requests. A Lambda function reacts to events, and there can be all kinds of events: everything from receiving a message from another service, to processing an Alexa command.

In order to wire up a lambda function to handle incoming HTTP requests from browsers or other clients, we first have to configure an API Gateway, which is a different kind of AWS resource.

And that’s one of the areas that SAM helps with. Instead of us going to a lot of extra work defining an API gateway, SAM uses metadata from our Function definitions to generate an API Gateway definition. And this line gives that generated definition a name and makes it part of the deployment.

If we restart sam local start-api,

we can now hit our endpoint at /roll.

Right, now that we’ve updated our template.yaml, it’s time to build a package for deployment.

We do that with the sam build command.

sam build
Building resource 'RollerFunction'
Running RubyBundlerBuilder:CopySource
Running RubyBundlerBuilder:RubyBundle
Running RubyBundlerBuilder:RubyBundleDeployment

Build Succeeded

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

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

Once that’s done, we can use sam deploy –guided to make our first deployment.

It asks us for a stack name, and we specify the name “roller”. We’ll talk more about what a “stack” means in a moment.

…and for the rest, we accept the defaults.

sam deploy
--guided

Configuring SAM deploy
======================

        Looking for samconfig.toml :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: roller
        AWS Region [us-east-1]:
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]:
        #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]:
        Save arguments to samconfig.toml [Y/n]:

        Looking for resources needed for deployment: Found!

                Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-v7husqqlc48f
                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

        Deploying with following values
        ===============================
        Stack name                 : roller
        Region                     : us-east-1
        Confirm changeset          : False
        Deployment s3 bucket       : aws-sam-cli-managed-default-samclisourcebucket-v7husqqlc48f
        Capabilities               : ["CAPABILITY_IAM"]
        Parameter overrides        : {}

Initiating deployment
=====================
Uploading to roller/1a607798cbe0830340778f954b28e69e  1287 / 1287.0  (100.00%)
Uploading to roller/580492b5a29b8e7e5101fd50e3035214.template  1092 / 1092.0  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
------------------------------------------------------------------------------------------------------------------
Operation                              LogicalResourceId                      ResourceType
------------------------------------------------------------------------------------------------------------------
+ Add                                  RollerFunctionRole                     AWS::IAM::Role
+ Add                                  RollerFunctionRollPermissionProd       AWS::Lambda::Permission
+ Add                                  RollerFunction                         AWS::Lambda::Function
+ Add                                  ServerlessRestApiDeployment8ff75e8f4   AWS::ApiGateway::Deployment
                                       f
+ Add                                  ServerlessRestApiProdStage             AWS::ApiGateway::Stage
+ Add                                  ServerlessRestApi                      AWS::ApiGateway::RestApi
------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:us-east-1:846313516496:changeSet/samcli-deploy1586552607/0257bae4-6cf9-4595-ad86-e6a71d83c7b4


2020-04-10 16:03:34 - Waiting for stack create/update to complete

CloudFormation events from changeset
-----------------------------------------------------------------------------------------------------------------
ResourceStatus               ResourceType                 LogicalResourceId            ResourceStatusReason
-----------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS           AWS::IAM::Role               RollerFunctionRole           Resource creation
                                                                                       Initiated
CREATE_IN_PROGRESS           AWS::IAM::Role               RollerFunctionRole           -
CREATE_COMPLETE              AWS::IAM::Role               RollerFunctionRole           -
CREATE_IN_PROGRESS           AWS::Lambda::Function        RollerFunction               -
CREATE_IN_PROGRESS           AWS::Lambda::Function        RollerFunction               Resource creation
                                                                                       Initiated
CREATE_COMPLETE              AWS::Lambda::Function        RollerFunction               -
CREATE_IN_PROGRESS           AWS::ApiGateway::RestApi     ServerlessRestApi            Resource creation
                                                                                       Initiated
CREATE_IN_PROGRESS           AWS::ApiGateway::RestApi     ServerlessRestApi            -
CREATE_COMPLETE              AWS::ApiGateway::RestApi     ServerlessRestApi            -
CREATE_IN_PROGRESS           AWS::Lambda::Permission      RollerFunctionRollPermissi   Resource creation
                                                          onProd                       Initiated
CREATE_IN_PROGRESS           AWS::ApiGateway::Deploymen   ServerlessRestApiDeploymen   -
                             t                            t8ff75e8f4f
CREATE_IN_PROGRESS           AWS::Lambda::Permission      RollerFunctionRollPermissi   -
                                                          onProd
CREATE_IN_PROGRESS           AWS::ApiGateway::Deploymen   ServerlessRestApiDeploymen   Resource creation
                             t                            t8ff75e8f4f                  Initiated
CREATE_COMPLETE              AWS::ApiGateway::Deploymen   ServerlessRestApiDeploymen   -
                             t                            t8ff75e8f4f
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      RollerFunctionRollPermissi   -
                                                          onProd
CREATE_COMPLETE              AWS::CloudFormation::Stack   roller                       -
-----------------------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------
Outputs
--------------------------------------------------------------------------------------------------------------------
Key                 RollerFunction
Description         Dice Roller Lambda Function ARN
Value               arn:aws:lambda:us-east-1:846313516496:function:roller-RollerFunction-JFVCAK2VVR8B

Key                 RollerFunctionIamRole
Description         Implicit IAM Role created for Dice Roller function
Value               arn:aws:iam::846313516496:role/roller-RollerFunctionRole-WEO880BQL7Z4

Key                 RollerApi
Description         API Gateway endpoint URL for Prod stage for Dice Roller function
Value               https://dvxfbvtvig.execute-api.us-east-1.amazonaws.com/Prod/roll/
--------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - roller in us-east-1

This next bit takes a while, as SAM issues AWS requests to create the various resources that go into a whole Lambda app stack.

What do we mean by “stack” in this context, anyway? Well, AWS has a this service called “CloudFormation” which enables developers to define reusable “stacks” of related resources: for instance, a stack might include a few Lambda functions, a messaging channel to link them, a database, an S3 bucket for static assets, an API gateway, and so on. Behind the scenes, a CloudFormation stack is what SAM is creating for us. In a sense the template.yaml file is really a kind of shorthand for defining Lambda-centric CloudFormation stacks.

OK, now that it’s done deploying we’re back at the command line, and it says that a stack was successfully created.

The sam deploy output has helpfully provided a URL where our code should be live. Let’s copy it.

…and then make a slight correction to it, because at least with the version of SAM I’m using, it doesn’t actually output the URL corresponding to the path configured in the template.yaml. Hopefully they’ll fix this in a future version, or clarify the output.

And then we make a request to it, along with a dice specification for five six-sided dice.

Ta-da! There’s our dice roll, served from the AWS serverless cloud!

What if we wanted to make a change and deploy it again? In that case we’d run sam build, followed by sam deploy, and this time it doesn’t pester us with questions.

So like we did in episode #567, we’ve once again caused Ruby code to run “serverless” with AWS Lambda. This time, though, we have our whole app’s stack defined in a versionable YAML file. We can try out the code locally before pushing it to the cloud. And we have a repeatable, automatable build and deploy procedure that we can integrate into our continuous integration system. Happy hacking!

Responses