botを作りたいけどサーバ管理したくない...そうだ！Cloud Runでサーバレスにしようぜ！

こんにちは！ひでし(@y_hideshi)です！
この記事は、[All About Group（株式会社オールアバウト） Advent Calendar 2019](https://qiita.com/advent-calendar/2019/allabout) 11日目の記事です。

今日は10月に行なった開発合宿でbotを作った話をしたいと思います！

# なぜbotを作るようになったのか
あれは今から2ヶ月前...私と後輩くんは開発合宿で何かを作ることになっていましたが、作るものが決まらず困っていました。

ひでし 「開発合宿なにつくろうかな...上長さん...何かいいアイデアないスカ...」
上長 「お！じゃあ、 **定期的にGCPの確約利用割引の期日の自動チェック & アラートあげるbot** とかどう！？」
上長 「確約利用割引とかは期日とか見落としそうになるんだから、人力チェックから自動チェックに切り替えたいんだ」
ひでし 「採用です！」
ひでし 「おっしゃ、後輩くん！これ一緒に作るで！」
後輩くん 「了解です！」
ひでし 「というわけで、まずやりたいこと・やりたくないことを上げて作っていくで〜！」
> ※確約利用割引 = 1 年間または 3 年間の支払いを確約する代わりに、特定の量の vCPU、メモリ、GPU、ローカル SSD を割引価格で購入できる仕組み。年単位の契約なので期日がある。

# システム構成
というわけで、できたシステムの全体像がこちらです。
![スクリーンショット 2019-12-10 20.24.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176573/ca4da5a6-b61d-95dc-3ed2-e492f4ce804f.png)

## システムを作るときに譲れなかったポイント
* botのためにインスタンスを作らない
    * 運用コストをあまりかけたくない
        * インスタンス代
    * 数年後、このインスタンスはなんだ！？ってなってほしくない
    * インスタンスの面倒を見たくない（脆弱性対応など）
    * サーバレスにしよう！！
* 開発・運用（破壊と創造）をしやすくする
    * いらなくなったら簡単に捨てれるようにする
* 認証情報などはなるべくこちら側でもたせない
* なるべく楽がしたい！！（大事）

これらのことを諸々考えた結果、マネージド環境を多分に使った構成となりました！

## 構成要素紹介
* [Cloud Scheduler](https://cloud.google.com/scheduler/?hl=ja)
    * フルマネージドのcronジョブスケジューラ
    * 定期的に処理を行うようにCloudRunへHTTPリクエストを投げる
* [Cloud Run](https://cloud.google.com/run/?hl=ja)
    * Googleが提供するフルマネージド環境でコンテナを動かすことができる。
    * イメージさえ用意しておけば、ネットワークの経路なども自動で用意してくれて便利！！
* [Cloud PubSub](https://cloud.google.com/pubsub/?hl=ja)
    * グローバルメッセージングとイベント取り込みが簡単になるよ！


今回は主に「Slackへの通知を行うCloudRun周り」と「PubSub周り」にてやったこと&工夫したポイントについて話していきます！
確約利用割引周りのことは後日後輩くんが話してくれる...はず...!

### Slack通知を行うCloudRun周りについて
CloudRun周りでやったことは下記の通りです！

* PubSubから送られてくるjsonデータをもとにslackへ通知を行う(goで実行)
* CloudBuildでイメージのpushやCloudRunへのデプロイは自動化した
    * デプロイ時、CloudRunは外部からのリクエストには応じないように認証を行なうようにした

#### PubSubから受け取ったjsonデータをもとにslackへ通知を行うようにgolangを書いた
全部のっけると少し長いので、コードの一部を記載します！

やっていることはシンプルで、pubsubから送られてくるjsonデータを自分で定義した構造体で受け取ります。
その後、slackのライブラリ([github.com/nlopes/slack](https://github.com/nlopes/slack))を利用して受け取ったデータをslackに送信しています。


```golang
	msg := slack.WebhookMessage{
		Attachments: []slack.Attachment{attachment},
	}
	if httpRequestData.JsonParams.Attributes.WebhookUrl != "" {
		postWebhookUrl = httpRequestData.JsonParams.Attributes.WebhookUrl
	}
	err := slack.PostWebhook(postWebhookUrl, &msg)
	if err != nil {
		fmt.Println(err)
	}
	return true
```

##### CloudRunのここが素敵！
* ローカルで実行していたdockerコンテナがクラウドで動くようになるだけなので、学習コストや開発コストが低い
    * CloudRunの仕様（リッスンしているポート番号）を少し学ぶくらいです。
* ネットワーク経路の用意もGoogleが自動でやってくれるのでとても楽！
* インスタンスに比べて使用した分だけお金がかかるので、利用頻度が少ないアプリはとても安価ですむ

#### CloudBuildでイメージのpushやCloudRunへのデプロイは自動化した
CircleCIでも良かったのですが、GCPの認証情報などを持たせるのが嫌だった（めんどくさかった）のでCloudBuildにしました！！

##### CloudBuildのここが素敵！
* GCPへの認証はすでに通っているので認証情報を持たす必要がありません
* CloudBuildのサービスアカウントに権限をつけることで、他のGCPサービスとの連携が容易（今回は下記の権限を追加）
    * CloudRun 管理者
    * サービスアカウントユーザー



cloudbuid.yaml

```yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
  # $REPO_NAMEだとbitbucketというprefixがつくので別のものを使う
  args: ['build', '-t', 'レポジトリ名', '.']
  # setting docker tag
- name: 'gcr.io/cloud-builders/docker'
  args: ['tag', 'レポジトリ名', 'レポジトリ名:latest']
  # コミットのハッシュ値(先頭7文字)とbuildIDをタグにつける
- name: 'gcr.io/cloud-builders/docker'
  args: ['tag', 'レポジトリ名', 'レポジトリ名:${SHORT_SHA}-${BUILD_ID}']
  # docker image push to GCR
- name: 'gcr.io/cloud-builders/docker'
  args: ['push', 'レポジトリ名:latest']
- name: 'gcr.io/cloud-builders/docker'
  args: ['push', 'レポジトリ名:${SHORT_SHA}-${BUILD_ID}']
  # cloud run deploy
- name: 'gcr.io/cloud-builders/gcloud'
  args: ['beta', 'run', 'deploy', '${_CLOUD_RUN_SERVICE_NAME}', '--image', 'レポジトリ名:latest', '--platform', 'managed', '--region', '${_CLOUD_RUN_SERVICE_REGION}', '--no-allow-unauthenticated']
  # check deploy service
- name: 'gcr.io/cloud-builders/gcloud'
  args: ['beta', 'run', 'services', 'list', '--platform', 'managed', '--region', '${_CLOUD_RUN_SERVICE_REGION}']
timeout: 600s
substitutions:
  _CLOUD_RUN_SERVICE_NAME: slack-notification
  _CLOUD_RUN_SERVICE_REGION: us-central1
```



### PubSubについて
CloudRunと連携するために、PubSubには下記の3つのリソースを作成しました。

* Slack通知のイベントを受け取るためのトピック。
* CloudRunのサービスにhttp postするためのサブスクリプション。
* トピックに送られたメッセージを確認するためのサブスクリプション。

リソースを都度画面から作ったり、自分しか作れないなんて状態になるのが嫌だったので、Terraformで作成することにしました（下記のtfファイルを利用する際は、varの部分の変数を定義するか、適宜置き換えてください）。

```yaml

provider "google" {
  credentials = file(var.credentials)
  project    = var.project
}

resource "google_pubsub_topic" "slack_topic" {
  name = var.topic_name
}

resource "google_pubsub_subscription" "cloud_run_slack_notification" {
  name  = var.cloud_run_slack_notification_subscription_id
  topic = google_pubsub_topic.slack_topic.name

  # 確認応答期間
  ack_deadline_seconds = 20
  # メッセージ保持期間: 3600 * 24 * 7 = 604800s(7 days)
  message_retention_duration = "604800s"

  push_config {
    push_endpoint = var.cloud_run_slack_notification_endpoint
    # 認証を有効にしたい場合はこのオプションを追加する
    oidc_token  {
      service_account_email = var.cloud_run_invoker_service_account
    }
  }
}

# メッセージを確認できるようにpull型subscriptionを作成する
resource "google_pubsub_subscription" "check_slack_topic_message" {
  name  = var.check_slack_topic_message_subscription_id
  topic = google_pubsub_topic.slack_topic.name

  # メッセージ保持期間: 3600 * 24 * 7 = 604800s(7 days)
  message_retention_duration = "604800s"
  ack_deadline_seconds       = 20
  # 確認したメッセージを保存する
  retain_acked_messages      = true
}

```

#### ポイント
上記のtfファイルで定義している`endpoint`にはCloudRunのサービスURLを指定しており、CloudRunでは認証を行うように設定しています。
tfファイル内の`oidc_token` で定義してあるサービスアカウントには「CloudRun起動者」の権限を持たせています。そうすることで、endpointに対してリクエストを送る際、Google側で自動で認証を行なってくれます。
そのため、簡単にセキュアなシステムを作ることができます！

```yaml
    oidc_token  {
      service_account_email = var.cloud_run_invoker_service_account
    }
```

## まとめ
* GCPの各種サービスを連携して、簡単にセキュアなbotを作成することができました！
* CloudRunはいいぞ！
    * 学習コストはdockerとCloudRunのquickstartを見るくらい
    * サクッとデプロイしてサクッと消せるのが好き！
* PubSubもいいぞ！！
    * PubSubを挟むことで、CloudRunとの認証をGCP側に持たすことができました
    * Slack通知処理をgo以外で行いたくなった時は、pubsubのendpointを変えるだけでいいのでシステムに柔軟性を持たせることができる
* GCPの同一プロジェクト内で閉じることで認証処理・認証情報をプログラムに記載する必要がなくったのでとても楽ができました！


皆さんも是非、いろんなサービスを使ってみてください！