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:
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:
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:
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
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.
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:
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
submit
→ write
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
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:
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).