Erlang, Cowboy, Rebar3によるHello World HTTPサーバーのチュートリアル
本記事では、Erlang, Cowboy, Rebar3によるHello worldを出力するHTTPサーバーの実装方法を解説する。
目的は、このプログラムを実行中に、以下のような挙動になることだ。
$ curl "htttp://localhost:8080/"
hello,world
ErlangもCowboyもRebar3も、情報が極めて少ない。しかも公式ドキュメントすら間違っている。公式ドキュメントすら間違っているのには理由があり、実際の実装とドキュメントに差がでているのだ。Erlangは変化の少ない言語ではあるが、それでもOTP17からmapが追加されたりと言語的に少し変わっているし、mapの追加により、cowboyも以前ならproplistsを使っていた部分でmapを使うようになり、しかも2.0でAPIかなり破壊的な変更が入り、2.5からはモジュールの構成も少し変わってしまった。Rebar3は本当にひどい。名前が示すように、すでに破壊的な変更を2回経ている。そして技術的負債の塊だ。
この記事で解説する程度の知識を得るのに私は公式ドキュメントと何ヶ月も格闘するはめになった。公式ドキュメントですらこうなのだから、非公式なドキュメントは本当に参考にならない。ここに書いてあることは2020年の時点では正しいが、来年はわからない。
準備
Erlang実装とRebar3が必要だ。CowboyについてはRebar3がダウンロードしてくれるので考えなくてよい。
Debian系のディストロでErlangをインストールする。
$ apt install erlang
GNU/Linuxでrebar3をインストールする最も信頼できる方法は、自前でビルドすることだろう。
$ git clone https://github.com/erlang/rebar3.git
$ cd rebar3
$ ./bootstrap
これで"rebar3"というファイルができるので、このファイルをPATHの通ったディレクトリにコピーするかシンボリックリンクをはればよい。
ln -s rebar3 /usr/local/bin
これで準備が完了した。
プロジェクト作成
まずrebar3を使ってプロジェクトを作成する。名前を"hello_server"としよう。
$ rebar3 new release hello_server
$ cd hello_server
このコマンドで"hello_server"というディレクトリが作成される。その中にはテンプレート生成されたファイルがいくつかある。重要なものだけ説明する。
"hello_server/rebar.config"は設定ファイルで、cowboyの依存を追加するために編集する。
"hello_server/apps"ディレクトリにはアプリケーションが配置される。rebar3のreleaseプロジェクトはumbrella projectと呼ばれていて、複数のアプリケーションを持つことができる。
"hello_server/apps/hello_server"はrebar3がテンプレートから生成したアプリケーションだ。このディレクトリ内には"src"ディレクトリがあり、3つのファイルが作成されている。"hello_server_app.erl", "hello_server_sup.erl", "hello_server.app.src"だ。
"hello_server_app.erl"はapplication behaviourを実装するソースファイルだ。
"hello_server_sup.erl"はsupervisor behaviourを実装する。今回は編集しない。
"hello_server.app.src"はapplication resource fileを生成するためのソースファイルだ。Erlang VMをどのように実行するかということを設定するためのファイルだ。rebar3はこのファイルから実際のapplication resource fileを生成する。このファイルも編集する。
Cowboyを依存に追加
Cowboyを依存に追加してrebar3にダウンロードしてもらう。そのために"hello_server/rebar.config"を編集する。
$ vim rebar.config
最初の数行は以下のようになっている。
[erl_opts, [debug_info]}.
{deps, []}.
[relx, [{release, {hello_server, "0.1.0"},
...
今回編集するのは2行目、つまり"{deps,[]}."という部分だ。このlistのなかに依存を記述していく。記述のフォーマットは様々だが、すべて"{ package_name, ...}"という形のtupleになっている。このチュートリアルではパッケージをhex.pmからダウンロードしてくるので、フォーマットは"{ package_name, "version number"}"になる。本チュートリアルを執筆時点で、最新の安定版のcowboyのバージョンは2.7.0だ。
{deps, [
{cowboy, "2.7.0"}
]}.
rebar3は依存ライブラリが必要になった時に自動的にダウンロードするが、今回は正しく記述できていることを確認するために明示的にダウンロードしてみよう。
$ rebar3 upgrade
次に、アプリケーションリソースファイルを編集して、cowboyを先にスタートさせるようにする。今実装しているアプリケーションはcowboyを使っているので、cowboyを先にスタートさせておかなければならない。その設定方法として、"hello_server.app.src"を編集する。
$ vim apps/hello_server/src/hello_server.app.src
このファイルの中身を抜粋すると以下のようになっている。
{application, hello_server,
[...
{applications
[kernel,
stdlib
]},
...
]}.
applicationsのtagged tupleの中のlistに"cowboy"を追加する。
{application, hello_server,
[...
{applications
[kernel,
stdlib, % カンマを忘れないこと
cowboy % この行を追加
]},
...
]}.
これはErlangのlistなのでカンマを忘れないようにすること。
HTTPサーバーの始動
容易が全て整ったので、HTTPサーバーを開始する。まず"apps/hello_server/src/hello_server_app.erl"を編集する。
vim apps/hello_server/src/hello_server_app.erl
このソースコードはrebar3によって生成されたapplication behaviourを実装するためのモジュールだ。start/2を変更して、HTTPサーバーを開始する。
start(_StartType, _StartArgs) ->
hello_server_sup:start_link().
HTTPサーバーを開始してコネクションをlistenするには、まずcowboy用語でルートと呼ばれているものを設定する。これは特定のリモートホストやパスをcowboy_handlerに関連付けるための設定だ。ルートを設定するにはcowboy_router:compile/1を使う。この関数は引数としてcowoy_router:routes()型を取る。型は"[{Host, Pathlist}]"となっている。PathList型を展開すると、"[{Host, [{Path, Handler, InitialState}]}"となる。
start(_StartType, _StartArgs) ->
Dispatch = cowboy_router:compile([
{ Host, [{Path, Handler, InitialState}]}
]),
hello_server_sup:start_link().
ホストとして'_'を指定すると、任意のホストからのコネクションを受けつける。ホストを制限したい場合、例えばlocalhostからの接続しか受け付けたくない場合は、<<"localhost">>を指定する。
今回の場合、Pathは<<"/">>だ。今回は"http://localhost/aa/bb/cc"のようなPathは受け付けないのでこれでいい。
Handlerにはhello_handlerというatomを指定する。これは後でcowboy_handler behaviourを実装するモジュールとして実装する。
特に状態は持たないので、InitialStateは空のlistを使う。
すべてまとめると、以下のようなコードになる。
start(_StartType, _StartArgs) ->
Dispatch = cowboy_router:compile([
{ <<"localhost">>, [{<<"/">>, hello_handler, [] }]
]),
hello_server_sup:start_link().
ルートが準備できたので、HTTPリスナーを開始する。ここでは素のHTTPを使うので、cowboy:start_cear/3を使う。引数はstart_claer( Name, TransportOpts, ProtocolOpts )だ。
NameはこのHTTPリスナーを識別するための名前で、ErlangのTermであればなんでもよい。通常はatomが使われる。ここでは"hello_listener"を使う。
TransportOptsには様々なオプションがあるが、このチュートリアルではlistenするポートを指定するだけだ。今回はHTTPサーバーのポートはは通常80だが、今回は8080を使うので、"{{port, 8080}}"となる。
ProtocolOptsでは先程設定したrouteを指定する。ProtocolOptsの型はmapで、envというキーがあり、値はdispatch型だ。ここに先程束縛したDispatch変数を指定する。
成功した場合、start_clear/2は{ok, pid}を返す。okのtagged tupleに束縛することで失敗時のことはapplication behaviourにまかせよう。
start(_StartType, _StartArgs) ->
Dispatch = cowboy_router:compile([
{ <<"localhost">>, [{<<"/">>, hello_handler, []}] }
]),
{ok, _} = cowboy:start_clear(
ello_listener,
[{port, 8080}],
#{env => #{dispatch => Dispatch}}
),
hello_server_sup:start_link().
接続の処理
HTTP listenerの用意が出来たので、やってきた接続要求を処理していく。そのためにはさきほどのhello_handlerを実装しなければならない。これはcowboy_handler behaviourとして実装する。まず新しいソースファイルを作成する。
$ vim apps/hello_server/src/hello_handler.erl
まず基本的なところを埋めていこう。
-module(hello_handler).
-behaviour(cowboy_handler).
-export([init/2]).
init( Req, State ) ->
{ok, Req, State}.
Reqはリクエストとレスポンスを保持している。Stateは好きに使える状態だが今回は使わないので空のlistとする。
やるべきことは、HTTPステータスコードとして200を返し、ヘッダーのcontent-typeとしてはtext/plainを指定し、コンテンツは"hello,world"とするだけだ。これにはcowboy_req:reply/4を使う。引数の型は"reply(Status, Headers, Body, Req)"だ。
StatusはHTTPステータスコードで型はnon_reg_integer()もしくはbinary()だ。今回は200を指定する。
HeaderはHTTP headerをmap()で指定する。今回は"content-type"を"text/plain"にする。
Bodyには<<"hello,world">>を指定する。
Reqは現在のReqオブジェクトだ。
replyは新しいReqオブジェクトを返す。これ以降Reqオブジェクトを使う際には、この新しいReqオブジェクトを使わなければならない。
init/2のコードは以下のようになる。
init( Req, State ) ->
Req_1 = cowboy_req:reply(
200,
#{<<"content-type">> => <<"text/plain">>},
<<"hello,world">>,
Req
),
{ok, Req, State}.
プログラムの実行
$ rebar shell
確認しよう。
$ curl "http://localhost:8080/"
hello,world
次はErlangのエラー処理について書こうと思う。Erlangのエラー処理はその場で処理を中断して無視していい場合は簡単だが、エラーに明示的な対処が必要だととたんに面倒になる。