Serverless API in Ruby with AWS Lambda
Motivation
Today I’ll introduce you to a recipe that enables to create a simple API in Ruby and publish it to the cloud, and all of that in less than an hour. The cool thing is that to put this recipe to use, you don’t need years of experience in DevOps and Network Administration. Besides its simplicity and ease of use, the deployment process of this approach is very cost-efficient. The serverless nature of AWS Lambda allows the API’s author to pay only when said API is being used, without having to waste money on idle servers when it isn’t.
This recipe can prove quite useful when you want to quickly spin up a prototype of a web-service you need for work. Or if you wish to implement something that other teams and companies find useful, and you can sell it through Rapid API or other similar platforms.
Prerequisites
- Linux or macOS machine with GNU Make
- Ruby 2.7 and some familiarity with the language
- AWS account and AWS Command Line Tool
The API Contract
The API that we’ll create today is a simple web service for vote counting. It will take requests containing a sequence of votes and count them, responding back and deducting whether there’s a winner by absolute majority.
Example:
$ curl -X POST \
-H ’Content-Type: application/json’ \
-d ’{“votes”: [“Alice”, “Bob”, “Carol”, “Bob”, “Bob”]}’ \
https://myapi.com{“result”: “Bob wins!”}$ curl -X POST \
-H ’Content-Type: application/json’ \
-d ’{"votes": ["Alice", "Bob", "Alice", "Carol"]}’ \
https://myapi.com{"result": "No winner. Vote again."}
Hello World
This section will describe the minimum infrastructure necessary for running a Ruby function in AWS to handle HTTP requests.
The first step is to create three files like in the example below:
.
|-- Makefile
|-- stack.yml
|-- vote_counter
|-- handler.rb
handler.rb
The file called vote_counter/handler.rb
will be processing the incoming HTTP requests and responding with a plaintext Hello World! message for now. The code inside it should look like this:
def main(event:, context:)
{
body: "Hello World!\n",
headers: { 'Content-Type' => 'text/plain' },
statusCode: 200
}
end
stack.yml
stack.yml
is a CloudFormation template that will describe every one of the necessary cloud resources in code. Infrastructure as Code (IaC) makes configuring and provisioning your hosting environment easier, faster, and safer by reducing the human error factor. The code below describes the architecture where an AWS API Gateway registers HTTP requests, proxies them to the Ruby Lambda function, and uses the result to respond back to the client. It also describes the permission configurations necessary for the API Gateway to be able to invoke the function as well as logging configurations.
Additionally, AWS CloudFormation can produce output values like the ApiUrl at the end of stack.yml
. Later by reading that output value we’ll be able to discover the URL of our API.
Makefile
Makefile is used to describe the recipes and commands needed to build the function package and deploy it to the cloud.
Notice how the stack
target doesn’t use the stack.yml
file directly. Instead it declares packaged-stack.yml
target as its dependency and deploys the packaged template to the cloud. AWS CloudFormation packaging is an extremely convenient way of referencing files from your local machine in the template that automatically gets compressed, uploaded to S3, and correctly referenced in the cloud.
Example:
Deploy
At this point everything should be ready to finally deploy the Hello World API powered by AWS Lambda. To do that, simply run the following command:
$ make stack
GNU make should automatically resolve the packaged-stack.yml
and artifact-bucket
dependencies. When the command finishes you can discover the URL of the API by running the following:
$ make api-urlhttps://9hj3hvk5o8.execute-api.eu-central-1.amazonaws.com/v1
Try to invoke the API URL using a tool like Postman, httpie, or good old Curl:
$ curl -X POST $(make api-url)Hello World!
Congrats! We have just set up our API infrastructure.
The majority vote counter
Now that we have all the necessary components wired together correctly, we can replace the stub ”Hello World!” implementation with a real vote counter. A good way to do that is to start with a test. Create a file called vote_counter/handler_test.rb
containing the following code:
The test scenarios above create artificial event objects similar to the ones that the AWS API Gateway will send to the lambda function. Some of them simulate empty requests to test if the function can handle them without crashing. Other scenarios have non-empty inputs where there can be, although not in every case, a winner by majority.
Next describe how to run the test in the Makefile
:
test:
ruby vote_counter/handler_test.rb
Run the tests with make test and see them fail. The failures are expected because the only thing that the function can do is to respond with Hello World!
And now is the moment to fix that. Replace the contents of vote_counter/handler.rb
with the code below:
Try running make test
now. It should pass.
Let’s deploy the function to the cloud by running make stack
again. When the command finishes, AWS CloudFormation will have packaged the new revision of the function, uploaded it to S3, and updated the AWS Lambda resource to use the new code location. You can now call the API over HTTP passing it sequences of votes:
$ curl -X POST \
-H ’Content-Type: application/json’ \
-d ’{"votes": ["Alice", "Bob", "Carol", "Bob", "Bob"]}’ \
$(make api-url){"result": "Bob wins!"}$ curl -X POST \
-H ’Content-Type: application/json’ \
-d ’{"votes": ["Alice", "Bob", "Alice", "Carol"]}’ \
$(make api-url){"result": "No winner. Vote again."}
Adding RubyGems dependencies
So far we’ve only used the abilities of the Ruby standard library to run the program, but in real life application, it’s highly likely that we’ll need to reuse some of the open-source libraries in a typical Ruby API. One of the most used Ruby gems is ActiveSupport, which is a part of the Ruby on Rails framework. It is a powerful library which is almost a separate dialect of Ruby allowing programmers to write things like:
3.days.ago
2.years.from_now
1.ordinalize # => “1st”
Let’s add an ActiveSupport gem to our function for the purpose of a demo. One of the methods it has, is called #present?
. We can use it to check whether the request body is neither nil nor an empty string in one call:
For that to work we need to use Bundler for dependency management. So, create a file called Gemfile
with the dependencies listed in it:
Run bundle install
and update the test
target in the Makefile for it to pick up the installed gems:
Lambda Layers
When we use Bundler it takes care of downloading, organizing, and finding Ruby gems on the local machine. But how will the AWS Lambda function find them in the cloud? The answer is AWS Lambda Layers. They provide a convenient way to package libraries and other dependencies that can be used with Lambda functions. Using layers reduces the size of uploaded deployment archives and makes it faster to deploy the function code itself. Let’s update the stack.yml
template adding a Lambda layer for gem files.
The above modification declares a layer, referencing the local directory, which is going to be the source of the installed gems, for them to be compressed and uploaded to S3 the AWS package command runs. The changes made to the Lambda function itself include connecting it to the layer and configuring the Ruby runtime to look for the gems in /opt/ruby/2.7.0
by setting the GEM_PATH
environment variable. That location is described in AWS docs for lambda layers. The Timeout is to simply make sure that the Lambda has enough time to load the dependencies even during a cold start. When it’s warm it should respond within less than 100ms.
Finally, let’s change the Makefile to prepare the gems locally for the packaged-stack.yml
:
The change instructs Bundler to install only the gems needed for production and put them in a custom folder that we have earlier referenced in the layer resource from the stack.yml
. And let’s deploy the updated CloudFormation stack by running make stack
. When it finishes, check that the API keeps responding to HTTP requests in the same way as it has earlier.
Wrap-up
That’s it folks. The API we’ve built in this tutorial is quite primitive, but simple and effective. If you liked the idea of serverless Ruby though, and you’d wish it was closer to how a typical Rails app looks — check out Ruby On Jets which does exactly that. Thank you!