電気ひつじ牧場

技術メモ

Cloud Pub/SubとCloud Functions gen2の連携(with DLQ)

Pub/Subに届いたメッセージをトリガーにしてCloud Functionsを呼び出す方法について

Terraformを使うケースを想定する

TL;DR

  • Dead Letter Queue(DLQ)を使いたい場合はサブスクリプションで設定する
    • サブスクリプションはCloud Functions側でイベントトリガーとなるトピックを設定すると自動で生成される
    • そのためTerraformではresourceとして現れず、(importしない限り)設定できない
    • サブスクリプションからHTTPトリガーでCloud Functionsを呼び出す形にするとこの問題を回避できる
  • HTTPトリガーで認証付きのCloud Functions gen2を呼び出す場合は、呼び出し側にroles/run.invokerのロールが必要
    • gen1で必要となるroles/cloudfunctions.invokerと異なる

Pub/SubトリガーでCloud Functionを呼び出す場合

Pub/SubとCloud Functionsを接続する際はPub/Subトリガーを使って呼び出すのがよくあるパターンである。 次のようなTerraformをapplyすると、Pub/SubトピックとCloud Functionに加え、自動でサブスクリプションとEventarcのトリガーが作成される。

resource "google_pubsub_topic" "tftest" {
  name = "tftest-topic"
}

resource "google_cloudfunctions2_function" "image_transform" {
  name        = "image-copy-function-2"
  description = "Copies image from source bucket to destination bucket"
  location    = "asia-northeast1"

  build_config {
    runtime     = "nodejs16"
    entry_point = "handleImageUpload"
    source {
      storage_source {
        bucket = google_storage_bucket.image_transform_function.name
        object = google_storage_bucket_object.function_source.name
      }
    }
  }

  service_config {
    max_instance_count               = 3
    min_instance_count               = 1
    available_memory                 = "4Gi"
    timeout_seconds                  = 60
    max_instance_request_concurrency = 80
    available_cpu                    = "4"
    environment_variables = {
      DESTINATION_BUCKET = google_storage_bucket.tftest.name
    }
    ingress_settings               = "ALLOW_INTERNAL_ONLY"
    all_traffic_on_latest_revision = true
    service_account_email          = google_service_account.image_transform_function.email
  }

  event_trigger {
    event_type     = "google.cloud.pubsub.topic.v1.messagePublished"
    trigger_region = "asia-northeast1"
    pubsub_topic   = google_pubsub_topic.tftest.id
    retry_policy   = "RETRY_POLICY_RETRY"
  }
}

しかしこの場合だと、Cloud Functionsが失敗した際にメッセージを転送するDLQを設定することができない。

理由としては、Pub/SubのDLQはサブスクリプションに対して設定するため、自動生成されるサブスクリプションはTerraformのコードで扱えないから。

回避方法

Pub/SubのPushサブスクリプションを使い、Cloud FunctionのHTTPエンドポイントを呼び出す。このHTTPトリガーはgoogle_cloudfunctions2_functionを作成すると自動で作られ、デフォルトでは呼び出しに認証が必要になる。

この呼び出し方法だと、google_pubsub_subscriptionをTerraformで記述する必要があるので、その中でDLQが指定できる。

resource "google_pubsub_subscription" "tftest_http" {
  name                 = "tftest-http"
  topic                = google_pubsub_topic.tftest.name
  ack_deadline_seconds = 10

  push_config {
    push_endpoint = google_cloudfunctions2_function.image_transform.service_config[0].uri

    attributes = {
      x-goog-version = "v1"
    }

    oidc_token {
      service_account_email = google_service_account.invoke_function_2.email
    }
  }

  dead_letter_policy {
    dead_letter_topic     = google_pubsub_topic.dead_letter_topic.id
    max_delivery_attempts = 5
  }

  retry_policy {
    minimum_backoff = "5s"
    maximum_backoff = "30s"
  }
}

resource "google_pubsub_topic" "dead_letter_topic" {
  name                       = "dead-letter-topic"
  message_retention_duration = "600s"
}

権限まわり

サブスクリプションに紐づけるSAには次の権限が必要になる

GCPが提供するpubsubのSAには次の権限が必要になる

  • SAにトークンを発行する権限
  • DLQのトピックへメッセージを発行する権限
  • メッセージに対して確認応答する権限(DLQに転送する際、元のキューから除去するために必要)

Terraformでは次のようになる

resource "google_service_account" "invoke_function_2" {
  account_id   = "invoke-function-2"
  display_name = "invoke-function-2"
}

resource "google_cloud_run_service_iam_member" "invoke" {
  location = "asia-northeast1"
  service  = google_cloudfunctions2_function.image_transform.name
  role     = "roles/run.invoker"
  member   = google_service_account.invoke_function_2.member
}

resource "google_project_iam_member" "project" {
  project = data.google_project.self.id
  role    = "roles/iam.serviceAccountTokenCreator"
  member  = "serviceAccount:service-${data.google_project.self.number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}

resource "google_pubsub_topic_iam_member" "publish_dlq" {
  topic  = google_pubsub_topic.dead_letter_topic.id
  role   = "roles/pubsub.publisher"
  member = "serviceAccount:service-${data.google_project.self.number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}

resource "google_pubsub_subscription_iam_member" "dequeue" {
  subscription = google_pubsub_subscription.tftest_http.id
  role         = "roles/pubsub.subscriber"
  member       = "serviceAccount:service-${data.google_project.self.number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}

おわりに

Pub/SubトリガーでCloud Functionsを呼び出す場合、TerraformでDLQの設定ができないという問題はProvider PluginのIssueにも上がっていた

github.com

GCP側にうまく扱えるAPIが無いらしい。残念。