psuter.net

Node.js on OpenWhisk

An edited version of this post first appeared on an external blog. To read more about OpenWhisk and Bluemix, head to the Bluemix Blog.

In the months preceding the general availability of OpenWhisk on Bluemix, the development team made many improvements to the user experience of programming Node.js actions. This post covers the most important ones.

Pick your runtime

Users can now decide which version of Node.js to use to run their actions.

This is exposed through the notion of an action kind. When using the command line tool wsk to create or update an action, we can specify the kind using the --kind flag.

Two versions of Node.js are currently supported:

Assuming a file version.js with the following content:

function main() {
    return { 'version': process.version };
}

We can observe the effect of the --kind used for action creation/update:

$ wsk action create node_version version.js --kind nodejs
ok: created action node_version
$ wsk action invoke -br node_version
{
    "version": "v0.12.17"
}

…and similarly:

$ wsk action update node_version version.js --kind nodejs:6
ok: updated action node_version
$ wsk action invoke -br node_version
{
    "version": "v6.9.1"
}

Note that the exact versions may change over time without notice, but --kind nodejs will always correspond to a version in the 0.12.x series, and --kind nodejs:6 to a version in the 6.x.x series.

In addition to the two described above, there is a special kind nodejs:default; currently, it is equivalent to nodejs:6, but over time it will reflect advances in the ecosystem and resolve to the most recent stable version supported on OpenWhisk. Creating a Node.js action without specifying the kind is akin to using nodejs:default, so all actions created today already run on Node 6 by default.

We encourage users with actions created with the Node.js 0.12.x runtime to update them to use the most recent version.

A taste of ES6

One major advantage of Node 6 is the support for a large set of improvements to JavaScript introduced in the ECMAScript June 2015 specification — commonly referred to as ES6: Node.js 6 is reported to cover 99% of the standard.

To mention some of the most visible features:

As a concrete example, the following is a valid JavaScript action of the nodejs:6 kind:1

var main = args => {
    let { name = 'stranger' } = args
    return { greetings: `Hello ${name}!` }
}

To the promised land

Another change related to evolving trends in the JavaScript world is that OpenWhisk actions can now use Promises to compute and return results asynchronously.

JavaScript Promises offer an encapsulated and composable way of representing asynchronous computations that may succeed or fail, and are therefore a natural fit for OpenWhisk actions. Functions manipulating Promises are often easier to reason about and to compose that their callback-passing versions.

With this evolution, the signature for OpenWhisk JavaScript actions is now:

As an illustration, the action below consumes a JSON API, reformats its response and handles errors:

function main(args) {
  const request = require('request-promise')

  const reqPromise = request({
    uri: 'https://api.github.com/users/openwhisk/repos',
    json: true,
    headers: { 'User-Agent': 'OpenWhisk' }
  })

  return reqPromise
    .then(res => ({ repos: res.map(obj => obj.name) }))
    .catch(err => ({ error: `There was an error fetching repos: ${err}.`}))
}

Using Promises to return asynchronous results superseeds the previous mechanism of using a combination of whisk.async(), whisk.done() and whisk.error(), and these functions are now marked as deprecated.

Packed and ready to go

Maybe the most important change for Node.js on OpenWhisk in terms of unlocking developer productivity has been the introduction of packaged actions: OpenWhisk now allows the creation of JavaScript actions from an archived NPM module.

JavaScript developers are used to the power of the NPM ecosystem, and the magic of npm install. The OpenWhisk Node.js runtimes include some common libraries by default for convenience, but there will always be requirements unique to specific users.

The steps to create an OpenWhisk packaged action are as follows:

Running npm install locally ensures that users know exactly which versions of dependencies were in use when the action was created.

Packaged actions by example

Consider a directory with two files, package.json:

{
  "name": "test-action",
  "version": "1.0.0",
  "description": "An action written as an npm package.",
  "main": "index.js",
  "author": "OpenWhisk team",
  "license": "Apache-2.0",
  "dependencies": {
    "prog-quote": "2.0.0"
  }
}

…and index.js:

function entryPoint(args) {
    const pq = require('prog-quote')();
    return pq.next().value;
}

module.exports.main = entryPoint;

Running npm install will fetch all dependencies into node_modules:

$ npm install
...
$ find node_modules/ | wc -l
      44

Now that all dependencies are available locally, we package the action. We must create a .zip file. Other archive formats such as .gz are not currently supported.

$ zip -r action.zip .

We can now create the OpenWhisk action. When creating an action from an archive, we must specify the kind, as wsk cannot infer it from the file extension.

$ wsk action create quotes action.zip --kind nodejs:default

We confirm that the action works as expected:

$ wsk action invoke -br quotes
{
  "author": "Ken Thompson",
  "quote": "When in doubt, use brute force."
}

Some tips:

Reflexive invocations

Actions were always able to programmatically invoke other actions or fire triggers. These mechanisms are useful when writing, e.g., custom advanced pipelines. This facility was exposed through whisk.invoke() and whisk.fire(). The whisk object however is magical, in that it is only available when the action code is executed in the OpenWhisk runtime, which made local testing difficult.

The functions above are now deprecated, in favor of using the NPM openwhisk package.

The package is available by default in all Node.js runtimes; when running on OpenWhisk, it is automatically configured to use the same credentials that were used to invoke the action. When running in other environments, it can be configured simply through environment variables. See the openwhisk package documentation for details.

This change, in combination of the deprecation of whisk.async() in favor of Promises as described above means that the whisk object is no longer required, and that testing is simplified.

Enter through here

Finally, it is now possible to choose alternative action entry points; by default the runtime will look for a function called main (or for an export called main for packaged actions), but users can now override this behavior at action creation time.

Assuming hello.js with the contents:

function greet(args) {
  return { msg: `Hello ${args.name}!`}
}

…we can use the --main flag as follows:

$ wsk action create hello hello.js --main greet
ok: created action hello
$ wsk action invoke -br hello -p name reader
{
    "msg": "Hello reader!"
}

For packaged actions, the --main flag defines the name of the exported function to use as an entry point.

In conclusion

To summarize, the JavaScript support in OpenWhisk has improved considerably:

These improvements keep OpenWhisk in line with evolving JavaScript practices. As a direct consequence, some of the features being replaced with modern alternatives are or will soon be deprecated. We recommend this blog post on migration strategies for OpenWhisk Node.js actions.

  1. Note that main must be declared either as a function or as a var, due to the scoping rules for const and let which make such declarations invisible to the action runtime.