Shogo's Blog

Dec 16, 2018 - 3 minute read - Comments - perl aws lambda

AWS LambdaでCGIを蘇らせる

この記事は Perl Advent Calendar 2018の15日目の記事です。 (キリの良いところまでできたのと、記事が書かれていなかったので代打投稿)


Custom Runtime のリリースにより、AWS Lambda 上でPerlが動くようになりました。

ということで、やっていきましょう。

できたもの

動かすのはもちろん、 CGIアクセスカウンター 。 なんと嬉しいことに、最近になって WwwCounter の新バージョン(Ver3.16)がリリースされ、 Perl 5.26 に対応しました!

2018-11-11 perl 5.26に対応。(Ver3.16)

更新履歴によれば一つ前の Ver 3.15 のリリースは2003-03-23なので、なんと15年ぶりのアップデートです。 杜甫々さんの AWS Lambda で動かしてくれ!! という声が聞こえてきそうですね・・・!!!

CGIが動作する様子

動いたーーーー!!!!

実装はこちら


ちなみにWwwCounterのアップデートはPerl 5.26で「@INCからカレントディレクトリが削除」された件への対応だと思います(コミットログがないので予想)。

実装説明

「そもそもCGIってなんだ?」っていう人も多くなってきたと思うので、そこらへんの歴史の話にも軽く触れます。 この辺の歴史をリアルに体験したわけではないので、誤り等あればご指摘ください。

CGIとは

Common Gateway Interface の略で、 WebサーバーとCLI(Command Line Interface)アプリケーションのやり取りの方法を決めた規格です。

CGIができたのは1993年。 Perl(1987年登場)やPython(1991年登場)といった、2018年現在ではWebアプリケーション記述言語として主流となった言語たちは、 まだまだできたてホヤホヤな時代です。 Ruby on Railsで一躍有名となった Ruby (1995年登場) に至ってはまだ登場すらしていません。 (ちなみに1993年当時筆者5歳・・・CGIのほうが若かったのか・・・)

そんな当時のプログラミング言語たちには自力でHTTPをしゃべる能力はありませんでした。 そんな時代に生まれたのがCGIです。環境変数と標準入出力さえ扱うことができれば、どんなプログラミング言語でもWebアプリケーションを開発できます。

以下はC言語で書いた「Hello CGI」を書かれたWebページを返すだけの簡単なCGIプログラムの例です。

#include <stdio.h>

int main() {
    printf("Content-type: text/html\n\n");
    printf("<!DOCTYPE html>\n");
    printf("<html>");
    printf("<head><title>Hello CGI!</title></head>");
    printf("<body>Hello CGI!</body>");
    printf("</html>");
    return 0;
}

使っている関数は printf だけ。とても簡単ですね!

とはいえ少し複雑なことをしようとすると文字列処理が必要となり、C言語だけで正しい文字列処理を行うのは大変です。 また、開発環境のOSとサーバーのOSとが違った場合、クロスコンパイルが必要となるため反映作業が煩雑となります。 そんな中スクリプト言語としては少し先輩だった Perl が CGI の記述言語として主流となっていきます。

画像式CGIアクセスカウンター

今で言うGoogle Analyticsのようなアクセス解析サービスの超簡易版といったところでしょうか。

1990年台のJavaScript(1995年登場)はまだまだ普及段階で、すべての閲覧者の環境でJavaScriptを使えるとは限りませんでした。 (JavaScriptが大きな注目を集めるようになるのは、2005年にAjaxという言葉が登場するまで待たなければなりません。) そのため、今のような解析用のJavaScriptを埋め込む形式には限界があります。 そんななか注目されたのが、静的なページにも手がるに埋め込むことができる画像です。

IMGタグを埋め込むだけでアクセス数がわかるので、アクセス解析の手法として 「そうこそ!あなたは〇〇人目の訪問者です!!」の文言をトップページに置くのが流行りました。 (きっと懐かしいと思う人がたくさんいるはず)

画像式CGIアクセスカウンターとGIF

当時の画像式CGIアクセスカウンターではGIFが主に使われていました。 というのも後発のPNGはまだまだ普及率が低く、PurePerlでJPEGのエンコーダーを実装する酔狂な人はいなかったからだと思います(たぶん)。

しかしGIFにも全く問題がないわけではなく、特許に関する問題がありました。 GIFのエンコードに使われているLZWは Unisys社が特許を持っており、GIF画像を扱うソフトの開発に使用料を取っていたのです。

これに対抗してネットの民たちは、GIFのアニメーション機能を匠に使って LZW エンコードをしないで、 GIF画像の編集を行うハックを開発しました。 そのハックを利用して作られた、代表的なアクセスカウンターが最初に出てきた WwwCounter です。

ちなみに作者の杜甫々さんは、90年代後半から2000年代のウェブ制作者の間では結構有名な人です。 少なくともインタビュー記事が書かれるくらいには(元記事は消えてしまってアーカイブしか見つからなかった・・・)。

API Gateway/ALB のイベントを PSGI に変換する

さて、歴史の話はこれくらいにして、時間を現代に戻しましょう。 CGIは言語を問わないとても汎用性が高く便利な仕組みでしたが、インターネットが普及するにつれパフォーマンスが問題となってきました。 そこで言語毎にWebサーバーとのより高速なインターフェースが作られるようになります。 Perlの世界では PSGI がそれに当たります。

PSGIではWebアプリケーションを関数の形で定義します。 例えば、以下は “hello, world” と返す簡単なWebアプリケーションです。

my $app = sub {
    return [200, ['Content-Type' => 'text/plain'], ["hello, world\n"]];
}

入出力の形式は PSGIの仕様 で定義されています。

一方 API Gateway/ALB のイベントの形式は AWSの公式ドキュメントに記載されています。

イベントはJSON形式なので、それをうまいことPSGIのインターフェースに変換します。

PSGIをCGIに変換する

PSGIもCGIもHTTPをやり取りするためのインターフェースなので、相互に変換できます。 これに関してはPSGIの公式リファレンス実装である Plack が変換モジュールを用意してくれているので、それを利用します。

自分でも動かす

ビルド済み Docker Image を使う

lambci/lambda をベースにビルド済みPerlを組み込んだDockerイメージを公開しました。

使い方は lambci/lambda と同様です。 Perl の依存モジュールのインストールや、イベントの実行などを、手元の環境で行うことができます。

# 依存モジュールをインストールする
docker run --rm -v $(PWD):/var/task shogo82148/p5-aws-lambda:build-5.28 \
        cpanm --notest -L extlocal --installdeps .

# イベントを実行する
docker run --rm -v $(PWD):/var/task shogo82148/p5-aws-lambda:5.28 \
        handler.handle '{"some":"event"}'

ビルド済みイメージには API Gateway/ALB のイベントを PSGI に変換するモジュールも同梱してあります。 以下のコードを追加するだけで、既存の PSGIアプリケーションが API Gateway や ALB をかえして AWS Lambda 上で動くようになります。

use utf8;
use warnings;
use strict;
use AWS::Lambda::PSGI;

my $app = require "$ENV{'LAMBDA_TASK_ROOT'}/app.psgi";
my $func = AWS::Lambda::PSGI->wrap($app);

sub handle {
    my $payload = shift;
    return $func->($payload);
}

1;

ビルド済みの 公開 Lambda Layer を使う

ビルド済みの AWS Lambda Layer も用意しました。 新規レイヤーと追加するときに「Provide a layer version ARN」を選択し「Layer version ARN」に以下のARNを入力してください。 (ちなみに ap-northeast-1 の 5.26 だけバージョンが4なのは、デプロイスクリプトのミスです。もとに戻せないの悲しい。)

  • Perl 5.28
    • arn:aws:lambda:ap-northeast-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:ap-northeast-2:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:ap-south-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:ap-southeast-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:ap-southeast-2:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:ca-central-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:eu-central-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:eu-west-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:eu-west-2:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:eu-west-3:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:sa-east-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:us-east-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:us-east-2:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:us-west-1:445285296882:layer:perl-5-28-runtime:3
    • arn:aws:lambda:us-west-2:445285296882:layer:perl-5-28-runtime:3
  • Perl 5.26
    • arn:aws:lambda:ap-northeast-1:445285296882:layer:perl-5-26-runtime:4
    • arn:aws:lambda:ap-northeast-2:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:ap-south-1:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:ap-southeast-1:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:ap-southeast-2:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:ca-central-1:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:eu-central-1:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:eu-west-1:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:eu-west-2:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:eu-west-3:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:sa-east-1:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:us-east-1:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:us-east-2:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:us-west-1:445285296882:layer:perl-5-26-runtime:3
    • arn:aws:lambda:us-west-2:445285296882:layer:perl-5-26-runtime:3

ビルド済みの zip アーカイブを使う

以下のURLにビルドしたzipアーカイブを置きました。${REGION} に使用しているリージョンを入れてご使用ください。

  • https://s3-${REGION}.amazonaws.com/shogo82148-lambda-perl-runtime-${REGION}/perl-5-28-runtime.zip

東京リージョンの場合は以下のようになります。

新規レイヤーを作る際に「Upload a file from Amazon S3」を選択し、このURLを入力すれば使えるようになります。

既知の問題

  • カウンターの値が永続化されない
    • AWS Lambda では /tmp にしか書き込み権限がないので、先の動作例ではここにカウンターの値を書き込むよう修正をしました。
    • /tmp なのでもちろん永続化はされません。放置しておくと0リセットされます。
  • CGI::Compile が使えない
    • Perl製のCGIスクリプトをPSGIスクリプトに変換するという闇モジュールなのですが、AWS Lambda内では $0 を操作する部分で死にます。
    • 「Can’t set $0 with prctl(): Operation not permitted」だ、そうです
  • API Gatewayでレスポンスにバイナリを含むことができない
    • ALBはリクエスト、レスポンスともにBodyにバイナリを含むことができます
    • リクエストに関してはAPI Gatewayでも「Binary Media Types」にメディアタイプを追加することで送信できました
    • Enable Binary Support Using the API Gateway Console

まとめ

AWS Lambda 上で CGIアクセスカウンターが動きました。

あとは CGI::Compile が動いてくれれば、forkのコストを気にする必要がなくなるので、CGI+Perlを使った開発がはかどりますね! (その場合、結局最後はPSGIアプリケーションに変換されるんだけど、気にしない気にしない)


16日目は @magnoliakさんで「Time::Pieceを使って日付の計算をしようとしてハマった話をします」です!

参考