2022年2月14日

BuildkiteによるオートスケーリングCI環境

CIInfrastructureBuildkiteKubernetesGCP

cover

Infrastructure Divisionの藤原です。普段は社内の継続的インテグレーション(CI)環境の構築や、ビルドツール(Bazel)の整備などを担当しています。

今回はLeapMindのCI環境のオートスケーリングの仕組みを紹介します。低コストで並列度の高いCI環境を実現したい方、Kubernetes(k8s)を活用したCI環境に興味がある方にとって、何かの参考になれば幸いです。

BuildkiteによるCI

LeapMindではBuildkiteというCIサービスを利用しています。一般的なCIサービスでは、ジョブの実行環境はサービス側で用意されていて、料金プランによってスペックや並列数を選択できる、というものが多いです。またJenkinsのような完全セルフホスト型のCIツールでは、WebUIやAPIのホスティングと、ジョブの実行環境の用意の両方を自分で行う必要があります。

Buildkiteは半セルフホスト型のCIサービスで、これらの中間的なCIサービスであるといえます。WebUIやAPIのホスティングはサービス側に任せて楽をしつつ、実行環境を自由に選択できるという利点があります。任意のクラウドサービス(またはオンプレミス)環境で、任意のスペック・台数の実行環境を、コストと相談しつつ選択することができます。

Buildkiteの仕組み

さて、一般にCI環境の負荷は曜日や時間帯によって大きく変動します。GitHubへのソースコードのプッシュをトリガとしてCIを実行する構成の場合、勤務時間帯には多くのジョブがリクエストされますが、夜間や休日はほとんどリクエストされません。参考までに、ある1週間(月~日曜日)にCIがトリガされた回数をグラフにしてみました(夜間帯や休日中の実行は、事前にスケジュールされた定期実行を含みます)。

ある1週間のCIジョブ数の推移

Buildkiteではこのような負荷の増減に対して、エージェントの数を増減させることによって対応できます。

オートスケーリングの仕組み

Buildkiteエージェントの実体は、ジョブの割り当てを待機するデーモンプロセスです。そのコンテナイメージはBuildkiteが公式に用意しているので、それをベースに必要なツールをインストールしたイメージを作成しておきます。

コンテナ化されたアプリケーションをオートスケールするためには、k8sや各クラウドサービスのコンテナオーケストレーション機能を活用するのが良いでしょう。LeapMindではGoogle Cloud Platform(GCP)のGoogle Kubernetes Engine(GKE)を利用しています。

GKEにはクラスタオートスケーリング機能があるので、ノード(クラスタを構成するインスタンス)のオートスケーリングはこれに任せることができます。以下では、クラスタ上に起動するエージェントプロセス数のオートスケーリングの方法を考えます。

Kubernetesクラスタ上でエージェントをオートスケールする

k8sのHorizontalPodAutoscalerを使う案(不採用)

k8sにはHorizontalPodAutoscaler(HPA)というオートスケーリングの機能があります。DeploymentやStatefulSetなどの複数のPodを生成するリソースと併せて使用し、PodのCPU使用率をもとにレプリカ数を自動的に調節してくれるものです。

BuildkiteエージェントのCPU使用率は、待機中はほぼ0%、ジョブ実行中はある程度大きな値になると予想できます。エージェントをk8sのDeploymentとしてデプロイし、HPAでCPU使用率の目標値を適切に設定しておくことで、うまくスケーリングしてくれそうです。

実際LeapMindでもこの方法を試していました。スケールアウトの挙動は想定通りだったのですが、スケールインの挙動に問題がありました(v1.19時点)。Deploymentのレプリカ数が下げられると、停止するPodはCPU使用率に関係なく無作為に選択されます。したがってジョブが実行中であろうと、エージェントが停止されてしまうのです。ジョブの中断・再実行が頻発してしまったため、この方法は断念しました。

待機中のジョブ数に応じてスケーリングする案(採用)

CPU使用率のような間接的なメトリクスを使わずに、待機中のジョブ数に応じてエージェントを起動する方法を考えます。この方法では、BuildkiteのWebAPIを利用して待機中のジョブ数を取得し、必要な数のエージェントを起動する、自前のアプリケーション(下図のAgentScaler)を実装します。

AgentScalerの仕組み

Buildkiteエージェントはデーモンとして常時起動しておくのが通常の使い方ですが、起動時にdisconnect-after-jobオプションを指定することで、ジョブ終了後、次のジョブを待たずに安全に終了させることができます(この場合、エージェントプロセスはDeploymentではなくk8sのJobリソースとして起動することになります)。こうすることで、HPAのスケールイン時のようにジョブが中断される問題を回避できました。

ある時間帯のエージェント数の推移をグラフにしました。リクエスト数に追随してエージェント数が調節されていることが確認できます。めでたしめでたし。

エージェント数の推移

実行環境のコスト

一般的なクラウドサービスのインスタンスは、延べ実行時間に対して課金されます。オートスケーリングの導入によって、必要なときに必要なだけインスタンスを起動できるようになったため、無駄なコストを削減することができました。

また並列数をN倍にして実行時間が1/Nになれば、同一ジョブを実行するコストは並列数に依存しません。もちろん実際にはスケーリングのタイムラグがあったり、インスタンス以外のコストが増加したりと、厳密にコスト据え置きとはなりません。しかしCI結果が早く得られるようになることで、コスト増加に見合う開発効率の向上が期待できると思います。

クラウドサービスベンダによっては、余剰の計算リソースを安価なインスタンスとして提供していることがあります。GCPであればSpotVMがそれに該当します。たとえばn2-standard-2マシンタイプ(us-west1)であれば、通常\0.097/hourのところ、SpotVMであれば0.097/hourのところ、SpotVMであれば\\0.024/hourと、約1/4のコストで利用できます。このような安価なインスタンスを活用することで、さらにコストを削減することができます。

まとめ

拡張性の高いCIサービスBuildkiteと、k8sクラスタを利用したオートスケーリングの仕組みにより、低コストな高並列CI環境を実現した例を紹介しました。

LeapMindにはCIシステムを構築するエンジニア以外にも、機械学習やハードウェア開発を専門とするエンジニアが多数在籍しています。興味を持った方はぜひ一度採用ページをご覧ください。