すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

【Unity Localization】 GCPのサービスアカウントでプライベートSheetsと連携 (Cronジョブ対応含む)

概要

今回も前回に引き続きUnity Localizationに関するTipsです。
GCPのサービスアカウントでプライベートSheetsと連携する方法を紹介します。

Unity Localization標準のOAuth認証を使用すれば一応プライベートSheetsにアクセスすることは可能ですが、Cronジョブなどで定期更新に対応したいときに不都合があります。

そんな問題をGCPのサービスアカウントを用いて解決したいと思います!

おまけでCronジョブでGoogle SheetsからStringTableCollectionを定期更新するTipsも紹介します。

前回の記事

良かったら合わせてどうぞ!

tsgcpp.hateblo.jp

動作環境

  • Unity 2021.3.6f1
    • Unity 2020 でも可
  • Unity Localization 1.3.2

備考

今回紹介する機能は拡張パッケージとしても用意しています。

記事の最後にGitHubへのリンクを記載していますため、よろしければどうぞ。

GCPのサービスアカウント対応手順

サービスアカウントの作成

  • GCPのCredentialsページにてサービスアカウントを作成
    • GCP自体のアカウント作成などは各自調べてください

console.cloud.google.com

サービスアカウントの認証用JSON形式の鍵をダウンロード

以下のようなJSON形式の鍵がダウンロード出来ます。
JSON文字列は後述するクラスに渡すために使用します。

{
  "type": "service_account",
  "project_id": "xxx-workspace",
  "private_key_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...",
  "client_email": "unity-localization-example@xxx.xxx.gserviceaccount.com",
  "client_id": "121212121212121212121",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "..."
}

※アクセス権限に使用する鍵のため、厳重に管理してください

対象のSheetsにサービスアカウントのアクセス権限を付与

Pullのみの場合は閲覧権限、Pushも行う場合は編集権限をサービスアカウントに与えてください。

GCPのサービスアカウント向けSheetsServiceProviderを作成

1.3.2時点のUnity LocalizationにはGCPのサービスアカウント連携用クラスが用意されていなかったため用意します。
いずれ公式から提供される気がしますが、今回は実装します。

    public class ServiceAccountSheetsServiceProvider : IGoogleSheetsService
    {
        private readonly string _serviceAccountKeyJson;
        private readonly string _applicationName;

        public ServiceAccountSheetsServiceProvider(
            string serviceAccountKeyJson,
            string applicationName)
        {
            _serviceAccountKeyJson = serviceAccountKeyJson;
            _applicationName = applicationName;
        }

        public SheetsService Service => GetSheetsService();

        private SheetsService GetSheetsService()
        {
            var credential = GoogleCredential.FromJson(_serviceAccountKeyJson);
            var initializer = new BaseClientService.Initializer
            {
                HttpClientInitializer = credential,
                ApplicationName = _applicationName,
            };
            var sheetsService = new SheetsService(initializer);
            return sheetsService;
        }
    }
  • serviceAccountKeyJson には、先程作成したサービスアカウントのJSON文字列を渡す
  • applicationName には GoogleSheetsService (ScriptableObject) に設定した ApplicationName を指定

GCPのサービスアカウント向けSheetsServiceProviderを使用してPull or Push

  • 先程作成した ServiceAccountSheetsServiceProvider を用いてPull or Pushを実施
    • ※以下の実装は、前回の記事で紹介したコードをServiceAccountSheetsServiceProviderに入れ替えたもの
            // 対象のStringTableCollectionを取得
            var collection = AssetDatabase.LoadAssetAtPath<StringTableCollection>("Assets/<path to StringTableCollection>");

            // GoogleSheetsExtensionをStringTableCollectionから取得
            var sheetsExtension = collection.Extensions.OfType<GoogleSheetsExtension>().FirstOrDefault();

            // *前回の記事と異なり、ここでServiceAccountSheetsServiceProviderを使用
            var serviceProvider = new ServiceAccountSheetsServiceProvider(
                serviceAccountKeyJson: "<GCPサービスアカウントのJSON形式の鍵の文字列>",
                applicationName: "<GoogleSheetsService (ScriptableObject) に設定したApplicationName>");

            // Google Sheetsアクセス用インスタンスを生成
            var sheets = new GoogleSheets(serviceProvider);

            // ※必ずSpreadSheetIdをGoogleSheetsインスタンスに指定してください!
            sheets.SpreadSheetId = sheetsExtension.SpreadsheetId;

            // 対象のStringTableCollection内の全言語(全Locale)のPullを実施
            sheets.PullIntoStringTableCollection(
                sheetId: sheetsExtension.SheetId,
                collection: collection,
                columnMapping: sheetsExtension.Columns);

上記の方法を使用することで、GCPのサービスアカウントでPull及びPushが可能となります。

以上がGCPのサービスアカウントを使用した連携方法となります。


CronによるGoogle SheetsからのStringTableCollectionの定期更新

さて、今回の実質的な本題に移りたいと思います!
今回、GCPサービスアカウントを用いた本当の理由はCron(定期ジョブ)を使ったStringTableCollection更新に対応させるためです!

そもそもなぜOAuthが使えないのか

Cron対応なしで運用する場合はそもそもGCPサービスアカウント対応は不要です。
概要で少し触れましたがプライベートのSheetsには標準のOAuth認証でも連携可能です。

Unity Localization標準のOAuthの問題は、主に以下の2点です。

  • 認証にUnityエディタ上でのUI操作が必要
    • batchモード(CLI)での認証が困難
  • 認証後に生成される認証ファイルが Library/Google/GoogleSheetsService 以下で管理
    • LibraryフォルダはUnityにおけるキャッシュフォルダでもあるため、クリーンビルドを行うCIなどと相性が悪い

というわけで、
batchモードで認証を完結させるために、JSON形式の鍵文字列を渡すだけで認証が可能なServiceAccountSheetsServiceProvider を用意した感じです。

鍵文字列をServiceAccountSheetsServiceProviderに渡す方法

サービスアカウントの鍵はアクセス権限を握るセンシティブなデータファイルなので、鍵の文字列をgitリポジトリ内で管理することはタブーです!

セキュリティを考慮して使用後はキャッシュとして残らない方法と取りましょう。

1. 環境変数で渡す

CIとも相性がよくジョブプロセス単位で管理可能なため、筆者としても環境変数を利用する方法がオススメです!

            const string EnvironmentGoogleServiceAccountKey = "UNITY_LOCALIZATION_GOOGLE_SERVICE_ACCOUNT_KEY";
            var serviceAccountKeyJson = Environment.GetEnvironmentVariable(EnvironmentGoogleServiceAccountKey);

            var serviceProvider = new ServiceAccountSheetsServiceProvider(
                serviceAccountKeyJson: serviceAccountKeyJson,
                applicationName: "<GoogleSheetsService (ScriptableObject) に設定したApplicationName>");

2. ジョブ中のみ鍵ファイルを生成

環境変数を使用できない場合は、ジョブ中のみJSON形式の鍵ファイルを生成する対応が一つの選択肢になります。

以下はbashを用いて環境変数からJSON鍵ファイルを復元する方法です。

$ echo "${UNITY_LOCALIZATION_GOOGLE_SERVICE_ACCOUNT_KEY}" > "<path to key>/service-account-key.json"

あとは、上記鍵ファイルからJSON文字列を取り出して渡すだけです。

            const string JsonKeyPath = "<path to key>/service-account-key.json";
            string serviceAccountKeyJson = File.ReadAllText(keyJsonPath);

            var serviceProvider = new ServiceAccountSheetsServiceProvider(
                serviceAccountKeyJson: serviceAccountKeyJson,
                applicationName: bundle.SheetsServiceProvider.ApplicationName);

ジョブの成否に関わらず、不要になったら生成した鍵ファイル削除を忘れずに!

$ rm "<path to key>/service-account-key.json"

余談、現時点の game-ci/unity-builder では独自の環境変数をメソッドに渡せない

GitHub ActionsでGameCIを使っている人は結構いるかと思いますが、
残念ながらgame-ci/unity-builderbuildMethodでUnity側のメソッドを叩くときに独自の環境変数を渡せないようでした。。。 (知っている方がいたらぜひ教えてください!)

そんな経緯もあって、「ジョブ中のみ鍵ファイルを生成」という方法を紹介しました。

Pull or Push

あとは「GCPのサービスアカウント向けSheetsServiceProviderを使用してPull or Push」と同様に生成したserviceProviderを使用するだけです。

            // Google Sheetsアクセス用インスタンスを生成
            var sheets = new GoogleSheets(serviceProvider);

            // ※必ずSpreadSheetIdをGoogleSheetsインスタンスに指定してください!
            sheets.SpreadSheetId = sheetsExtension.SpreadsheetId;

            // 対象のStringTableCollection内の全言語(全Locale)のPullを実施
            sheets.PullIntoStringTableCollection(
                sheetId: sheetsExtension.SheetId,
                collection: collection,
                columnMapping: sheetsExtension.Columns);

おまけ、GitHub ActionsでSheets連携をCronジョブ対応

サンプルを用意したので、よければ参考にして下さい。

大まかな流れは以下です。

  • GitHub ActionsのSecrets (GOOGLE_SERVICE_ACCOUNT_KEY_JSON_BASE64) から鍵文字列をデコードしてJSONファイルを生成
    • 鍵文字列のBase64対応は必須ではないですが、環境依存な文字(かっこやカンマなど)があっても対応しやすいので入れています
  • Unity側のメソッド PullAllLocalizationTablesFromTempKeyJsongame-ci/unity-builderbuildMethod経由で実行
    • game-ci/unity-builderbuildMethodの使用方法はGameCIのドキュメントを参照
  • Pull完了後は生成した鍵ファイルを削除
  • Commit, Push して Pull Request

ちなみに、サンプルでは以下のようなプルリクエストが発行されます。

github.com

拡張パッケージ (Example含む)

使用方法はREADME.mdの "Feature" を参照

github.com

雑感

Unity Localization第2弾でした。
また時間が空いてしまいました。。。

UnityとGitHub Actionsを活用したワークフローに関するノウハウも結構溜まってきたので、機会があれば紹介したいですね。
次回の記事は決まってませんが、状況が落ち着いたらまた何か書きます。

それでは~