Cloud Run入門 - コンテナをサーバーレスでデプロイ
はじめに
皆さん、こんにちは!
今回は Google cloud が提供しているコンテナサーバーレスコンピューティングサービスである Cloud Run をテーマにした記事を書いてみました!!
入門編として Cloud Run の概要についてもまとめています!
チュートリアルにトライした時の記録もありますのでぜひ最後まで読んでください!
Cloud Runとは?
Cloud Runは、Google Cloudが提供するサーバーレスコンピューティングサービスの一つです。
※ 2024年11月で GAになってから 5周年とのことです!
コンテナをベースにしたアプリケーションを、特定のインフラ管理やスケーリングの手間なしに、簡単にデプロイ・実行できます。
AWSにも似たようなサービスとして Fargate があります。
特徴としては以下の点が挙げられます。
-
自動スケーリング
リクエストに応じてインスタンス数を自動的に増減 -
完全マネージド
インフラ管理が不要 -
どんな言語でも対応
コンテナ化されたアプリケーションであれば、どんなプログラミング言語でも動作可能 -
HTTPリクエスト駆動
外部からのHTTPリクエストでアプリケーションをトリガー -
費用効率
使った分だけ料金が発生し、アイドル状態では課金されない
Cloud Runを使うメリット
Cloud Runの最大のメリットは、コンテナをそのままサーバーレスで実行できる点です。
-
スケーラビリティ
トラフィックに応じて自動的にスケールするため、負荷の変動が激しいアプリケーションに適しています。 -
開発スピード向上
インフラ管理を気にせずに、コードの実装に集中できる。 -
マルチクラウド対応
標準的なDockerコンテナを使用しているため、ローカル環境や他のクラウド環境でも再利用が容易。例えば Dockerfileを用意すれば AWS Fargateなどでも動かすことが可能ですね。
マルチクラウドが主流になっている今、この点は非常に重要ですね!
Cloud Runの基本的な使い方
今回は3つのチュートリアルに挑戦してみました!
アプリケーションとして動かしてみるものとジョブとして動かしてみるものです!!
アプリケーションとして動かしてみるものについては事前に用意されたコンテナ上で動かすものと自分でカスタムしたコンテナで動かすものの2週類に挑戦してみました!
-
セットアップ
まず gcloud CLIを動かせるようにする必要があるので以下のコマンドでインストールします!
curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-477.0.0-linux-x86_64.tar.gz tar -xf google-cloud-cli-477.0.0-linux-x86_64.tar.gz ./google-cloud-sdk/install.sh ./google-cloud-sdk/bin/gcloud init
これでバージョンコマンドが実行できればOKです!!
-
事前に用意されたコンテナ上でアプリケーションとして動かしてみる
まずはサービスAPIを有効化してみます!
gcloud services enable run.googleapis.com
次にサービスアカウントを作成します。
gcloud projects add-iam-policy-binding lyrical-art-273306 \ --member=serviceAccount:429380797965-compute@developer.gserviceaccount.com \ --role=roles/cloudbuild.builds.builder
ちなみに今回動かすのは以下のような非常にシンプルな Express + Node.js のアプリケーションです!
import express from 'express'; const app = express(); app.get('/', (req, res) => { const name = process.env.NAME || 'World'; res.send(`Hello ${name}!`); }); const port = parseInt(process.env.PORT) || 8080; app.listen(port, () => { console.log(`helloworld: listening on port ${port}`); });
今回は、 Google Cloud 側で事前に用意されたコンテナ上で動かしてみます(そのためDockerfileは不要です)!
以下のコマンドでデプロイされます!
gcloud run deploy
しばらく待つとAPIエンドポイントが出力されます!
Service [helloworld] revision [helloworld-00001-n5w] has been deployed and is serving 100 percent of traffic. Service URL: https://helloworld-tbj5qgjmvq-bq.a.run.app
Service URL にアクセスして
Hello World!
と表示されたらOKです!めちゃくちゃ簡単ですね!!
びっくりしました!
一応、マネジメントコンソール側でも確認してみます!!
-
自分でコンテナをビルドしてアプリケーションを動かしてみる
次は自分で Dockerfileを用意してアプリケーションを動かしてみます!
もろもろ条件があるみたいなので要確認のこと。
-
サービスはリクエストをリッスンする必要があります。
リクエストの送信に使われるポートを構成できます。Cloud Run インスタンス内では、リクエスト送信先ポートが PORT 環境変数の値に常に反映されます。
この PORT 環境変数が存在するかどうかをコードで検査してください。存在する場合は、移植性が最大になるようそのポートでリッスンするのが適切です。
-
サービスはステートレスである必要があります。永続的なローカル状態に依存することはできません。
-
サービスがリクエスト処理の範囲外のバックグラウンド アクティビティを実行する場合は、[CPU を常に割り当てる] 設定を使用する必要があります。
-
サービスがネットワーク ファイル システムを使用する場合は、第 2 世代の実行環境を使用する必要があります。
第2世代の実行環境については以下のサイトを参照のこと
https://cloud.google.com/run/docs/about-execution-environments?hl=ja
以下で最新化
gcloud components update
テンプレコードをcloneする。
git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git
サンプルコードの格納先に移動する。
cd nodejs-docs-samples/run/system-package/
以下のコマンドを
Dockerfile
に追加する。RUN apt-get update -y && apt-get install -y graphviz && apt-get clean
中身の構造は以下の通り。
. ├── Dockerfile ├── README.md ├── app.js ├── index.js ├── package.json └── test ├── app.test.js ├── e2e_test_cleanup.yaml ├── e2e_test_setup.yaml ├── retry.sh └── system.test.js
Dockerfile
の中身は以下の通りFROM node:20-alpine WORKDIR /usr/src/app RUN apt-get update -y && apt-get install -y graphviz && apt-get clean # RUN apk --no-cache add graphviz ttf-ubuntu-font-family COPY package*.json ./ RUN npm install --only=production COPY . . CMD [ "npm", "start" ]
package.json の中身は以下の通り
{ "name": "graphviz-web", "version": "1.0.0", "description": "Demonstrates a Cloud Run service which provides a CLI tool over HTTP.", "main": "index.js", "private": true, "scripts": { "start": "node index.js", "test": "c8 mocha -p -j 2 test/app.test.js --check-leaks", "system-test": "echo 'SKIPPING E2E TEST: SEE b/358734748'", "FIXME-system-test": "c8 mocha -p -j 2 test/system.test.js --timeout=360000 --exit" }, "engines": { "node": ">=18.0.0" }, "dependencies": { "express": "^4.17.1" }, "devDependencies": { "c8": "^10.0.0", "google-auth-library": "^9.0.0", "got": "^11.5.0", "mocha": "^10.0.0", "supertest": "^7.0.0" } }
ローカルでコンテナをビルドする。
docker build .
ロジックを実装している
app.js
の中身// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const {execSync} = require('child_process'); const fs = require('fs'); const express = require('express'); const app = express(); // Verify the the dot utility is available at startup // instead of waiting for a first request. fs.accessSync('/usr/bin/dot', fs.constants.X_OK); // [START cloudrun_system_package_handler] app.get('/diagram.png', (req, res) => { try { const image = createDiagram(req.query.dot); res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', image.length); res.setHeader('Cache-Control', 'public, max-age=86400'); res.send(image); } catch (err) { console.error(`error: ${err.message}`); const errDetails = (err.stderr || err.message).toString(); if (errDetails.includes('syntax')) { res.status(400).send(`Bad Request: ${err.message}`); } else { res.status(500).send('Internal Server Error'); } } }); // [END cloudrun_system_package_handler] // [START cloudrun_system_package_exec] // Generate a diagram based on a graphviz DOT diagram description. const createDiagram = dot => { if (!dot) { throw new Error('syntax: no graphviz definition provided'); } // Adds a watermark to the dot graphic. const dotFlags = [ '-Glabel="Made on Cloud Run"', '-Gfontsize=10', '-Glabeljust=right', '-Glabelloc=bottom', '-Gfontcolor=gray', ].join(' '); const image = execSync(`/usr/bin/dot ${dotFlags} -Tpng`, { input: dot, }); return image; }; // [END cloudrun_system_package_exec] module.exports = app;
Artifact Registry を作成する。
gcloud artifacts repositories create cloud-run-source-deploy --repository-format docker --location us-central1
以下のコマンドでコンテナイメージをビルドし、 Artifact Registryに登録する。
gcloud builds submit --tag us-central1-docker.pkg.dev/lyrical-art-273306/cloud-run-source-deploy/graphviz
以下のように表示されればOK!
SUCCESS
マネジメントコンソールでも確認が取れました!
サービスアカウントを作成する。
gcloud iam service-accounts create sampleGcloudAccount
サービスをデプロイする。
gcloud run deploy graphviz-web --service-account sampleGcloudAccount@lyrical-art-273306.iam.gserviceaccount.com --image us-central1-docker.pkg.dev/lyrical-art-273306/cloud-run-source-deploy/graphviz
以下のように表示されればOK!!
Done. Service [graphviz-web] revision [graphviz-web-00002-nl5] has been deployed and is serving 100 percent of traffic. Service URL: https://graphviz-web-429380797965.us-east1.run.app
マネジメントコンソールでも確認してみます!
以下でデプロイした機能を試せます。
https://graphviz-web-429380797965.us-east1.run.app/diagram.png?dot=digraph Run { rankdir=LR Code -> Build -> Deploy -> Run }
ダイアグラムが出力されます!!
めっちゃ簡単でびっくりしました!!
Fargateよりも使い勝手が良さそう!!
-
-
ジョブを動かしてみた
最後にジョブとして動かしてみました!!
基本はこれまでと同じです。
ちょっと異なるのは タスク などの概念が出てくるところですかね。
-
Cloud Run ジョブ
Cloud Run ジョブを使用すると、実行すタスクの数を指定できます。
組み込みの環境変数として CLOUD_RUN_TASK_INDEX というものがあるらしく、今回はこれを利用します。
これは、各タスクがコンテナの 1 つの実行中のコピーを表します。タスクは通常、並行して実行されます。各タスクが独立してデータのサブセットを処理できる場合は、複数のタスクを使用すると便利です。
各タスクはインデックスを認識し、CLOUD_RUN_TASK_INDEX 環境変数に格納されます。そして組み込みの CLOUD_RUN_TASK_COUNT 環境変数には、ジョブの実行時に --tasks パラメータを介して指定されたタスクの数が含まれています。
このコードは、組み込みの CLOUD_RUN_TASK_ATTEMPT 環境変数を使用してタスクを再試行する方法を示しています。この変数はタスクの再試行回数を表します。最初の再試行が行われると、この変数に 0 が設定され、--max-retries になるまで再試行のたびに値が 1 ずつ増加します。
-
Procfile
もう一つ Procfile というものも登場します!
ProcfileはHerokuへのRailsアプリケーションのデプロイでおなじみの方もおられるかと思いますが、Foremanやhonchoでも使用される設定ファイルの形式です。Dockerfileのように拡張子はなくProcfileというファイルを作成します。
今回だと以下のような内容です。
# Define the application's entrypoint to override default, `npm start` # https://github.com/GoogleCloudPlatform/buildpacks/issues/160 web: node index.js
今回動かす
index.js
の中身は以下の通りです。// Retrieve Job-defined env vars const { CLOUD_RUN_TASK_INDEX = 0, CLOUD_RUN_TASK_ATTEMPT = 0 } = process.env; // Retrieve User-defined env vars const {SLEEP_MS, FAIL_RATE} = process.env; /** * メインスクリプト */ const main = async () => { console.log( `Starting Task #${CLOUD_RUN_TASK_INDEX}, Attempt #${CLOUD_RUN_TASK_ATTEMPT}...` ); // Simulate work if (SLEEP_MS) { await sleep(SLEEP_MS); } // Simulate errors if (FAIL_RATE) { try { randomFailure(FAIL_RATE); } catch (err) { err.message = `Task #${CLOUD_RUN_TASK_INDEX}, Attempt #${CLOUD_RUN_TASK_ATTEMPT} failed.\n\n${err.message}`; throw err; } } console.log(`Completed Task #${CLOUD_RUN_TASK_INDEX}.`); }; // Wait for a specific amount of time const sleep = ms => { return new Promise(resolve => setTimeout(resolve, ms)); }; // Throw an error based on fail rate const randomFailure = rate => { rate = parseFloat(rate); if (!rate || rate < 0 || rate > 1) { console.warn( `Invalid FAIL_RATE env var value: ${rate}. Must be a float between 0 and 1 inclusive.` ); return; } const randomFailure = Math.random(); if (randomFailure < rate) { throw new Error('Task failed.'); } }; // Start script main().catch(err => { console.error(err); process.exit(1); // Retry Job Task by exiting the process });
まず、Artifact Registryにコンテナイメージを送信します!!
プッシュする際にタスク数などを環境変数として渡しています!
gcloud run jobs deploy job-quickstart \ --source . \ --tasks 50 \ --set-env-vars SLEEP_MS=10000 \ --set-env-vars FAIL_RATE=0.1 \ --max-retries 5 \ --region us-east1 \ --project=lyrical-art-273306
マネジメントコンソールを確認してみます!
無事にデプロイできているようです!
ではジョブを実行してみます!!
gcloud run jobs execute job-quickstart --region us-east1
✓ Creating execution... Done. Done. Execution [job-quickstart-g8ng5] has successfully started running. View details about this execution by running: gcloud run jobs executions describe job-quickstart-g8ng5 Or visit https://console.cloud.google.com/run/jobs/executions/details/us-east1/job-quickstart-g8ng5/tasks
マネジメントコンソールに移動してみると!!
ちゃんと50タスク実行されていそうですね!!
めっちゃ簡単!!!
-
まとめ
いかがでしたでしょうか?
Cloud Runは、アプリケーションをコンテナ化してサーバーレスで実行するための強力なツールでした!!
今回の入門記事では、Cloud Runの基本的な使い方とメリットを紹介しましたが、もっと多くのサンプルやワークショップにトライしてみて高度なアプリケーションを実装できるようになりたいなと思いました!!
AWS も Google Cloud も最初はよく分かんなかったですが、動かせるようになるとこれが非常に楽しい!!
サーバーレス は最高だ!!
ここまで読んでいただきありがとうございました!
参考文献
- Google Cloud コンソール
- Qita - Google Cloud Run を使うまで
- Cloud Run を最速で触ってみる
- Cloud Runのドキュメント
- Cloud Run クイックスタート
- Cloud Run コンソール画面
- Cloud Run Node.js クイックスタート
- Cloud CLIのインストール手順
- Cloud Run - 実行環境
- カスタムイメージを作ってCloud Runで動かす方法のチュートリアル
- Artifact Registry コンソール画面
- Cloud Run での Node.js ジョブのビルドと作成
- GitHub - GoogleCloudPlatform/cloud-run-samples
- Zenn - Cloud Run ジョブ ことはじめ
- GitHub - google-cloud-japan/gcp-getting-started-cloudrun
- FireStore ドキュメント
- Firebase CLI ドキュメント
Discussion