James Thomas

Notes on JavaScript

Python Packages in OpenWhisk

OpenWhisk’s Python runtime includes popular third-party libraries like requests, scrapy and simplejson. Developers don’t have to manually install packages to use those libraries.

Great, but what about using other libraries that aren’t pre-installed?

In a previous blog post, we showed how to deploy Node.js actions from zip files containing third-party modules. These modules are then made available in the Node.js runtime.

Recent updates to OpenWhisk allow us to use the same approach with the Python runtime!

Python Packages

Python packages can be installed using the pip tool. This can be used to install individual packages or a series of dependencies from an external file.

1
2
$ pip install blah
$ pip install -r requirements.txt

pip defaults to installing packages in a global location (site-packages) which is shared between all users. This can cause issues when different projects require different versions of the same package.

virtualenv

virtualenv is a tool that solves this issue by creating virtual python environments for projects. The virtual environment includes a custom site-packages folder to install packages into.

1
2
3
4
5
$ virtualenv env
Using base prefix '/Library/Frameworks/Python.framework/Versions/3.6'
New python executable in /private/tmp/env/bin/python3.6
Also creating executable in /private/tmp/env/bin/python
Installing setuptools, pip, wheel...done.

OpenWhisk recently added support for using virtualenv in the Python runtime.

custom packages on openwhisk

OpenWhisk actions can be created from a zip file containing source files and other resources.

If the archive includes a virtual Python environment folder, the platform runs the ./bin/activate_this.py script before executing Python actions. This script modifies the module search path to include the local site-packages folder.

This will only happen during “cold” activations.

This feature comes with the following restrictions.

  • Virtual Python environment must be in a folder called virtualenv under the top-level directory.
  • Packages must be available for the Python runtime being used in OpenWhisk (2.7 or 3.6).

Let’s look at an example of building an OpenWhisk Python action which uses an external Python package.

Python Package Example

The pyjokes package provides a library for generating (terrible) jokes for programmers. Let’s turn this package into an API (Jokes-as-a-Service!) using the Python runtime on OpenWhisk.

Start by creating a new directory for your project and set up the virtual Python environment.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ mkdir jokes; cd jokes
$ virtualenv virtualenv
Using base prefix '/Library/Frameworks/Python.framework/Versions/3.6'
New python executable in /tmp/jokes/virtualenv/bin/python3.6
Also creating executable in /tmp/jokes/virtualenv/bin/python
Installing setuptools, pip, wheel...done.
$ source virtualenv/bin/activate
(virtualenv) $ pip install pyjokes
Collecting pyjokes
  Using cached pyjokes-0.5.0-py2.py3-none-any.whl
Installing collected packages: pyjokes
Successfully installed pyjokes-0.5.0
(virtualenv) $

In the project directory, create a new file (__main__.py) and paste the following code.

1
2
3
4
import pyjokes

def joke(params):
    return {"joke": pyjokes.get_joke()}

Check the script works with the Python intepreter.

1
2
3
4
(virtualenv) $ python -i .
>>> joke({})
{'joke': 'What do you call a programmer from Finland? Nerdic.'}
>>>

Add the virtualenv folder and Python script to a new zip file.

1
2
3
4
5
6
7
8
$ zip -r jokes.zip virtualenv __main__.py
  adding: virtualenv/ (stored 0%)
  adding: virtualenv/.Python (deflated 65%)
  adding: virtualenv/bin/ (stored 0%)
  adding: virtualenv/bin/activate (deflated 63%)
  ...
$ ls
__main__.py  jokes.zip   virtualenv

Create a new OpenWhisk action for the Python runtime using the wsk cli.

1
2
$ wsk action create jokes --kind python:3 --main joke jokes.zip
ok: created action jokes

Invoking our new action will return (bad) jokes on-demand using the third-party Python package.

1
2
3
4
$ wsk action invoke jokes --blocking --result
{
    "joke": "Software salesmen and used-car salesmen differ in that the latter know when they are lying."
}

Installing Packages With Docker

In the example above, the Python runtime used in development (v3.6) matched the OpenWhisk runtime environment. Packages installed using virtualenv must be for the same major and minor versions of the Python runtime used by OpenWhisk.

OpenWhisk publishes the runtime environments as Docker images on Docker Hub.

Running containers from those runtime images provides a way to download packages for the correct environment.

1
2
3
4
5
6
7
8
9
10
11
$ docker run --rm -v "$PWD:/tmp" openwhisk/python3action sh \
  -c "cd tmp; virtualenv virtualenv; source virtualenv/bin/activate; pip install pyjokes;"
Using base prefix '/usr/local'
New python executable in /tmp/virtualenv/bin/python3.6
Also creating executable in /tmp/virtualenv/bin/python
Installing setuptools, pip, wheel...done.
Collecting pyjokes
  Downloading pyjokes-0.5.0-py2.py3-none-any.whl
Installing collected packages: pyjokes
Successfully installed pyjokes-0.5.0
$

This will leave you a virtualenv folder in the current directory with packages for the correct Python runtime.

Speeding Up Deployments

Peeking inside the virtualenv folder reveals a huge number of files to set up the virtual Python environment. If we just want to use a third-party package from the local site-packages folder, most of those files are unnecessary.

Adding this entire folder to the zip archive will unnecessarily inflate the file size. This will slow down deployments and increase execution time for cold activations. OpenWhisk also has a maximum size for action source code of 48MB.

Manually including individual site-packages folders, rather than the entire virtualenv directory, will ensure the archive file only contains packages being used. We must also add the Python script (virtualenv/bin/activate_this.py) executed by OpenWhisk to modify the module search path.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ zip -r jokes_small.zip virtualenv/bin/activate_this.py virtualenv/lib/python3.6/site-packages/pyjokes __main__.py
updating: virtualenv/bin/activate_this.py (deflated 54%)
updating: virtualenv/lib/python3.6/site-packages/pyjokes/ (stored 0%)
updating: virtualenv/lib/python3.6/site-packages/pyjokes/__init__.py (deflated 20%)
updating: virtualenv/lib/python3.6/site-packages/pyjokes/jokes_de.py (deflated 29%)
updating: virtualenv/lib/python3.6/site-packages/pyjokes/jokes_en.py (deflated 61%)
updating: virtualenv/lib/python3.6/site-packages/pyjokes/jokes_es.py (deflated 40%)
updating: virtualenv/lib/python3.6/site-packages/pyjokes/pyjokes.py (deflated 68%)
updating: __main__.py (deflated 18%)
$ ls -lh
total 40984
-rw-r--r--  1 james  wheel    74B 21 Apr 11:01 __main__.py
-rw-r--r--  1 james  wheel    20M 21 Apr 11:07 jokes.zip
-rw-r--r--  1 james  wheel   9.3K 21 Apr 13:36 jokes_small.zip
drwxr-xr-x  6 james  wheel   204B 21 Apr 11:25 virtualenv

The archive file is now less than ten kilobytes! 🏃

With The Serverless Framework

The Serverless Framework is a popular open-source framework for building serverless applications. This framework handles the configuration, packaging and deployment of your serverless application.

OpenWhisk is supported through a provider plugin. Recent versions of the plugin added support for the Python runtime environment.

Using the application configuration file for the framework, users can add include and exclude parameters to control the contents of the archive file before deployment.

Here’s an example of the configuration needed to only include the necessary files for the application above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
service: pyjokes

provider:
  name: openwhisk
  runtime: python:3

functions:
  jokes:
    handler: handler.joke

plugins:
  - serverless-openwhisk

package:
  exclude:
    - virtualenv/**
    - '!virtualenv/bin/activate_this.py'
    - '!virtualenv/lib/python3.6/site-packages/pyjokes/**'

conclusion

Python has a huge community of third-party packages for everything from parsing JSON, making HTTP requests and even generating jokes. OpenWhisk already provided a number of the most popular packages within the Python runtime.

Users can install additional packages locally using the pip and virtualenv tools. Bundling those files within the deployment archive means they are extracted into the OpenWhisk Python runtime environment.

Recent changes to the Python runtime allows the platform to automatically add local package folders to the module search path.

This means Python functions running on OpenWhisk can now use any third-party library as if it was installed globally.

Hurrah 👌!

Building an SMS Bot for Slack.

This is smsbot.

It provides an integration with Slack that connects SMS messages into channels. People can text an external number and have their messages posted into the channel. Channel users can respond to the messages and have their response sent back to the sender using SMS.

smsbot was developed in under a few hours and less than one hundred lines of code using a serverless cloud platform.

Want to understand how it works? Let’s find out…

The first challenge was how to programmatically send and receive SMS messages.

Want to deploy smsbot yourself? Follow the instructions at the bottom or check out the Github repository.

Twilio

Twilio provides a platform for building SMS, voice and messaging applications using an API.

Developers can register phone numbers through the service that invoke webhooks for incoming calls and SMS messages. Webhooks are passed message details and return a custom markup language (TwilML) to encode the instructions on how to respond to the request.

The platform also provides a REST API to initiate phone calls and SMS messages.

We now have a way to handle text messages for our bot, how do we integrate a new bot in Slack?

Slack

Slack also provides a webhook-based mechanism to integrate custom bots into the platform. The platform has two different integrations…

Incoming Webhooks.

Provide a way to post messages into Slack from external sources. It provides a custom URL that supports HTTP requests with a JSON payload. These requests are turned into channel messages. The JSON payload is used to control the content and formatting of the message.

https://api.slack.com/incoming-webhooks

Outgoing Webhooks

Allow you to listen for messages in channels without using the full real-time API. Slack sends HTTP requests to registered URLs when specific trigger words appear in channel messages. The HTTP request body contains the message details.

https://api.slack.com/outgoing-webhooks

Now we just need a way to write simple HTTP services to listen for webhook requests…

OpenWhisk

OpenWhisk is an open-source serverless cloud platform. Serverless platforms make it easy to create microservices in the cloud without having to set up or manage any infrastructure.

Developers push their code directly into the platform. The platform will instantiate the runtime and invoke the code on-demand for each request. Serverless functions can be exposed as HTTP endpoints or connected to event sources like message queues or databases.

Serverless platforms make it easy to create simple HTTP services to handle webhook requests.

Web Actions

Web Actions are a new feature in OpenWhisk for exposing serverless functions as simple HTTP endpoints. Functions have access to the full HTTP request and can control the HTTP response returned. This method is suitable for simple public endpoints that do not need more enterprise features supported by the API gateway.

Web Actions are available at the following platform API path.

1
https://{APIHOST}/api/v1/experimental/web/{USER_NAMESPACE}/{PACKAGE}/{ACTION_NAME}.{TYPE}
  • APIHOST - platform endpoint e.g. openwhisk.ng.bluemix.net.
  • USER_NAMESPACE - must be explicit and cannot use the default namespace (_).
  • PACKAGE - action package or default.
  • ACTION_NAME - function identifier.
  • TYPE - .json, .html, .text or .http.

Example

Here’s a simple Web Actions that returns HTML content when invoked through the API.

1
2
3
4
5
6
7
8
function main(args) {
    var msg = "you didn't tell me who you are."
    if (args.name) {
        msg = `hello ${args.name}!`
    }
    return {body:
       `<html><body><h3><center>${msg}</center></h3></body></html>`}
}

Actions can be turned into web-accessible actions by setting a custom annotation.

1
$ wsk action create greeting source.js --annotation web-export true

The greeting function can then be invoked through a HTTP request to the following endpoint.

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@host.com_dev/default/greeting.http?name=James

1
2
3
4
5
$ http post https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@host.com_dev/default/html_greeting.http?name=James
HTTP/1.1 200 OK
...

<html><body><h3><center>hello James!</center></h3></body></html>

Twilio <=> Slack

OpenWhisk Web Actions are a great solution for creating webhook endpoints. Connecting Twilio to Slack (and vice-versa) can be implemented using two different OpenWhisk Web Actions.

  • Twilio Webhook. Invoked for SMS messages. Uses the Slack Incoming Webhook to create a bot messages from content. ​
  • Slack Outgoing Webhook. Invoked for channel replies. Uses Twilio API to send replies as SMS messages.

Let’s have a look at the Twilio webhook first…

Twilio Webhook

When a new SMS message is received, we want to post this bot message into our Slack channel.

Twilio allows developers to configure webhooks for each registered phone number. The webhook endpoint will be invoked for each SMS message that is received. Twilio can either send a HTTP POST request, with parameters in the body, or a HTTP GET request, with URL query parameters.

OpenWhisk Web Actions support both formats. Request parameters will be available as properties on the function argument.

Here’s a simple Web Action that would log the message sender and content for each SMS received.

1
2
3
function main (params) {
  console.log(`Text message from ${params.From}: ${params.Body}`)
}

Creating Bot Messages From SMS

When an SMS message is received, we need to send a HTTP POST to the Incoming Webhook URL. The JSON body of the HTTP request is used to configure the channel message. Using the username, icon_emoji and and text properties allows us to customise our bot message.

OpenWhisk Actions in Node.js have numerous popular NPM modules pre-installed in the environment. This includes a HTTP client library. This code snippet demonstrates sending the HTTP request to create out bot message. The Slack Webhook URL is bound as a default parameter to the action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const request = require('request')

const slack_message = text => ({
  username: 'smsbot',
  icon_emoji: ':phone:',
  text
})

function main (params) {
  return new Promise(function (resolve, reject) {
    request.post({
      body: slack_message(`Text message from ${params.From}: ${params.Body}`),
      json: true,
      url: params.slack.webhook
    }, err => {
      if (err) return reject(err);
      resolve();
    })
  })
}

Returning a Promise ensures the request is completed before the function exits.

Sending Acknowledgement Message

Returning TwilML content allows us to control the response from Twilio to the incoming message.

This snippet would send an SMS reply to sender with the content “Hello World!”.

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Message>Hello World!</Message>
</Response>

Twilio’s client library for Node.js can be used to programatically generate TwilML.

1
2
3
const twilio = require('twilio')
const resp = new twilio.TwimlResponse()
const twilml = resp.message('Thanks for letting us know.').toString()

Returning XML content as the HTTP response requires us to set the response headers, body and status code in the Web Action.

1
2
3
4
5
6
7
8
9
10
function main () {
  const xml = '...'
  return {
    headers: {
      'Content-Type': 'text/xml'
    },
    code: 200,
    body: xml
  }
}

Web Action Source

Adding the XML response code into the existing function completes the OpenWhisk Web Action required to handle incoming SMS messages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const request = require('request')
const twilio = require('twilio')

const resp = new twilio.TwimlResponse()
const twilml = resp.message('Thanks for letting us know.').toString()

const response = {
  headers: {
    'Content-Type': 'text/xml'
  },
  code: 200,
  body: twilml
}

const slack_message = text => ({
  username: 'smsbot',
  icon_emoji: ':phone:',
  text
})

function main (params) {
  return new Promise(function (resolve, reject) {
    request.post({
      body: slack_message(`Text message from ${params.From}: ${params.Body}`),
      json: true,
      url: params.slack.webhook
    }, err => {
      if (err) return reject(err);
      resolve(response);
    })
  })
}

Register Webhook

Once we have deployed the Web Action, we can configure the Twilio SMS webhook endpoint to use following URL.

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@email.com_dev/default/smsbot-dev-incoming.http

Slack Outgoing Webhook

When someone sends a channel message to the bot, smsbot should send that content as an SMS message to the last person who sent an SMS to the channel. An Outgoing Webhook will be used to trigger the bot.

Outgoing Webhooks have a configurable trigger word. Channel messages which start with this word are send as HTTP requests to the list of URLs registered for that webhook. We will use smsbot as our trigger word.

Request Parameters

Slack sends the following parameters for each channel message.

1
2
3
4
5
6
7
8
9
10
token=XXXXXXXXXXXXXXXXXX
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
timestamp=1355517523.000005
user_id=U2147483697
user_name=Steve
text=googlebot: What is the air-speed velocity of an unladen swallow?
trigger_word=googlebot:

In OpenWhisk Web Actions, these parameters will be available on the function argument object.

Here’s a simple Web Action that would parse and log the message contents when the webhook is fired.

1
2
3
4
function main (params) {
  const msg = params.text.slice(params.trigger_word.length + 1)
  console.log('channel message:', msg)
}

When this webhook is fired, we need to add code to send an SMS message with the channel message.

Sending SMS Messages

Twilio’s API allows us to programatically send SMS messages from our registered numbers.

This snippet shows you how to use their Node.js client library to send sample message.

1
2
3
4
5
6
7
8
9
10
const twilio = require('twilio')
const creds = { account: '...', auth: '...' }

const client = twilio(creds.account, creds.auth)
const callback = (err, message) => {
  if (err) return console.log(err)
  console.log('sent sms message.')
}

client.messages.create({ to: '...', from: '...', body: 'hello world' }, callback)

The webhook should use this client library to send a message to the last person who send us an incoming message.

Reply to Message Sender

How can we determine who was the last person who sent a message to our bot? The Web Action processing the incoming messages is a separate service to the Web Action sending SMS messages.

Rather than setting up a database to share application state, the service can use Twilio’s API to retrieve the received message details.

1
2
3
4
5
6
7
8
const twilio = require('twilio')
const creds = { account: '...', auth: '...' }
const client = twilio(creds.account, creds.auth)

client.messages.list({to: '+44....'}, (err, data) => {
  const last = data.messages[0]
  console.log(`last message from: ${last.from}`)
})

SMSBot Channel Response

Outgoing Webhooks which respond with a JSON body will generate a new channel message.

1
2
3
4
5
{
  "username": "smsbot",
  "icon_emoji": ":phone:",
  "text": "sms sent to ..."
}

Web Action Source

Combing the channel message parsing code with the snippets for sending SMS messages and obtaining the last message sender completes the Web Action needed to handle the Outgoing Webhook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const twilio = require('twilio')

const slack_message = text => ({
  username: 'smsbot',
  icon_emoji: ':phone:',
  text
})

function reply (params) {
  const client = twilio(params.twilio.account, params.twilio.auth)
  return new Promise((resolve, reject) => {
    client.messages.list({to: params.twilio.number}, (err, data) => {
    if (err) return Promise.reject(err)

    const last = data.messages[0]
    const msg = params.text.slice(params.trigger_word.length + 1)
    const options = { to: last.from, from: last.to, body: msg }
    client.messages.create(options, (err, message) => {
        if (err) return Promise.reject(err)
        resolve(slack_message(`sms reply sent to ${last.from}`))
      })
    })
  })
}

Twilio account credentials are bound as default parameters to the Web Action during deployment.

Deployment

smsbot is built using The Serverless Framework.

This framework makes building serverless applications really easy. The tool handles the entire configuration and deployment process for your serverless provider. OpenWhisk recently released integration with the framework through a provider plugin.

Let’s look at how to use the framework to deploy our serverless application…

OpenWhisk

Register for an account with an OpenWhisk provider, e.g. IBM Bluemix.

Set up the wsk CLI and run the command to authenticate against the platform endpoint.

1
wsk property set --apihost openwhisk.ng.bluemix.net --auth SECRET

Serverless Framework

Install the The Serverless Framework and the OpenWhisk provider plugin.

1
npm install --global serverless serverless-openwhisk

Source Code

Download the source code from Github and install the project dependencies.

1
2
3
$ git clone https://github.com/ibmets/smsbot.git
$ cd smsbot
$ npm install

Create a new file called credentials.yml with the following content.

1
2
3
4
5
6
7
twilio:
    account:
    auth:
    number:
numbers:
slack:
    webhook:

Twilio

Register an account with Twilio and provision a new phone number. Make a note of the phone number. Retrieve the account identifier and auth token from the Twilio console.

Fill in the account identifier, auth token and phone number in the credentials.yml file.

1
2
3
4
twilio:
    account: AC_USER_ID
    auth: AUTH_TOKEN
    number: '+441234567890'

Important: the twilio.number property value must be a quoted string.

Phone Numbers

During Twilio’s free trial, you will need manually verify each phone number that you want to send messages to.

Fill in all verified numbers in credentials.yml.

1
2
3
numbers:
    '+441234567890': Joe Smith
    '+441234567891': Jane Smith

Important: the numbers property values must be a quoted strings.

Incoming Webhook

Create a new Incoming Webhook integration for the Slack channel messages should appear in.

Fill in the slack.webhook property in credentials.yml with this url.

1
2
slack:
    webhook: https://hooks.slack.com/services/XXXX/YYYY/ZZZZ

Deploy Application

Use The Serverless Framework to deploy your application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ serverless deploy
Serverless: Packaging service...
Serverless: Compiling Functions...
Serverless: Compiling API Gateway definitions...
Serverless: Compiling Rules...
Serverless: Compiling Triggers & Feeds...
Serverless: Deploying Functions...
Serverless: Deployment successful!

Service Information
platform:   openwhisk.ng.bluemix.net
namespace:  _
service:    smsbot

actions:
smsbot-dev-incoming    smsbot-dev-reply

triggers:
**no triggers deployed**

rules:
**no rules deployed**

endpoints:
**no routes deployed**

Twilio Webhook

On the Phone Numbers page in the Twilio console, configure the ”Messaging” webhook URL.

Use this Web Action URL, replacing user@host.com_dev with your namespace.

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@host.com_dev/default/smsbot-dev-incoming.http

Outgoing Webhook

Create a new Outgoing Webhook integration for the Slack channel messages should appear in. Use smsbot as the Trigger Word.

Use this Web Action URL, replacing user@host.com_dev with your namespace.

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@host.com_dev/default/smsbot-dev-reply.json

Test it out!

Send a text message to the phone number you registered through Twilio. smsbot should post the contents into Slack and send an SMS response with the message ”Thanks for letting us know!”.

If you send a channel message starting with the trigger word (smsbot), the phone number should receive a new SMS message with the message text.

Awesome-sauce 😎.

Conclusions

OpenWhisk Web Actions provide a convenient way to expose serverless functions as simple HTTP APIs. This feature is ideal for implementing webhook endpoints.

Both Slack and Twilio provide webhook integration for developers to use their platforms. Using OpenWhisk Web Actions, we can write serverless functions that act as a bridge between these services. With less than a hundred lines of code, we’ve created a new slack bot that can connect users to channels using SMS messages.

Pretty cool, huh?! 👏👏👏

Openwhisk and the Serverless Framework

The Serverless Framework is the most popular open-source framework for building serverless applications.

Recent releases included support for using the framework with non-AWS providers. This feature makes it easier for developers to try different serverless platforms and move applications between providers.

Since last summer, I’ve been leading the technical effort to provide an OpenWhisk provider plugin for the framework.

OpenWhisk is the first non-AWS serverless provider to complete integration into the framework.

Getting Started

Documentation for the provider plugin is available on The Serverless Framework’s website.

Example projects using the framework to build applications for OpenWhisk are now available in this repository.

More details on the provider plugin can be found on the project repository.

Found an issue? Feature request? Need help? Please open issues on Github.

Video Demonstration

Serverless Meetup Presentation

Last week, I was invited to speak at the London’s Serverless Meetup about building multi-provider serverless applications using this feature.

Slides from the presentation are here:

There is a (low-fi) video recording from the event here:

Watch live video from serverlessldn on www.twitch.tv

OpenWhisk and Rust

This blog post is one of a series looking at using Docker Actions in OpenWhisk to support extra runtimes.

Let’s look at writing serverless functions for OpenWhisk using Rust.

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.

Rust has been growing in popularity since it launched in 2010. Rust is a popular language for writing microservices due to the focus on the attention to safety and strong concurrency support.

None of the major serverless platform natively support Rust at the moment. OpenWhisk does not include this as a default runtime. However, recent updates to OpenWhisk provide a path for writing serverless functions with Rust.

Let’s re-write the example from the previous post in Rust and see how to get it running using this new approach…

Have you seen this post explaining how Docker-based Actions work? This post assumes you have already read that first.

Rust Language Actions

Rust has a build system that supports creating static binaries. These binaries contain the application source code and dependent libraries.

Using the same approach as the Go-based example, bundling this binary into a zip file allows us to overwrite the runtime stub prior to invocation.

Runtime binaries will be executed by the Python-based invoker for each invocation. Request parameters will be passed as a JSON string using the first command-line argument. The invoker expects the Action result to be written to standard output as a JSON string.

Action Source Code

Here’s a simple Rust function that returns a greeting string from an input parameter. It parses the JSON string provided on the command-line to look for a name parameter. If this isn’t present, it defaults to stranger. It returns a JSON object with the greeting string (msg) by writing to the console.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
extern crate rustc_serialize;
use rustc_serialize::json;
use rustc_serialize::json::Json;
use std::env;

#[derive(RustcDecodable, RustcEncodable)]
pub struct Greeting {
    message: String
}

fn main() {
    let mut name = "stranger".to_string();

    // first arg contains JSON parameters
    if let Some(arg1) = env::args().nth(1) {
        // parse JSON and extract 'name' field
        let params = Json::from_str(&arg1).unwrap();
        if let Some(params_obj) = params.as_object() {
            if let Some(params_name) = params_obj.get("name") {
                name = params_name.as_string().unwrap().to_string();
            }
        }
    };

    let greeting = Greeting {
        message: format!("Hello, {}!", name),
    };

    println!("{}", json::encode(&greeting).unwrap());
}

Set Up Project

Using Rust’s package management tool, create a new project for our serverless function.

Add the source code above into the src/main.rs file.

1
2
3
4
5
6
7
8
9
10
11
$ cargo new action; cd action
     Created library `action` project
$ mv src/lib.rs src/main.rs
$ vim src/main.rs
$ tree .
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

This function uses the rustc-serialize crate to handle parsing and producing JSON.

Add this identifier to the project’s dependencies listed in Cargo.toml.

1
2
3
4
5
6
7
[package]
name = "action"
version = "0.1.0"
authors = ["Me <me@email.com>"]

[dependencies]
rustc-serialize = "0.3"

Build and run the binary to test it works as expected.

1
2
3
4
5
6
7
8
9
10
11
$ cargo run
    Updating registry `https://github.com/rust-lang/crates.io-index`
   Compiling rustc-serialize v0.3.22
   Compiling action v0.1.0 (file:///private/tmp/test/action)
    Finished debug [unoptimized + debuginfo] target(s) in 7.0 secs
     Running `target/debug/action`
{"message":"Hello, stranger!"}
$ cargo run '{"name": "James"}'
    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/action {\"name\":\ \"James\"}`
{"message":"Hello, James!"}

Before we can deploy this binary to OpenWhisk, it must be compiled for the platform architecture.

Cross-Compiling Locally

Rust’s compiler uses LLVM under the covers, making it possible to generate machine code for different architectures. Cross-compiling for different platforms requires having the correct compiler, linker and libraries for that architecture installed.

Rust recently released a toolchain manager to simplify this process.

Install the Rust toolchain for the x86_64-unknown-linux-musl runtime.

1
2
3
$ rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'

Add the configuration file to set the correct linker for the runtime.

1
2
3
$ cat .cargo/config
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

We can now cross-compile the binary for the correct environment.

1
2
3
4
$ cargo build --target=x86_64-unknown-linux-musl --release
   Compiling rustc-serialize v0.3.22
   Compiling action v0.1.0 (file:///Users/james/code/bluemix/openwhisk-languages/rust/action)
    Finished release [optimized] target(s) in 9.30 secs

Checking the file type demonstrates we have built a static binary for the Linux x86_64 platform.

1
2
$ file target/x86_64-unknown-linux-musl/release/action
target/x86_64-unknown-linux-musl/release/action: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, not stripped

Cross-Compiling Using Docker

If you don’t want to install the Rust development toolchain, Docker can be used to start a container with the environment set up.

1
2
3
4
5
6
7
8
$ docker pull ekidd/rust-musl-builder
$ docker run -it -v $(pwd):/home/rust/src ekidd/rust-musl-builder cargo build --release
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rustc-serialize v0.3.22
   Compiling action v0.1.0 (file:///home/rust/src)
    Finished release [optimized] target(s) in 1.80 secs
$ file target/x86_64-unknown-linux-musl/release/action
target/x86_64-unknown-linux-musl/release/action: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, not stripped

Create & Deploy Archive

Add the binary to a zip file, ensuring the file is named exec in the archive.

Use the wsk command-line to create a new Docker Action using this archive.

1
2
3
4
5
$ cp target/x86_64-unknown-linux-musl/release/action exec
$ zip action.zip exec
  adding: exec (deflated 64%)
$ wsk action create rust_test action.zip --docker
ok: created action rust_test

Invoking Action

Test the action from the command-line to verify it works.

1
2
3
4
5
6
7
8
$ wsk action invoke rust_test --blocking --result
{
    "msg": "Hello, Stranger!"
}
$ wsk action invoke rust_test --blocking --result --param name James
{
    "msg": "Hello, James!"
}

Success 😎.

Openwhisk and Go

In an earlier blog post, I explained how to use Go language binaries on OpenWhisk using Docker-based Actions. It relied on building Docker images for each serverless function and hosting them on Docker Hub.

Recent updates to Docker-based Actions have made this process much simpler. Developers don’t need to build and expose public images anymore.

Let’s re-visit the example from the previous post and see how to get it running using this new approach…

Have you seen this post explaining how Docker-based Actions work? This post assumes you have already read that first.

Go Language Actions

Go’s build system combines application source code and dependencies into a single execution binary. Bundling this binary into a zip file allows us to overwrite the runtime stub prior to invocation.

Runtime binaries will be executed by the Python-based invoker for each invocation. Request parameters will be passed as a JSON string using the first command-line argument. The invoker expects the Action result to be written to standard output as a JSON string.

Action Source Code

Here’s a simple Go function that returns a greeting string from an input parameter. It parses the JSON string provided on the command-line to look for a name parameter. If this isn’t present, it defaults to Stranger. It returns a JSON object with the greeting string (msg) by writing to the console.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "encoding/json"
import "fmt"
import "os"

func main() {
  // native actions receive one argument, the JSON object as a string
  arg := os.Args[1]

  // unmarshal the string to a JSON object
  var obj map[string]interface{}
  json.Unmarshal([]byte(arg), &obj)
  name, ok := obj["name"].(string)
  if !ok {
      name = "Stranger"
  }
  msg := map[string]string{"msg": ("Hello, " + name + "!")}
  res, _ := json.Marshal(msg)
  fmt.Println(string(res))
}

Building this locally allows us to test it works.

1
2
$ go run test.go '{"name": "James"}'
{"msg":"Hello, James!"}

Before we can deploy this binary to OpenWhisk, it must be compiled for the platform architecture.

Cross-Compiling Locally

Go 1.5 introduced much improved support for cross-compilation.

If you have the development environment installed locally, you can compile the binary for another platform by setting environment variables. The full list of supported architectures is available here.

OpenWhisk uses an Alpine Linux-based environment to execute Actions.

1
$ env GOOS=linux GOARCH=amd64 go build exec.go

Checking the file type demonstrates we have built a static binary for the Linux x86_64 platform.

1
2
$ file exec
exec: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Cross-Compiling Using Docker

If you don’t want to install the Go development toolchain, Docker can be used to start a container with the environment set up.

1
2
3
4
5
6
7
8
$ docker pull golang
$ docker run -it -v $(pwd):/go/src golang
root@0a2f1655eece:/go# cd src/
root@0a2f1655eece:/go/src# go build exec.go
root@0a2f1655eece:/go/src# ls
exec  exec.go
$ file exec
exec: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Create & Deploy Archive

Add the binary to a zip file, ensuring the file is named exec in the archive.

Use the wsk command-line to create a new Docker Action using this archive.

1
2
3
4
$ zip action.zip exec
  adding: exec (deflated 66%)
$ wsk action create go_test action.zip --docker
ok: created action go_test

Invoking Action

Test the action from the command-line to verify it works.

1
2
3
4
5
6
7
8
$ wsk action invoke go_test --blocking --result
{
    "msg": "Hello, Stranger!"
}
$ wsk action invoke go_test --blocking --result --param name James
{
    "msg": "Hello, James!"
}

Success 😎.

OpenWhisk Docker Actions

OpenWhisk recently announced the following changes to Docker-based Actions.

Developers can now deploy runtime files to the Action environment prior to invocation.

This makes it much easier to support (almost) any programming language in OpenWhisk. Awesome!

Let’s start by explaining how this new feature works…

Docker Actions

Docker Actions in OpenWhisk are built from the following repository using the python:2.7.12-alpine base image. This image is available on Docker Hub as openwhisk/dockerskeleton.

The image includes a Python application which implements the HTTP API used to handle platform requests, e.g. invoke the action with these parameters.

This service executes a file (/action/exec) for each invocation. Replacing this file allows us to control the runtime environment.

Request parameters are passed, using a JSON string, as the first command-line argument. Response values are interpreted as JSON written to stdout.

Developers can now include a zip file when creating Docker-based Actions. This archive will be extracted into the /action directory prior to invocations. If the archive contains a file named exec this will replace the exectuable file called by the invocation handler.

Testing It Out

Using the wsk command-line, developers can create Actions using this Docker image.

If the archive file is missing, the /action/exec path contains the the following stub file.

1
2
3
4
$ wsk action create skeleton --docker openwhisk/dockerskeleton
ok: created action skeleton
$ wsk action invoke skeleton --blocking --result
{ "error": "This is a stub action. Replace it with custom logic." }

Let’s update this stub file to return a custom greeting.

1
2
3
4
5
6
7
8
9
10
11
$ cat exec
#!/bin/bash
echo "{ \"hello\": \"ran without a docker pull!\" }"
$ ./exec
{ "hello": "ran without a docker pull!" }
$ zip exec.zip exec
  adding: exec (stored 0%)
$ wsk action create custom_docker_action exec.zip --docker
ok: created action custom_docker_action
$ wsk action invoke custom_docker_action --blocking --result
{ "hello": "ran without a docker pull!" }

The archive file could include a static binary, or even a complete runtime, to replace the exec stub.

All files in the archive file will be available under the /action directory.

Running Locally

The openwhisk/dockerskeleton image exposes a Python-based HTTP server on port 8080.

Pulling the openwhisk/dockerskeleton image from Docker Hub allows us to run it locally for development.

1
2
$ docker pull openwhisk/dockerskeleton
$ docker run -it -p 8080:8080 openwhisk/dockerskeleton

The platform uses the following HTTP endpoints to initialise and invoke Actions.

  • POST /init -> Set up Action source from JSON payload.
  • POST /run -> Invoke Action

Initialising The Environment

Before invoking Actions using this image, we need to deploy and unpack the archive file into the /action directory.

Reviewing the Python source code, the platform triggers this by sending a HTTP POST with the following JSON to /init endpoint.

1
2
3
4
5
6
{
  "value": {
    "binary": true,
    "code": "..."
  }
}

code contains the archive file as a base64 encoded string.

Let’s try this out using the action archive we created above.

1
2
3
4
5
6
7
8
$ base64 exec.zip  | echo "\"$(cat)\"" | jq '{value: {binary: true, code: .}}' > init.json
$ cat init.json
{
  "value": {
    "binary": true,
    "code": "UEsDBAoAAAAAAOlqMEr1+JNAQQAAAEEAAAAEABwAZXhlY1VUCQADRcl8WFDJfFh1eAsAAQT1AQAABBQAAAAjIS9iaW4vYmFzaAplY2hvICJ7IFwiaGVsbG9cIjogXCJyYW4gd2l0aG91dCBhIGRvY2tlciBwdWxsIVwiIH0iClBLAQIeAwoAAAAAAOlqMEr1+JNAQQAAAEEAAAAEABgAAAAAAAEAAADtgQAAAABleGVjVVQFAANFyXxYdXgLAAEE9QEAAAQUAAAAUEsFBgAAAAABAAEASgAAAH8AAAAAAA=="
  }
}

Now we can issue the HTTP request to push this archive into the container.

1
2
3
4
5
6
7
$ http post localhost:8080/init < init.json
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: text/html; charset=utf-8
Date: Mon, 16 Jan 2017 14:11:04 GMT

OK

Accessing the container filesystem allows us to verify the archive has been extracted correctly.

1
2
3
4
5
6
7
8
9
10
$ docker ps
CONTAINER ID        IMAGE                         COMMAND                  CREATED             STATUS              PORTS                    NAMES
b37a7dc1cab1        openwhisk/dockerskeleton      "/bin/bash -c 'cd ..."   About an hour ago   Up About an hour    0.0.0.0:8080->8080/tcp   relaxed_davinci
$ docker exec -it b37a7dc1cab1 /bin/sh
/ # cd /action
/action # ls
exec
/action # cat exec
#!/bin/bash
echo "{ \"hello\": \"ran without a docker pull!\" }"

Invocation Requests

Action invocations are triggered by sending a HTTP POST to the /run endpoint.

This endpoint expects the following JSON body.

1
2
3
4
5
{
  "value": {
    "foo": "bar"
  }
}

The inner object parameters under the value property are passed, as a JSON string, to the executable as the first command-line argument.

Sending this request to our container will trigger the shell script from our archive and return the JSON response.

1
2
3
4
5
6
7
8
9
$ echo "{}" | jq '{value: .}' | http post localhost:8080/run
HTTP/1.1 200 OK
Content-Length: 44
Content-Type: application/json
Date: Mon, 16 Jan 2017 14:17:15 GMT

{
    "hello": "ran without a docker pull!"
}

Conclusion

Recent updates to Docker-based Actions in OpenWhisk make it much easier to customise the runtime environment.

Being able to deploy arbitrary files into the runtime container, prior to invocation, simplifies the process of supporting new runtimes.

Hopefully this blog post has shown you how to get started with this feature.

Over the next few weeks, we’re going to show you how to use this approach to run lots of new programming languages on the platform. Stay tuned for updates…

OpenWhisk Docker Actions

OpenWhisk recently announced the following changes to Docker-based Actions.

Developers can now deploy runtime files to the Action environment prior to invocation.

This makes it much easier to support (almost) any programming language in OpenWhisk. Awesome!

Let’s start by explaining how this new feature works…

Docker Actions

Docker Actions in OpenWhisk are built from the following repository using the python:2.7.12-alpine base image. This image is available on Docker Hub as openwhisk/dockerskeletion.

The image includes a Python application which implements the HTTP API used to handle platform requests, e.g. invoke the action with these parameters.

This service executes a file (/action/exec) for each invocation. Replacing this file allows us to control the runtime environment.

Request parameters are passed, using a JSON string, as the first command-line argument. Response values are interpreted as JSON written to stdout.

Developers can now include a zip file when creating Docker-based Actions. This archive will be extracted into the /action directory prior to invocations. If the archive contains a file named exec this will replace the exectuable file called by the invocation handler.

Testing It Out

Using the wsk command-line, developers can create Actions using this Docker image.

If the archive file is missing, the /action/exec path contains the the following stub file.

1
2
3
4
$ wsk action create skeleton --docker openwhisk/dockerskeleton
ok: created action skeleton
$ wsk action invoke skeleton --blocking --result
{ "error": "This is a stub action. Replace it with custom logic." }

Let’s update this stub file to return a custom greeting.

1
2
3
4
5
6
7
8
9
10
11
$ cat exec
#!/bin/bash
echo "{ \"hello\": \"ran without a docker pull!\" }"
$ ./exec
{ "hello": "ran without a docker pull!" }
$ zip exec.zip exec
  adding: exec (stored 0%)
$ wsk action create custom_docker_action exec.zip --docker
ok: created action custom_docker_action
$ wsk action invoke custom_docker_action --blocking --result
{ "hello": "ran without a docker pull!" }

The archive file could include a static binary, or even a complete runtime, to replace the exec stub.

All files in the archive file will be available under the /action directory.

Running Locally

The openwhisk/dockerskeleton image exposes a Python-based HTTP server on port 8080.

Pulling the openwhisk/dockerskeleton image from Docker Hub allows us to run it locally for development.

1
2
$ docker pull openwhisk/dockerskeleton
$ docker run -it -p 8080:8080 openwhisk/dockerskeleton

The platform uses the following HTTP endpoints to initialise and invoke Actions.

  • POST /init -> Set up Action source from JSON payload.
  • POST /run -> Invoke Action

Initialising The Environment

Before invoking Actions using this image, we need to deploy and unpack the archive file into the /action directory.

Reviewing the Python source code, the platform triggers this by sending a HTTP POST with the following JSON to /init endpoint.

1
2
3
4
5
6
{
  "value": {
    "binary": true,
    "code": "..."
  }
}

code contains the archive file as a base64 encoded string.

Let’s try this out using the action archive we created above.

1
2
3
4
5
6
7
8
$ base64 exec.zip  | echo "\"$(cat)\"" | jq '{value: {binary: true, code: .}}' > init.json
$ cat init.json
{
  "value": {
    "binary": true,
    "code": "UEsDBAoAAAAAAOlqMEr1+JNAQQAAAEEAAAAEABwAZXhlY1VUCQADRcl8WFDJfFh1eAsAAQT1AQAABBQAAAAjIS9iaW4vYmFzaAplY2hvICJ7IFwiaGVsbG9cIjogXCJyYW4gd2l0aG91dCBhIGRvY2tlciBwdWxsIVwiIH0iClBLAQIeAwoAAAAAAOlqMEr1+JNAQQAAAEEAAAAEABgAAAAAAAEAAADtgQAAAABleGVjVVQFAANFyXxYdXgLAAEE9QEAAAQUAAAAUEsFBgAAAAABAAEASgAAAH8AAAAAAA=="
  }
}

Now we can issue the HTTP request to push this archive into the container.

1
2
3
4
5
6
7
$ http post localhost:8080/init < init.json
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: text/html; charset=utf-8
Date: Mon, 16 Jan 2017 14:11:04 GMT

OK

Accessing the container filesystem allows us to verify the archive has been extracted correctly.

1
2
3
4
5
6
7
8
9
10
$ docker ps
CONTAINER ID        IMAGE                         COMMAND                  CREATED             STATUS              PORTS                    NAMES
b37a7dc1cab1        openwhisk/dockerskeleton      "/bin/bash -c 'cd ..."   About an hour ago   Up About an hour    0.0.0.0:8080->8080/tcp   relaxed_davinci
$ docker exec -it b37a7dc1cab1 /bin/sh
/ # cd /action
/action # ls
exec
/action # cat exec
#!/bin/bash
echo "{ \"hello\": \"ran without a docker pull!\" }"

Invocation Requests

Action invocations are triggered by sending a HTTP POST to the /run endpoint.

This endpoint expects the following JSON body.

1
2
3
4
5
{
  "value": {
    "foo": "bar"
  }
}

The inner object parameters under the value property are passed, as a JSON string, to the executable as the first command-line argument.

Sending this request to our container will trigger the shell script from our archive and return the JSON response.

1
2
3
4
5
6
7
8
9
$ echo "{}" | jq '{value: .}' | http post localhost:8080/run
HTTP/1.1 200 OK
Content-Length: 44
Content-Type: application/json
Date: Mon, 16 Jan 2017 14:17:15 GMT

{
    "hello": "ran without a docker pull!"
}

Conclusion

Recent updates to Docker-based Actions in OpenWhisk make it much easier to customise the runtime environment.

Being able to deploy arbitrary files into the runtime container, prior to invocation, simplifies the process of supporting new runtimes.

Hopefully this blog post has shown you how to get started with this feature.

Over the next few weeks, we’re going to show you how to use this approach to run lots of new programming languages on the platform. Stay tuned for updates…

NPM Modules in OpenWhisk

OpenWhisk now supports creating Node.js Actions from a zip file. The archive file will be extracted into the runtime environment by the platform. This allows us to split microservice logic across multiple files, use third-party NPM modules or include non-JavaScript assets (configuration files, images, HTML files).

“Hello World” Example

Let’s look at a “Hello World” example of registering a serverless function from a zip file. Our archive will contain two files, the package descriptor and a JavaScript file.

Here is the minimal package.json file required for loading a module from a directory.

package.json
1
2
3
{
  "main": "my_file.js"
}

In my_file.js, a function is returned through the main property on the exports object. This function implements the Action interface.

my_file.js
1
2
3
exports.main = function (params) {
  return {result: "Hello World"};
};

Creating a zip file from the current directory, we can deploy this Action through the command-line utility.

1
2
$ zip -r action.zip *
$ wsk action create hello_world --kind nodejs:default action.zip

When this Action is invoked, the archive will be unzipped into a temporary directory. OpenWhisk loads the directory as a Node.js module and invokes the function property on the module for each invocation.

1
2
3
4
$ wsk action invoke hello_world --result
{
    "result": "Hello world"
}

Using NPM Dependencies

Let’s look a more complicated example which uses an external NPM module in our Action.

index.js
1
2
3
4
5
6
7
8
const leftPad = require("left-pad")

function myAction(args) {
    const lines = args.lines || [];
    return { padded: lines.map(l => leftPad(l, 30, ".")) }
}

exports.main = myAction;

This module uses the extremely popular left-pad module to process an array of strings, passed through a request parameter. The resulting output is returned in the response.

Before using this module, we need to install the dependencies listed in package.json.

1
2
3
4
5
6
7
8
{
  "name": "my-action",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies" : {
    "left-pad" : "1.1.3"
  }
}

OpenWhisk does not automatically install dependencies listed in package.json in the runtime environment.

The developer has to run npm install locally and include the node_modules directory in the zip file.

  • Install NPM dependencies locally.
1
$ npm install
  • Create a .zip archive containing all files.
1
$ zip -r action.zip *
  • Create the action using command-line utility.
1
$ wsk action create packageAction --kind nodejs:default action.zip

Now we can test out our action to check it works….

1
2
3
4
5
6
7
8
$ wsk action invoke --blocking --result packageAction --param lines "[\"and now\", \"for something completely\", \"different\" ]"
{
    "padded": [
        ".......................and now",
        "......for something completely",
        ".....................different"
    ]
}

Native Module Dependencies

Node.js provides a mechanism for JavaScript modules to include native platform code as if they were ordinary modules. This is often used to improve performance by deferring operations to native C/C++ libraries. NPM handles compiling native code during the dependency install process.

Using modules with native dependencies in Actions requires the native code to be compiled for the platform runtime.

Compiling dependencies with Docker

One solution to this problem uses Docker to simulate the same runtime environment.

OpenWhisk uses Docker to manage the runtime environments for Actions. The buildpack-deps:trusty-curl image is used as the base image for all Node.js Actions.

Running a local container from this image will give access to the same runtime environment. Running npm install within this container will produce the node_modules directory with native code compiled for the correct architecture.

Action With Native Modules

Let’s look at an example…

index.js
1
2
3
4
5
6
7
8
9
const SHA3 = require('sha3');

function SHA(args) {
  const d = new SHA3.SHA3Hash();
  d.update(args.payload);
  return { sha: d.digest('hex') };
}

exports.main = SHA;

This module returns a function that calculates a SHA3 cryptographic hash for the invocation payload. The hex string for the hash is returned as the function response.

The NPM module (sha3) used to calculate the digest uses a C++ extension for the hashing algorithm.

package.json
1
2
3
4
5
6
7
8
{
  "name": "hashing-service",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "sha3": "^1.2.0"
  }
}

Action Runtime Environments

OpenWhisk uses a public Docker image as the base image for the Action environments. It then builds a custom image by installing Node.js and NPM for the particular runtime version.

Rather than building this image ourselves, we can use existing images published on Docker Hub.

NodeSource provides public Docker images pre-installed with different Node.js versions. Provided the base image (Ubuntu Trusty) and Node.js version (6.7) matches, the runtime environment will be the same.

Starting a local container from this image, we can use Docker’s host volume support to mount the local directory into the host container.

1
$ docker run -it -v "/action:/usr/src/app" nodesource/trusty:6.7 /bin/sh

Running npm install in the container, the sha3 dependency is compiled and installed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# npm install

> sha3@1.2.0 install /usr/src/app/node_modules/sha3
> node-gyp rebuild

make: Entering directory `/usr/src/app/node_modules/sha3/build'                                                       
make: Warning: File `sha3.target.mk' has modification time 0.19 s in the future
  CXX(target) Release/obj.target/sha3/src/addon.o
  CXX(target) Release/obj.target/sha3/src/displayIntermediateValues.o
  CXX(target) Release/obj.target/sha3/src/KeccakF-1600-reference.o
  CXX(target) Release/obj.target/sha3/src/KeccakNISTInterface.o
  CXX(target) Release/obj.target/sha3/src/KeccakSponge.o
  SOLINK_MODULE(target) Release/obj.target/sha3.node
  COPY Release/sha3.node
make: warning:  Clock skew detected.  Your build may be incomplete.
make: Leaving directory `/usr/src/app/node_modules/sha3/build'
my-action@1.0.0 /usr/src/app
`-- sha3@1.2.0
  `-- nan@2.4.0

The node_modules directory will be available on the host system after exiting the container. Repeat the steps above to archive the source files and deploy our serverless function.

1
2
3
$ zip -r action.zip *
$ wsk action create packageAction --kind nodejs:6 action.zip
ok: created action packageAction

Invoking the Action will now use the native code to produce hash values for the invocation parameters.

1
2
3
4
5
$ wsk action invoke packageAction -b -p payload "Hello" --result
{
    "sha": "c33fede18a1ae53ddb8663710f8054866beb714044fce759790459996196f101d94dfc7bd8268577f7ee3d2f8ff0cef4004a963222
7db84df62d2b40682d69e2"
}

Action Package Details

Upon invocation, OpenWhisk extracts the action’s zip file to a temporary directory in the runtime environment. It then loads the directory as a standard Node.js module, using require.

Node.js expects the directory to have a valid package.json file. The main property is used to define which JavaScript file is evaluated when the module is loaded. This file can assign values to the global exports object. These references are then returned when require is called for this module.

OpenWhisk expects the returned module object to have a property called main which references a function. This function will be executed for each invocation request.

Request parameters are passed as object properties on the first function argument. The function must return an object for the invocation response.

Other files included in the archive will be available in the current working directory. These can also be loaded as modules or read directly from the file-system.

Conclusions

OpenWhisk support for Action packages is a huge step forward for the platform. Node.js has an enormous ecosystem of third-party modules. Developers can now easily use any of these modules within their Actions.

This feature can also be used to include non-JS files within the runtime environment. It would be possible to use configuration files in JSON or static assets like HTML or CSS files.

The team are now working on providing support for other runtimes, watch this space…

Serverless Logs With Elasticsearch

Serverless platforms can seem like magic.

Taking your code and turning it into scalable microservices in the cloud without having to set up or manage any infrastructure.

No provisioning VMs. No configuring Linux environments. No upgrading middleware packages.

Which is wonderful until something goes wrong with your microservices in production…

“Let me just log into the machine.”

Serverless platforms do not allow this.

No tracing system calls. No running top. No connecting a debugger to the process. You can’t even grep through the logs!

Many of the tools and techniques we use to diagnose bugs rely on having access to the environment.

Fortunately, we do still have access to logging output generated by our serverless functions. Phew.

Storing, searching and analysing these logs is crucial to efficiently diagnosing and fixing issues on serverless platforms.

In this blog post, we’re going to look at using a popular open-source solution to manage the logs being generated by our serverless functions. This solution is also known as ”The ELK Stack”.

TLDR: There is now a Logstash input plugin for OpenWhisk. This will automatically index serverless application logs into Elasticsearch. See here for usage instructions: https://github.com/jthomas/logstash-input-openwhisk

Elasticsearch, Logstash and Kibana

…are the three open-source projects that, when combined, are known as The ELK Stack. It provides a scalable search engine for indexed documents.

Elasticsearch “is a search engine based on Lucene. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.”

Logstash is a tool for managing events and logs. You can use it to collect logs, parse them, and store them for later use (like, for searching). If you store them in Elasticsearch, you can view and analyze them with Kibana.

Kibana is an open source analytics and visualization platform designed to work with Elasticsearch. You use Kibana to search, view, and interact with data stored in Elasticsearch.

The ELK Stack is a perfect solution for managing logs from our serverless functions.

But how do we configure this solution to automatically index logs from our serverless platform?

Let’s start by looking serverless platform we are using…

OpenWhisk

OpenWhisk is an open-source serverless platform developed by IBM. Developers deploy functions to execute in response to external events, e.g. database updates, messages on a queue or HTTP requests. The platform invokes these functions on-demand in milliseconds, rather than having services sat idle waiting for requests to arrive.

Let’s walk through an example.

Serverless Functions

Here’s a sample serverless function which returns a greeting to the user. The code logs the invocation parameters and response message.

logs.js
1
2
3
4
5
6
7
8
9
function main (params) {
  console.log('invoked with parameters:', params)

  const user = params.user || 'Donald Trump'
  const response = { greeting: `Hello ${user}` }

  console.log('returns: ', response)
  return response
}

Deploying this serverless function to OpenWhisk and invoking it generates an activation record.

1
2
3
4
5
6
7
8
9
10
$ wsk action create logs logs.js
ok: created action logs
$ wsk action invoke logs -b -r -p user 'Bernie Sanders'
{
    "greeting": "Hello Bernie Sanders"
}
$ wsk activation list
activations
2adbbbcc0242457f80dc51944dcd2039                 logs
...

OpenWhisk activation records are available through the platform API. Each record contains the stdout and stderr logs generated during the serverless function invocation.

Serverless Logs

Retrieving the activation record for the previous invocation, we can see the output generated by the calls to console.log.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ wsk activation get 2adbbbcc0242457f80dc51944dcd2039
ok: got activation 2adbbbcc0242457f80dc51944dcd2039
{
    "namespace": "james.thomas@uk.ibm.com",
    "name": "logs",
    "version": "0.0.3",
    "publish": false,
    "subject": "james.thomas@uk.ibm.com",
    "activationId": "2adbbbcc0242457f80dc51944dcd2039",
    "start": 1477925373990,
    "end": 1477925374063,
    "response": {
        "status": "success",
        "statusCode": 0,
        "success": true,
        "result": {
            "greeting": "Hello Bernie Sanders"
        }
    },
    "logs": [
        "2016-10-31T14:49:34.059745626Z stdout: invoked with parameters: {}",
        "2016-10-31T14:49:34.061228724Z stdout: returns:  { greeting: 'Hello Donald Trump' }"
    ],
    ...
}

OpenWhisk stores these records indefinitely, making them available for retrieval by the activation id.

However, developers need more than being able to retrieve logs to be effective at diagnosing and resolving issues with serverless functions.

Forwarding these logs to Elasticsearch will enable us to run full-text search across all logs generated, quickly retrieve all output for a particular serverless function, set up monitoring dashboards and much more…

Using Logstash will allow us to ingest and transform OpenWhisk logs into Elasticsearch documents.

Logstash Input Plugins

Logstash supports a huge variety of event sources through the use of a plugin mechanism. These plugins handle retrieving the external events and converting them to Elasticsearch documents.

Logstash has a huge repository of official and community supported input plugins. These plugins ingest everything from log files, syslog streams, databases, message queues, websockets and much more.

HTTP Polling Input Plugin

Logstash already has an input plugin for pulling events from a HTTP URL by polling. Users provide the URL in the logstash configuration, along with the polling schedule. Logstash will automatically retrieve and ingest the JSON response as an event stream.

1
2
3
4
5
6
7
8
9
10
11
input {
  http_poller {
    urls => {
      "my_events" => "http://localhost:8000/events"
    }
    # Poll site every 10s
    interval => 10
    request_timeout => 60
    codec => "json"
  }
}

Great, so we can configure this plugin to call OpenWhisk API for retrieving activation records and automatically ingest them into Elasticsearch?

Unfortunately not…

Polling OpenWhisk Logs?

Each time the client calls the API to retrieve the activation records, we want to retrieve only those records that have occurred since the last poll. This ensures we are not ingesting the same records more than once.

The OpenWhisk API for retrieving activation records supports a query parameter (since) which restricts results to those that occurred after the parameter value’s timestamp.

Using this parameter in the polling URL, updated to the value of the last polling time, will allow us to ensure we only retrieve new activation records.

Unfortunately, the HTTP input plugin does not support setting dynamic query string parameters.

This means we cannot use the existing plugin to efficiently ingest OpenWhisk logs into Elasticsearch.

So we started work on a new plugin to support this behaviour…

OpenWhisk Input Plugin

This input plugin drains logs from OpenWhisk into Elasticsearch.

Install the plugin with the following command.

1
$ bin/logstash-plugin install logstash-input-openwhisk

Once the plugin is installed, you need to configure Logstash with your platform endpoint and user credentials.

This sample configuration will poll the OpenWhisk platform for new logs every fifteen minutes and index them into Elasticsearch. Each activation record will be a separate document.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
input {
  openwhisk {
    # Mandatory Configuration Parameters
    hostname => "openwhisk.ng.bluemix.net"
    username => "sample_user@email.com"
    password => "some_password"
    # Supports "cron", "every", "at" and "in" schedules by rufus scheduler
    schedule => { "every" => "15m"}
  }
}

output {
  elasticsearch {
    hosts => ["localhost:9200"]
  }
}

The plugin supports the same configuration values for the schedule parameter as the HTTP input plugin.

More examples of using the plugin are available in the examples directory in the project repository.

Demonstration

Here’s a demonstration of the OpenWhisk input plugin being used in the ELK stack. As we invoke serverless functions in OpenWhisk, Kibana shows the activation records appearing in the dashboard. Logstash is polling the logs API and ingesting the records into Elasticsearch in real-time.

Conclusion

Developers using serverless platforms have no access to the infrastructure environment running their code. Debugging production bugs relies on using logging output to diagnose and resolve issues.

Elasticsearch, Logstash and Kibana has become the scalable open-source solution for log management and analysis.

Using the Logstash plugin for OpenWhisk, serverless logs will be automatically indexed into Elasticsearch in real-time. Developers can use the Kibana frontend to easily diagnose and monitor issues in production.

In the next post, we’ll look at using Docker to set up Elasticsearch, Logstash and Kibana with our custom OpenWhisk plugin.

Until then… 😎

OpenWhisk Workshop

serverless london

Serverless Conference comes to London later this month.

IBM will be hosting a full-day workshop at the event. Developers can come and learn how to use OpenWhisk, the open-source serverless platform.

I’m going to be one of the mentors on the day, along with members from the product team.

Working on training material for the session, I remembered that the Node.js community had a popular workshop tool for running training sessions around the world.

NodeSchool

NodeSchool provides developers with a command-line utility that helps them learn the platform. This tool provides a series of interactive exercises to test their knowledge. Each exercise requires the developers to write some code. The application can then verify their solution and record their progress.

The Node.js community open-sourced the tools used to develop NodeSchool. 

Using this toolchain makes it simple to create similar exercise-led workshops for developers.

OpenWhiskSchool?

OpenWhisk has great documentation. The project repository includes Markdown files for each feature of the platform.

Would it be possible to use this material with the NodeSchool toolchain to create an interactive OpenWhisk workshop for developers?

Developers would review the relevant documentation for a particular feature and use the tool to test their knowledge through an interactive exercise.

Each exercise would require them to build, deploy and configure a sample serverless function which used that platform feature.

After getting set up with the toolchain and reviewing other example projects, we started work on it…

openwhisk-workshop

🎉 Developers can now install the workshop from NPM as a global command. 🎉

1
$ npm install -g openwhisk-workshop

This tool needs the OpenWhisk command-line utility installed and authenticated against an instance of the platform. For more details on getting this environment setup, see the following documentation here.

Once the tool is installed, developers can open the application by running the following command.

1
$ openwhisk-workshop

overview

The list of exercises will be displayed, along with current completion progress. Using the arrow keys () to navigate the menu, press RETURN to open an exercise.

On selecting an exercise, the problem challenge will be printed to the terminal.

exercise

Each exercise comes with a documentation page which explains the concepts behind the challenge. Use the following command to display the exercise documentation in the terminal.

1
$ openwhisk-workshop more

Once the developer has solved the challenge, they can verify their solution with the following command.

1
$ openwhisk-workshop verify

If their solution is correct, that task is marked as completed and the utility returns to the list of exercises. Developers can continue working through the exercises until they have completed them all.

verify

feedback

If you have problems with the workshop, please raise an issue in the repository.

Need more general help with OpenWhisk?