GitHub Actions:開発PCの片隅で始める高速CI / Rootless Docker編
fondi DEVチームのmuoです。
みなさま、今日もGitHub Actionsしてますか?
オンライン英会話学習アプリfondiはUnityで作られており、日々の実機動作確認にUnityとXcodeでのビルドが欠かせません。
もちろん毎回手動でビルドするわけにはいかないので2022年的なファーストチョイスとしてGitHub ActionsでGitHubのリポジトリと連携したビルド体制を組むのですが、自動化できたところがスタート地点というか、ビルド速度やストレージ不足を筆頭に解決すべき課題がいろいろと表面化してきます。
今回はRootless DockerでGitHub Actionsセルフホスト環境をうまく運用していく話です。
GitHub Actionsの本家ランナーではいろいろと足りない
GitHub Actionsの標準ビルド環境(GitHub-hosted runners)は多様なビルド用途を賄えるように構成されていますが、大規模なUnityプロジェクトをビルドする際にはCPU性能・メモリ容量・ストレージ容量の各方面で不足が生じます。
使える一時ストレージはせいぜい30-40GB
CPUとメモリは割とどうしようもない(swap領域を用意してどうにかなるわけではない)のですが、ストレージ容量については定番のチューニングが存在します。
ビルド用の一時領域を確保するためにビルド用VM上にインストールされていてビルド時に使わないソフトウェアをディレクトリごと削除する手法で、おおむね20-30GBは安定して確保できます(参考:https://github.com/easimon/maximize-build-space )。
しかし50GBほど空きが欲しいときには相当頑張って不要ディレクトリを削り切る努力が必要となり、ビルドステージごとに適切な一時ファイル削除スクリプトを仕込むなど、正直不毛です。
Unityの巨大import済キャッシュ(ビルドプラットフォームごとに概算30GB)類を丸ごとロードして高速ビルドしたい、などを考え始めると現実的にきびしさがあります。
そもそもVMに割り当てられているディスク自体が90GB弱しかありません。
macOSインスタンスの料金が高め
実行時間ごとにかかる料金はOSによって異なります。macOSインスタンスのランニングコストがそれなりに高い(Linuxの10倍、1時間あたり$4.8、$1=135円換算で650円ほど)ため、規模の大きなUnityアプリのXcodeビルドを頻繁に実行するユースケースではコストがそれなりにかさみます。
ざっくりと1ビルドあたりmacOSインスタンスを1時間利用、月間100回ビルドすると月間65,000円かかります(IL2CPPビルド部分をLinuxノードへ切り出す前提でトータル8万円ぐらいです)。
これらふたつの事情から、もし手元にあまり利用頻度が高くなくてスタンバイ電力が低めの常時起動PCがあればそちらにビルドロールを押し付けたいところです。
GitHub Actionsではself-hosted runnerというビルド環境自前ホスティングの仕組みが提供されているので、これを使っていきます。
GitHub Actions self-hosted runnerでやっていけ…る?
基本的には公式ドキュメントを読んでのとおりです。
どことなく(?)Azure Pipelinesのかおりがしますね。
基本的にメンテフリーのお気楽分散CI
GitHubプロジェクトに紐付けたGitHub Actions self-hosted runnerを用意しておき、適宜ロール(Xcodeが必要ならmacOSへ、など)を関連付けておくとGitHub側のagentが適当にオンライン状態のノードへとタスクを割り振ってくれます。
Jenkinsを自前でメンテするのとは異なり、ジョブ実行に必要な最低台数を割り込みさえしなければ特段管理の必要はありません。self-hosted runnerのエージェントプログラム自体も適宜自動アップデートされます(たまにコケますが)。
LinuxとmacOS開発機に相乗りさせる、、注意点:Docker
WindowsやVisual Studio環境が絡んでくるとライセンスの兼ね合いもあって社内の開発環境をカジュアルに兼用CIサーバ化するのは気が引けるのですが、Ubuntu LinuxとmacOSでほどよくタスクを分け合って実行する分にはクリアです(AppleがXserveを販売していた頃はサーバワークロードをサーバOSへとバインドする流れがあったように思いますが、2022年4月21日をもって原則的にすべて終了しています)。
GitHub ActionsはDocker利用も想定していますが、重要な注意点としてmacOSに立てているself-hosted runner上ではDockerコンテナ連携をサポートしていません。
とはいえ、これから新規に環境構築する際は多くの場合Arm(M1/M2世代)のMacを利用すると思われるため、どのみちx86-64なDockerイメージとの相性がかなり悪いことを考えると無理にmacOSで動かさずにLinux環境も適宜用意して分業させようと割り切れるのではないでしょうか。
開発PCの片隅で:self-hosted runner+Dockerでビルドする際の課題
なるべくDocker上でビルドしたい
ソフトウェアのビルドは大半のステップをDockerコンテナ内へ閉じ込めることが望ましいです。ビルドに利用するソフトウェアや設定情報をホスト環境から切り離すことで環境依存の問題から解放されます(将来さらに強力なビルドマシンへと環境を切り替える場合でも最小限の手間で環境再現が可能)。
Docker実行ユーザーの権限をうまく制限したい
クラウド上に専用ビルドインスタンスを用意する場合とは違い、今回は通常の開発にも利用しているPCの空き時間でビルドを実行する相乗り環境を構築する以上、Docker daemonへ無邪気にroot権限を保持させることは避けたいです。
Dockerコンテナの役割がテンポラリ領域へのアクセスと演算にとどまる場合、つまり成果物(artifacts)をネットワーク経由でS3やGCSへアップロードするだけであればまだ話がシンプルなのですが、ビルド時間短縮のために既存のローカルキャッシュを使い回したり成果物をホスト側のファイルシステム上へ配置したいといった需要がある場合はDockerコンテナとの間でvolumeのやりとりが発生します。
Docker、Linux環境、volume、実行権限、とキーワードを並べるだけで頭痛がしてきますが、これらのオペレーションを一般ユーザー権限(Docker専用ユーザーでもなく)へ押し込み、rootユーザーへの依存なく実行する方法があります。
Rootless-Docker(Rootlessモード)
2019年7月にリリースされたDocker Engine v19.03以降では、一般ユーザー権限でDockerを実行できるRootless-Docker(Rootlessモード)をサポートしています。
基本的に公式ドキュメントに沿って導入すればOKです。
厳密には`newuidmap`と`newgidmap`が必要で、これは特定のユーザーに紐付いたサブユーザーやサブグループを64K個ほど生やす仕組みです。
手元では新しめのUbuntu(20.04)を利用しているので、カーネルパラメータの調整類も不要です。
サーバ実行系、特にLAN内からアクセスできるネットワークサービスのhostingには一定の制限がありますが、GitHub Actionsのself-hosted runnerと組み合わせて利用する分にはまず問題にならないはずです。
Rootless-Docker固有の設定
Docker環境のインストールと実行にroot権限を必要としないのは本当に素晴らしいものです。
環境を簡単に作り、そして壊してもシステム全体へ悪影響が残らないということです。
しかしGitHub Actionsのself-hosted runnerはrootless Dockerのことを意識していません(公式サポートしていません)。
DOCKER_HOST環境変数設定
rootless-DockerはデフォルトインストールしたDockerと異なりユーザー個別のsockファイルを介してUnix Domain Socket通信します。
`docker`コマンドの通信先を`DOCKER_HOST`環境変数で設定
という具合です。
このため、各ビルドステージでのDocker経由実行時に都度`DOCKER_HOST`の環境変数設定が必要です。
fondiではしばらくGitHub Actionsのビルド手順YAMLファイルの各ステージでのDocker呼び出し時に`DOCKER_HOST`環境変数設定を付与して実行していました。
しかしこの方法はあるタイミングで行き詰まり、今では利用していません。
理由は次の機会に紹介します。
トラブル:ビルドが不意に止まる(1)
よしよし、ちゃんとビルド動いているな、と思ってログアウトしてしばらく経つとself-hosted runnerのdaemonとの通信が途絶してビルドエラーへ陥りました。
これはsystemctlでの設定を思いっきり忘れていました。
Rootless-Dockerのインストール時メッセージでもきちんと警告されているのですが、次のように設定することが必須です。
root権限不要と言ってしまった手前ちょっとかっこわるいですが、ここではroot権限が必要です(sudoを見なかったことにしてはいけません)。
トラブル:ビルドが不意に止まる(2)
Ubuntu Desktopの標準設定では、一定時間以上デスクトップアクセスがない場合に自動的にスタンバイモードへ入ります。
Ubuntu Serverばかりを使っていると案外気付かないのですが、デスクトップ利用(Unity EditorもGUI実行)するUbuntu環境へ相乗りしたために表面化した問題でした。
解決方法はいくつかあるのですが、手元の環境では常時デスクトップPCへリモートアクセスしているのでいっそのことdisplay-managerを停止することにしました。
root権限不要と言ってしまった手前ちょっとかっこわるいですが、ここではroot権限が必要です(sudoを見なかったことにしてはいけません)。
PCの再起動後に1回だけ実行すればよいので実用上は誤差とみなします。
一部の省電力機能が使えないなど若干の残念さは残りますが、原則的に問題ありません。
宣伝
fondiでは、一緒にプロダクト開発を加速させてくれるエンジニア仲間を募集しています!