Featured image of post Plack/PSGI - Perl Webアプリケーションの基盤技術

Plack/PSGI - Perl Webアプリケーションの基盤技術

Plack/PSGIはPerlのWebアプリケーション開発に革命をもたらした基盤技術です。PSGIの概念、Plackの使い方、ミドルウェアの活用方法を、実際に動作するコード例とともに詳しく解説します

Plack/PSGIとは何か

Perl Webアプリケーション開発において、Plack/PSGIは革命的な存在です。2009年に宮川達彦氏によって提案されたPSGI(Perl Web Server Gateway Interface)は、PerlにおけるWebアプリケーションとWebサーバーの間のインターフェース仕様です。そして、Plackはそのリファレンス実装として、PSGIアプリケーションの実行環境とミドルウェアのエコシステムを提供します。

PSGIが解決した問題

PSGIが登場する前のPerl Web開発では、各フレームワーク(Catalyst、CGI::Application等)が独自にWebサーバーとのやり取りを実装していました。これには以下のような問題がありました:

  • フレームワーク固有の実装: 各フレームワークがApache mod_perl、FastCGI、CGIなどに個別対応
  • デプロイの複雑さ: フレームワークを変更すると、デプロイ方法も変更が必要
  • 再利用性の低さ: 認証、ロギング、セッション管理などの共通機能が各フレームワークで個別実装
  • テストの困難さ: Webサーバーを起動しないとテストできない場合が多い

PSGIはこれらの問題を、アプリケーション層とサーバー層を分離するという明確な仕様によって解決しました。

PSGIの仕様

PSGIアプリケーションは、非常にシンプルな仕様で定義されます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 最小のPSGIアプリケーション
my $app = sub {
    my $env = shift;  # リクエスト情報を含むハッシュリファレンス
    
    return [
        200,                             # HTTPステータスコード
        [ 'Content-Type' => 'text/plain' ],  # HTTPヘッダー(配列リファレンス)
        [ "Hello World" ],               # レスポンスボディ(配列リファレンス)
    ];
};

たったこれだけです!PSGIアプリケーションは、環境変数のハッシュリファレンスを受け取り、3要素の配列リファレンス(ステータス、ヘッダー、ボディ)を返すサブルーチンリファレンスです。

この単純さこそがPSGIの強みです。Python の WSGI、Ruby の Rackに影響を受けた設計思想が、Perl の Web開発に大きな変革をもたらしました。

Plackのインストールと基本的な使い方

インストール

1
2
3
4
5
# cpanmを使う場合
cpanm Plack

# cpanを使う場合
cpan Plack

最初のPSGIアプリケーション - Hello World

hello.psgi という名前でファイルを作成します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# hello.psgi
my $app = sub {
    my $env = shift;
    
    return [
        200,
        [ 'Content-Type' => 'text/html; charset=UTF-8' ],
        [ "<html><body><h1>Hello, Plack!</h1></body></html>" ],
    ];
};

実行は plackup コマンドを使います:

1
2
plackup hello.psgi
# HTTP::Server::PSGI: Accepting connections at http://0:5000/

ブラウザで http://localhost:5000/ にアクセスすると、「Hello, Plack!」が表示されます。

環境変数($env)の活用

$env ハッシュリファレンスには、リクエストに関する豊富な情報が含まれています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# env_info.psgi
my $app = sub {
    my $env = shift;
    
    my $path = $env->{PATH_INFO};        # リクエストパス
    my $method = $env->{REQUEST_METHOD}; # HTTPメソッド
    my $query = $env->{QUERY_STRING};    # クエリ文字列
    
    my $body = <<"HTML";
<html>
<head><title>環境変数の例</title></head>
<body>
<h1>リクエスト情報</h1>
<ul>
<li>パス: $path</li>
<li>メソッド: $method</li>
<li>クエリ: $query</li>
</ul>
</body>
</html>
HTML
    
    return [
        200,
        [ 'Content-Type' => 'text/html; charset=UTF-8' ],
        [ $body ],
    ];
};

パスによる分岐

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# router.psgi
my $app = sub {
    my $env = shift;
    my $path = $env->{PATH_INFO};
    
    if ($path eq '/') {
        return [
            200,
            [ 'Content-Type' => 'text/html; charset=UTF-8' ],
            [ "<h1>トップページ</h1>" ],
        ];
    }
    elsif ($path eq '/about') {
        return [
            200,
            [ 'Content-Type' => 'text/html; charset=UTF-8' ],
            [ "<h1>Aboutページ</h1><p>このサイトについて</p>" ],
        ];
    }
    else {
        return [
            404,
            [ 'Content-Type' => 'text/html; charset=UTF-8' ],
            [ "<h1>404 Not Found</h1>" ],
        ];
    }
};

Plackミドルウェア - 機能を積み重ねる

Plackの真の力は、ミドルウェアのエコシステムにあります。ミドルウェアは、PSGIアプリケーションをラップして機能を追加する仕組みです。

ミドルウェアの基本

ミドルウェアは Plack::Builderbuilderenable を使って適用します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# middleware_basic.psgi
use Plack::Builder;

my $app = sub {
    my $env = shift;
    return [
        200,
        [ 'Content-Type' => 'text/plain' ],
        [ "Original App Response" ],
    ];
};

builder {
    enable 'AccessLog', format => 'combined';  # アクセスログ
    $app;
};

便利なミドルウェアの紹介

Plack::Middleware::Static - 静的ファイル配信

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# static_files.psgi
use Plack::Builder;

my $app = sub {
    return [ 200, [ 'Content-Type' => 'text/html' ], [ "<h1>Dynamic Content</h1>" ] ];
};

builder {
    enable 'Static',
        path => qr{^/static/},  # /static/で始まるパスを静的ファイルとして扱う
        root => './public';      # ドキュメントルート
    $app;
};

Plack::Middleware::Auth::Basic - Basic認証

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# auth_basic.psgi
use Plack::Builder;

my $app = sub {
    return [ 200, [ 'Content-Type' => 'text/html' ], [ "<h1>Protected Content</h1>" ] ];
};

builder {
    enable 'Auth::Basic', authenticator => sub {
        my ($username, $password, $env) = @_;
        return $username eq 'admin' && $password eq 'secret';
    };
    $app;
};

Plack::Middleware::Session - セッション管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# session.psgi
use Plack::Builder;
use Plack::Request;

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $session = $req->session;
    
    # セッションからカウンターを取得(初回は0)
    my $count = $session->{counter} || 0;
    $count++;
    $session->{counter} = $count;
    
    return [
        200,
        [ 'Content-Type' => 'text/html; charset=UTF-8' ],
        [ "<h1>訪問回数: $count</h1>" ],
    ];
};

builder {
    enable 'Session';  # デフォルトでCookieベースのセッション
    $app;
};

ミドルウェアの積み重ね

複数のミドルウェアを組み合わせることで、強力なアプリケーションを構築できます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# combined_middleware.psgi
use Plack::Builder;
use Plack::Request;

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    
    my $session = $req->session;
    my $user = $session->{user} || 'ゲスト';
    
    return [
        200,
        [ 'Content-Type' => 'text/html; charset=UTF-8' ],
        [ "<h1>ようこそ、${user}さん!</h1>" ],
    ];
};

builder {
    # アクセスログ
    enable 'AccessLog', format => 'combined';
    
    # 静的ファイル配信
    enable 'Static',
        path => qr{^/(images|css|js)/},
        root => './public';
    
    # セッション管理
    enable 'Session',
        store => 'File';  # ファイルベースのセッション保存
    
    # エラーページのカスタマイズ
    enable 'HTTPExceptions';
    
    # レスポンスの圧縮
    enable 'Deflater',
        content_type => ['text/css', 'text/html', 'text/javascript', 'application/javascript'];
    
    $app;
};

ミドルウェアは上から順に適用され、レスポンスは逆順(下から上)に処理されます。これは、まさに「PSGI アプリケーションをミドルウェアが層状にラップする」という構造です。

実用的なアプリケーション例

Plack::Requestを使った本格的なアプリケーション

Plack::RequestPlack::Response を使うと、より直感的にリクエスト・レスポンスを扱えます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# todo_app.psgi
use strict;
use warnings;
use Plack::Builder;
use Plack::Request;
use Plack::Response;
use JSON qw(encode_json decode_json);

# 簡易的なTODOリスト(本来はDBやファイルに保存)
my @todos = (
    { id => 1, title => 'Plackを学ぶ', done => 0 },
    { id => 2, title => 'PSGIアプリを作る', done => 0 },
);
my $next_id = 3;

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = Plack::Response->new(200);
    
    my $path = $req->path_info;
    my $method = $req->method;
    
    # ルーティング
    if ($path eq '/' && $method eq 'GET') {
        # TODO一覧表示(HTML)
        $res->content_type('text/html; charset=UTF-8');
        
        my $html = "<html><head><title>TODO App</title></head><body>";
        $html .= "<h1>TODO リスト</h1><ul>";
        
        for my $todo (@todos) {
            my $status = $todo->{done} ? '✓' : '○';
            $html .= "<li>$status $todo->{title}</li>";
        }
        
        $html .= "</ul></body></html>";
        $res->body($html);
    }
    elsif ($path eq '/api/todos' && $method eq 'GET') {
        # API: TODO一覧取得(JSON)
        $res->content_type('application/json');
        $res->body(encode_json(\@todos));
    }
    elsif ($path eq '/api/todos' && $method eq 'POST') {
        # API: TODO追加
        my $params = decode_json($req->content);
        
        push @todos, {
            id => $next_id++,
            title => $params->{title},
            done => 0,
        };
        
        $res->status(201);
        $res->content_type('application/json');
        $res->body(encode_json({ success => 1 }));
    }
    else {
        $res->status(404);
        $res->content_type('text/plain');
        $res->body("Not Found");
    }
    
    return $res->finalize;
};

builder {
    enable 'AccessLog';
    enable 'Static', path => qr{^/static/}, root => './public';
    $app;
};

テンプレートエンジンの利用

Text::Xslateなどのテンプレートエンジンと組み合わせることで、より保守性の高いアプリケーションになります:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# template_app.psgi
use strict;
use warnings;
use Plack::Builder;
use Plack::Request;
use Text::Xslate;

my $tx = Text::Xslate->new(
    path => ['templates'],
    syntax => 'TTerse',
);

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    
    my $html = $tx->render('index.tx', {
        title => 'Plack Template Example',
        items => [
            { name => 'Item 1', price => 100 },
            { name => 'Item 2', price => 200 },
            { name => 'Item 3', price => 300 },
        ],
    });
    
    return [
        200,
        [ 'Content-Type' => 'text/html; charset=UTF-8' ],
        [ $html ],
    ];
};

builder {
    enable 'Static', path => qr{^/static/}, root => './public';
    $app;
};

デプロイと本番運用

開発時は plackup で十分ですが、本番環境では高性能なPSGIサーバーを使用します。

Starlet - シンプルで高速

Starletは、プリフォーク型の高性能HTTPサーバーです:

1
2
3
4
5
# インストール
cpanm Starlet

# 起動(ワーカープロセス10個)
plackup -s Starlet --max-workers=10 --port=5000 app.psgi

Starman - 本番環境向け

Starmanは、より本番環境向けに作られたサーバーです:

1
2
3
4
5
6
7
8
# インストール
cpanm Starman

# 起動
starman --workers=10 --port=5000 app.psgi

# デーモンとして起動
starman --daemonize --workers=10 --port=5000 app.psgi

リバースプロキシ構成

本番環境では、NginxやApacheをリバースプロキシとして前段に置くのが一般的です:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Nginx設定例
upstream backend {
    server 127.0.0.1:5000;
    server 127.0.0.1:5001;
}

server {
    listen 80;
    server_name example.com;
    
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location /static/ {
        alias /path/to/app/public/;
    }
}

Systemdでのサービス化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# /etc/systemd/system/myapp.service
[Unit]
Description=My Plack Application
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/path/to/app
ExecStart=/usr/local/bin/starman --workers=10 --port=5000 app.psgi
Restart=always

[Install]
WantedBy=multi-user.target
1
2
3
4
# サービスの有効化と起動
systemctl enable myapp
systemctl start myapp
systemctl status myapp

既存フレームワークとの関係

Mojolicious

Mojoliciousは、Plack/PSGIに対応しています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Mojoliciousアプリを PSGI として動かす
use Mojolicious::Lite;

get '/' => sub {
    my $c = shift;
    $c->render(text => 'Hello from Mojo!');
};

# PSGIアプリケーションとして起動
app->start;
1
2
# plackup で起動可能
plackup myapp.pl

Dancer2

Dancer2も完全にPSGI準拠です:

1
2
3
4
5
6
7
8
9
# Dancer2 アプリ
use Dancer2;

get '/' => sub {
    return 'Hello from Dancer2!';
};

# PSGIアプリケーションを返す
to_app;
1
plackup -s Starman bin/app.psgi

Catalyst

Catalystは初期からPSGIに対応しています:

1
2
# Catalyst アプリをPSGIで起動
plackup -s Starman myapp.psgi

これらのフレームワークはすべてPSGIという共通基盤の上に構築されているため、デプロイ方法やミドルウェアの適用方法は共通です。フレームワークの選択は、アプリケーションの要件や開発者の好みによって自由に行えます。

カスタムミドルウェアの作成

独自のミドルウェアも簡単に作成できます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# lib/Plack/Middleware/Timer.pm
package Plack::Middleware::Timer;
use strict;
use warnings;
use parent qw(Plack::Middleware);
use Time::HiRes qw(gettimeofday tv_interval);

sub call {
    my ($self, $env) = @_;
    
    my $start = [gettimeofday];
    
    my $res = $self->app->($env);
    
    return $self->response_cb($res, sub {
        my $res = shift;
        
        my $elapsed = tv_interval($start);
        
        # レスポンスヘッダーに処理時間を追加
        push @{$res->[1]}, 'X-Runtime' => sprintf("%.6f", $elapsed);
        
        warn sprintf("Request processed in %.6f seconds\n", $elapsed);
    });
}

1;

使用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# app_with_timer.psgi
use Plack::Builder;
use lib 'lib';

my $app = sub {
    sleep 1;  # 処理に時間がかかるシミュレーション
    return [ 200, [ 'Content-Type' => 'text/plain' ], [ "Done" ] ];
};

builder {
    enable '+Plack::Middleware::Timer';  # + を付けてフルパッケージ名を指定
    $app;
};

テストの容易さ

PSGIアプリケーションは、Webサーバーを起動せずにテストできます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# test.t
use strict;
use warnings;
use Test::More;
use Plack::Test;
use HTTP::Request::Common;

my $app = sub {
    my $env = shift;
    return [ 200, [ 'Content-Type' => 'text/plain' ], [ "Hello" ] ];
};

test_psgi $app, sub {
    my $cb = shift;
    
    my $res = $cb->(GET "/");
    is $res->code, 200, 'status is 200';
    is $res->content, 'Hello', 'content is Hello';
};

done_testing;

Plack::Test を使えば、HTTPリクエストをシミュレートしてアプリケーションをテストできます。これは、Webサーバーの起動が不要で、高速にテストを実行できることを意味します。

まとめ

Plack/PSGIは、Perl Web開発に以下の恩恵をもたらしました:

  1. 標準化されたインターフェース: アプリとサーバーの分離により、デプロイの選択肢が広がった
  2. 再利用可能なミドルウェア: 認証、ロギング、セッション管理などを簡単に追加できる
  3. テストの容易さ: Webサーバー不要でユニットテストが可能
  4. フレームワーク非依存: Mojolicious、Dancer2、Catalystすべてが同じ基盤を共有
  5. シンプルな仕様: 学習コストが低く、理解しやすい

PSGIの仕様は非常にシンプルですが、その上に構築されたPlackエコシステムは豊富で強力です。CPAN上には200以上のPlackミドルウェアが存在し、あらゆるニーズに対応できます。

Plack/PSGIを理解することは、モダンなPerl Web開発の基礎を理解することです。小さなスクリプトから大規模なWebアプリケーションまで、Plack/PSGIは常に選択肢に入る、頼れる基盤技術なのです。

参考リンク

Plack/PSGIは、PerlのWeb開発において避けて通れない重要技術です。ぜひ実際に手を動かして、その便利さを体感してください!

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。