Cloud Run functions を使ってみる

1. はじめに

概要

Cloud Run functions は、Cloud RunEventarc を活用した Google Cloud の Functions as a Service ソリューションです。パフォーマンスとスケーラビリティを高度な方法で制御できるうえ、関数ランタイムの制御性が向上し、90 以上のイベントソースをトリガーとして利用できます。

この Codelab では、HTTP 呼び出しに応答し、Pub/Sub メッセージと Cloud Audit Logs によってトリガーされる Cloud Run functions の関数を作成します。

この Codelab では、--base-image フラグを使用してベースイメージを指定することで、関数デプロイのベースイメージの自動更新も使用します。Cloud Run のベースイメージの自動更新を構成すると、Google はベースイメージのオペレーティング システムと言語ランタイム コンポーネントにセキュリティ パッチを自動的に適用できます。ベースイメージを更新するためにサービスを再ビルドまたは再デプロイする必要はありません。詳細については、ベースイメージの自動更新をご覧ください。

ベースイメージの自動更新を使用しない場合は、この Codelab に示す例から --base-image フラグを削除します。

学習内容

  • Cloud Run functions の概要と、ベースイメージの自動更新の使用方法について説明します。
  • HTTP 呼び出しに応答する関数を作成する方法。
  • Pub/Sub メッセージに応答する関数を作成する方法。
  • Cloud Storage イベントに応答する関数を作成する方法。
  • 2 つのリビジョン間でトラフィックを分割する方法。
  • 最小インスタンス数を設定してコールド スタートを回避する方法。

2. 設定と要件

ルートフォルダを作成する

すべての例のルートフォルダを作成します。

mkdir crf-codelab
cd crf-codelab

環境変数を設定する

この Codelab 全体で使用する環境変数を設定します。

gcloud config set project <YOUR-PROJECT-ID>
REGION=<YOUR_REGION>

PROJECT_ID=$(gcloud config get-value project)

API を有効にする

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

gcloud services enable \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  eventarc.googleapis.com \
  run.googleapis.com \
  logging.googleapis.com \
  pubsub.googleapis.com

3. HTTP 関数

最初の関数として、HTTP リクエストに応答する認証済みの Node.js 関数を作成します。また、10 分のタイムアウトを使用して、HTTP リクエストに対する関数の応答時間を延長できることを確認します。

作成

アプリのフォルダを作成し、そのフォルダに移動します。

mkdir hello-http
cd hello-http

HTTP リクエストに応答する index.js ファイルを作成します。

const functions = require('@google-cloud/functions-framework');

functions.http('helloWorld', (req, res) => {
  res.status(200).send('HTTP with Node.js in Cloud Run functions!');
});

依存関係を指定する package.json ファイルを作成します。

{
  "name": "nodejs-run-functions-codelab",
  "version": "0.0.1",
  "main": "index.js",
  "dependencies": {
    "@google-cloud/functions-framework": "^2.0.0"
  }
}

導入

関数をデプロイします。

gcloud run deploy nodejs-run-function \
      --source . \
      --function helloWorld \
      --base-image nodejs22 \
      --region $REGION \
      --timeout 600 \
      --no-allow-unauthenticated

このコマンドは、Buildpack を使用して、関数ソースコードを本番環境対応のコンテナ イメージに変換します。

次の点にご注意ください。

  • --source フラグは、関数を実行可能なコンテナベースのサービスにビルドするように Cloud Run に指示するために使用されます。
  • --function フラグ(新規)は、呼び出す関数シグネチャになるように新しいサービスのエントリ ポイントを設定するために使用されます。
  • --base-image フラグ(新規)は、関数のベースイメージ環境(nodejs22python312go123java21dotnet8ruby33php83 など)を指定します。ベースイメージと各イメージに含まれるパッケージの詳細については、ランタイム ベースイメージをご覧ください。
  • (省略可)--timeout フラグを使用すると、HTTP リクエストに応答する関数のタイムアウトを延長できます。この例では、600 秒を使用して 10 分の応答時間を示しています。
  • (省略可)関数が一般公開で呼び出し可能にならないようにする --no-allow-unauthenticated

テスト

次のコマンドを使用して関数をテストします。

# get the Service URL
SERVICE_URL="$(gcloud run services describe nodejs-run-function --region $REGION --format 'value(status.url)')"

# invoke the service
curl -H "Authorization: bearer $(gcloud auth print-identity-token)" -X GET $SERVICE_URL

レスポンスとして HTTP with Node.js in Cloud Run functions! というメッセージが表示されます。

4. Pub/Sub 関数

2 つ目の関数では、特定のトピックにパブリッシュされた Pub/Sub メッセージによってトリガーされる Python 関数を作成します。

Pub/Sub 認証トークンを設定する

2021 年 4 月 8 日以前に Pub/Sub サービス アカウントを有効にした場合は、Pub/Sub サービス アカウントに iam.serviceAccountTokenCreator ロールを付与します。

PROJECT_NUMBER=$(gcloud projects list --filter="project_id:$PROJECT_ID" --format='value(project_number)')

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member  serviceAccount:service-$PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
  --role roles/iam.serviceAccountTokenCreator

作成

サンプルで使用する Pub/Sub トピックを作成します。

TOPIC=cloud-run-functions-pubsub-topic
gcloud pubsub topics create $TOPIC

アプリのフォルダを作成し、そのフォルダに移動します。

mkdir ../hello-pubsub
cd ../hello-pubsub

CloudEvent ID を含むメッセージをロギングする main.py ファイルを作成します。

import functions_framework

@functions_framework.cloud_event
def hello_pubsub(cloud_event):
   print('Pub/Sub with Python in Cloud Run functions! Id: ' + cloud_event['id'])

次の内容の requirements.txt ファイルを作成して、依存関係を指定します。

functions-framework==3.*

導入

関数をデプロイします。

gcloud run deploy python-pubsub-function \
       --source . \
       --function hello_pubsub \
       --base-image python313 \
       --region $REGION \
       --no-allow-unauthenticated

サービス アカウント ID に使用するプロジェクト番号を取得します。

PROJECT_NUMBER=$(gcloud projects list --filter="project_id:$PROJECT_ID" --format='value(project_number)')

トリガーを作成する

gcloud eventarc triggers create python-pubsub-function-trigger  \
    --location=$REGION \
    --destination-run-service=python-pubsub-function  \
    --destination-run-region=$REGION \
    --event-filters="type=google.cloud.pubsub.topic.v1.messagePublished" \
    --transport-topic=projects/$PROJECT_ID/topics/$TOPIC \
    --service-account=$PROJECT_NUMBER-compute@developer.gserviceaccount.com

テスト

トピックにメッセージを送信して関数をテストします。

gcloud pubsub topics publish $TOPIC --message="Hello World"

受信した CloudEvents イベントがログに表示されます。

gcloud run services logs read python-pubsub-function --region $REGION --limit=10

5. Cloud Storage 関数

次の関数では、Cloud Storage バケットからのイベントに応答する Node.js 関数を作成します。

設定

Cloud Storage の関数を使用するには、Cloud Storage サービス アカウントに pubsub.publisher IAM ロールを付与します。

SERVICE_ACCOUNT=$(gsutil kms serviceaccount -p $PROJECT_NUMBER)

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member serviceAccount:$SERVICE_ACCOUNT \
  --role roles/pubsub.publisher

作成

アプリのフォルダを作成し、そのフォルダに移動します。

mkdir ../hello-storage
cd ../hello-storage

Cloud Storage イベントに応答するだけの index.js ファイルを作成します。

const functions = require('@google-cloud/functions-framework');

functions.cloudEvent('helloStorage', (cloudevent) => {
  console.log('Cloud Storage event with Node.js in Cloud Run functions!');
  console.log(cloudevent);
});

依存関係を指定する package.json ファイルを作成します。

{
  "name": "nodejs-crf-cloud-storage",
  "version": "0.0.1",
  "main": "index.js",
  "dependencies": {
    "@google-cloud/functions-framework": "^2.0.0"
  }
}

導入

まず、Cloud Storage バケットを作成します(または、既存のバケットを使用します)。

export BUCKET_NAME="gcf-storage-$PROJECT_ID"
​​export BUCKET="gs://gcf-storage-$PROJECT_ID"
gsutil mb -l $REGION $BUCKET

関数をデプロイします。

gcloud run deploy nodejs-crf-cloud-storage \
 --source . \
 --base-image nodejs22 \
 --function helloStorage \
 --region $REGION \
 --no-allow-unauthenticated

関数がデプロイされると、Cloud Console の Cloud Run セクションに表示されます。

Eventarc トリガーを作成します。

BUCKET_REGION=$REGION

gcloud eventarc triggers create nodejs-crf-cloud-storage-trigger \
  --location=$BUCKET_REGION \
  --destination-run-service=nodejs-crf-cloud-storage \
  --destination-run-region=$REGION \
  --event-filters="type=google.cloud.storage.object.v1.finalized" \
  --event-filters="bucket=$BUCKET_NAME" \
  --service-account=$PROJECT_NUMBER-compute@developer.gserviceaccount.com

テスト

バケットにファイルをアップロードして関数をテストします。

echo "Hello World" > random.txt
gsutil cp random.txt $BUCKET/random.txt

受信した CloudEvents イベントがログに表示されます。

gcloud run services logs read nodejs-crf-cloud-storage --region $REGION --limit=10

6. Cloud Audit Logs

次の関数では、Compute Engine VM インスタンスの作成時に Cloud Audit Log イベントを受け取る Node.js 関数を作成します。この関数は、応答として、VM の作成者を示すラベルを、新しく作成された VM に追加します。

新しく作成された Compute Engine VM を特定する

Compute Engine は、VM の作成時に監査ログを 2 つ出力します。

1 つ目のログは VM の作成が開始されたときに出力されます。2 つ目のログは VM の作成が完了した後に出力されます。

監査ログでは、オペレーション フィールドが異なり、first: true 値と last: true 値が含まれています。2 つ目の監査ログには、インスタンスにラベルを付けるために必要な情報がすべて含まれているため、Cloud Run functions では、last: true フラグを使用して 2 つ目のログを検出します。

設定

Cloud Audit Logs 関数を使用するには、Eventarc で Cloud Audit Logs を利用できるようにし、eventarc.eventReceiver ロールを持つサービス アカウントを使用する必要があります。

  1. Compute Engine API で、Cloud Audit Logs の管理読み取り、データ読み取り、データ書き込みの各ログタイプを有効にします。
  2. デフォルトの Compute Engine サービス アカウントに eventarc.eventReceiver IAM ロールを付与します。
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
  --role roles/eventarc.eventReceiver

関数を作成する

この Codelab では node.js を使用しますが、他の例については https://github.com/GoogleCloudPlatform/eventarc-samples をご覧ください。

package.json ファイルを作成する

{
  "dependencies": {
    "googleapis": "^84.0.0"
  }
}

node.js ファイルを作成する

// Copyright 2021 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 { google } = require("googleapis");
var compute = google.compute("v1");

exports.labelVmCreation = async (cloudevent) => {
  const data = cloudevent.body;

  // in case an event has >1 audit log
  // make sure we respond to the last event
  if (!data.operation || !data.operation.last) {
    console.log("Operation is not last, skipping event");
    return;
  }

  // projects/dogfood-gcf-saraford/zones/us-central1-a/instances/instance-1
  var resourceName = data.protoPayload.resourceName;
  var resourceParts = resourceName.split("/");
  var project = resourceParts[1];
  var zone = resourceParts[3];
  var instanceName = resourceParts[5];
  var username = data.protoPayload.authenticationInfo.principalEmail.split("@")[0];

  console.log(`Setting label username: ${username} to instance ${instanceName} for zone ${zone}`);

  var authClient = await google.auth.getClient({
    scopes: ["https://www.googleapis.com/auth/cloud-platform"]
  });

  // per docs: When updating or adding labels in the API,
  // you need to provide the latest labels fingerprint with your request,
  // to prevent any conflicts with other requests.
  var labelFingerprint = await getInstanceLabelFingerprint(authClient, project, zone, instanceName);

  var responseStatus = await setVmLabel(
    authClient,
    labelFingerprint,
    username,
    project,
    zone,
    instanceName
  );

  // log results of setting VM label
  console.log(JSON.stringify(responseStatus, null, 2));
};

async function getInstanceLabelFingerprint(authClient, project, zone, instanceName) {
  var request = {
    project: project,
    zone: zone,
    instance: instanceName,
    auth: authClient
  };

  var response = await compute.instances.get(request);
  var labelFingerprint = response.data.labelFingerprint;
  return labelFingerprint;
}

async function setVmLabel(authClient, labelFingerprint, username, project, zone, instanceName) {
  var request = {
    project: project,
    zone: zone,
    instance: instanceName,

    resource: {
      labels: { "creator": username },
      labelFingerprint: labelFingerprint
    },

    auth: authClient
  };

  var response = await compute.instances.setLabels(request);
  return response.statusText;
}

導入

関数をデプロイします。

gcloud run deploy gce-vm-labeler \
  --source . \
  --function labelVmCreation \
  --region $REGION \
  --no-allow-unauthenticated

次に、トリガーを作成します。この関数は、--trigger-event-filters フラグを使用して、Compute Engine の挿入に関する監査ログをフィルタしています。

gcloud eventarc triggers create gce-vm-labeler-trigger \
  --location=$REGION \
  --destination-run-service=gce-vm-labeler \
  --destination-run-region=$REGION \
  --event-filters="type=google.cloud.audit.log.v1.written,serviceName=compute.googleapis.com,methodName=v1.compute.instances.insert" \
  --service-account=$ROJECT_NUMBER-compute@developer.gserviceaccount.com

テスト

環境変数を設定します。

# if you're using europe-west1 as your region
ZONE=europe-west1-d
VM_NAME=codelab-crf-auditlog

VM を作成するには、次のコマンドを実行します。

gcloud compute instances create $VM_NAME --zone=$ZONE --machine-type=e2-medium --image-family=debian-11  --image-project=debian-cloud

VM の作成が完了すると、Cloud コンソールの VM の [基本情報] セクションに、または次のコマンドを使用して、追加された creator ラベルが表示されているはずです。

gcloud compute instances describe $VM_NAME --zone=$ZONE

出力に次の例のようなラベルが表示されます。

...
labelFingerprint: ULU6pAy2C7s=
labels:
  creator: atameldev
...

クリーンアップ

VM インスタンスを削除してください。このラボでは、このバケットは再度使用されません。

gcloud compute instances delete $VM_NAME --zone=$ZONE

7. トラフィック分割

Cloud Run functions では、関数の複数のリビジョンがサポートされ、トラフィックをリビジョン間で分割したり、関数を以前のバージョンにロールバックしたりできます。

このステップでは、関数の 2 つのリビジョンをデプロイし、トラフィックを 50 対 50 で分割します。

作成

アプリのフォルダを作成し、そのフォルダに移動します。

mkdir ../traffic-splitting
cd ../traffic-splitting

色に関する環境変数を読み取り、その色の背景の Hello World を返す Python 関数を含む main.py ファイルを作成します。

import os

color = os.environ.get('COLOR')

def hello_world(request):
    return f'<body style="background-color:{color}"><h1>Hello World!</h1></body>'

次の内容の requirements.txt ファイルを作成して、依存関係を指定します。

functions-framework==3.*

導入

背景がオレンジ色の 1 つ目のリビジョンの関数をデプロイします。

COLOR=orange
gcloud run deploy hello-world-colors \
 --source . \
 --base-image python313 \
 --function hello_world \
 --region $REGION \
 --allow-unauthenticated \
 --update-env-vars COLOR=$COLOR

この時点で、ブラウザで HTTP トリガー(上記のデプロイ コマンドの URI 出力)を表示して関数をテストすると、背景がオレンジ色の「Hello World」が表示されます。

36ca0c5f39cc89cf.png

背景が黄色の 2 つ目のリビジョンをデプロイします。

COLOR=yellow
gcloud run deploy hello-world-colors \
 --source . \
 --base-image python313 \
 --function hello_world \
 --region $REGION \
 --allow-unauthenticated \
 --update-env-vars COLOR=$COLOR

これが最新のリビジョンであるため、関数をテストすると、黄色の背景の Hello World が表示されます。

391286a08ad3cdde.png

トラフィックを 50 対 50 で分割する

オレンジと黄色のリビジョン間でトラフィックを分割するには、Cloud Run サービスのリビジョン ID を確認する必要があります。リビジョン ID を表示するコマンドは次のとおりです。

gcloud run revisions list --service hello-world-colors \
  --region $REGION --format 'value(REVISION)'

出力例を以下に示します。

hello-world-colors-00001-man
hello-world-colors-00002-wok

次に、次の手順で 2 つのリビジョン間でトラフィックを分割します(リビジョン名に応じて X-XXX を更新します)。

gcloud run services update-traffic hello-world-colors \
  --region $REGION \
  --to-revisions hello-world-colors-0000X-XXX=50,hello-world-colors-0000X-XXX=50

テスト

パブリック URL にアクセスして関数をテストします。オレンジ色のリビジョンと黄色のリビジョンが半々の割合で表示されます。

36ca0c5f39cc89cf.png 391286a08ad3cdde.png

詳細については、ロールバック、段階的なロールアウト、トラフィックの移行をご覧ください。

8. 最小インスタンス数

Cloud Run functions では、いつでもリクエストを処理できるようにウォーム状態に維持する関数インスタンスの最小数を指定できます。これは、コールド スタートの数を制限するのに役立ちます。

このステップでは、初期化に時間がかかる関数をデプロイします。コールド スタートの問題が発生することを確認します。次に、コールド スタートを回避するために、最小インスタンス数の値を 1 に設定して関数をデプロイします。

作成

アプリのフォルダを作成し、そのフォルダに移動します。

mkdir ../min-instances
cd ../min-instances

main.go ファイルを作成します。この Go サービスには、時間がかかる初期化をシミュレートするために 10 秒間スリープする init 関数があります。また、HTTP 呼び出しに応答する HelloWorld 関数も含まれています。

package p

import (
        "fmt"
        "net/http"
        "time"
)

func init() {
        time.Sleep(10 * time.Second)
}

func HelloWorld(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "Slow HTTP Go in Cloud Run functions!")
}

導入

最小インスタンス数のデフォルト値である 0 を使用して、最初のリビジョンの関数をデプロイします。

gcloud run deploy go-slow-function \
 --source . \
 --base-image go123 \
 --function HelloWorld \
 --region $REGION \
 --no-allow-unauthenticated

次のコマンドで関数をテストします。

# get the Service URL
SERVICE_URL="$(gcloud run services describe go-slow-function --region $REGION --format 'value(status.url)')"

# invoke the service
curl -H "Authorization: bearer $(gcloud auth print-identity-token)" -X GET $SERVICE_URL

最初の呼び出しでは、10 秒の遅延(コールド スタートの実行)の後に、以下のメッセージが表示されます。2 回目以降の呼び出しはすぐに返されます。

最小インスタンス数を設定する

最初のリクエストでコールド スタートが実行されるのを避けるために、次のように --min-instances フラグを 1 に設定して関数を再デプロイします。

gcloud run deploy go-slow-function \
 --source . \
 --base-image go123 \
 --function HelloWorld \
 --region $REGION \
 --no-allow-unauthenticated \
 --min-instances 1

テスト

関数をもう一度テストします。

curl -H "Authorization: bearer $(gcloud auth print-identity-token)" -X GET $SERVICE_URL

最初のリクエストで 10 秒の遅延が発生しなくなります。最小インスタンス数を設定したおかげで、(長時間呼び出しがなかった後の)最初の呼び出しでコールド スタートが実行される問題が解消されました。

詳細については、最小インスタンスの使用をご覧ください。

9. 完了

以上で、この Codelab は完了です。

学習した内容

  • Cloud Run functions の概要と、ベースイメージの自動更新の使用方法について説明します。
  • HTTP 呼び出しに応答する関数を作成する方法。
  • Pub/Sub メッセージに応答する関数を作成する方法。
  • Cloud Storage イベントに応答する関数を作成する方法。
  • 2 つのリビジョン間でトラフィックを分割する方法。
  • 最小インスタンス数を設定してコールド スタートを回避する方法。