The Hanami team & I, recently announced Hanami::API: a minimal, extremely fast, lightweight Ruby framework for HTTP APIs.
Its minimalism, the small memory footprint and its performance, make Hanami::API a good candidate to build microservices.
Today, we’re gonna deploy a small service on Amazon AWS Lambda. It’s an Amazon AWS product that allows to deploy an autoscaling HTTP service. You are charged for every 100ms your code executes and the number of times your code is triggered. Having a fast framework like Hanami::API helps to reduce the costs of running a service on AWS Lambda.
Ruby on Amazon AWS Lambda
By looking at the Amazon AWS Lambda documentation for Ruby, the classic hello world function looks like this:
# frozen_string_literal: true
require "json"
def handler(event:, context:)
{ event: JSON.generate(event), context: JSON.generate(context.inspect) }
end
It’s a toplevel function named handler
that accepts two keyword arguments: event
and context
.
The first argument is a free form Hash
that carries on the optional input.
The last argument is the execution context of the AWS Lambda function; it exposes information like the function_name
, or aws_request_id
.
This design is simple, but it has one problem: it isn’t compatible with Ruby HTTP services based on Rack, like Hanami::API.
Rack on Amazon AWS Lambda
When we deploy Ruby HTTP apps, Rack compatible servers take care to transform the raw HTTP request into an input called Rack env. Because on AWS Lambda we don’t have a Ruby server, but a generic server that takes care only to pass the incoming request to the AWS Lambda function, we can’t expect a Rack compatible framework to work out of the box.
We need to construct manually the Rack env from the AWS Lambda event
argument, and then pass it to the Ruby microservice.
This is a simplified version of the code required to process the event
argument:
# frozen_string_literal: true
require "rack"
module AWS
module Lambda
module Rack
def self.env_for(event)
{
"REQUEST_METHOD" => event.fetch("httpMethod", "GET"),
"SCRIPT_NAME" => "",
"PATH_INFO" => event.fetch("path", "/"),
"rack.version" => ::Rack::VERSION,
}
end
end
end
end
Please remember that the event
is a free form argument, we’re constructing a convention to pass special payload attributes to determine the HTTP request (see httpMethod
), or the path that we want to hit on the service (e.g. /
).
To establish this convention, we are going to follow AWS Lambda documentation, that uses "httpMethod"
and "path"
attributes.
With the Rack env, now we can invoke the Rack app, but we’re not done yet. Because, Rack protocol says that a Rack response is an Array made of three elements (HTTP status code, headers, and body), we need to translate this value back to something meaningful for AWS Lambda.
# frozen_string_literal: true
require "rack"
module AWS
module Lambda
module Rack
def self.success(response)
{
statusCode: response[0],
headers: response[1],
body: response[2].map(&:to_s).join
}
end
end
end
end
Our AWS Lambda handler (handler.rb
) delegates our AWS::Lambda
private library:
# frozen_string_literal: true
require_relative "./aws/lambda"
require_relative "./app"
$app ||= App.new
def handler(event:, context:)
AWS::Lambda.call($app, event, context)
end
Hanami::API on Amazon AWS Lambda
Now that we’re able to run a Rack based application, it’s time to build our service with Hanami::API
# frozen_string_literal: true
require "bundler/setup"
require "hanami/api"
class App < Hanami::API
get "/" do
"Hello, World"
end
end
Because we resolved the mismatch between the AWS Lambda request and Rack, the Hanami::API application is completely unware that it’s being deployed on AWS Lambda.
This has a great advantage development process: you can still keep using a Rack based server (e.g. Puma) and unit test the service with rack-test
.
We can also receive and send JSON data:
# frozen_string_literal: true
require "bundler/setup"
require "hanami/api"
require "hanami/middleware/body_parser"
class App < Hanami::API
use Hanami::Middleware::BodyParser, :json
post "/tracks" do
status 201
json(track: params[:track])
end
end
AWS Lambda Deployment
AWS Lambda expects a .zip
package file to be uploaded on its servers.
Because AWS Lambda don’t run bundle install
, in order to work, our app package must contain all the Ruby gems that are needed to run the app itself.
With a bit of Bundle-fu, we can package only the gems that are required in production and excluding testing libraries (e.g. rspec
).
Our resulting function.zip
file, which contains the app, hanami-api
, and all the other Ruby gems, it’s only 1.2 Megabyte.
It can fit into an old 💾
floppy disk!
âš¡ ls -al function.zip
-rw-r--r-- 1 luca staff 1248377 Mar 4 11:38 function.zip
Conclusion
The small code footprint, its great performance, make Hanami::API a natural candidate to run serverless microservices on Amazon AWS Lambda.
For a complete working example, that includes tests, deploy script, and Rack utilities for AWS Lambda, please have a look at this GitHub repository: jodosha/hanami-api-amazon-aws-lambda.