Google Cloud の Batch を触ってみました

2023.06.01

こんにちは、川田です。今回は Google Cloud の Batch を触ってみた作業ログを記載します。

実施してみたこと

Cloud Storage 上に置かれたファイルをダウンロードして、zip 圧縮し、Cloud Storage 上に置き直す、という単純な Batch ジョブを作成しています。作業時、以下の設定/実行内容について確認しています。

  • 利用する Compute Engine をコンフィグファイルにて管理する
  • Compute Engine には外部 IP アドレスを付与せず、限定公開の Google アクセスを利用させる
  • ジョブで実行されるコードは、コンテナにて定義する
  • コンテナ環境変数を利用する
  • 実行されるコンテナ上プロセスに引数を渡す
  • ジョブの実行を gcloud コマンドにて行う

環境

  • Google Cloud SDK 433.0.0

事前の準備

必要となる諸々の準備を行います。

VPC を作成

Compute Engine を起動させる VPC を作成します。

$ gcloud compute networks create batch-vpc --subnet-mode=custom

gcloud compute networks create

サブネットを作成します。前述の通り、作成するサブネットは限定公開の google アクセスを有効にしておきます。

$ gcloud compute networks subnets create batch-subnet \
--region=us-central1 \
--network=batch-vpc \
--range=10.10.0.0/24 \
--enable-private-ip-google-access

gcloud compute networks subnets create

コンテナイメージを作成

ジョブとして実行するコンテナイメージを作成します。

以下がディレクトリ構成です。

.
├── app
│ └── main.py
├── Dockerfile
└── requirements.txt

app/main.py

前述の通り、「Cloud Storage 上に置かれたファイルをダウンロードして、zip 圧縮し、Cloud Storage 上に置き直す」という Python のコードです。アクセスする Cloud Storage のバケット名をコンテナ環境変数より取得します。ダウンロードするファイル名を、実行引数より受け取るようにしています。

import argparse
import os
import zipfile

from google.cloud import storage

TMPDIR = "/tmp/batch"
BUCKET_NAME = os.environ["BUCKET_NAME"]

gcs_client = storage.Client()

def main(args: argparse.Namespace):
    os.makedirs(TMPDIR)
    bucket = gcs_client.bucket(BUCKET_NAME)

    dl_blob = bucket.blob(f"batch/dl/{args.file_name}")
    dl_blob.download_to_filename(f"{TMPDIR}/{args.file_name}")

    with zipfile.ZipFile(f"{TMPDIR}/{args.file_name}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=3) as zf:
        zf.write(f"{TMPDIR}/{args.file_name}", arcname=args.file_name)

    ul_blob = bucket.blob(f"batch/ul/{args.file_name}.zip")
    ul_blob.upload_from_filename(f"{TMPDIR}/{args.file_name}.zip")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--file_name", type=str, required=False)
    args = parser.parse_args()

    main(args)

Dockerfile

FROM python:3.11.3-slim
WORKDIR /
COPY . .
RUN pip install --no-cache-dir --requirement requirements.txt
ENTRYPOINT ["python", "-m", "app.main"]

requirements.txt

google-cloud-storage==2.9.0

Artifact Repository に新規リポジトリを作成して、

$ gcloud artifacts repositories create batch \
--repository-format=docker \
--location=us-central1 \
--description="sample"

gcloud artifacts repositories create

Cloud Build にてビルドします。

$ gcloud builds submit --tag us-central1-docker.pkg.dev/PROJECT_ID/batch/app .

gcloud builds submit

サービスアカウントを作成

Compute Engine に付与するサービスアカウントを作成します。

$ gcloud iam service-accounts create sa-batch --display-name="Batch"

gcloud iam service-accounts create

作成したサービスアカウントに、必要となる事前定義ロールを付与します。

$ gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:sa-batch@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/logging.logWriter"
$ gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:sa-batch@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/artifactregistry.reader"
$ gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:sa-batch@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/batch.agentReporter"
$ gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:sa-batch@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.objectAdmin"

gcloud projects add-iam-policy-binding

以下の事前定義ロールを利用しています。

ロール名 付与理由
roles/logging.logWriter Cloud Logging 出力用
roles/artifactregistry.reader コンテナイメージの pull 用
roles/batch.agentReporter Compute Engine が Batch のサービスエージェントをやり取りする際に必要となる
roles/storage.objectAdmin コンテナ内のコードで Cloud Storage にアクセスするため

コンテナジョブを利用する場合、上位3つのロールは必須になるとものと考えています。

Batch ジョブを実行

必要となる設定ファイルを用意して、ジョブを実行してみます。

必要ファイルの作成(ジョブ定義ファイル)

ジョブの構成を定義した JSON ファイルを作成します。

job.json

{
  "taskGroups": [
    {
      "taskSpec": {
        "computeResource": {
          "cpuMilli": "2000",
          "memoryMib": "8000"
        },
        "maxRetryCount": 1,
        "maxRunDuration": "1800s"
      },
      "taskCount": 1,
      "parallelism": 1,
      "taskEnvironments": [
        {
          "variables": {
            "BUCKET_NAME": "xxxx"
          }
        }
      ]
    }
  ],
  "allocationPolicy": {
    "location": {
      "allowedLocations": ["regions/us-central1"]
    },
    "instances": [
      {
        "policy": {
          "machineType": "e2-standard-2",
          "provisioningModel": "STANDARD",
          "bootDisk": {
            "type": "pd-standard",
            "sizeGb": 30,
            "image": "batch-cos"
          }
        }
      }
    ],
    "serviceAccount": {
      "email": "sa-batch@PROJECT_ID.iam.gserviceaccount.com"
    },
    "labels": {
      "gce": "batch-instance"
    },
    "network": {
      "networkInterfaces": [
        {
          "network": "projects/PROJECT_ID/global/networks/batch-vpc",
          "subnetwork": "projects/PROJECT_ID/regions/us-central1/subnetworks/batch-subnet",
          "noExternalIpAddress": true
        }
      ]
    }
  },
  "labels": {
    "batch": "sample"
  },
  "logsPolicy": {
    "destination": "CLOUD_LOGGING"
  }
}

いろいろ書いていますが、.taskGroups[] に実行したいジョブの情報を定義し、.allocationPolicy に作成したい Compute Engine の情報を定義します。projects.locations.jobs の REST API Resource がリファレンスとなるため、そちらを眺めつつ必要な情報を埋めていきます。

Google Cloud | REST Resource: projects.locations.jobs

.taskGroups[].taskEnvironments[] に設定した値が、コンテナの環境変数として渡されます。.allocationPolicy.network.networkInterfaces[] に設定した値が、Compute Engine の起動するネットワーク情報となり、noExternalIpAddress の値を true とすることで、外部 IP アドレスを利用しなくなります。

必要ファイルの作成(実行引数の定義ファイル)

コンテナ上プロセスの、実行引数となる値をファイルに定義します。イメージとしては、Dockerfile の CMD 命令文に記載したい内容を記述することになり、exec 形式で書く場合の要素を、1 行ずつファイルに記述することになります。

cmd.text

-m
app.main
--file_name
test-data

前述の Dockerfile 内では ENTRYPOINT を指定していましたが、実際には、Batch ジョブを実行する際に ENTRYPOINT の値を上書きすることになります(後述されます)。ENTRYPOINT の値は Python の実行 EXE の値に上書きすることになるので、上記ファイルの情報を含めると python -m app.main --file_name test-data という内容が、コンテナ上の起動プロセスコマンドになります。

ジョブを実行

gcloud コマンドを利用してジョブを実行します。今回は、以下の bash スクリプトを作成して、ジョブの実行と結果を併せて確認できるようにしています。

#!/bin/bash
set -eu

now=$(date +%Y%m%d%H%M%S)

gcloud beta batch jobs submit sample-job-"${now}" \
--location us-central1 \
--config ./job.json \
--container-image-uri "us-central1-docker.pkg.dev/PROJECT_ID/batch/app:latest" \
--container-entrypoint python \
--container-commands-file ./cmd.text

while true;
do
    status=$(gcloud beta batch jobs describe sample-job-"${now}" --location us-central1 --format="value(status.state.scope())")
    [[ ${status} = "SUCCEEDED" ]] || [[ ${status} = "FAILED" ]] && break
    printf .
    sleep 10
done

echo "finished. status: ${status}"
exit

gcloud beta batch jobs submit がジョブを実行するコマンドです。--config オプションに作成したジョブ定義ファイルを指定しています。--container-xxx ではじまるオプションで、利用したいコンテナ情報を指定しています。ここで指定した値は Dockerfile 内で定義された値を上書きすることになります。--container-entrypoint オプションでは、Dockerfile の ENTRYPOINT 命令文で書くような shell 形式の値を設定することはできず、単純に実行ファイルの値のみを設定する必要があります。--container-commands-file オプションにて作成した実行引数の定義ファイルを指定しています。

gcloud beta batch jobs submit

submit コマンドのレスポンスでは、ジョブを投入した結果のみが返されるため、gcloud beta batch jobs describe コマンドをループさせて、実行結果を確認しています。

gcloud beta batch jobs describe

スクリプトの出力内容はこんな感じです。

Job sample-job-2023060-5d49d5f8-8e32-4f1b0 was successfully submitted.
(中略)
.....................finished. status: SUCCEEDED

その他

コンテナ上の標準出力/エラー出力のログは、Cloud Logging へ自動的に連携してくれます。

また、以下のようなジョブのステータスの履歴も記録してくれます。

$ gcloud beta batch jobs describe sample-job-20230601012902 --location us-central1 --format="value(status.statusEvents[].scope())"
[
    {
        "description": "Job state is set from QUEUED to SCHEDULED for job projects/123456789012/locations/us-central1/jobs/sample-job-20230601012902.",
        "eventTime": "2023-05-31T16:29:09.249751384Z",
        "type": "STATUS_CHANGED"
    },
    {
        "description": "Job state is set from SCHEDULED to RUNNING for job projects/123456789012/locations/us-central1/jobs/sample-job-20230601012902.",
        "eventTime": "2023-05-31T16:30:25.876520330Z",
        "type": "STATUS_CHANGED"
    },
    {
        "description": "Job state is set from RUNNING to SUCCEEDED for job projects/123456789012/locations/us-central1/jobs/sample-job-20230601012902.",
        "eventTime": "2023-05-31T16:32:48.154303080Z",
        "type": "STATUS_CHANGED"
    }
]

動作は未確認ですが、ジョブ定義ファイルの JobNotification では Cloud Pub/Sub の topic を指定することができ、上記のステータスの情報を Publish してくれるようです。

Google Cloud | REST Resource: projects.locations.jobs jobnotification


追記:ローカル SSD を利用したパターンの記事を投稿しています。

DevelopersIO | Google Cloud の Batch を触ってみました(ローカル SSD 利用編)