OSSプロジェクトにLambCIを導入しようとした話

最近はLambStatusというOSSを作っているのですが、今回は、このプロジェクトでCIを回すためにLambCIを導入しようとした話です。結果は失敗に終わったのですが、つまづいた箇所とかが参考になるかもしれないので残しておきます。

LambCIとは

LambCIは、AWS LambdaベースのCIシステムです。〜CIというとCircleCIみたいなクラウドサービスっぽいですが、クラウドサービスではなく、自分で構築して運用していくものです。

以下のMediumの記事が一時期話題になったので、聞いたことがある方もいるかもしれません。

medium.com

なぜ今更そんな自前運用のCIシステムかというと、LambCIがいわゆるサーバレスアーキテクチャで作られていることが大きいと思います。Jenkins等のCIシステムを自前運用していくのはなかなか大変ですが、サーバレスなら自前運用の辛さをかなり軽減できるかもしれない、また、CI系のクラウドサービスはビルドの並列度を上げると料金が結構高くなりますが、サーバレスなら並列度を上げても全体の料金はほぼ変わらなそう、などのサーバレスを活かした特徴が注目されているようです。

より詳しい紹介やアーキテクチャについては、上記の記事を参照して下さい。

LambCIをOSSプロジェクトで使ってみる

今回LambCIを導入するOSSプロジェクトはJavascriptで作っていて、テストはkarma、mocha、electron等を利用して動かしています。実際のテストや設定はこちらから確認できます。

LambCIのインストールについては公式のREADMEに詳しく書かれているので省略して、設定ファイル.lambci.jsの作成から始めます。レポジトリのトップディレクトリに、以下のような設定ファイル.lambci.jsを作成しました。

module.exports = {
  cmd: 'cd packages/frontend && npm install && npm run test',
  build: true
}

GitHubにPushしたところ、AWS Lambdaでのビルド中に、以下のようなエラーが出ました。

f:id:ks888:20161119150257p:plain

末尾にError: ENOSPC: no space left on device, writeと書かれています。npm install中に、ディスクサイズが足りなくなったようです。ローカル環境で確認してみると、babelやelectron等のパッケージにより、依存パッケージだけで700MB程度のディスクを消費していました。AWS Lambdaの一時ディスク容量は512MBなので、これが原因のようです。

まぁディスク容量に限らず、AWS Lambdaには色々制限があります。LambCIは、AWS Lambdaの制限によりビルドがうまく行かないときは、ECS (EC2 Container Service)との組み合わせを勧めています。ディスク制限の回避は難しそうなので、素直にECSを使ってみます。

LambCIとEC2 Container Serviceを組み合わせて使ってみる

LambCIとECSを組み合わせる場合は、以下のレポジトリを利用します。

https://github.com/lambci/ecs

こちらはまだREADMEがあまり書かれていないので、導入手順を紹介します。

1.CloudFormationスタックの立ち上げ

こちらのテンプレートを使って、AWSコンソールからCloudFormationスタックを立ち上げます。これで、ECSクラスタ等が立ち上がります。今回はt2.smallインスタンスタイプを使いました。

2.CloudFormationテンプレートとスタックの更新

(ECSではなく)LambCI本体の立ち上げに使用したCloudFormationテンプレートを更新します。LambCIのGitHubサイトからテンプレートをダウンロードして、LambdaExecutionという名前のIAM Roleリソースに、以下のポリシーを書き加えます。

{
  "PolicyName": "RunECSTask",
  "PolicyDocument": {
    "Statement": {
      "Effect": "Allow",
      "Action": "ecs:RunTask",
      "Resource": "arn:aws:ecs:*:*:task-definition/lambci-ecs-BuildTask-XXXXXXXXXXXXX"
    }
  }

XXXXXXXXXXXXXの箇所は、Step.1で作成されたECSのタスク名で置き換えてください。

書き換えたテンプレートを利用して、AWSコンソールからLambCIのCloudFormationスタックを更新します。

3.設定ファイルの更新と追加

.lambci.jsに対して、ビルドに使用するクラスタ名などを以下のように指定します。

module.exports = {
  ... 既存の設定 ...
  docker: {
    cluster: 'lambci-ecs-Cluster-XXXXXXXXXXXXX',
    task: 'lambci-ecs-BuildTask-XXXXXXXXXXXXX'
  }
}

XXXXXXXXXXXXXの箇所は、Step.1で作成したECSのクラスタ名とタスク名です。

これに加えて、Dockerfile.testというファイルをレポジトリのトップディレクトリに作成します。これは、ビルド処理を動かすDockerイメージをビルドするためのファイルです。

今回は以下のようなDockerfile.testファイルを作成しました。

FROM node:4.3.2

RUN apt-get update && apt-get install -y xvfb x11-xkb-utils xfonts-100dpi xfonts-75dpi xfonts-scalable \
  xfonts-cyrillic x11-apps clang libdbus-1-dev libgtk2.0-dev libnotify-dev libgnome-keyring-dev \
  libgconf2-dev libasound2-dev libcap-dev libcups2-dev libxtst-dev libxss1 libnss3-dev \
  gcc-multilib g++-multilib libgconf2-4 gtk2-engines-pixbuf

ADD packages/frontend/package.json /frontend_build/

RUN cd /frontend_build && npm install

ADD . .

RUN rm -rf packages/frontend/node_modules && mv /frontend_build/node_modules ./packages/frontend

CMD cd packages/frontend && xvfb-run -a --server-args="-screen 0 1024x768x24" npm run test

キャッシュを効かせるためにpakcage.jsonをごにょごにょしているため少しややこしいですが、基本的にはnpm installnpm run testをしているだけです。これに加えて、非GUI環境でelectronを動かすため、aptで色々入れています。

実際の設定ファイルは、こちらで確認できます。

これらのファイルをGitHubにPushすると、ECS上でのビルドが実行されます。なお、S3上のビルド結果画面には以下のようなログしか表示されないため、ビルド結果はCloudWatch LogsのECSのログで確認します。

f:id:ks888:20161119151548p:plain

初回は20分ほどかかりましたが、無事にビルドが成功しました。t2.smallインスタンスタイプを利用したので仕方ないと思いますが、同等のビルドをCIサービスのwerckerで動かすと7分ほどで終わったので、ちょっと遅めです。

LambCI導入をやめた理由

ここまでは進めたのですが、しばらくはクラウドサービスを使おうかな、と思っています。理由としては以下です。

  • ECS上でビルドした結果がS3に反映されない

    ECSを利用しない場合、ビルド結果はS3に渡されるため、S3を通してビルド結果を他の開発者と共有できます。同様にビルド結果を示すバッジ画像も、S3を通してREADME等に表示できます。しかしECSを利用すると、ビルド結果がS3に反映されないため、ずっとビルド中の状態になってしまいます。ビルド結果はCloudWatch Logsからわかるのですが、CloudWatch Logsの閲覧権限をパブリックに共有するのは気が進みません。

  • Lambdaの代替としてECSを使いたくない

    今回はLambdaの代わりにECSを使おうとしましたが、Lambdaに期待することをECSで実現するのは無理があると感じました。

    例えば、今回は最初t2.microインスタンスクラスタを立ち上げたのですが、これだとビルドが何回繰り返しても通りませんでした。インスタンスの性能が足りないのか思い、試しにt2.smallにしてみたところ、通りました。また、クラスタを立ち上げる度にECS Agentが古いから更新しろと警告してきます(更新は数クリックで済みますし、AMIを適切に作り直せば警告は消えると思いますが)。こういう問題は、始めからECSを利用するつもりなら何でもない話ですが、Lambdaを使うつもりだった立場からすると、ドウシテコウナッタ感があります。

    また、LambCIは、ビルド並列度を上げても全体の料金がほぼ変わらないところや、ビルドが実行されてないときは料金がほぼかからないところが利点です。しかし、ECSだとその恩恵にあずかるのは難しくなります。オートスケールを組み合わせれば多少ましになるかもしれませんが、それでも、1時間単位課金と100ms単位課金ではだいぶ料金が変わってくる気がします。

1つ目はそれほど大きな理由ではないし、回避策もあると思うのですが、2つ目の理由は結構厳しいと思い、導入をやめました。Lambdaの制限が緩和されて、ECSなくてもいけそう!と思えた段階で、改めて導入しようかと思います。

ちなみに、今はwerckerを使っています。こんな設定ファイルを書くといい感じにテストしてくれます。