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:
- Node.js 0.12 (
--kind nodejs
) - Node.js 6 (
--kind nodejs:6
)
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:
const
andlet
statements, allowing for greater control over variable scopes.- destructuring assignments, providing an expressive syntax for extracting values from objects. They are particularly useful in OpenWhisk, since actions receive all their input arguments as a single dictionary.
- template literals allowing for string interpolation.
- arrow functions, a compact form for defining anonymous functions.
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 Promise
s to compute and return results asynchronously.
JavaScript Promise
s 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 Promise
s 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:
- accept a single dictionary as argument
- return either a dictionary or a
Promise
- a
Promise
resolved with a dictionary is a successful activation - a rejected
Promise
results in failed activation
- a
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 Promise
s 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:
- develop the action as an NPM module, declaring meta-data and dependencies in
package.json
as is usual - expose the action entry point as an export, e.g.
module.exports.main = ...
- run
npm install
locally - archive the module directory in a
zip
file - upload the
zip
file as a Node.js action
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:
- Be mindful of what goes in the archive; avoid for instance
devDependencies
, which can include large number of files, potentially slowing down action creation and invocation. - For local debugging, it can be helpful to unzip the action archive in a clean directory, and from
node
runrequire('.').main({ ... })
. This is essentially what the OpenWhisk runtime does. - If you depend on binary (native) libraries, either directly or transitively, you may not be able to have your action run in the OpenWhisk environment. This blog post explains how you can work around binary incompatibilities.
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 Promise
s 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:
- you can now pick which version of the Node.js runtime to run your actions with
- you can use
Promise
s to compute and return asynchronous results - you can write actions as packages with NPM Dependencies
- actions no longer use a special
whisk
object, improving local testing - you can define alternative action entry points
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.
-
Note that
main
must be declared either as afunction
or as avar
, due to the scoping rules forconst
andlet
which make such declarations invisible to the action runtime. ↩