サービスアカウントキーを使わずに GitHub Actions で Cloud Functions を自動デプロイする

# 概要
GitHub Actions を使用して Cloud Functions をデプロイできるようにする。
一度設定すれば、GitHub に push したソースを自動で Cloud Functions へ反映できる。

# 前提

なお、ここで扱う Cloud Funtctions は第一世代。
第二世代を扱う場合は実態が Cloud Run なので設定等は微妙に異なるはず。
https://cloud.google.com/functions/docs/concepts/version-comparison?hl=ja

# 設定方法
よく記事で見かける Secret にサービスアカウントキーを設定する方法ではなく、[Workload Identity](https://cloud.google.com/iam/docs/workload-identity-federation?hl=ja) 連携を利用して設定を行う。
サービスアカウントキーを発行する必要がないので、セキュリティ上のリスクを抑えることができる。

# 手順
## GCP 側の設定
ここではコンソール上で設定を行う。
gcloud コマンドを使う設定方法は、[こちらの記事](https://book.st-hakky.com/hakky/how-to-use-workload-identity-gha)にわかりやすくまとまっている。

### サービスアカウントを作成する
「IAM と管理」の 「サービスアカウント」のページを開き「サービスアカウントを作成」を選択。
サービスアカウント名とIDを設定し、必要なロールを付与する。
- Cloud Functions 開発者(roles/cloudfunctions.developer)
- サービスアカウントユーザー(roles/iam.serviceAccountUser)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/356e1d42-342c-09ed-97d0-1371de299249.png)


### Workload Identity プールを作成する
「IAM と管理」の「Workload Identity 連携」のページを開き、「使ってみる」を選択。
![スクリーンショット 2024-04-14 15.20.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/a72998f3-82a6-b7f3-54f6-0bb02b28f363.png)

必要事項を入力する
- プール名：任意の名前
- プールID：任意のID
    - デフォルトはプール名と同じ値
- 説明：空欄でもOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/33691a31-c689-2923-f44a-7aa4123579b5.png)

### プールにプロバイダを追加する
- プロバイダの選択：OpenID Connect（OIDC）
- プロバイダ名：任意の名前
- プロバイダID：任意のID
- 発行元 (URL)：https://token.actions.githubusercontent.com
- JWK ファイル：空欄
    - 指定しなければ発行元、つまり GitHub Actions から取得される
- オーディエンス：デフォルトのオーディエンス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/f447af2c-7023-5e90-ae76-7247897e55a8.png)

### プロバイダの属性を構成する
Google トークンの属性と GitHub Actions から送られるトークンの属性をマッピングする
- google.subject：assertion.sub
    - 必須項目
- attribute.actor：assertion.actor
    - 任意項目
    - これ以外にも色々ある

[公式ブログ](https://cloud.google.com/blog/ja/products/identity-security/enabling-keyless-authentication-from-github-actions#:~:text=GitHub%20Actions%20%E3%81%AE%20ID%20%E9%80%A3%E6%90%BA%E3%81%AE%E8%A8%AD%E5%AE%9A)によると
> 属性マッピングは、GitHub Actions JWT のクレームを、リクエストについて行うことのできるアサーションにマップします（リポジトリや GitHub Action を呼び出すプリンシパルの GitHub ユーザー名など）。これらを使用して、--attribute-condition フラグで認証をさらに制限できます。たとえば、属性の repository 値をマップして、認証を特定のリポジトリに制限するためにこの値を後で使用することができます。

つまり、Google と GitHub Actions の属性情報をマッピングすることで、トークン発行を制限するための属性条件が設定できるということ。
任意の項目には設定したい属性条件に必要なものを設定する。
どんな属性が含まれるかは[こちら](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)を参照。

今回は自分しか触らないリポジトリで GitHub Actions を設定するため、「自分のアカウントIDで実行された場合だけトークンが発行される」という条件を追加した。
`attribute.actor == <GitHubのアカウントID>`

:::note warn
actor とよく似た属性に actor_id があるが、全くの別物なので注意
actor_id には数値が入るようだが、アカウントの何にあたるのかはわからなかった
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/617d3be8-b01e-adc0-ac6b-b1dabcc0e04c.png)

### サービスアカウントへのアクセス権を付与する

作成したプールの詳細画面で「アクセスを許可」を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/3ca4106b-562a-543e-c37f-277747d9ab54.png)

先ほど作成したサービスアカウントを指定し、プリンシパルを選択する。
これによって指定したサービスアカウントが持つ権限を一時的に借りられる。
属性名に指定した値が入っている外部ID（GCP外部からの接続）にだけ権限を与える。
この辺りは筆者も理解しきれていない部分もあるが、複数のリポジトリの GitHub Actions で使い回すことを想定して自分の GitHub アカウントが入っている外部IDに同様の権限を付与する設定にした。

![スクリーンショット 2024-04-14 21.08.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/101de710-552d-27d1-7a3d-cf33bfe66abc.png)

登録後、構成ファイルをダウンロードできる。
このファイル自体は不要だが、audience に記載されたプロバイダのIDはこの後の工程で必要になる。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f313231373036342f62666532336230652d623239312d313633652d663130662d6230363262346661346630322e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/b5fd964c-1c76-89e8-4215-9cad5df116da.png)

中身はこんな感じ

```json
{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider",
  "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "token_url": "https://sts.googleapis.com/v1/token",
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/github-actions@xxxxxxxxxxx.iam.gserviceaccount.com:generateAccessToken",
  "credential_source": {
    "url": "https://token.actions.githubusercontent.com",
    "headers": {},
    "format": {
      "type": "json",
      "subject_token_field_name": "access_token"
    }
  }
}
```


## GitHub Actions 側の設定
### リポジトリシークレットの設定（任意）
念の為に先ほど作成したプロバイダIDと、GCPプロジェクトのIDをシークレットとして設定する。
リポジトリから「Settings」→「Secrets and variables」→「Actions」へ遷移し「New repository secret」を選択。

- WORKLOAD_IDENTITY_PROVIDER_ID（プロバイダID）
- GCP_PROJECT_ID（プロジェクトID）

の2つを登録する。
プロバイダIDは先ほどの JSON の audience の project 以降のパスにあたる
`projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider`

### ymlファイルの作成
リポジトリのルートディレクトリに .github/workflows ディレクトリを作成し、その中に任意の名前の yml ファイルを作成する。

```yml
name: Deploy Cloud Functions
on:
  # mainへのPush時、または手動でワークフローを実行する
  workflow_dispatch:
  push:
    branches: [main]

jobs:
  job_id:
    runs-on: "ubuntu-latest"
    permissions:
      contents: "read"
      id-token: "write"

    steps:
      - uses: "actions/checkout@v4"

      - id: "auth"
        uses: "google-github-actions/auth@v2"
        with:
          workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER_ID}} # 作成したプロバイダのID
          service_account: "github-actions@${{ secrets.GCP_PROJECT_ID }}.iam.gserviceaccount.com" # プロバイダで指定したサービスアカウント

      - id: "deploy"
        uses: "google-github-actions/deploy-cloud-functions@v2"
        with: # Cloud Functions の設定(下記は一例)
          name: "function_name" # 関数名
          entry_point: "main" # エントリポイント
          runtime: "python312" # 関数に使うランタイム
          env_vars: ENV1=hogehoge, ENV2=fugafuga # 環境変数
          source_dir: "cloud_functions" # ルートディレクトリに main.py が無い時はこれでディレクトリを指定する
          timeout: 120 # 関数のタイムアウト時間
          max_instances: 10 # 最大インスタンス数
          region: "asia-northeast1" # リージョン
```

内容は [deploy-cloud-functions](https://github.com/google-github-actions/deploy-cloud-functions) のREADMEに従う。

- on
    - ワークフローを実行する条件を指定する
- `id: "auth"`
    - Workload Identity連携を利用してGitHub ActionsからGoogle Cloudへ認証を行う
    - パラメータの意味は[こちら](https://github.com/google-github-actions/auth#setting-up-workload-identity-federation)を参照
- `id: "deploy"`で Cloud Functions をデプロイする
    - ここに記載した設定で Cloud Functions をデプロイする
    - パラメータの意味は[こちら](https://github.com/google-github-actions/deploy-cloud-functions/blob/main/README.md)を参照

### ワークフローの実行
作成した yml ファイルを push する。
GitHub のリポジトリにアクセスし、Actions タブに選択するとワークフローが表示されるはず。
トリガーの条件を満たしていれば自動で実行されている。
手動で動かす場合は、ワークフロー名を選択して「Run workflow」から実行できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217064/b5576b0d-a99d-5787-f5db-80ca5b6e7696.png)

ジョブが成功すれば Cloud Functions がデプロイされているはず。
失敗した場合は Workload Identity か yml を見直す。

# 終わりに
Workload Identity を利用して GitHub Actions から Cloud Functions をデプロイできるようにしました。もちろん、GitHub Actions や Cloud Functions 以外にも応用が可能です。
サービスアカウントキーを利用するよりも手間はかかりますが、よりセキュアな設定が求められる場合はぜひ利用したい。

# 参考記事
- https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds?hl=ja#github-actions
- https://book.st-hakky.com/hakky/how-to-use-workload-identity-gha
- https://zenn.dev/cloud_ace/articles/7fe428ac4f25c8
    - 正直 Workload Identity についてはこちらの記事の方がわかりやすい