こんにちは、飯沼です。
以前、花田さんが悩みながら構築した「CloudRun/CloudSchedulerのエラーをSlackに通知するサービス」が無事リリースされ、運用が開始されました!
花田さんの記事:
それまでは毎日の運用業務として40以上あるCloudRun/CloudSchedulerのエラーチェックを目視で行っていたのですが、
その作業から解放されました!
ありがたいことです。
今回はこのエラーを通知するサービスをIaC化したお話になります。
Web UIで作成した Sink,Pub/Sub,Cloud FunctionsでSlackにエラー通知を行うサービスをTerraformでコード化してデプロイすることについて書いていきます。
概要
まず、エラーを検知する対象のサービスと、エラーを通知するサービスについて簡単におさらいします。
エラーを検知する対象サービスについて
サービスは3つあり指定時間になると外部データをBigQueryに保存します。
エラーになると後続のサービスに影響があるため、エラーの監視を行いたいです。
この組み合わせが14パターンありますのでエラーを検知する対象は、 14 × 3 で42になります。
(パターンは今後も増える。。。)
エラーを通知するサービスについて
2種類あります。
- 各サービスでエラーが発生したらSlackに通知(通常エラー)
- 処理時間が30分を超えたらSlackに通知(処理時間超過エラー)
エラーを通知するサービスの構成図
- Cloud Logging(Sink):
- フィルター設定でScheduler/CloudRunの特定のログを取得できるようにします。(フィルター設定を汎用的にすることでパターンが増えてもコードの修正がないようにします)
- Cloud Pub/Sub:
- Sinkの宛先をCloud Pub/Subにします。
- Cloud Functions:
- Cloud Pub/SubをトリガーにCloud Functionsを動かしてSlackに通知します。
やりたいこと
Web UIで作成したGCPの各サービスをTerraform化します。
- Cloud Logging(Sink)
- Cloud Pub/Sub
- Cloud Functions
ディレクトリ構成
通常エラー用と処理時間超過エラー用とそれぞれディレクトリを分けてCloud Functionsのコードを管理しています。
index.jsがCloud Functionsのコードになります。
それぞれterraformディレクトリを作りました。
同じ構成なので、通常エラー用の方だけ説明します。
Cloud Logging(Sink)のTerraform化
Web UIで作成したSinkをコード化します。
- Sink名: sink-A-error
- フィルタ設定:
resource.type=("cloud_scheduler_job" OR "cloud_run_revision") resource.labels.service_name=~"A-cloudrun" OR resource.labels.job_id=~"A-cron" NOT textPayload=~"It is very likely that you can safely ignore this message and that this is not the cause of any error you might be troubleshooting." "error"
cloud_logging.tf
resource "google_logging_project_sink" "sink-A-error" { name = "sink-A-error" destination = "pubsub.googleapis.com/projects/${local.project}/topics/${google_pubsub_topic.topic.name}" filter = <<EOT resource.type=("cloud_scheduler_job" OR "cloud_run_revision") resource.labels.service_name=~"A-cloudrun" OR resource.labels.job_id=~"A-cron" NOT textPayload=~"It is very likely that you can safely ignore this message and that this is not the cause of any error you might be troubleshooting." "error" EOT unique_writer_identity = true }
ポイント
- filterに改行を含む文字列を設定したい場合は、heredocで記述します。
- unique_writer_identity = true とすると p123456789012-123456@gcp-sa-logging.iam.gserviceaccount.com のような名前のサービスアカウントが作られます。
- この自動で作られるサービスアカウントに「Pub/Sub パブリッシャー」のロールをつける必要があります。
- Terraformからロールをつける方法もあるのですが、ロールの設定はシステム管理者につけてもらう運用になっているので、サービスアカウントが作成された後つけてもらいます。
参考ドキュメント
- Terraform:
- GCP:
- シンクを構成する - エクスポート先の権限を設定する Sinkの書き込みID(サービスアカウント)にロールを設定する
Sinkのサービスアカウントについて
シンクを作成すると、Logging によって一意の書き込み ID と呼ばれるシンクのサービスアカウントが新規に作成されます。このサービスアカウントは Cloud Logging によって所有、管理されているため、直接管理できません。シンクが削除されると、このサービスアカウントは削除されます。
Cloud Pub/SubのTerraform化
Web UIで作成したPubSubトピックをコード化します。
- トピックID:pubsub-A-error
cloud_pubsub.tf
resource "google_pubsub_topic" "topic" { name = "pubsub-A-error" }
参考ドキュメント
- Terraform:
Cloud FunctionsのTerraform化
Web UIで作成したCloud Functionsをコード化します。
Terraformでデプロイまで行います。
- Cloud Functionsのソースコードをzip化
- zipをGCSにアップロード
- Cloud Functionsがzipファイルを取得してデプロイ
entry_point は、"sendNotificationSlack"という名前で作っています。
event_trigger は上で作ったpubsub topicにします。
cloud_functions.tf
data "archive_file" "function_archive" { type = "zip" output_path = "../index.zip" source { content = file("../index.js") filename = "index.js" } source { content = file("../package.json") filename = "package.json" } } resource "google_storage_bucket" "bucket" { name = "error-A-slack" } resource "google_storage_bucket_object" "archive" { name = "index${data.archive_file.function_archive.output_md5}.zip" bucket = google_storage_bucket.bucket.name source = data.archive_file.function_archive.output_path } resource "google_cloudfunctions_function" "function" { name = "error-A-slack" description = "Cloud SchedulerとCloud Runで発生したエラーをslackに通知する" runtime = "nodejs14" available_memory_mb = 256 source_archive_bucket = google_storage_bucket.bucket.name source_archive_object = google_storage_bucket_object.archive.name event_trigger { event_type = "providers/cloud.pubsub/eventTypes/topic.publish" resource = google_pubsub_topic.topic.name } entry_point = "sendNotificationSlack" environment_variables = { PROJECT = local.project, SLACK_WEBHOOK_SM_KEY = local.secret_manager_key, SM_VERSION = local.secret_version, } }
ポイント
- data archive_file でCloud Functionsのソースコードをzipでアーカイブしています
- ディレクトリごとアーカイブする方法がお手軽なのですが、不要なファイルまでデプロイしたくないためfile関数を使って必要なファイルだけを指定するようにしました
- 機密情報をシークレットマネージャで管理して、環境変数にシークレットマネージャのキーを入れるようにしています
- Cloud Functionsのコード内で扱うSlackのWebhookのような機密情報は環境変数に直接入れないでシークレットマネージャから参照するようにしました
- シークレットを参照できるように、Cloud Functionsを実行するサービスアカウントに「Secret Manager のシークレットアクセサー」のロールを追加します
- Cloud Functionsを実行するサービスアカウントは、<プロジェクトID>@appspot.gserviceaccount.com という名前なのでこれにロールを追加します
参考ドキュメント
- Terraform:
- GCP:
- シークレット バージョンへのアクセス
- アクセス制御 「Secret Manager のシークレット アクセサー」のロールを追加する
Terraformの実行
Terraformを実行するサービスアカウントに下記ロールを追加します
ロール | 用途 |
---|---|
ストレージ管理者 | Terraformの構成作成 |
Pub/Sub 編集者 | Pub/Sub の設定 |
Cloud Functions 管理者 | Cloud Functionsの作成 |
サービスアカウント ユーザー | Cloud Functions作成時に必要 |
ログ構成書き込み | Sinkの設定 |
terraform apply で、Sink,Pub/Sub,Cloud Functions を構築することができます。
デプロイ後に必要なロールの設定を行います。
作成されたSinkのサービスアカウントにロールを追加します。
ロール | 用途 |
---|---|
Pub/Sub パブリッシャー | シンクを作成したときに、Logging によって一意の書き込み ID と呼ばれるシンクのサービスアカウントが新規に作成される。そのサービスアカウントにロールを追加する |
Cloud Functionsを実行するサービスアカウント <プロジェクトID>@appspot.gserviceaccount.com にロールを追加します。
ロール | 用途 |
---|---|
Secret Manager のシークレットアクセサー | シークレットを参照 |
これでコード化とデプロイまで行うことができました。
感想
すでに動いているサービスをTerraform化しました。
すんなりいくのかなと思っていましたが、実際にやってみるとチームのレビューでは気づかなかったことや変えたほうがいいところが見えてきました。
実装とIaC化をチーム内で別の人が担当したのですが、学びがあり、サービスとしてもブラッシュアップできたので、良い試みだと思いました。
また、チームでロールの管理や機密情報の管理をどうするか話し合えたことも良かったです。
以上になります。何かの学びがあれば幸いです。