PerlのWebアプリケーションフレームワーク(Web Application Framework, 以降「WAF」と省略)の一種「Amon2」の使い方について紹介します. このチュートリアルは二部構成になっています. 第1部となるこの記事では, Amon2のインストールや初期設定, そして基本的な機能の解説を行います. 次の第2部では, 第1部で解説したAmon2の基本的な機能を使って, 簡単なスケジュール管理サービスを作成しながら, Amon2を使ったWebアプリケーションの開発の流れを体験してみます.
Amon2は, @tokuhiromさんを中心に開発されている, Perl製のWAFです. 様々な言語の様々なWAFの中で, Amon2がどのようなポジションにいるのかを見てみる為に, Amon2を含むPerlの代表的なWAFを中心に, その「重さ(機能の充実性)」で分類し, 表にしてみました. (ただしこの表は, だいぶ @papix の主観がはいっています...)
クラス | 具体的なWAF | 概要 |
---|---|---|
重量級 | Ruby on Rails (Ruby) | 単体で完結できる, フルスタックなWAF |
中量級 | Mojolicious(Perl), Catalyst (Perl), FuelPHP (PHP) | RoR程ではないが高機能なWAF |
軽量級 | Amon2 (Perl) | WAFとして非常にシンプル, その分拡張性に富む |
超軽量級 | Sinatra (Ruby), Amon2::Lite (Perl) | ページ数が少ない(2〜3)アプリケーション向け |
上の表にもあるように, Amon2は, WAFの中ではかなり軽量な分類に入ります. Ruby on Railsのような重量級, 或いはPerlのMojoliciousやPHPのFuelPHPのような中量級のWAFに比べて, Amon2そのものが備えている機能は少ないです. シンプルではありますが, WAFとして必要最低限の機能は備えていますし, 軽量ゆえに拡張性があり, プロダクトに最適なWAFを構築することが出来ます. そういう意味で, Amon2は「WAFのフレームワーク」と言うことが出来るかもしれません.
このチュートリアルでは, Amon2の基本的な機能を利用しながら, 簡単なスケジュール管理システムを作っていきます. 時間の制約上, 「WAF(Amon2)を拡張していく」という部分については割愛し, 別の機会に紹介したいと思います.
チュートリアルに利用するAmon2は2015年3月10日時点の最新版であるAmon2 6.11を利用します. Amon2は基本的には後方互換性を維持しながら開発が進んでいるので, 6.11以降のAmon2でも動作すると思います.
先程の表にもあったように, Amon2に比べて, Ruby on Railsは様々な機能を自前で持っているフルスタックなWAFと言えます. そのため, 「常にRuby on Railsを使えばいいのでは?」という意見もあるかと思いますが, 個人的にはWAFは適材適所で使い分けるべきだと思っています.
例えば, Ruby on Railsは高性能な代わりに, その進化は非常に早いです(進化の速さについては, Rubyという言語そのものにも当てはまるでしょう). 常に手を入れ続けるプロダクトであれば, その進化に対応しながら, 新しく追加された機能を使いつつ, 効率的に開発を進めることが出来るでしょう.
しかし社内ツールであったり, 或いはプロダクトやエンジニアチームの支援をするようなツール(社内向けプロダクト)であれば, 一旦開発をしきった後はそこまで手を加えずに, 長く使うという場合もあると思います. そういう場合は, 言語そのものやライブラリの後方互換性を意識(重視)したPerl製のWAFを採用した方が, セキュリティフィックスなどによる言語/モジュールの更新を実施しやすい... という見方も出来るでしょう.
...このように, WAF1つをとっても様々な選択肢が可能で, チームやプロダクトにとっての「適切な技術選択」というのは非常に難しいものです. この研修の中でも, きっと様々な「技術選択」の機会があると思いますので, その都度その都度, しっかり考えていきましょう.
PerlチュートリアルのPerl環境の構築内の, モジュールとCPANという部分で, Perlのモジュールをインストールするためのcpanm
というコマンドをインストールしているはずです.
$ which cpanm
/Users/username/.plenv/shims/cpanm
Amon2もCPANに公開されているので, cpanm
コマンドからインストールすることが出来ます.
$ cpanm Amon2
--> Working on Amon2
Fetching http://www.cpan.org/authors/id/T/TO/TOKUHIROM/Amon2-6.11.tar.gz ... OK
Configuring Amon2-6.11 ... OK
Building and testing Amon2-6.11 ... OK
Successfully installed Amon2-6.11
1 distribution installed
もしAmon2の依存モジュールが導入されていない場合, cpanm
は自動的にそれらのインストールも実行します.
Amon2を使ったWebアプリケーションの雛形は, amon2-setup.pl
コマンドから生成出来ます.
$ amon2-setup.pl
% amon2-setup.pl MyApp
--flavor=Basic basic flavour (default)
--flavor=Lite Amon2::Lite flavour (need to install)
--flavor=Minimum minimalistic flavour for benchmarking
--flavor=Standalone CPAN uploadable web application(EXPERIMENTAL)
--vc=Git setup the git repository (default)
--list-flavors (or -l) Shows the list of flavors installed
--help Show this help
Amon2には, Sinatraのような超軽量のAmon2::Lite
を利用する, Liteフレーバー(雛形の元)も用意されていますが, 今回はBasicフレーバーを利用します.
Amon2で中規模〜大規模なWebアプリケーションを開発する場合, Basicフレーバーで雛形を生成し, 必要に応じて改変していく... という流れが多いです.
それでは早速, 雛形を生成していきましょう.
amon2-setup.pl
を利用した雛形生成時には, このWebアプリケーションの名前を引数として与える必要があります.
今回は, スケジュール管理のアプリケーションなので, 「Scheduler」という名前にしましょうか.
$ amon2-setup.pl Scheduler
-- Running flavor: Basic --
[main] Loading asset: jQuery
[main] Loading asset: Bootstrap
[main] Loading asset: ES5Shim
[main] Loading asset: MicroTemplateJS
[main] Loading asset: StrftimeJS
... 中略 ...
create mode 100644 tmpl/include/layout.tx
create mode 100644 tmpl/include/pager.tx
create mode 100644 tmpl/index.tx
create mode 100644 xt/01_pod.t
create mode 100644 xt/02_perlcritic.t
--------------------------------------------------------------
Setup script was done! You are ready to run the skelton.
You need to install the dependencies by:
> carton install
And then, run your application server:
> carton exec perl -Ilib script/scheduler-server
--------------------------------------------------------------
カレントディレクトリにScheduler
というディテクトリが生成され, その中に雛形が生成されました.
雛形の中身を詳しく見る前に, Cartonを使って依存モジュールをインストールしておきましょう.
Cartonは, RubyのBundlerのようなもので, Webアプリケーションの依存モジュールを, cpanm
コマンドでインストールしたモジュールとは別に管理してくれるアプリケーションです.
Cartonを利用すれば, Webアプリケーションが動作する際に利用するライブラリとそのバージョンを固定することが出来るので, 開発環境と本番環境でライブラリのバージョンに差異が生じるなどといった事態を防ぐことができます.
$ cpanm Carton
まず, cpanm
コマンドでCartonをインストールします.
Amon2の時と同様, Cartonの依存モジュールも自動的にインストールしてくれます.
$ cd Scheduler
$ carton install
Cartonのインストールが終わったら, amon2-setup.pl
が生成した雛形が格納されているScheduler
ディレクトリに移動し, carton install
コマンドで依存モジュールをインストールします.
自分の環境では, 次のような出力が得られました.
$ carton install
Installing modules using /Users/username/Scheduler/cpanfile
Successfully installed CPAN-Meta-2.150001 (upgraded from 2.140640)
Successfully installed Module-Build-0.4211 (upgraded from 0.4205)
Successfully installed URI-1.67
... 中略 ...
Successfully installed Test-LongString-0.17
Successfully installed Test-WWW-Mechanize-1.44
Successfully installed Test-WWW-Mechanize-PSGI-0.35
115 distributions installed
Complete! Modules were installed into /Users/username/Scheduler/local
Cartonは, アプリケーションのルートディレクトリ(今回の場合, /Users/username/Scheduler
)の直下に, local
というディレクトリを生成し, この中に依存モジュールをインストールします.
carton install
でインストールしたモジュールを利用しながら, Perlのスクリプトを動かすには, carton exec
を利用します.
carton exec --
の後に, 例えばperl hello.pl
など, 任意のPerlスクリプトを動かすコマンドを入力すると, そのPerlスクリプトは
local`ディレクトリ以下のモジュールを利用しながら実行してくれます.
「Perlスクリプトを動かすコマンド」ですが, このSchedulerの場合, amon2-setup.pl
で雛形を生成した際にscript/scheduler-server
という起動用スクリプトが生成されているので, これを使います.
$ carton exec -- perl -Ilib script/scheduler-server
Scheduler: http://127.0.0.1:5000/
ブラウザで, localhost:5000
に繋いでみて下さい.
...このような画面が確認出来ましたか? 出来たのであれば, Amon2を使ったWebアプリケーションの開発の, 第一歩を踏み出すことが出来ました!
動作確認が出来たところで, Amon2の構成について学んでいきます. Amon2は, Webアプリケーションについて考える時によく出てくる「MVC」, すなわちModel, View, Controllerのうち, ViewとControllerを提供するWAFです. Modelに関しては, 「それぞれが使いやすい形で実装せよ」というのが, Amon2の方針になっています.
まずはControllerの部分を見て行きましょう.
SchedulerアプリケーションのControllerは, lib/Scheduler/Web/Dispatcher.pm
の部分になります.
package Scheduler::Web::Dispatcher;
use strict;
use warnings;
use utf8;
use Amon2::Web::Dispatcher::RouterBoom;
any '/' => sub {
my ($c) = @_;
my $counter = $c->session->get('counter') || 0;
$counter++;
$c->session->set('counter' => $counter);
return $c->render('index.tx', {
counter => $counter,
});
};
post '/reset_counter' => sub {
my $c = shift;
$c->session->remove('counter');
return $c->redirect('/');
};
post '/account/logout' => sub {
my ($c) = @_;
$c->session->expire();
return $c->redirect('/');
};
1;
このコードでは, Amon2::Web::Dispatcher::RouterBoom
というモジュールが提供するany
やpost
というメソッドを利用して, 「あるメソッドで」, 「あるパスに接続したら」, 「ある処理をする」という組み合わせを記述しています.
any '/' => sub {
my ($c) = @_;
my $counter = $c->session->get('counter') || 0;
$counter++;
$c->session->set('counter' => $counter);
return $c->render('index.tx', {
counter => $counter,
});
};
この部分を中心に見ていきましょう. そもそも, 「これ, 正しい記法なの?」と思うかもしれませんが, こう書き換えてみるとどうでしょう?
any ('/' => sub { ... });
any
というメソッドに, '/'
と, sub { ... }
という引数を渡している訳です.
ちなみに, sub { ... }
の部分は, サブルーチンのリファレンスになっています.
さて, 先程このコードでは「あるメソッドで」, 「あるパスに接続したら」, 「ある処理をする」という組み合わせを書いている, と書きました. これをこのコードに当てはめると...
- あるメソッド :
any
(anyは, HTTPのメソッドのうち, GETとPOSTの両方) - あるパス :
'/'
- ある処理 :
sub { ... }
となります.
なお, メソッドについては, ここで紹介したany
の他にGETメソッドを利用する時のget
, POSTメソッドを利用する時のpost
を利用することができます.
例えば...
get '/hoge/fuga' => sub { ... };
このように書いた場合, サブルーチンリファレンスに書かれた処理はGETメソッドで/hoge/fuga
にアクセスした際に実行されます. また,
post '/hoge/fuga/new' => sub { ... };
このように書いた場合, サブルーチンリファレンスに書かれた処理はPOSTメソッドで/hoge/fuga/new
にアクセスした際に実行されます.
それでは次に, サブルーチンリファレンスの中身について詳しく見て行きましょう.
sub {
my ($c) = @_;
my $counter = $c->session->get('counter') || 0;
$counter++;
$c->session->set('counter' => $counter);
return $c->render('index.tx', {
counter => $counter,
});
};
ここで重要なのは, $c
で受けている「コンテキスト」です.
コンテキストの重要性については, Amon2の作者であるtokuhiromさんもPerl Hackers Hubでの「Amon2によるWebアプリケーション開発の高速開発(2)」において, 「Amon2の世界では,コンテキストというものの存在が非常に重要です。コンテキストを理解すればAmon2の80%を理解したと言ってもよいでしょう。」と述べています.
なぜコンテキストが重要かと言うと, Amon2を利用したWebアプリケーションは, この$c
から利用できるメソッドを利用して組み立てていく必要があるからです.
ここでは, その一部をここで紹介します(なお, ここで紹介するメソッドはBasicフレーバーで雛形を生成した際に利用出来るものの一部です. Amon2を拡張した結果, 利用できなくなったメソッドや, 新しく使えるようになったメソッドがある場合もあります).
リクエストに関する情報を取得することができます. よく使うのは, フォームに入力したパラメータをWebアプリケーション側で取得する時です.
<form method="POST" action="/post">
<input type="text" name="name">
<input type="submit">
</form>
このようなHTMLがあった時, Controllerにおいて
post '/post' => sub {
my ($c) = @_;
my $name = $c->req->parameters->{name};
...
};
のように書くと, $name
の中には, テキストフォームに入力した文字列が入ります.
ちなみに, $c->req->param('name')
でも同じように取得することが出来ますが, このような書き方は脆弱性を招く可能性があるので使わないようにしましょう: Perl 初心者がウェブアプリケーションを書く時に気をつけるべきこと
レスポンスを生成します.
render
については, Viewの機能と密接に繋がる部分が多いので, 詳しくは後述します.
$path
で指定した別のページへ遷移します(リダイレクト).
$c->render()
も同様ですが, これらのメソッドは「実行したタイミングで処理される」のではなく, 「このメソッドの返り値をサブルーチンリファレンスの返り値にした場合に適用される」ものです.
そのため, 次のように書くと期待通りの動作をしてくれません.
sub {
my ($c) = @_;
...
$c->redirect('/'); # '/'に遷移したいが...
print "ok!"; # サブルーチンリファレンスの返り値は`print "ok!"`の実行結果, すなわち`1`になるので, リダイレクトしない!
};
Tengと呼ばれるO/R Mapper(ORM)を利用して, DBを操作します(正確に言えば, $c->db
でTengのオブジェクトを取得することが出来るので, これを元にしてDBを操作することができます).
Tengの使い方についての詳細は割愛しますが, Perl Advent Calendar Japan 2011のTeng Tracが詳しいです.
例えば, Tengのsearch
メソッドを使って, schedules
テーブルの全てのレコードを取得するには, 次のように書きます.
sub {
my ($c) = @_;
...
my @schedules = $c->db->search('schedules');
...
};
これまでControllerにあたる部分のコードを見てきました. 続いてViewについて見て行きましょう.
Webアプリケーションは, ユーザ(のブラウザ)からリクエストがあると, それを処理して, HTMLをレンダリングして返します. この辺りの処理を担うのが, 「View」の役割です. 大雑把な説明になりますが, Viewは「Controllerからもらったパラメータを使って, HTMLを適切に描画する」部分を担っている, と思えば良いでしょう.
sub {
my ($c) = @_;
my $counter = $c->session->get('counter') || 0;
$counter++;
$c->session->set('counter' => $counter);
return $c->render('index.tx', {
counter => $counter,
});
};
これまで見てきたControllerのサブルーチンリファレンスの中において, $c->render
の部分でHTMLをレンダリングしています.
このrender
メソッドは, 2つの引数を持ちます. 第1引数がレンダリング時に使うテンプレートの指定で, 第2引数はテンプレートに与えるパラメータです.
次に, このサブルーチンリファレンスで使われている, index.tx
というテンプレートを見てみましょう.
テンプレートは, Webアプリケーションのルートにあるtmpl
ディレクトリの中にあります.
: cascade "include/layout.tx"
: override content -> {
<h1 style="padding: 70px; text-align: center; font-size: 80px; line-height: 1; letter-spacing: -2px;">Hello, Amon2 world!</h1>
... 中略 ...
<h2>Session counter demo</h2>
<p>You seen this page <: $counter :> times.</p>
<form method="post" action="/reset_counter">
<input type="submit" name="Reset counter." class="btn btn-default">
</form>
: }
Amon2では, テンプレートエンジンとしてText::Xslateを使っています. Text::Xslateは, @__gfx__さんが開発した, Perl界最速のテンプレートエンジンです. 速さと高性能さを兼ね備えたテンプレートエンジンで, PerlでWebアプリケーションを開発するのであれば, このテンプレートエンジンを使っておけばまず間違いはないでしょう.
Text::Xslateには, いくつかのシンタックスがありますが, 今回はデフォルトのKolonを利用します. 詳しい使い方は, Text::Xslate::Syntax::KolonのPOD(ドキュメント)を見ると良いでしょう.
ここでは, その中でも重要かつ有用なcascade/overrideと変数の展開についてだけ説明しておきます.
: cascade "include/layout.tx"
: override content -> {
...
: }
まずこの部分ですが, cascade/overrideの部分です.
簡単に言えば: override content -> {
から: }
で囲まれた部分を, include/layout.tx
の<: block content -> { } :>
の部分に埋め込む, という意味になります.
このように書くことで, 例えばHTMLのヘッダや, Webサービスのナビゲーションバーの部分を共通化することが出来ます(詳しくは, tmpl/include/layout.tx
を見てみましょう).
次に変数の展開の部分ですが, テンプレート内部に次のような部分があると思います.
<p>You seen this page <: $counter :> times.</p>
この, <: $counter :>
の部分で, 先程render
メソッドで渡したパラメータを展開することができます.
return $c->render('index.tx', {
counter => $counter,
});
この, counter
というキーに紐付いた値が展開されます.
例えば, $counter
が「3」であれば...
<p>You seen this page 3 times.</p>
のようにHTMLはレンダリングされます.
index.tx
を, 次のように書き換えてシンプルにしましょう.
: cascade "include/layout.tx"
: override content -> {
<h1 style="padding: 70px; text-align: center; font-size: 80px; line-height: 1; letter-spacing: -2px;">Scheduler</h1>
: }
また, これによってControllerにおける/reset_counter
や/account/logout
といったパスへの処理は不要になりました.
これらのコードを削除し, Controller(Scheduler/Web/Dispatcher.pm
)のコードを, 「GETメソッドで」, 「/
にアクセスしたとき」, 「index.tx
をレンダリングする」というコードだけを含むように書き換えましょう.
GETメソッドで'/user'
にアクセスした時, 自分のプロフィール(名前, 趣味, 年齢, 出身校など...)を表示するように, テンプレート(tmpl/user.tx
)を作成し, Controllerにおいて必要なコードを用意してみましょう.
なお, プロフィールの具体的なパラメータについては, テンプレートに直接書くのではなく, Controllerから渡して, レンダリングするようにしましょう.
-> Amon2入門 (第2部)に続く