Serverless Node-Red applications with OpenWhisk and Docker

IBM recently launched a service called OpenWhisk; a distributed compute service to execute application logic in response to events. The most notable advantages of such serverless framework is:

  • Auto-Scaling: automatically instantiate multiple instances to fulfill concurrent requests.
  • Pay-Per-Use: pay only for the compute time you use, in millisecond increments!

OpenWhisk supports Node.JS, Python, Swift, and Docker. However, it does not natively support Node-Red, a graphical editor for “wiring” the Internet of Things.

Being Node-Red one of my favorite tools, I have been thinking about ways to use the Node-Red graphical editor and still benefiting from OpenWhisk’s auto-scaling. Here’s what I want to achieve:

 

Proof of concept Architecture:

Since OpenWhisk accepts Docker container, I decided to try out build a Docker image of with Node-Red app, push that image to DockerHub and have OpenWhisk using that image on-demand.

 

Architecture of Node-Red with OpenWhisk and Docker

 

Sample Node-Red ‘Compute App.’

This the Node-Red flow I use to emulate a massive computation process that takes 5 seconds. The 5 seconds delay is to ensure that OpenWhisk will not use the same instance to serve concurrent requests. I wanted to force OpenWhisk to provision multiple instances.

A way I implement to confirm the uniqueness of each Node-Red run-time instance it by having the code generating a random id at start-up, and carry that number throughout the life of the process. I call it the  ‘instance id’. And you’ll see that number in the response JSON.

[{"id":"a20dbc9c.07b21","type":"http in","z":"5d386e3f.b9637","name":"","url":"/init","method":"post","swaggerDoc":"","x":200,"y":180,"wires":[["9310d36a.e3fea"]]},{"id":"1cc439b7.cbc226","type":"http response","z":"5d386e3f.b9637","name":"","x":710,"y":180,"wires":[]},{"id":"c9d917bc.24dd88","type":"http in","z":"5d386e3f.b9637","name":"","url":"/run","method":"post","swaggerDoc":"","x":200,"y":220,"wires":[["d9203786.e38d28"]]},{"id":"65eaa64a.821668","type":"http response","z":"5d386e3f.b9637","name":"","x":710,"y":220,"wires":[]},{"id":"d9203786.e38d28","type":"function","z":"5d386e3f.b9637","name":"response","func":"var instance = context.get('inst') || 0;\nif (!instance){\n    instance = Math.random();\n    context.set('inst', instance);\n}\nmsg.payload = { 'text': 'It works! Instance ID: ' + instance };\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":220,"wires":[["78753f7a.39edf"]]},{"id":"e81ec6d9.f53598","type":"http in","z":"5d386e3f.b9637","name":"","url":"/test","method":"get","swaggerDoc":"","x":200,"y":260,"wires":[["42e8ca5a.76ffd4"]]},{"id":"d40a8882.725068","type":"http response","z":"5d386e3f.b9637","name":"","x":710,"y":260,"wires":[]},{"id":"78753f7a.39edf","type":"delay","z":"5d386e3f.b9637","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":540,"y":220,"wires":[["65eaa64a.821668"]]},{"id":"42e8ca5a.76ffd4","type":"function","z":"5d386e3f.b9637","name":"response","func":"var instance = context.get('inst') || 0;\nif (!instance){\n    instance = Math.random();\n    context.set('inst', instance);\n}\nmsg.payload = { 'text': 'It works! Instance ID: ' + instance };\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":260,"wires":[["d40a8882.725068"]]},{"id":"9310d36a.e3fea","type":"function","z":"5d386e3f.b9637","name":"response","func":"var instance = context.get('inst') || 0;\nif (!instance){\n    instance = Math.random();\n    context.set('inst', instance);\n}\nmsg.payload = { 'text': 'It works! Instance ID: ' + instance };\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":180,"wires":[["1cc439b7.cbc226"]]}]

Build the Node-Red ‘compute app’ for OpenWhisk

Step 1: Create a folder and add you flows.json and the Dockerfile below:

Step 2: The Dockerfile extends the Node-Red image.

1
2
3
4
5
FROM nodered/node-red-docker
RUN npm install node-red-node-wordpos
ENV PORT 8080
ADD flows.json /data/flows.json
EXPOSE 8080:8080

Step 3: Build the image and push to DockerHub.

1
2
docker build -t isratx/mynodered .
docker push isratx/mynodered

Step 4: Setup a new Action in OpenWhisk.

 

Results for single OpenWhisk request:

Below OpenWhisk’s Invocation Console, showing the result of a single invoking of the Nore-Red image. OpenWhisk bills me for 5sec, matching the execution time I designed in the ‘compute app’. In the response text, you see the Instance ID,  mentioned above a way to verify the uniqueness of a process.

 

Concurrent Requests

After confirming the concept works for a single request, now we’ll run the setup with five concurrent requests. Below is a sample app in Node-Red. Its job is just to send five requests at the same time.

 Sample Parent App

This sample app calls five OpenWhisk actions concurrently.

[{"id":"148617d5.c10598","type":"inject","z":"d3a8bc3c.e14e9","name":"go","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":90,"y":200,"wires":[["bab44a0e.2a14a8"]]},{"id":"bab44a0e.2a14a8","type":"function","z":"d3a8bc3c.e14e9","name":"","func":"msg.headers = {\n    'Authorization': 'Basic <your auth here>',\n    'Content-Type': 'application/json'\n};\nmsg.payload = { \"arg\": \"choose a value\"};\nvar space = '<your space here>';\nmsg.url = 'https://openwhisk.ng.bluemix.net/api/v1/namespaces/' + space + '/actions/DockerNodeRed?blocking=true';\nreturn msg;","outputs":1,"noerr":0,"x":210,"y":200,"wires":[["9376160f.746b38","9f589329.1022c","8a2fcf15.3a0ab","a181fcee.229a6","2e2cd7ae.0756d8"]]},{"id":"9376160f.746b38","type":"http request","z":"d3a8bc3c.e14e9","name":"OpenWhisk Action","method":"POST","ret":"txt","url":"","tls":"","x":390,"y":100,"wires":[["c535fe90.468d2"]]},{"id":"c535fe90.468d2","type":"function","z":"d3a8bc3c.e14e9","name":"","func":"var m = JSON.parse(msg.payload);\n\nif (m.hasOwnProperty('response') )\n{\n    msg.payload = m.response.result.text;\n    return msg;\n}\nreturn msg;\n","outputs":1,"noerr":0,"x":570,"y":200,"wires":[["5aa9477e.705c28"]]},{"id":"5aa9477e.705c28","type":"debug","z":"d3a8bc3c.e14e9","name":"","active":true,"console":"false","complete":"false","x":710,"y":200,"wires":[]},{"id":"10d476de.7c0a69","type":"function","z":"d3a8bc3c.e14e9","name":"","func":"msg.headers = {\n    'Authorization': 'Basic <your auth here>',\n    'Content-Type': 'application/json'\n};\n\n//msg.payload - JSON.parse(msg.payload);\n//var id = msg.payload.activationId;\nvar id = ''; \n\nvar base = \"https://openwhisk.ng.bluemix.net/api/v1\";\n\nvar args = \"/namespaces/<your space here>/activations/\"+ id ;\n\nmsg.url = base + args;\n\nreturn msg;","outputs":1,"noerr":0,"x":210,"y":480,"wires":[["a8936e27.7640f"]]},{"id":"44d570c0.5e7dc","type":"debug","z":"d3a8bc3c.e14e9","name":"","active":true,"console":"false","complete":"false","x":630,"y":480,"wires":[]},{"id":"8a2fcf15.3a0ab","type":"http request","z":"d3a8bc3c.e14e9","name":"OpenWhisk Action","method":"POST","ret":"txt","url":"","tls":"","x":390,"y":220,"wires":[["c535fe90.468d2"]]},{"id":"9f589329.1022c","type":"http request","z":"d3a8bc3c.e14e9","name":"OpenWhisk Action","method":"POST","ret":"txt","url":"","tls":"","x":390,"y":160,"wires":[["c535fe90.468d2"]]},{"id":"a181fcee.229a6","type":"http request","z":"d3a8bc3c.e14e9","name":"OpenWhisk Action","method":"POST","ret":"txt","url":"","tls":"","x":390,"y":280,"wires":[["c535fe90.468d2"]]},{"id":"2e2cd7ae.0756d8","type":"http request","z":"d3a8bc3c.e14e9","name":"OpenWhisk Action","method":"POST","ret":"txt","url":"","tls":"","x":390,"y":340,"wires":[["c535fe90.468d2"]]},{"id":"c0161e93.02c18","type":"inject","z":"d3a8bc3c.e14e9","name":"go","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":90,"y":480,"wires":[["10d476de.7c0a69"]]},{"id":"a8936e27.7640f","type":"http request","z":"d3a8bc3c.e14e9","name":"OpenWhisk Activations","method":"GET","ret":"txt","url":"","tls":"","x":410,"y":480,"wires":[["44d570c0.5e7dc"]]}]

 

 OpenWhisk Results

Four out of five requests worked just as planned. Note that 4 responses came at the same time, and Instance IDs confirm that multiple Node-Red instances were used to fulfill my request.
Request five returned an ActivationId instead of the results.  Not sure why that happened. Let’s examine OpenWhisk logs.

OpenWhisk Activity Log

When analyzing timestamps & instance Id, we can conclude request #5 successfully. However, it took extra 1min and 3 seconds to give us a response. Further, OpenWhisk only returned an Activation Id instead of the JSON response object.

Since I’m new to OpenWhisk, and it is an experimental service, I’ll assume these issues are either because a config parameter I missed or a current limitation/bug of the service.

Overall this was a successful experiment that confirms that you can have a Node-Red app running on OpenWhisk!

What’s Next?

Make the development more seamless.

  • Can I pass flows.json as an argument instead of baking into the image?
  • What about having flows.json in a Cloudant database, and pass URI as an argument?

 

Resources:

These were the articles I read to help me perform this experiment: