Unity & GitHub Actions & AWSによるCI/CDの検討

参考
記事要約
1. 背景と目的
Unityのアセットバンドル機能
- Unityでは、動画・画像などのメディア資産をプラットフォームごとに最適化するため「アセットバンドル」という仕組みを利用
- これまでアセットバンドルビルドおよびUnityアプリのビルドにJenkinsを採用していた
従来システムの問題点
Jenkinsは永続的なワークスペース(キャッシュの維持)や、非エンジニア向けのUIが提供できる点で有利だが以下の課題があった
- 設定がGUI中心でGit管理(GitOps)がしにくい
- プラグイン依存によるメンテナンスコストの増大
- 物理マシン(MacProなど)の所有・運用コストが発生する
2. 新システムの概要と移行理由
GitHub Actionsの採用
- Jenkinsの代替として、タスク定義をYAMLで管理できるGitHub Actionsに移行することで、設定のGitOps化が実現可能
セルフホステッドランナーとしてのEC2利用
- GitHubのホステッドランナーは毎回クリーンな環境が提供されるため、巨大なアセットキャッシュの管理に不向き
- そこで、EC2インスタンスをセルフホステッドランナーとして利用し、EBSをルートボリュームとしてマウントすることで、ビルド間でのワークスペース(キャッシュ)の維持を実現
管理UIの導入
- GitHub ActionsはGitHubアカウントが必要だが、非エンジニアにも利用してもらうため、専用の管理画面を作成
- GitHubアカウントを持たないユーザーでもビルドの発火、ログやアーティファクトの確認が可能になった
3. 実装の詳細
EC2インスタンスの起動と停止
- ビルド対象ごとにEC2インスタンスを起動し、セルフホステッドランナーとして登録
- ビルド完了後はインスタンスを停止することで、不要なコンピュート料金を削減
- EC2のルートボリュームにはEBSを使用し、インスタンス停止後もキャッシュは保持されるように設定
GitHub Actionsのワークフロー定義
YAMLファイルで以下のプロセスを定義することで、各ジョブ間でのワークスペースの維持やキャッシュ利用が可能になる
- EC2起動(OIDC認証を通じAWSコマンドで実行)
- アセットバンドルビルドの実行(EC2上でセルフホステッドランナーとして動作)
- EC2停止
その他の工夫
- キャッシュ管理:巨大なアセットファイルのキャッシュをネットワーク経由でストア/リストアする負荷を回避
- コスト最適化:通常はEC2を停止状態にしておき、必要な時だけ起動
- オンデマンドVMの採用:リソースの確実な起動、動的なインスタンスタイプ変更の柔軟性、及び中断リスク回避が挙げられる
4. 移行によるメリット
ワークスペースの永続性
- Jenkinsと同様にキャッシュを維持できるため、巨大なアセットバンドルのビルドが高速かつ安定して実行可能
GitOpsによる管理の向上
- ワークフロー定義がYAMLに集約され、レビュー可能な形でGitで管理できるため、設定の透明性と保守性が向上
柔軟なリソース管理
- EC2インスタンスの起動・停止を自動化することで、必要なタイミングで必要なリソースを確保でき、コスト効率が改善
非エンジニア向けUI
- 管理画面の導入により、GitHubアカウントを持たないメンバーでもビルドジョブの発火や結果確認が可能になった
用語
Unityアセットバンドル
Unityにおいて、動画や画像などのメディア資産をプラットフォーム別に最適化・圧縮してパッケージングする仕組み
Jenkins
Java製のオープンソースCI/CDツール
GUIを利用したジョブ設定やプラグイン管理が可能で、永続的なワークスペースを利用できるため、キャッシュが必要なビルドに有利
GitHub Actions
GitHub上で動作するCI/CDプラットフォーム
ワークフローはYAMLで定義され、GitOpsに適した運用が可能。ただし、ホステッドランナーは毎回クリーンな環境を提供する
セルフホステッドランナー
ユーザーが管理する実行環境をGitHub Actionsのジョブ実行に利用する仕組み
これにより、永続的なワークスペース(キャッシュ)が維持できる
EC2(Amazon Elastic Compute Cloud)
AWSが提供する仮想サーバー
オンデマンドでインスタンスを起動・停止でき、必要な時だけ稼働させることでコスト削減が可能
EBS(Amazon Elastic Block Store)
EC2に接続する永続的なストレージ
インスタンス停止後もデータが保持され、キャッシュとして活用できる
GitOps
インフラやシステム設定をGitで一元管理する運用手法
GUI中心のJenkins設定と異なり、YAMLによるコード管理が可能となるため、透明性と保守性が向上する
キャッシュ管理
ビルドプロセスにおいて、前回のビルド成果物や中間成果物を再利用する仕組み
特にUnityのアセットバンドルではデータ量が大きく、永続的なキャッシュ管理が求められる
Workflow Dispatch
GitHub Actionsで手動実行を可能にするトリガー機能
非エンジニア向けUIを構築する際の起点として利用される
YAML
データ記述言語
GitHub Actionsのワークフロー定義に用いられ、バージョン管理・レビューが容易になる
OIDC認証
OpenID Connectによる認証方式
GitHub ActionsからAWSの各種コマンドを実行する際に、セキュアに認証するために利用される
スポットインスタンス
AWSの余剰リソースを低価格で提供する仕組み
ただし、起動タイミングや中断リスクがある

システムの検討
記事を参考に考えてみる
ハードウェア・クラウド構成
- AWS EC2
- Windows Server ベースのインスタンス
- Unity、Visual Studio Build Tools、GitHub Actions ランナーを事前インストール
- インスタンスのルートボリュームは EBS に設定し、キャッシュを保持
- GitHub Actions
- セルフホステッドランナーを利用
- ワークフローで EC2 の起動、ビルド、停止を自動化
AMI 作成
ベース環境の構築
- Windows Server 2022 の EC2 インスタンスを起動
- 必要なツールをインストール
- Unity Hub / Unity Editor(特定バージョン+必要モジュール)
- Visual Studio Build Tools(Windows IL2CPP ビルド用)
- GitHub Actions ランナーのインストール(C:\actions-runner に格納)
- Unity ライセンスの認証設定(services-config.json を作成)
- GitHub Actions ランナーのセットアップ
- config.cmd を実行し、ランナーを設定
- svc.cmd install でサービス登録
- svc.cmd start でバックグラウンド実行
- Sysprep の実施
- AMI として保存
EC2 インスタンス起動
- GitHub Actions の start-instance ジョブで EC2 を起動
- ユーザーデータスクリプトを活用し、ランナーの状態を確認
- 既にランナーが設定済みなら何もしない
- 設定がない場合のみ config.cmd を実行
- 状態を runner_configured.flag で管理し、不要な再登録を防止
ユーザーデータスクリプト
- 初回起動時のみ GitHub Actions ランナーを登録
- EC2 の停止・再起動時には設定を保持
- ランナーの実行状況をログファイルに記録
GitHub Actions ワークフロー
- start-instance ジョブ
- build ジョブ
- stop-instance ジョブ

AWSではないが、セルフホステッドランナーの設定方法がわかりやすく解説されている

アーキテクチャに沿ったテストの実施
DDDを例にすると、
- ドメイン層やアプリケーション層は外部依存が少ないため、ユニットテスト中心でテストが可能
- インフラ層やUI層など外部と連携するレイヤーは、インテグレーションテストも必須
テスト内容
- ユニットテスト
- 純粋なC#ロジック(ドメイン・アプリケーション層)を中心にテスト
- 各メソッドの入力・出力、例外処理、エッジケースなど、独立して動作する小さな単位の検証
- 細かな部分での不具合を早期に発見し、仕様変更に対してコードの安全性を担保
- インテグレーションテスト
- 複数コンポーネントの連携、シーン上での動作、UIやユーザーインターフェイスの挙動など
- ゲームオブジェクトのライフサイクル、アセット(Addressablesを含む)の読み込み、シーン遷移、ユーザー操作シミュレーションなど
- 複数のコンポーネントが連携して正しく動作するか、また実際のプレイ環境に近い形での検証
- パフォーマンステスト/負荷テスト(必要に応じて)
- フレームレート、メモリ使用量、ロード時間など
- 定常状態でのパフォーマンス測定、負荷をかけた場合の挙動の確認
- リリース時にユーザーが体感するパフォーマンスを確保し、ボトルネックを早期に発見
テストコード実装のタイミング
新機能実装時
- プロダクトコードの実装と同時にテストコードを作成
- 機能ごとにテストケースが定義されるため、初期段階から自動テストの実行が可能になり、後からテストコードの追加・修正が必要な部分を減らす
リファクタリングや仕様変更時
- プロダクトコードに変更があった場合、既存のテストコードもそれに合わせて更新・修正
- テストコードはプロダクトコードの動作を保証するためのものであり、コードの仕様変更に合わせたテストの修正がないと誤った結果やテストの不備を招く可能性がある
CI/CDパイプラインの導入・改善時
- セルフホステッドランナーやCI環境上で自動実行できるよう、テストコードはエディタ上で手動実行できる状態と、コマンドライン(バッチモード)で実行できる状態の両方を整備する必要がある
- 手動テストは開発中の確認やデバッグ用として有用だが、CI/CDでは自動化が必須となるため、バッチモードでの実行環境に合わせたテストコードの設定が求められる
プロジェクト初期段階
- プロジェクトの骨組みや主要なドメインロジックが固まり始めたタイミングで、基本的なユニットテストを整備
- 早期にテスト環境を構築しておくことで、後から大規模な変更が入った際の回帰検出や品質担保に役立つ

ブランチとトリガー設定の検討
- featureブランチ
- 機能開発用
- プルリク時およびPush時に、ユニットテストのみを走らせることが多い
- developブランチ
- 開発統合用
- ユニットテスト・インテグレーションテスト・Addressablesビルドを実施
- releaseブランチ
- リリース前の最終検証用。包括的なテストとビルドを行い、ステージングなどに配置
- mainブランチ
- 本番用
- テストがすべて通った段階で本番ビルド+デプロイまで実行
典型的なイベント
on.push.branches と on.pull_request.branches を使って、「どのブランチがPush/PRされたときにどのジョブを回すか」を定義

ジョブの検討
「feature」「develop」「release」「main」等、ブランチごとに異なるジョブを実行
以下の4つのジョブを用意し、ブランチによって実行の有無を切り替え
- UnitTestJob: ユニットテスト実行
- IntegrationTestJob: インテグレーションテスト実行
- AddressablesBuildJob: アドレサブルビルド
- BuildAndDeployJob: リリースビルドやデプロイを実行
※ IntegrationTestJob が UnitTestJob に依存、AddressablesBuildJob がテスト結果に依存、BuildAndDeployJob がそれらに依存、といった順序制御を設定したい
ジョブ投入ルールの検討
- featureブランチ → ユニットテストのみ
- developブランチ → ユニットテスト+インテグレーションテスト+Addressablesビルド
- releaseブランチ → 包括的なテスト+リリースビルド(ステージング向け)
- mainブランチ → 全テスト+本番ビルド+デプロイ