Cloud Run ジョブを使ってみる

1. はじめに

96d07289bb51daa7.png

概要

Cloud Run サービスは、期限なしで実行され HTTP リクエストをリッスンするコンテナに適しています。一方、Cloud Run ジョブは、完了まで実行されリクエストを処理しないコンテナに向いています。たとえば、データベースのレコードの処理、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 を使用してスケジュールに基づいて実行するようにジョブを設定します。

プレビューの制約

プレビュー期間中、Cloud Run ジョブには次の制約があります。

  • 1 リージョン、1 プロジェクトあたり、(同じジョブまたは異なるジョブから)同時に実行できるのは最大 50 件です。
  • Cloud Console の Cloud Run ジョブのページでは、既存のジョブの表示、実行の開始、実行ステータスのモニタリングを行うことができます。現時点で Cloud Console はジョブの新規作成をサポートしていないため、ジョブの新規作成には gcloud を使用します。
  • 本番環境のワークロードには、Cloud Run ジョブを使用しないでください。信頼性やパフォーマンスが保証されていないためです。Cloud Run のジョブは、下位互換性のない方法で変更される可能性があり、一般提供の前にほとんど通知されません。

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

学習内容

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

2. 設定と要件

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

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

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

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

Cloud Shell の起動

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

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

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

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

gcloud を設定する

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

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

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 ボタンをクリックして、組み込みのテキスト エディタを使用します。

f78880c00c0af1ef.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 node:17-alpine

# Installs latest Chromium (92) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      npm

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy all files
COPY . .

# Run everything after as a non-privileged user.
USER pptruser

ENTRYPOINT ["node", "screenshot.js"]

5. コンテナ イメージをビルドして公開する

Artifact Registry は、Google Cloud 上の、コンテナ イメージのストレージと管理のサービスです。詳細については、コンテナ イメージの操作をご覧ください。Artifact Registry は、Docker および OCI コンテナ イメージを Docker リポジトリに保存できます。

containers という Artifact Registry リポジトリを新規作成します。

gcloud artifacts repositories create containers --repository-format=docker --location=$REGION

コンテナ イメージをビルドして公開します。

gcloud builds submit -t $REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot:v1

数分後、Artifact Registry でビルドおよびホストされているコンテナ イメージが表示されます。

62e50ebe805f9a9c.png

6. ジョブを作成する

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

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 create screenshot \
  --image=$REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot:v1 \
  --args="https://example.com" \
  --args="https://cloud.google.com" \
  --tasks=2 \
  --task-timeout=5m \
  --set-env-vars=BUCKET_NAME=screenshot-$PROJECT_ID \
  --service-account=screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com

Cloud Run ジョブが作成されますが、ジョブは実行されません。

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

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

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

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

7. ジョブを実行する

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

gcloud beta run jobs list

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

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

gcloud beta run jobs execute screenshot

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

gcloud beta run jobs executions list --job screenshot

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

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

gcloud beta run jobs executions describe screenshot-znkmm
✔ 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

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

e59ed4e532b974b1.png

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

f2f86e60b94ba47c.png

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

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

gcloud beta run jobs executions delete screenshot-znkmm

8. ジョブを更新する

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

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

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

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

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

gcloud beta run jobs execute screenshot --wait

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

ce91c96dcfd271bb.png

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

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

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

gcloud services enable cloudscheduler.googleapis.com

毎日 9:00 に Cloud Run ジョブを実行する Cloud Scheduler ジョブを作成します。

PROJECT_NUMBER="$(gcloud projects describe $(gcloud config get-value project) --format='value(projectNumber)')"

gcloud scheduler jobs create http screenshot-scheduled --schedule "0 9 * * *" \
   --http-method=POST \
   --uri=https://$REGION-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/$PROJECT_ID/jobs/screenshot:run \
   --oauth-service-account-email=$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
   --location $REGION

Cloud Scheduler ジョブが作成済みであり、Cloud Run ジョブを呼び出す準備ができていることを確認します。

gcloud scheduler jobs list

ID: screenshot-scheduled
LOCATION: $REGION
SCHEDULE (TZ): 0 9 * * * (Etc/UTC)
TARGET_TYPE: HTTP
STATE: ENABLED

テストするには、Cloud Scheduler を手動でトリガーします。

gcloud scheduler jobs run screenshot-scheduled

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

971ea598020cf9ba.png

10. 完了

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

学習した内容

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