AWS SAM and Ruby

jameshamann

James Hamann

Posted on September 10, 2019

AWS SAM and Ruby

AWS SAM stands for Serverless Application Model and serves as a framework to build serverless apps on AWS. This is a collection of Lambda functions and other AWS resources that come together to form an app.

Whilst you are able to upload a lambda function and use the lambda console exclusively for simple apps, SAM offers a standardised template and process for developing, building and deploying apps.

I moved away from using just Lambda when I found myself having to bundle and build my ruby function in a docker container using the same image as Lambda uses to ensure it was compatible. It proved a pain as the complexity of the project grew. SAM offered a simpler build and deployment solution, as well as providing a CLI which helped with developing locally.

Requirements

  • AWS Account
  • AWS CLI
  • Basic experience with the command line
  • Basic experience with ruby
  • Docker
  • Postman (Not strictly required but helps speed up development)

Getting started

First of all, we need to install AWS SAM CLI. I’ll be using Homebrew, a package manager for macOS and linux, if you’re using windows head over here for other instructions.

#bash
$ brew install aws/tap
[...]
$ brew install

Once installed, we can use the SAM CLI to initialise our project.

#bash
$ sam init — runtime ruby2.5 — name aws-sam-ruby
[+] Initializing project structure...
Project generated: ./aws-sam-ruby
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 aws-sam-ruby/README.md for further instructions
[*] Project initialization is now complete
This would be a good time to commit and push your project, assuming you’ve got a repo setup. It’s always a good idea to commit regularly, with meaningful messages. It helps provide a history of your project and will prove useful later down the line.
#bash 
$ git add .
$ git commit -m 'initialises aws sam ruby project using sam cli'
[master (root-commit) 8b7733d] initialises aws sam ruby project using sam cli
 8 files changed, 697 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Gemfile
 create mode 100644 README.md
 create mode 100644 event.json
 create mode 100644 hello_world/Gemfile
 create mode 100644 hello_world/app.rb
 create mode 100644 template.yaml
 create mode 100644 tests/unit/test_handler.rb
$ git push origin master

Explore the project directory, notice how the CLI generated pretty much all the files and folders we need for a simple project. It provides a great foundation following best practices.

# Project Structure
├── Gemfile
├── README.md
├── event.json
├── hello_world
│ ├── Gemfile
│ └── app.rb
├── template.yaml
└── tests
 └── unit
 └── test_handler.rb

Now, using the SAM CLI, we’re able to develop and test our function locally.

#bash 
$ sam local start-api

Once complete, a server will be running at localhost:3000.
This is done using docker, the CLI spins up a docker instance matching the lambda runtime specified in the template.yml file. This ensures your function is being tested on exactly the same version and build of linux that AWS Lambda is currently using. Ensuring the version is correct is especially important when bundling dependancies; if the app isn’t built on the platform it’s being run on, issues arise with compatibility and your function simply won’t work.

Open up Postman and try sending a request to localhost:3000. Nothing right?

Postman Empty Response

That’s because our skeleton app has a few defaults already setup. Open up the template.yml file and have a look.

#template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  aws-sam-ruby
Sample SAM Template for aws-sam-ruby
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: ruby2.5
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get
Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

First thing you’ll notice is the function is called HelloWorld. You’ll also see that we’ve only defined one route, /hello.

Let’s open up our app.rb file and see what we should expect to happen when we visit our route, /hello.

#hello-world/app.rb
# require 'httparty'
require 'json'
def lambda_handler(event:, context:)
  # Sample pure Lambda function
# Parameters
  # ----------
  # event: Hash, required
  #     API Gateway Lambda Proxy Input Format
  #     Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
# context: object, required
  #     Lambda Context runtime methods and attributes
  #     Context doc: https://docs.aws.amazon.com/lambda/latest/dg/ruby-context.html
# Returns
  # ------
  # API Gateway Lambda Proxy Output Format: dict
  #     'statusCode' and 'body' are required
  #     # api-gateway-simple-proxy-for-lambda-output-format
  #     Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
# begin
  #   response = HTTParty.get('http://checkip.amazonaws.com/')
  # rescue HTTParty::Error => error
  #   puts error.inspect
  #   raise error
  # end
{
    statusCode: 200,
    body: {
      message: "Hello World!",
      # location: response.body
    }.to_json
  }
end

It looks big, but it’s mostly comments. The actual method is quite small and, as you’ll notice, we should be expecting to see Hello World! in the response. One thing to note for the future is that the response must always be in the same format and structure as listed above, otherwise some errors can occur when deploying your function.

Let’s give it a go.

Postman Success Response

It works! Now you’re ready to develop and work on your app locally.

Package and Deployment

Once ready, you’ll want to deploy your app. Assuming you’ve used gems, you’ll want to make sure to build your app using a docker container with the lambda-ruby image. This is achieved using one command from the SAM CLI.

#bash 
$ sam build --use-container
2019-06-27 06:37:55 Starting Build inside a container
2019-06-27 06:37:55 Building resource 'HelloWorldFunction'
Fetching lambci/lambda:build-ruby2.5 Docker container image......
2019-06-27 06:37:57 Mounting YOUR_FILE_PATH as /tmp/samcli/source:ro,delegated inside runtime container
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>
Running RubyBundlerBuilder:CopySource
Running RubyBundlerBuilder:RubyBundle
Running RubyBundlerBuilder:RubyBundleDeployment

Now we’ll create an S3 bucket to store our deployment. This is pretty helpful as it acts as a log of all the different app versions and, if something were to go wrong with a deployment, it would be pretty quick and easy to revert to a previous version stored in your S3 bucket.

Creating an S3 Bucket

To create bucket, head over to the AWS console and search for S3. Click Create Bucket, the default settings are fine, just make sure to call your bucket something useful, like the name of your app/function.

Naming your S3 Bucket

With everything setup, we’ll now package our app, upload it to the bucket and then deploy.

#bash 
$ sam package \
    --template-file template.yaml \ # This is our template file
    --output-template-file serverless-output.yaml \ # This is the template file used with our package, SAM CLI creates a new file based on our template.yml.
    --s3-bucket aws-sam--ruby # Where we upload our package
Successfully packaged artifacts and wrote output template to file serverless-output.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /Users/jameshamann/Documents/Development/aws-sam-ruby/serverless-output.yaml --stack-name <YOUR STACK NAME>

You’ll notice once the package command finishes, it gives us the command to run to deploy. You can use either this or sam’s deploy command to complete deployment.

#bash 
$ sam deploy \
    --template-file serverless-output.yaml \ #Template file generated by our package command.
    --stack-name aws-ruby-sam \ #This is the name of the stack we'll be creating. It creates a cloudformation stack, allowing for easy and quick deployment.
    --capabilities CAPABILITY_IAM
Waiting for changeset to be created...
Waiting for stack create/update to complete
Successfully created/updated stack - aws-ruby-sam

Now your app is live! Open up your AWS API Gateway Console and you should see your new API!

API Gateway

Let’s give our API a test. First we need the endpoint, open up the API and click on one of the stages, Prod or Staging is fine. Your endpoint link will be clearly listed at the top of the page.

API Environments

Open up Postman and fire a request through to your URL. Don’t forget to append your path /hello.

Postman Success Response

And there you have it! A fully functioning Ruby serverless application, deployed on AWS following best practices.
Now you can look at expanding your function, or creating a front-end for it using React, Vue or whatever framework you like.

All source code can be found here.

💖 💪 🙅 🚩
jameshamann
James Hamann

Posted on September 10, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

AWS SAM and Ruby
aws AWS SAM and Ruby

September 10, 2019