⚒️

Batch と Cloud Run Jobs ってどっち使えばいいの? - Batch 編 -

2023/05/07に公開

AWS と Google Cloud で構築したデータ基盤の開発・運用に携わっているデータエンジニアです。5 年くらい携わっていて、この業務がきっかけで Google Cloud が好きになりました。

この記事では Google Cloud での定期実行のマネージド化について検証したいと思います。現状は Cloud Functions + Pub/Sub + Cloud Scheduler という構成で実行しています。そちらをいい感じにできないかを探っていきたいと思います。

選択肢として BatchCloud Run Jobs が思い浮かんだのですが、やっぱり触ってみないと特性の差がわからないなと。。ということで、手始めに Batch を中心としたマネージドな定期実行の実装 についてまとめていきます。

Overview

Batch を活用した定期実行方法について検証

  • 最終的には Batch + Workflows + Cloud Scheduler で実現できました
  • 現在は Cloud Functions + Cloud Pub/Sub + Cloud Scheduler という構成で実装してあり、もっとこうなると良いという点は下記です


    ◼️ VPC 内で実行したい
    VPC Service Controls を利用しているため Functions でコネクタの利用が必須となっている、VPC SC 適用サービスにアクセスできる代わりにインターネットに出れない
    ⇒ Batch のジョブを VPC 内で実行できるため VPC SC 適用サービスへのアクセスは問題なし、閉域でのインターネット接続も Cloud Nat との併用で可能そう


    ◼️ IaC のコード量を減らしたい
    Terraform で記述しているがコード量が多くなってしまっている、より完結に記述できたら似たような構成で定期実行のデプロイをスケールしやすい
    ⇒ 今回の検証内容であれば、Workflows リソースおよびワークフロー定義 / Cloud Scheduler のみで良い、ファイルを上手く切り出せたら結構減らせそう


    ◼️ 新しいサービスを使いたい
    現状の構成は上記のサービスが出る前の構成であるため、よりモダンなものとしたい
    ⇒ Batch にせよ Workflows にせよ比較的新しいサービスである

総じて、移行は可能だし得られるメリットもありそうです。ただ、Functions 中心の構成だとコードだけ用意すればよかったところに、Batch 中心の構成ではコンテナも必要となります。その点も手間といえば手間ですが、自動化の余地はあるので問題にはならそうです。

キーワード

余談

今回は Cloud Workstations で開発しました。

Functions で実行しているコードをいじったり、Cloud Batch で必要となるジョブ定義や Workflows で必要となるワークフロー定義などを作成する際に GUI ベース(Code OSS)で編集できたこと & 定期実行環境と同様のネットワーク内で実行できた点が非常に便利でした。

Batch による定期実行

基本的な使い方に関しては参考記事を見ていただければと思います。
今回 Cloud Batch はコンテナによる実行で検証してみます。

1.コンテナの作成

Python で実行するコードを用意しました。サービスアカウントキーが払い出されて、そのキーが作成されてから特定の日数経過していたら削除するというものです。(コードは別の機会に共有します)

Artifact Registry にコンテナイメージを格納します。今回は手っ取り早く Python3 のイメージにコードと必要なライブラリを内包させます。

terminal
# Artifact Registry に image-for-batch というレジストリを作成します
gcloud artifacts repositories create image-for-batch \
--repository-format=docker --location=asia-northeast1

# python3 というコンテナに必要な資材をセットアップしている前提です
docker commit python3 \
asia-northeast1-docker.pkg.dev/image-for-batch/observe-sa-key:latest

# Artifact Registry に認証します
gcloud auth configure-docker asia-northeast1-docker.pkg.dev

# コンテナを格納します
docker push \
asia-northeast1-docker.pkg.dev/image-for-batch/observe-sa-key:latest

2.ジョブの実行

いつの間にか東京リージョンで利用できるようになっていましたね。(公式ドキュメント)

Batch でのジョブ実行はコンソールからも出来ますが、設定項目が少ないです。例えば、ネットワーク設定なんかはできません。 なので、今回はジョブ定義を JSON 形式で記載して gcloud コマンドに渡して実行させます。

JSON 形式の書き方は参考記事の G-gen さんのブログを見つつ、公式ドキュメントで調べながら記述しました。

jobconfig.json
{
  "taskGroups": [
    {
      "taskSpec": {
        "runnables": [
	  {
            "container": {
	      "imageUri": "asia-northeast1-docker.pkg.dev/[PROJECT_ID]/image-for-batch/observe-sa-key",
	      "entrypoint": "/bin/bash",
	      "commands": [
                "-c",
	        "python3 /root/observe_sakey.py"
	      ]
            }
	  }
	],
	"environment": {
          "variables": {
	    "GCP_PROJECT": "[PROJECT_ID]"
	  }
	},
	"computeResource": {
          "cpuMilli": 1000,
          "memoryMib": 1000
        }
      },
      "taskCount": 1
    }
  ],
  "allocationPolicy": {
    "serviceAccount": {
      "email": "[SERVICE_ACCOUNT]"
    },
    "instances": [
      {
        "policy": { "machineType": "e2-small" }
      }
   ],
   "network" : {
     "networkInterfaces": [
	{
          "network": "projects/[PROJECT_ID]/global/networks/batch-vpc",
          "subnetwork": "projects/[PROJECT_ID]/regions/asia-northeast1/subnetworks/batch-subnet"
        }
      ]
    }
  },
  "logsPolicy": {
    "destination": "CLOUD_LOGGING"
  }
}

こちらを下記の gcloud コマンドでジョブを実行します。 batch-test というジョブ名で東京リージョンかつ上記で記述した jobconfig.json をジョブ定義ファイルとして渡します。

terminal
gcloud batch jobs submit batch-test \
--location asia-northeast1  --config jobconfig.json

試行錯誤の末、コンソール上で SUCCESS となりました。今回は Batch の理解が優先事項だったので Batch についてはこんなもんで。ちょいちょいエラーにはまってしまったので、トラブルシュートも簡単にまとめておきます。

トラブル①:No VM has agent reporting correctly within time window

こちらは公式ドキュメントのトラブルシュートに載っていました。 Agent Report ロールが足りなかったために発生していました。

Batch のジョブ定義に設定したサービスアカウントに権限を付与することで解決できました。

トラブル②:ジョブ実行は成功、ログは一切出ていない

こちらも公式ドキュメントのトラブルシュートにありましたが、タスクが実行されなかったということしかわかりませんでした。

ジョブの特性上の問題と考えて Cloud Batch のジョブ定義に設定したサービスアカウントに権限を付与することで解決できました。(よくある広めに権限をつけて狭めていくような形で対応しました。)

3. Workflows によるジョブ再利用化

今回 Batch を使ってみて初めて知りましたが、Batch でのジョブはワンショット実行でした。つまり、1 回の実行が(成否に関わらず)完了したものは再利用できないということです。(実行完了済みジョブを複製することで新たなジョブとして実行することは可能ですが。)

定義したジョブを再利用可能な状態にするためには Workflows を利用します。基礎情報は参考記事にある G-gen さんのブログがおすすめです。Batch との連携の実態はワークフロー定義に Batch のジョブ定義を記述しておき、Worflows を実行する度に Batch ジョブを生成しているようなイメージです。

下記は Workflows にデプロイした JSON 形式のワークフロー定義ファイルです。公式ドキュメントに記載されているサンプルコードを利用しています。

workflowconfig.json
{
  "main": {
    "params": [
      "args"
    ],
    "steps": [
      {
        "init": {
          "assign": [
            {
              "projectId": "${sys.get_env(\"GOOGLE_CLOUD_PROJECT_ID\")}"
            },
            {
              "region": "asia-northeast1"
            },
            {
              "batchApi": "batch.googleapis.com/v1"
            },
            {
              "batchApiUrl": "${\"https://\" + batchApi + \"/projects/\" + projectId + \"/locations/\" + region + \"/jobs\"}"
            },
            {
              "jobId": "${\"job-observe-sa-key-\" + string(int(sys.now()))}"
            }
          ]
        }
      },
      {
        "createAndRunBatchJob": {
          "call": "http.post",
          "args": {
            "url": "${batchApiUrl}",
            "query": {
              "job_id": "${jobId}"
            },
            "headers": {
              "Content-Type": "application/json"
            },
            "auth": {
              "type": "OAuth2"
            },
            "body": {
              "taskGroups": [
                {
                  "taskSpec": {
                   "runnables": [
                    {
                     "container": {
                      "imageUri": "asia-northeast1-docker.pkg.dev/[PROJECT_ID]/image-for-batch/observe-sa-key",
                     "entrypoint": "/bin/bash",
                     "commands": [
                       "-c",
                      "python3 /root/observe_sakey.py"
                     ]
                   }
                 }
                ],
                "environment": {
                    "variables": {
                    "GCP_PROJECT": "[PROJECT_ID]"
                  }
                },
                "computeResource": {
                  "cpuMilli": 1000,
                 "memoryMib": 1000
                }
                },
                  "taskCount": 1
                } 
                ],
                "allocationPolicy": {
                "serviceAccount": {
                    "email": "[SERVICE_ACCOUNT]"
                },
                "instances": [
                    {
                    "policy": { "machineType": "e2-small" }
                    }
                ],
                "network" : {
                "networkInterfaces": [
                {
                        "network": "projects/[PROJECT_ID]/global/networks/batch-vpc",
                        "subnetwork": "projects/[PROJECT_ID]/regions/asia-northeast1/subnetworks/batch-subnet"
                    }
                    ]
                }
                },
                "logsPolicy": {
                "destination": "CLOUD_LOGGING"
                }
            }
            },
            "result": "createAndRunBatchJobResponse"
          }
        },
      {
        "getJob": {
          "call": "http.get",
          "args": {
            "url": "${batchApiUrl + \"/\" + jobId}",
            "auth": {
              "type": "OAuth2"
            }
          },
          "result": "getJobResult"
        }
      },
      {
        "logState": {
          "call": "sys.log",
          "args": {
            "data": "${\"Current job state \" + getJobResult.body.status.state}"
          }
        }
      },
      {
        "checkState": {
          "switch": [
            {
              "condition": "${getJobResult.body.status.state == \"SUCCEEDED\"}",
              "next": "returnResult"
            },
            {
              "condition": "${getJobResult.body.status.state == \"FAILED\"}",
              "next": "failExecution"
            }
          ],
          "next": "sleep"
        }
      },
      {
        "sleep": {
          "call": "sys.sleep",
          "args": {
            "seconds": 10
          },
          "next": "getJob"
        }
      },
      {
        "returnResult": {
          "return": {
            "jobId": "${jobId}"
          }
        }
      },
      {
        "failExecution": {
          "raise": {
            "message": "${\"The underlying batch job \" + jobId + \" failed\"}"
          }
        }
      }
    ]
  }
}

一見複雑そうですが、Batch との連携に関しては createAndRunBatchJob のステップに注目です。こちらの内容は JSON 形式で定義した Batch のジョブ定義になります、これが API へのリクストに乗っかってジョブの作成および実行まで行われます。

こちらも gcloud コマンドに渡してデプロイします。

terminal
gcloud workflows deploy workflow-for-batch \
--location=asia-northeast1 \
--source=workflowconfig.json \
--service-account=hogehoge@[PROJECT_ID].iam.gserviceaccount.com

これで Batch で実行したいジョブは再利用可能な状態になりました。手動であればポチポチで実行できますが、定期実行ということで最後の仕上げに Cloud Scheduler と連携させます。

4. Cloud Scheduler で Workflows をキック

定期実行ということで最後はやはり Cloud Scheduler になるんですね(笑)
(元の構成と似通ってきたような・・)

でも、Scheduler の細かい設定は必要なく Workflows のコンソール画面で今回作成した Workflow の編集の中のトリガーオプションの設定でさくっと可能でした。こちらで設定すると、Cloud Scheduler のコンソールにリソースが作成されていました。

めでたく Batch + Workflows + Cloud Scheduler という構成で定期実行ができました!!
思ったよりボリュームがあって 1 日費やしてしましたが、色々サービスを連携させて動くと気持ちがいいですね(笑)

さいごに

Batch を中心としたマネージド定期実行についてまとめました。
VPC 内で実行できるということでネットワーク制御がしやすそうという印象です。
今回のケースでの検討は冒頭に書いた通りです。処理自体は軽いので GPU が必要といった特殊な要件はないので上記の内容で Cloud Run Jobs の構成と比較してみたいです。

参考記事

Discussion