無理せず始めるUnityアセットバンドルビルドのJenkins→GitHub Actionsセルフホステッドランナー移行

はじめに
はじめまして、株式会社QualiArtsでバックエンドエンジニアをしている@karamaru_alphaです。
Unityには動画や画像などのアセットに対してプラットフォームごとの変換処理や圧縮処理などを事前に行うアセットバンドルという仕組みがあります。 弊社では従来、このアセットバンドルビルドやUnityアプリビルド環境としてJenkinsを採用してきました。
Jenkinsは手軽にCI/CDプラットフォームを構築できる強力なツールです。 一方で、複数タイトルで運用を行なっていく中でいくつかのデメリットも感じるようになりました。
そこで、本記事で はUnityアセットバンドルビルド環境をJenkinsからGitHub Actionsに移行した取り組みについて紹介します。 はじめに、Unityのビルド環境でなぜJenkinsが採用されてきたのか、その背景を説明します。 次に、弊社で利用してきたJenkins×MacPro構成のデメリットについて共有します。 最後に、なぜGitHub ActionsとEC2セルフホステッドランナーの構成に行き着いたのか、その理由と実装方法を紹介します。
弊社技術ブログ記事「UnityのアプリビルドをGitHub Actionsに移行した話」で紹介されている取り組みと併せて、現在筆者が所属しているプロジェクトではJenkinsを完全に廃止しGitHub Actionsに移行することになりました。 こちらのブログもぜひご確認ください。
先に結論
システム概要
UnityのアセットバンドルビルドをGitHub Actionsで実行するようにしました(旧: Jenkins×MacPro)。
ビルドターゲットごと(iOS/Android...)にEC2インスタンスを用意し、それをActionsのセルフホステッドランナーとしてアセットバンドルビルド実行時のみ起動させる構想です。
また、EC2はルートボリュームとしてEBSをマウントしており、キャッシュを含むファイルシステムが実行毎に引き継がれるステートフルな構成になっています(アセットバンドルビルドにおいて実行ごとにクリーンなCI環境が適さない理由は後述します)。
管理画面(非エンジニア向けUI)
アセットバンドルビルド(GitHub Actions Workflow)の実行やそのステータスの確認には独自の画面を作成しました(旧: JenkinsController)。
GitHub Actionsはon: workflow_dispatchの記述でGitHubのGUI上からワークフローを実行できます。
しかし、すべてのアセットバンドルビルド実行者がGitHubアカウントを所持しているとは限りません。 弊社もプランナーやクリエイターなどがそれに該当するため、ワークフローの実行やその実行状況・ログ・アーティファクトの閲覧をGitHubアカウントなしで行えるようにUIを作成しました。
以下、このような構成になった背景や実装内容について記述します。
なぜゲーム業界でJenkinsが利用され続けるのか
Jenkinsは自前でホストすることによって汎用的なCI/CDプラットフォームを構築できるJava製のOSSです。 弊社でもUnityアプリビルドやアセットバンドルビルドでJenkinsを活用してきました。具体的には、JenkinsController(タスクランナーへの実行依頼やUI提供)としてECSを、JenkinsAgent(タスクランナー)として物理PC(MacPro)を採用していました。
実際にJenkinsの中でどのような処理が行なわれていたかについては、弊社技術ブログ記事「Unity開発の現場でJenkinsがしていることの紹介」をご参照ください。
さて、GitHub ActionsやAWS Code Build、Google Cloud Buildなどクラウドビルドが台頭する現代において、どうしてゲーム業界ではJenkinsが未だ選択され続けるのでしょうか。 以下、ゲーム業界におけるJenkinsの有用性を主にGitHub Actions(GitHubホステッドランナー)と比較して紹介したいと思います。
前回の実行環境を引き継いで実行できる
まず、キャッシュ取り回しの観点でJenkinsが優れたソリューションになるケースを紹介します。
一般的に、冪等性の観点からCIは毎回クリーンな環境で行われる方が良いとされています。GitHubホステッドランナーもその例に漏れず、Job毎にクリーンな環境を提供してくれます。
さて、Unityのアセットバンドルビルドをこのような環境で実現しようとすると、巨大なキャッシュシステムのハンドリングに困ることになります。 通常Actionsにおけるキャッシュはactions/cacheを通じて行いますが、アセットのキャッシュは巨大すぎてGitHub側の10GBのキャッシュ制限内にまず収まりません。 キャッシュを引き回すシステムを自作したとしても、巨大なファイル群のストア/リストア自体をネットワーク越しに行うのは実行時間が長くかかりますし、金銭的なコストも無視できません。
そこで、JenkinsのCI環境を実行毎に引き継ぐという特性が活用できます。 JenkinsAgentを単一の物理PCやVMとすれば、キャッシュはただの永続ファイルシステムとして扱うことができます。 冪等性を担保するというCIの一般原則から外れステートフルな環境を提供することで、巨大なキャッシュをネットワーク越しにストア・リストアすることを回避しているのです。
この「ワークスペースの維持」という特性はゲーム業界でJenkinsが重宝されるもっとも大きな理由だと思います。 とくにキャッシュが巨大になるアセットバンドルビルドにおいて、このソリューションは大きな力を発揮します。
非エンジニアでも見やすいUIの提供・コスト面のメリット
次に、実装・金銭コストにおけるJenkinsのメリットを紹介します。
Jenkinsは自前でホストさえしてしまえばアカウント年会費などの従量課金は発生しません。 一方GitHubは2024/08/30現在Enterprise版は1人あたり252ドルの課金が発生します。 タスクを実行したいすべてのメンバーがGitHubアカウントを持っているとは限らないため、非エンジニアも利用できるプラットフォームとしてJenkinsは導入しやすい特性があります。
また、Jenkinsはタスクの実行やそのステータス閲覧を行えるUIをデフォルトで提供します。 非エンジニアのメンバーでも見やすいUIを、実装の手間をかけずに作成できるのもJenkinsの大きなメリットと言えるでしょう。
上記がゲーム業界で現在もJenkinsが活用される理由の一部です。 より詳しく知りたい方は弊社アドベントカレンダー記事「【Unity】ゲーム開発の現場でなぜJenkinsが利用され続けるのか」の参照をオススメします。
Jenkins×MacPro構成の課題感
先述した通り、弊社ではUnityアプリ・アセットバンドルビルド環境にJenkinsを利用していたのですが、運用していく中でいくつかのデメリットを感じるようになりました。
タスク定義をGitOpsにできないことによる保守性の低下
Jenkinsは基本的にタスク定義などの操作をGUI上で行います。 Git連携するプラグインもあることにはあるのですが、すべての設定や定義をGitOps管理することは難しいです(そもそもプラグインのインストールもGUI経由ですし)。
GitOpsに寄せきれないことでレビューを通さないタスク定義や設定が散らばった状態になり、視認性や保守性が低下した 結果、Jenkinsの扱い自体が属人化してしまうという危険性があります。 この透明性の低さは、プロジェクト毎に「Jenkins職人」なる人が誕生する原因の1つだと思います。
Jenkins自体のメンテナンスコスト
Jenkinsの特性上、その機能・挙動の多くはプラグインに強く依存しています。 当然そのプラグイン群を定期的にアップデートしていく必要があるのですが、既存ジョブやプラグインとの互換性を保ちつつアップデート作業を行うのは骨が折れる作業です。
さらに上記GitOpsの課題に関連して、JenkinsController側の保守についても属人化しやすい点がデメリットとして挙げられます。
弊社は基盤管理を専門とするのSREチームが存在しないということもあり、ホストする対象が増えるのは(アップデート作業に工数がかかるなら尚更)避けたいところです。
物理PCの所有コスト
これはJenkinsAgentを物理マシンに入れている場合に限りますが、物理リソースの管理コストもデメリットとして挙げられます。 タスクランナーに物理PCを採用するとマシンスペックが固定されますし、なにより故障のリスクが生じます。
コンピュートが従量課金にならないのはメリットですが、マシンスペックの柔軟性をはじめとするクラウドの恩恵にあやかれないデメリットの方がはるかに大きく感じます。
GitHub Actions×EC2(オンデマンドVM)×管 理画面 構成に移行する
ここまで、ゲーム業界でJenkinsが用いられる理由と、弊社で採用していたJenkins×MacPro構成のデメリットについてお伝えしました。 おさらいすると、従来構成のメリットはワークスペースの維持とポータブルなUI提供で、デメリットは非GitOpsによる属人性とJenkins自体・物理マシンの管理コストでしたね。
さて、キャッシュサイズが大きいアセットバンドルビルドについて、従来(Jenkins×MacPro構成)のワークスペースの維持やUI提供のメリットを保ちつつ、デメリットを解消する(クラウドの恩恵にあやかる・管理コストを下げる・定義をGit管理する)にはどうしたらいいでしょうか?
そこで、現在筆者が所属しているプロジェクトではワークフローの定義をGitHub Actionsで管理し、その実行環境(セルフホステッドランナー)としてビルドターゲット毎に建てたEC2を必要な時だけ起動するという方法を採用することにしました(画像再掲)。
以下、新しいアセットバンドルビルド環境の設計と実装を共有したいと思います。 まずはこの構成がもたらす恩恵やその特性について紹介します。
各実行でワークスペースが維持される
各CIでクリーンな実行環境が提供されるGitHubホステッドランナーと異なり、EC2をセルフホステッドランナーとすることで実行毎にワークスペースを維持できます。 EC2はルートボリューム(ファイルシステム)をEBSに保存するようになっており、これはインスタンスの停止が行われても保持されます。 ボリュームのマウントはネットワーク越しにダウンロードが走るわけではないため、先述した巨大なアセットのキャッシュをストア/リストアすることで実行時間とコストがかかる問題について、Jenkinsと似たアプローチでクリアできます。
マシンスペックを柔軟に設定できる
EC2(オンデマンドVM)は動的にインスタンスタイプを変更できます。これにより、必要な時に必要な分のマシンパワーを柔軟に確保する ことが可能です。キャッシュをなくして1からアセットバンドルビルドしたいけど時間も惜しいような場合などに有効だと思います。
さらに、EC2にアタッチするEBSの容量も後から増やすことが可能です(縮小はできないことに留意)。
実行環境が物理マシン(MacPro)だった時代に比べて、リソース確保の柔軟性というクラウドの恩恵にあやかれるのは大きなメリットではないでしょうか。
各種定義をGitOpsできる
GitHub Actionsを採用することでタスク定義は当然yamlに集約され、定期実行などの設定情報も含めてGitHubで一元管理できます。 レビューを通せる点や一覧性が高いという点で、属人性や保守性の観点で従来構成のデメリットを解消できます。
コンピュート課金削減の取り組み
この構成でもっとも料金がかかりそうな部分はEC2のコンピュート課金でしょう。
そこで、EC2が停止状態の時はコンピュート課金がされないことを利用し、通常インスタンスを停止状態にしておき、アセットバンドルビルドの実行が必要な時だけ起動させることでコストの削減を実現しています(EBSやElasticIPなどの課金は継続発生する点に留意)。
コンピュート費用を気にするならスポットインスタンスを使った方がいいのではという声が聞こえてきますが、その検討は後述します。
実装
以上が大枠の設計とその特性の紹介でした。 EC2インスタンスを使い回すことでアセットバンドルのキャッシュ特性をクリアした上で、従来デメリットであったマシンスペックの柔軟性や定義の保守性を担保できる気がしてきましたね。
次は詳細な実装について記載します。
EC2の作成 / セルフホステッドランナーとして登録
特定のVMをGitHub Actionsのセルフホステッドランナーとして認識させる手順はこちらで公式が提供してくれています。
より具体的なコマンドはRepositoryのSetting>Actions>Runners>New self-hosted runner
から確認できます。
たとえばLinux環境であればhttps://github.com/${owner}/${repository}/settings/actions/runners/new?arch=x64&os=linux
を参照ください。
systemdのserviceとして登録することで、VMの起動と同時にセルフホステッドランナーとして認識させるためのコマンドです(インスタンス起動時、下画像のステータスがidleに変化する)。
登録処理が済んだらそれぞれのtargetだとわかりやすいようなラベルをつけておきましょう。 Actionsのruns-onで指定する時に必要になります。
ちなみに、EC2はuser_dataでVM作成時に任意のスクリプトを実行させることができるため、ここに上記ランナー登録処理を設定しています。
また細かいところで言うと、万が一インスタンスがシャットダウンされてもEBSが消失しないように、delete_on_terminationはfalseにしています。
また、インスタンスはSSHの他にRDP(Remote Desctop Protocol)接続を許可していることにも言及しておきます。 これはランナーが物理PCだった時代と同様にデスクトップ画面でUnityを開きそれを確認しながらデバッグしたいという要望対応です。
GitHub Actions定義
インフラの準備が整ったらGitHub Actionsの定義です。 流れは以下になります。
- EC2インスタンスの起動(on GitHubホステッドランナーなど)
- アセットバンドルビルド(on EC2セルフホステッドランナー)
- EC2インスタンスの停止(on GitHubホステッドランナーなど)
yaml定義のイメージを以下に示します。 on: workflow_call句を用いることでJobの定義とその実行環境を切り分け・組み合わせています。
# .github/workflows/build-pipeline.yml
name: Build Pipeline
run-name: ${{ github.workflow }} ${{ inputs.target }} ${{ inputs.octo-tag }} / @${{ github.actor }}
on:
workflow_dispatch:
inputs:
target:
type: choice
required: true
description: ビルドターゲット # 本当はmacos/windowsとかもある
options:
- ios
- android
# その他パラメータ
concurrency:
group: ${{ github.workflow }}-${{ inputs.target }}
cancel-in-progress: false
jobs:
# EC2の起動 on GitHubホステッドランナーなど (OIDC認証経由でawsコマンド叩く)
start-instance:
uses: ./.github/workflows/start-instance.yml
secrets: inherit
with:
target: ${{ inputs.target }}
# アセットバンドルビルド on EC2
build:
needs: [ start-instance ]
uses: ./.github/workflows/build.yml
secrets: inherit
with:
platform: ${{ inputs.target }}
runner-label: build-${{ inputs.target }}-asset-bundle-runner
# EC2の停止 on GitHubホステッドランナーなど
stop-instance:
if: always()
needs: [ build ]
uses: ./.github/workflows/stop-instance.yml
secrets: inherit
with:
target: ${{ inputs.target }}
ここまでの実装で、必要な時だけEC2を立ち上げその上でアセットバンドルビルドのジョブを走らせることができるようになりました。 アセッ トバンドルビルドの実行者が全員GitHubアカウントを持っている想定の場合、すべての実装がここで完了します。
いかがでしょうか、蓋を開けてみれば拍子抜けするほどシンプルな構成でしたね。 アプリケーション開発と並行して基盤も見なくてはならない弊社SWEの特性上”無理せずに移行・運用できる”ことを目標に設計したので、オートスケールなどの機能は入れない必要十分な実装になりました。
非エンジニア向けUI(管理画面)作成
最後に、プランナーやクリエイターなどGitHubアカウントを持っていないメンバーでもアセットバンドルビルドのワークフローを発火できるように専用の管理画面を作成します。 画面をComponent化して、GitHubWorkflowIDを渡せばどんなワークフローにも対応できるようにしています(画像再掲)。
ちなみに、GitHubアカウントを持っていない人に対して実行ログやアーティファクトを公開する際は以下のAPIが有用です。 有効期限1分で認証が必要ないURLを発行してくれます。
上記管理画面にある出口のようなアイコンからアーティファクトの取得が、時計のようなアイコンからログの取得ができます。
システム構築は以上になります。
最後にQ&A
なぜスポットインスタンスを採用しなかったのか
今回はセルフホステッドランナーとしてEC2のオンデマンドVMを採用しました。 コンピュート費用がより安価なスポットインスタンスを使う構想もあったのですが、以下理由から見送りました。
- 市場にリソースが余っていないと起動できない
- 動的にインスタンスタイプを変更できない
- (中断動作を設定できるとは言え)意図しない瞬断リスクがある
状況に応じてボリュームをアタッチし直したり、オンデマンドVMとの併用を検討したりすることで、スポットインスタンスを採用することは確かにできるのかもしれません。 一方で、VM瞬断におけるキャッシュ不整合などのトラブルシュートコストや、システムの複雑性が上がることを考えると、節約できる料金以上の恩恵はないのかなと判断しました。 オンデマンドVMであっても基本的には停止状態になるので、費用面の懸念がそこまでなかったということも理由です。
非エンジニア向けのUIでSlackは検討したか
検討しました。認証をワークスペースに預けられるという利点もありますし、そちらでもいいなと思います。 今回管理画面での実装を選択した理由は、一覧性や拡張性が気持ち高いと感じたことや、すでに管理画面でRPC(API)単位でロールを管理できる認証機能が存在していてそれにあやかれると思ったからです。 正直ここはお好みかと思います。
AWSを選定した理由
これは単に弊社が管理ツールをAWSに寄せる構成をとっているためです。 ランナーのVMに使用するクラウドサービスはよしなに選択していただければと思います。
おわりに
本記事ではアセットバンドルビルド環境をJenkinsからGitHub Actions(EC2)に移行した取り組みについて紹介しました。 アプリケーション開発と兼務しながら基盤運用を行うことに焦点を当て、なるべく運用コストがかからない構成にできたと思います。
この記事が皆様のUnityビルド環境構築において少しでもご参考になれば幸いです。