さくらVPS移行メモ

メモリが 512MB から 2GB に。ヤッター。OS は Debian 6 amd64 をさくっと入れておく。
他にもやっておいた方がよさそうなものがあったら教えて下さい。

ssh root@ip

とりあえず foo で sudo 出来るように。

# apt-get update
# apt-get upgrade
# dpkg-reconfigure tzdata
# apt-get install sudo
# vi /etc/sudoers
# diff -u /tmp/sudoers /etc/sudoers
--- /tmp/sudoers	2012-05-04 05:20:48.000000000 +0900
+++ /etc/sudoers	2012-05-04 05:21:05.000000000 +0900
@@ -15,6 +15,7 @@
 
 # User privilege specification
 root	ALL=(ALL) ALL
+foo	ALL=(ALL) ALL
 
 # Allow members of group sudo to execute any command
 # (Note that later entries override this, so you might need to move
# exit

ssh foo@ip

公開鍵認証

$ mkdir .ssh
$ vi .ssh/authorized_keys
$ chown -R foo:foo .ssh
$ chmod 700 .ssh
$ chmod 600 .ssh/authorized_keys
$ exit

デフォルトのポートはログが悲惨なことになるので適当に。rootとパスワード認証は禁止する

$ sudo vi /etc/ssh/sshd_config 
$ diff -u /tmp/sshd_config /etc/ssh/sshd_config 
--- /tmp/sshd_config	2012-05-04 05:31:05.000000000 +0900
+++ /etc/ssh/sshd_config	2012-05-04 05:35:15.000000000 +0900
@@ -2,7 +2,7 @@
 # See the sshd_config(5) manpage for details
 
 # What ports, IPs and protocols we listen for
-Port 22
+Port 2000 # ここは適当に
 # Use these options to restrict which interfaces/protocols sshd will bind to
 #ListenAddress ::
 #ListenAddress 0.0.0.0
@@ -47,7 +47,9 @@
 ChallengeResponseAuthentication no
 
 # Change to no to disable tunnelled clear text passwords
-#PasswordAuthentication yes
+PasswordAuthentication no
+
+PermitRootLogin no
 
 # Kerberos options
 #KerberosAuthentication no
$ sudo service ssh restart

とりあえず http, https, ssh だけ開けておく。

$ sudo apt-get install arno-iptables-firewall

※ここで別のターミナルを開いて ssh foo@ip -p 2000 で締め出されていないか確認。

logwatch で定期的にメールでレポートを送る。

$ sudo apt-get install postfix logwatch
$ sudo vi /usr/share/logwatch/default.conf/logwatch.conf
$ diff -u /tmp/logwatch.conf /usr/share/logwatch/default.conf/logwatch.conf
--- /tmp/logwatch.conf	2012-05-04 06:35:12.000000000 +0900
+++ /usr/share/logwatch/default.conf/logwatch.conf	2012-05-04 06:37:50.000000000 +0900
@@ -32,7 +32,7 @@
 #Output/Format Options
 #By default Logwatch will print to stdout in text with no encoding.
 #To make email Default set Output = mail to save to file set Output = file
-Output = stdout
+Output = mail
 #To make Html the default formatting Format = html
 Format = text
 #To make Base64 [aka uuencode] Encode = base64
@@ -41,7 +41,7 @@
 # Default person to mail reports to.  Can be a local account or a
 # complete email address.  Variable Output should be set to mail, or
 # --output mail should be passed on command line to enable mail feature.
-MailTo = root
+MailTo = user@example.com
 # WHen using option --multiemail, it is possible to specify a different
 # email recipient per host processed.  For example, to send the report
 # for hostname host1 to user@example.com, use:
@@ -67,7 +67,7 @@
 
 # The default time range for the report...
 # The current choices are All, Today, Yesterday
-Range = yesterday
+Range = Today
 
 # The default detail level for the report.
 # This can either be Low, Med, High or a number.
$ sudo EDITOR=vi crontab -e
0 1  * * *          /usr/sbin/logwatch

気軽にグラフを見て監視したいので munin で、1台だけなので設定はこれだけ。

$ sudo apt-get install munin-node munin
$ sudo vi /etc/munin/munin.conf 
$ diff -u /tmp/munin.conf /etc/munin/munin.conf 
--- /tmp/munin.conf	2012-05-04 06:54:10.000000000 +0900
+++ /etc/munin/munin.conf	2012-05-04 06:55:34.000000000 +0900
@@ -5,10 +5,10 @@
 # must be writable by the user running munin-cron.  They are all
 # defaulted to the values you see here.
 #
-# dbdir	/var/lib/munin
-# htmldir /var/cache/munin/www
-# logdir /var/log/munin
-# rundir  /var/run/munin
+dbdir	/var/lib/munin
+htmldir /var/cache/munin/www
+logdir /var/log/munin
+rundir  /var/run/munin
 #
 # Where to look for the HTML templates
 # tmpldir	/etc/munin/templates
$ sudo /etc/init.d/munin-node restart

便利ツールを一式入れておく

$ sudo apt-get build-dep perl
$ sudo apt-get install \
    build-essential \
    ssh \
    htop \
    vim \
    git-core \
    screen \
    unzip \
    global \
    ctags \
    curl \
    spell \
    strace \
    sysstat \
    tree \
    libpcre3-dev \
    libssl-dev \
    expat \
    libexpat1-dev \
    libxml2-dev \
    libjpeg8-dev \
    libgif-dev \
    libpng12-dev \
    daemontools-run

あとは必要な httpd, memcached, mysql 等を好きなように。

疎結合と MVC と JSON API とオーバーヘッドと非同期とレスポンスタイムに関する適当な考察

最近考えていることを話す機会があったので文章にしてまとめてみる。

疎結合

昨今の複雑化するウェブアプリケーションを効率的に開発するにあたって、疎結合な設計にすることは開発/保守効率を上げるためには必須の条件となることは経験上嫌というほど皆が経験している。(このへんの感覚がわからない人は一度疎結合なアプリケーションを書いてみると良い)

疎結合な設計をすることで問題の切り分けが容易になり、自動化されたユニットテストコンポーネントテスト・ UAT が手元の MacBook で実行でき、高いカバレッジに助けられて臆すること無くコードに手を入れられる環境を入手でき、開発する上でストレスなく障害も少ないというメリットが享受できる。

話を戻して、疎結合な設計の例をみると、ウェブサーバとアプリケーションを分離することであるとか、 MVC であるとか、良質かつ単体で動く小さなモジュールを組み合わせて書くといった例があげられ、また最近ではWebサービスAPIを組み合わせるマッシュアップも台頭し、これも一種の疎結合な設計に含めて考えても良い。この概念に近いものとして MVC にサービス層を含めるような設計をすることでモデル層を抽象化し、コンポーネントテストを書きやすくする設計もチラホラ見かける。

MVCJSON API

MVC + サービス層」の設計でアプリケーションを書くほど抽象化するのであれば、それはWebサービスAPIを提供することとやっていることの本質はさほど変わらないのではないかと思う。そこでいっそのこと振り切って考えて、サービス層に当たる部分に JSON を返せるだけの薄い Controller をのせ、 HTTP サーバとして投入することで設計をより一層疎結合なものへと変更できる。
こうすることでブラウザからリクエストを受けるアプリサーバの Controller は、認証等が済んだ状態で JSON API を叩きレスポンスを View に渡す(あるいは JSON をそのまま通過させる)だけの機能にまで落とし込める。

オーバーヘッド

ここまでやってしまうと気になることが API を叩くために HTTP を経由させることで発生するオーバーヘッド。リクエストを飛ばすことによる CPU 負荷や直列化されたネットワーク IO がボトルネックになりそうなことは容易に想像できる。 CPU 負荷に関してはより高速なライブラリを選ぶなどしてチューニングすべきではあるが…。(他にも内部のトラフィックが増大する可能性もあり、 API サーバは deflate を使って通信を圧縮する必要が有りそうではあるがここでは無視する。インフラの人ごめんなさい)

非同期

直列化されたネットワーク IO がレスポンスタイムに対して大きな比率を占めることは容易に想像できるが、近年のブームを見ているとここは非同期化して解決する方法がわりと簡単に使えるのではないかと思う。リクエストを受けるサーバとして node.js が頭に浮かぶが、 node.js は自由が効かないので選択肢から除外する。(ポイントとなるのは API に対するリクエストの並列化であって、ブラウザからのリクエストに対して全体を非同期化することではない)

非同期化の対象を Controller 内での API に対するリクエストに局所化する理由
  • prefork モデルの方が運用上のノウハウが豊富にある
  • callback地獄を回避する
  • レスポンスタイムが問題になるほど大量にリクエストしている箇所だけをチューニングできる(=最小の努力で最大の効果を得る)
  • 全てが非同期で処理される場合かつ一人の開発者がブロックするコードを混ぜてしまった場合、プロセス全体のパフォーマンスを悪化させてしまう(これだけの設計が必要な規模になるとコミットが大量になるはずで、現実的に考えてデバッグは不可能に近い、気がする)

全体を非同期化することで得られるメリットはこれらを無視してでも手にすべきほど魅力的なものか、現状では考えにくい。

pseudocode
package MyApp::Controller::MyPage;
use strict;
use warnings;
use parent 'MyApp::Handler';
use AE;
use MyApp::UserAgent::AE;

my $ua = MyApp::UserAgent::AE->new;

sub show {
  my ($self, $ctx) = @_;
  my $id = $ctx->request->session->{user_id};

  my $params = {};

  my $cv = AE::cv;

  $cv->begin;
  $ua->get("friends/list", { user_id => $id }, sub {
    my ($res) = @_;
    $params->{friends_list} = $res;
    $cv->end;
  });

  $cv->begin;
  $ua->get("activity", { user_id => $id }, sub {
    my ($res) = @_;
    $params->{activity} = $res;
    $cv->end;
  });

  $cv->begin;
  $ua->get("notifications", { user_id => $id }, sub {
    my ($res) = @_;
    $params->{notifications} = $res;
    $cv->end;
  });

  # ...

  $cv->recv;

  $self->render("mypage", $params);
}

1;

こんな感じに設計できたらいいよねーというお話でした。
疎結合になるとインフラの人も色々と手を入れやすくなるのでデメリットばっかりじゃないよ><

LWP::UserAgent と LWP::Protocol::PSGI でテストを書くと楽できる話

Plack::Test + HTTP::Request::Common

世の中には Plack::Test + HTTP::Request::Common という方法もあるが、この場合ブラウザを模したようなテストを書くと意外にも破綻しやすい。とりわけセッション周りの挙動が必須になると大変な手間になる。

LWP::UserAgent + LWP::Protocol::PSGI

最近は LWP::UserAgent + LWP::Protocol::PSGI で楽をするような方法で書くようにしている。ユーザー寄りのテスト(ログイン後の処理やCSRF対策用のトークンが必須等)をわりと楽に書ける点がメリット。

subtest と scope のメリットも享受できる /zento+/ 方式が見通し良く出来そう。*1

サンプル

Some::Middleware::CSRFDefenderのテストを書くとすればこんな感じ。

use Test::More;

use LWP::UserAgent;
use LWP::Protocol::PSGI;

use Plack::Request;
use Plack::Session;
use Plack::Middleware::Session;

use Some::Middleware::CSRFDefender;

my $raw_app = sub {
    my $env = shift;
    my $ses = Plack::Session->new($env);
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body( $ses->get("csrf_token") );
    return $res->finalize;
};

my $url = "http://localhost/";

subtest "Should not work without session middleware" => sub {
    my $app = Some::Middleware::CSRFDefender->wrap($raw_app);
    my $guard = LWP::Protocol::PSGI->register($app);

    my $res = LWP::UserAgent->new->get($url);
    is $res->code, 500;
};

subtest "Basic test cases" => sub {
    # Prepare environments for testing
    my $app = Plack::Middleware::Session->wrap(
        Some::Middleware::CSRFDefender->wrap(
            $raw_app, error_message => "Forbidden"
        )
    );

    my $guard = LWP::Protocol::PSGI->register($app);
    my $ua = LWP::UserAgent->new( cookie_jar => {} );

    my $token = $ua->get($url)->content;

    subtest "returns the same token for the same client" => sub {
        my $res = $ua->get($url);
        is $res->code, 200;
        is $res->content, $token;
    };

    subtest "returns different token for another client" => sub {
       my $another_token = LWP::UserAgent->new->get($url)->content;
       isnt $another_token, $token;
    };

    subtest "POST with valid token" => sub {
        my $res = $ua->post($url, [csrf_token => $token]);
        is $res->code, 200;
        is $res->content, $token;
    };

    subtest "POST with invalid token" => sub {
        my $res = $ua->post($url, [csrf_token => 'invalid token']);
        is $res->code, 403;
        is $res->content, 'Forbidden';
    };

    subtest "POST without token" => sub {
        my $res = LWP::UserAgent->new->post($url);
        is $res->code, 403;
        is $res->content, 'Forbidden';
    };

};

done_testing;

node.js + jQuery で Web scraping

jsdomとjQueryつかって富豪的にホクホクできるらしい。セレクタさえ丹念に集めれば Readability/Instapaper のようにコンテンツのテキストだけ抽出するようなこともさほど難しくないか。

var jsdom = require("jsdom"),
    fs = require("fs");
var jquery = fs.readFileSync("./jquery.js");

jsdom.env({
  html : 'http://atnd.org/events/26373',
  src  : [ jquery ],
  done : function (err, window) {
    var $ = window.$;
    $("ol.a-b > li").each(function () {
      console.log( $("a", this).text() );
    });
  }
});

LESS/JavaScript/CSS/HTMLでインデントを2スペースに設定する vimrc

普段の設定

set tabstop=4
set expandtab
set shiftwidth=4

こんな設定でインデントをスペース4つとして書いると、LESS/JavaScript/CSS/HTML の場合は嫌でもインデントが深くなり、縦分割して作業すると悲惨なことになってしまう問題が起きる。

function を定義して autocmd で呼び出す

function を定義して autocmd で呼び出すやり方で解決した。ftplugin/に書いても問題なさそうではあるが、vimrc 自体が小さいこともあり、見通しよくするために直書き(分量が増えたらあとでftpluginに移せば良い)

条件として
  • この4つのファイルタイプの場合のみインデントをスペース2つとして設定する
  • それ以外のファイルタイプでは影響が出ないようにする
  • ~/.vimrc に直書き(大した量ではないので ftplugin はナシ)
設定をvimrcに追記

ファイルタイプ毎にメインの設定とは別の設定で利用できる。

function! s:javascript_filetype_settings()
    setlocal tabstop=2
    setlocal shiftwidth=2
    setlocal cindent
endfunction
autocmd FileType javascript call s:javascript_filetype_settings()

function! s:html_filetype_settings()
    setlocal tabstop=2
    setlocal shiftwidth=2
    setlocal includeexpr=substitute(v:fname,'^\\/','','') |
endfunction
autocmd FileType html call s:html_filetype_settings()

function! s:css_filetype_settings()
    setlocal tabstop=2
    setlocal shiftwidth=2
    setlocal cindent
endfunction
autocmd FileType css  call s:css_filetype_settings()
autocmd FileType less call s:css_filetype_settings()

Ustream 放送の開始を通知してくれる Chrome 拡張を書いてた

Ustream の配信は通知系が無いのでいつ始まっているのか把握困難。そこで開始を通知してくれる拡張を作ってた。ここ一年くらい動いてます。
Chrome拡張はちょろちょろ〜っと書けていいですね

スクリーンショット

ピロっと音が鳴って、こんなnotificationが出ます。クリックすれば配信ページに飛べるという寸法。



既知の問題

長時間アラート表示を放置しておくとクリックしてもページを開かなくなる

これは恐らくChrome側が悪いはず。patches welcome

App::ExtractUsed というものを書いている話

CPAN形式で開発していると避けて通れないのが依存モジュールの炙り出しと Makefile.PL へ requires を書き出す作業。これは意外と面倒なのでもっと怠惰に楽をしたいところ。

たとえモジュールが要求するperlのバージョンを決めていたとしても「どれがコアモジュールでどれが違うのか」「parent.pm は 5.10.1 からコアモジュールになった」など、漏れなく抽出しないと新しい環境でインストールする時に悲しいことになってしまう。

こんな面倒事はコマンド叩くだけで済ませてしまいたい、というわけで GitHub - punytan/p5-App-ExtractUsed をでっち上げた。

使い方

extractused コマンドがインストールされるので、基本的にはこれを叩くだけで lib/ と t/ を走査して requires, test_requires が出力される感じ。

$ extractused 
requires 'WWW::NicoVideo::Download';
requires 'WWW::YouTube::Download';
test_requires 'Coro';

"t::" のようなテストディレクトリにある名前空間は強制的に取り除かれるので、心置きなく use t::Util; できてホクホクです。

オプション

--dir

指定したディレクトリのみを走査したい場合に。デフォルトは lib と t ディレクトリ。

$ extractused --dir bin --skip YourProject
--file

任意のファイルのみから抽出したいときはこのオプションでパスを指定。

$ extractused --file lib/YourProject.pm --skip YourProject
--skip

プロジェクトのnamesapaceを無視したいときに。
ここの挙動は自動的に判定するように変えたい(けれどロジック書くのがちょっと厄介)。
前方一致で判定するのでこの場合、YourProject::Web もスキップされる。

$ extractused --skip YourProject
--perl

最低限サポートしたい perl のバージョン。perlのバージョンによってコアモジュールが変わってくる(5.10.1あたりだと parent.pm, それ以前だと version.pm 等)ので、ちょっと古めの環境でも使うなら指定しておいたほうが無難。デフォルトは現在走ってるバージョン。

$ extractused --perl 5.10.1

まだまだ alpha-quality

使ってみて気になる所があればぜひ GitHub - punytan/p5-App-ExtractUsed に pull-req を。
ある程度インターフェースが固まってきたらCPANize予定。それまでは互換性考慮せず変えていきたいと思ってます。