Composer is an IBM Cloud Functions programming model for composing individual functions into larger applications. Compositions, informally named apps, run in the cloud using automatically managed compute and memory resources. Composer is an extension of the function-as-a-service computing model, and enables stateful computation, control flow, and rich patterns of data flow.
Programming for the serverless cloud is a uniquely new experience. For this reason, we have developed a unified environment that offers the benefits and familiarity of a command line interface, with visualization and a graphical interface to assist in certain tasks. This environment is offered through a new tool called IBM Cloud Shell, or just Shell.
Shell is a cross-platform desktop application powered by Electron.
You must have a valid IBM Cloud (i.e., Bluemix) account, or deploy Apache OpenWhisk locally. This is needed because Composer builds on and extends Apache OpenWhisk, which powers IBM Cloud Functions.
Existing wsk
CLI users: You can go directly to Installing Shell.
New users using composer with IBM Cloud Functions: you need an IBM Cloud account, and the IBM Cloud CLI (bx
). You will also need to install the Cloud Function Plugin for bx:
$ bx plugin install Cloud-Functions -r bluemix
After installing bx
and the Cloud Function plugin, use bx login
to generate a access token for Cloud Function.
$ bx login -a api.ng.bluemix.net -o yourBluemixOrg -s yourBluemixSpace
Run a test to generate credentials and verify your setup. Here, we perform a blocking (synchronous) invocation of echo, passing it “hello” as an argument. If you see the return message, you are good to go.
$ bx wsk action invoke /whisk.system/utils/echo -p message hello --result { "message": "hello" }
New users using composer with Apache OpenWhisk: you need a valid $HOME/.wskprops
file and a locally deployed OpenWhisk instance.
Shell is currently distributed through the Node package manager.
$ npm install -g @ibm-functions/shell
We roll out frequent updates and bug fixes. You can check for new releases via fsh version -u
.
$ fsh version -u You are currently on version x.y.z Checking for updates... you are up to date!
We recommend updating the shell via the same npm install
command shown earlier. Consult the troubleshooting guide if your installation fails.
$ fsh shell
You will see a window popping up. Welcome to Shell!
Tip: If you are using Mac, you can keep Shell in the dock by right-clicking on the blue Cloud Function Shell icon and choose Options > Keep in Dock
. Next time you can click on the icon in the dock to launch Shell.
Compositions are described using a Node.js library which offers an SDK for describing control structures. We call these combinators. The simplest combinator constructs a sequence. For example, here is a snippet for your first app:
composer.sequence(args => ({msg: `hello ${args.name}!`}))
The code describes a sequence app with just one function that is inlined for convenience.
There are two ways to deploy a composition code snippet. One is using Shell to write the code and deploy it. The other is using any editor to write this code, save it as a local file, and deploy the file using a Shell command. We describe both here.
In Shell, enter
# enter in Shell > compose myApp
where myApp
is the name of the app in the cloud. This command opens a built-in editor in a sidecar for writing code. Copy the composer.sequence
code above and paste it into the editor. Hit “Deploy” at the bottom stripe to deploy it. After the app is successfully deployed, Shell will show a flow graph that represents the textual composition code at the bottom of the editor as a verification.
Tip: Enter edit myApp
to edit myApp
after it is deployed.
Your first app, composed in Shell. |
You may also use your favorite editor to compose apps. When finished, save your code to a file on your machine with the extension .js
. To view your local composition javascript file as a graph, enter
# enter in Shell > app preview path/to/file.js
Shell watches the file you are editing and automatically updates the graph as you compose. You can use this active preview mode to incrementally build your application, sanity checking your control flow as you go.
Writing an app in my own editor, and previewing the code as a graph in Shell. |
To deploy a local composition file to the cloud, enter this command:
# enter in Shell > app create myApp path/to/file.js
Again, myApp
is the name of the app in the cloud.
Tip: If you have an action already named myApp
, the shell will report a name conflict. Use a different name for your app, or use app update
if you want to update an existing app. Apps are stored as OpenWhisk actions, and hence the naming restrictions for OpenWhisk apply.
Run your first app using this command:
# enter in Shell > app invoke myApp -p name composer
You will see the result in the sidecar. Click on different buttons in the sidecar bottom stripe to explore different views.
Tip #1: Enter app invoke --help
to view the usage of the app invoke
command. You can access the usage information of other commands in Shell in the same way using --help
.
Tip #2: Enter session list
to view a list of previous app executions.
Combinators accept either inline Node.js functions or actions by name. For the latter, you may use a fully qualified name of an action (i.e., /namespace[/package]/action
) or its short name. Here is an example using the date
action from the /whisk.system/utils
package.
composer.sequence('/whisk.system/utils/date')
A composition which refers to actions by name will not run correctly if there are missing referenced entities. The app preview
will highlight any missing entities. As an example, preview the built-in @demos/if.js
composition, which is described in the next section. The control flow graph should be self-explanatory. An action is gray when it is not yet deployed, and blue otherwise.
# enter in Shell > app preview @demos/if.js
Control flow graph for if combinator. An action that is not yet deployed is gray, and blue otherwise. |
Tip: Shell supports wsk
CLI commands for deploying OpenWhisk actions. We will explain how to do so next. You can also read more about using wsk
commands in Shell here.
The main difference between using an inline function verses a OpenWhisk action in a composition is that an inline function does not generate an activation like an OpenWhisk action. Activations record runtime data like the execution time and output. They are useful for debugging.
When making a real app, we encourage you to create the main components as OpenWhisk actions, as OpenWhisk actions can be reused by different apps and are easier to debug. Inline functions can be used as a convenient way to connect different components together (such as renaming input and output, generating an error message) and are better kept short and simple.
You now have the basic tools to build a serverless composition, invoke it, and inspect its execution and result. Currently, Composer offers 13 different combinators to support conditions, iterations, error handling, variable declarations and other common programming constructs for building various types of apps.
This section will introduce you to some combinators for creating richer control and data flow, while other combinators are covered in the reference manual. All javascript code described below is bundled in Shell and can be accessed within Shell using the prefix @demos/
.
if
combinatorAn if
combinator allows you to describe a conditional flow with a then
and optional else
branch. This is convenient for short-circuiting a sequence for example, or taking data-dependent paths in the control flow.
Here is a short example. Say you have a function welcome
which generates an HTML page.
// @demos/welcome.js let welcome = args => ({ html: `<html><body>welcome ${args.name}!</body></html>` })
In order to use this function as part of an authenticated API, we can modify the function itself to introduce authentication middleware. Or, we can compose it with an authentication function.
// @demos/authenticate.js let authenticate = args => ({ value: args.token === "secret" })
For illustration purposes, authenticate
is a simple token based checker. If the token equals the secret value, return true
, and false
otherwise. In a real scenario, this function may delegate to a third party service or identity provider.
Let's add a third function, this one to deal with the non-authenticated case and return a different HTML page, perhaps informing the client to try again with the proper secret.
// @demos/login.js let login = args => ({ html: `<html><body>please say the magic word.</body></html>` })
The if
combinator composes these three functions as you might expect.
// @demos/if.js composer.if( /* cond */ 'authenticate', /* then */ 'welcome', /* else */ 'login')
Now, enter the following in Shell to deploy and run the app.
# enter in Shell # create required actions > action create authenticate @demos/authenticate.js > action create welcome @demos/welcome.js > action create login @demos/login.js # create app > app create if @demos/if.js # invoke app, with no secret parameter > app invoke if { html: "<html><body>please say the magic word.</body></html>" } # now invoke with secret parameter > app invoke if -p token secret -p name if-combinator { html: "<html><body>welcome if-combinator!</body></html>" }
Tip: You can see the output data of an action node in the Session Flow
graph by clicking on the node. This will bring you to the corresponding activation. Note: An inline function node is not clickable as it does not generate an activation.
Each of the activations will have a different session id, which are reported by listing the available sessions.
# enter in Shell > session list
Clicking on a session id from the list will open that session in the sidecar.
Tip: Clicking on a session id invokes the command session get sessionId
to view the session info in the sidecar.
try
combinatorAnother common composition pattern is for error handling and recovery. Composer offers a try
combinator that is analogous to try-catch
.
A example to illustrate using try
is a schema or data validation action. Let validate
be an action which checks if a string is base64 encoded, and which throws an exception if the input is not valid. A try
combinator allows an error handler to rewrite the result, as one example, to suite the particular usage scenario in the app.
// @demos/try.js composer.try( /* try */ 'validate', /* catch */ args => ({ ok: false }))
The validate
action is available as @demos/validate.js
and the composition as @demos/try.js
for your convenience.
# enter in Shell # create validate action > action create validate @demos/validate.js # create app > app create try @demos/try.js # invoke app with valid parameter > app invoke try -p str aGVsbG8gdHJ5IQ== { ok: true } # and now for the failing case > app invoke try -p str bogus { ok: false }
It is worth looking at the session flow of the second app invoke where the catch handler is invoked.
# enter in Shell > session get --last try
Session execution for try where the handler is invoked. |
Notice that the validate
action failed, as expected. This is visually recognized by the red-colored action, and the hover text which shows the action result containing the error. The app result is successful however, as the handler rewrites the exception into a different result.
An important property of the combinators is that they nest. This encourages modularity and composition reuse. The example that follows illustrates both composition nesting, and data forwarding. The example builds on the try
app described in the previous section. Here, after the validate task, we extend the composition with a base64 decoder to render the input str
in plain text.
Recall that the result of the validate
task is {ok: true}
, not the str
argument that it processed. So we need a way to forward str
around this action. In other words, we retain the input arguments to validate
, and pass them to the next action in the sequence. Composer offers a combinator for just this purpose. Below is the composition showing the inner sequence with the data forwarding combinator retain
.
// @demos/retain.js composer.try( composer.sequence( composer.retain('validate'), args => ({ text: new Buffer(args.params.str, 'base64').toString() })), args => ({ ok: false }))
The retain
combinator produces an output with two fields: params
and result
. The former is the input parameter of the composition. The latter is the output of validate
. The control and dataflow for this composition is shown below, and is available in the shell as @demos/retain.js
.
# enter in Shell > app preview @demos/retain.js
Control flow graph showing the retain combinator and the implied dataflow around validate . |
The app will now produce the decoded text as its final output.
# enter in Shell # create app > app create retain @demos/retain.js # invoke app with valid parameter > app invoke retain -p str aGVsbG8gdHJ5IQ== { text: "hello try!" } # and now for the failing case > app invoke retain -p str bogus { ok: false }
The composer allows you to introduce variables within a composition, and to limit their scope. This is useful when you have to introduce service keys and credentials for example. A scoped variable is defined using let
. The example below illustrates how you might introduce a “secret” for a specific task without its value escaping to other compositions or functions.
// @demos/let.js composer.sequence( composer.let({secret: 42}, composer.task(_ => ({ ok: secret === 42 }))), composer.task(_ => ({ ok: (typeof secret === 'undefined') })))
The composition will execute successfully only if secret
is not leaked to the final task in the composition, while the value is available inside the task nested within the let
.
# enter in Shell > app create let @demos/let.js > app invoke let { ok: true }
The examples shown here illustrate the more common combinators you may use to create serverless compositions. There are more combinators available in the Composer library. Refer to the Composer reference manual for more details.
Try the second tutorial, building a translation chatbot.