[Ruby] Getting Started

This tutorial is for versions prior to 0.5. While most of what's in here will still work on 0.5, some things are different. We are currently working on the new docs for 0.5.


FaaStRuby is a serverless platform built for Ruby developers. You deploy functions to workspaces and trigger them via HTTP endpoints.

The platform currently supports Ruby versions 2.5.3, 2.6.0 and 2.6.1.


Workspaces are groups of functions and can be used to mimic environments or to compose a set of routes for a distributed app based on functions. They are represented by the URL https://api.tor1.faastruby.io/WORKSPACE_NAME. Note that workspace names must be unique.
When you create a workspace you receive an API key/secret pair. You need to use those credentials to make changes to the workspace so store them somewhere safe.

You can upload functions to any existing workspaces you own. Functions can be triggered through their workspace URL. For example, a function named 'slack-bot' in the workspace 'catops-prod' will have the URL https://api.tor1.faastruby.io/catops-prod/slack-bot.

Creating a Workspace

The first thing you need to do is create a workspace. The command below will send a request to create a workspace 'catops-prod' on FaaStRuby and write the credentials to '~/.faastruby.tor1', where 'tor1' is the region code for Toronto, Canada.

~$ faastruby create-workspace catops-prod
◐ Requesting credentials... Done!
Writing credentials to /Users/demo/.faastruby.tor1
~ f /Users/demo/.faastruby.tor1
Workspace 'catops-prod' created

If you want to print the credentials to STDOUT instead of saving it to a file, use the option --stdout when creating the workspace:

~$ faastruby create-workspace catops-prod --stdout
◐ Requesting credentials... Done!
IMPORTANT: Please store the credentials below in a safe place. If you lose them you will not be able to manage your workspace.
API_KEY: 63a02778c9342993801936ef4a87412e
API_SECRET: voeIjDPY5sloCZ0oo68nIw==
Workspace 'catops-prod' created

You can also attach an email address when creating a workspace. Although this is not required, it's a good idea so you can be contacted in case of any problems with your workspace. To create a workspace attaching an email address to it, run:

~$ faastruby create-workspace catops-prod -e you@example.com

Your first function

Let's create and deploy your first function. The function will take a JSON payload {"name": "Ruby"} and respond with the string "Hello, Ruby!". Let's call it 'hello-world':

~$ cd catops-prod
~/catops-prod$ faastruby new hello-world --runtime ruby:2.5.3
+ d ./hello-world
+ d ./hello-world/spec    # Put your tests in here
+ d ./hello-world/spec/helpers    # Spec helpers go here
+ f ./hello-world/spec/helpers/faastruby.rb    # FaaStRuby::SpecHelper
+ f ./hello-world/spec/handler_spec.rb    # Spec for handler.rb
+ f ./hello-world/spec/spec_helper.rb    # Helper to load FaaStRuby::SpecHelper
+ f ./hello-world/Gemfile    # Just a regular Gemfile
+ f ./hello-world/handler.rb    # The function handler. Must define a method 'handler'
+ f ./hello-world/faastruby.yml    # Your function configuration
◑ Installing gems... Done!

Let's take a look at 'faastruby.yml'

name: hello-world    # The function name
runtime: ruby:2.5.3  # The function runtime
test_command: rspec    # The command to run tests
abort_build_when_tests_fail: true    # Abort package build when tests fail
abort_deploy_when_tests_fail: true    # Abort deploy when tests fail

Now let's look inside the file handler.rb. This is the file that is required by a runner when your function gets invoked. Note that the function returns the value from a 'render' call.

# require 'cool-gem'

# To deploy this function, cd into its folder and run:
# faastruby deploy-to WORKSPACE_NAME
def handler event
  # The 'event' argument has the following attributes
  # event.body - The request body
  # event.context - The execution context
  # event.headers - The request headers
  # event.query_params - The query params

  # You can render text, json, yaml, html or js. Example:
  # render html: '<p>Hello World!</p>'
  # render yaml: {hello: 'world!'}
  # Status:
  # The default status is 200. You can set a custom status like this:
  # render json: {error: 'Could not perform the action'}, status: 422
  # Headers:
  # The 'Content-Type' header is automatically set when you use 'render'.
  # You can set custom headers using a hash with string keys. Example:
  # render text: 'It Works!', headers: {'TransactionId' => 23928}
  render text: "Hello, World!\n"

You can call render with the keys text:, json:, js:, html:, yaml:, png:, jpeg:, svg:, css:, icon:, gif:, data: and body:. This method will set the header Content-Type automatically for you, and if you use the keyword arguments data: or body:, the Content-Type header will default to "application/octet-stream".

You can also set the response status code and headers. The status code defaults to 200, and the headers parameter must be a Hash of string keys.

def handler event
  render text: "Hello, World!", status: 200, headers: {'Content-Type' => 'text/plain'}

Now let's update the function handler so it will get the request body, parse it and respond with "Hello, _____!" or "Hello, World!" if no name is present.

The 'event' parameter

The function handler takes a parameter 'event'. This parameter is a Struct with the following attributes:

  • event.body - The request body string. It is up to you to parse it. Ex: JSON.parse(event.body)
  • event.query_params - A Hash with the URL query parameters. Ex: for 'foo=bar&lorem=ipsum', event.query_params['foo'] #=> "bar" and event.query_params['lorem'] #=> "ipsum"
  • event.headers - A hash with the request headers.
  • event.context - The execution context. Learn more.

Let's say we decide to send a JSON payload to the function with the following content:

{"name": "Ruby"}

Then we parse 'event.body' and modify the function response. Here is the code:

require 'json'

def handler event
  name = event.body ? JSON.parse(event.body)['name'] : 'World'
  response = "Hello, #{name}!\n"
  render text: response

Can't forget to write some tests! 🙂
Here is the content for 'spec/handler_spec.rb':

require 'spec_helper'
require 'handler'

describe 'handler(event)' do
  it 'should add the name to the response string' do
    event = SpecHelper::Event.new(body: '{"name": "Ruby"}')
    body = handler(event).call.body
    expect(body).to be == 'Hello, Ruby!'
  it 'should say Hello, World! when name is not present' do
    event = SpecHelper::Event.new(body: nil)
    body = handler(event).call.body
    expect(body).to be == 'Hello, World!'

Now let's deploy this function to the FaaStRuby servers. You need to specify the workspace in which your function will be deployed. Let's use our previously created 'catops-prod' workspace.

~/catops-prod$ cd hello-world
~/catops-prod/hello-world$ faastruby deploy-to catops-prod
◐ Running tests... Passed!

Finished in 0.00519 seconds (files took 0.14255 seconds to load)
3 examples, 0 failures

◐ Building package... Done!
◒ Deploying to workspace 'catops-prod'... Done!
Endpoint: https://api.tor1.faastruby.io/catops-prod/hello-world

Here is what happened:

  1. The tests run locally.
  2. A deployment package was built with the contents of the hello-world function.
  3. The package was uploaded to FaaStRuby.
  4. On the server side, the function is unpacked and the gems are installed (the group 'test' is included).
  5. The tests run again on the server to make sure the functions will work once deployed.
  6. The gems from the group 'test' are removed and the function is deployed.

Time to test it out by invoking the function with curl:

~/catops-prod/hello-world$ curl https://api.tor1.faastruby.io/catops-prod/hello-world
Hello, World!

It works! Now let's try again passing a name via JSON body:

~/catops-prod/hello-world$ curl -X POST -H 'Content-Type: application/json' -d '{"name":"Ruby"}' 'https://api.tor1.faastruby.io/catops-prod/hello-world'
Hello, Ruby!

Writing temporary files

For security reasons, FaaStRuby functions cannot write to disk. You can, however, create temporary files up to 5MB of size. Example:

require 'tmpdir'
def handler event
  temp_dir = Dir.mktmpdir('tmp')
  # ...

Execution Context

Sometimes you want your function to know about some data that you don't want to commit to source control or pass it on each request. With contexts you can have pre-loaded data available to your function on every run.

Execution context data is encrypted at rest, decrypted at runtime and passed to your function via 'event.context'.

To add an execution context to a function, you use the 'update-context' command and pass the function's workspace name:

~/catops-prod/hello-world$ faastruby update-context catops-prod --data '{"super_secret":"abc123"}'
◐ Uploading context data to 'catops-prod'... Done!

You can also read from stdin:

~/catops-prod/hello-world$ echo '{"super_secret":"abc123"}' | faastruby update-context catops-prod --stdin

Every time you run this command, the previous data is replaced with the new one. The data is handed to your function as-is, so it's up to you to parse it within your function.

require 'json'

def handler event
  context = JSON.parse(event.context)
  # context["super_secret"] #=> "abc123"
  render json: event.context # will print back the context

Custom responses: Binary, HTML, JSON, YAML, etc

If you need to respond with JSON, HTML or YAML, all you need to do is pass the correct keyword argument to the method render, and the response headers will be set for you. When using JSON or YAML, the parser will try to serialize the value unless it is a String. Example:

def handler event
  response = {foo: 'bar', far: 'boo'}
  render yaml: response # will be processed with .to_yaml

Another example, rendering a PNG image:

require 'open-uri'

def handler event
  link = 'https://www.ruby-lang.org/images/header-ruby-logo.png'
  render png: open(link).read

The "render" method will automatically set the correct headers:

  • json: application/json
  • text: text/plain
  • yaml: text/yaml
  • html: text/html
  • js: text/javascript
  • css: text/css
  • body: application/octet-stream
  • data: application/octet-stream
  • png: image/png
  • gif: image/gif
  • jpeg: image/jpeg
  • icon: image/x-icon
  • svg: image/svg+xml

If you render "body:" or "data:", you can pass a "binary:" argument, to make sure the response will be rendered correctly. When "body:" is used, "binary:" defaults to false. When "data:" is used, "binary:" defaults to true. You can override the values like this:

# You could just "render data:" here, but I am using body to illustrate how you can override the type.
def handler event
  file = open('./foo.zip', "rb") {|io| io.read }
  render body: file, binary: true, headers: {'Content-Type' => 'application/zip'}

Scheduling functions

You can schedule your functions to run periodically or at any time in the future. Functions can run on multiple schedules, and you configure them inside "faastruby.yml". Example:

    when: every 2 hours
    body: {"foo": "bar"}
    method: POST
    query_params: {"param": "value"}
    headers: {"Content-Type": "application/json"}

The schedule configuration takes the following keys:

Accepts plain English or the Cron syntax. Examples:

  • every 3 hours
  • 0 14 * * *
  • every day at 2am
  • every Tuesday at noon
  • every 5 days
  • every Sunday at 23:00

The request body to be passed to your function. Defaults to nil. Available as "event.body" within the function.

The request method to be used. Defaults are: GET when "body" is nil, and POST when "body" is not nil.

The URL query params to be passed to your function, in JSON format. Default to {}. Available as event.query_params.

The headers to be passed to your function. They will be available as "event.headers" and defaults to {}.
IMPORTANT: If you don't specify a "Content-Type" header, the body will be processed as form data (key=value pairs).

Calling functions from within a function (async function calls)

To call another function you must first require it on the top of handler.rb, passing a string that will be converted to a constant. You then use the constant to call the function and get its response.

To call the function, use the method call. Here is an example.

require_function 'paulo/hello-world', as: 'HelloWorld'
def handler(event)
  hello = HelloWorld.call    # Async call
  hello.class                #=> FaaStRuby::RPC::Function
  hello.returned?            #=> false # READ BELOW TO UNDERSTAND
  hello                      #=> 'Hello, World!' - The RPC response body
  hello.body                 #=> 'Hello, World!' - Also the RPC response body
  hello.returned?            #=> true # READ BELOW TO UNDERSTAND
  hello.code                 #=> 200 - The status code
  hello.headers              #=> {"content-type"=>"text/plain", "x-content-type-options"=>"nosniff", "connection"=>"close", "content-length"=>"5"} - The response headers
  render text: hello

The biggest problem with serverless applications is the latency resultant of multiple calls to different functions. This is called tail latency. To minimize this problem, faastruby-rpc will handle requests to other functions asynchronously. You should design your application with that in mind. For example, do your external function calls early during the function execution and perform other tasks while you wait for the response.

In the example above, hello = HelloWorld.call will issue a non-blocking HTTP request in a separate thread to the called function's endpoint, assign it to a variable and continue execution. When you need the return value from that function, just use the variable. If you call the variable before the request is completed, the execution will block until an answer is received from the external function. To minimize tail latency, just design your application around those async calls.

If at any point you need to know if the external function call already returned without blocking the execution of your function, use the method returned?. So in the example above, hello.returned? will be false until the request is fulfilled.

Passing arguments to the called function

Say you have the following function in my-workspace/echo:

# Note the required keyword argument 'name:'
def handler(event, name:)
  render text: name

If you want to call this function from another function, you can simply pass the argument with call:

require_function 'my-workspace/echo', as: 'Echo'
def handler(event)
  name = Echo.call(name: 'John Doe') # Async call
  render text: "Hello, #{name}!" # calling 'name' will block until Echo returns

You can use positional or keyword arguments when calling external functions, as long as the external function's handlermethod is defined with matching arguments.

This gem is already required when you run your functions in FaaStRuby, or using faastruby server.

Running code when the invoked function responds

If you pass a block when you call another function, the block will execute as soon as the response arrives. For example:

require_function 'my-workspace/echo', as: 'Echo'
def handler(event)
  name = Echo.call(name: 'john doe') do |response|
    # response.body      #=> "john doe"
    # response.code      #=> 200
    # response.headers   #=> {"content-type"=>"text/plain",...}
    # What you return from the block will be the value of `name` or `name.body`
  render text: "Hello, #{name}!" # Will render 'John Doe'

Handling errors

By default, an exception is raised if the called function HTTP status code is greater or equal to 400. This is important to make your functions easier to debug, and you will always know what to expect from that function call.

To disable this behaviour, pass raise_errors: false when requiring the function. For example:

require_function 'paulo/hello-world', as: 'HelloWorld', raise_errors: false

Stubbing RPC calls in your function tests

If you are testing a function that required another one, you likely will want to fake that call. To do that, use the following test helper:

# This will make it fake the calls to 'paulo/hello-world'
# and return the values you pass in the block.
require 'faastruby-rpc/test_helper'
FaaStRuby::RPC.stub_call('paulo/hello-world') do |response|   
  response.body = "hello, world!"
  response.code = 200
  response.headers = {'A-Header' => 'foobar'}
<a name="templates"></a>

Creating a function from a template

Introduced with version 0.4.16.

You have the option to use a template when creating a function. You can use templates from the local disk or a remote Git repository. The syntax is:

~$ faastruby new FUNCTION_NAME --template TYPE(local|git|github):SOURCE


# Use a template from a Github repository:
~$ faastruby new my-function --template github:faastruby/hello-world-ruby-template

# Use a template from a Git repository:
~$ faastruby new my-function --template git:git@github.com:FaaStRuby/hello-world-ruby-template.git

# Use a template from the local disk:
~$ faastruby new my-function --template local:/path/to/template

FaaStRuby Development Environment (beta)

Since version 0.3.2, the CLI has 2 extra commands: 'server' and 'deploy'.

How to use the FaaStRuby Server

You can use the FaaStRuby server to develop full apps using functions. To start using it, follow the steps below.

1. Create a folder that will contain all your workspaces and functions.

~$ mkdir ~/my-faastruby-app
~$ cd ~/my-faastruby-app

2. Create the workspaces.

~/my-faastruby-app$ faastruby create-workspace workspace-1
~/my-faastruby-app$ faastruby create-workspace workspace-2

3. Create functions in the workspaces

~/my-faastruby-app/workspace-1$ cd workspace-1
~/my-faastruby-app/workspace-1$ faastruby new hello-world
~/my-faastruby-app/workspace-1$ cd ../workspace-2
~/my-faastruby-app/workspace-2$ faastruby new hello-world

4. Deploy the project. All folders inside "~/my-faastruby-app" will be created as workspaces, and their functions deployed accordingly.

~/my-faastruby-app/workspace-2$ cd ~/my-faastruby-app
~/my-faastruby-app$ faastruby deploy