🧑‍🎓

Google Cloud 入門:Cloud Run Jobs ハンズオン

2024/11/10に公開

概要

Cloud Run Jobs を初めて使う人に向けて、基本的な概念と使い方をまとめた。
チュートリアルとして、GoのCLIアプリケーションを Cloud Run Job にデプロイし実行する方法を記載した。

Cloud Run Job とは

Cloud Run はコンテナ化されたアプリケーションを実行するためのサーバーレスプラットフォームで、Cloud Run サービスと Cloud Run Job の2種類がある。

Cloud Run サービスは HTTP リクエストを受信するアプリケーション向きである事に対し、Cloud Run Job は作業(ジョブ)を実行し作業の完了後に終了するコードの実行、いわゆるバッチ処理の用途で使用される。

ジョブ・ジョブ実行・タスク

Cloud Run Job の実行における説明では、「ジョブ」「ジョブ実行」「タスク」という3つの用語が登場する。

  • ジョブ:デプロイするアプリケーションであり、特定の Google Cloud リージョンに配置される。ジョブは1つ以上の「タスク」を含む。
  • ジョブ実行:ジョブが実行されると「ジョブ実行」が作成され、ジョブに含まれるすべてのタスクを開始する。ジョブ実行を成功させるには、すべてのタスクが正常に完了する必要がある。
  • タスク:各タスクは 1 つのコンテナインスタンスを実行する。タスクの再試行回数を設定でき、障害が発生した場合やタスクのタイムアウトを超過した場合にタスクが再施行される。タスクの再試行最大回数を超えると、そのタスクは失敗とマークされ、ジョブ実行は全てのタスクの実行後に失敗とマークされる。

(参考:リソースモデル ジョブを作成する

エントリポイント

ジョブが実行されイメージのコンテナが起動すると、イメージのデフォルトのエントリポイントコマンドとデフォルトのコマンド引数が実行される。
これを上書きしたい場合は、コンテナ構成の command フィールドと args フィールドを使用する(参考:コンテナを構成する)。

実行方法

ジョブは Google Cloud コンソール画面やgcloud CLIから任意のタイミングで実行できる他に、スケジュール実行ワークフローの一部として実行することができる。

事前準備:サンプルアプリケーションの作成

ディレクトリ構成
.
├── Dockerfile
├── go.mod
└── main.go
Dockerfile
FROM golang:1.22.0

COPY . .
RUN GOOS=linux GOARCH=amd64 go build -o /go/main main.go

ENTRYPOINT ["/go/main"]
go.mod
module google-cloud-sample

go 1.22
main.go
package main

import (
	"log"
	"os"
	"strconv"
	"time"
)

type Config struct {
	// Job-defined
	taskNum    string
	attemptNum string

	// User-defined
	sleepMs int64
}

func configFromEnv() (Config, error) {
	// Job-defined
	taskNum := os.Getenv("CLOUD_RUN_TASK_INDEX")      // タスクのインデックス。タスクが2つある時、最初のタスクは0、2つ目のタスクは1となる。
	attemptNum := os.Getenv("CLOUD_RUN_TASK_ATTEMPT") // タスクの再試行回数。最初の試行は0、再試行するたびに1ずつ増加する。
	// User-defined
	sleepMs, err := sleepMsToInt(os.Getenv("SLEEP_MS"))

	if err != nil {
		return Config{}, err
	}

	config := Config{
		taskNum:    taskNum,
		attemptNum: attemptNum,
		sleepMs:    sleepMs,
	}
	return config, nil
}

func sleepMsToInt(s string) (int64, error) {
	if s == "" {
		return 0, nil
	}
	return strconv.ParseInt(s, 10, 64)
}

func main() {
	log.Printf("os.Args:%#v", os.Args)
	config, err := configFromEnv()
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("Starting TaskNo=%s, AttemptNo=%s ...", config.taskNum, config.attemptNum)

	// Simulate work
	if config.sleepMs > 0 {
		time.Sleep(time.Duration(config.sleepMs) * time.Millisecond)
	}

	log.Printf("Completed TaskNo=%s, AttemptNo=%s ...", config.taskNum, config.attemptNum)
}

上記GoアプリケーションをDockerイメージとしてビルドする。

docker build -t asia-northeast1-docker.pkg.dev/sample-project/sample-repo/run-job:latest .

ローカル環境で実行して動作を確認する。

docker run --env CLOUD_RUN_TASK_INDEX=0 \
                --env CLOUD_RUN_TASK_ATTEMPT=0 \
                --env SLEEP_MS=3000 \
                asia-northeast1-docker.pkg.dev/sample-project/sample-repo/run-job:latest \
                 "Hello!!!"
2024/11/09 11:03:12 os.Args:[]string{"/go/main", "Hello!!!"}
2024/11/09 11:03:12 Starting TaskNo=0, AttemptNo=0 ...
2024/11/09 11:03:15 Completed TaskNo=0, AttemptNo=0 ...

Artifact Registry の sample-repo リポジトリに push する(Artifact Registry の使い方はこちらを参照)。

docker push asia-northeast1-docker.pkg.dev/sample-project/sample-repo/run-job:latest

これでサンプルアプリケーションが出来上がり、Cloud Run Job を使う準備が整った。

注意事項

アプリケーションを作成する際、次の内容に注意する。

Cloud Run ジョブの場合、ジョブが正常に完了したときに、コンテナは終了コード 0 で終了する必要があります。ジョブが失敗した場合は、ゼロ以外の終了コードで終了する必要があります。
ジョブはリクエストを処理しないため、コンテナでポートのリッスンや、ウェブサーバーの起動は行わないでください。

引用元:コンテナランタイムの契約

ジョブの作成

公式ドキュメントの「ジョブを作成する」に記載されているYAMLを使った方法に沿ってジョブを作成する。

YAMLファイルの作成

次のようなYAMLファイルを用意する。

job.yaml
apiVersion: run.googleapis.com/v1
kind: Job
metadata:
  name: sample-job # ジョブの名前
spec:
  template:
    metadata:
      labels: # ラベル。ジョブデプロイ時のログや、ジョブ実行時のログに付与される
        label-key: label-value
    spec:
      taskCount: 4 # タスク数
      # 並列処理数
      # まずタスク0とタスク1を並列で走らせ、終わったらタスク3・タスク4を実行する
      parallelism: 2
      template:
        spec:
          # Cloud Run Job に紐づくサービスアカウント
          serviceAccountName: sample-sa@sample-project.iam.gserviceaccount.com
          maxRetries: 1      # 最大再試行回数
          timeoutSeconds: 15 # タイムアウト(s)
          containers:
            # 実行されるコンテナインスタンスの元になるイメージ
            - image: asia-northeast1-docker.pkg.dev/sample-project/sample-repo/run-job:latest
              # 環境変数
              env: 
                - name: SLEEP_MS
                  value: "5000"
              resources:
                limits:
                  memory: 600Mi # メモリ上限
                  cpu: 2000m    # CPU上限

必須項目はジョブの名前とコンテナイメージだが、ここでは以下の項目も設定している。

  • ラベル

  • タスク数

    • 実行するタスクの総数。環境変数 CLOUD_RUN_TASK_COUNT から取得できる。
    • 「並列処理数」の値以上にする必要あり。
  • 並列処理数

    • 並列処理されるタスクの数。
    • 「タスク数」の値以下にする必要あり。環境変数 CLOUD_RUN_TASK_INDEX からプログラムを実行しているTaskのインデックスを取得できる(開始値は 0)。
    • この環境変数を使って次のようなクエリを実行すると、タスク毎に実行する処理を割り振ることができる。
      • SELECT * FROM example_table WHERE processed = 0 LIMIT 100 OFFSET $CLOUD_RUN_TASK_INDEX * 100
  • サービスアカウント

  • 最大再試行回数

  • タスクのタイムアウト

    • デフォルトでは、各タスクは最大 10 分間実行される。この時間は短くすることも、長くすることも可能(最長 24 時間まで)。
  • 環境変数

  • メモリ上限

  • CPU上限

その他に設定できる項目はYAMLスキーマを参照。

ジョブのデプロイ

先ほど作成した YAMLファイルを使って、ジョブをデプロイする。

gcloud run jobs replace job.yaml

ジョブの実行

次のコマンドで指定のジョブ(sample-job)を実行する。

gcloud run jobs execute sample-job

実行すると Google Cloud コンソール画面に結果が表示され、正常終了していること、各種設定値が反映されていることが確認できる。

ジョブ構成をオーバーライドして実行する

引数や環境変数などの設定値を上書きして実行することもできる(参考:特定の実行のジョブ構成をオーバーライドする)。
次のコマンドは引数に"Hello World!"を設定、環境変数SLEEP_MSを20sに上書き、総タスク数を2に上書きして実行している。

gcloud run jobs execute sample-job \
     --args "Hello World!" \
     --update-env-vars SLEEP_MS=20000 \
     --tasks 2

実行した結果を見ると、総タスク数は4から2に変更されており、どちらのタスクも失敗している。

1つ目のタスクログを見ると、コマンド引数に渡した "Hello World!" が表示されている。
また、スリープ時間を20秒に上書きしたためスリープしている間にタイムアウトが到来してタスクが再実行され、再実行も失敗している。

スリープ時間を10sに変更して実行すると、タスクが成功するようになる。

gcloud run jobs execute sample-job \
     --args "Hello World!" \
     --update-env-vars SLEEP_MS=10000 \
     --tasks 2

備考

ロールバックする機能は Cloud Run Job には見当たらなかった。
Cloud Deploy を使ってリリースを管理すればロールバックすることができるので、今後別記事で紹介したい。

Discussion