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.

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 and a cron scheduler, let’s run in the terminal:

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

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!

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 freshly installation, if we visit the <apihost> you will see a very simple page with:

Welcome to OpenServerless 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.

Inside that create a new folder called web which will store our static frontend, and add there a index.html file with the following:

<!DOCTYPE html>
<html>
   <head>
      <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
   </head>

   <body>
      <div id="container">
         <div class="row">
            <div class="col-md-8 col-md-offset-2">
               <h4>Get in Touch</h4>
               <form method="POST">
                  <div class="form-group">
                     <input type="text" name="name" class="form-control" placeholder="Name">
                  </div>
                  <div class="form-group">
                     <input type="email" name="email" class="form-control" placeholder="E-mail">
                  </div>
                  <div class="form-group">
                     <input type="tel" name="phone" class="form-control" placeholder="Phone">
                  </div>
                  <div class="form-group">
                     <textarea name="message" rows="3" class="form-control" placeholder="Message"></textarea>
                  </div>
                  <button class="btn btn-default" type="submit" name="button">
                     Send
                  </button>
               </form>
            </div>
         </div>
      </div>
   </body>

</html>

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 web upload web/

Pass to ops web upload the path to folder where the index.html is stored in (the web folder) and visit again <apihost>.

Now you should see the new index page:

Form

The Contact Package

The contact form we just uploaded does not do anything. To make it work let’s start by creating a new package to hold our actions. Moreover, we can bind to this package the database url, so the actions can directly access it!

With the debug command you can see what’s going on in your deployment. This time let’s use it to grab the “postgres_url” value:

ops -config -d | grep POSTGRES_URL

Copy the Postgres URL (something like postgresql://...). Now we can create a new package for the application:

ops package create contact -p dbUri <postgres_url>
ok: created package contact

The actions under this package will be able to access the “dbUri” variable from their args!

To follow the same structure for our action files, let’s create a folder packages and inside another folder contact to give our actions a nice, easy to find, home.

To manage and check out your packages, you can use the ops packages subcommands.

ops package list

packages
/openserverless/contact  private
/openserverless/hello    private     <-- a default package created during deployment

And to get specific information on a package:

ops package get contact

ok: got package contact
{
   "namespace": "openserverless",
   "name": "contact",
   "version": "0.0.1",
   "publish": false,
   "parameters": [
      {
            "key": "dbUri",
            "value": <postgres_url>
      }
   ],
   "binding": {},
   "updated": 1696232618540
}

3 - Form validation

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

Form validation

Now that we have a contact form and a package for our actions, we have to handle the 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 in our packages/contact folder.

function main(args) {
  let message = []
  let errors = []
  // TODO: Form Validation
  // TODO: 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.

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 action create contact/submit submit.js --web true
ok: created action contact/submit

The --web true specifies it is a web action. We are creating a submit action in the contact package, that’s why we are passing contact/submit.

You can retrieve the url with:

ops url contact/submit

$ <apihost>/api/v1/web/openserverless/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:

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

Upload the web folder again with the new changes:

ops web upload web/

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
/openserverless/contact/submit               private nodejs:18

And you can also get info on a specific action:

ops action get contact/submit

{
    "namespace": "openserverless/contact",
    "name": "submit",
    "version": "0.0.1",
    "exec": {
        "kind": "nodejs:18",
        "binary": false
    },
  ...
}

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

Here is the complete the submit.js action:

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.

Since we are using a relational database, we need to create a table to store the contact data. We can do that by creating a new action called create-table.js in the packages/contact folder:

const { Client } = require('pg')

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

    const createTable = `
    CREATE TABLE IF NOT EXISTS contacts (
        id serial PRIMARY KEY,
        name varchar(50),
        email varchar(50),
        phone varchar(50),
        message varchar(300)
    );
    `
    // Connect to database server
    await client.connect();
    console.log('Connected to database');

    try {
        await client.query(createTable);
        console.log('Contact table created');
    } catch (e) {
        console.log(e);
        throw e;
    } finally {
        client.end();
    }
}

We just need to run this once, therefore it doesn’t need to be a web action. Here we can take advantage of the cron service we enabled! There are also a couple of console logs that we can check out.

With the cron scheduler you can annotate an action with 2 kinds of labels. One to make OpenServerless periodically invoke the action, the other to automatically execute an action once, on creation.

Let’s create the action with the latter, which means annotating the action with autoexec true:

ops action create contact/create-table create-table.js -a autoexec true
ok: created action contact/create-table

With -a you can add “annotations” to an action. OpenServerless will invoke this action as soon as possible, so we can go on.

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 make sure create-table was invoked, we can do it with this command. The cron scheduler can take up to 1 minute to run an autoexec action, so let’s wait a bit and run ops activation list again.

ops activation list

Datetime            Activation ID                    Kind      Start Duration   Status  Entity
2023-10-02 09:52:01 1f02d3ef5c32493682d3ef5c32b936da nodejs:18 cold  312ms      success openserverless/create-table:0.0.1
..

Or we could run ops activation poll to listen for new logs.

ops activation poll

Enter Ctrl-c to exit.
Polling for activation logs

When the logs from the create-table action appear, we can stop the command with Ctrl-c.

Each activation has an Activation ID which can be used with other ops activation subcommands or with the ops logs command.

We can also check out the logs with either ops logs <activation-id> or ops logs --last to quickly grab the last activation’s logs:

ops logs --last

2023-10-15T14:41:01.230674546Z stdout: Connected to database
2023-10-15T14:41:01.238457338Z stdout: Contact table created

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:

const { Client } = require('pg')

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

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

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

    try {
        let res = await client.query(
            'INSERT INTO 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 action create contact/write write.js
ok: created action contact/write

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
/openserverless/contact/submit-write                  private sequence
/openserverless/contact/write                         private nodejs:18
/openserverless/contact/create-table                  private nodejs:18
/openserverless/contact/submit                        private nodejs:18

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:

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

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

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

If you want to retrive info from you 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, let’s run:

ops devel psql sql "SELECT * FROM CONTACTS"

[{'id': 1, 'name': 'OpenServerless', 'email': 'info@nuvolaris.io', 'phone': '5551233210', 'message': 'This is awesome!'}]

5 - Sending notifications

Sending notifications on user interaction

Sending notifications

Contact notification

It would be great if we receive a notification when an user tries to contact us. For this tutorial we will pick slack to receive a message when it happens.

We need to:

  • have a slack workspace where we can send messages;

  • create a slack app that will be added to the workspace;

  • activate a webhook for the app that we can trigger from an action;

Check out the following scheme for the steps:

Slack Webhook

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

// notify.js
function main(args) {
    const { name, email, phone, message } = args;

    let text = `New contact request from ${name} (${email}, ${phone}):\n${message}`;
    console.log("Built message", text);

    return fetch(args.notifications, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ text }),
    })
        .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,
            };
        });
}

This action has the args.notifications 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.

We’ve also put some logs that we can use for debugging purposes.

Let’s first set up the action:

ops action create contact/notify notify.js -p notifications <your webhook>
ok: created action contact/notify

We are already setting the notifications parameter on action creation, which is the webhook. The other one is the text that the submit action will give in input at every invocation.

Creating Another Action Sequence

We have developed an action that can send a Slack 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
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

{
    "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 index.html:

---            <form method="POST" action="/api/v1/web/openserverless/contact/submit-write"
                enctype="application/x-www-form-urlencoded"> <-- old
+++            <form method="POST" action="/api/v1/web/openserverless/contact/submit-notify"
                enctype="application/x-www-form-urlencoded"> <-- new

Don’t forget to re-upload the web folder with ops web upload web/.

Now try to fill out the form again and press send! It will execute the sequence and you will receive the message from your Slack App.

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

Packaging the App

With OpenServerless you can write a manifest file (in YAML) to have an easy way to deploy applications.

In this last chapter of the tutorial we will package the code to easily deploy the app, both frontend and actions.

Start The Manifest File

Let’s create a “manifest.yaml” file in the packages directory which will be used to describe the actions to deploy:

packages:
  contact:
    actions:
      notify:
        function: contacts/notify.js
        web: true
        inputs:
          notifications:
            value: $NOTIFICATIONS

This is the basic manifest file with just the notify action. 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.

If we apply this manifest file (we will see how soon), it will be the same as the previous ops action create contact/notify <path-to-notify.js> -p notifications $NOTIFICATIONS --web true. You need to have the webhooks url in the NOTIFICATIONS environment variable.

The Submit Action

The submit action is quite straightforward:

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

The Database Actions

Similarly to the notify and submit actions, let’s add to the manifest file the two actions for the database. We also need to pass as a package parameter the DB url, so we will use inputs key as before, but at the package level:

packages:
  contact:
    inputs:
      dbUri:
        type: string
        value: $POSTGRES_URL
    actions:
      ...
      write:
        function: contact/write.js
        web: true

      create-table:
        function: contact/create-table.js
        annotations:
          autoexec: true

Note the create-table action does not have the web set to true as it is not needed to be exposed to the world. Instead it just has the annotation for cron scheduler.

The Sequences

Lastly, we created a sequence with submit and notify that we have to specify it in the manifest file as well.

packages:
  contact:
    inputs:
      ...

    actions:
      ...

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

We just have to add the sequences key at the contact level (next to actions) and define the sequences we want with the available actions.

Deployment

The final version of the manifest file is:

packages:
  contact:
    inputs:
      dbUri:
        type: string
        value: $POSTGRES_URL
    actions:
      notify:
        function: contact/notify.js
        web: true
        inputs:
          notifications:
            value: $NOTIFICATIONS

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

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

      create-table:
        function: contact/create-table.js
        annotations:
          autoexec: true

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

ops comes equipped with a handy command to deploy an app: ops project deploy.

It checks if there is a packages folder with inside a manifest file and deploys all the specified actions. Then it checks if there is a web folder and uploads it to the platform.

It does all what we did manually until now in one command.

So, from the top level directory of our app, let’s run (to also set the input env var):

export POSTGRES_URL=<your-postgres-url>
export NOTIFICATIONS=<the-webhook>

ops project deploy

Packages and web directory present.
Success: Deployment completed successfully.
Found web directory. Uploading..

With just this command you deployed all the actions (and sequences) and uploaded the frontend (from the web folder).