Workload Identity 連携を使って、Github ActionsでGoogle Cloud Storageにアクセスする

Github ActionsでGoogle Cloud Storageにアクセスし、バケットからデータをダウンロードできるようにする。Google Cloud Storageへの認証は、Workload Identity 連携を使用する。

背景は、Github Actionsで定期的にビルドしているgitリポジトリ内にそれなりに大きい(数十MB以上)jsonファイルがいくつかあり、当初はGit LFSにデータを置いてそこからダウンロードしていたが、このダウンロードするデータ量がGithubが無料で提供しているGit LFS帯域幅1ヶ月あたり10GiB)をそれなりに使用していたことが問題だったことから。将来的に無料枠を使い切る懸念があったため、代わりにGoogle Cloud Storageからダウンロードすることを試みた。Google Cloud Storageだと特定のリージョン(us-west1とか)において1ヶ月あたり100GBまでの外向きのデータ転送が無料なので、Githubの10GiBよりは余裕がある。また、当該jsonファイルはそもそもバージョン管理する必要もなかったため、その点でもgit LFSをやめる動機になった。

※上記の無料枠の情報は2025年9月時点での話のため、注意。

前置き

Google Cloudの認証の話

Github ActionsからGoogle Cloudにアクセスするために、一般的な感覚だと、Google Cloud側で何らかの鍵(APIキーのようなもの)を事前に発行しておいて、その鍵をGithub Actions側で使って認証することで、Github ActionsからGoogle Cloudのデータにアクセスできるようになる、といったことを想像すると思うが、実際にはもう少しややこしかった。

まず、Github ActionsでGoogle Cloudに認証するために使うアクションとして、'google-github-actions/auth'がある。(この記事では、@v2を使った)

アクションとしてはこれ1つなのだが、アクションで認証する方法が以下の通りいくつもある。(README.mdを参照のこと)

  • Workload Identity 連携
  • Workload Identity 連携(サービスアカウント経由)
  • Service Account Key JSON
  • Generating OAuth 2.0 access tokens
  • Generating ID tokens

一番上のWorkload Identity 連携(英語だと、'Direct Workload Identity Federation')が推奨されている。

ややこしいのは、2つ目にもWorkload Identity 連携があること(ただしこちらは、サービスアカウント経由)。英語だと'Workload Identity Federation through a Service Account'で、これは、Workload Identity 連携ではあるのだが、その引数にサービスアカウントを指定する。

サービスアカウントというのは、人間ではなくアプリケーションやプログラムがGoogle Cloudのリソースにアクセスするための特別なGoogleアカウントのことで、Google Cloud上で何ができるかの権限がサービスアカウントに紐づくため、基本的にはサービスアカウントは認証情報と同じで機密情報として秘匿すべきものらしい。サービスアカウント単体ですぐに情報が漏れるわけではないが、攻撃者からすると攻撃の入口になってしまうらしい。

よって本記事では、サービスアカウントを指定しないWorkload Identity 連携を使ってGoogle Cloudに認証する。

Google Cloudの認証の話(おまけ)

ちなみに、Workload Identity 連携のサービスアカウントの指定の有無は、Github Actionsでいうと以下の違いになる。

Workload Identity 連携

- uses: 'google-github-actions/auth@v2'
  with:
    workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}

Workload Identity 連携(サービスアカウント経由)

- uses: 'google-github-actions/auth@v2'
  with:
    workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
    service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

※サービスアカウントもsecrets(や環境変数など)で指定するのでログなどで簡単に見えるわけではないのだが、そもそも指定する必要さえない方がセキュリティ的にはよりいいよねという話(たぶん)。

設定

前置きが長くなった。

Google Cloudの設定

Workload Identityプロバイダを作成する

「IAMと管理」→「Workload Identity 連携」→「プールを作成」

IDプールを作成する

項目 備考
名前 (適当に書く) 例:Github Actions Pool
説明 (適当に書く)

プールにプロバイダを追加する

項目 備考
プロバイダの選択 OpenID Connect(OIDC) OpenID Connect - GitHub Docs
プロバイダ名 (適当に書く) 例:Github Actions Provider
プロバイダID (適当に書く) 例:github-actions-provider
発行元(URL) https://token.actions.githubusercontent.com Google Cloud Platform での OpenID Connect の構成 - GitHub Docsに記載されている
オーディエンス デフォルトのオーディエンス

プロバイダの属性を構成する

属性のマッピング

Google OIDC 備考
google.subject assertion.sub
attribute.repository assertion.repository

属性条件

備考
attribute.repository == "octcat/oct-repo" 左記は、octcatというGithubアカウントのoct-repoというリポジトリの例

個人的にはこの「属性のマッピング」と「属性条件」が、当初Workload Identity 連携を理解する上で、一番意味がわからなかった。

以下は自分なりの理解。

「属性のマッピング」とは、OIDCプロトコルが提供するデータの各属性を、Google Cloudが認証で使う属性に対応付け(or 関連付け)ること。対応付けるので、「マッピング(Mapping)」という名前になっている。

ちなみにOIDCプロトコルが提供するデータは「OIDCトークン」と呼ぶらしく、OpenID Connect - GitHub Docsの「OIDCトークンの概要」のところに実際にGithubが提供するデータの例が記載されている。その記載を一部抜粋したのが以下。

{
  "sub": "repo:octo-org/octo-repo:environment:prod",
  "environment": "prod",
  "aud": "https://github.com/octo-org",
  "ref": "refs/heads/main",
  "repository": "octo-org/octo-repo",
  "repository_owner": "octo-org"
}

jsonデータになっていて、よく見るとGithubリポジトリ名やアカウント名、ブランチ名などなどに関するデータが含まれている。これらのデータが認証時にGoogle Cloudへ提供される。

「属性条件」とは、認証OK、認証NGの判定基準。上記で設定したattribute.repository == "octcat/oct-repo"の場合、Githubリポジトリ"octcat/oct-repo"からの認証リクエストはOKとし、それ以外はNGとするということ。なのでこの条件を変えることで、例えばアカウント名が自分のアカウントだったらすべて認証OKとか、特定のリポジトリの特定のブランチだけ認証OKにする、とかができる。

仮にこの属性条件を指定しないと、ここで作成したWorkload Identityプロバイダについて、Githubからの認証リクエストはすべてOKになる(つまり、別の誰かがこのWorkload IdentityプロバイダのIDを入手したら、Github Actionsから限定ではあるが、あなたのGoogle Cloudにアクセスできるということ。たぶん)。

Workload IdentityプロバイダにGoogle Cloud Storageのアクセス権限を付与する

「Cloud Storage」→「バケット」→(アクセス対象のバケット)→「権限」→「アクセスを許可」

プリンシパルの追加

項目 備考
新しいプリンシパル principalSet://iam.googleapis.com/projects/(自分のPROJECT_NUMBER)/locations/global/workloadIdentityPools/(自分のPOOL_ID)/attribute.repository/octcat/oct-repo 左記はoctcat/oct-repoというリポジトリの場合

公式マニュアルの'All identities in a workload identity pool with a certain attribute'の箇所を参照すると、

principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/attribute.ATTRIBUTE_NAME/ATTRIBUTE_VALUE

とあり、PROJECT_NUMBER, POOL_ID, ATTRIBUTE_NAME, ATTRIBUTE_VALUEは自分の環境に応じた値を設定する必要がある。

PROJECT_NUMBER, POOL_IDは、さきほど作成したWorkload Identityプロバイダの詳細画面で確認できる。

「IAMと管理」→「Workload Identity 連携」→「Github Actions Pool(上記の例だと)」へ行き、プールの詳細に「IAMプリンシパル」という項目がある。以下のようになっているはずなので、PROJECT_NUMBER, POOL_IDを確認する。

principal://iam.googleapis.com/projects/(自分のPROJECT_NUMBER)/locations/global/workloadIdentityPools/(自分のPOOL_ID)/subject/SUBJECT_ATTRIBUTE_VALUE

なおプリンシパルに関して「principalSet」と「principal」(Setが付いてる、付いてない)は意味が違うので注意。

ロールを割り当てる

「Storageオブジェクト閲覧者」(いわゆるread権限)を割り当てる。write権限も必要であれば、「Storageオブジェクト管理者」などを設定する。

Github Actionsの設定

ジョブの設定

steps:
  - name: Checkout
    uses: actions/checkout@v4
  - id: 'auth'
    uses: 'google-github-actions/auth@v2'
    with:
      project_id: ${{ secrets.GCP_PROJECT_ID }}
      workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
  - name: 'Set up Google Cloud SDK'
    uses: 'google-github-actions/setup-gcloud@v2'
    with:
      version: '>= 363.0.0'
  - name: 'Use gcloud CLI'
    run: 'gcloud info'
  - name: 'Access Cloud Storage'
    run: 'gcloud storage cp gs://YOUR_BUCKET_NAME/hoge.json' ./

注意点

  • 'google-github-actions/auth@v2'よりも前に'actions/checkout@v4'を実行しておくこと。README.mdにPrerequisitesとして記載されているため。
  • 'google-github-actions/auth@v2'のproject_idは、Cloud Storageのバケットが所属しているプロジェクトのIDを設定すること。
  • 'google-github-actions/auth@v2'のworkload_identity_providerは、projects/(自分のPROJECT_NUMBER)/locations/global/workloadIdentityPools/(自分のPOOL_ID)/providers/(自分のPROVIDER_ID) の形式で書く。
  • 'google-github-actions/setup-gcloud@v2'のバージョンは、363.0.0以降にすること。README.mdに、Workload Identity連携はそのバージョン以降が必要と記載されているため。
  • 上記の例では、GCP_PROJECT_ID, GCP_WORKLOAD_IDENTITY_PROVIDERは、Github Actionsのsecretsに登録している。
  • 最後のgcloud storage cpでGoogle Cloud StorageからGithub Actionsのローカルにファイルをコピーしている。

一発でうまくいくことの方が少ないと思われるため、トラブルシュートとしては、各ステップごとに成功するかを前から順番に確認していく。

'google-github-actions/auth@v2'は、Workload Identityプロバイダが作成できていて、引数workload_identity_providerに適切な値を渡せていれば成功する。project_idは渡さなくても成功したはずで、ただしproject_idが指定されていないというWarningのようなメッセージが出た気がする(たしか)。

'google-github-actions/setup-gcloud@v2'は、自分の環境では失敗しなかった。

'gcloud info'は、authが成功していれば、成功する。このコマンドが成功していれば、少なくともGoogle Cloudには認証したことを示す。

'gcloud storage cp'が、自分の場合、一番ハマったところだった。エラーとしては、対象のCloud Storageのバケットにアクセス権限がない、というのが大半だった。Workload Identityプロバイダに対して、対象のCloud Storageのバケットのアクセス権限を適切に付与できていなかったのだが、その原因となったのが主に「属性のマッピング」と「属性条件」だった。この2つは設定値が変な値、辻褄が合わない値でも設定自体はできてしまうので、正しい値を設定できているのか否かの判別が難しかった。

最後に

Workload Identity 連携に関して、ネット上ではサービスアカウントを使用する場合の情報が多く、今回のようにサービスアカウントを使用しない場合の情報は少ない印象だった。

GeminiにWorkload Identity 連携について質問しても、サービスアカウントを紐付けたWorkload Identity 連携についての回答しかしてこなかったので、それなりに悪戦苦闘した。

最後にはなんとかGithub ActionsからGoogle Cloud Storageへアクセスできるようになったので、定期的なビルド時にGit LFSを使わずに数十MBのjsonファイル群をダウンロードできるようになった。

参考

Unity FAQまとめ(自分用)

UnityのFAQのまとめ(自分用)です。

シーン

シーンをロードする

using UnityEngine.SceneManagement;

SceneManager.LoadScene("Main"); // シーン名「Main」をロードする
SceneManager.LoadScene(SceneManager.GetActiveScene().name); // 現在のシーンをロードし直す

参考

データの永続化

シーン間でデータを共有する

UnityではシーンをロードするときにそのシーンのHierachyウィンドウ配下のGameObjectが生成され、シーンがアンロードされるときにそのシーンのHierachyウィンドウ配下のGameObjectが破棄される。つまり、シーンが切り替わる際、遷移元のシーンに紐づくGameObjectはすべて破棄される。UnityのSceneManagerがデータを遷移先シーンに引き継ぐ用のAPIを用意しているわけでもないため、シーン間でデータを共有するにはそれ用の実装が必要になる。

シーン間でデータを共有する方法はいくつかある。

方法1:DontDestroyOnLoadを使う

DontDestroyOnLoadを使うと、シーンのアンロード時に特定のGameObjectは破棄しないようにできる。これを利用して、いわゆるSingleton的な実装でシーン間でデータを共有する。

初期シーンで空のゲームオブジェクト(GameManager)を作成し、以下のGameManager.csをアタッチする。

HierachyウィンドウのDontDestroyOnLoad配下にGameManagerが存在し続けるので、それを介してシーン間でデータを共有できる。

public class GameManager : MonoBehaviour
{
    public static GameManager Instance;
    public int SharedData;

    void Awake()
    {
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }
}

GameManagerの利用者側で以下のように参照すればよい。

GameManager.Instance.SharedData = someData;

備考

  • 上記の例ではInstanceやSharedDataにを直接参照しているが、実際にはgetter/setterなどのメソッドを介してデータに安全にアクセスすべき。
  • Awake時にInstanceがすでにあるときはDestroy(gameObject)するのがイマイチだが、MonoBehaviourを継承してUnityのフレームワークに乗る以上そうせざるを得ない。代替手段は、MonoBehaviourを継承しない自作クラスでSingletonを実装すること。ケースバイケース。

参考

方法2:LoadSceneMode.Additiveを使う

いわゆるマネージャーシーンに共有用のゲームオブジェクトを置いて、その上で個別のシーンをLoadSceneMode.Additiveを指定してロードするやり方。

TODO

参考

ゲームのデータを保存する

いわゆるセーブ機能の実装。ローカルストレージに何らかのファイルの形でデータを保存する。

方法1:PlayerPrefsを使う

ポイント

  • PlayerPrefsは、Unityが提供しているシンプルなkey-valueストアのライブラリ
  • keyはstringのみ、valueはfloat, int, stringの3つ
  • ローカルストレージのパスは指定する必要がなく、PlayerPrefsが隠蔽している

APIは以下のとおり。

// getter
float GetFloat(string key);
int GetInt(string key);
string GetString(string key);

// setter
void SetFloat(string key, float value);
void SetInt(string key, int value);
void SetString(string key, string value);

// deleter
void DeleteAll();
void DeleteKey(string key);

// others
bool HashKey(string key);
void Save();

方法2:JsonUtilityを使う

JSONファイルの保存場所は、Application.persistentDataPathで取得すればOK。

参考

その他

ゲームを終了する

#if UNITY_EDITOR
UnityEditor.EditorApplication.ExitPlaymode();

#else
Application.Quit();

#endif

Unityエディターで起動するときと、本番起動するときとでは終了の仕方が異なるので注意。

参考

npm run devしてENOSPCが出たときの対処方法

ENOSPC自体は空き容量がないことを示すERRNOだが、今回の件だとwatchシステムコールで起きてたので、ファイルの数が多すぎてwatchしきれませんというエラーにあたる模様。ちなみにプロジェクトフォルダ内に少なくとも30万ファイルはあった。watchするファイル数の上限を上げることで対処できた。

環境

  • Laravel (Laravel Sailの環境)
  • Inertia + Vue

エラー内容

vendor/bin/sail upして、vendor/bin/sail npm run devすると、以下のエラーメッセージが出た。

node:internal/fs/watchers:247
    const error = new UVException({
                  ^

Error: ENOSPC: System limit for number of file watchers reached, watch '/var/www/html/storage/app/private/ItemLibrary/582'
    at FSWatcher.<computed> (node:internal/fs/watchers:247:19)
    at Object.watch (node:fs:2490:36)
    at createFsWatchInstance (file:///var/www/html/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:42779:17)
    at setFsWatchListener (file:///var/www/html/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:42826:15)
    at NodeFsHandler._watchWithNodeFs (file:///var/www/html/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:42981:14)
    at NodeFsHandler._handleDir (file:///var/www/html/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:43217:19)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async NodeFsHandler._addToNodeFs (file:///var/www/html/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:43267:16)
Emitted 'error' event on FSWatcher instance at:
    at FSWatcher._handleError (file:///var/www/html/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:44480:10)
    at NodeFsHandler._addToNodeFs (file:///var/www/html/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:43295:18)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  errno: -28,
  syscall: 'watch',
  code: 'ENOSPC',
  path: '/var/www/html/storage/app/private/ItemLibrary/582',
  filename: '/var/www/html/storage/app/private/ItemLibrary/582'
}

なおストレージには十分な空き容量があった。プロジェクトフォルダ内に少なくとも30万ファイル以上置いたあとに起こったので、ファイル数が多すぎてwatchしきれなくなった模様。

対処方法

ググった結果、対処方法としてはどれもwatchの上限をあげるというものだった。それらにならって次のコマンドを実行すると、ENOSPCの問題は解消し、npm run devは正常に起動するようになった。

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

参考

watchman - React Native Error: ENOSPC: System limit for number of file watchers reached - Stack Overflow