Movable Type Serverless - LambdaコンテナでMTを動かしてみた

Movable Type Advent Calendar 2020 12日目です。

つい先日、AWS LambdaがDockerコンテナをサポートしました。

これはアツい！ということでMTを(途中まで)動かしてみた、という話です。

# Movable Type Serverless

最終的にはこんなイメージでいます。

![mt-serverless-arch2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/69900834-66ca-3184-5558-e79453dc193f.png)

MTの静的出力という特徴を活かし、Webサーバーだけ常時起動させますが、MTは普段存在すらしておらず、編集者が使うときだけ起動します。

* 夜中や休日など編集者が**使わない時間はコンピューティングコスト節約！**
* 利用者が増えたらRDSやEFSの限界まで**事実上無限に性能拡張！**
* もし再構築を並列で実行できたら時間を圧倒的に短縮！

なにこれすごい！

# とりあえずログインできるところまでやってみた

今回はRDSと連携してログインできるところまでやってみました。

![mt-serverless-trial2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/f62c3f12-6077-ffbb-eeb6-e329ddc4d168.png)

* EFSとの連携以降はやってません。
* セキュリティ考慮していません。
* 日本語だと文字化けしたので**とりあえず英語**で動かします。

とまあ、PoCレベルなので誰か遺志を継いでください。

## 手順1 RDSインスタンスを起動

RDSにMT用のMySQLデータベースインスタンスとユーザーを作成します。

一時的な実験なのでセキュリティは考慮していません！ 随時読み替えて適切に設定してください。

```sql
create user 'mt_serverless'@'%' identified by '(データベースのパスワード)';
grant all on mt.* to 'mt_serverless'@'%' with grant option;
flush privileges;
```

## 手順2 MTのプログラム一式を展開する

ここからはRDSに接続可能な環境で作業します。

開発者ライセンスでMTを入手します。

https://www.sixapart.jp/inquiry/movabletype/developer.html

## 手順3 mt-config.cgi

MTを展開したディレクトリ直下に設定ファイル`mt-config.cgi`を作成します。

※ 日本語が文字化けしちゃったので英語UIに逃げます。

```perl:mt-config.cgi
CGIPath        /mt/
StaticWebPath  /mt/mt-static/
StaticFilePath /app/mt-static

ObjectDriver DBI::mysql
Database mt
DBUser mt_serverless
DBPassword (データベースのパスワード)
DBHost (RDSインスタンスのホスト名)

EmailAddressMain (管理者メールアドレス)
    
DefaultLanguage en

ImageDriver Imager
```

## 手順4 Dockerfile

ひとまずplack/PSGIで動かすため、MTを展開したディレクトリ直下にこんな`Dockerfile`を作ります。

個人的に慣れてるUbuntuを使っていますが、ディストリビューションはお好きにどうぞ。



```dockerfile:Dockerfile
FROM ubuntu:20.04

RUN apt-get update \
  && apt-get install -y perl cpanminus build-essential \
    libdbi-perl libdbd-mysql-perl \
    libimager-perl libxmlrpc-lite-perl \
    libplack-perl libcgi-psgi-perl \
  && cpanm XMLRPC::Transport::HTTP::Plack

RUN cpanm AWS::Lambda

RUN apt-get -y remove build-essential \
  && apt-get -y clean \
  && apt-get -y autoremove \
  && rm -rf /var/lib/apt/lists/* /var/cache/apt/*

ADD . /app
WORKDIR /app

CMD [ "plackup", "mt.psgi" ]
```

ここでインストールしているナイスなモジュール`AWS::Lambda`が、`Lambda - Dockerコンテナ - PSGIアプリケーションとしてのMT`を繋いでくれます。

https://github.com/shogo82148/p5-aws-lambda

## 手順5 PSGI版で動作確認兼DBセットアップ

```bash
docker build -t mt-serverless .
docker run -it --rm -p 5000:5000 mt-serverless
```

ブラウザから、`http://localhost:5000/mt/mt.cgi` (localhostは作業環境に合わせて)にアクセスすると、セットアップウィザードが開始されます。

![Create_Your_Account___Movable_Type_Pro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/c1d982f2-c10a-3f24-b767-39158d1d5031.png)

文字化けの問題で、`Language`は`English`でひとまず進めてください。

![Initializing_database______Movable_Type_Pro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/e90dfa5e-dddc-6900-7604-2261922e3fed.png)

データベースのセットアップができたらOKです。

## 手順6 コンテナ用エントリーポイント `bootstrap`

### コンテナサポート ≒ カスタムランタイム

AWS Lambdaのコンテナサポートは、2年前にリリースされた`カスタムランタイム`のプロトコルを踏襲しています。

カスタムランタイムはHTTPによりLambdaサービスと連携しますが、コンテナも同様です。なのでコンテナの情報はまだ少ないのですが、カスタムランタイムについて調べると大抵の問題は解決できます。

この試みでもAWS Lambdaカスタムランタイム用のモジュール`AWS::Lambda`を大いに活用します。

https://github.com/shogo82148/p5-aws-lambda

MTのディレクトリ直下に`bootstrap`を作成し、実行権限を付与します。

```perl:boostrap
#!/usr/bin/perl

use strict;
use AWS::Lambda::Bootstrap;
use File::Basename;

# Lambdaサービスから渡されそうだが…
$ENV{'LAMBDA_TASK_ROOT'} = dirname(__FILE__);

# Lambdaにハンドラの入力欄がないからこちらは来ないかも？
# AWS::Lambda向けに固定
$ENV{'_HANDLER'} = 'mt-lambda.handler';

my $bootstrap = AWS::Lambda::Bootstrap->new;
$bootstrap->handle_events;
```

カスタムランタイムと違い、名前は`bootstrap`でなくてもよいのですが慣習に沿います。

```bash
chmod +x bootstrap
```

## 手順7 Lambdaコンテナ用MTエントリーポイント `mt-lambda.pl`

次に`bootstrap`から呼び出されるMTのエントリーポイント`mt-lambda.pl`を作成します。

```perl:mt-lambda.pl
use strict;
use lib $ENV{MT_HOME} ? "$ENV{MT_HOME}/lib"    : 'lib';
use lib $ENV{MT_HOME} ? "$ENV{MT_HOME}/extlib" : 'extlib';
use MT::PSGI;
use AWS::Lambda::PSGI;
my $app = MT::PSGI->new()->to_app();
my $func = AWS::Lambda::PSGI->wrap($app);

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

1;
```

このファイル名と`sub handler`の名称は、下記のように`bootstrap`の記述と関連しているので変更する場合は併せて行う必要があります。

```perl
$ENV{'_HANDLER'} = 'mt-lambda.handler';
# mt-lambda → mt-lambda.pl
# handler → sub handler
```

最初は`bootstrap`ファイル内ですべて完結させる予定でしたが、`AWS::Lambda`モジュールの実装によりそれが難しかったので`bootstrap`と`mt-lambda.pl`に分かれています。

## 手順8 ECRにプッシュ

ECR上のリポジトリにイメージをプッシュします。細かい説明は省略します。

```bash
docker tag mt-serverless (固有のID).dkr.ecr.ap-northeast-1.amazonaws.com/mt-serverless:latest
docker push (固有のID).dkr.ecr.ap-northeast-1.amazonaws.com/mt-serverless:latest
```

## 手順9 Lambdaを作成

コンテナイメージを元にLambda関数を作成します。

![Lambda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/30c66338-cae1-89f6-401d-f0fcece65738.png)

今のコンテナイメージは`plackup`でPSGI版のMTを起動するので、`CMD`にLabmda用のエントリポイント`/app/bootstrap`を指定します。

作成後にLambdaのメモリとタイムアウトを変更します。

適切な設定は要検討ですが、とりあえずタイムアウト3秒は短いので30秒にしました。

![Lambda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/b496131c-e440-73c9-ae8e-74b77a9836fd.png)

## 手順10 Elastic Load Balancerの作成

やっと終わりが見えてきました…

Application Load Balancerを作成してLambda関数を割り当てます。

* Application Load Balancer (HTTP/HTTPS) を作成します。
* リスナーは必要に応じてHTTPSを追加してください。
* サブネットはふたつ必要です。

![ロードバランサーの作成___EC2_Management_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/2ae3575f-a235-e749-9608-52d4ab1115f4.png)

次に証明書とセキュリティグループを選択します。

セキュリティグループは当然、ポート80と443でのインバウンド接続を許可していください。

新しいターゲットグループとして、種類=Lambda関数を選択します。

![ロードバランサーの作成___EC2_Management_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/b7b01386-c1e1-b011-b989-366554afe55a.png)

最後に作成したLambda関数を選択します。

![ロードバランサーの作成___EC2_Management_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/09ed7e23-1a8d-f17d-bab1-2c430784f993.png)

# ついにMovable Type Serverlessにアクセス

DNSの反映に少し時間がかかるようですが、ブラウザで次のURLを開くと…

http://(ロードバランサーのDNS名)/mt/mt.cgi

き…きたー！ MTのログイン画面！

![Sign_in___Movable_Type_Pro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/c6644f07-fcfa-434e-929e-5cc35f7723dc.png)

セットアップ時に登録したユーザーでログインすると…

![Dashboard___Movable_Type_Pro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35178/b8d2dda9-99c4-ed38-e04b-e4ee956f2f28.png)

ダッシュボードだー！

しかも意外と動作が軽快。

PSGIをインターフェースとして使っていますが、ライフサイクルとしてはリクエストごとにプロセスが発生するので、挙動としてはCGIに近いはずです。もしかしたらLambdaサービスはPSGI的にプロセスがリクエスト間で使い回しているかもですが。

でもコンテナを読み込むオーバーヘッドは感じられず、実用性も期待できます。

## 続きを期待します

他の動作確認はしていません。そもそもEFSなどでファイルシステムを永続化しないとサイトも公開できません。

でも使った分だけ課金、並列爆速再構築など、期待が膨らみますね。

あなたがここまで読んだということは、私はすでに力尽きているだろう。ここから先のチャレンジは、あなたに道を譲ろうと思う。検討を祈る！