This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Tutorial

Showcase serverless development in action

Tutorial

This tutorial walks you through developing a simple OpenServerless application using the Command Line Interface (CLI) and Javascript (but any supported language will do).

Its purpose is to showcase serverless development in action by creating a contact form for a website. We will see the development process from start to finish, including the deployment of the platform and running the application.

1 - Getting started

Let’s start building a sample application

Getting started

Build a sample Application

Imagine we have a static website and need server logic to store contacts and validate data. This would require a server, a database and some code to glue it all together. With a serverless approach, we can just sprinkle little functions (that we call actions) on top of our static website and let OpenServerless take care of the rest. No more setting up VMs, backend web servers, databases, etc.

In this tutorial, we will see how you can take advantage of several services which are already part of a OpenServerless deployment and develop a contact form page for users to fill it with their emails and messages, which are then sent via email to us and stored in a database.

Finally, we’ll see how to activate external services using Web hooks.

Openserverless CLI: Ops

Serverless development is mostly performed on the CLI, and OpenServerless has its tool called ops. It’s a command line tool that allows you to deploy (and interact with) the platform seamlessly to the cloud, locally and in custom environments.

Ops is cross-platform and can be installed on Windows, Linux and MacOS. You can find the project and the sources on Apache OpenServerless Cli Github page

Deploy OpenServerless

To start using OpenServerless you can refer to the Installation Guide. You can follow the local installation to quickly get started with OpenServerless deployed on your machine, or if you want to follow the tutorial on a deployment on cloud you can pick one of the many supported cloud provider. Once installed come back here!

Enabling Services

After installing OpenServerless on a local machine with Docker or on a supported cloud, you can enable or disable the services offered by the platform. As we will use Postgres database, the Static content with the Minio S3 compatible storage, let’s run in the terminal:

ops config enable --postgres --static --minio --cron

This is the default set of services.

Since you should already have a deployment running, we have to update it with the new services so they get deployed. Simply run:

ops update apply

And with just that (when it finishes), we have everything we need ready to use!

💡 NOTE

If you’ve installed the local development environment using the instructions from the Docker installation page you’ve already the base services enabled by default.

You can check what services are enabled with the command:

ops config status

This should be the output:

OPERATOR_COMPONENT_MINIO=true
OPERATOR_COMPONENT_MONGODB=true
OPERATOR_COMPONENT_POSTGRES=true
OPERATOR_COMPONENT_STATIC=true
OPERATOR_COMPONENT_CRON=true
OPERATOR_COMPONENT_REDIS=true

Create a user

If you don’t have a user, it’s the time to create one. We we’ll use it to work on this tutorial.

WARNING

To create a user, we need to be the administrator, like described in this section.

ops admin adduser opstutorial <youremail> SimplePassword --all 

The output will be:

Generated OPSTUTORIAL user secrets.
Creating user opstutorial...
whiskuser.nuvolaris.org/opstutorial created

Login as user

After user creation, it’s time to perform ops login.

💡 NOTE

The ops ide login command will log you in on the server and dump the proper configuration of active services for your user. The configuration is automatically used by ops for all the tasks. You only need to run ops ide login once (unless you need to log in to another OpenServerless server or with another OpenServerless user).

Change your APIHOST accordly, if you’ve specified a custom one during the system setup

ops ide login opstutorial http://localhost:80
*** Configuring Access to OpenServerless ***
apihost=http://localhost:80 username=opstutorial
Logging in http://localhost:80 as opstutorial
Enter Password: 
Successfully logged in as opstutorial.
ok: whisk auth set. Run 'wsk property get --auth' to see the new value.
ok: whisk API host set to http://localhost:80
OpenServerless host and auth set successfully. You are now ready to use ops!

Cleaning Up

Once you are done and want to clean the services configuration, just run:

ops config disable --postgres --static --minio --cron

2 - First steps

Move your first steps on Apache Openserverless

First steps

Starting at the Front

Right now, after a fresh installation, and after added the opstutorial user, if we visit the <apihost> you will see a very simple page with:

Welcome to Nuvolaris static content distributor landing page!!!

That’s because we’ve activated the static content, and by default it starts with this simple index.html page. We will instead have our own index page that shows the users a contact form powered by OpenServerless actions. Let’s write it now.

Let’s create a folder that will contain all of our app code: contact_us_app.

💡 NOTE

You can find the full source code of the tutorial at this GitHub Repository: Contact Us App.

The repository has a tag for each step. So after cloning it in your local directory, follow the istruction on it’s README page.

Inside that create two new folders called web, which will store our static frontend, and packages, which will store our backend actions. Inside the web folder an index.html file.

The directory structure should look like:

contact_us_app
├── packages
└── web
    └── index.html

Paste the following markup inside the index.html file:

<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Get In Touch</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
    <div class="container">
        <a class="navbar-brand" href="#">Apache OpenServerless™ Tutorial</a>
    </div>
</nav>

<div class="container d-flex justify-content-center align-items-center" style="min-height: 80vh;">
    <div class="w-50 p-4 border rounded bg-light shadow">
        <h2 class="text-center mb-4">Get In Touch</h2>
        <form>
            <div class="mb-3">
                <label for="name" class="form-label">Name</label>
                <input type="text" class="form-control" id="name" name="name" placeholder="Insert your name">
            </div>
            <div class="mb-3">
                <label for="email" class="form-label">Email</label>
                <input type="email" class="form-control" id="email" name="email" placeholder="Insert your email">
            </div>
            <div class="mb-3">
                <label for="phone" class="form-label">Phone Number</label>
                <input type="tel" class="form-control" id="phone" name="phone" placeholder="Insert you phone number">
            </div>
            <div class="mb-3">
                <label for="message" class="form-label">Message</label>
                <textarea class="form-control" id="message" name="message" rows="4" placeholder="Type here your message"></textarea>
            </div>
            <button type="submit" class="btn btn-primary w-100">Send !</button>
        </form>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

WARNING

Before move on, be sure to have completed once the login as indicated here

Now we just have to upload it to our OpenServerless deployment. You could upload it using something like curl with a PUT to where your platform is deployed at, but there is an handy command that does it automatically for all files in a folder:

ops ide deploy

The output will be:

> Scan:
> Deploying:
build process exited with code 0
UPLOAD ASSETS FROM web
==================| UPLOAD RESULTS |==================
| FILES      : 1
| COMPLETED  : 1
| ERRORS     : 0
| SKIPPED    : 0
| EXEC. TIME : 37.83 ms
======================================================
URL: http://opstutorial.localhost:80

The command will scan both packages and web directories and will upload the index.html to the web bucket. Finally it will show the URL where the frontend have been published. If you visit the URL within your browser, you should see the new index page:

Form

Development Tools

Apache OpenServerless has a set of development tools, inside the ops ide command, details of which are available in this section of the guide.

As shown before, we will be using ops ide for publishing, as this make the process quicker and easier.

The command ops ide login will enable the development tools.


3 - Form validation

Learn how to add form validation from front to back-end

The Contact Package

The contact form we just uploaded does not do anything. To make it work let’s start to fill our package directory with backend actions.

Form validation

We well start to handle the form submission. We can do that by adding a new action that will be called when the form is submitted. Let’s create a submit.js file inside our packages/contact folder.

You directory structure should looks like this:

contact_us_app
├── packages
│   └── contact
│       └── submit.js
└── web
    └── index.html

Paste this content inside the submit.js file:

//--web true
//--kind nodejs:default

function main(args) {
    let message = [];
    let errors = [];

    // TODO: Add here Form Validation code

    // TODO: Add here the code for returning the Result
}

This action is a bit more complex. It takes the input object (called args) which will contain the form data (accessible via args.name, args.email, etc.). With that. we will do some validation and then return the result.

💡 NOTE

You may have noticed the strange comments at the beginning of the file. Those comments are used by ops ide to automatically handle the publishing of files by calling ops package or ops action as needed. In particular:

  • the --web true will enable this as a web action;
  • --kind nodejs:default will ask OpenServerless to run this code on the nodejs default runtime.

Validation

Let’s start filling out the “Form Validation” part by checking the name:

// validate the name
if (args.name) {
    message.push("name: " + args.name);
} else {
    errors.push("No name provided");
}

Then the email by using a regular expression:

// validate the email
var re = /\S+@\S+\.\S+/;
if (args.email && re.test(args.email)) {
    message.push("email: " + args.email);
} else {
    errors.push("Email missing or incorrect.");
}

The phone, by checking that it’s at least 10 digits:

// validate the phone
if (args.phone && args.phone.match(/\d/g).length >= 10) {
    message.push("phone: " + args.phone);
} else {
    errors.push("Phone number missing or incorrect.");
}

Finally, the message text, if present:

// validate the message
if (args.message) {
    message.push("message:" + args.message);
}

Submission

With the validation phase, we added to the “errors” array all the errors we found, and to the “message” array all the data we want to show to the user. So if there are errors, we have to show them, otherwise, we store the message and return a “thank you” page.

// return the result
if (errors.length) {
    var errs = "<ul><li>" + errors.join("</li><li>") + "</li></ul>";
    return {
        body: "<h1>Errors!</h1>" +
            errs + '<br><a href="javascript:window.history.back()">Back</a>'
    };
} else {
    var data = "<pre>" + message.join("\n") + "</pre>";
    return {
        body: "<h1>Thank you!</h1>" + data,
        name: args.name,
        email: args.email,
        phone: args.phone,
        message: args.message
    };
}

Note how this action is returning HTML code. Actions can return a { body: <html> } kind of response and have their own url so they can be invoked via a browser and display some content.

The HTML code to display is always returned in the body field, but we can also return other stuff. In this case we added a a field for each of the form fields. This gives us the possibility to invoke in a sequence another action that can act just on those fields to store the data in the database.

Let’s start deploying the action:

ops ide deploy

You should see output like this:

/home/openserverless/.ops/tmp/deploy.pid
PID 70925
> Scan:
>> Action: packages/contact/submit.js
> Deploying:
>> Package: contact
$ $OPS package update contact 
ok: updated package contact
>>> Action: packages/contact/submit.js
$ $OPS action update contact/submit packages/contact/submit.js --web true --kind python:default --param POSTGRES_URL $POSTGRES_URL
ok: updated action contact/submit
build process exited with code 0
UPLOAD ASSETS FROM web
==================| UPLOAD RESULTS |==================
| FILES      : 1
| COMPLETED  : 1
| ERRORS     : 0
| SKIPPED    : 0
| EXEC. TIME : 40.76 ms
======================================================
URL: http://opstutorial.localhost:80

You can retrieve the url of the action with:

ops url contact/submit

You should see this output:

ok: got action submit
http://localhost:80/api/v1/web/opstutorial/contact/submit

If you click on it you will see the Error page with a list of errors, that’s because we just invoked the submit logic for the contact form directly, without passing in any args. This is meant to be used via the contact form page!

We need to wire it into the index.html. So let’s open it again and add a couple of attributes to the form. Change the <form> tag as follow:

<form method="POST" action="/api/v1/web/opstutorial/contact/submit"
      enctype="application/x-www-form-urlencoded">

Upload the web folder again with the new changes:

ops ide deploy

Now if you go to the contact form page the send button should work. It will invoke the submit action which in turn will return some html.

If you fill it correctly, you should see the “Thank you” page.

Submit Result

Note how only the HTML from the body field is displayed, the other fields are ignored in this case.

The ops action command can be used for many more things besides creating actions. For example, you can use it to list all available actions:

ops action list
actions
/opstutorial/contact/submit                 private nodejs:21

And you can also get info on a specific action:

ops action get contact/submit
ok: got action contact/submit
{
    "namespace": "opstutorial/contact",
    "name": "submit",
    "version": "0.0.1",
    "exec": {
        "kind": "nodejs:21",
        "binary": false
    },
    "annotations": [
        {
            "key": "web-export",
            "value": true
        },
        {
            "key": "raw-http",
            "value": false
        },
        {
            "key": "final",
            "value": true
        },
        {
            "key": "provide-api-key",
            "value": false
        },
        {
            "key": "exec",
            "value": "nodejs:21"
        }
    ],
    "parameters": [
        {
            "key": "POSTGRES_URL",
            "value": "postgresql://opstutorial:<password>@nuvolaris-postgres.nuvolaris.svc.cluster.local:5432/opstutorial"
        }
    ],
    ...
}

These commands can come in handy when you need to debug your actions.

Here is the complete the submit.js action:

//--web true
//--kind nodejs:default

function main(args) {
    let message = [];
    let errors = [];

    // validate the name
    if (args.name) {
        message.push("name: " + args.name)
    } else {
        errors.push("No name provided")
    }

    // validate the email
    var re = /\S+@\S+\.\S+/;
    if (args.email && re.test(args.email)) {
        message.push("email: " + args.email);
    } else {
        errors.push("Email missing or incorrect.");
    }

    // validate the phone
    if (args.phone && args.phone.match(/\d/g).length >= 10) {
        message.push("phone: " + args.phone);
    } else {
        errors.push("Phone number missing or incorrect.");
    }

    // validate the message
    if (args.message) {
        message.push("message:" + args.message);
    }

    // return the result
    if (errors.length) {
        var errs = "<ul><li>" + errors.join("</li><li>") + "</li></ul>";
        return {
            body: "<h1>Errors!</h1>" +
                errs + '<br><a href="javascript:window.history.back()">Back</a>'
        };
    } else {
        var data = "<pre>" + message.join("\n") + "</pre>";
        return {
            body: "<h1>Thank you!</h1>" + data,
            name: args.name,
            email: args.email,
            phone: args.phone,
            message: args.message
        };
    }
}

4 - Use database

Store data into a relational database

Use database

Storing the Message in the Database

We are ready to use the database that we enabled at the beginning of the tutorial.

Usually, when working with relational databases, the best choice is to use a schema migration system. In our case, to keep things simple, we will emulate a migration using an action.

Now, we need to create a table to store the contact data: start by creating a new action called create-table.js in the packages/contact folder.

The directory structure have to be like this:

contact_us_app
├── packages
│   └── contact
│       ├── create-table.js
│       └── submit.js
└── web
    └── index.html

Put this content inside the create-table.js file:

//--kind nodejs:default
//--param POSTGRES_URL $POSTGRES_URL

const { Client } = require('pg')

async function main(args) {
    console.log('Starting create-table action')
    const client = new Client({ connectionString: args.POSTGRES_URL });

    const createSchema = `CREATE SCHEMA IF NOT EXISTS demo;`

    const createTable = `
    CREATE TABLE IF NOT EXISTS demo.contacts (
        id serial PRIMARY KEY,
        name varchar(50),
        email varchar(50),
        phone varchar(50),
        message varchar(300)
    );
    `

    try {
        console.log(`Connecting to ${args.POSTGRES_URL}`);
        await client.connect();
        console.log('Connected to database');
        await client.query(createSchema);
        console.log('Schema demo created');
        await client.query(createTable);
        console.log('Contact table created');
        return { result: 'OK' };
    } catch (e) {
        if (e instanceof AggregateError) {
            for (const err of e.errors) {
                console.error('[ERROR] - ', err.message || err);
            }
        } else if (e instanceof Error) {
            console.error('[ERROR]  - ', e.message);
        } else {
            console.error('[ERROR] - ', e);
        }
        return { result: 'ERROR' };
    } finally {
        console.log('Closing connection');
        if (client) {
            await client.end();
        }
    }
}

💡 NOTE

You may have noticed here again the comments on top of the file. As said before, these comments are used by ops ide to automatically handle the publishing of files by calling ops package or ops action as needed. In particular:

  • --kind nodejs:default will ask OpenServerless to run this code on the nodejs default runtime.
  • the --param POSTGRES_URL $POSTGRES_URL will automatically fill in the parameters required by the action, taking it's value from ops's configuration file.

The action is idempotent, so you may call the action multiple times, but the schema and the table is created only once.

You can deploy this action using ops ide deploy command.

ops ide deploy

The output will be like:

/home/openserverless/.ops/tmp/deploy.pid
PID 52906
> Scan:
>> Action: packages/contact/create-table.js
>> Action: packages/contact/submit.js
> Deploying:
>> Package: contact
$ $OPS package update contact 
ok: updated package contact
>>> Action: packages/contact/create-table.js
$ $OPS action update contact/create-table packages/contact/create-table.js --kind nodejs:default --param POSTGRES_URL $POSTGRES_URL
ok: updated action contact/create-table
>>> Action: packages/contact/submit.js
$ $OPS action update contact/submit packages/contact/submit.js --web true --kind nodejs:default
ok: updated action contact/submit
build process exited with code 0
UPLOAD ASSETS FROM web
==================| UPLOAD RESULTS |==================
| FILES      : 1
| COMPLETED  : 1
| ERRORS     : 0
| SKIPPED    : 0
| EXEC. TIME : 28.37 ms
======================================================
URL: http://opstutorial.localhost:80

In OpenServerless an action invocation is called an activation. You can keep track, retrieve information and check logs from an action with ops activation. For example, with:

ops activation list

You can retrieve the list of invocations. For caching reasons the first time you run the command the list might be empty. Just run it again and you will see the latest invocations (probably some hello actions from the deployment).

If we want to invoke the create-table action, we can do it with this command.

ops action invoke contact/create-table

The output will be like:

ok: invoked /_/contact/create-table with id e67a6c6f5a9c4667ba6c6f5a9c46675b

The activation will return an id: in our case the id is e67a6c6f5a9c4667ba6c6f5a9c46675b. You can retrieve the activation log with the command ops activation logs <id> or ops activation logs --last to retrieve the last activation log.

ops activation logs e67a6c6f5a9c4667ba6c6f5a9c46675b
2025-03-17T23:28:03.390748125Z stdout: Starting create-table action
2025-03-17T23:28:03.391745125Z stdout: Connecting to postgresql://opstutorial:password@nuvolaris-postgres.nuvolaris.svc.cluster.local:5432/opstutorial
2025-03-17T23:28:03.405132167Z stdout: Connected to database
2025-03-17T23:28:03.406006792Z stdout: Schema demo created
2025-03-17T23:28:03.406601042Z stdout: Contact table created
2025-03-17T23:28:03.406604209Z stdout: Closing connection
..

We could run ops activation poll or ops ide poll to listen for new logs.

To check that the table is really there, and inspect it’s schema you can use the ops devel psql describe tool:

ops devel psql describe "demo.contacts" --format=table

You should see:

┌───┬───────────────┬──────────────┬─────────────┬───────────────────┬─────────────┐
│   │ table_catalog │ table_schema │ column_name │ data_type         │ is_nullable │
├───┼───────────────┼──────────────┼─────────────┼───────────────────┼─────────────┤
0 │ opstutorial   │ demo         │ id          │ integer           │ NO          │
1 │ opstutorial   │ demo         │ name        │ character varying │ YES         │
2 │ opstutorial   │ demo         │ email       │ character varying │ YES         │
3 │ opstutorial   │ demo         │ phone       │ character varying │ YES         │
4 │ opstutorial   │ demo         │ message     │ character varying │ YES         │
└───┴───────────────┴──────────────┴─────────────┴───────────────────┴─────────────┘

The Action to Store the Data

We could just write the code to insert data into the table in the submit.js action, but it’s better to have a separate action for that.

Let’s create a new file called write.js in the packages/contact folder:

// write.js

//--kind nodejs:default
//--param POSTGRES_URL $POSTGRES_URL

const {Client} = require('pg')

async function main(args) {
    const client = new Client({connectionString: args.POSTGRES_URL});

    // Connect to database server
    await client.connect();

    const {name, email, phone, message} = args;

    try {
        let res = await client.query(
            'INSERT INTO demo.contacts(name,email,phone,message) VALUES($1,$2,$3,$4)',
            [name, email, phone, message]
        );
        console.log(res);
    } catch (e) {
        console.log(e);
        throw e;
    } finally {
        client.end();
    }

    return {
        body: args.body,
        name,
        email,
        phone,
        message
    };
}

Very similar to the create table action, but this time we are inserting data into the table by passing the values as parameters. There is also a console.log on the response in case we want to check some logs again.

Let’s deploy it:

ops ide deploy
/home/openserverless/.ops/tmp/deploy.pid
/Users/bruno/.ops/tmp/deploy.pid
PID 57700
> Scan:
>> Action: packages/contact/write.js
>> Action: packages/contact/create-table.js
>> Action: packages/contact/submit.js
> Deploying:
>> Package: contact
$ $OPS package update contact 
ok: updated package contact
>>> Action: packages/contact/write.js
$ $OPS action update contact/write packages/contact/write.js --kind nodejs:default --param POSTGRES_URL $POSTGRES_URL
ok: updated action contact/write
>>> Action: packages/contact/create-table.js
$ $OPS action update contact/create-table packages/contact/create-table.js --kind nodejs:default --param POSTGRES_URL $POSTGRES_URL
ok: updated action contact/create-table
>>> Action: packages/contact/submit.js
$ $OPS action update contact/submit packages/contact/submit.js --web true --kind nodejs:default
ok: updated action contact/submit
build process exited with code 0
UPLOAD ASSETS FROM web
==================| UPLOAD RESULTS |==================
| FILES      : 1
| COMPLETED  : 1
| ERRORS     : 0
| SKIPPED    : 0
| EXEC. TIME : 28.92 ms
======================================================
URL: http://opstutorial.localhost:80

Finalizing the Submit

Alright, we are almost done. We just need to create a pipeline of submitwrite actions. The submit action returns the 4 form fields together with the HTML body. The write action expects those 4 fields to store them. Let’s put them together into a sequence:

ops action create contact/submit-write  --sequence contact/submit,contact/write --web true
ok: created action contact/submit-write

With this command we created a new action called submit-write that is a sequence of submit and write. This means that OpenServerless will call in a sequence submit first, then get its output and use it as input to call write.

Now the pipeline is complete, and we can test it by submitting the form again. This time the data will be stored in the database.

Note that write passes on the HTML body so we can still see the thank you message. If we want to hide it, we can just remove the body property from the return value of write. We are still returning the other 4 fields, so another action can use them (spoiler: it will happen next chapter).

Let’s check out again the action list:

ops action list
actions
/opstutorial/contact/submit-write                                      private sequence
/opstutorial/contact/submit                                            private nodejs:21
/opstutorial/contact/create-table                                      private nodejs:21
/opstutorial/contact/write                                             private nodejs:21

You probably have something similar. Note the submit-write is managed as an action, but it’s actually a sequence of 2 actions. This is a very powerful feature of OpenServerless, as it allows you to create complex pipelines of actions that can be managed as a single unit.

Trying the Sequence

As before, we have to update our index.html to use the new action. First let’s get the URL of the submit-write action:

ops url contact/submit-write
<apihost>/api/v1/web/openserverless/contact/submit-write

Then we can update the index.html file. Change the form submit action with the url from the previous command:

<form method="POST" action="/api/v1/web/opstutorial/contact/submit-write"
          enctype="application/x-www-form-urlencoded">

We just need to add -write to the action name.

Now give a ops ide deploy to publish all the modifications.

Try again to fill the contact form (with correct data) and submit it. This time the data will be stored in the database.

View data from db

If you want to retrieve data from your database, ops provides several utilities under the ops devel command. They are useful to interact with the integrated services, such as the database we are using.

For instance, to interact with PostgreSQL database, let’s run:

echo "SELECT * FROM demo.CONTACTS" | ops devel psql sql --format=table

You should see an output like this:

┌───┬────┬────────────────┬─────────────────────────┬─────────────┬──────────────────────────────┐
│   │ id │ name           │ email                   │ phone       │ message                      │
├───┼────┼────────────────┼─────────────────────────┼─────────────┼──────────────────────────────┤
01  │ OpenServerless │ user@openserverless.dev │ 39123123123 │ Hello Apache OpenServerless! │
└───┴────┴────────────────┴─────────────────────────┴─────────────┴──────────────────────────────┘

5 - Sending notifications

Sending notifications on user interaction

Sending notifications

Contact notification

It would be great if we receive a notification when a user tries to contact us.

For this tutorial we will use a free service that instantly generates a unique URL through which you can receive and view webhook payloads in real time.

💡 NOTE

You could replace this service with a workflow automation system, with your CRM webhook, with Slack hooks etc.

By navigating to the site https://webhook.site/ you will receive a unique url, as in the image below:

Webhook.site

Take note of the url under the “Your unique URL” title.

Once we have a webhook we can proceed to create a new action called notify.js (in the packages/contact folder).

The directory structure will be:

contact_us_app
├── packages
│   └── contact
│       ├── create-table.js
│       ├── notify.js
│       ├── submit.js
│       └── write.js
└── web
└── index.html

Place this content inside the notify.js file:

// notify.js

//--param NOTIFICATION_URL $NOTIFICATION_URL

function main(args) {
    const { name, email, phone, message } = args;

    const subject = `New contact request from Apache OpenServerless`;
    const payload = {
        subject,
        name,
        email,
        phone,
        message,
    };

    console.log("Built message", payload);

    return fetch(args.NOTIFICATION_URL, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
    })
        .then(response => {
            if (!response.ok) {
                console.log("Error sending message. Status code:", response.status);
            } else {
                console.log("Message sent successfully");
            }
            return {
                body: args.body,
            };
        })
        .catch(error => {
            console.log("Error sending message", error);
            return {
                body: error,
            };
        });
}

💡 NOTE

In this case, we don’t need to annotate the action as web. This because this action will be invoked in a sequence: so it’s an internal action and is not exposed as an api.

This action has the args.NOTIFICATION_URL parameter, which is the webhook. It also has the usual 4 form fields parameters that receives in input, used to build the text of the message. The action will return the body of the response from the webhook.

The NOTIFICATION_URL may contains different values between a development environment and a production one. No problem! Apache OpenServerless deployer supports .env file. Create a .env file under the package directory.

The directory structure now will look like:

contact_us_app
├── packages
│   ├── .env
│   └── contact
│       ├── create-table.js
│       ├── notify.js
│       ├── submit.js
│       └── write.js
└── web
└── index.html

Inside the .env file put this content:

NOTIFICATION_URL=<url>

Replace <url> with the url received from webhook.site.

Now deploy everything as usual, giving:

ops ide deploy

Creating Another Action Sequence

We have developed an action that can send a message as a standalone action, but we designed it to take the output of the submit action and return it as is. Time to extend the previous sequence!

Note that it will send messages for every submission, even for incorrect inputs, so we will know if someone is trying to use the form without providing all the information. But we will only store the fully validated data in the database.

Let’s create the sequence, and then test it:

ops action create contact/submit-notify --sequence contact/submit-write,contact/notify --web true

You should see this output:

ok: created action contact/submit-notify

We just created a new sequence submit-notify from the previous sequence submit-write and the new notify.

If you want to get more info about this sequence, you can use the ops action get command:

ops action get contact/submit-notify

You should see this output:

{
    "namespace": "openserverless/contact",
    "name": "submit-notify",
    "version": "0.0.1",
    "exec": {
        "kind": "sequence",
        "components": [
            "/openserverless/contact/submit-write",
            "/openserverless/contact/notify"
        ]
    },
    ...
}

See how the exec key has a kind of sequence and a list of components that are the actions that compose the sequence.

Now to start using this sequence instead of using the submit action, we need to update the web/index.html page to invoke the new sequence.

As before let’s grab the url:

ops url contact/submit-notify
<apihost>/api/v1/web/openserverless/contact/submit-notify

And update the action inside the file web/index.html:

<form method="POST" action="/api/v1/web/opstutorial/contact/submit-notify"
    enctype="application/x-www-form-urlencoded">

Don’t forget to re-publish everything with ops ide deploy.

Now try to fill out the form again and press send! It will execute the sequence and you will receive the message piped from action /contact/submit-write to /contact/notify.

Webhook.site result

The tutorial introduced you to some utilities to retrieve information and to the concept of activation. Let’s use some more commands to check out the logs and see if the message was really sent.

The easiest way to check for all the activations that happen in this app with all their logs is:

ops activation poll

Enter Ctrl-c to exit.
Polling for activation logs

This command polls continuously for log messages. If you go ahead and submit a message in the app, all the actions will show up here together with their log messages.

To also check if there are some problems with your actions, run a couple of times ops activation list and check the Status of the activations. If you see some developer error or any other errors, just grab the activation ID and run ops logs <activation ID>.


6 - App Deployment

Learn how to deploy your app on Apache Openserverless

App Deployment

Deploy

Apache OpenServerless makes publishing a project a very simple operation. The project, organized in two main folders packages for the backend and web for the frontend, can be published immediately using the command ops ide deploy.

Once launched, the command takes care of:

  • creating the packages
  • preparing the actions with the relative dependencies
  • publishing the actions

Through the use of files according to the OpenWhisk manifests.yml standard, it is also possible to publish sequences, triggers and much more at the same time.

💡 NOTE

An OpenWhisk’s manifest file can be useful to automate the deploy of sequences, triggers, rules. Action and packages are simpler to deploy using ops ide deploy

The ops ide deploy command also takes care of managing the parameters inserted in the annotations and injecting the variables from the configuration or from the .env file located in the packages folder.

Packaging the App

Even if not necessary, we’ll package both actions and sequences. Let’s create, inside the packages folder, two files:

  • 01-actions.yaml
  • 02-sequences.yaml

We’ll do so, because actions are required to deploy sequences.

The directory structure should be like this:

contact_us_app
├── packages
│   ├── 01-actions.yaml
│   ├── 02-sequences.yaml
│   └── contact
│       ├── create-table.js
│       ├── notify.js
│       ├── submit.js
│       └── write.js
└── web
    └── index.html

The Action Manifest File

Inside the 01-actions.yaml put this content:

packages:
  contact:
    inputs:
      POSTGRES_URL:
        type: string
        value: $POSTGRES_URL    

    actions:
      submit:
        function: contact/submit.js
        web: true

      write:
        function: contact/write.js
        web: true

      notify:
        function: contact/notify.js
        web: true
        inputs:
          NOTIFICATION_URL:
            type: string
            value: $NOTIFICATION_URL

      create-table:
        function: contact/create-table.js

At the top level we have the standard packages keyword, under which we can define the packages we want. Until now we created all of our actions in the contact package so we add it under packages.

Then under each package, the actions keyword is needed so we can add our action custom names with the path to the code (with function). Finally we also add web: true which is equivalent to --web true when creating the action manually.

Finally we used the inputs keyword to define the parameters to inject in the function.

This file will be automatically deployed by the ops ide deploy command.

The Sequences Manifest File

Inside the 01-actions.yaml put this content:

packages:
  contact:
    sequences:
      submit-write:
        actions: submit, write
        web: true
      submit-notify:
        actions: submit-write, notify
        web: true

At the top level we define the packages keyword and immediately after, the contact package. We just have to add the sequences key at the contact level and define the sequences we want with the available actions.

Also this file will be automatically deployed by the ops ide deploy command.

Test the deploy

To test the deploy, let’s run again the command ops ide deploy:

ops ide deploy
/Users/openserverless/.ops/tmp/deploy.pid
PID 28177
> Scan:
>> Action: packages/contact/write.js
>> Action: packages/contact/create-table.js
>> Action: packages/contact/submit.js
>> Action: packages/contact/notify.js
> Deploying:
>> Package: contact
$ $OPS package update contact 
ok: updated package contact
>>> Action: packages/contact/write.js
$ $OPS action update contact/write packages/contact/write.js --kind nodejs:default --param POSTGRES_URL $POSTGRES_URL
ok: updated action contact/write
>>> Action: packages/contact/create-table.js
$ $OPS action update contact/create-table packages/contact/create-table.js --kind nodejs:default --param POSTGRES_URL $POSTGRES_URL
ok: updated action contact/create-table
>>> Action: packages/contact/submit.js
$ $OPS action update contact/submit packages/contact/submit.js --web true --kind nodejs:default
ok: updated action contact/submit
>>> Action: packages/contact/notify.js
$ $OPS action update contact/notify packages/contact/notify.js --param NOTIFICATION_URL $NOTIFICATION_URL
ok: updated action contact/notify
Found packages .env file. Reading it
>>> Manifest: packages/01-actions.yaml
$ $OPS -wsk project deploy --manifest packages/01-actions.yaml
Success: Deployment completed successfully.
>>> Manifest: packages/02-sequences.yaml
$ $OPS -wsk project deploy --manifest packages/02-sequences.yaml
Success: Deployment completed successfully.
build process exited with code 0
UPLOAD ASSETS FROM web
==================| UPLOAD RESULTS |==================
| FILES      : 1
| COMPLETED  : 1
| ERRORS     : 0
| SKIPPED    : 0
| EXEC. TIME : 35.72 ms
======================================================
URL: http://opstutorial.localhost:80

As you can see, after deploying the actions, the deployer will find the manifest files and deploy them in lexicographic order.


7 - Conclusions

Let’s continue our journey

Summarizing what we have seen so far, in this tutorial we have seen how to:

  1. set up an application;
  2. create and publish the frontend
  3. create and publish the backend, in the form of packages and actions;
  4. interact with services using the ops utility;
  5. publish the application and distribute it on test and production environments.

At this point, all you have to do is give space to your developer imagination and create your applications by taking advantage of the flexibility and scalability of Apache OpenServerless.

If you have questions or need support, reach us through: