Skip to content

Instantly share code, notes, and snippets.

@jthomas
Last active February 20, 2023 20:52
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jthomas/37a365e38d6cfd012b81a2c343729102 to your computer and use it in GitHub Desktop.
Save jthomas/37a365e38d6cfd012b81a2c343729102 to your computer and use it in GitHub Desktop.
OpenWhisk Workshop

OpenWhisk Workshop

Hello πŸ‘‹.

This workshop will teach you how to develop serverless applications, composed of loosely coupled microservice-like functions, using an open-source serverless platform.

Starting with getting the development environment set up, it'll move onto creating, deploying and invoking serverless functions for multiple runtimes. Once you are comfortable creating serverless functions, the next step is to connect functions to events, like message queues, allowing microservices to fire automatically. Finally, we'll demonstrate how to expose serverless functions as public API endpoints, allowing to build serverless web applications.

Welcome to the future of cloud development, you'll never want to manage another server again 😎.

You, ready? Let's go! πŸš—

Table Of Contents

Serverless Computing

Serverless computing is a cloud computing execution model in which the cloud provider dynamically manages the allocation of machine resources. Pricing is based on the actual amount of resources consumed by an application, rather than on pre-purchased units of capacity.[1] It is a form of utility computing.

Serverless computing still requires servers.[1] The name "serverless computing" is used because the server management and capacity planning decisions are completely hidden from the developer or operator. Serverless code can be used in conjunction with code deployed in traditional styles, such as microservices. Alternatively, applications can be written to be purely serverless and use no provisioned services at all.[2]

Apache OpenWhisk

Apache OpenWhisk is an open-source serverless computing platform. Developed at IBM, OpenWhisk was donated to the Apache Foundation in 2016. Developers can use a hosted version of the platform on IBM Bluemix, run the platform locally or deploy to an infrastructure provider of their choosing.

Apache OpenWhisk has the following advantages over other serverless platforms…

  • OpenWhisk is open-source rather than being a proprietary platform, reducing vendor-lockin.
  • OpenWhisk supports Node.js, Java, Python and Swift runtimes. It also supports using Docker images as the runtime allowing any language to be used on the platform.
  • OpenWhisk has a more advanced orchestration model for serverless functions including high-order operations like function sequences, events triggers and feeds, built into the platform.

Apache OpenWhisk uses the following terminology for serverless applications:

  • action - a serverless function running on openwhisk, this is the code you have deployed.
  • trigger - named event source that fires when an external event occurs, e.g. messages on a queue or database updates.
  • rule - binding between triggers and actions, allows you to automatically fire actions when external events occur, like messages on a queue or database updates.

Setting Up Your Development Environment

Register Bluemix Account

  1. Open a browser window
  2. Navigate to https://console.ng.bluemix.net/registration/
  3. Fill in registration form and follow link in validation email when it arrives.
  4. Create "Organisation" upon logging in for the first time.
    • Region: United Kingdom
    • Name: (your email address)
  5. Create "Space" upon logging in for the first time.
    • Name: dev
  6. Click "I'm Ready" to finalise the account setup.

Set up OpenWhisk CLI

  1. From the Bluemix homepage, select the "burger menu" on the top-left hand corner of the toolbar.
  2. Click the "OpenWhisk" menu item.

Image of Yaktocat

  1. Click on "Download OpenWhisk CLI"

Getting Starting

  1. Follow the instructions for installing and configuring the OpenWhisk CLI

    • Download the OpenWhisk CLI (and install this into your path on Linux).
    • Authenticate the CLI against the platform using the wsk property set command given.
  2. Verify your setup using the following command.

    $ wsk action invoke /whisk.system/utils/echo -p message hello --result
    {
            "message": "hello"
    }
    

πŸŽ‰πŸŽ‰πŸŽ‰ Congratulations, you've successfully registered an account, setup OpenWhisk for development and executed your first serverless function! Let's start using the platform to create our own serverless functions… πŸŽ‰πŸŽ‰πŸŽ‰

Creating and invoking OpenWhisk actions

Actions are stateless code snippets that run on the OpenWhisk platform. An action can be written as a JavaScript, Swift, or Python function, a Java method, or a custom executable program packaged in a Docker container. For example, an action can be used to detect the faces in an image, respond to a database change, aggregate a set of API calls, or post a Tweet.

Actions can be explicitly invoked, or run in response to an event. In either case, each run of an action results in an activation record that is identified by a unique activation ID. The input to an action and the result of an action are a dictionary of key-value pairs, where the key is a string and the value a valid JSON value. Actions can also be composed of calls to other actions or a defined sequence of actions.

Creating and invoking JavaScript actions

Review the following steps and examples to create your first JavaScript action.

If you prefer to use Python, see the section below with the Python example code and follow the same instructions.

  1. Create a JavaScript file with the following content. For this example, the file name is 'hello.js'.
function main() {
    return {payload: 'Hello world'};
}

The JavaScript file might contain additional functions. However, by convention, a function called main must exist to provide the entry point for the action.

  1. Create an action from the following JavaScript function. For this example, the action is called 'hello'.
wsk action create hello hello.js
ok: created action hello
  1. List the actions that you have created:
wsk action list
actions
hello       private

You can see the hello action you just created.

  1. After you create your action, you can run it in the cloud in OpenWhisk with the 'invoke' command. You can invoke actions with a blocking invocation (i.e., request/response style) or a non-blocking invocation by specifying a flag in the command. A blocking invocation request will wait for the activation result to be available. The wait period is the lesser of 60 seconds or the action's configured time limit. The result of the activation is returned if it is available within the wait period. Otherwise, the activation continues processing in the system and an activation ID is returned so that one may check for the result later, as with non-blocking requests (see here for tips on monitoring activations).

This example uses the blocking parameter, --blocking:

wsk action invoke --blocking hello
ok: invoked hello with id 44794bd6aab74415b4e42a308d880e5b
{
    "result": {
        "payload": "Hello world"
    },
    "status": "success",
    "success": true
}

The command outputs two important pieces of information:

  • The activation ID (44794bd6aab74415b4e42a308d880e5b)
  • The invocation result if it is available within the expected wait period

The result in this case is the string Hello world returned by the JavaScript function. The activation ID can be used to retrieve the logs or result of the invocation at a future time.

  1. If you don't need the action result right away, you can omit the --blocking flag to make a non-blocking invocation. You can get the result later by using the activation ID. See the following example:
wsk action invoke hello
ok: invoked hello with id 6bf1f670ee614a7eb5af3c9fde813043
wsk activation result 6bf1f670ee614a7eb5af3c9fde813043
{
    "payload": "Hello world"
}
  1. To access the most recent activation record, activation results or activation logs, use the --last or -l flag. Run the following command to get your last activation result.
wsk activation result --last
{
    "payload": "Hello world"
}

Note that you should not use an activation ID with the flag --last.

  1. If you forget to record the activation ID, you can get a list of activations ordered from the most recent to the oldest. Run the following command to get a list of your activations:
wsk activation list
activations
44794bd6aab74415b4e42a308d880e5b         hello
6bf1f670ee614a7eb5af3c9fde813043         hello

πŸŽ‰πŸŽ‰πŸŽ‰ Great work, you have now learned how to create, deploy and invoke your own serverless functions on OpenWhisk. What about passing data into actions? Let's find out more… πŸŽ‰πŸŽ‰πŸŽ‰

Passing parameters to an action

Parameters can be passed to the action when it is invoked.

  1. Use parameters in the action. For example, update the 'hello.js' file with the following content:
function main(params) {
    return {payload:  'Hello, ' + params.name + ' from ' + params.place};
}

The input parameters are passed as a JSON object parameter to the main function. Notice how the name and place parameters are retrieved from the params object in this example.

  1. Update the hello action and invoke the action, while passing it name and place parameter values. See the following example:
wsk action update hello hello.js
  1. Parameters can be provided explicitly on the command-line, or by supplying a file containing the desired parameters

To pass parameters directly through the command-line, supply a key/value pair to the --param flag:

wsk action invoke --result hello --param name Bernie --param place Vermont

In order to use a file containing parameter content, create a file containing the parameters in JSON format. The filename must then be passed to the param-file flag:

Example parameter file called parameters.json:

{
    "name": "Bernie",
    "place": "Vermont"
}
wsk action invoke --result hello --param-file parameters.json
{
    "payload": "Hello, Bernie from Vermont"
}

Notice the use of the --result option: it implies a blocking invocation where the CLI waits for the activation to complete and then displays only the result. For convenience, this option may be used without --blocking which is automatically inferred.

Additionally, if parameter values specified on the command-line are valid JSON, then they will be parsed and sent to your action as a structured object. For example, if we update our hello action to:

function main(params) {
    return {payload:  'Hello, ' + params.person.name + ' from ' + params.person.place};
}

Now the action expects a single person parameter to have fields name and place. If we invoke the action with a single person parameter that is valid JSON:

wsk action invoke --result hello -p person '{"name": "Bernie", "place": "Vermont"}'

The result is the same because the CLI automatically parses the person parameter value into the structured object that the action now expects:

{
    "payload": "Hello, Bernie from Vermont"
}

πŸŽ‰πŸŽ‰πŸŽ‰ That was pretty easy, huh? We can now pass parameters and access these values in our serverless functions. What about parameters that we need but don't want to manually pass in every time? Guess what, we have a trick for that… πŸŽ‰πŸŽ‰πŸŽ‰

Setting default parameters

Actions can be invoked with multiple named parameters. Recall that the hello action from the previous example expects two parameters: the name of a person, and the place where they're from.

Rather than pass all the parameters to an action every time, you can bind certain parameters. The following example binds the place parameter so that the action defaults to the place "Vermont":

  1. Update the action by using the --param option to bind parameter values, or by passing a file that contains the parameters to --param-file

To specify default parameters explicitly on the command-line, provide a key/value pair to the param flag:

wsk action update hello --param place Vermont

Passing parameters from a file requires the creation of a file containing the desired content in JSON format. The filename must then be passed to the -param-file flag:

Example parameter file called parameters.json:

{
    "place": "Vermont"
}
wsk action update hello --param-file parameters.json
  1. Invoke the action, passing only the name parameter this time.
wsk action invoke --result hello --param name Bernie
{
    "payload": "Hello, Bernie from Vermont"
}

Notice that you did not need to specify the place parameter when you invoked the action. Bound parameters can still be overwritten by specifying the parameter value at invocation time.

  1. Invoke the action, passing both name and place values. The latter overwrites the value that is bound to the action.

Using the --param flag:

wsk action invoke --result hello --param name Bernie --param place "Washington, DC"

Using the --param-file flag:

File parameters.json:

{
  "name": "Bernie",
  "place": "Vermont"
}
wsk action invoke --result hello --param-file parameters.json

{  
    "payload": "Hello, Bernie from Washington, DC"
}

πŸŽ‰πŸŽ‰πŸŽ‰ Default parameters are awesome for handling parameters like authentication keys for APIs. Letting the platform pass them in automatically means you don't have include these keys in invocation requests or include them in the action source code. Neat, huh? πŸŽ‰πŸŽ‰πŸŽ‰

Creating asynchronous actions

JavaScript functions that run asynchronously may need to return the activation result after the main function has returned. You can accomplish this by returning a Promise in your action.

  1. Save the following content in a file called asyncAction.js.
function main(args) {
     return new Promise(function(resolve, reject) {
       setTimeout(function() {
         resolve({ done: true });
       }, 2000);
    })
 }

Notice that the main function returns a Promise, which indicates that the activation hasn't completed yet, but is expected to in the future.

The setTimeout() JavaScript function in this case waits for two seconds before calling the callback function. This represents the asynchronous code and goes inside the Promise's callback function.

The Promise's callback takes two arguments, resolve and reject, which are both functions. The call to resolve()fulfills the Promise and indicates that the activation has completed normally.

A call to reject() can be used to reject the Promise and signal that the activation has completed abnormally.

  1. Run the following commands to create the action and invoke it:
wsk action create asyncAction asyncAction.js
wsk action invoke --result asyncAction
{
    "done": true
}

Notice that you performed a blocking invocation of an asynchronous action.

  1. Fetch the activation log to see how long the activation took to complete:
wsk activation list --limit 1 asyncAction
activations
b066ca51e68c4d3382df2d8033265db0             asyncAction

wsk activation get b066ca51e68c4d3382df2d8033265db0

 {
     "start": 1455881628103,
     "end":   1455881648126,
     ...
 }

Comparing the start and end time stamps in the activation record, you can see that this activation took slightly over two seconds to complete.

Actions have a timeout parameter that enforces the maximum duration for an invocation. This value defaults to 60 seconds and can be changed to a maximum of 5 minutes.

Let's look at what happens when an action invocation takes longer than the timeout.

  1. Update the asyncAction timeout to 1000ms.
wsk action update asyncAction --timeout 1000
ok: updated action asyncAction
  1. Invoke the action and block on the result.
$ wsk action invoke asyncAction --result
{
    "error": "The action exceeded its time limits of 1000 milliseconds."
}

The error message returned by the platform indicates the action didn't return a response within the user-specified timeout. If we change the timeout back to a value higher than the artificial delay in the function, it should work again.

  1. Update the asyncAction timeout to 3000ms.
$ wsk action update asyncAction --timeout 3000
ok: updated action asyncAction
  1. Invoke the action and block on the result.
$ wsk action invoke asyncAction --result
{
    "done": true
}

πŸŽ‰πŸŽ‰πŸŽ‰ Asynchronous actions are necessary for calling other APIs or cloud services. Don't forget about that timeout though! Let's have a look at using an asynchronous action to invoke another API… πŸŽ‰πŸŽ‰πŸŽ‰

Using actions to call an external API

The examples so far have been self-contained JavaScript functions. You can also create an action that calls an external API.

This example invokes a Yahoo Weather service to get the current conditions at a specific location.

  1. Save the following content in a file called weather.js.
var request = require('request');

function main(params) {
    var location = params.location || 'Vermont';
    var url = 'https://query.yahooapis.com/v1/public/yql?q=select item.condition from weather.forecast where woeid in (select woeid from geo.places(1) where text="' + location + '")&format=json';

    return new Promise(function(resolve, reject) {
        request.get(url, function(error, response, body) {
            if (error) {
                reject(error);
            }
            else {
                var condition = JSON.parse(body).query.results.channel.item.condition;
                var text = condition.text;
                var temperature = condition.temp;
                var output = 'It is ' + temperature + ' degrees in ' + location + ' and ' + text;
                resolve({msg: output});
            }
        });
    });
}

Note that the action in the example uses the JavaScript request library to make an HTTP request to the Yahoo Weather API, and extracts fields from the JSON result. The References detail the Node.js packages that you can use in your actions.

This example also shows the need for asynchronous actions. The action returns a Promise to indicate that the result of this action is not available yet when the function returns. Instead, the result is available in the request callback after the HTTP call completes, and is passed as an argument to the resolve() function.

  1. Run the following commands to create the action and invoke it:
wsk action create weather weather.js

wsk action invoke --result weather --param location "Brooklyn, NY"

{
    "msg": "It is 28 degrees in Brooklyn, NY and Cloudy"
}

πŸŽ‰πŸŽ‰πŸŽ‰ This is more like a real-word example. This action could be invoked thousands of times in parallel and it would just work! No having to manage infrastructure to build a scalable cloud API. πŸŽ‰πŸŽ‰πŸŽ‰

(Optional) Packaging an action as a Node.js module

This step requires you to have the Node.js and NPM development tools installed. It can be ignored if you haven't set these up.

As an alternative to writing all your action code in a single JavaScript source file, you can write an action as a npmpackage. Consider as an example a directory with the following files:

First, package.json:

{
  "name": "my-action",
  "main": "index.js",
  "dependencies" : {
    "left-pad" : "1.1.3"
  }
}

Then, index.js:

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

exports.main = myAction;

Note that the action is exposed through exports.main; the action handler itself can have any name, as long as it conforms to the usual signature of accepting an object and returning an object (or a Promise of an object). Per Node.js convention, you must either name this file index.js or specify the file name you prefer as the mainproperty in package.json.

To create an OpenWhisk action from this package:

  1. Install first all dependencies locally
$ npm install

  1. Create a .zip archive containing all files (including all dependencies):
$ zip -r action.zip *

Please note: Using the Windows Explorer action for creating the zip file will result in an incorrect structure. OpenWhisk zip actions must have package.json at the root of the zip, while Windows Explorer will put it inside a nested folder. The safest option is to use the command line zip command as shown above.

  1. Create the action:
wsk action create packageAction --kind nodejs:6 action.zip

Note that when creating an action from a .zip archive using the CLI tool, you must explicitly provide a value for the --kind flag.

  1. You can invoke the action like any other:
wsk action invoke --result packageAction --param lines "[\"and now\", \"for something completely\", \"different\" ]"

{
    "padded": [
        ".......................and now",
        "......for something completely",
        ".....................different"
    ]
}

Finally, note that while most npm packages install JavaScript sources on npm install, some also install and compile binary artifacts. The archive file upload currently does not support binary dependencies but rather only JavaScript dependencies. Action invocations may fail if the archive includes binary dependencies.

πŸŽ‰πŸŽ‰πŸŽ‰ Node.js has a huge ecosystem of third-party packages which can be used on OpenWhisk with this method. The platform does come built-in with popular packages listed here This method also works for any other runtime. πŸŽ‰πŸŽ‰πŸŽ‰

Creating and invoking Python actions

The process of creating Python actions is similar to that of JavaScript actions. The following sections guide you through creating and invoking a single Python action, and adding parameters to that action.

Creating and invoking a Python action

An action is simply a top-level Python function. For example, create a file called hello.py with the following source code:

def main(args):
    name = args.get("name", "stranger")
    greeting = "Hello " + name + "!"
    print(greeting)
    return {"greeting": greeting}

Python actions always consume a dictionary and produce a dictionary. The entry method for the action is main by default but may be specified explicitly when creating the action with the wsk CLI using --main, as with any other action type.

You can create an OpenWhisk action called helloPython from this function as follows:

wsk action create helloPython hello.py

The CLI automatically infers the type of the action from the source file extension. For .py source files, the action runs using a Python 2.7 runtime. You can also create an action that runs with Python 3.6 by explicitly specifying the parameter --kind python:3. See the Python reference for more information about Python 2.7 vs. 3.6.

Action invocation is the same for Python actions as it is for JavaScript actions:

wsk action invoke --result helloPython --param name World

  {
      "greeting": "Hello World!"
  }

Creating and invoking Action Sequences

OpenWhisk supports a special kind of action called a "sequence". These actions are created using a list of existing actions. When the sequence is invoked, each action in executed in a sequence. The input parameters are passed to the first action in the sequence. The output from that function is passed as the input to the next function and so on. The output from the last action in the sequence is returned as the response result.

Here's an example of defining a sequence (my_sequence) which will invoke three actions (a, b, c).

wsk action create my_sequence --sequence a,b,c

Sequences behave like normal actions, you create, invoke and manage them as normal through the CLI.

Let's look at an example of using sequences.

  1. Create the file (funcs.js) with the following contents:
function split(params) {
  var text = params.text || ""
  var words = text.split(' ')
  return { words: words }
}

function reverse(params) {
  var words = params.words || []
  var reversed = words.map(word => word.split("").reverse().join(""))
  return { words: reversed }
}

function join(params) {
  var words = params.words || []
  var text = words.join(' ')
  return { text: text }
}
  1. Create the following three actions
$ wsk action create split funcs.js --main split
$ wsk action create reverse funcs.js --main reverse
$ wsk action create join funcs.js --main join
  1. Test each action to verify it is working
$  wsk action invoke split --result --param text "Hello world"
{
    "words": [
        "Hello",
        "world"
    ]
}
$  wsk action invoke reverse --result --param words '["hello", "world"]'
{
    "words": [
        "olleh",
        "dlrow"
    ]
}
$  wsk action invoke join --result --param words '["hello", "world"]'
{
    "text": "hello world"
}
  1. Create the following action sequence.
wsk action create reverse_words --sequence split,reverse,join
  1. Test out the action sequence.
$ wsk action invoke reverse_words --result --param text "hello world"
{
    "text": "olleh dlrow"
}

Using sequences is a great way to develop re-usable action components that can be joined together into "high-order" actions to create serverless applications.

For example, what if you have serverless functions to implement an external API and want to enforce HTTP authentication? Rather than manually adding this code to every action, you could define an "auth" action and use sequences to define new "authenticated" actions by joining this action with the existing API actions.

πŸŽ‰πŸŽ‰πŸŽ‰ Sequences are an "advanced" OpenWhisk technique. Congratulations for getting this far! Now let's move on to something all together different, connecting functions to external event sources… πŸŽ‰πŸŽ‰πŸŽ‰

Connecting actions to event sources

Serverless applications are often described as "event-driven" because you can connect serverless functions to external event sources, like message queues and database changes. When these external events fire, the serverless functions are automatically invoked, without any manual intervention.

In the previous example, we've been manually invoking actions using the command-line. Let's move onto connecting your actions to external event sources. OpenWhisk supports multiple external event sources like CouchDB, Apache Kafka, a cron-based scheduler and more.

Before we jump into the details, let's review some concepts which explain how this feature works in Apache OpenWhisk.

OpenWhisk Triggers

Triggers are a named channel for a class of events. The following are examples of triggers:

  • A trigger of location update events.
  • A trigger of document uploads to a website.
  • A trigger of incoming emails.

Triggers can be fired (activated) by using a dictionary of key-value pairs. Sometimes this dictionary is referred to as the event. As with actions, each firing of a trigger results in an activation ID.

Triggers can be explicitly fired by a user or fired on behalf of a user by an external event source. A feed is a convenient way to configure an external event source to fire trigger events that can be consumed by OpenWhisk. Examples of feeds include the following:

  • Cloudant data change feed that fires a trigger event each time a document in a database is added or modified.
  • A Git feed that fires a trigger event for every commit to a Git repository.

πŸŽ‰πŸŽ‰πŸŽ‰ **Triggers are an implementation of the Observer pattern. Instances of triggers can be fired with parameters. Next we need to find out how to register observers. ** πŸŽ‰πŸŽ‰πŸŽ‰

OpenWhisk Rules

A rule associates one trigger with one action, with every firing of the trigger causing the corresponding action to be invoked with the trigger event as input.

With the appropriate set of rules, it's possible for a single trigger event to invoke multiple actions, or for an action to be invoked as a response to events from multiple triggers.

For example, consider a system with the following actions:

  • classifyImage action that detects the objects in an image and classifies them.
  • thumbnailImage action that creates a thumbnail version of an image.

Also, suppose that there are two event sources that are firing the following triggers:

  • newTweet trigger that is fired when a new tweet is posted.
  • imageUpload trigger that is fired when an image is uploaded to a website.

You can set up rules so that a single trigger event invokes multiple actions, and have multiple triggers invoke the same action:

  • newTweet -> classifyImage rule.
  • imageUpload -> classifyImage rule.
  • imageUpload -> thumbnailImage rule.

The three rules establish the following behavior: images in both tweets and uploaded images are classified, uploaded images are classified, and a thumbnail version is generated.

πŸŽ‰πŸŽ‰πŸŽ‰ **Just remember rules allow you to register an observer on a trigger. That's all we need to know for now, let's look at using these new concepts… ** πŸŽ‰πŸŽ‰πŸŽ‰

Creating Triggers

Triggers represent a named "channel" for a stream of events.

Let's create a trigger to send location updates:

$ wsk trigger create locationUpdate
ok: created trigger locationUpdate

You can check that the trigger has been created like this:

$ wsk trigger list
triggers
locationUpdate                         private

So far we have only created a named channel to which events can be fired.

Let's now fire the trigger by specifying its name and parameters:

$ wsk trigger fire locationUpdate -p name "Donald" -p place "Washington, D.C"
ok: triggered locationUpdate with id 11ca88d404ca456eb2e76357c765ccdb

Events you fire to the locationUpdate trigger currently do not do anything. To be useful, we need to create a rule that associates the trigger with an action.

πŸŽ‰πŸŽ‰πŸŽ‰ That was easy? Let's keep going by connecting actions to triggers… πŸŽ‰πŸŽ‰πŸŽ‰

Creating Rules

Rules are used to associate a trigger with an action. Each time a trigger event is fired, the action is invoked with the event parameters.

As an example, create a rule that calls the hello action whenever a location update is posted.

  1. Create a 'hello.js' file with the action code we will use:
function main(params) {
   return {payload:  'Hello, ' + params.name + ' from ' + params.place};
}

  1. Make sure that the trigger and action exist.
$ wsk trigger update locationUpdate

$ wsk action update hello hello.js

  1. Create the rule. Note that the rule will be enabled upon creation, meaning that it will be immediately available to respond to activations of your trigger. The three parameters are the name of the rule, the trigger, and the action.
$ wsk rule create myRule locationUpdate hello

At any time, you can choose to disable a rule.

$ wsk rule disable myRule

  1. Fire the locationUpdate trigger. Each time that you fire an event, the hello action is called with the event parameters.
$ wsk trigger fire locationUpdate --param name Donald --param place "Washington, D.C."

ok: triggered locationUpdate with id d5583d8e2d754b518a9fe6914e6ffb1e
  1. Verify that the action was invoked by checking the most recent activation.
$ wsk activation list --limit 1 hello

activations
9c98a083b924426d8b26b5f41c5ebc0d             hello

$ wsk activation result 9c98a083b924426d8b26b5f41c5ebc0d

{
   "payload": "Hello, Donald from Washington, D.C."
}

You see that the hello action received the event payload and returned the expected string.

You can create multiple rules that associate the same trigger with different actions. Triggers and rules cannot belong to a package. The rule may be associated with an action that belongs to a package however, for example:

$ wsk rule create recordLocation locationUpdate /whisk.system/utils/echo

You can also use rules with sequences. For example, one can create an action sequence recordLocationAndHellothat is activated by the rule anotherRule.

$ wsk action create recordLocationAndHello --sequence /whisk.system/utils/echo,hello
$ wsk rule create anotherRule locationUpdate recordLocationAndHello

πŸŽ‰πŸŽ‰πŸŽ‰ Right, now we have a way to connect actions to events in OpenWhisk, how do we connect triggers to event sources like messages queues? Enter trigger feeds… πŸŽ‰πŸŽ‰πŸŽ‰

Connecting Trigger Feeds

Trigger feeds allow you to connect triggers to external event sources. Event sources will fire registered triggers each time an event occurs. Here's a list of the event sources currently supported on OpenWhisk: https://github.com/apache/incubator-openwhisk/blob/master/docs/catalog.md

This example shows how to use a feed in the Alarms package to fire a trigger every second, and how to use a rule to invoke an action every second.

  1. Get a description of the feed in the /whisk.system/alarms package.
$ wsk package get --summary /whisk.system/alarms

package /whisk.system/alarms
 feed   /whisk.system/alarms/alarm

$ wsk action get --summary /whisk.system/alarms/alarm

action /whisk.system/alarms/alarm: Fire trigger when alarm occurs
   (params: cron trigger_payload)

The /whisk.system/alarms/alarm feed takes two parameters:

  • cron: A crontab specification of when to fire the trigger.
  • trigger_payload: The payload parameter value to set in each trigger event.
  1. Create a trigger that fires every eight seconds.
$ wsk trigger create everyEightSeconds --feed /whisk.system/alarms/alarm -p cron "*/8 * * * * *" -p trigger_payload "{\"name\":\"Mork\", \"place\":\"Ork\"}"
ok: created trigger feed everyEightSeconds

  1. Create a 'hello.js' file with the following action code.
function main(params) {
    return {payload:  'Hello, ' + params.name + ' from ' + params.place};
}
  1. Make sure that the action exists.
$ wsk action update hello hello.js
  1. Create a rule that invokes the hello action every time the everyEightSeconds trigger fires.
$ wsk rule create myRule everyEightSeconds hello
ok: created rule myRule

  1. Check that the action is being invoked by polling for activation logs.
$ wsk activation poll

You should see activations every eight seconds for the trigger, the rule, and the action. The action receives the parameters {"name":"Mork", "place":"Ork"} on every invocation.

IMPORTANT: Let's delete the trigger and rule or this event will be running forever!

$ wsk trigger delete everyEightSeconds
$ wsk rule delete myRule

πŸŽ‰πŸŽ‰πŸŽ‰ Understanding triggers and rules allows you to build event-driven applications on OpenWhisk. Create some actions, hook up events and let the platform take care of everything else, what could be easier? πŸŽ‰πŸŽ‰πŸŽ‰

Creating Public APIs for OpenWhisk Actions

Serverless applications are a great use case for building public API endpoints. Developers are now building "serverless web applications" by hosting their static files on a CDN and then using serverless platforms for their APIs.

OpenWhisk has a comprehensive RESTful API for the platform that allows you to invoke actions using authenticated HTTP requests. However, if you want to build APIs for public web sites or mobile applications, the authentication credentials will need embedding in client-side files. This is a terrible idea for obvious reasons…. but don't panic!

OpenWhisk has a solution for creating public APIs to invoke your actions without exposing credentials. 😎

OpenWhisk Web Actions

Web actions are OpenWhisk actions annotated to quickly enable you to build web based applications. This allows you to program backend logic which your web application can access anonymously without requiring an OpenWhisk authentication key. It is up to the action developer to implement their own desired authentication and authorization (i.e. OAuth flow).

Web action activations will be associated with the user that created the action. This actions defers the cost of an action activation from the caller to the owner of the action.

Let's take the following JavaScript action hello.js,

$ cat hello.js
function main({name}) {
  var msg = 'you did not tell me who you are.';
  if (name) {
    msg = `hello ${name}!`
  }
  return {body: `<html><body><h3>${msg}</h3></body></html>`}
}

You may create a web action hello in the package demo for the namespace guest using the CLI's --web flag with a value of true or yes:

$ wsk package create demo
ok: created package demo
$ wsk action create demo/hello hello.js --web true
ok: created action demo/hello
$ wsk action get demo/hello --url
ok: got action hello
https://${APIHOST}/api/v1/web/${NAMESPACE}/demo/hello

Using the --web flag with a value of true or yes allows an action to be accessible via REST interface without the need for credentials. A web action can be invoked using a URL that is structured as follows:https://{APIHOST}/api/v1/web/{QUALIFIED ACTION NAME}.{EXT}. The fully qualified name of an action consists of three parts: the namespace, the package name, and the action name.

The fully qualified name of the action must include its package name, which is default if the action is not in a named package.

An example is guest/demo/hello. The last part of the URI called the extension which is typically .http although other values are permitted as described later. The web action API path may be used with curl or wget without an API key. It may even be entered directly in your browser.

Try opening https://${APIHOST}/api/v1/web/${NAMESPACE}/demo/hello.http?name=Jane in your web browser. Or try invoking the action via curl:

$ curl https://${APIHOST}/api/v1/web/${NAMESPACE}/demo/hello.http?name=Jane

Here is an example of a web action that performs an HTTP redirect:

function main() {
  return { 
    headers: { location: 'http://openwhisk.org' },
    statusCode: 302
  }
}

Or sets a cookie:

function main() {
  return { 
    headers: { 
      'Set-Cookie': 'UserID=Jane; Max-Age=3600; Version=',
      'Content-Type': 'text/html'
    }, 
    statusCode: 200,
    body: '<html><body><h3>hello</h3></body></html>' }
}

Or returns an image/png:

function main() {
    let png = <base 64 encoded string>
    return { headers: { 'Content-Type': 'image/png' },
             statusCode: 200,
             body: png };
}

Or returns application/json:

function main(params) { 
    return {
        statusCode: 200,
        headers: { 'Content-Type': 'application/json' },
        body: new Buffer(JSON.stringify(params)).toString('base64'),
    };
}

It is important to be aware of the response size limit for actions since a response that exceeds the predefined system limits will fail. Large objects should not be sent inline through OpenWhisk, but instead deferred to an object store, for example.

Web actions must return a JSON object and can use the following properties to control the HTTP response.

  • headers: a JSON object where the keys are header-names and the values are string values for those headers (default is no headers).
  • statusCode: a valid HTTP status code (default is 200 OK).
  • body: a string which is either plain text or a base64 encoded string for binary data (default is empty response).

πŸŽ‰πŸŽ‰πŸŽ‰ OpenWhisk Web Actions are an awesome feature. Exposing public APIs from actions is minimal effort. Let's finish off this section by looking at an additional approach, using an API Gateway. πŸŽ‰πŸŽ‰πŸŽ‰

OpenWhisk API Gateway

OpenWhisk actions can benefit from being managed by API management.

The API Gateway acts as a proxy to Web Actions and provides them with additional features including HTTP method routing , client id/secrets, rate limiting and CORS. For more information on API Gateway feature you can read the api management documentation

Let's look a short example of using the API Gateway service…

  • Create a web action with the following name: fibonacci
var sequence = [1];
var invocations = 0;

function main(args) {
	
	invocations = 0;
	var int = parseInt(args.num);
	//num is a zero-based index
	
	return { 
		n:int,
		value:fibonacci(int),
		sequence: sequence.slice(0,int+1),
		invocations: invocations
	}
}

function fibonacci(num) {
	
	invocations ++;
	var result = 0;
	
	if (sequence[num] != undefined) {
		return sequence[num];
	}

	if (num <= 1 || isNaN(num)) {
		result = 1;
	} else {
		result = fibonacci(num-1) + fibonacci(num-2);
	}
	if (num >= 0) {
		sequence[num] = result;
	}
	return result;
}
$ wsk action create fibonacci fib.js --web true
  • Test it works
$ wsk action invoke fibonacci -p num 5 -b -r
{
   "n": 5,
   "value": 8,
   "sequence": [1, 1, 2, 3, 5, 8],
   "invocations": 9
}
  • Create an API with base path /hello, path /world and method get with response type json
$ wsk api create /fibonacci get fibonacci --response-type json
ok: created API /fibonacci GET for action /_/fibonacci
https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/1310a834667721bb9bf6968e828aa286aa5a287b4e5d46a513aa813a775602fb/fibonacci
  • Test out the API by sending a HTTP request to the endpoint with the num query parameter.
$ curl https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/1310a834667721bb9bf6968e828aa286aa5a287b4e5d46a513aa813a775602fb/fibonacci?num=5
{
  "n": 5,
  "value": 8,
  "sequence": [1, 1, 2, 3, 5, 8],
  "invocations": 1
}

πŸŽ‰πŸŽ‰πŸŽ‰ The API Gateway service add services like request routing, based on method and paths, rate limiting and pluggable authentication. It is perfect for high-traffic public APIs. πŸŽ‰πŸŽ‰πŸŽ‰

@vittorioscibetta
Copy link

In the trigger section, running
wsk trigger fire locationUpdate --param name Donald --param place "Washington, D.C."
gave back an error
{ "error": "An error has occurred: TypeError: Cannot read property 'name' of undefined"
Using instead quotes for name param
/wsk trigger fire locationUpdate --param name "Donald" --param place "Washington, D.C."

worked fine.

@vittorioscibetta
Copy link

Missing the step to get authentication to the Gateway Api

wsk bluemix login

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment