Eventarcを使用した非同期でジョブを実行するSlackAppづくり 

# はじめに
本記事は[Google Cloud + Gaming Advent Calendar 2020](https://qiita.com/advent-calendar/2020/gcp-gaming) 7日目の記事です。
**[Cloud Run](https://cloud.google.com/run)を使った[SlackApp](https://slack.com/intl/ja-jp/help/articles/360001537467-%E3%82%A2%E3%83%97%E3%83%AA%E3%81%A8-Slack-App-%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6#u12450u12503u12522u12392u12399u65311)において普通のVMにおける開発とは違う注意点**があったのでそのあたりを書いていきたいと思います。
なぜCloud Runを使うかについては「[開発環境をGKE運用からサーバーレス運用に変更してみた](https://qiita.com/iyu/items/c57f2d1c28660c8859b1#%E3%81%A9%E3%81%86%E3%81%97%E3%81%A6cloud-run%E3%82%92%E9%81%B8%E3%82%93%E3%81%A0%E3%81%8B)」にて書かせていただきました。こちらも合わせて読んでいただけると嬉しいです。とはいえSlackAppづくりだけであれば同じサーバーレスサービスの[Cloud Functions](https://cloud.google.com/functions)の方が向いているとは思います(笑)。
また本記事でのEventarcについては一部まだpreview版の機能を使用しているため今後の変更により動かなくなることもあるかもしれませんのでご了承ください。

# Eventarcについて
先月発表されましたGCPの新しい機能です。下手な説明するよりもこちらの記事が分かりやすいです。

- [Trigger Cloud Run with events from more than 60 Google Cloud sources](https://cloud.google.com/blog/products/serverless/build-event-driven-applications-in-cloud-run)
- [60 を超える Google Cloud ソースのイベントで Cloud Run をトリガーする](https://cloud.google.com/blog/ja/products/serverless/build-event-driven-applications-in-cloud-run)

簡潔に説明するのなら、、上記タイトルの通りです(笑)。
以前から[Cloud Build](https://cloud.google.com/cloud-build/)や[Cloud Storage](https://cloud.google.com/storage)等のイベントは特定の[Cloud Pub/Sub](https://cloud.google.com/pubsub)トピックにpushされる仕組みがあって（[cloudbuild-notifications](https://cloud.google.com/cloud-build/docs/subscribe-build-notifications), [storage-notifications](https://cloud.google.com/storage/docs/pubsub-notifications)）それを使ってCloud Functions等を動かしていたので、特別使用感が変わることはなさそうですが、Eventarcによってそれらイベントが一元管理されるのかな、ぐらいの認識です。
今後のビジョンとしてはCloud Run意外のたくさんのトリガー(Event Sinkというみたいです)を作れるようになるみたいです。
[Long Term Vision](https://codelabs.developers.google.com/codelabs/cloud-run-events#1)
![vision.png](https://codelabs.developers.google.com/codelabs/cloud-run-events/img/5480b3d19596f0.png)
まだ発表されたばかりなので今後の動きに注目です！

とはいえ現在はCloud RunのみSinkを作れるのでCloud Functionsではまだ使えません。
したがって(?)、**本記事のタイトルはEventarcですが基本的にはCloud Runの話です。** Eventarcのユースケースを考えていたらSlackAppづくりで使えそうと思ったので、、手段が目的になっています。

# Cloud RunにおけるSlackAppづくりの注意点
## 環境変数に機密情報を入れてはいけない
多くの場合SlackApp開発のサンプルコードでは、**SlackのAccessTokenやSigningSecretは環境変数から取得**するものが多いと思います。普通ならそれでも特に問題ないのですがGCPのサーバーレスサービスにおいては大問題です。これはCloud RunだけでなくCloud Functionsも同様です。
GCPのマネージドサーバーレスサービスはログを[Cloud Logging](https://cloud.google.com/logging)に出力してくれます。とても便利です。しかし、使用しているライブラリによっては環境変数をすべてログに出力するようなものもあるみたいなので、環境変数に機密情報を入れていると思わぬところで流出しかねません（外部に、というよりログ閲覧権限しかIAMで振ってない人やサービスに対しても、という意味。）。したがって環境変数に機密情報を格納すべきではないです。

Cloud Run
> [注意: Secret の保存と使用に環境変数を使用しないでください。](https://cloud.google.com/run/docs/configuring/environment-variables)

Cloud Functions
> [環境変数は関数の構成に使用できますが、データベースの認証情報や API キーなどの機密情報の格納には適しません。 このような機密性の高い値は、ソースコードや外部の環境変数以外の場所に保存する必要があります。一部の実行環境やフレームワークでは、環境変数の内容がログに送信されることがあります。](https://cloud.google.com/functions/docs/env-var#managing_secrets)

ただ、そもそもCloud RunやCloud Functionsに限らず環境変数に機密情報は置かないほうが良いと思います。これは個人の意見です。

### 機密情報管理のベストプラクティス
ずばり[Secret Manager](https://cloud.google.com/secret-manager)を使うことです。とても簡単に使えて便利です。
あまりに簡単なので[ドキュメント](https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets#create)通りに作るだけです。普段はgcloudコマンドで作成・更新しています。Makefileにまとめているのでgcloudコマンドを意識してはいませんが(笑)。

#### Secret Managerへアクセスするための設定（Terraform）
普段GCPの設定は[Terraform](https://www.terraform.io/)を使っているのでTerraformにて説明していきます。ただし、Secret自体の作成・更新は性質上terraformのコードとはリポジトリを切り分けたかったのでterraformではなくgcloudコマンドを使っています。

▼ サービスアカウントの作成

```terraform
resource "google_service_account" "this" {
  account_id   = "サービスアカウントのID(なんか適当に)"
  display_name = "CloudRun用のサービスアカウント"
}
```

▼ 作成したサービスアカウントにSecretManagerへのアクセス権を付与

```terraform
resource "google_secret_manager_secret_iam_member" "this" {
  secret_id = "作成したシークレット名"
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.this.email}"
}
```

▼ そのサービスアカウントをCloudRunサービスに使わせる
Cloud Run自体のTerraformはいろいろ省略してますがサービスアカウントの部分だけを見ていただければと思います。
これをしないとデフォルトでは**Compute Engine のデフォルトのサービス アカウント**が使用されます。それは**Editor権限を持ったサービスアカウントなので強すぎます**。[ドキュメントでは専用のサービスアカウントにすることをおすすめする](https://cloud.google.com/run/docs/securing/service-identity#runtime_service_account)程度ですがCloud Runのサービスごとサービスアカウントは分けたほうが良いと思います。これは個人の意見です。
あとEditor権限だけだとSecretManagerへのアクセス権はないので共同開発者にはEditor権限とは別に[Secret Manager Admin権限](https://cloud.google.com/secret-manager/docs/access-control)等も必要かもしれません。

```terraform
resource "google_cloud_run_service" "this" {
  name     = "CloudRunサービス名"
  location = "asia-northeast1"

  template {
    spec {
      containers {
        // 省略...
      }
      service_account_name = google_service_account.this.email
    }
  }
}
```

#### Secret Managerへアクセスする（Golang）
普段Golangで開発することが多いのでGolangでの説明になります。また、実際に動いているコードではなく抜粋して編集しているため、もしかするとそのままだと動かないかもしれません。
また、SecretManagerで保存したテキストは下記のようなyaml形式を想定したものです。

```yaml
SlackAccessToken: xoxb-xxxxxxxxxxx-xxxxxxxxxx
SlackSigningSecret: xxxxxxxxxxxxxxxxxxxxxx
```

```golang
package main

import (
	"context"
	"fmt"
	"os"

	"cloud.google.com/go/secretmanager/apiv1"
	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
	"gopkg.in/yaml.v2"
)

func main() {
	// Cloud RunではPROJECT_IDとPORTが自動でついてきます。
	// httpサーバーをリッスンするときにこのPORTを使います。
	port := os.Getenv("PORT")
	projectID := os.Getenv("PROJECT_ID")
	// SecretManagerのSecretNameはコード埋め込みでも環境変数でもどちらでもいいと思います。
	secretName := "demo-secret"
	// SecretManagerのSecretVersionは環境変数で渡してます。
	// これに関してはコード埋め込みだとCloudRun上では使い勝手悪いと思います。
	secretVersion := os.Getenv("SECRET_VERSION")

	ctx := context.Background()

	// Secret Manager Client作成
	client, err := secretmanager.NewClient(ctx)
	if err != nil {
		panic(err)
	}
	defer client.Close()

	// Secret Managerへアクセスする
	req := &secretmanagerpb.AccessSecretVersionRequest{
		Name: fmt.Sprintf("projects/%s/secrets/%s/versions/%s", projectID, secretName, secretVersion),
	}
	result, err := client.AccessSecretVersion(ctx, req)
	if err != nil {
		panic(err)
	}

	// yamlのunmarshal
	cfg := &config{}
	if err := yaml.Unmarshal(result.Payload.Data, cfg); err != nil {
		panic(err)
	}
}

type config struct {
	SlackAccessToken   string   `yaml:"SlackAccessToken"`
	SlackSigningSecret string   `yaml:"SlackSigningSecret"`
}
```

## slackの制約上3秒以内にレスポンスしたいが非同期でジョブを動かせない
本記事の本題です。
まずslackのslash commandやinteractive eventは[3000ms以内にレスポンスを返さないとタイムアウトしてしまいます](https://api.slack.com/interactivity/slash-commands#responding_to_commands)。
したがってslash command等で重いジョブを動す場合、通常であれば一度slackにレスポンスを返して裏で非同期でジョブを動かす手法が多いと思います。シーケンス図で表すとこのようなイメージだと思います。

```
User -> Slack :　スラッシュコマンド実行
Slack -> 自作SlackApp : webhook発行
自作SlackApp -> 自作SlackApp : Slackからの通信か検証
自作SlackApp -> Slack : 一旦200OKで返す
Slack -> User : スラッシュコマンドが完了する
自作SlackApp -> 自作SlackApp : 任意のジョブ動かす
自作SlackApp -> Slack : chat.postMessage APIを使ってジョブ結果を伝える
Slack -> User : 追加の情報が表示される
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19658/b3fc2969-1fe5-93f7-c6d2-c05d63bd5df5.png)

### Cloud Runはリクエストがなくなればコンテナがなくなる？

> [Cloud Run は、リクエストがない場合はゼロにスケーリングし、リソースを使用しません。](https://cloud.google.com/blog/ja/products/containers-kubernetes/when-to-use-google-kubernetes-engine-vs-cloud-run-for-containers)

リクエスト終了からどのくらい経つとコンテナが終了するのかはわかりませんが、さきほどのシーケンス図でいうところの「一旦200OKで返す」をして**任意のジョブを動かしている最中にコンテナが強制終了してしまうかもしれません**。これに関しては申し訳ありませんが未確認です。

### Webhookプロバイダにタイムアウト制約がある場合の非同期処理
前述の方法でジョブが中断されるかは未確認ではありますが、ドキュメントにはこういったケースの注意事項が記載されています。

> [データ処理が Cloud Run または Webhook プロバイダによって割り当てられた時間を超える場合は、Pub/Sub や Cloud Tasks など、処理を非同期で完了することを許可するプロダクトを使用する必要があります。これらのプロダクトを使用すると、データを迅速に引き渡し、すぐに Webhook プロバイダに成功レスポンスを返して、タイムアウトの心配なく処理を続行できます。](https://cloud.google.com/run/docs/triggering/webhooks#timeouts)

まさにこれはslackのことを言っているのではないでしょうか！
ということで従来であればPub/Subへpushして別のCloud Runがそのpushをトリガーに起動する、といった方法を取るところですが、今回はEventarcを使っていきたいと思います。

### Eventarc設定方法
EventarcについてはまだTerraformもありませんしGCPコンソール上にもありませんのでgcloudコマンドで設定していきます。
ドキュメントは[こちら](https://cloud.google.com/run/docs/events/pubsub#pubsub-trigger)です。
...書いていこうかと思ったのですが、ほぼドキュメントと同じ記述になってしまうのでやめました。
golang側のコードに関しても[こちら](https://github.com/GoogleCloudPlatform/golang-samples/tree/bb2cc0bdf2aca07a939383f0c093ec7f6fd1ef63/eventarc/pubsub)がそのまま使えると思います。
簡単な流れは

- Eventarcをトリガーに起動する用(Event Sink)のCloud Runを作成する。
  - 今回でいうとこれはジョブを実行する用のCloud Runです。
- そのCloud Run(Event Sink)に紐付いたEventarcのpub/subトリガーを作成
- するとpub/subトピックが作成されている
- slackからのレシーバー用Cloud Runからそのトピックへpushする

```
User -> Slack :　スラッシュコマンド実行
Slack -> 自作SlackApp1 : webhook発行
自作SlackApp1 -> 自作SlackApp1 : Slackからの通信か検証
自作SlackApp1 -> CloudPubSub : Eventarc用のトピックへ情報をpush
自作SlackApp1 -> Slack : 200OKで返す
Slack -> User : スラッシュコマンドが完了する
CloudPubSub -> 自作SlackApp2 : Eventarcによって起動する
自作SlackApp2 -> 自作SlackApp2 : 任意のジョブ動かす
自作SlackApp2 -> Slack : chat.postMessage APIを使ってジョブ結果を伝える
Slack -> User : 追加の情報が表示される
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19658/96fc4968-fb69-907f-8f3c-6d3d77a5a416.png)


# まとめ
今回Eventarcのユースケースについて考えてみました。Eventarcを使わずに普通のPub/Subを使ってしまうと実装者によってバラバラなフォーマットがトピックに流れてしまうかもしれません。Eventarcを使うことで統一され、より整合性のあるコードになるのではないかと思います。また、今回カスタムソースについてしか触れませんでしたが、他にも60を超えるGoogleCloudソースが扱えるわけですから、別の利用方法も模索していきたいと思います。