Cloud Run ジョブを使ってみる

1. はじめに

1965fab24c502bd5.png

概要

Cloud Run サービスは、HTTP リクエストをリッスンして無期限に実行されるコンテナに適しています。一方、Cloud Run ジョブは、完了まで実行され(現在は最大 24 時間)、リクエストを処理しないコンテナに適しています。たとえば、データベースのレコードの処理、Cloud Storage バケットのファイルリストの処理、円周率の計算などの長時間実行オペレーションは、Cloud Run ジョブとして実装するのが適切です。

ジョブには、リクエストを処理したり、ポートをリッスンしたりする機能はありません。つまり、Cloud Run サービスとは異なり、ジョブでウェブサーバーをバンドル処理するのは適切ではありません。ジョブのコンテナは、処理が完了した時点で終了する必要があります。

Cloud Run ジョブでは、タスク数を指定することにより、コンテナの複数のコピーを並列実行できます。各タスクが、コンテナの 1 つの実行中のコピーを表します。各タスクが独立してデータのサブセットを処理できる場合は、複数のタスクを使用すると便利です。たとえば、Cloud SQL の レコードを 10,000 処理する場合や、Cloud Storage のファイルを 10,000 処理する場合は、10 個のタスクがそれぞれ 1,000 個のレコードまたはファイルを並列処理すると、より高速に処理できます。

Cloud Run ジョブを使用するには、次の 2 つの手順を行います。

  1. ジョブを作成する: これにより、コンテナ イメージ、リージョン、環境変数など、ジョブの実行に必要なすべての構成がカプセル化されます。
  2. ジョブを実行する: ジョブの新しい実行が作成されます。必要に応じて、Cloud Scheduler を使用してスケジュールに基づいて実行するようにジョブを設定します。

この Codelab では、まず Node.js アプリケーションを探索し、ウェブページのスクリーンショットを撮影して Cloud Storage に保存します。続いて、アプリケーションのコンテナ イメージをビルドし、Cloud Run ジョブで実行し、ジョブを更新して追加のウェブページを処理し、Cloud Scheduler を使用してスケジュールに基づいてジョブを実行します。

学習内容

  • アプリを使用してウェブページのスクリーンショットを撮影する方法
  • アプリケーションのコンテナ イメージをビルドする方法
  • アプリケーションの Cloud Run ジョブを作成する方法
  • Cloud Run ジョブとしてアプリケーションを実行する方法
  • ジョブを更新する方法
  • Cloud Scheduler を使用してジョブのスケジュールを設定する方法

2. 設定と要件

セルフペース型の環境設定

  1. Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

295004821bab6a87.png

37d264871000675d.png

96d86d3d5655cdbe.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列です。いつでも更新できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud コンソールでは一意の文字列が自動生成されます。通常は、この内容を意識する必要はありません。ほとんどの Codelab では、プロジェクト ID(通常は PROJECT_ID と識別されます)を参照する必要があります。生成された ID が好みではない場合は、ランダムに別の ID を生成できます。または、ご自身で試して、利用可能かどうかを確認することもできます。このステップ以降は変更できず、プロジェクトを通して同じ ID になります。
  • なお、3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud コンソールで課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルの終了後に請求が発生しないようにリソースをシャットダウンするには、作成したリソースを削除するか、プロジェクトを削除します。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloud Shell の起動

Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。

Google Cloud Console で、右上のツールバーにある Cloud Shell アイコンをクリックします。

Cloud Shell をアクティブにする

プロビジョニングと環境への接続にはそれほど時間はかかりません。完了すると、次のように表示されます。

環境が接続されていることを示す Google Cloud Shell ターミナルのスクリーンショット

この仮想マシンには、必要な開発ツールがすべて用意されています。永続的なホーム ディレクトリが 5 GB 用意されており、Google Cloud で稼働します。そのため、ネットワークのパフォーマンスと認証機能が大幅に向上しています。この Codelab での作業はすべて、ブラウザ内から実行できます。インストールは不要です。

gcloud を設定する

Cloud Shell で、プロジェクト ID と、Cloud Run ジョブのデプロイ先にするリージョンを設定します。これらの情報は、PROJECT_ID 変数と REGION 変数として保存します。今後、Cloud Run のロケーションからリージョンを選択できるようになります。

PROJECT_ID=[YOUR-PROJECT-ID]
REGION=us-central1
gcloud config set core/project $PROJECT_ID

API を有効にする

必要なサービスをすべて有効にします。

gcloud services enable \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  run.googleapis.com

3. コードを取得する

まず Node.js アプリケーションを探索し、ウェブページのスクリーンショットを撮影して Cloud Storage に保存します。その後、アプリケーションのコンテナ イメージをビルドし、Cloud Run でジョブとして実行します。

Cloud Shell で次のコマンドを実行して、こちらのリポジトリからアプリケーション コードのクローンを作成します。

git clone https://github.com/GoogleCloudPlatform/jobs-demos.git

アプリケーションが格納されているディレクトリに移動します。

cd jobs-demos/screenshot

次のようなファイル レイアウトが表示されます。

screenshot
 |
 ├── Dockerfile
 ├── README.md
 ├── screenshot.js
 ├── package.json

以下は、各ファイルに関する簡単な説明です。

  • screenshot.js には、アプリケーションの Node.js コードが格納されています。
  • package.json は、ライブラリの依存関係を定義します。
  • Dockerfile は、コンテナ イメージを定義します。

4. コードを探索する

コードを探索するには、Cloud Shell ウィンドウの上部にある Open Editor ボタンをクリックして、組み込みのテキスト エディタを使用します。

15a2cdc9b7f6dfc6.png

以下は、各ファイルに関する簡単な説明です。

screenshot.js

まず、screenshot.js が Puppeteer と Cloud Storage を依存関係として追加します。Puppeteer は、ウェブページのスクリーンショットを撮影するために使用する Node.js ライブラリです。

const puppeteer = require('puppeteer');
const {Storage} = require('@google-cloud/storage');

Puppeteer を初期化する initBrowser 関数と、指定された URL のスクリーンショットを撮影する takeScreenshot 関数があります。

async function initBrowser() {
  console.log('Initializing browser');
  return await puppeteer.launch();
}

async function takeScreenshot(browser, url) {
  const page = await browser.newPage();

  console.log(`Navigating to ${url}`);
  await page.goto(url);

  console.log(`Taking a screenshot of ${url}`);
  return await page.screenshot({
    fullPage: true
  });
}

次に、Cloud Storage バケットを取得または作成する関数と、ウェブページのスクリーンショットをバケットにアップロードする別の関数があります。

async function createStorageBucketIfMissing(storage, bucketName) {
  console.log(`Checking for Cloud Storage bucket '${bucketName}' and creating if not found`);
  const bucket = storage.bucket(bucketName);
  const [exists] = await bucket.exists();
  if (exists) {
    // Bucket exists, nothing to do here
    return bucket;
  }

  // Create bucket
  const [createdBucket] = await storage.createBucket(bucketName);
  console.log(`Created Cloud Storage bucket '${createdBucket.name}'`);
  return createdBucket;
}

async function uploadImage(bucket, taskIndex, imageBuffer) {
  // Create filename using the current time and task index
  const date = new Date();
  date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
  const filename = `${date.toISOString()}-task${taskIndex}.png`;

  console.log(`Uploading screenshot as '${filename}'`)
  await bucket.file(filename).save(imageBuffer);
}

最後に、main 関数はエントリ ポイントです。

async function main(urls) {
  console.log(`Passed in urls: ${urls}`);

  const taskIndex = process.env.CLOUD_RUN_TASK_INDEX || 0;
  const url = urls[taskIndex];
  if (!url) {
    throw new Error(`No url found for task ${taskIndex}. Ensure at least ${parseInt(taskIndex, 10) + 1} url(s) have been specified as command args.`);
  }
  const bucketName = process.env.BUCKET_NAME;
  if (!bucketName) {
    throw new Error('No bucket name specified. Set the BUCKET_NAME env var to specify which Cloud Storage bucket the screenshot will be uploaded to.');
  }

  const browser = await initBrowser();
  const imageBuffer = await takeScreenshot(browser, url).catch(async err => {
    // Make sure to close the browser if we hit an error.
    await browser.close();
    throw err;
  });
  await browser.close();

  console.log('Initializing Cloud Storage client')
  const storage = new Storage();
  const bucket = await createStorageBucketIfMissing(storage, bucketName);
  await uploadImage(bucket, taskIndex, imageBuffer);

  console.log('Upload complete!');
}

main(process.argv.slice(2)).catch(err => {
  console.error(JSON.stringify({severity: 'ERROR', message: err.message}));
  process.exit(1);
});

main メソッドに関しては、次の点に注意してください。

  • URL は引数として渡されます。
  • バケット名は、ユーザー定義の BUCKET_NAME 環境変数として渡されます。バケット名は、Google Cloud 全体で一意である必要があります。
  • CLOUD_RUN_TASK_INDEX 環境変数は、Cloud Run ジョブによって渡されます。Cloud Run ジョブは、アプリケーションの複数のコピーを一意のタスクとして実行できます。CLOUD_RUN_TASK_INDEX は、実行中のタスクのインデックスを表します。Cloud Run ジョブの外部でコードが実行された場合のデフォルトは 0 です。アプリケーションが複数のタスクとして実行される場合、各タスクまたはコンテナは担当する URL を選択し、スクリーンショットを撮影して、画像をバケットに保存します。

package.json

package.json ファイルは、アプリケーションを定義し、Cloud Storage と Puppeteer に関する依存関係を指定します。

{
  "name": "screenshot",
  "version": "1.0.0",
  "description": "Create a job to capture screenshots",
  "main": "screenshot.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Google LLC",
  "license": "Apache-2.0",
  "dependencies": {
    "@google-cloud/storage": "^5.18.2",
    "puppeteer": "^13.5.1"
  }
}

Dockerfile

Dockerfile は、必要なすべてのライブラリと依存関係を含む、アプリケーションのコンテナ イメージを定義します。

FROM ghcr.io/puppeteer/puppeteer:16.1.0
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENTRYPOINT ["node", "screenshot.js"]

5. ジョブをデプロイする

ジョブを作成する前に、ジョブの実行に使用するサービス アカウントを作成する必要があります。

gcloud iam service-accounts create screenshot-sa --display-name="Screenshot app service account"

サービス アカウントに storage.admin ロールを付与して、バケットとオブジェクトの作成に使用できるようにします。

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --role roles/storage.admin \
  --member serviceAccount:screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com

これで、ジョブの実行に必要な構成がされた Cloud Run ジョブをデプロイする準備が整いました。

gcloud beta run jobs deploy screenshot \
  --source=. \
  --args="https://example.com" \
  --args="https://cloud.google.com" \
  --tasks=2 \
  --task-timeout=5m \
  --region=$REGION \
  --set-env-vars=BUCKET_NAME=screenshot-$PROJECT_ID-$RANDOM \
  --service-account=screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com

これはソースベースのデプロイを使用し、実行せずに Cloud Run ジョブを作成します。

ウェブページがどのように引数として渡されるかに注意してください。スクリーンショットを保存するためのバケット名が、環境変数として渡されます。

--tasks フラグを使用して実行するタスクの数を指定することで、コンテナの複数のコピーを並列実行できます。各タスクが、コンテナの 1 つの実行中のコピーを表します。各タスクが独立してデータのサブセットを処理できる場合は、複数のタスクを使用すると便利です。この処理を容易にするために、各タスクはそのインデックスを認識します。インデックスは CLOUD_RUN_TASK_INDEX 環境変数に格納されます。どのタスクがデータのどのサブセットを処理するかは、コードが決定します。このサンプルの --tasks=2 の部分に着目してください。これにより、処理する 2 つの URL に対して 2 つのコンテナが実行されるようになります。

各タスクは、最長で 24 時間実行できます。このタイムアウトは、この例で実施したように、--task-timeout フラグを使用して短くすることができます。ジョブが正常に完了するには、すべてのタスクが正常に行われる必要があります。デフォルトでは、失敗したタスクは再試行されませんが、タスクは失敗時に再試行するように構成できます。いずれかのタスクが再試行回数を超えると、ジョブ全体が失敗となります。

デフォルトでは、ジョブは可能な限り多くのタスクと並列実行されます。この並列実行タスクの数は、ジョブのタスク数と等しくなり、最大は 100 です。スケーラビリティに制限があるバックエンドにアクセスするジョブには、並列処理を控えめに設定することをおすすめします(サポートするアクティブな接続の数に制限があるデータベースの場合などです)。--parallelism フラグを使用すると、並列処理を低く抑えることができます。

6. ジョブを実行する

ジョブを実行する前に、ジョブを表示して、作成済みであることを確認します。

gcloud run jobs list

✔
JOB: screenshot
REGION: us-central
LAST RUN AT:
CREATED: 2022-02-22 12:20:50 UTC

次のコマンドを使用してジョブを実行します。

gcloud run jobs execute screenshot --region=$REGION

これでジョブが実行されます。現在と過去の実行を一覧表示できます。

gcloud run jobs executions list --job screenshot --region=$REGION

...
JOB: screenshot
EXECUTION: screenshot-znkmm
REGION: $REGION
RUNNING: 1
COMPLETE: 1 / 2
CREATED: 2022-02-22 12:40:42 UTC

実行状態を記述します。緑色のチェックマークと、「tasks completed successfully」というメッセージが表示されます。

gcloud run jobs executions describe screenshot-znkmm --region=$REGION

✔ Execution screenshot-znkmm in region $REGION
2 tasks completed successfully


Image:           $REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot at 311b20d9...
Tasks:           2
Args:            https://example.com https://cloud.google.com
Memory:          1Gi
CPU:             1000m
Task Timeout:    3600s
Parallelism:     2
Service account: 11111111-compute@developer.gserviceaccount.com
Env vars:
  BUCKET_NAME    screenshot-$PROJECT_ID-$RANDOM

また、Cloud Console の Cloud Run ジョブのページを調べて、ステータスを確認することもできます。

1afde14d65f0d9ce.png

Cloud Storage バケットを調べると、次の 2 つのスクリーンショット ファイルが作成されたことがわかります。

7c4d355f6f65106.png

場合によっては、実行を完了前に停止する必要が生じることがあります。異なるパラメータを使用してジョブを実行する必要があるか、コードにエラーがあることに気づいて、計算時間を無駄使いしたくないケースなどです。

ジョブの実行を停止するには、実行を削除する必要があります。

gcloud run jobs executions delete screenshot-znkmm --region=$REGION

7. ジョブを更新する

次回の実行時、Cloud Run ジョブは新しいバージョンのコンテナを自動では選択しません。ジョブのコードを変更した場合は、コンテナを再ビルドしてジョブを更新する必要があります。タグ付きの画像を使用すると、現在使用されている画像のバージョンを特定できます。

同様に、一部の構成変数を更新する場合は、ジョブも更新する必要があります。以降のジョブ実行では、新しいコンテナと構成設定が使用されます。

ジョブを更新し、--args フラグでアプリがスクリーンショットを撮影するページを変更します。また、ページ数を反映するように --tasks フラグを更新します。

gcloud run jobs update screenshot \
  --args="https://www.pinterest.com" \
  --args="https://www.apartmenttherapy.com" \
  --args="https://www.google.com" \
  --region=$REGION \
  --tasks=3

ジョブを再度実行します。今回は、--wait フラグを渡して実行が完了するまで待ちます。

gcloud run jobs execute screenshot --region=$REGION --wait

数秒後、次の 3 つのスクリーンショットがバケットに追加されたことが確認できます。

ed0cbe0b5a5f9144.png

8. ジョブのスケジュールを設定する

これまでは、ジョブを手動で実行していました。現実のシナリオでは、イベントに応じて、またはスケジュールに基づいてジョブを実行するが一般的です。Cloud Scheduler を使用して、スケジュールに基づいてスクリーンショット ジョブを実行する方法を説明します。

まず、Cloud Scheduler API が有効になっていることを確認します。

gcloud services enable cloudscheduler.googleapis.com

Cloud Run ジョブの詳細ページに移動し、Triggers セクションをクリックします。

3ae456368905472f.png

Add Scheduler Trigger ボタンを選択します。

48cbba777f75e1eb.png

右側にパネルが開きます。この構成で毎日 9:00 に実行する Scheduler ジョブを作成し、Continue を選択します。

81fd098be0db216.png

次のページで、デフォルトのコンピューティング サービス アカウントを選択し、Create を選択します。

fe479501dfb91f9f.png

新しく作成された Cloud Scheduler トリガーが表示されます。

5a7bc6d96b970b92.png

View Details をクリックして、Cloud Scheduler ページに移動します。

スケジューラが起動する午前 9 時まで待つか、Force Run を選択して Cloud Scheduler を手動でトリガーします。

959525f2c8041a6a.png

数秒後、Cloud Scheduler ジョブが正常に実行されたことを確認できます。

d64e03fc84d61145.png

Cloud Scheduler からの呼び出しによって追加された 3 つのスクリーンショットも確認できます。

56398a0e827de8b0.png

9. 完了

お疲れさまでした。これでこの Codelab は終了です。

クリーンアップ(省略可)

課金が発生しないように、リソースをクリーンアップすることをおすすめします。

プロジェクトが不要になった場合は、プロジェクトを削除します。

gcloud projects delete $PROJECT_ID

プロジェクトが必要な場合は、リソースを個別に削除できます。

ソースコードを削除します。

rm -rf ~/jobs-demos/

Artifact Registry リポジトリを削除します。

gcloud artifacts repositories delete containers --location=$REGION

サービス アカウントを削除します。

gcloud iam service-accounts delete screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com

Cloud Run ジョブを削除します。

gcloud run jobs delete screenshot --region=$REGION

Cloud Scheduler ジョブを削除します。

gcloud scheduler jobs delete screenshot-scheduler-trigger --location=$REGION

Cloud Storage バケットを削除します。

gcloud storage rm --recursive gs://screenshot-$PROJECT_ID

学習した内容

  • アプリを使用してウェブページのスクリーンショットを撮影する方法
  • アプリケーションのコンテナ イメージをビルドする方法
  • アプリケーションの Cloud Run ジョブを作成する方法
  • Cloud Run ジョブとしてアプリケーションを実行する方法
  • ジョブを更新する方法
  • Cloud Scheduler を使用してジョブのスケジュールを設定する方法