Using serverless computing, also called function as a service (FaaS), developers can write applications free from worries about server management and scaling. However, the servers are always there in the background, up in the clouds, and they need to be paid for. It is often cheaper to use your own data center processing capacity for most cases, with the ability to burst into the public cloud when processing requirements exceed the locally available capacity.

In this tutorial you learn how to do exactly that. You configure a FaaS private cloud, set up a long-running action on both that private cloud and a public cloud, and write an application that calls that action.

The application calls the action. The logic in the application decides which action to call (the private or the public cloud), based on the current load it places on the private cloud server.

Learning objectives

  • Configure a local FaaS server using Apache OpenWhisk.
  • Write a FaaS action that uses a Promise object to simulate slow processing.
  • Call FaaS, local and remote, from a Node.js server-side application.
  • Add logic to decide whether to use the local OpenWhisk or the remote IBM Cloud Functions.

Prerequisites

Estimated time

4 hours

Steps

To configure the application, you first install OpenWhisk locally to create a private cloud. Then, you install the same action on that private cloud and on the public IBM Cloud. Next, you create an application that calls on that action, alternating between the private and public clouds. Finally, you add the logic to use the public cloud only when the private cloud is loaded with enough action calls.

Step 1: Install OpenWhisk locally

There are multiple ways to install OpenWhisk locally, as you can read here. The easiest method for me was to install a Vagrant virtual machine (VM) on Ubuntu Server 18.04 LTS (with more than 50 GB disk). If it is a virtual machine, make sure that Intel VT-x/EPT and AMD-V/RVI are enabled. The following steps show how to install a Vagrant VM on Ubuntu Server:

  1. Install the prerequisite software:

     sudo apt install virtualbox
    
     wget -c \ https://releases.hashicorp.com/vagrant/2.0.3/vagrant_2.0.3_x86_64.deb
    
     sudo dpkg -i vagrant_2.0.3_x86_64.deb
    

    VirtualBox lets you run a virtual machine inside a computer. It is similar to VMWare, but it does not require paying for a license.

    Vagrant controls VirtualBox. It ensures that the correct base image is installed and then installs the correct software on it. I found there was a bug in the official Ubuntu 18.04 LTS version of the Vagrant package, so I downloaded the latest version from Vagrant instead.

  2. Download the OpenWhisk repository:

     git clone --depth=1 \
     https://github.com/apache/incubator-openwhisk openwhisk
    
  3. Initialize and start the virtual machine:

     cd openwhisk/tools/vagrant
     ./hello
    

    This is a very long process the first time. Vagrant is building a virtual machine. To do so, it first downloads the OS image. Then, with the initial OS image running, it downloads the specific packages that are needed for Apache OpenWhisk.

    This program runs until the Vagrant VM is ready (it could take hours. On my system it took 80 minutes). However, you can complete the following step while the Vagrant VM is still configuring.

  4. Open another terminal (or if you don’t use X11, type Alt-F2 and log on), and install the command line interface for OpenWhisk:

     sudo apt install linuxbrew-wrapper
     brew update
     brew install wsk
     sudo cp /home/linuxbrew/.linuxbrew/bin/wsk \
     /usr/local/bin
    
  5. After the Vagrant VM is completed, configure the connection parameters for the Vagrant VM OpenWhisk, without any encryption:

     wsk property set --apihost http://192.168.33.16:10001 \
     --auth $(vagrant ssh -- cat openwhisk/ansible/files/auth.guest)
    
  6. Get the list of names in the default namespace:

    wsk list

    It should have four categories: packages, actions, triggers, and rules. All categories should be empty.

  7. Create a file named echo.js that contains the following content:

    const main = params => {return {inp: params};};

    There are multiple ways to program OpenWhisk actions, the functions that are implemented by OpenWhisk. The file is a JavaScript Node.js action. It is written using the JavaScript arrow function notation. If you are not familiar with that notation, the following code shows the equivalent function declaration.

     function main(params) {
         return {inp: params};
     };
    

    The function receives a dictionary as a parameter, and returns it as a single parameter of the return dictionary, inp.

  8. Create the echo action:

    wsk action create echo echo.js

  9. Invoke the echo action:

    wsk action invoke echo -p a b -p c d -r

    The -p command line arguments specify parameter names and values. The -r flag specifies that wsk is to wait until the results are available and return them, rather than return immediately with an invocation number that can be used to get the response later.

    Then see the following result. The response includes the parameters specified on the command line:

     {
       "inp": {
       "a": "b",
       "c": "d"
     }
     }
    
  10. Get the ID of the OpenWhisk Vagrant VM:

    vagrant global-status

    You see an ID like the following screen capture:

    images

    The reason you need this ID is to start and stop the VM that Vagrant had just created.

    Note that before you shut down the server on which it is running, you need to run the following command:

    vagrant suspend <id>

    To start it again after a reboot, run the following command:

    vagrant up <id>

    On a production system you start and stop the Vagrant VM automatically, for example, by using systemd on Ubuntu.

Step 2: Install the same action locally and remotely

To be able to share the load between your local server and IBM Cloud Functions you need to run the same action on both.

  1. Read the local OpenWhisk properties and save them somewhere safe:

    cat ~/.wksprops

  2. Create a longAction.js action, by downloading the file and running the following command:

    wsk action update long longAction.js --kind nodejs:10

    Now, to simulate long acting actions (such as reading from a database or contacting the API for another service), you need an action that takes a long time.

    Here is how it works:

    The timeout function returns a Promise object. These objects are used in JavaScript when a process is expected to take a long time. It is created with a function to run that performs the actual processing, and this function gets two parameters: resolve, a function to call in the case of success, and reject, a function to call in case of failure.

    The following Promise function uses setTimeout to wait a number of seconds, and then it calls the resolve function to report success:

     const timeout = seconds => {
         return new Promise((resolve, reject) => {
             setTimeout(() => resolve(), seconds*1000);
         });
     };
    

    The following function is called when the action is invoked: const main = async params => {

    It actually returns a Promise object, but it uses the async/await syntax, which lets you ignore the single threaded nature of JavaScript.

    The following await keyword specifies that the expression following it is a Promise:

     await timeout(30);
     return {inp: params};
     };
    

    JavaScript appears to run the Promise, pause the execution of the function until the Promise returns, and then resume. Behind the scenes, the Promise objects implement this behavior, but as an application designer, you don’t need to be concerned about it.

  3. Invoke the action. Note that it takes about 30 seconds.

    wsk action invoke long -p a 1 -p b 2 -r

  4. Open the IBM Cloud Functions Console and log in with your IBM account.

  5. Click API Key in the left sidebar.
  6. Copy the HOST and KEY to your ~/.wksprops file. You are changing a URL value into a host name.
  7. Run the same wsk command you did earlier to create the action on the private cloud. It will create the exact same action on the public cloud. Then, run the same wsk command that you did earlier to invoke that action on the public cloud.
  8. Make a note of the HOST and KEY for the public cloud.

Step 3: Create a Node.js application that uses FaaS

The next step is to create an application that uses the action you configured. Your local OpenWhisk server is not externally accessible, so this application runs on the machine that runs Vagrant. The following instructions assume that you are running Ubuntu 18.04 LTS. Other operating systems are similar.

  1. Install the prerequisites:

    sudo apt install nodejs npm

    Under the assumption that most readers who got this far understand JavaScript. I decided to write the application using Node.js. Node.js also requires its package manager, npm.

  2. Create a directory for the application and move to it:

    mkdir app; cd app

  3. Node.js applications are described by a package.json file. The following command creates that file:

    npm init

    The values requested are unimportant in a demonstration application.

  4. Install the libraries (called modules) that are needed. In this case, you need two libraries: the Express module that provides the web server and the OpenWhisk module that lets you call the remote functions.

    npm install express openwhisk --save

  5. Download the file as app.js. Specify your local OpenWhisk access parameters on lines 6-7, and the remote IBM Cloud Functions parameters on lines 11-12.

  6. Run the application:

    nodejs app.js

  7. Browse to the IP address of the computer running the application (available through ifconfig under Linux, ipconfig under Windows) to port 3000.

    Notice that it takes about half a minute to reply, and then returns with the requested information. Open multiple tabs to see that requests are processed in parallel. Some requests get handled locally, and others are handled remotely on Cloud Functions. The expected result looks like the following screen capture:

    image

    The application is mostly standard Node.js. The following parts are specific to FaaS.

    The following line specifies that the program uses the OpenWhisk module:

    const ow = require("openwhisk");

    The following lines specify the connection parameters for the two FaaS servers that you use:

     const owOptsLocal = {
         apihost: "http://192.168.33.16:10001",
         api_key: <<< redacted >>>
     };
    
     const owOptsRemote = {
         apihost: "us-south.functions.cloud.ibm.com",
         api_key: <<< redacted >>>
     };
    

    The next step, shown in the following lines, creates the FaaS connections:

     const owLocal = ow(owOptsLocal);
     const owRemote = ow(owOptsRemote);
    

    You use the following variable to distinguish between different invocations:

    var invokeNum = 0;

    This distinguishing between the invocations is important because additional requests can come in while you are waiting for the response to an invocation.

    The following simple function invokes an action for you:

     const invoke = async (server, name, params) => {
         return await server.actions.invoke({
             name: name,
             blocking: true,
             params: params
         });
     };  // invoke
    

    The following function call sets up the response to an HTTP request for /.
    app.get("/", async (req, res) => {

    This function is part of the application that actually creates the HTML that you see on the browser.

    The following me variable is used to distinguish between different requests: const me = invokeNum++;

    As a local variable, it has a different value in each invocation of this function.

    The application reports the time and invocation number to the log. If you run the application from the command line, you see it in standard output: console.log(`${new Date} start of ${me}`);

    This expression uses the conditional operator to choose between the local and remove FaaS servers. The percent sign (%) stands for modulo. If the me variable is even, then me % 2 is zero (meaning false), and the server used is owRemote. If the me variable is odd, then me % 2 is one (meaning true), and the server used is owLocal. You can verify connectivity to both the local server and the remote one. The second parameter is the name of the action, and the third parameter of the function is a dictionary with the parameters that are provided to the action. The parameters are only there to show how to send parameters to the action, so a constant value is fine.

       const owRes = await invoke( me%2 ? owLocal : owRemote,
       "long", {a: 1, b:2});
    

    Next, the application logs and sends the result. The log entries let you see how long it took to process each action. The following results in owRes tell you how it was processed, as well as other information:

     console.log(`${new Date} ${me} result is ${
             JSON.stringify(owRes)}`);
    
         res.send(owRes);
     });  // app.get
    

    As you can see in the following screen capture, invocation number zero started at 18:20:20 and ended at 18:20:52, 32 seconds later. The path, developerWorks_Ori-Pomerantz-apps/long, is the path for the action on the IBM Cloud. Invocation number one started at 18:20:44, and ended at 18:21:15, 31 seconds later. You can see from the path, guest/long, that it was local.

    For clarity, I marked the times in red and the paths in blue.

    image

Step 4: Add logic to decide how to distribute the calls

Now that you have an application that can use both FaaS servers, the final step is to figure out which one to use. The easiest way to determine which server to use is to count how many invocations the local server is currently handling. If that number exceeds a limit, then route requests to the public cloud instead.

You can see the source code for an application that implements this technique on GitHub at 03_app.js.

Here is how the application works.

First, you need a global variable to identify how many local requests are currently running. When you run a simple, single threaded application, that is sufficient. You need a more sophisticated solution for a complicated application that can run in multiple instances (for fault tolerance or performance), for example a Cloudant database entry.

Alternatively, you can assign each instance a number of local requests, which when exceeded causes it to use Cloud Functions. The following algorithm might incur additional expenses, but is a lot simpler.

```
var localReqs = 0;

...

app.get("/", async (req, res) => {
const me = invokeNum++;
```

The following boolean variable holds whether to go to the local server, or the remote server (if there are less than ten requests on it right now, which is a ridiculously low value for production, but one that makes testing the program easy):

```const local = localReqs < 10;```

The application uses the local variable to identify the server to use:

```const owServer = local ? owLocal : owRemote;```

If you are running a local request, the application increments the number of current local requests because you are about to issue one more:

```
  if (local) localReqs++;

  ...

    const owRes = await invoke(owServer, "long", {a: 1, b:2});

    ...
```

After the request returns, if it is local, the application decrements the number of current local requests:

```
if (local) localReqs--;

    res.send(owRes);
});  // app.get
```

To test this application, run it, browse to it, and reload the browser tab multiple times. As shown in the following screen capture, if you do so within a thirty second period, the log file shows that the first ten invocations (numbers zero to nine) are local, but the eleventh and subsequent ones, starting at ten, are remote. If you wait for some of the local ones to finish, new ones also are local.

image

Summary

You should now be able to write applications that can choose between running the FaaS component locally or remotely on IBM Cloud Functions. Using this capacity, you can write applications that have highly varying requirements (for example a tax filing application that almost nobody uses between April 16th and December 31, but that sees a lot of use during the first quarter of the year and extremely high use just before the April 15 deadline in the US).

Consider a few tips for writing applications that use this technique. The data storage for a hybrid cloud application needs to be accessible through the Internet to enable access from both the private cloud and the public cloud, for example the Cloudant database. If you also want to run the front end of the application (the Node.js application in this tutorial) on a public server, you need to make the OpenWhisk accessible through the Internet. For example, you can configure an NGINX proxy.