Microservice-aware Web Application (MAWA)
Introduction (and TL;DR)
You want to build a website based on microservices.
Leonardo makes it easy to build the services and handle separation of concerns by routing messages to (sub)services exposed to the web browser. Jo exposes these services as JavaScript objects to the webpage (an idea I’ve been toying with recently). I call the result a Microservice-aware Web Application (MAWA), because the webpage accesses explicitly (is aware of) the different (micro)services made available by the server.
The purpose of this document is to show you how. The only software you need installed in your computer to try out what I show here is Docker.
I’m going to use some Jolie language [Montesi et al., 2014], in particular its web features [Montesi, 2016], because it makes things very simple for the purposes of this tutorial. You do not need to know Jolie to read what follows: the code is simple enough to be explained on the fly.
References
You can run the finished application with the following Docker commands.
docker pull fmontesi/jo-demo-chuck
docker run -it --rm -p 8080:8080 fmontesi/jo-demo-chuck
Just browse to http://localhost:8080/ once the container is running.
You can look at the finished source code here: https://github.com/fmontesi/jo-demo-chuck.
Get started
Create the following directory structure.
app/
internal/
web/
We will use app
for our services, and web
for our web frontend files.
Install the web server
Go to app/internal
and download the Leonardo web server in the subdirectory leonardo
:
curl -L https://github.com/jolie/leonardo/archive/0.2.tar.gz | tar xvz --transform 's/leonardo-0.2/leonardo/'
We will use Leonardo to serve static content and to route service invocations from webpages to our services.
Write the main application
Now we write our main Jolie application, in file app/main.ol
.
// This includes the LeonardoAdmin output port, which we'll use to configure Leonardo
include "internal/leonardo/ports/LeonardoAdmin.iol"
// Embedding is Jolie for running a service inside of the same VM
embedded {
Jolie:
/*
Run Leonardo and link it to the output port LeonardoAdmin
Communications over embedding use local memory (they're fast)
The Standalone=false parameter means that we'll have to configure Leonardo
*/
"-C Standalone=false internal/leonardo/cmd/leonardo/main.ol" in LeonardoAdmin
}
main
{
with( config ) {
.wwwDir = "../web" // The web content directory
};
config@LeonardoAdmin( config )(); // Send the configuration to Leonardo
linkIn( Shutdown ) // Wait until somebody terminates us
}
Make a run script
First, pull Jolie.
docker pull jolielang/jolie
Now we make a convenience script to run our website. Write this in run.sh
in the root directory of your project.
#!/bin/sh
docker run -it --rm \
-v "$(pwd)"/web:/web \
-v "$(pwd)"/app:/app \
-w /app \
-p 8080:8080 \
jolielang/jolie \
jolie main.ol
The script mounts the app
and web
directories as volumes, goes in app
, and runs our program in main.ol
. We expose the TCP port 8080.
Make the script executable:
chmod +x run.sh
Whenever I say “run the application” in the remainder, I mean: go to the root directory of the project and run ./run.sh
(sh run.sh
also works, if you did not make run.sh
executable).
Test if your setup works
Create a file web/index.html
with this content:
<html><body>Hello, World!</body></html>
Run the application (reminder: this means executing run.sh
).
You should see the output:
Leonardo started
Location: socket://localhost:8080/
Web directory: /web/
Go to http://localhost:8080/ with your web browser. You should see the “Hello, World!” message we wrote in web/index.html
.
Adding services
Let’s add some dynamic fun. We can use Leonardo to proxy requests to both external and internal services. External services are services run outside of our application. Internal services are provided by our application. Other than that, their configuration in Leonardo is the same.
External services
By “dynamic fun”, I meant Chuck Norris jokes. There’s an API for that: https://api.chucknorris.io/. (If you didn’t know about this, reading this tutorial might be worth it just for getting that link now…) For example, to search for a joke containing the word “computer”, we just invoke https://api.chucknorris.io/jokes/search?query=computer.
To get Chuck Norris in our application, we need to update app/main.ol
as follows.
I comment the new lines (the rest is as before).
include "internal/leonardo/ports/LeonardoAdmin.iol"
// Output port to contact api.chucknorris.io
outputPort Chuck {
Location: "socket://api.chucknorris.io:443/" // Use TCP/IP, port 443
Protocol: https {
.osc.search.method = "get"; // HTTP method for operation search
.osc.search.alias = "jokes/search?query=%{query}" // URI template for operation search
}
RequestResponse: search // Request-response operation declaration (search)
}
embedded {
Jolie:
"-C Standalone=false internal/leonardo/cmd/leonardo/main.ol" in LeonardoAdmin
}
main
{
with( config ) {
.wwwDir = "../web";
// Expose a service to the web application
with( .redirection[0] ) {
.name = "ChuckNorris"; // Name of the exposed service
.binding -> Chuck // Binding information (from output port Chuck)
}
};
config@LeonardoAdmin( config )();
linkIn( Shutdown )
}
Our web server now exposes a ChuckNorris service that supports operation search
.
Next, we update the web/index.html
page to invoke this service.
We use Jo (a library to interact with Jolie web servers) to call the web server,
and jQuery to interact with the DOM (but you can use any other framework you like).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script
src="https://raw.githubusercontent.com/fmontesi/jo/master/lib/jo.js"></script>
<script
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
crossorigin="anonymous"></script>
</head>
<body>
<p>
<input type="text" id="jokeQuery" value="Computer" />
<button id="getJokeBtn" type="button">Get me a Chuck joke!</button>
</p>
<p id="display"></p>
<script>
$(document).ready( () => {
$("#getJokeBtn").click( () => {
$("#display").html( "Work in progress..." );
// Call operation search on the exposed service ChuckNorris
Jo("ChuckNorris").search( { query: $("#jokeQuery").val() } )
.then( response => {
// Pick a random joke
// api.chucknorris.io returns jokes in a "result" array subelement
$("#display").html(
( Array.isArray( response.result ) )
? response.result[ Math.floor(Math.random()*response.result.length) ].value
: "This is not a joke"
);
} )
.catch( JoHelp.parseError ).catch( alert );
} );
} );
</script>
</body>
</html>
Clicking the button “Get me a Chuck joke!” will fetch a Chuck Norris joke from https://api.chucknorris.io/ for us. I got this joke:
Internal services
What good is a joke if you can’t share it?
We write a Jolie service to post our jokes to https://telegra.ph/.
I’m going to put all the code in our app/main.ol
. (In a real project, you’d probably want to start creating separate files at this point and embed them, just like we did with Leonardo.)
Here’s the new app/main.ol
.
include "internal/leonardo/ports/LeonardoAdmin.iol"
outputPort Chuck {
Location: "socket://api.chucknorris.io:443/"
Protocol: https {
.osc.search.method = "get";
.osc.search.alias = "jokes/search?query=%{query}"
}
RequestResponse: search
}
embedded {
Jolie:
"-C Standalone=false internal/leonardo/cmd/leonardo/main.ol" in LeonardoAdmin
}
include "string_utils.iol"
// Output port to Telegraph. We're going to need it to implement our internal service.
outputPort Telegraph {
Location: "socket://api.telegra.ph:443/"
Protocol: https {
.format = "json";
// Yeah, I'm using querystrings... that's the official example in the Telegraph API documentation
.osc.createAccount.alias =
"createAccount?short_name=%{short_name}";
.osc.createPage.alias =
"createPage?access_token=%{access_token}&title=%{title}&content=[\"%{content}\"]"
}
RequestResponse: createAccount, createPage
}
type CreatePageRequest:void {
.title:string
.content:string
}
interface TelegraphPosterIface {
RequestResponse: createPage(CreatePageRequest)(string) throws TelegraphError(string)
}
// Here's the internal service that we're going to expose to the webpage
service TelegraphPoster {
Interfaces: TelegraphPosterIface
main {
createPage( postRequest )( postUrl ) {
// Create a fresh account on Telegraph
short_name = new;
replaceAll@StringUtils( short_name { .regex = "-", .replacement = "" } )( short_name );
createAccount@Telegraph( { .short_name = short_name } )( response );
if ( !response.ok ) throw( TelegraphError, response.error );
// Post the joke with the account we've just created
access_token = response.result.access_token;
createPage@Telegraph( {
.access_token = access_token,
.title = postRequest.title,
.content = postRequest.content
} )( response );
if ( !response.ok ) throw( TelegraphError, response.error );
postUrl = response.result.url // Return the post's url to the webpage
}
}
}
main
{
with( config ) {
.wwwDir = "../web";
with( .redirection[0] ) {
.name = "ChuckNorris";
.binding -> Chuck
};
with( .redirection[1] ) { // We add the redirection for TelegraphPoster
.name = "TelegraphPoster";
.binding.location = TelegraphPoster.location
}
};
config@LeonardoAdmin( config )();
linkIn( Shutdown )
}
So now we have a new service TelegraphPoster that the webpage can invoke to post content on Telegraph. Let’s use it to share our jokes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script
src="https://raw.githubusercontent.com/fmontesi/jo/master/lib/jo.js"></script>
<script
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
crossorigin="anonymous"></script>
</head>
<body>
<p>
<input type="text" id="jokeQuery" value="Computer" />
<button id="getJokeBtn" type="button">Get me a Chuck joke!</button>
</p>
<p id="display"></p>
<p>
<button id="postJokeBtn" type="button">Share this wisdom</button>
</p>
<p id="telegraphDisplay"></p>
<script>
$(document).ready( () => {
$("#getJokeBtn").click( () => {
$("#display").html( "Work in progress..." );
Jo("ChuckNorris").search( { query: $("#jokeQuery").val() } )
.then( response => {
// Pick a random joke
$("#display").html(
( Array.isArray( response.result ) )
? response.result[ Math.floor(Math.random()*response.result.length) ].value
: "This is not a joke"
);
} ).catch( JoHelp.parseError ).catch( alert );
} );
// Should be self-explanatory by now
// When the button is clicked, invoke createPage at TelegraphPoster
$("#postJokeBtn").click( () => {
$("#telegraphDisplay").html( "Work in progress..." );
Jo("TelegraphPoster").createPage( {
title: "Chuck Norris Wisdom",
content: $("#display").html()
} ).then( response => {
$("#telegraphDisplay").html(
`Wisdom shared at <a href=\"${response.$}\" target=\"_new\">${response.$}</a>`
);
} ).catch( JoHelp.parseError ).catch( alert );
} );
} );
</script>
</body>
</html>
Here’s a screenshot of what the final page looks like.
Done!
It’s done!
If you’d like to containerise your application, you just need a simple Dockerfile, like the following.
FROM jolielang/jolie
COPY app /app
COPY web /web
WORKDIR /app
EXPOSE 8080
CMD ["jolie","main.ol"]
Some cloud platforms like Heroku do not enforce the EXPOSE
directive. They require a fixed HTTP endpoint port number, so change the last line into the following:
CMD jolie -C Location_Leonardo=\"socket://localhost:$PORT/\" main.ol
References
- Fabrizio Montesi. (2016). Process-aware web programming with Jolie. Sci. Comput. Program., 130, 69–96. doi arxiv
- Fabrizio Montesi, Claudio Guidi, & Gianluigi Zavattaro. (2014). Service-Oriented Programming with Jolie. In A. Bouguettaya, Q. Z. Sheng, & F. Daniel (Eds.), Web Services Foundations (pp. 81–107). Springer. doi pdf