僕がこの一年の間に言われた仕事と関係無くやってた事の成果を発表します。
「サービス固有のコードを書く事に集中できる環境を提供する」というのが WAF(Web Application Framework)の役割の一つではありますが、オープンな WAF という物は得てして社内事情を考慮しないものなので、結果的に「社内事情の多岐に渡る作業」を開発者が行うハメになります。 このセッションでは、既存の WAF をうまく利用しつつ社内事情を考慮した雛形を作る事で、真の「サービス固有のコードを書く事に集中できる環境を提供」を目指したお話になります。
ここ数年の間に YAPC::Asia では発表されてこなかった、仕事で実用的なWeb アプリケーション開発の話をします。 特に毎月何個もサービスをリリースするような環境で必要になってくる、プロジェクトの雛形を作る時の考え方その実装時の知見をシェアします。 話の前提として Amon2 を利用した開発を前提とする。 また、その雛形開発の副産物となる実装の紹介をします。
具体的には、このプレゼンの後に僕の働いてる会社にエントリーして、無事ぼくと一緒に開発するようになった時に知っとくと得する情報ばかりです。
kdmsnr さんから贈呈してもらった Team Geek は、割と当たり前の事をしっかりと活字になっているので良い本です。わりと僕は反省しました。
最初にミッションステートメントを書いておくとぶれなくていいって Team Geek にも書いてありました。
このプレゼンで言及される環境は、特定の会社に限定されるわけでは無いです。色々混じってます。 あと、一部誇張してる所もあります。
$ amon2-setup.pl BeerMoris
などのように WAF 標準のセットアップスクリプトを使って雛形を作る。
これは机が飛んでくる事例です。(僕らはインフラじゃないので椅子が使えません)
社内環境が一切考慮されていない
机返して下さい。すみませんでした。 社内インフラも何も決まってない状態で雛形とか言って本当にごめんなさい。 サービスがリリースされた後には、管理画面や別サービス等のプロジェクトを始める事があると思うので、その時に開発環境の標準化を一緒に考えた方が確実です。
好き勝手に使ってるのが良いか悪いかは判断出来ないけど、突然隣の同僚が病欠になってしまって急に良くわからないWAFで書かれたコードのメンテをする事になって、初日から障害多発とかいう状況になったら嫌でしょ?
なるべくなら使う道具は揃えておいた方がノウハウ溜まりやすいし誰かが助けてくれやすくなるし、社外でも使われている物を使うと新しい人の学習コストも下がるかもしれないし、誤差かもしれない。し、開発コストってそういう所じゃなかったりするのが社会の厳しさ。あと歴史ある会社だと層も言ってらんないけど、新しいプロジェクトくらいはなんとかしたいね。
僕と一緒に考えましょう。
普段はコピペプログラミング嫌ってるくせに、どうせ最初だけだからって気にする事無くやりがち。「最近始まった開発プロジェクトはあれだから、あれ使っとけば大丈夫!」とか良くわからない自信で使うんですよね。ミドルウェアとかは標準的に統一されてたりするとちょっと残念。
基本的な WAF の雛形に補足する形で社内事情に特化したコードをコピペ量産するので、ノウハウが固まらなかったりバグ修正情報が周知されない、周知されても自分が関係しているかがわからなくて危険な状態になる。 あとは、新しく入ってきた開発者が、新規開発をする時にドキュメントがあったとしても既存の社内開発の流儀を他のプロジェクトのコードから学習しないと安心して開発出来ないケースがでてくる。
コピペ駆動開発のデメリットを無くす為の社内標準雛形。
社内固有の API を利用する為のモジュール等は、独立した社内用モジュールとして実装してリポジトリ管理をするべきです。もちろん一般的な会社では社内用モジュールを作ってメンテナンスして居ると思います。 しかし、社内モジュールは部品にすぎないので、それを実際のアプリケーションで利用するには手作業で実装を進める必要があります。 社内標準雛形を作ると、こういった部分の手間もカバーするように作り込めるので、社内モジュールとは違った角度での恩恵を受けられます。
実用的な社内標準的な雛形を作る一番のポイントがバランスです。
たとえば、位置情報系サービスを1つ作れば十分な会社なのに、その雛形位置情報サービスを提供する為のイロハを全て入れるのは無駄です。標準の雛形に機能を詰め込みすぎると今度は不要な実装を削除する手間がかかってしまい、結局無かった方が良いと思われてしまいます。
どの会社にもある固有のルールに適した雛形を作るべきです。
こういった開発ルールが有る場合には、雛形を生成する時に nginx の設定ファイルや daemontools の run ファイルを一緒に作るべきです。
例えば Ikachan Server や GrowthForecast など、社内用のツールが予め提供されている場合には、開発者が簡単に利用出来るような形のモジュールやスクリプトを予め提供するようにします。事前に提供する事により開発者が利用する敷居が低くなり、結果的に良質な開発サイクルにする事が出来ます。
開発者が雛形を生成してから、サービスリリースを行う為に必要な全てのツールは予め提供しておくべきです。
schema loader, deploy tools, CI tools, run file, batch loader, translate data loader, sass/scss, js/css packer, etc etc
ただし、専用のミドルウェアを使っている箇所は省くなり、それの設定ファイルを標準で提供するべきです。
結果的にはこれは自社 WAF を作った事になります。 Amon2 は、とても小さな WAF なため、いわゆる use parent 'Amon2' をしている context class の拡張が簡単になっていて、コードの見た目にもわかりやすいメンテしやすい雛形を作れています。
default implementation
DB Class, Model, Validation, Exceptions, Constants, Xslate warn_handler, sample controller, ikachan, Teng Schema Dumper, stf tool-kit, ServerStatus::Lite, DBIx::QueryLog, IsuDefender, proclet, ukigumo, growthforecast, application luncher, deploy tools, daemontools, nginx, apache, development server setup script
use Data::FormValidator;
use [% module %]::Exceptions;
model の error 処理を例外でやってるのが嫌なんだけどいい方法なかなかみっかってない。
error の見つけ方があんま良くない。。
use parent qw([% module %]::DB); # use Teng
for my $method (qw/ insert fast_insert bulk_insert ... txn_rollback txn_end do /) {
no strict 'refs';
*{$method} = sub { die "[% module %]::DB::Slave は slave 用クラスなのに $method を呼び出すなんてとんでもない!!!" };
}
package [% module %]::Exception;
use parent 'Exception::Tiny';
use Class::Accessor::Lite ( ro => [qw/ code message /] );
package [% module %]::Constants;
package [% module %];
use constant {
FOO => 1,
}; # is([% module %]->FOO, 1);
my $dev_config = do('conf/dev.pl') or die $!;
my $pro_config = do('conf/pro.pl') or die $!;
my $has_error = 0;
for my $key (sort keys %$dev_config) {
unless (exists $pro_config->{$key}) {
warn "key:$key not exsit in dev.pl.\n";
++$has_error;
}}
exit($has_error ? 1 : 0);
依存してる git repository の処理は以下の普通な感じです。
mkdir "submodules";
!system 'git submodule add git://foo/bar.git submodules/bar' or die $?;
chdir 'lib';
!system 'ln -s ../submodules/bar/lib/Bar.pm Bar.pm' or die $?;
chdir '..';
折角、開発の雛形生成のコツを発表するのに皆が持ち帰れるコードとか用意しないの馬鹿だよね、折角作り直すんだから外出し可能な物作らないとね?
直訳「玄人の為のシステム生成のコツ」
https://github.com/yappo/p5-Ksgk
アプリケーションの雛形生成を支援する為の簡単ツール。
setup.pl = ユーザ実装の雛形スクリプト。設定が重要。
assets/core = 複数の雛形で使い回せるテンプレート
assets/roles = 雛形のテンプレート
assets/options = 拡張機能のテンプレート
use Ksgk;
my $ksgk = Ksgk->new(
argv => \@ARGV,
assets_dir => 'assets',
config => +{ ...config...} ,
);
core_dir => 'core',
roles => [
name => '雛形名',
description => '雛形説明',
options => [ この雛形で利用可能な拡張機能 ],
],
options => [
拡張機能名 => +{
description => '拡張機能の説明',
},
],
hooks => {
before => sub {
my $ksgk = shift;
雛形を生成する直前の hook },
after => sub {
my $ksgk = shift;
雛形を生成した直後の hook },
},
hooks.before = テンプレートの値処理の前段階の callback
hooks.before = テンプレートの値処理の後
core[].name = 基本の設定 key (テンプレートで <ks: $key :gk> で呼ぶ)
core[].description = 設定の説明
core[].default = sub { my($conf, $ksgk) } デフォルト値を作って return する
core[].validation = sub { my($value, $cinf, $ksgk) } 入力された $value の値チェックで、エラーだったら偽を返す
roles.foo[].name = 雛形名'foo'の設定 key (テンプレートで <ks: $key :gk> で呼ぶ)
description, default, validation は core と同じ。拡張機能に関しても同じで roles を options に読み替える。
core, roles/:name, options/:name 以下のファイル構造をそのまま雛形として書き出してくれます。 $ksgk->templrate_config の中の key を $key として filepath として書く事で、出力時のファイル名をアプリケーションに合わせて動的書き換え出来ます。
# $ksgk->templrate_config->{module_path} = 'Foo/Bar' の時
core/lib/$module_path.pm => lib/Foo/Bar.pm
基本的に Xslate の Kolon 記法になっていて、 $ksgk->templrate_config の中身を tmpl_vars として使えます。
package <ks: $module :gk>;
:ksgk: if (1) {
tag_start, tag_end, line_start は、通常使わないであろう文字列にしてある。
Ksgk のファイルの優先順位は core -> roles -> options という順序になっています。 各ファイル階層に同名のファイルがあった場合、例えば core/README と roles/basic/README がある場合には、下位順位のファイルから読み込まれます。
下位のファイルからテンプレート読み込み処理されるのですが、下位ファイルの中に CONTENTS(key, value) という function がある時に key - values の組み合わせを記憶しておき、上位の core/README の中に INCLUDE(key) があった場合に、同名の key の CONTENTS の内容を展開します。
複数拡張機能を利用していて同じファイルが複数あった場合には、読み込まれた順に出力されます。
特定の機能に関わる最小限のコードレシピが蓄積される事により、既存のレガシーなプロジェクトに対しても十分に参考になる環境になって行くのではないか?
僕は、社内基盤を整備する仕事は本業ではなくて、サービスの開発をやらなきゃいけないので、こういったセットアップツールに使う時間は殆ど取れないです。 だいたいは、新規のサービスを作るとき、そのサービスで新規に必要になる要件が後々他の人も使いそうだなというとき、でかつ実装開始まで数日猶予がある隙をねらってバレないように作ってます。
ツールだけを作る人になってしまうと、多分本気に考えられないというか、実際に使い始めてみた時の不便さとかがわからないので、他の人に使ってもらえるような物にはならないんだとおもいます。
実行時に自動的に依存モジュールを入れて立ち上げたかったので以下のようなコードをスクリプト先頭に入れた。
Log::Minimal 等の既存のモジュールは、理由は忘れたけど使いづらかったので独自に log method を追加。fluentd を使える為の雛形を用意してあるので、 fluentd と住み別ける。 ログの用途によって method を増やしていけば、これといって困った事にはならないしがっつりやりたいなら fluentd で。
今どきのスマホ時代は、いつ全世界で流行るかわからないので Amon2 ユーザが他言語対応しやすい環境を調えたのが今年の僕です。
Locale::Maketext::Extract::Plugin::Xslate Xslate のパーサの結果を解析して template 中の function から翻訳対象となるテキストを抽出出来ます。以下のような l という function で翻訳結果を返す実装になっている template で使えます。
[% l('string') %]
[% l('string %1', args[, ...]) %]
[% IF l('string') = 'FOO' %]
Amon2 アプリの中で po/ja.po などのファイルを読み込んで翻訳結果を簡単にテンプレート出力する為のプラグインを書きました。
# in Your.pm
__PACKAGE__->load_plugins('L10N' => {
accept_langs => [qw/ en ja zh-tw zh-cn fr /],
});
# in Your/Web/ViewFunctions.pm
sub l {
my $string = shift;
my @args = map { Text::Xslate::html_escape($_) } @_; # escape arguments
Text::Xslate::mark_raw( Amazlet-> context-> loc($string, @args) );
}
# in po/ja.po
msgid "Hello"
msgstr "こんにちわ"
# in tmpl/index.tt
[% l('Hello') %]
上のような簡単なサンプルコードで、日本語設定のブラウザでアクセスすると「こんにちわ」が表示されます。
Accept-Language head parser
my $lang = HTTP::AcceptLanguage->new(
$req->header('Accept-Language')
)->match(qw/ en fr es ja zh-tw /);
RFC 信用するとお家帰れないので作った。
僕はテンプレートの中のテキストを l('Hello') のように英語で書くのが好みなのですが、一つの英語表記でも使う場所や言語によっては違う意味になるケースがでてきます。 そういった場合には翻訳すべきテキストに key や id などを割り当てて管理して l('config.helo') のような key をテンプレートに書いてく事になるのですが、長文の英語だと key が表す文章が理解不能になるケースがあって困るので l('Hello###config.helo###') のように msgid と msgstr を同時に記述する雛形を作っている。
# in en_translate.txt
config.helo=Hello
# in po/en.po
msgid="Hello"
msgstr="Hello"
msgid="Hello###config.helo###"
msgstr="Hello"
以上のようなルールで .po ファイルを自働生成している。
Perl は基本的に例外処理という概念は無いに等しいのですが、 model の処理で validation error とか、その他の error とかを戻り値ベースで対応してコード書くと reterun code とか考えるのだるくなるので例外ベースで model の error を処理しがちです
package MyE;
use parent 'Exception::Tiny';
eval { MyE->throw }
$@ && MyE->caught($@) && do { ... }
Exception class の為の最低限の実装しか書いてなくて、拡張は perl らしく CPAN つかう。
Kantan exception Catch
try {
MyE->throw;
} (
'MyE' => sub {
say $@;
}
);
やっpり model の error を exception にするの微妙
git clone, pull, submodule update などの処理してから carton install, perl Makefile.PL して prove -r t > /tmp/result.txt するコマンド叩いたあとに、 result.txt の内容を ukigumo web api に post するスクリプトを cron で回してる
# 素の make test つかうと blib/ にコピーされたりしてうざいので prove 直接たたいてやります
sub MY::test_via_harness {
"\tprove -rl t"
}
これで make test すると prove -rl t してくれる
:ksgk: if (!$no_carton) {
"--exec=carton exec -- perl -Ilib -I."
:ksgk: }
-Pretty
--color
-v
GrowthForecast と HRForecast と shib と fluentd とか組み合わせればおっけ。
grouthforecast-worker.pl を同梱してる。
keyword: kazeburo tagomoris
use explain, use index
for ループの回し方でクエリ数が増えて椅子とぶとかあるけど、上記二点が絶対条件。
if (<ks: $module :gk>->is_development) {
enable sub { my $app = shift; sub {
my $env = shift;
if ($env->{QUERY_STRING} =~ /querylog=1/) {
$env->{'xdbix.querylog'} = DBIx::QueryLog->guard;
}
$app->($env);
}};
}
my $logdir = $vardir->child('log');
$logdir->mkpath; chmod 0777, $logdir;
$logger = File::RotateLogs->new(
logfile => $logdir->child('access_log.%Y%m%d%H%M'),
linkname => $logdir->child('access_log')
);
enable 'AxsLog',
combined => 1,
response_time => 1,
logger => sub { $logger->print(@_) };
rsync で template をバラまく時に、アプリケーションのコードが restart される前にテンプレートが利用されるので、アプリケーション側の仕様とテンプレート側を同時に反映するとエラーになる。DB Schema とアプリケーションでも同様。
いつなんどきサービスが流行るかもわからないので、いつでもスケールするDB設計やアプリケーション設計をするのは当然ですが、みんな忘れがちな物として新しいサーバが増設されたらどれだけ早く本番投入出来るか!?
アプリケーションバラまいたら直ちに正しく動作する順部を deploy script で行う
Use a spacebar or arrow keys to navigate