Amazon ECS と AWS Lambda で汎用 self-hosted runner を提供する基盤

技術部 SRE グループの @s4ichi です。ここ最近は本業に加えて Overwatch2 のヒーローとして戦いに明け暮れています。救わなければならないレートがある。

GitHub flow に従った開発では GitHub Actions が非常に便利です。特に最近では CI 用途だけでなく、ソフトウェアのデリバリーなども Actions で完結させる事例も見かけます。しかしながら、クックパッド社内では GitHub Enterprise Server を使っているため、GtiHub Actions の利用には self-hosted runnner の利用が不可欠になっています。

そこで、社内では Amazon ECS 上に ephemeral で汎用的な self-hosted runner を提供しています。実行する job の数に応じた autoscaling を備え、runner の起動を司る部分は AWS Lambda を用いたサーバーレスな構成です。今日はその基盤についての構成やチャレンジ、そして独自拡張の話をします。

クックパッドにおける GitHub Actions

社内では、ほとんどのチームの開発に GitHub Enterprise Server(以下、GHES)が使われています。GHES は EC2 上にデプロイされており、開発者に不自由が無いよう、利用規模や用途に合わせて調整する形で SRE チームが運用をしています。ただし、最新のバージョン 3.7.0 現在においても GHES には GitHub-hosted runner が提供されていないため、self-hosted runner を用意しないことには GitHub Actions を使えませんでした。

そんな中、昨年12月にリリースされた GHES のバージョン 3.3.0 にて、self-hosted runner を ephemeral に扱えて、かつ autoscale に用いることができる webhook が提供されるようになりました。依然として self-hosted runner 自体を用意することには変わりませんが、GHES でも比較的容易に GitHub Actions を運用するための機能が提供されました。世の中的にも GitHub Actions の機能が拡充されるに連れて利用の幅が大きくなっているのを感じていたのもあり、GitHub Actions を試してみようという動きが始まりました。

今回はこうした背景のもとで構築された GitHub Actions を提供する基盤の話になります。

ghe-actions: 汎用的な self-hosted runner を提供する基盤

ghe-actions は社内の GHES 上で GitHub Actions を提供する内製の仕組みです。汎用的な用途で使える self-hosted runner を ECS Task として提供します。runner は ephemeral な用途で使えるよう job ごとにクリーンな環境が用意され、同時実行する job の数に応じて必要な Container instance を調整する autoscaling の機能を備えています。

ghe-actions 概略

ghe-actions は GitHub Apps と AWS 上のいくつかのサービスを組み合わせて実現しています。

  • self-hosted runner 本体である Amazon ECS Task
  • GHES から webhook を受け取って runner を起動する AWS Lambda function
  • runner の ECS Task を起動するためのAmazon SQS queue

ghe-actions の構成は運用の手間が少なくなるよう、GHES 本体以外に状態を持たない設計にしました。状態遷移を保存するためのデータストアを持たないことで依存するコンポーネント管理の手間を削減するのが目的です。

利用の流れ

社内の GitHub Actions のユーザーは、まず ghe-actions のための GitHub App を自身のユーザーや organization にインストールします。GitHub App には workflow の job が起動した webhook(workflow_job)を Lambda の function URL へ送る設定がされており、インストールされた organization のリポジトリで workflow が起動すると、workflow_job の webhook から Lambda が起動します。

workflow_job の webhook を受け取った Lambda は、webhook の内容を解釈して必要なデータを抽出します。それらのデータは runner 起動のための SQS に SendMessage されます。SQS は別の Lambda から ReceiveMessage されたのち、GHES の API を使って runner 登録のための token を発行し、job を起動したリポジトリ専用の self-hosted runner を ECS Task として起動します。

runner は起動して job を実行したら Task を終了します。ephemeral な runner なので、同じ Task で複数回 job を実行することはありません。実行ごとに完全に削除されます。

構成要素の詳細

ユーザーがインストールする GitHub App は必要な permission を最低限要求し、以下の操作のために使われます。

  • 有効にしたリポジトリの workflow_job webhook の購読
  • リポジトリ向けの Registration Token の発行
    • self-hosted runner はこの token を利用して runner を GHES に登録する

2つの Lambda function はそれぞれ以下の役割を持ちます。

  • function URL から webhook を受け取る Lambda function
    • workflow_job の webhook の中でも Queued となるイベントにフィルターする他、payload を解釈して必要なデータだけを抽出して SQS に SendMessage する
  • SQS から ReceiveMessage してrunner を起動する Lambda function
    • SQS 経由で受け取った payload からリポジトリや labels を特定し、Registration Token の発行と labels に応じた適切な runner を決定・起動する

self-hosted runner 本体である ECS Task は次の特徴を持ちます。

  • マルチサイズ・アーキテクチャに対応
    • 最小 1 vcpu, 2 GiB RAM から最大 8 vcpu, 15 GiB RAM まで4種類のサイズを提供
    • アーキテクチャは amd64, arm64 環境を提供
  • Ubuntu ベースで基本的なパッケージは導入済み
    • 社内のユースケースに合わせつつ、GitHub-hosted runner に近い体験を提供
  • sidecar コンテナ内で dockerd を rootless で起動して利用
    • workflow 内の Docker コンテナの実行をセキュアに保つ

以上が汎用的な self-hosted runner を提供するための基盤である ghe-actions の概要です。ここからは ghe-actions の提供にあたって検討したチャレンジを紹介します。

汎用的な self-hosted runner を提供するために

GitHub-hosted runner では ubuntu-latest などの labels を指定すると、様々なパッケージが事前に用意されている汎用的な runner の環境が手に入ります。しかし、GHES の GitHub Actions には GitHub-hosted runner はありません。そのため ubuntu-latest で起動してくるような汎用的な self-hosted runner の提供が必要です。まったく同程度の汎用性までは求めずとも、社内の開発における不便さを解消できるところまでは提供する必要がありました。

self-hosted runner は https://github.com/actions/runner をインストールした計算機を repository や organization に登録することで runner として機能し、job を実行できるようになります。actions/runner 自体の requirements はそこまで大きくないため、いくつかのパッケージを入れるだけで満たせます。しかしながら、その状態の self-hosted runner は私たちが普段開発する上で必要な機能が不足しています。

ubuntu-latest などの labels で起動してくる GitHub-hosted runner は https://github.com/actions/runner-images から提供されているイメージです(着手当時は virtual-environments という名前だったんですが、変わってました)。例えばこれを Docker イメージ化することで同等の環境が提供できるのですが、イメージのサイズが数十 GiB とかなり大きくなってしまいます。AMI 化して EC2 で提供する方法も試しましたが、ephemeral な環境を提供するにあたって、毎度 EC2 の起動を待つことは実行時間の増加が見込まれるため諦めることにしました。

さて、管理や運用の楽さ、そして起動時間の観点から、やはりコンテナ化して提供することが好ましいです。しかし、コンテナ環境で self-hosted runner を提供するには課題があります。それはコンテナの中で Docker CLI を使えるようにしなければならないことです。workflow ではイメージのビルドはもちろん、MySQL などのミドルウェアの起動にも Docker を使います。

Docker コンテナ内で Docker CLI を使うためには、dockerd が必要です。コンテナ内で dockerd を提供する方法として、ホストで動いている dockerd のソケットをコンテナ内に共有する dood(docker-outside-of-docker)、もしくはコンテナ内でホストとは別な dockerd を立ち上げる dind(docker-in-docker)があります。前者はホストの dockerd を直接操作できるため、ホストで動いているコンテナを触ることができ、後者はコンテナごとに完全に独立した dockerd の環境が与えられます。

今回、リポジトリによっては秘匿値を扱ったり AssumeRole の操作を含むこともあり、汎用的な用途を目指すうえでセキュリティ面を重視する必要があります。そのため、dood のようにホストの dockerd が共有される環境は、簡単に他のコンテナへ侵入できるためセキュリティの観点から危うく、必然的に dind を選ばなければなりません。しかし dind でもコンテナ内で dockerd を実行するにはコンテナに特権を渡す必要があります。特権を持つコンテナは、ホスト環境を参照できてしまうので脆い状態には変わりません。そういった状況で、今回はコンテナに特権を与えつつ dind でセキュアな環境を提供するため rootless な dind の docker を利用しました。docker は公式で dind-rootless なイメージが配布されています。

そのイメージを ECS Task の sidecar コンテナとして起動する方針を取りました。dind の特権付与済みコンテナを runner のコンテナで dood した、というわけです。これで runner が参照する dockerd の環境は特権を持ちますが、root を持たないため、できることが限られます。以下に構成図を示します。

dind-rootless を sidecar として起動する ECS Task

図中では書いていませんが、sidecar に切り分けた dind-rootless コンテナ内の dockerd は、runner のコンテナと同じ network で動かし、localhost アドレスを共有するため、ECS Task を awsvpc network mode で起動しています。volume も共有して使うことで、基本的なユースケースでは runner と dockerd のコンテナが異なることを気にすることはほとんどありません。

ここまで来たらあとは必要なパッケージを同梱するだけです。actions/runner のアプリケーションをベースに、よく使われるパッケージを事前に入れた Docker イメージを提供しました。イメージサイズは ubuntu をベースにした actions/runner で 1.47 GiB、sidecar の dind-rootless コンテナは 367 MiB です。

多様な self-hosted runner を提供する

今回提供している self-hosted runner は ECS Task として起動できる形式ならば様々な拡張が可能です。ghe-actions は社内のユースケースに合わせてマルチアーキテクチャに対応しており、用途に応じて runner サイズの選択もできるようにしています。

workflow_job の webhook から受け取れて、かつ input とみなせる情報として labels があります。この labels で表現できる範囲であれば容易に多様な runner を提供することが可能です。現状は前述したアーキテクチャとサイズのみを展開していますが、例えば特定の SecurityGroup を持つ runner を起動するなど、GitHub-hosted runner では難しい領域で拡張していく予定です。

AWS 利用料金の節約

self-hosted runner が提供できるようになったとはいえ、提供する ECS Task は無料ではありません。今回は継続的に基盤を提供し続けるためにも、できるだけ利用料金を節約する方向に ECS Cluster を構築しました。

ghe-actions の ECS Cluster に属する Container instance は全て Spot Instance で構成されています。Spot Instance の利用は、On-Demand Instance と比べてかなり利用料金を削減できるためです。Spot Instance interruptions により workflow が途中で終了してしまう可能性もありますが、capacity-optimized allocation strategy を用いることで頻度を抑えるようにしているため、今のところ困ることもほとんど無く済んでいます。

Container instance は job の実行数に応じて autoscaling する構成になっています。そのため、利用頻度の低いときは台数を減らして待機させることにしました。autoscaling を有効にすることで、一度に大量の job が起動する場合は Container instance の起動時間のペナルティがかかりますが、大量の job を一気に起動する用途は限られるため、通常の用途では困ることは少ないでしょう。

現行の制約

ghe-actions は社内のほとんどの用途で問題無く使えますが、本家の GitHub-hosted runner と比べればいくつか制約が残ります。

Service containers の機能が使えない

Service containers は GitHub Actions の workflow で記述できる Docker コンテナ起動のための記法です。actions/runner はまだコンテナ内での起動を公式にサポートしているわけではないため、 Service containers の機能を使う前の実行環境の validation を違反してしまう状態です。

GitHub Marketplace にある action が全て使える状態ではない

ほとんどのケースにおいて問題にはならないんですが、GitHub Marketplace にある action の一部は使えないことがあります。action の実装によっては ghe-actions の構成と非互換な使われ方をしていることがあるためです。都度回避策を用意するなどして回避していますが、稀に存在するため、GitHub-hosted runner と完全な互換だと想定することができない現状です。例えば、docker/build-push-actiondocker/setup-buildx-action は README にある通りの設定では ghe-actions の環境で動作しないため、事前に別途コマンドを実行した上での利用が必要です。

おわりに

本記事では社内の GHES に導入した ghe-actions の基盤についてその背景やチャレンジ、独自拡張について紹介しました。GitHub-hosted runner と比較して不足する部分もあるものの、self-hosted runner であるために細かな調整や独自拡張を入れやすい環境が提供できています。

運用の手間や起動時間を最小限にしつつ、現実的なコストで運用ができる程度の汎用的な runner を提供する基盤が整っています。今後は社内にある開発フローやシステムとの連携を進め、より使いやすい環境を提供していきます。

クックパッドでは、このような CI/CD 環境の構築を始めとした開発者向けの仕組みの基盤整備にも積極的に取り組んでいます。こうした分野の改善には幅広い領域への理解と深い専門性が必要になります。開発者向けの基盤、及びサービスの信頼性向上に興味のある方は、ぜひお気軽にご連絡ください。下記の採用ページからも申し込むことができます。

cookpad.careers cookpad.wd3.myworkdayjobs.com