Adobe I/O Runtime is based on OpenWhisk, and uses its architecture to provide function-as-a-service. Here is a look at the high-level OpenWhisk architecture:
This figure shows how Runtime (via OpenWhisk) is set up to respond to events and direct invocations. Whether the event comes from an external or internal source, it gets associated with a trigger, which invokes an action in accordance with whatever rules are applied. You can also invoke an action directly using the Runtime (OpenWhisk) CLI or the REST API.
Actions in Adobe I/O Runtime are written in JavaScript/Node.js. When Runtime receives a trigger, it instantiates and executes the associated action; the more triggers Runtime receives, the more actions are executed. Also, you can chain actions together by creating a sequence; this doesn’t require writing any code at all. With a sequence, you can execute a number of actions in series, piping the output of one action to the input of the next.
When each action is complete, the instantiation is disposed, so there’s no maintenance of state between actions. And, because the code isn’t maintained in memory bewteen instantiations, there’s no cost while the code isn’t computing. This makes Runtime very economical for the app developer, and also inherently scalable; there’s no limit to the number of actions that can be invoked, and the actions invoked always correspond to the trigger rate. This is very different from traditional long-running VMs or containers, which need to be architected for resiliency—provisioning multiple VMs or containers that run in parallel to take over if one VM or container fails. Such architectures incur the costs of constant uptime and require expertise and dedicated resources to design and configure properly and keep running.
Here we’ll trace the entire process, beginning with an event and finishing with the complete action executed in response. OpenWhisk (and therefore Runtime) is built on well-established open-source tools such as Nginx, Docker, Kafka, and CosmosDB. These are assembled together into a seamless pipeline to provide serverless event-based processing.
Before you can trace the process, you need an action against which to trigger an event. Actions are functions, so a simple JavaScript function will do.
If you want to follow along with these steps, you’ll need to get access to Runtime, then install and configure the Runtime (OpenWhisk) CLI. To do so, see Creating Actions.
Create the following function in any editor:
function main() {
console.log('Hello World');
return { hello: 'world' };
}
Save it as hello.js. This function will print "Hello World" to stdout and return a JSON object containing the key-value pair "hello: world".
Next, you need to upload this function to Runtime as an action. In the CLI, type the following command:
wsk action create helloAction <path>/hello.js
Now that the action is created, it’s ready to be invoked via an HTTP call or associated with a trigger. For this exercise, however, you’ll invoke it directly, again by means of the CLI:
wsk action invoke helloAction --result
Now that you’ve got an action and invoked it, how is it actually processed? As an HTTP request, of course. Since the Runtime (OpenWhisk) system is an open REST API and completely HTTP-based, the invoke you sent in the CLI is translated into an HTTP request against Runtime. The command translates roughly into the following POST:
POST /api/v1/namespaces/$userNamespace/actions/helloAction
Host: $openwhiskEndpoint
Note the $userNamespace variable. You can’t submit requests to Runtime without having access to a namespace; specifically, the same namespace into which the action was created. When you’re configured for an account in Runtime, you’re given a personal namespace. Internal process flow
Nginx is a reverse proxy and HTTP server. The OpenWhisk architecture uses it to terminal SSL and forward the HTTP request to the next component in the processing loop.
The Controller is the core component of Runtime (OpenWhisk). It serves as the interface for everything a user can do. It’s an imlementation of the actual REST API, written in the Scala programming language, and built on the Akka runtime environment and the Spray REST/HTTP toolkit.
The Controller receives your HTTP request from nginx and interprets it. The result may be a CRUD request, or a direct invocation of an action. In this example, the Controller reads your HTTP POST request to an existing action as an invocation of that action. It then moves to the next step.
Now the Controller has to verify that you are who you are (authentication), and you have permission to do what you’re asking (authorization). It does this by checking your credentials, which are stored in a CosmosDB database called subjects. If you have an account and you have the permissions required to invoke the action you’re requesting, which also depends on the action being in a namespace you own, then the Controller proceeds to the next step: retrieving the action itself.
CosmosDB is not only used to store users’ credentials, it’s also used to store the code for the actions themselves. So, once the Controller has determined your permissions by the first call to CosmosDB, it calls CosmosDB again, this time to a database called whisks. CosmosDB returns the code for the action, so the Controller can take the next step: queueing the action for processing.