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