AWS S3+SQS+Lambda で格安でセキュアなジャッジシステムを作ろうとして一ヶ月溶かした

[AWS Advent Calendar 2015](http://qiita.com/advent-calendar/2015/aws) の14日目の記事です。

競技プログラミングで使われるジャッジシステムをAWS上に構築しました。が、あまり上手く行きませんでした。

## <i class="fa fa-check"></i> TL;DR

* ジャッジシステムをAWSのサービスを組み合わせて構築できた
* コードのコンパイルはフロントエンドのEC2が担当し、実行可能ファイルはLambda上で走らせる。実行結果のやりとりはS3で行う。
* 良い点
 * 安価。Lambdaは実際に動いた分だけ課金されるので、ユーザからのコードの提出がなければお金がかからない。
 * セキュア。Lambdaはサンドボックス環境が整備されているのでシステムを壊される心配がない。
* 悪い点
 * CPUの性能が実行の度にバラける。
 * 固定のAMIを利用することになるため、スクリプト言語実行時の環境指定が不自由。

## <i class="fa fa-book"></i> ジャッジシステムって？

ジャッジシステムとは、プログラミングコンテストにおいて参加者の提出したプログラムを実行し、自動で採点をするシステムです。採点は、ある種のユニットテストによって行います。仮想環境上でプログラムを実行させ、予め決められた入力に対して正しい答えが出力されることをチェックするのです。

![judge.png](https://qiita-image-store.s3.amazonaws.com/0/21141/9e9d332d-bddc-5da0-15ae-4b27c1c65639.png)


一般に、ジャッジシステムは以下の条件を満たす必要があります。

* セキュリティの担保 : ユーザーから送りつけられる任意のプログラムを実行しなければならないという性質上、悪意のあるコードから環境を守らなければいけません。
* 公平性の担保 : 実行するプログラムは一般にCPUとメモリを食います。問題によって実行制限時間が付いているので、他のプロセスの影響を受けて遅くなった、ということがあってはいけません。

## <i class="fa fa-gears"></i> 仕組みの解説

試作したシステムにおいて、ユーザがコードを提出してから何が起こるかを順番に説明します。

![aws0.png](https://qiita-image-store.s3.amazonaws.com/0/21141/fd506207-51bd-767d-dae1-b55aba487500.png)

### 1. 解答プログラムのサブミットとコンパイル

ユーザからPOSTリクエストで解答プログラムを受け取り、それをEC2上でコンパイルします。生成された実行可能ファイルを S3<プログラム置き場> にアップロードします。

### 2. <テストケース生成屋>のLambda関数で並列実行の準備を行う

プログラム置き場のS3に特定のファイルが置かれると、それに反応して <テストケース生成屋>のLambda関数が発火します。

こいつは問題ごとのテストケースを準備して、SQSに送ります。SQSのメッセージは、

* 解答プログラム(実行可能ファイル)のパス
* 採点用プログラムのパス
* 標準入力のパス

から成っており、これ単体が問題に対する一つのテストケースに相当します。
SQSにメッセージを投げたら、10個ほど <ジャッジ屋> のLambda関数を発火させます。<ジャッジ屋> は SQSをポーリングして、実際に採点を行う機能を持ちます。

これでテストケースを実行する準備ができました。

### 3. <ジャッジ屋>のLambda関数で実行ファイルが走る

<ジャッジ屋>のLambda関数の中身はこんなふうになっています。

```
1. SQSに未処理のメッセージが残ってないか調べる。
2. もしあれば、そのうちの一つを取得する。なければ終了。
3. メッセージに書かれている情報を見て、以下を<プログラム置き場>のS3からダウンロードする。
 3.1 提出された実行可能ファイル
 3.2 採点プログラム
 3.3 採点プログラムに与える入力
4. 採点プログラムを走らせる。
5. 実行が終わったら、点数を <結果置き場>のS3 に置く。
6. メッセージを削除する。
7. 未処理のテストケースが残ってそうだったら、自身を再度発火する。
```

2〜6番までが、一つのプログラムで一つのテストケースを実行している部分です。実行するプログラムとテストケースはSQSの中身だけに依存するので、Lambda関数を発火する数を増やせばテストケースが並列で処理され、ジャッジが高速になる仕組みになっています。また、関数の実行が途中でこけてもSQSにメッセージが残るので、望む回数だけリトライが可能です。

ここで運用上注意すべき点がいくつかあるので補足です。

#### ☆4.の実行時間について

外部プログラムの実行には Node.jsの child_process というコマンドを用いていますが、これは 60秒経つと強制終了します（これはNode.jsの仕様でしょうか）なので、Lambda関数全体の実行時間は余裕を持って設定しておきましょう。これで実行時間が長すぎるor無限ループするプログラムに対処可能です。

#### ☆同時に動かせるLambda関数の上限

[ドキュメント <i class="fa fa-external-link"></i>](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/limits.html) によると、(関数の平均実行時間) と 同時実行数が 100 を超えると制限がかかるそうです。この上限を超えないよう、関数の発火を抑える仕組みが必要です。

### 4. 結果置き場を見て、実行結果を教える

ユーザから結果クエリが飛んできた場合、結果置き場のS3を見に行ってその内容を教えてやります。

## <i class="fa fa-exclamation-triangle"></i> 問題点

本システムには、以下の問題点があります。

### CPUの性能が実行の度にバラける

ベンチマーク用のコードを何度が走らせてみたのですが、実行結果が安定しません。
回避策は、何度か実行してみて一番良い結果を採用するぐらいですかね・・・

### Lambda関数上で固定のAMIを利用することになるため、スクリプト言語実行時の環境指定が不自由

特定の言語のバージョンを上げたいとき、困りそうですね・・・

## <i class="fa fa-check"></i> 結論

現在のAWS Lambdaの仕様では実用に耐えうるジャッジシステムを作るのは難しいということが分かりました。が、手元のコードを100ケースほどさくっと実行したいときは便利なので、しばらく自分だけで使っていこうと思います。

もし本格的に組みたい場合は素直にDocker on EC2を使ったほうがいいと思います。

## <i class="fa fa-book"></i> 参考資料

* [AWS Lambdaをいろいろ暴く](http://qiita.com/Keisuke69/items/9951a93fd711360a61c5)
* [paizaのオンラインジャッジを支えるDockerとその周辺 <i class="fa fa-external-link"></i>](http://www.slideshare.net/paiza_official/paizadocker)
* [Imo Judge を支える技術  <i class="fa fa-external-link"></i>](http://imoz.jp/note/onlinejudge.html)