GitHub Actionsを使ってGCP Cloud Functionsをデプロイする(IAMエラーの解消方法)

## TL;DR;

- GitHub ActionsでCloud Functionsをデプロイしようとした
- 単純に「Cloud Functions開発者」ロールを付与したサービスアカウント( `ci-deploy@PROJECT_ID.iam.gserviceaccount.com` とする )でデプロイしようとしたらエラーになった
- 下記コマンドでデフォルトのサービスアカウント `PROJECT_ID@appspot.gserviceaccount.com` を `ci-deploy@PROJECT_ID.iam.gserviceaccount.com` にバインドして実行したら無事デプロイできた。

```sh
gcloud iam service-accounts add-iam-policy-binding PROJECT_ID@appspot.gserviceaccount.com \
  --member='serviceAccount:ci-deploy@PROJECT_ID.iam.gserviceaccount.com' \
  --role=roles/iam.serviceAccountUser
```

## 実現したいこと

devブランチにマージしたときに、GitHub ActionsでCloud Functionsをデプロイすること。

## IAMエラー発生

### サービスアカウントを作成

1. GitHub Actionsでデプロイを実行するサービスアカウントとして `ci-deploy@PROJECT_ID.iam.gserviceaccount.com` を作成
2. 「Cloud Functions開発者(roles/cloudfunctions.developer)」ロールを付与
3. サービスアカウントのJSONキーを作成
    - 参考: [GCP サービス アカウントキーの作成と管理](https://cloud.google.com/iam/docs/creating-managing-service-account-keys?hl=ja#creating_service_account_keys)

### GitHub Secretsの設定

- サービスアカウントのJSONキーの中身を `GCP_SA_KEY_DEV` という名前でセット

### GitHub ActionsのWorkflowを定義

下記Workflowを定義します。
Actionsの中身の説明は本旨ではないので割愛します。
gcloudを実行するために使っている GoogleCloudPlatform/github-actions/setup-gcloud については、リポジトリを参照してください。
https://github.com/GoogleCloudPlatform/github-actions/tree/master/setup-gcloud


```yaml
name: Deploy Cloud Functions(Dev)

on:
  push:
    branches:
      - dev

jobs:
  build-deploy:
    name: Deploy Functions
    runs-on: ubuntu-latest

    env:
      REGION: asia-northeast1

    steps:
      - uses: actions/checkout@v2
      - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
        with:
          # OSSであれば project_id や service_account_email もsecretsに入れるのが良いでしょう
          project_id: PROJECT_ID
          service_account_email: ci-deploy@PROJECT_ID.iam.gserviceaccount.com
          service_account_key: ${{ secrets.GCP_SA_KEY_DEV }}
      - name: Deploy Functions
        run: |
          gcloud functions deploy FUNCTION1_NAME --source ./src/FUNCTION1_NAME --region=${REGION}
          gcloud functions deploy FUNCTION2_NAME --source ./src/FUNCTION2_NAME --region=${REGION}

```

### GitHub Actionsの実行

任意のfeatureブランチからdevにマージしてGitHub Actionsを起動します。
すると、`gcloud functions deploy` で下記エラーが発生します。

```
[Missing necessary permission iam.serviceAccounts.actAs for  on the service account PROJECT_ID@appspot.gserviceaccount.com. 
Ensure that service account PROJECT_ID@appspot.gserviceaccount.com is a member of the project PROJECT_ID, and then grant  the roles/iam.serviceAccountUser role. 
You can do that by running 'gcloud iam service-accounts add-iam-policy-binding PROJECT_ID@appspot.gserviceaccount.com --member= --role=roles/iam.serviceAccountUser' 
In case the member is a service account please use the prefix 'serviceAccount:' instead of 'user:'.]
```

## IAMエラー解消

### IAMアカウントにロールを追加

エラーメッセージを見て、え？ `PROJECT_ID@appspot.gserviceaccount.com` とかお呼びじゃないんだけど？
いや、実行するのに使ってるのは `ci-deploy@PROJECT_ID.iam.gserviceaccount.com` なんだけど？
などと思いながらも、メッセージの指示通り下記コマンドを実行します。

```sh
gcloud iam service-accounts add-iam-policy-binding PROJECT_ID@appspot.gserviceaccount.com \
  --member='serviceAccount:ci-deploy@PROJECT_ID.iam.gserviceaccount.com' \
  --role=roles/iam.serviceAccountUser
```

これにより、 `PROJECT_ID@appspot.gserviceaccount.com` にroles/iam.serviceAccountUserロールが付き、かつ `ci-deploy@PROJECT_ID.iam.gserviceaccount.com` がバインドされるようです。

なお、バインド情報は下記コマンドで確認できます。
(GCPコンソールで確認する方法は不明です)

```sh
> gcloud iam service-accounts get-iam-policy PROJECT_ID@appspot.gserviceaccount.com
bindings:
- members:
  - serviceAccount:ci-deploy@PROJECT_ID.iam.gserviceaccount.com
  role: roles/iam.serviceAccountUser
etag: xxxxxxxxxx
version: 1
```

### GitHub Actionsの再実行

上記コマンドを実行した上で、再度、GitHub Actionsを実行(GitHub上でRe-run)すると、無事、Cloud Functionsのデプロイに成功しました。


## IAMエラーの原因究明(半分くらい)

しかし、この時点では、結局、何が問題であって、なぜ上手くいったのか理解できていませんでした。
人に聞いたり、調べたりしてみたところ、リファレンスに

https://cloud.google.com/functions/docs/concepts/iam?hl=ja#runtime_service_account

> デフォルトでは、Cloud Functions はランタイムに App Engine のデフォルトのサービス アカウント（PROJECT_ID@appspot.gserviceaccount.com）を使用します。

という記述を見つけます。
ここで `gcloud functions deploy` のエラーコマンドに立ち戻ってみると、

```
Missing necessary permission iam.serviceAccounts.actAs for  on the service account PROJECT_ID@appspot.gserviceaccount.com.
```

つまり、 `PROJECT_ID@appspot.gserviceaccount.com` が、指定したサービスアカウントとして振る舞う(iam.serviceAccounts.actAs)権限が無い、ということを言っています(たぶん)。
この権限を与えるのが先ほどのコマンド(再掲)

```sh
gcloud iam service-accounts add-iam-policy-binding PROJECT_ID@appspot.gserviceaccount.com \
  --member='serviceAccount:ci-deploy@PROJECT_ID.iam.gserviceaccount.com' \
  --role=roles/iam.serviceAccountUser
```

のようです。


## まだ残る疑問

ただ、 `PROJECT_ID@appspot.gserviceaccount.com` は、あくまでFunctionsのランタイムのIAMユーザであって、デプロイ時のものではないと、私は認識しています。
むしろ、こちらに記述のある、

https://cloud.google.com/functions/docs/concepts/iam?hl=ja#cloud_functions_service_account

> Cloud Functions サービスでは、関数の作成、更新または削除中にプロジェクトの管理アクションを実行するため、Google Cloud Functions サービス エージェント アカウント（service-PROJECT_NUMBER@gcf-admin-robot.iam.gserviceaccount.com）を使用します。

`service-PROJECT_NUMBER@gcf-admin-robot.iam.gserviceaccount.com` がデプロイに使うIAMユーザなのではないかと。
だとすれば、権限を付けるべきユーザはこちらなのでは……しかし、デプロイは上手く動いてるし……謎。
GCPのIAM周り、難しいですね。


## 謝辞
エラー原因について相談の乗ってくれて、いろいろな情報をくれた @inductor さん、@0Delta さん、ありがとうございました。
おかげで「何か知らんが動いた」で終わらせずに済みました。