1. はじめに
概要
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 つだけです。
- ジョブを作成します。これにより、コンテナ イメージ、リージョン、環境変数など、ジョブの実行に必要なすべての構成がカプセル化されます。
- ジョブを実行します。これにより、ジョブの実行が新規作成されます。必要に応じて、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. 設定と要件
セルフペース型の環境設定
- Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。
- プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列で、いつでも更新できます。
- プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud Console により一意の文字列が自動生成されます(通常は内容を意識する必要はありません)。ほとんどの Codelab では、プロジェクト ID を参照する必要があります(通常、プロジェクト ID は「
PROJECT_ID
」の形式です)。好みの文字列でない場合は、別のランダムな ID を生成するか、独自の ID を試用して利用可能であるかどうかを確認することができます。プロジェクトの作成後、ID は「フリーズ」されます。 - 3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
- 次に、Cloud のリソースや API を使用するために、Cloud Console で課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルを終了した後に課金が発生しないようにリソースをシャットダウンするには、Codelab の最後にある「クリーンアップ」の手順を行います。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。
Cloud Shell の起動
Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。
Google Cloud Console で、右上のツールバーにある Cloud Shell アイコンをクリックします。
プロビジョニングと環境への接続にはそれほど時間はかかりません。完了すると、次のように表示されます。
この仮想マシンには、必要な開発ツールがすべて用意されています。永続的なホーム ディレクトリが 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
ボタンをクリックして、組み込みのテキスト エディタを使用します。
以下は、各ファイルに関する簡単な説明です。
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 でビルドおよびホストされているコンテナ イメージが表示されます。
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. 完了
お疲れさまでした。これでこの Codelab は終了です。
学習した内容
- アプリを使用してウェブページのスクリーンショットを撮影する方法
- アプリケーションのコンテナ イメージをビルドする方法
- アプリケーションの Cloud Run ジョブを作成する方法