はじめに
最近、TROCCOのSelf-Hosted Runner(自己管理の環境下でデータ転送を行える機能)の技術検証をしているのですが、そのなかでCloud Runの設定を調整する方法が実装できたので、参考までに記事にまとめておきます。
なお、今回はGoogle Cloud環境でCloud SchedulerとCloud Workflowsを利用していますが、AWSやAzureでも同様の考え方で処理できるのではないかと考えています。
こんな方におすすめ
- TROCCO Self-Hosted Runnerで利用するCloud Runの環境をいい感じに調整したい方
- Cloud Runの設定を定期的に調整する方法について知りたい方
今回できたこと
仕組みとしては共通ですが、
- ①Cloud Runのインスタンス数を調整する
- ②Cloud Runの課金モデルを変更する
の2つの方法をまとめています。
Self-Hosted Runnerでこれらが有用そうなのは、セキュアなデータ転送のためのアウトバウンド通信のみを許可する仕様のため、インバウンドアクセスに応じて自動スケーリングさせるWebアプリケーションとは挙動が異なるためです。
以下に詳細のコードを紹介します。TerraformのHCL形式ですが、設定項目がコード化されているだけなので、GUIでも同様の設定をしていただければ利用可能です。
事前準備
まず、各種の実行のために必要な権限をサービスアカウントに付与します。(他の部分で付与しているものもあるので、漏れがあるかもしれません)
resource "google_project_iam_member" "self_hosted_runner__controller" {
for_each = toset([
"roles/workflows.invoker", # Cloud SchedulerからCloud Workflowsを実行するために必要
"roles/logging.logWriter", # Cloud Workflowsのログ出力に必要
"roles/run.developer", # Cloud Runの更新に必要
"roles/iam.serviceAccountUser" # Cloud Runの更新時にサービスアカウントを利用するために必要
])
project = var.project_id
role = each.value
member = google_service_account.trocco_self_hosted_runner.member
}
では、ここからが実際のコードです。
①Cloud Runのインスタンス数を調整する
処理としては、
- Cloud Runの設定を取得する
- 変更したい設定項目を上書きする
- 変更した設定でCloud Runをデプロイする
という流れになっています。
インスタンス数の調整にあたっては、Cloud Scheduler側で最小/最大インスタンス数を指定し、それをもとにCloud Workflowsを実行することで実現しています。
resource "google_cloud_scheduler_job" "instance_scaler" {
name = "shr-test-${var.tested_by}-instance-scaler"
schedule = "0 9 * * *" # これは適当な時間
time_zone = "Asia/Tokyo"
http_target {
uri = "https://workflowexecutions.googleapis.com/v1/projects/${var.project_id}/locations/${var.default_location}/workflows/${google_workflows_workflow.cloud_run_instance_scaler.name}/executions"
http_method = "POST"
headers = {
"Content-Type" = "application/json"
}
body = base64encode(jsonencode({
argument = jsonencode({
min_instance_count = 1
max_instance_count = 1
})
}))
oauth_token {
service_account_email = google_service_account.trocco_self_hosted_runner.email
}
}
}
resource "google_workflows_workflow" "cloud_run_instance_scaler" {
name = "shr-test-${var.tested_by}-instance-scaler"
region = var.default_location
description = "Cloud Run Instance Scaler for TROCCO Self-Hosted Runner"
service_account = google_service_account.trocco_self_hosted_runner.email
# ref: cloud.google.com/run/docs/reference/rest/v2/projects.locations.services
source_contents = <<-EOT
main:
params: [args]
steps:
- log_start:
call: sys.log
args:
severity: "INFO"
text: '$${"スケーリング開始: " + ${google_cloud_run_v2_service.trocco_self_hosted_runner.name}'
- get_current_service:
call: googleapis.run.v2.projects.locations.services.get
args:
name: ${google_cloud_run_v2_service.trocco_self_hosted_runner.id}
result: current_service
- update_scaling:
assign:
- updated_service: $${current_service}
- updated_service.template.containers[0].resources.cpuIdle: false # これが課金モデルの設定
- updated_service.template.scaling.minInstanceCount: $${args.min_instance_count}
- updated_service.template.scaling.maxInstanceCount: $${args.max_instance_count}
- update_service:
call: googleapis.run.v2.projects.locations.services.patch
args:
name: ${google_cloud_run_v2_service.trocco_self_hosted_runner.id}
body: $${updated_service}
result: updated_result
- log_success:
call: sys.log
args:
severity: "INFO"
text: '$${"スケーリング完了: " + ${google_cloud_run_v2_service.trocco_self_hosted_runner.name}'
- return_result:
return:
status: "success"
service: ${google_cloud_run_v2_service.trocco_self_hosted_runner.name}
min_instance_count: $${args.min_instance_count}
max_instance_count: $${args.max_instance_count}
updated_result: $${updated_result}
EOT
deletion_protection = false
}
②Cloud Runの課金モデルを変更する
Cloud Runにはリクエストベースの課金モデルとインスタンスベースの課金モデルがありますが、前者でSelf-Hosted Runnerを動かすと、異様に処理が遅くなります。
これは、外部からのリクエストが全く発生しないSelf-Hosted Runnerでは、リクエストベースの課金モデルではリクエストがないことでCPUがアイドル状態になってしまうからだと思われます。
そこで、
- ジョブ実行をする時間帯:インスタンスベースの課金モデルとする
- ジョブ実行をしない時間帯:リクエストベースの課金モデルとする
ことで、費用の最適化ができると想定されます。(リクエストベースの課金モデルで、実際料金が減っているかまでは検証できていないですが、おそらく大丈夫なはず)
resource "google_cloud_scheduler_job" "pricing_controller" {
name = "shr-test-${var.tested_by}-pricing-controller"
schedule = "0 19 * * *" # これは適当な時間
time_zone = "Asia/Tokyo"
http_target {
uri = "https://workflowexecutions.googleapis.com/v1/projects/${var.project_id}/locations/${var.default_location}/workflows/${google_workflows_workflow.cloud_run_pricing_controller.name}/executions"
http_method = "POST"
headers = {
"Content-Type" = "application/json"
}
body = base64encode(jsonencode({
argument = jsonencode({
min_instance_count = 0
max_instance_count = 1
})
}))
oauth_token {
service_account_email = google_service_account.trocco_self_hosted_runner.email
}
}
}
resource "google_workflows_workflow" "cloud_run_pricing_controller" {
name = "shr-test-${var.tested_by}-pricing-controller"
region = var.default_location
description = "Cloud Run Pricing Controller for TROCCO Self-Hosted Runner"
service_account = google_service_account.trocco_self_hosted_runner.email
source_contents = <<-EOT
main:
params: [args]
steps:
- log_start:
call: sys.log
args:
severity: "INFO"
text: '$${"課金モデル変更開始: " + ${google_cloud_run_v2_service.trocco_self_hosted_runner.name}'
- get_current_service:
call: googleapis.run.v2.projects.locations.services.get
args:
name: ${google_cloud_run_v2_service.trocco_self_hosted_runner.id}
result: current_service
- update_pricing_model:
assign:
- updated_service: $${current_service}
- updated_service.template.containers[0].resources.cpuIdle: true
- updated_service.template.scaling.minInstanceCount: $${args.min_instance_count}
- updated_service.template.scaling.maxInstanceCount: $${args.max_instance_count}
- update_service:
call: googleapis.run.v2.projects.locations.services.patch
args:
name: ${google_cloud_run_v2_service.trocco_self_hosted_runner.id}
body: $${updated_service}
result: updated_result
- log_success:
call: sys.log
args:
severity: "INFO"
text: '$${"課金モデル変更完了: " + ${google_cloud_run_v2_service.trocco_self_hosted_runner.name}'
- return_result:
return:
status: "success"
service: ${google_cloud_run_v2_service.trocco_self_hosted_runner.name}
updated_result: $${updated_result}
EOT
deletion_protection = false
}
おわりに
今回初めてCloud Workflowsを利用しました。今回の処理はCloud Run FunctionsでPythonなどのコードを書いてもできますが、yamlだけでサクッとできるのは便利に思いました。