SREチームの長田です。 Tech KAYAC Advent Calendar 2019 4日目の記事です。
今回はLobiで長らくCI実行環境として使用していたJenkinsから、CircleCIに移行したはなしです。
Jenkins時代
皆様御存知のJenkinsです。 LobiではCI実行のために使用していましたが、ジョブ実行を定型化する汎用的なアプリケーションです。
GitHub上のリポジトリへのpushをトリガーに、対象branchについてCIを実行する、という使い方をしていました。 動作環境はAmazon EC2で、Jenkins本体が稼働しているmaster(x1)と、実際にCIを実行するslave(x2)の3台構成でした。
Jenkins時代もCIするという目的は達成されていたのですが、大きく分けて2つの課題がありました。
メンテナンス
Jenkinsもひとつのアプリケーションなので、アップデートがあれば対応が必要です。 Jenkins自体に変更がなくとも、稼働しているEC2のインスタンスタイプが古くなったり、 OSのアップデートがあったり、セキュリティ対応があったりと、何かと手がかかります。
CI環境が快適に使えるようメンテナンスすることはもちろん意味のあることです。 CIが信用できなくなると開発スピードがだだ下がりしてしまいますからね。
とはいえ、自分たちのビジネスにおいて注力したいのは、CI環境のお世話ではなくて顧客に提供するサービスであるはずです。 Jenkins緊急メンテイベントがあるたびに「もっとやりたいことがあるんだけどなぁ」と思いながら作業することになるわけです。
待ち時間
CIには時間がかかります。 テストをひとつずつ実行した場合、とても現実的な所要時間には収まりません。 実際にはEC2のm4.16xlarge(64コア)インスタンス上で64並列でテストを実行して、ようやく10分を切るか切らないかくらいのテスト量でした。
テストを実行するslaveは2台しかないので、3人が同じタイミングでリポジトリに変更をpushするとCI開始までに待ち時間が発生します。 Lobiでは頻繁にデプロイを行うため、待ち時間が長いのは問題です。 早く不具合修正をデプロイしたいのにCIが完了するのは30分後、なんてこともありました。 実行中のジョブをキャンセルして不具合修正branchのCIを優先することはできますが、その後のリトライ操作が必要だったりして煩雑なのです。
slaveを増やせば待ち時間はある程度解消しますが、その分EC2の使用量がかかります。 必要に応じてslaveを増減させるという手もありますが、その仕組みのメンテナンスは誰がやるんだという話もあり結局採用されませんでした。
また、1つのslave上で並列数を上げる方法は、EC2インスタンスのコア数が上限です。 それ以上に並列数を上げても、CI完了までの時間はほとんど変わらないか、逆に遅くなってしまいます。 ひとつのCIジョブを複数のslaveに分散させる方法もありますが、これも構成が複雑になるためそれ誰がメンテ問題で実行はしませんでした。
契機はAmazon Linux 1 EoL
そんな折、Amazon Linux 1 EoLの期限が近づいてきました。
Q: Amazon Linux AMI はどれくらいの期間、サポートされますか?
AWS では 2020 年 6 月 30 日まで、Amazon Linux AMI 2018.03 の最新バージョンに対するセキュリティ更新の提供を継続し、Amazon Linux 2 への移行を促進します。Amazon Linux 2 では、2023 年 6 月 30 日まで長期サポートを受けることができます。
Jenkinsのmaster & slaveもAmazon Linux 1上で動いていたので、移行作業が必要でした。 とはいえJenkinsおじさんのお世話にこれ以上時間を取られるのはモチベーションが上がりません。
AWS CodeBuildへの移行も検討したのですが、実行環境は自前で用意しなければならないため、 CodeBulid職人が生まれるだけなのではということで不採用に。
結局Performance Planが最高らしいという噂を耳にしていたCircleCIに移行することにしました。
CircleCIへ
Performance Plan登場以前は、月単位で使用するコンテナ数分の使用量を支払うというプランしかありませんでした。 月額なので、コンテナの使用有無に関わらず定額の使用量がかかります。 CIの所要時間を短縮するためにコンテナ数を増やすと、CIしていない期間のコンテナ使用料がそのまま課金されるのでもったいない感がありました。 また、同時に実行できるジョブ数もコンテナ数に依存するので、実行キューに複数のジョブが積まれている状態では待ち時間が発生していました。
一方Performance Planは従量課金です。 コンテナの利用時間を元に課金されます。 並列数を上げてコンテナ数を増やしても、CIを実行していない時間の利用料はゼロです。 このプランが登場したおかげでCI所要時間の短縮がデメリットなく行えるようになりました。 ジョブの同時実行も、上限はあるものの通常の使用ではこれにかかることはまずないはずです。
移行のためにやったこと
もともとAmazon Linux上で動かしていたものを、Amazon Linuxをベースイメージとしたコンテナ上で動かすことになるので、 アプリケーションそのものは変更なく移行できました。
CI用のコンテナイメージ
LobiではPerl、Go、Node.jsで書かれたアプリケーションが混在しています。 GoおよびNode.jsは永続化層を持たないアプリケーションのみだったので、CircleCIが公開している各言語ごとのイメージを利用すれば事足りました。 Perlで書かれたものはLobiのメインとなるアプリケーションで、歴史もあり依存も多かったため専用のイメージをビルドして使っています。
並列実行数の調整
CricleCIにはジョブを分割して並列実行する仕組みがあります。
適切にテスト結果を保存すれば、それを元に並列数の指定に応じていい感じに分割してくれます。 Lobiの場合はX-large(8コア)コンテナを20並列で動かし、その中で8並列でテストを実行しています。
移行した結果・・・
CIの所要時間は10分から4分に短縮されました。 コンテナの並列数を上げれば更に縮められそうですが、その分セットアップにかかる時間の割合が多くなり、コストパフォーマンスが下がリます。 バランスを取るためにCI所要時間=5分を目標に調整しています。
複数のジョブを同時に実行できるようになったため、CI開始までの待ち時間はほぼゼロになりました。 誰かのCIが実行されているときでも、pushして5分待てば確実にCIが完了している状態になりました。
費用
数万円程度だったCI関連費用は、およそ2倍の十数万円になりました。 出費としては増えましたが、JenkinsのメンテナンスコストとCI待ち時間の削減効果を考えればお釣りが来るくらいの高コスパです。
マネージドサービスの導入を検討する段階ではついつい人件費を考慮せずに目に見える出費を優先してしまいがちですが、 大抵の場合は出費に見合う効果が期待できるはずです。 餅は餅屋に任せて本当にやりたいことに注力しましょう。
おわり
メンテナンスコスト大幅ダウン & 待ち時間大幅減少でとても良い感じです。 多少融通がきかないこともありますが、マネージドな有料サービスなので日々改善されていくことでしょう。
今回はLobiのはなしでしたが、カヤック社内のプロジェクトはおおよそCircleCIに移行しました。 ツールを共通化すると知見の共有ができるのでいいですね。
あとさらっと書きましたが、PerlアプリのCIに使っているベースイメージがAmazon Linux 1なんですよね。 CircleCIへの以降とAmazon Linux 1からの脱却は切り分けて進めたので後者はまだ終わっていないのです。 こちらについては来年のMigration Trackネタということで。