Codebuildで安く楽する話


この記事は Progate Advent Calendar 2017 の11日目です。
今日はJenkins上のCIをAWSのCodebuildに移して幸せになった話をしようと思います。

前提

現在のProgateでは、コンテンツの制作にリソースの多くを注いでいるため、コードのコミット自体は多い方ではありません。
しかし、コミットの時間帯が集中したり、他のJenkinsタスクが走る時間帯とかぶると、なかなかテストが通らないことがあり、エンジニアから苦情が出る、ということが起こります。

Jenkinsのしんどいところ

弊社でもつい最近まで、m4.xlarge上のJenkinsを使ってPull Requestにテストを走らせていました。
機械的なチェックで、linterやユニットテストなど、最低限の品質を担保する上で大いに役立っていたのですが、課題もありました。

いくつか例を挙げると

  • スペック的にもうテストが足せない…
  • テスト項目同士の依存関係の管理が大変
  • ディスク容量との兼ね合いで、数日経つとビルド結果が消える
  • Jenkinsが混むと、なかなかビルドが始まらない
  • 長時間実行しているJenkinsでなぜかエラーになる
    • メモリが足りなくなったりDNSエラーで git pull が落ちてたり…
  • etc...

CI as a Service なソリューションは高い

これらの解決策として、一番に思いつくのは Circle CI や Travis CI などの活用です。

しかし、Pricing のページを見るとわかるように、これらのサービスは 高い!

たとえば Circle CI だと、4人が同時にCIをまわそうとすると 1万5千円もします

「問題をカネで解決して時間を買う」という考え方をするなら妥当な選択肢ではあるのですが、常時なにかしらコミットがなされている、というわけでもないので、ちょっと勿体無い感じがしますよね。

そこでCodebuildですよ

今回、弊社のCIで「つらい」部分をまとめると次のようになりました。

  • 実行環境のスペックを気にせず、必要なテストを足したい
  • ただし実行時間が伸びるのはNG
    • 並列にスケールしてほしい
  • 「なぜかエラーになる」というのはNG
  • CI項目はリポジトリ側で管理できる
  • 結果はいつでも見られる

Codebuild は去年の re:Invent で発表されたもので、Dog Year なこの業界においてはそこまで新しい選択肢でもないのですが、今回の弊社のユースケースにおいてはぴったりでした。

Codebuildの特徴を簡単にまとめると、以下のような感じになります。

完全マネージド型

実行環境に関してほとんど気を使わなくてよくなります。

従量課金

深夜などの待機時間には課金されません。

実行環境はDocker

dockerのイメージ化しておけば、特殊なビルド環境でも利用できます。

今回Progateでは利用していませんが、ビルドの成果物をS3などに置くこともできます。

Codebuild の使い方

GitHub上のリポジトリに対して、Codebuildで何か実行しようとすると、一般的には次のようになるかと思います。

  1. AWSのコンソールからCodebuildにプロジェクトを作る
  2. プロジェクトにリポジトリを登録する
  3. リポジトリの適当なところに buildspec.yml を置く
  4. どうにかして aws codebuild start-build --project-name={プロジェクト名} を叩く

buildspec.yml はS3に置いたりAWSのコンソールからCodebuildのプロジェクトに登録することもできますが、CI項目をリポジトリ側で管理することを考えると取りづらい選択肢でしょう。

また、Circle CI や Travis CI のように「git push に反応して自動でビルドする」といった機能は現在のところ提供されていないので、自前でなんとかする必要があります。

実装方針

今回は、Lambda で解決することにしました。

GitHub PUSH -> Amazon SNS -> Lambda

の流れで実行して、

  • start-build を叩く動作
  • GitHub側に Commit Status (pending) をつける動作

を Lambda にやらせます。

GitHub と Amazon SNS の連携は こちら が参考になります。

また、ビルドの終了をCloudWatch Eventで拾って、そこから別の Lambda にやらせます。

Cloudwatch Event -> Lambda

ここで、success または failure の Commit Status をつけます。

実行時間・並列度について

buildspec.yml 中に

build:
  commands:
    - bundle install
    - npm install
    - rspec
    - eslint
    - flow
    - ...

と書き連ねても良いのですが、これだと、項目が増えるごとにCI時間が長くなってしまい、あんまり幸せになれないです。

さいわい Codebuildでは、プロジェクト作成時だけでなく、start-build の際にも buildspec.yml について設定することができます。

そこで、Progateでは、各 buildspec.yml の実行時間が大体同じになるよう、リポジトリ上の buildspec/ の下にファイルを複数用意することにしました。

用意した buildspec.yml たちの一覧は、GitHub API を使えば次のように取得できます。

curl \
 -u {username}:{token} \
  'https://api.github.com/repos/{owner}/{repository}/contents/{path_to_file_or_directory}?ref={branch}'

あとは各 buildspec.yml について、aws codebuild start-build してやれば、無事に望みのものが手に入るはずです!

実行結果は https://${region}.console.aws.amazon.com/codebuild/home?region=${region}#/builds/${id}/view/new で取得できます(コンソールへのアクセス権が必要です)

実行時間について

docker pull 以後の動作時間については課金対象としてカウントされるので、bundle install やら npm install やらは buildspec.yml の中でやりたくないですよね。

実行環境で使用する docker イメージは指定できるので、こういった準備を事前に済ませたイメージをビルドして、ECRに入れて置くのが良さそうです。

移行した結果

冒頭で挙げた、「Jenkinsのしんどいところ」はほぼ解消されました。

  • コンテナのプロビジョニングにかかる時間で多少前後するものの、おおむね5〜10分でテスト結果が出るようになった
  • ビルド結果が消える、ディスクが溢れる、ときどきエラーになる、といった煩わしさから解放された
  • CI項目を buildspec.yml としてコード上で管理できるようになった

課題

CodebuildのLimitにも書いてあるのですが、同時に実行できるビルドの数には上限があります。

節操なく start-build しまくって上限を超えてしまうと、ビルドが失敗します。

とりあえずQueueに突っ込んでよしなに処理する、みたいな機能はCodebuild側には用意されていませんので、そこも自前でやる必要があります。

最後に

プロダクトのフェーズが変わってコミットが増えてくるとまた状況が変わるかとは思いますが、現状では、安くて便利なビルド環境が確保できて生産性が向上しました。

コードがほとんど公開できないために味気ない記事になってしまいましたが、ご興味を持たれた方は是非試してみていただけると幸いです。