Serverless Ruby with Docker and Apache OpenWhisk

Update 2018/9/14: Apache OpenWhisk launched Native support for Ruby. This following article is still relevant for languages and Ruby version that don’t have native support.

In this article we are going to get our Ruby scripts running in a Serverless environment using Apache OpenWhisk’s native support for Docker actions. And we’ll host them on IBM Cloud’s managed OpenWhisk instance: IBM Cloud Functions.

Ruby is a language that excels in allowing us to write quick short scripts that make our life easier. Serverless is a great way to (not have to) manage these scripts and autoscale them. Unfortunately no Serverless platforms actually support Ruby out of the box.

My favorite thing about the Apache OpenWhisk serverless platform (apart from it being open source) is that it also supports any type of Docker container as a serverless action. We can go crazy running exotic languages in a serverless environment! Or we just easily add support for our Ruby scripts!

OpenWhisk’s architecture. The natively supported languages get executed in Docker “invokers”. In our case we’ll use our very own Docker container running Ruby!

Understanding how OpenWhisk’s Docker Actions work

To get Docker Actions to work there are a couple components we need to implement:

  • We’ll need a Docker Image containing a webserver bound to 0.0.0.0 and exposed via port 8080.
  • An endpoint accepting POST /init.
    And responding with status 200 and the text “OK”.
    This endpoint is called the first time an action is invoked and is useful if you need to compile or setup anything to before /run is called.
  • An endpoint accepting POST /run.
    This is where the magic happens, all Serverless actions are invoked via this endpoint. OpenWhisk will forward any set parameters as the “value” argument in JSON via the POST body. 
    The cURL equivalent of an invoked action:
$ curl http://localhost:8080/run -X POST -d '{ "value": { "any": "parameters", "I send": "to OpenWhisk" }' -H "Content-Type: application/json"

With these simple components we should be able to build any Docker container and use it as an OpenWhisk Serverless action.

Getting Started

Now that we have some of the theory down lets get coding!

Prerequisites

Install docker. Install docker-compose. Download the wskdeploy cli. Create a Docker Hub account. Create an IBM Cloud account (we’ll be using IBM Cloud Functions because it’s easy & free, but we can use any other managed OpenWhisk here if you’d like).

Setup

In these next couple steps we are going to create the bare minimum we’ll need to create our Docker container + Ruby script. Once were done with the setup we can deploy it to a serverless environment and test it out.

First we are going to need a folder structure like the following listed below.

I like using docker-compose’s cli over docker’s cli as it makes life a lot easier but if you are more comfortable with docker’s cli feel free to omit “docker-compose.yml" in your folder structure.

Let’s create the action.rb with our Ruby script. We are using Sinatra as our web server framework here since Sinatra is dead simple to use. Towards the bottom of this file is where we’ll normally insert our serverless code. In this example we will be hosting a super simple Hello World script.

Then we need to create the Dockerfile to get everything running.

And now we can create our docker-compose.yml file just so we can use docker-compose’s easy to use CLI:

Important: We are going to push our docker image to Docker Hub to allow us to pull it back into OpenWhisk. Make sure you rename juice10 to your Docker Hub username and openwhisk-ruby to the name of Docker Hub repo you want to store your project in.

Boom! Setup’s done! That wasn’t too hard!

Building our container

It’s time to build our docker container. (Output listed below, yours may differ. Look out for any errors.)

$ docker-compose build
Building action
Step 1/5 : FROM ruby:2.5
---> 1624ebb80e3e
Step 2/5 : RUN gem install sinatra && gem install sinatra-contrib && gem install thin
---> Using cache
---> f89decc6eaa5
Step 3/5 : ADD action.rb /
---> 6b586b735a9b
Step 4/5 : EXPOSE 8080
---> Running in 34a42a8abc38
Removing intermediate container 34a42a8abc38
---> f1f9281c6d33
Step 5/5 : CMD [ "ruby", "/action.rb" ]
---> Running in 886c1b00aa05
Removing intermediate container 886c1b00aa05
---> 44253675793a
Successfully built 44253675793a
Successfully tagged juice10/openwhisk-ruby:latest

Let’s get our container running:

$ docker-compose up
Creating network "project_default" with the default driver
Creating project_action_1 ... done
Attaching to project_action_1
action_1 | == Sinatra (v2.0.1) has taken the stage on 8080 for production with backup from Thin

If everything is well we should get an output similar to what I listed above.

Testing our action

In another terminal window let’s simulate an OpenWhisk call to this action.

$ curl http://localhost:8080/run -X POST -d '{"value":{}}' -H "Content-Type: application/json"
{"message":"Hello World"}

If all is well our response should be as above.

Now lets pass in a name into the action:

$ curl http://localhost:8080/run -X POST -d '{"value":{"name": "Ada Lovelace"}}' -H "Content-Type: application/json" 
{"message":"Hello Ada Lovelace!"}

Push to Docker Hub

Your output the same?! Ok great! Now we verified it’s working locally, it’s time to push it to Docker Hub. Switch back to your first terminal window and lets push our image to Docker Hub.

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: ...
Password: ...
Login Succeeded
$ docker-compose push
Pushing action (juice10/openwhisk-ruby:latest)...
The push refers to repository [docker.io/juice10/openwhisk-ruby]
21b254d59a33: Pushed
...
e1df5dc88d2c: Pushed
latest: digest: sha256:dde089835c9a6f0088f498c68e9053ae06d135d03cde405774527d87dce84445 size: 2421

Deployment

Now that it’s pushed to docker hub we can pull it in to OpenWhisk.

Let’s use wskdeploy. Download the wskdeploy cli to the folder if you haven’t already. Choose the one that works for your OS.

wskdeploy is a great tool for deploying any serverless actions to OpenWhisk. It uses a manifest.yaml file to figure out what OpenWhisk instance it needs to deploy to and what actions, files and docker instances it needs to use for the deployment. There are many of deploying our serverless actions, another good one is using the Serverless framework or the IBM Cloud cli.

Get your key, host and namespace for OpenWhisk from the IBM Cloud Functions API Key page. Add these to the credential:, apiHost: andnamespace: parameters in manifest.yaml.

You’ll also have to add your docker-hub-username/repo-path-name to the docker: parameter in the same file.

Now lets deploy it to IBM Cloud Functions using wskdeploy.

$ ./wskdeploy
Info: The API host is [openwhisk.ng.bluemix.net], from manifest.yml.
Info: The auth key is set, from manifest.yml.
Info: The namespace is [watsonworkshop2_dev], from manifest.yml.
Info: Unmarshal OpenWhisk runtimes from internet at https://openwhisk.ng.bluemix.net.
Info: Deploying package [MyOpenWhiskActionPackage] ...
Info: package [MyOpenWhiskActionPackage] has been successfully deployed.
Info: Deploying action [MyOpenWhiskActionPackage/RubyAction] ...
Info: action [MyOpenWhiskActionPackage/RubyAction] has been successfully deployed.
Success: Deployment completed successfully.

Testing our action for real

Now let’s invoke it to make sure it works!

Head over to your list of actions in OpenWhisk.

Select on your action and click the “Invoke” button to execute your action.

And you should see a response like this:

{
"message": "Hello World"
}

If you run the action a second time we’ll notice the action’s time going down from aprox. 400ms to 4ms. This is the dreaded ‘cold start’ that you always hear everyone talking about.

The very first time you run your action it might take up to 30 seconds on top of the 400ms to build your Docker image behind the scenes. Don’t worry that only happens once and won’t be seen again as part of the dreaded ‘cold start’.

Now if you changed the input to {"name": "friend"} you should get a response like this:

{
"message": "Hello friend!"
}

Excellent! In a couple of easy steps you where able to create a serverless action on OpenWhisk, hosted on IBM Cloud Functions. All from your own Docker container running your favorite language. Well done!

Bonus

Public endpoints

To open our action up to the public we can select “Endpoints” from the menu, select “Enable as Web Action”, hit the “Save” button and then your serverless action will become available for anyone to access.

Try out mine like:

$ curl -X POST -d '{"name": "Ada Lovelace"}' -H "Content-Type: application/json" https://openwhisk.ng.bluemix.net/api/v1/web/watsonworkshop2_dev/MyOpenWhiskActionPackage/RubyAction_Thin.json
{
"message": "Hello Ada Lovelace!"
}

Inspiration

Rob Allen created a PHP runtime for OpenWhisk and before he did that he wrote a similar article to this one on how to get PHP running in OpenWhisk with Docker. Now let’s make Ruby a first class citizen in the Serverless world!

Performance Notes: Webrick vs Thin

I made two versions of this action, here are the pros and cons of each:

Server: webrick Docker base image:ruby:alpine

  • Pro: Docker image is super small
  • Pro: Docker build time is super quick
  • Pro: Cold starts and first invocations are super quick
  • Neutral: actions execute in 3–6ms.
  • Con: ERROR Errno:ECONNRESET: Connection reset by peer @ io_fillbuf errors litter your OpenWhisk logs. The actions work but these errors are a little annoying.

Server: thin Docker base image: ruby:2.5

  • Con: Docker image is slightly bigger
  • Con: Thin needs native extensions to build, that takes a little while
  • Con: Cold starts and first invocations take a little longer
  • Pro: Actions execute a lot faster 2–3ms.
  • Pro: No errors, the logs are clean