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"

受信した CloudEvent がログに表示されます。

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

受信した CloudEvent がログに表示されます。

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 を特定する

VM が作成されると、Compute Engine は 2 つの監査ログを出力します。

最初の 1 つは、VM の作成の開始時に発行されます。2 つ目は、VM の作成後に発行されます。

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

設定

Cloud Audit Log 関数を使用するには、Eventarc の監査ログを有効にする必要があります。また、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 Console の [基本情報] セクションで、または次のコマンドを使用して、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.*

導入

オレンジ色の背景で関数の最初のリビジョンをデプロイします。

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 秒の遅延(コールド スタート)が発生し、その後メッセージが表示されます。後続の呼び出しはすぐに返される必要があります。

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

最初のリクエストでコールド スタートを回避するには、次のように --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 つのリビジョン間でトラフィックを分割する方法。
  • 最小インスタンスを使用してコールド スタートを回避する方法。