Sync’ up! … without getting drained

dec 8

Berkeley smorgasbord

OpenBSD, as of ‘6.3,’ has some pretty mature solutions for serving up the web, all right there in the base system. And as it happens, with OpenBSD’s ‘httpd’ & ‘relayd,’ in combination with Erlang/OTP and a few projects like ‘elli’ & ‘erlydtl,’ the table is set for having most the things the modern web-app requires.

We thought it would be fun to put together all the above to showcase how one can serve an Erlang-based web-app while using some of the newer web serving solutions from the OpenBSD world.

To make this post as curt as possible, we will assume that a user, ‘joe,’ has doas privileges on the server, that port eighty is open to the public, and that a version of Erlang/OTP has been installed on said server (more on that last point can be read, here). Also, we assume that the DNS is all in order as we won’t be going into those details.

Base configuration

We will be configuring the ‘httpd’ and ‘relayd’ daemons, and to ensure they start automatically when the server boots, we add the following to ‘/etc/rc.conf.local’ :

httpd_flags=
relayd_flags=

‘Relayd’ will intercept all traffic heading towards our fictional domain, ‘foobar.com,’ and relay requests to our Erlang web-app. The web-app will be listening on port ‘8000,’ and forward all other requests to the ‘httpd’ service, listening on ports ‘8001’ & ‘8080.’

In ‘/etc/relayd.conf,’ the configuration looks like the following:

relayd_addr="0.0.0.0"
relayd_port="80"

app_port="8000"
table <app_hosts> { 127.0.0.1 }

cdn_port="8001"
table <cdn_hosts> { 127.0.0.1 }

web_port="8080"
table <web_hosts> { 127.0.0.1 }

log all

http protocol "httpfilter" {
  match header append "X-Forwarded-For" \
    value "$REMOTE_ADDR"
  match header append "X-Forwarded-By" \
    value "$REMOTE_ADDR:$SERVER_PORT"
  match header set "Keep-Alive" value "$TIMEOUT"

  ### TCP performance options
  tcp { 
    nodelay, sack, socket buffer 65536, backlog 100 }

  ### Return HTTP/HTML error pages
  return error

  match request quick header "Host" value "cdn.foobar.com" \
    forward to <cdn_hosts>
  match request quick header "Host" value "foobar.com" \
    forward to <app_hosts>
}

relay "proxy" {
  listen on $relayd_addr port $relayd_port
  protocol "httpfilter"

  # N.B. the order as such

  # main server table
  forward to <web_hosts> port $web_port check http "/" code 200

  # additional server tables used by custom rules
  forward to <cdn_hosts> port $cdn_port check tcp interval 60
  forward to <app_hosts> port $app_port
}

You’ll notice that we are making some room for other domains that we might be using, putting all those on port ‘8080.’ But to be clear, for our web-app, we will be using ‘8000’ for ‘elli,’ and port ‘8001’ for hosting a quasi CDN.

As ‘joe,’ we can test our new ‘relayd’ configuration in the following manner:

doas relayd -n

If all is okay, then we can shift our focus to the related HTTP service, ‘httpd.’

The ‘httpd’ service is a base package and one that is poised to replace Nginx. Not to be confused with Apache’s ‘httpd,’ OpenBSD ‘httpd’ is a lean but capable service for serving up websites of many types.

Here, we are using ‘httpd’ to serve our web-app’s static files. Included, as well, is a bit of code that is typical for serving up HTML files (here, in the ‘barbaz.com’ stanzas).

Similar to ‘relayd.conf,’ here is our ‘/etc/httpd.conf’ settings:

int_ip="127.0.0.1"

server "cdn.foobar.com" {
  listen on $int_ip port 8001
  location "/assets/*" { 
root "/htdocs/foobar/priv/assets" root strip 1
} } server "www.foobar.com" { listen on $int_ip port 8080 block return 301 "http://foobar.com$REQUEST_URI" } server "barbaz.com" { listen on $int_ip port 8080 location "/robots.txt" { root "/htdocs/local" } root "/htdocs/barbaz.com" } server "www.barbaz.com" { listen on $int_ip port 8080 block return 301 "http://barbaz.com$REQUEST_URI" }

To explain a few of the mechanisms, ‘relayd’ is forwarding only on ports ‘8001’ & ‘8080.’ Nothing on port eighty ever reaches ‘httpd.’ We are serving our web-app’s static assets on the subdomain ‘cdn.foobar.com,’ and for completeness, are showcasing serving a secondary static website (barbaz.com) in addition to performing redirects to naked domains when ‘www’ suffices have been requested. (Just a small gathering of typical web things, actually.)

This should give anyone a taste on how nice and simple OpenBSD ‘httpd’ configuring is, especially anyone coming straight from the Apache server world.

We can test this configuration — as ‘joe,’ again — as follows:

doas httpd -n

If all went okay, then we can put together our web-app.

OTP + elli + erlydtl

The rebar3 tool is a mainstay for OTP developers at this point, however, at Syncpup, we still rely on ‘Makefiles.’ We’d like to avoid pushing our process on anyone, so we will assume you have your own methods for creating an OTP application called ‘foobar’ with the ‘deps’ applications ‘elli’ & ‘erlydtl’ at the ready.

Here’s what the directory structure looks like for our particular application (via ls -R) :

.:
deps  foobar.config  priv  src

./deps:
elli  erlydtl

./priv:
assets  templates

./priv/assets:
stylesheets

./priv/assets/stylesheets:
screen.css

./priv/templates:
index.dtl  user_dashboard.dtl

./src:
foobar_app.erl  foobar.app.src  foobar_elli.erl
foobar_lib.erl  foobar_sup.erl

And what follows are the few files that just hold typical OTP boilerplate code:

‘src/foobar.src.app’

{application, foobar, [
  {description, "The little elli+bsd project"},
  {vsn, "1986.a"},
  {registered, [foobar_sup]},
  {modules, [
    foobar_user_dashboard_dtl,
    foobar_index_dtl, foobar_sup, 
    foobar_app, foobar_lib, foobar_elli]},
  {applications, [
    kernel,
    stdlib,
    sasl,
    erlydtl,
    elli]},
  {mod, {foobar_app, []}},
  {env, []}
]}.

‘src/foobar_app.erl’

-module(foobar_app).
-behavior(application).

%% callbacks
-export([start/2, stop/1]).

%%
%% behavior callback routines
%%

start(_Type, _Args) ->
    foobar_sup:start_link().

stop(_State) ->
    ok.

Now, what follows is the supervisor to the ‘elli’ callback module. It shouldn’t look too foreign for anyone who has spent time with OTP. Unlike the ‘cowboy’ project, the routing isn’t handled in the supervisor, but rather, is pattern-matched in the callback module itself. This makes for one clean looking supervisor module. The contents of our supervisor code is as follows:

‘src/foobar_sup.erl’

-module(foobar_sup).
-behavior(supervisor).

-define(REGISTERED, ?MODULE).

%% api
-export([start_link/0]).

%% callbacks
-export([init/1]).

%%
%% api routines
%%

start_link() ->
    supervisor:start_link(
      {local, ?REGISTERED}, ?MODULE, []).

%%
%% behavior callback routines
%%

init([]) -> 
    Spec = spec(),
    {ok, {{one_for_one, 5, 10}, [Spec]}}.

%%
%% support routines
%%

spec() ->
    Opts = opts(),
    {foobar_elli_tag, {elli, start_link, [Opts]},
      permanent, 5000, worker, [elli]}.

opts() -> 
    [{callback, foobar_elli}, {port, 8000}].

If you like how prim & proper the supervisor looks, then you’ll probably find the meat & potatoes of our application, the ‘elli’ callback module, quite yummy, as well.

Pattern matching URLs

In order to flesh out our HTTP service beyond an overly simplistic ‘Hello World’ application, we will be serving a quasi-static file (‘robots.txt’) in addition to the main index page and an additional dynamic ‘dashboard’ page (the later two which will be rendered via ‘erlydtl’).

The following is our ‘src/foobar_elli.erl’ callback module:

-module(foobar_elli).
-behavior(elli_handler).

-include_lib("elli/include/elli.hrl").

%% callback routines
-export([handle/2, handle_event/3]).

%%
%% callback routines
%%

handle(#req{method=X}=Req, _Args) ->
    Path = elli_request:path(Req),
    handle_request(X, Path, Req).

handle_event(_Event, _Data, _Args) -> ok.

%%
%% business routines
%%

handle_request('GET', [<<"robots.txt">>], _Req) ->
    Ys = headers(plaintext),
    X = <<"User-agent: *\n">>,
    {ok, Ys, X};
handle_request('GET', [
  <<"user">>, <<"dashboard">>], _Req) ->
    Ys = headers(),
    {content, X} = foobar_lib:dtl(
      user_dashboard, [{name, <<"Foobar">>}]),
    {ok, Ys, X};
handle_request('GET', [<<"index.html">>], Req) ->
    handle_request('GET', [], Req);
handle_request('GET', [], _Req) ->
    Ys = headers(),
    {content, X} = foobar_lib:dtl(index),
    {ok, Ys, X};
handle_request(_, _, _Req) ->
    {404, [], <<"Not Found">>}.

%%
%% support routines
%%

headers() ->
    [{<<"content-type">>, <<"text/html; charset=utf-8">>}].

headers(plaintext) ->
    [{<<"content-type">>, <<"text/plain; charset=utf-8">>}].

Anyone who’s spent any time with Erlang should find this code to be unsurprising, and quite beautiful. The ‘elli’ team has done a great job at keeping chaos out of their API.

With that, let’s switch gears to some HTML & CSS and swing back for a few more Erlang details shortly thereafter.

Front-end goodies

Let’s begin with laying out the structure of our application’s ‘priv’ directory. The following is what’s in there (using ls -R ./priv) :

priv:
assets  templates

priv/assets:
stylesheets

priv/assets/stylesheets:
screen.css

priv/templates:
index.dtl  user_dashboard.dtl

Nothing too incredible, but a note that we are calling our HTML files ‘.dtl’ files, as they will have some properties that sets them apart from static files.

Our ‘priv/assets/stylesheets/screen.css’ looks as follows:

html, body{
  color: #322;}

And, respectively, our ‘priv/templates/index.dtl’ and ‘priv/templates/user_dashboard.dtl’ files are as such:

<!doctype html>
<html lang='en'>
  <head>
    <meta charset='utf-8'>
    <meta http-equiv='content-type' content='text/html; charset=UTF-8' />
    <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
    <link href='//cdn.foobar.com/assets/stylesheets/screen.css' rel='stylesheet' type='text/css'>
    <title>Index</title>
  </head>
  <body>
    <nav>Index</nav>
  </body>
<html>

<!doctype html>
<html lang='en'>
  <head>
    <meta charset='utf-8'>
    <meta http-equiv='content-type' content='text/html; charset=UTF-8' />
    <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
    <link href='//cdn.foobar.com/assets/stylesheets/screen.css' rel='stylesheet' type='text/css'>
    <title>User dashboard</title>
  </head>
  <body>
    <nav>User dashboard</nav>
    <p>Welcome back, {{ name }}</p>
  </body>
<html>

Of course, one could DRY this up quite a bit by using Django’s ‘includes’ tag, but that’s trying to boil the ocean for what we’re striving to showcase via this tutorial.

You may have noticed that the ‘user_dashboard.dtl’ template is ready to receive dynamic data. If everything goes as planned, ‘{{ name }}’ will be dynamically changed to ‘Foobar’ at runtime. It’s all there in ‘src/foobar_elli.erl.’

A helper lib

There’s some Erlang that has yet to be completed. We’re relying on ‘erlydtl,’ but want to abstract away some of the details. This is a perfect opportunity to write some helper code that can make our main callback module lean & mean. Here’s the code for ‘src/foobar_lib.erl’ :

-module(foobar_lib).

%% api
-export([dtl/0, dtl/1, dtl/2]).

-define(DTLS, [
  {"index.dtl", foobar_index_dtl},
  {"user_dashboard.dtl", foobar_user_dashboard_dtl}]).

%%
%% api
%%

dtl() ->
    Fn = dtl_fun(),
    Opts = dtl_options(),
    [ erlydtl:compile_file(Fn(File), Mod, Opts) ||
      {File, Mod} <- ?DTLS ].

dtl(What) -> dtl(What, []).

dtl(What, Opts) ->
    Fn = dtl1(What),
    {ok, B} = Fn(Opts),
    {content, B}.

%%
%% support routines
%%

dtl1(index) -> fun foobar_index_dtl:render/1;
dtl1(user_dashboard) -> 
    fun foobar_user_dashboard_dtl:render/1.

dtl_fun() ->
    Priv = code:priv_dir(foobar),
    fun(X) ->
      filename:join([Priv, "templates", X])
    end.

dtl_options() ->
    As  = [verbose, report],
    Xs  = application:get_all_env(erlydtl),
    X1s = proplists:delete(included_applications, Xs),
    [ Y || X <- [As, X1s], Y <- X ]. 

And with that, it’s time to sew things up and toggle some OpenBSD switches.

Basic target system

It’s highly recommended that any Erlang code you put together gets the royal treatment and gets turned into a proper OTP release. However, for ‘toy apps,’ there’s no harm in just cobbling it all into a basic target system, just to get something up and running.

In our project’s root, a little configuration file can save us a few headaches, so not everything is hard-coded in our modules. Here’s what’s in ‘foobar.config’ :

[{elli, [
    {callback, foobar_elli}, 
    {port, 8000}]},
  {erlydtl, [
    {out_dir, "ebin"}]}].

In a full-fledged OTP release, these sort of details would go into one’s ‘sys.config’ file, but that’s neither here nor there.

At this point, one could put together a ‘Makefile’ to start & stop our application, but just starting it à la command-line is good enough for the sake of this tutorial. The following will compile our Erlang by hand; neglecting the automation rebar3 or make typically offers us :

# create some important directories
mkdir ebin
# copy over the application resource file
cp src/foobar.app.src ebin/foobar.app
# compile our Erlang code 
ERL_LIBS=deps erlc -o ebin src/*.erl
# compile our templates into `beam' byte code
ERL_LIBS=deps erl -noshell \
-config foobar -pz ../foobar/ebin \
-eval "application:ensure_all_started(foobar)." \
-s foobar_lib dtl \
-s init stop

N.B. if using csh, all lines starting with ‘ERL_LIBS’ need to be prepended with ‘env’ — or put the whole thing in a subshell, like as follows: ( setenv ERL_LIBS deps; <command> )

The above should give us what we need; although, we haven’t started anything yet, we’ve only just compiled everything. Since we will be daemonizing our basic target system, we can just get it all humming away on port ‘8000’ now and then serve it up by integrating with ‘relayd’ & ‘httpd’ soon enough.

The following will get our application running and have our ‘elli’ server listening for HTTP requests on port ‘8000’ :

ERL_LIBS=deps erl -pz ../foobar/ebin \
-detached -config foobar \
-eval "application:ensure_all_started(foobar)."

There won’t be much fanfare from the Erlang side, so to verify it’s running, use ps uaxww or top to ensure everything went swimmingly.

Toggle on

Now that our Erlang service is running, we can start the ‘httpd’ and ‘relayd’ daemons. There are a few ways to perform this task, but the one that’s currently in vogue is via OpenBSD’s rcctl tool. As ‘joe,’ we can accomplish this as follows:

doas rcctl start httpd
doas rcctl start relayd

Whenever we need to change anything in the configuration, we can use the ‘reload’ verb with the above. This ensures nothing is taken out of service when upgrading. Of course, you’ll want to run health checks on the configuration files via doas httpd -n and doas relayd -n, first and foremost.

(On a related note, when tinkering with this and that configuration, starting either service, say ‘httpd,’ in debug mode is enormously revealing. For example: doas httpd -dvvv.)

Trying it out

So long as firewalls have been dealt with properly (only port eighty needs to be open to the world) then a curl …, wget -qO- … or browser pointed to the newly created web-app will yield some nice results.

You’ll notice our quasi CDN is working well with our configuration, serving up our only static asset by way of a stylesheet. And best of all, ‘erlydtl’ is serving up dynamic content. The ‘/user/dashboard’ path will give us the greeting we’re expecting.

With these few tools, there’s a litany of potential for web enthusiast to unleash. And although OpenBSD is quite old, it will be interesting to see what size bites it’ll take out of the web server software space in the years to come.