[Cloud Build][CI/CD]GitHubにプッシュしたら自動でGoogle Cloud Functionsにデプロイする


#Overview

Google Cloud Functions（以降、Functions）のデプロイは、私の環境では約3分時間を要する。
リリース手順が多くなるとマスターブランチへのマージ忘れも発生しよくない状態になった。
デプロイをGCP上でできたらGoogle Cloud SDKもローカルにインストール必須じゃなくなるのに…よし！CI/CDだ！！！

今回はCloud Buildを利用して、GitHubのマスターブランチにプッシュしたら自動でテストとデプロイを行えるようにする。
基本的にはドキュメントに沿って作業するが、いくつかハマったのでトラブルシューティングとしてまとめておく。

#Target reader

- Functionsへのデプロイ作業を自動化したい方。

#Prerequisite

- FunctionsのソースはCloud Source Repositories（以降、CSR）からデプロイ可能になっている状態。
- 使用言語はNode.js


#Body

##どうしてGitHub ActionsではなくCloud Buildなの？

CI/CDを実行する選択肢として[GitHub Actions](https://help.github.com/ja/actions)がある。
どうしてCloud Buildなのかと言われたら明確な理由はない:sweat_smile:
しいて言うなら以下の理由から学習コストの低さを優先したに過ぎない。

- FunctionsのドキュメントにCloud Buildを利用したデプロイ手順が書かれている。
- GitHub Actionsを利用するならデプロイのための権限を付与しないといけない。
    - GitHubからCSRにミラーリングしているので、Cloud Buldなら新しく権限付与の必要はなさそう。

GitHub ActionsではNGという明確な理由はないので、何を優先するかによると思う。

デベロップブランチへのプッシュに対するテスト実行はGitHub Actionsを利用するかもしれない。
Cloud Buildを利用してGitHubのプッシュ等をトリガーにできるようなので、本当に採用するかは不明。


##ビルドトリガー vs GitHubアプリトリガー

GitHub Actionsを回避したのにCloud Buildにも複数の選択肢が:worried:
Cloud Buildには少なくともビルドトリガーとGitHubアプリトリガーという2種類がある模様。
前者はCSRを基本としたトリガーで、後者はその名の通りGitHubをトリガーとしたものと思われる。

ビルドトリガーを使用したビルドの自動化
https://cloud.google.com/cloud-build/docs/running-builds/automate-builds?hl=ja

GitHub アプリトリガーを作成する
https://cloud.google.com/cloud-build/docs/create-github-app-triggers?hl=ja

どっちを採用すべきか決め手を欠いていたとき、以下のFunctionsのドキュメントに救われる。
とりあえずFunctionsがビルドトリガーというので、そちらを採用:sweat_smile:

CI/CD
https://cloud.google.com/functions/docs/testing/test-cicd?hl=ja
FunctionsのCloud Buildに関する記事

この記事は「ビルドトリガーを使用したビルドの自動化」が汎用的であるため、Functionsではどうすればいいか補足されている。
乗るしかないこのビッグウェーブに！
ということで本記事では、「ビルドトリガーを使用したビルドの自動化」に沿って実施し、「CI/CD」で補完することにする。

##手順

作業する前に、本記事の最後に参考資料として載せている以下のドキュメントを読んでおくことをお勧めします。
読んでおくとどのような作業をしているのか何となく理解できるはずです

- 基本的なビルド構成ファイルの作成
- ビルド構成の概要
- アーティファクトのビルド、テスト、デプロイ

上記のドキュメントを読んだことを前提に、前述の「ビルドトリガーを使用したビルドの自動化」に沿って作業を実施します。
私が設定した値や注意点について記述しておきます。

APIを有効にしていない場合は、リンク先にAPIを有効化するボタンがあるのでそれを押下します。
ドキュメントはAPIを有効化を実施済みを前提にしていそうです。

ドキュメントではプロジェクト開いてレポジトリの選択とありますが、既にGitHub連携済みの場合はGitHubミラーリング済みとして表示されています。
右端のポップアップメニューより「トリガーを追加」を選択します。私が設定したトリガーの設定項目は以下。

- トリガーのタイプ
    - ブランチ
- ブランチ（正規表現）
    - ```^master$```
        - masterかdevelopブランチしかない環境でmasterと入力しようとすると、上記の正規表現の入力補助がありました。
- 無視されるファイル フィルタ（glob） 
    - ```.gitignore```
        - 入力例にあったのでそのまま使用。
- ビルド設定
    - Cloud Build構成ファイル（yamlまたはjson）

これでマスターブランチにプッシュやマージがあるとトリガーが発火する準備が整いました。
では発火して何を実施するかはまだ設定していないため、「CI/CD」ドキュメントの「Cloud Build の設定」のyamlファイルを自分のレポジトリに配置します。
私が実際に配置しているファイルを一部編集して載せておきます。
サンプルとパラメータが異なる点がいつくかありますが、説明は次項のトラブルシューティングに譲ります。

```yaml:cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/yarn'
  args: ['install', '--ignore-engines']
  dir: 'functions/autodeploy'
- name: 'gcr.io/cloud-builders/npm:current'
  args: ['test']
  dir: 'functions/autodeploy'
- name: 'gcr.io/cloud-builders/gcloud'
  args: ['functions', 'deploy', '[YOUR_DEPLOYED_FUNCTION_NAME]', '--trigger-topic', '[YOUR_TOPIC_ID]', '--runtime', 'nodejs10', '--retry', '--region=asia-northeast1', '--memory', '2048', '--timeout', '540', '--max-instances', '1', '--source', '<your-source>']
  dir: 'functions/autodeploy'

```

deployのオプションの意味について知りたい場合は公式ドキュメントを参照してください。
https://cloud.google.com/sdk/gcloud/reference/functions/deploy

これでマスターブランチのソースを変更し、Cloud Buildを実行します。（手動実行でもOK）
「CI/CD」ドキュメントの「ビルドとデプロイに必要な権限の付与」については次項のトラブルシューティングで実施します。

**<2020.3.28 追記>**
パッケージのバージョン指定が甘かったため、`npm cit`コマンドを使うように変更しました。
前述のinstallコマンドとtestコマンドを合わせて実行してくれるコマンドです。
更に`package-lock.json`を使用することで、開発環境と同じパッケージのバージョンたちをインストールするようになります。

```yaml:cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/npm:current'
  args: ['cit']
  dir: 'functions/autodeploy'
- name: 'gcr.io/cloud-builders/gcloud'
  args: ['functions', 'deploy', '[YOUR_DEPLOYED_FUNCTION_NAME]', '--trigger-topic', '[YOUR_TOPIC_ID]', '--runtime', 'nodejs10', '--retry', '--region=asia-northeast1', '--memory', '2048', '--timeout', '540', '--max-instances', '1', '--source', '<your-source>']
  dir: 'functions/autodeploy'
```

遭遇したトラブルについて知りたい場合は以下を参照のこと。
https://qiita.com/qrusadorz/items/937d97b177cb0140d1eb


## トラブルシューティング

ここからはビルドで遭遇したエラーのすべてになります。


```text:cloudbuilderのlog
The engine "node" is incompatible with this module. Expected version ">=10".
```

yarnがnode10と一致しないといっています。恐らくデフォルト設定がnode8のためだと思います。
ここではまだわからなかったため、stackoverflowの回答を丸々拝借し、installに無視オプション```--ignore-engines```を追加。
https://stackoverflow.com/a/57748163

```yaml:cloudbuild.yaml(抜粋)

steps:
- name: 'gcr.io/cloud-builders/yarn'
  args: ['install', '--ignore-engines']
  dir: 'functions/autodeploy'

```

次はもNodeのバージョンでエラー。

```text:cloudbuilderのlog
@grpc/grpc-js only works on Node ^8.13.0 || >=10.10.0
```
stackoverflowをみるとどうやらnodeのバージョン指定をすればよさそう。
https://stackoverflow.com/questions/56359586/how-to-upgrade-node-js-version-on-google-cloud-build

以下のようにnodeのオフィシャルイメージを利用することも可能。
https://github.com/GoogleCloudPlatform/cloud-builders/tree/master/npm

しかし、Functionsで動かないなら意味がないため、Cloud Buildが提供しているものを利用したい。
yamlに記述されているnameのURLに行くと以下にリダイレクトされ、Cloud Buildが利用しているイメージ一覧を見ることができる。
https://console.cloud.google.com/gcr/images/cloud-builders/GLOBAL/npm?gcrImageListsize=30

バージョンを固定で指定するより、latestやcurrentがいいだろうということで```npm:current```をyamlに反映。

```yaml:cloudbuild.yaml(抜粋)

- name: 'gcr.io/cloud-builders/npm:current'
  args: ['test']
  dir: 'functions/autodeploy'

```

私は```npm test```でjestのテストが実施されるが、それが正常終了すると以下のようなエラーになる。

```text:cloudbuilderのlog
 (gcloud.functions.deploy) ResponseError: status=[403], code=[Forbidden], message=[Permission 'cloudfunctions.functions.get' denied on resource 'projects/<your-project-id>/locations/asia-northeast1/functions/<your-function>' (or resource may not exist).]
```

これは想定したらエラーなので、「CI/CD」ドキュメントの「ビルドとデプロイに必要な権限の付与」の以下を実施することに。

>Cloud Functions をデプロイするときに、Cloud Functions デベロッパーの役割を Cloud Build サービス アカウント（PROJECT_NUMBER@cloudbuild.gserviceaccount.com）に割り当てることができます。

そうすると次のエラーに代わる。

```text:cloudbuilderのlog
ERROR: (gcloud.functions.deploy) ResponseError: status=[403], code=[Forbidden], message=[Missing necessary permission iam.serviceAccounts.actAs for  on the service account <your-project-id>@appspot.gserviceaccount.com. 
Please grant  the roles/iam.serviceAccountUser role. 
You can do that by running 'gcloud iam service-accounts add-iam-policy-binding <your-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:'.]
```

>Cloud Functions デベロッパー役割を使用する場合は、Cloud Functions ランタイム サービス アカウント（PROJECT_ID@appspot.gserviceaccount.com）に IAM サービス アカウントのユーザー役割を付与する必要があります。

次は二つ目のこれを実施すればいいと思った。
しかし、「IAM サービス アカウントのユーザー役割を付与する」ってなに？:thinking:

https://cloud.google.com/functions/docs/reference/iam/roles?hl=ja#adding_the_iam_service_account_user_role_to_the_runtime_service_account

リンク先のドキュメントでConsoleの手順を実施していくと```Click the Add member button.```でメンバーって何？と壁にぶつかる。
エラーにある```gcloud iam```コマンドを実行しようかと思ったら、prefixを付与してねとあるので更に混乱:tired_face:

もしかして日本語ドキュメントは古いかも？と思って「CI/CD」の英語バージョンを見て最終的に解決した。
（Consoleで実施したCloud Build サービス アカウントにCloud Functions デベロッパー役割を付与したものはいったん削除）
文章からコマンドに置き換わっており、ハマる確率が大幅に減少すると思われる。
https://cloud.google.com/functions/docs/testing/test-cicd?hl=en#granting_permissions_to_run_builds_and_deployments

>Allow the Cloud Build service account to act as the Cloud Functions Runtime service account:
>
```
gcloud iam service-accounts add-iam-policy-binding PROJECT_ID@appspot.gserviceaccount.com \
 --member serviceAccount:PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
 --role roles/iam.serviceAccountUser
```

ここでキーになるのは**PROJECT_NUMBER**。
以下のURLに行くと```@cloudbuild.gserviceaccount.com```に一致するCloud Build用のサービスアカウントが一つあるはず。
https://console.cloud.google.com/iam-admin/iam?hl=ja

@の前に数値が羅列しているが、どうもそれがPROJECT_NUMBERらしい。
以下のドキュメントで理解したが、プロジェクト番号として今までにダッシュボードで何度も見ていたとは:joy:
https://cloud.google.com/resource-manager/docs/creating-managing-projects?hl=ja#identifying_projects

謎は解決したので、あとは自分の```PROJECT_ID```と```PROJECT_NUMBER```に置き換える。
最後に残っている以下のコマンドについても同様に置き換えるだけでOK。

>Assign the Cloud Functions Developer role to the Cloud Build service account, which allows Cloud Build to deploy Cloud Functions:
>
```
gcloud projects add-iam-policy-binding PROJECT_ID \
 --member serviceAccount:PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
 --role roles/cloudfunctions.developer
```

これがすむと正常にデプロイが可能になる。

## トラブルシューティング ２nd

実はこの記事を上げてから運用している中で、なにかソースが古いような気がしていた。
ログの出力を変更し、クラウド実行のログを見ても変化がないことでようやくバグがあることが判明。
やるべきことは、`gcloud function deploy`のargsには必ずソースの指定をすること。
`'--source', '<your-source>'`
Cloud Buildのドキュメントにあるサンプルではこれがないものがあるが、これがないとフェッチしたしたソースとデプロイしたソースのハッシュ値が異なっていた。
ソース指定をすると以下の`【ハッシュ値】`の部分が一致して、正しく最新のmasterブランチのソースが適用されていることが確認できた。
当初developブランチからデプロイしていたのが残ってしまうのか？現在でも説明できない不思議な現象だった。
わかっているのはローカルからソース指定の`gcloud functions deploy`を実行した際のソースリビジョンをかたくなに使用するということ。
だから、必ずソース指定はしないといけないと理解している。

```text:cloudbuildのログcitのステップ

FETCHSOURCE
Initialized empty Git repository in /workspace/.git/
From https://source.developers.google.com/p/<your-project>/r/github_<your-account>_<your-project>
 * branch            【ハッシュ値】 -> FETCH_HEAD
HEAD is now at XXXX Merge pull request #307 from <your-account>/develop
```

```text:cloudbuildのログdeployのステップ

Starting Step #1
Step #1: Already have image (with digest): gcr.io/cloud-builders/gcloud
Step #1: Deploying function (may take a while - up to 2 minutes)...
Step #1: ...........................................................................done.
Step #1: sourceRepository:
Step #1:   deployedUrl: https://source.developers.google.com/projects/<your-project>/repos/github_<your-account>_<your-project>/revisions/【ハッシュ値】/paths/
Step #1:   url: https://source.developers.google.com/projects/<your-project>/repos/github_<your-account>_<your-project>/moveable-aliases/master/paths/
Finished Step #1
```

何かソースが最新じゃないと感じたら、GitHubの最新のコミットハッシュ値とdeployedUrlのハッシュ値を比較すればいい。

## トラブルシューティング ３rd

一旦Functionsを削除して、再度CloudBuildでデプロイしようとしたらエラーが発生。
Functionsが2020年1月にデフォルトは匿名呼び出しがではなくなったことが原因なのかは不明。
とにかく下記の権限エラーが発生した際は、指示に従ってCloud Resource APIの有効にするとビルドでエラーが発生しなくなった。
有効化をクリックして、実際に有効になるのに数分かかるようなので再ビルドは数分後に実行するといい（すぐに実行した人曰く）

```terminal:CloudBuildログ

Step #1: API [cloudresourcemanager.googleapis.com] not enabled on project 
Step #1: [<your-projectNo?>]. Would you like to enable and retry (this will take a 
Step #1: few minutes)? (y/N)?  
Step #1: ERROR: (gcloud.functions.deploy) User [<user_id>@cloudbuild.gserviceaccount.com] does not have permission to access project [<projectId>:testIamPermissions] (or it may not exist): Cloud Resource Manager API has not been used in project <your-projectNo?> before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=<your-projectNo> then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
Step #1: - '@type': type.googleapis.com/google.rpc.Help
Step #1:   links:
Step #1:   - description: Google developers console API activation
Step #1:     url: https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=<your-projectNo>
```



## ビルド結果の通知

テストとデプロイが可能になるとSlackで通知してほしいもの。
以下のドキュメント通りにすれば恐らくハマることなく通知できるはず。
私はもう疲れたので後日実施することに:mask:

ビルド通知の送信
https://cloud.google.com/cloud-build/docs/send-build-notifications?hl=ja
Slackに結果送信に使用する？

サードパーティ サービスの通知の構成
https://cloud.google.com/cloud-build/docs/configure-third-party-notifications?hl=ja
実際にSlackへの通知の実装例あり。

#Conclusion

Cloud Buildについてはどっちを使っていいのかわからなかったり、ロール回りでハマったり時間を浪費しました。
まあそれでもこれからはマスターブランチにプッシュすればデプロイしてくれるので、時間が経てば確実にメリットがあります。
今後はデベロップブランチにプッシュしたときにテスト実行もやりたいですね。

Cloud Buildは無料枠も大きいですし、トリガー作成＋yamlファイル作成+権限付与でCI/CDできるのでいい選択肢だと思います:thumbsup:

Have a great day!

**<2020.5.1 追記>**
ちなみにですが、たまにソースコードは問題なくても`npm cit`やFunctionsのデプロイで失敗することがあります。
その場合はダッシュボードからリトライすれば基本的には成功します。
リトライ指定ができればいいのですが、どうもyamlからは指定できそうにない:thinking:
yamlファイルの構成が恐らくREST APIの下記のドキュメントに該当します。
https://cloud.google.com/cloud-build/docs/build-config?hl=ja
https://cloud.google.com/cloud-build/docs/api/reference/rest/v1/projects.builds?hl=ja

retryオプションあればいいのですが、最下部にretryメソッドとして記述があります。
メソッドとして呼び出す必要がありそうなので、執筆時点では無理だろうと判断しました。
https://cloud.google.com/cloud-build/docs/api/reference/rest/v1/projects.builds/retry?hl=ja


#Appendices
なし

#References

基本的なビルド構成ファイルの作成
https://cloud.google.com/cloud-build/docs/configuring-builds/create-basic-configuration?hl=ja
ビルド構成ファイルの概要を理解できる。
以下の方はさらに詳しく説明している。
ビルド構成の概要
https://cloud.google.com/cloud-build/docs/build-config?hl=ja

アーティファクトのビルド、テスト、デプロイ
https://cloud.google.com/cloud-build/docs/configuring-builds/build-test-deploy-artifacts?hl=ja
npmのinstall、testとFunctionsへのデプロイの記述例あり。

ビルドを高速化する際のおすすめの方法
https://cloud.google.com/cloud-build/docs/speeding-up-builds?hl=ja
>.gcloudignore ファイルを使用して、ビルドに不要なファイルを除外します。

GitHub でのビルドの実行
https://cloud.google.com/cloud-build/docs/run-builds-on-github?hl=ja
GitHub アプリトリガーとの違いが不明。