[Java11]Google Cloud BuildからGoogle Cloud Functionsにデプロイする

## 前置き
GCP で Java を動かすには Google App Engine だと思っていたら、今年の春に Cloud Functions でも使えるようになっていました。
[Google Cloud Functions が Java 11 に対応](https://cloudblog.withgoogle.com/ja/products/application-development/introducing-java-11-on-google-cloud-functions/amp/)
※[比較表](https://cloud.google.com/serverless-options?hl=ja#product-comparison)にはまだ載っていない(2020/10/18時点)

これは、Javaで作ったとある処理を Cloud Functions で動かした記録です。

## 環境
- 構成
    - GitHub: ソース置き場
    - Google Cloud Build: GitHubへのpushをトリガーにビルド＆デプロイする
    - Google Cloud Functions: アプリケーションを動かす
        - trigger: HTTP リクエスト（Webhook）
- 言語
    - Java11
    - Gradle
    - yaml

## やったこと
### 1. CGPのプロジェクトでFunctionsを使えるようにする
Cloud Build も Cloud Functions も有効な請求先アカウントにリンクされていないと利用できません。
一応、課金を有効にしてもある程度は無料枠内で使うことができます。
（個人的に使う範囲ならおそらく大丈夫。最初はお試し枠もあるのでその期間なら安心）

次に、APIとサービス > ライブラリから**Cloud Functions API**を有効にします。

### 2. GCPでCloud BuildとGitHubを連携させる
公式ドキュメントを参考に、自分のリポジトリと連携させます。
[ビルドトリガーの作成と管理  |  Cloud Build のドキュメント  |  Google Cloud](
https://cloud.google.com/cloud-build/docs/automating-builds/create-manage-triggers?hl=ja)

イベントとソースの組み合わせで、どのブランチ/タグでいつ発動させるかを指定できます。
今回は、masterにpushしたときにだけ動かすようにしました。
（検証中は `^test.*` にして特定branchを対象にしました）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628295/ca92c8b2-9535-d8af-a60a-fddc3c5dc24e.png)

### 3. JavaをGoogle Cloud Functions で動く形にする
公式ドキュメントを参考に、関数の入り口となるクラスを作ります。
[最初の関数: Java  |  Google Cloud Functions に関するドキュメント](https://cloud.google.com/functions/docs/first-java?hl=ja#gradle_1)

`response.setContentType` は日本語を返さないなら不要だと思います。
今回はプレーンテキストで日本語を返す形にしたので設定しています。

```MessageFunction.java
import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;

public class MessageFunction implements HttpFunction {
  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    response.setContentType("Content-Type: text/plain; charset=UTF-8");

    if (request.getFirstQueryParameter("message").isEmpty()) {
      response.setStatusCode(400);
      response.getWriter().write("Query parameter 'message' is required");
      return;
    }

    String message = request.getFirstQueryParameter("message").get();
    if (message.length() == 0) {
      response.setStatusCode(400);
      response.getWriter().write("Query parameter 'message' is empty");
      return;
    }

    response.getWriter().write(message);
  }
}

```

### 4. Cloud buildでJavaアプリケーションのビルド、デプロイをする
公式ドキュメントを参考に設定ファイルを用意します。
[Java アプリケーションのビルド  |  Cloud Build のドキュメント  |  Google Cloud](https://cloud.google.com/cloud-build/docs/building/build-java?hl=ja)
[Cloud Functions へのデプロイ  |  Cloud Build のドキュメント  |  Google Cloud](
https://cloud.google.com/cloud-build/docs/deploying-builds/deploy-functions?hl=ja)

これだけあれば最低限Functionsにデプロイさせて動かせます。

```cloudbuild.yaml
steps:
  - name: 'gcr.io/cloud-builders/gcloud'
    args:
      - functions
      - deploy
      - Function名を指定する
      - --region=リージョンを指定
      - --source=.
      - --runtime=java11
      - --entry-point=HttpFunctionを実装したクラスをFQCNで指定
      - --trigger-http
```

`- name` がステップの単位となり、順番に処理が進みます。
stepでエラーがあると、複数ステップあってもそこまでで処理が止まります。

Javaのバージョン違いでビルドできなかったり、テスト失敗すると、ちゃんと履歴に「失敗」が表示され、履歴からログ詳細も確認できます。
画像は、デプロイ前にビルドが必要だと思っていたときの履歴です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628295/b351eb40-3e46-c2aa-9fe1-78c1d61a7dad.png)

※App Engineへのデプロイも同様にyamlで設定できます。
[App Engine へのデプロイ  |  Cloud Build のドキュメント  |  Google Cloud](
https://cloud.google.com/cloud-build/docs/deploying-builds/deploy-appengine?hl=ja)

### 5. 確認
うまく動くとこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628295/5cfdbfdd-5ff1-42a6-03bb-444ec6c07609.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628295/d9f5ebab-36c6-2c57-840e-aa0613fef954.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628295/26e25eb5-abc0-d4e1-0118-b264810be76c.png)

## つまづき
### Cloud Buildでgradleが動かない
gradleフォルダに含まれるファイルが必要でした。
ただ、そもそも`gcloud functions deploy`すればOKだったので、これはデプロイとは別でgradleを実行したい場合にだけです。

```
ERROR: (gcloud.functions.deploy) OperationError: code=3, message=Build failed: Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain
Caused by: java.lang.ClassNotFoundException: org.gradle.wrapper.GradleWrapperMain; Error ID: 42fc8383
ERROR
ERROR: build step 0 "gcr.io/cloud-builders/gcloud" failed: step exited with non-zero status: 1
```

### デプロイする権限が足りない
自動生成されるユーザーのままではだめでした。
（Cloud Buildで必ずしもFunctionsをデプロイするわけじゃないので当然といえば当然 😞 ）

Cloud Build > 設定でCloud Functions開発者を有効にします。
その上でIAMと管理からCloud Buildに紐づけられているメンバに、`サービス アカウント ユーザー`の IAM ロール（`roles/iam.serviceAccountUser`）を割り当てます。
https://cloud.google.com/functions/docs/troubleshooting#role-actAs

```
Step #1: Already have image (with digest): gcr.io/cloud-builders/gcloud
Step #1: Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
Step #1: ERROR: (gcloud.functions.deploy) ResponseError: status=[403], code=[Forbidden], message=[Missing necessary permission iam.serviceAccounts.actAs for $MEMBER on the service account アカウント名.
Step #1: Ensure that service account アカウント名 is a member of the project プロジェクト名, and then grant $MEMBER the role 'roles/iam.serviceAccountUser'. 
Step #1: You can do that by running 'gcloud iam service-accounts add-iam-policy-binding アカウント名 --member=$MEMBER --role=roles/iam.serviceAccountUser' 
Step #1: In case the member is a service account please use the prefix 'serviceAccount:' instead of 'user:'. Please visit https://cloud.google.com/functions/docs/troubleshooting for in-depth troubleshooting documentation.]
Finished Step #1
ERROR
ERROR: build step 1 "gcr.io/cloud-builders/gcloud" failed: step exited with non-zero status: 1
```

メンバーを編集 > 別のロールを追加で追加するのですが、ID名の`roles/iam.serviceAccountUser`ではヒットしません…(日本語じゃなくて英語表記にしていれば大丈夫かも)。
また空白の違いも厳密に検索しているみたいなので、日本語名をコピーして検索する必要がありました。
公式ドキュメントの名称をコピーするか、IAMと管理 > ロールからはID名で検索できるのでそこからタイトルをコピーして検索します。


### 作成されたトリガーのHTTPをコールしてもForbiddenになる
現在のFunctionsは、デフォルトで認証ありで作成されます。

ちゃんと認証してアクセスするか、認証なしでもコールできるように設定します。
[IAM によるアクセス管理  |  Google Cloud Functions に関するドキュメント > 認証されていない関数の呼び出しを許可する](https://cloud.google.com/functions/docs/securing/managing-access-iam#allowing_unauthenticated_function_invocation)

```
Error: Forbidden
Your client does not have permission to get URL /xxxxx from this server.
```

### ソースに書いた日本語が文字化けする

JavaファイルをUTF-8で書いていても、Cloud Buildした結果では文字化けが生じていました。
gradle を使っていたのですが、コンパイル時の文字コードにも UTF-8 を指定すると解消します。
(Cloud Buildのデフォルト文字コードなんなんだ…:thinking: )

```build.gradle(抜粋)
tasks.withType(JavaCompile) {
	options.encoding = 'UTF-8'
}

```

## 感想
GCPの権限回りでかなり手こずりました。
でも、これでWebhook作れました :tada: 
次はFunctions同士を連携させたい！