Google Cloud Run Servicesを土日と夜間に縮小運用しよう #googlecloud #opentofu #terraform

はじめに

以前は SlackボットをAzure App Serviceで動かしていました が、本稿執筆時点では Google Cloud Run Services 上に OpenTofu (乃至 Terraform)で構築して運用しています。本稿では、このSlackボットを土日と夜間に縮小運用するための仕組みについて見ていきます。

なお、TFコードやSlackボットの詳細については省略させていただきます。

Slackボットの通常運用時の設定

Slackボットは google_cloud_run_v2_service リソースを用いて構築しています。一部省略していますが次のようになっています。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
resource "google_cloud_run_v2_service" "this" {
name = var.project_name
location = var.region
scaling {
min_instance_count = 1
}
template {
service_account = google_service_account.cloudrun.email
scaling {
min_instance_count = 0
max_instance_count = 5
}
containers {
image = "<snip>"
resources {
limits = {
cpu = "1000m"
memory = "1Gi"
}
cpu_idle = false
}
...
}
...
}
...
}
resource "google_cloud_run_v2_service" "this" { name = var.project_name location = var.region scaling { min_instance_count = 1 } template { service_account = google_service_account.cloudrun.email scaling { min_instance_count = 0 max_instance_count = 5 } containers { image = "<snip>" resources { limits = { cpu = "1000m" memory = "1Gi" } cpu_idle = false } ... } ... } ... }
resource "google_cloud_run_v2_service" "this" {
  name     = var.project_name
  location = var.region

  scaling {
    min_instance_count = 1
  }

  template {
    service_account = google_service_account.cloudrun.email
    scaling {
      min_instance_count = 0
      max_instance_count = 5
    }
    containers {
      image = "<snip>"
      resources {
        limits = {
          cpu    = "1000m"
          memory = "1Gi"
        }
        cpu_idle = false
      }
...
    }
...
  }
...
}

ここでは次の2点に注目してください。

scaling.min_instance_count

scalingtemplate の中にも外にもあってややこしいですが、この scaling.min_instance_count は外側のほうで、「サービスレベルのスケーリング設定」です。

ここでは 1 を設定しているので、リクエストを処理していない(アイドル状態)のインスタンスを最低でも1つを維持します(ウォーム状態)。つまりリクエストがあった場合、起動したままのインスタンスを利用するので、すぐに応答があることが期待できます。ただし、リクエストを処理していない間も料金がかかっています。もし 0 の場合はアイドル状態のインスタンスが削除されるので、その間の料金はかかりませんが、リクエストがあったらインスタンスを起動する必要があるため応答に時間がかかります。

次も参照してください。

template.containers.resources.cpu_idle

template.containers.resources.cpu_idle は、リクエストベース課金(true)か、インスタンスベース課金(false)かを設定します。

リクエストベース課金では、リクエストの処理中のみにCPUが割り当てられ、そのときのみ料金がかかります。一方、インスタンスベース課金では、リクエストがない期間にもCPUが割り当てられ、言ってみればずっと料金がかかります。ここでは false なのでインスタンスベース課金です。

次も参照してください。

まとめると、このボットの設定は、

  • リクエストを処理していない(アイドル状態)のインスタンスを最低でも1つを維持
  • リクエストがない期間にもCPUが割り当てられるインスタンスベース課金

となっています。つまり、いつでもリクエストを即時リクエストを受け付けられるようにしているが、その分料金がかかっている状態です。

コスト削減するなら、

  • リクエストを処理していない(アイドル状態)のインスタンスはゼロ
  • リクエストがあったときのみCPUを割り当てるリクエストベース課金

とするのがよさそうです。しかし実際にこの設定にしてみたところ、思ったより応答が遅く、ユーザ体験があまりよくありませんでした。また、応答待ちでクルクルの代用に絵文字リアクションをつける 仕組みもうまく動きませんでした。

しかし、利用のない夜間や土日にも即時応答可能な待機状態はコストの無駄ではないか…?ということで考える必要がありました。

Cloud Scheduler と Cloud Run Job で定期設定変更

そこでGoogle Cloud SchedulerGoogle Cloud Run Job を定期実行し、その中で gcloud CLI による設定変更を行うことにしました。

cronとコマンドラインを書くなら次のようになります。

  • 平日の夜20時に運転を縮小する
    • "0 20 * * 1-5"
    • gcloud run services update [PROJECT_NAME] --region [REGION] --min 0 --cpu-throttling
  • 平日の朝9時に通常運転に戻す
    • "0 9 * * 1-5"
    • gcloud run services update [PROJECT_NAME] --region [REGION] --min 1 --cpu-no-throttling

縮小運転のTFコードを見てみましょう。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
resource "google_cloud_run_v2_job" "cold" {
name = "${var.project_name}-cold"
location = var.region
deletion_protection = false
template {
template {
service_account = google_service_account.hotcoldswitch.email
containers {
image = "google/cloud-sdk:512.0.0-alpine"
resources {
limits = {
cpu = "1000m"
memory = "512Mi"
}
}
args = [
"gcloud", "run", "services", "update",
var.project_name,
"--region", var.region,
"--min", "0",
"--cpu-throttling"
]
}
}
}
}
resource "google_cloud_run_v2_job_iam_member" "cold" {
project = google_cloud_run_v2_job.cold.project
location = google_cloud_run_v2_job.cold.location
name = google_cloud_run_v2_job.cold.name
role = "roles/run.invoker"
member = google_service_account.hotcoldswitch.member
}
resource "google_cloud_scheduler_job" "cold" {
name = google_cloud_run_v2_job.cold.name
description = "Genmaicha Cold Switch Cloud Run Job"
time_zone = "Asia/Tokyo"
schedule = "0 20 * * 1-5"
attempt_deadline = "360s"
retry_config {
retry_count = 3
}
http_target {
http_method = "POST"
uri = "https://${google_cloud_run_v2_job.cold.location}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${data.google_project.project.number}/jobs/${google_cloud_run_v2_job.cold.name}:run"
oauth_token {
service_account_email = google_service_account.hotcoldswitch.email
}
}
}
resource "google_cloud_run_v2_job" "cold" { name = "${var.project_name}-cold" location = var.region deletion_protection = false template { template { service_account = google_service_account.hotcoldswitch.email containers { image = "google/cloud-sdk:512.0.0-alpine" resources { limits = { cpu = "1000m" memory = "512Mi" } } args = [ "gcloud", "run", "services", "update", var.project_name, "--region", var.region, "--min", "0", "--cpu-throttling" ] } } } } resource "google_cloud_run_v2_job_iam_member" "cold" { project = google_cloud_run_v2_job.cold.project location = google_cloud_run_v2_job.cold.location name = google_cloud_run_v2_job.cold.name role = "roles/run.invoker" member = google_service_account.hotcoldswitch.member } resource "google_cloud_scheduler_job" "cold" { name = google_cloud_run_v2_job.cold.name description = "Genmaicha Cold Switch Cloud Run Job" time_zone = "Asia/Tokyo" schedule = "0 20 * * 1-5" attempt_deadline = "360s" retry_config { retry_count = 3 } http_target { http_method = "POST" uri = "https://${google_cloud_run_v2_job.cold.location}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${data.google_project.project.number}/jobs/${google_cloud_run_v2_job.cold.name}:run" oauth_token { service_account_email = google_service_account.hotcoldswitch.email } } }
resource "google_cloud_run_v2_job" "cold" {
  name     = "${var.project_name}-cold"
  location = var.region

  deletion_protection = false

  template {
    template {
      service_account = google_service_account.hotcoldswitch.email
      containers {
        image = "google/cloud-sdk:512.0.0-alpine"
        resources {
          limits = {
            cpu    = "1000m"
            memory = "512Mi"
          }
        }
        args = [
          "gcloud", "run", "services", "update",
          var.project_name,
          "--region", var.region,
          "--min", "0",
          "--cpu-throttling"
        ]
      }
    }
  }
}

resource "google_cloud_run_v2_job_iam_member" "cold" {
  project  = google_cloud_run_v2_job.cold.project
  location = google_cloud_run_v2_job.cold.location
  name     = google_cloud_run_v2_job.cold.name
  role     = "roles/run.invoker"
  member   = google_service_account.hotcoldswitch.member
}

resource "google_cloud_scheduler_job" "cold" {
  name             = google_cloud_run_v2_job.cold.name
  description      = "Genmaicha Cold Switch Cloud Run Job"
  time_zone        = "Asia/Tokyo"
  schedule         = "0 20 * * 1-5"
  attempt_deadline = "360s"

  retry_config {
    retry_count = 3
  }

  http_target {
    http_method = "POST"
    uri         = "https://${google_cloud_run_v2_job.cold.location}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${data.google_project.project.number}/jobs/${google_cloud_run_v2_job.cold.name}:run"
    oauth_token {
      service_account_email = google_service_account.hotcoldswitch.email
    }
  }
}

通常運転に戻すTFコードは省略します。google_cloud_run_v2_jobgoogle_cloud_scheduler_job も参照してください。

また、Cloud Run Servicesを起動しているサービスアカウント google_service_account.cloudrun に、Cloud Run Jobを実行しているサービスアカウント google_service_account.hotcoldswitch による実行を許すロールを与える必要があります。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
resource "google_service_account_iam_member" "by_hcs" {
service_account_id = google_service_account.cloudrun.name
role = "roles/iam.serviceAccountUser"
member = google_service_account.hotcoldswitch.member
}
resource "google_service_account_iam_member" "by_hcs" { service_account_id = google_service_account.cloudrun.name role = "roles/iam.serviceAccountUser" member = google_service_account.hotcoldswitch.member }
resource "google_service_account_iam_member" "by_hcs" {
  service_account_id = google_service_account.cloudrun.name
  role               = "roles/iam.serviceAccountUser"
  member             = google_service_account.hotcoldswitch.member
}

サービス アカウント ユーザーのロール も参照してください。Services を実行するのに必要なロールがあれば追加してください。

実行例

指標を確認したところ、夜20時に縮小し、翌朝9時に通常に復帰していることがわかります。これにより、Cloud Run Services の料金も半分に削減できました。

まとめ

Google Cloud Run Servicesの「サービスレベルのスケーリング設定」と「リクエストベース課金」を使うと料金を節約できますが、これらの機能を常時利用しているとユーザ体験があまりよくないため、Google Goolge Cloud Scheduler と Google Cloud Run Job を使うことで閑散期のみ利用するように切り替えてみました。

少し脱線しますが、最初 Google Cloud はとっつきづらい、わかりにくい…と思っていたのですが、Terraform / OpenTofu のおかげでだいぶ親しめるようになってきました。引き続き、現在運用中の Slack ボットに関する Google Cloud の機能や Terraform / OpenTofu のコードを紹介できたらと思います。

Author

Chef・Docker・Mirantis製品などの技術要素に加えて、会議の進め方・文章の書き方などの業務改善にも取り組んでいます。「Chef活用ガイド」共著のほか、Debian Official Developerもやっています。

Daisuke Higuchiの記事一覧

Neo4j_007_Retail-Innovation_whitepaper
新規CTA

目次を表示