はじめに
こんにちは、サーバーサイドエンジニアの加味(@kami_tsukai)です。弊社には、CloudStorage のファイナライズをトリガーとして、受け取った情報を処理し、Pub/Sub にパブリッシュする Cloud Functions 関数があります。その関数のGoバージョンを1.19から1.22に上げる際に第2世代の対応が必要になりました。
第2世代では、Go における CloudEventのサポート、同時実行、HTTPトリガーの場合のタイムアウトが最大9分から60分に延長されるなど、いくつかの重要な改善が行われています。第2世代と第1世代の詳細は公式ドキュメントを参照ください。
このブログでは、Cloud Functions を第2世代へ移行したときに実施した変更点など紹介します。
やること
- CloudEvent 関数を利用したコードに書き換える
- Eventarc APIの有効化とロールの付与
- CLIでデプロイしている場合)指定するオプションを変更
CloudEvent 関数を利用したコードに書き換える
Cloud Functions では、イベントをトリガーに関数を呼び出す場合にはイベントドリブン関数を使用します。この関数は、イベントデータの標準仕様である CloudEvent をベースにしており、SDKが提供されているのでそれらを利用して関数を作成します。イベントドリブン関数については、公式ドキュメントに詳しく載ってますのでそちらを参照ください、
※ Go 1.19 から 1.22 のアップデートも含まれているため、それに伴うコードの修正も必要になってきますが、この記事では割愛させていただきます。
まず、CloudEvent 関数を作成するのに必要なパッケージを追加します。
$ go get github.com/cloudevents/sdk-go/v2 $ go get github.com/GoogleCloudPlatform/functions-framework-go $ go get github.com/googleapis/google-cloudevents-go
特に、Functions Framework は、Cloud Functions のローカル開発用サーバーの起動などもできる便利なパッケージですが、今回は、CloudEvents 仕様に準拠したイベントの自動アンマーシャルの目的で利用しています。(ローカル開発はこちら)
次はコードを書き換えていきます。以下は、第2世代のGCSイベントをトリガーとしたコードです。こちらは、Google Cloud Platformが提供する functionsv2サンプルコード から引用しています。
package helloworld import ( "context" "fmt" "log" "github.com/GoogleCloudPlatform/functions-framework-go/functions" "github.com/cloudevents/sdk-go/v2/event" "github.com/googleapis/google-cloudevents-go/cloud/storagedata" "google.golang.org/protobuf/encoding/protojson" ) func init() { functions.CloudEvent("HelloStorage", helloStorage) } // helloStorage consumes a CloudEvent message and logs details about the changed object. func helloStorage(ctx context.Context, e event.Event) error { log.Printf("Event ID: %s", e.ID()) log.Printf("Event Type: %s", e.Type()) var data storagedata.StorageObjectData if err := protojson.Unmarshal(e.Data(), &data); err != nil { return fmt.Errorf("protojson.Unmarshal: %w", err) } log.Printf("Bucket: %s", data.GetBucket()) log.Printf("File: %s", data.GetName()) log.Printf("Metageneration: %d", data.GetMetageneration()) log.Printf("Created: %s", data.GetTimeCreated().AsTime()) log.Printf("Updated: %s", data.GetUpdated().AsTime()) return nil }
第1世代のコードはこちら
package helloworld
import (
"context"
"fmt"
"log"
"time"
"cloud.google.com/go/functions/metadata"
)
// GCSEvent is the payload of a GCS event.
type GCSEvent struct {
Kind string `json:"kind"`
ID string `json:"id"`
SelfLink string `json:"selfLink"`
Name string `json:"name"`
Bucket string `json:"bucket"`
Generation string `json:"generation"`
Metageneration string `json:"metageneration"`
ContentType string `json:"contentType"`
TimeCreated time.Time `json:"timeCreated"`
Updated time.Time `json:"updated"`
TemporaryHold bool `json:"temporaryHold"`
EventBasedHold bool `json:"eventBasedHold"`
RetentionExpirationTime time.Time `json:"retentionExpirationTime"`
StorageClass string `json:"storageClass"`
TimeStorageClassUpdated time.Time `json:"timeStorageClassUpdated"`
Size string `json:"size"`
MD5Hash string `json:"md5Hash"`
MediaLink string `json:"mediaLink"`
ContentEncoding string `json:"contentEncoding"`
ContentDisposition string `json:"contentDisposition"`
CacheControl string `json:"cacheControl"`
Metadata map[string]interface{} `json:"metadata"`
CRC32C string `json:"crc32c"`
ComponentCount int `json:"componentCount"`
Etag string `json:"etag"`
CustomerEncryption struct {
EncryptionAlgorithm string `json:"encryptionAlgorithm"`
KeySha256 string `json:"keySha256"`
}
KMSKeyName string `json:"kmsKeyName"`
ResourceState string `json:"resourceState"`
}
func HelloStorage(ctx context.Context, e GCSEvent) error {
meta, err := metadata.FromContext(ctx)
if err != nil {
return fmt.Errorf("metadata.FromContext: %w", err)
}
log.Printf("Event ID: %v\n", meta.EventID)
log.Printf("Event type: %v\n", meta.EventType)
log.Printf("Bucket: %v\n", e.Bucket)
log.Printf("File: %v\n", e.Name)
log.Printf("Metageneration: %v\n", e.Metageneration)
log.Printf("Created: %v\n", e.TimeCreated)
log.Printf("Updated: %v\n", e.Updated)
return nil
}
Functions Framweork の記法に変更します。init 関数で functions-framework-go にハンドラを登録します。登録することで、CloudEvents 仕様に準拠したイベントを自動的にアンマーシャリングしてくれます。
func init() { functions.CloudEvent("HelloStorage", helloStorage) }
引数として受け取った event
オブジェクトのデータを、StorageObjectData
にアンマーシャルしています。これにより、受け取ったイベントを直感的に扱えるようにします。
StorageObjectData
を用意してくれているので、第1世代のように GCSイベントのペイロードを受け取るようの構造体を定義しなくて済みます。そのため、コードもスッキリ書けるようになりました。
var data storagedata.StorageObjectData if err := protojson.Unmarshal(e.Data(), &data); err != nil { return fmt.Errorf("protojson.Unmarshal: %w", err) }
Eventarc APIの有効化とロールの付与
今回は、Eventarc トリガーを利用するため、Eventarc API を有効にします。有効化の方法は公式ドキュメントに記載あるので参照していただければと思います。
以下の権限がないと関数をデプロイすることができないため、ロールを付与します。こちらはコンソールで作成すると、権限付与するボタンが出現するのでそちらを押下するだけでロールが付与されます(便利)
- Cloud Storage サービスエージェントに
roles/pusub.publisher
を付与する- Eventarc では、イベントを Pub/Sub push サブスクリプションを介して、特定の宛先(Cloud Functions など)に送信します。そのため、Cloud Storage サービスエージェントに Pub/Sub へのパブリッシュ権限が必要になります。
- Cloud Functions のサービスアカウントに
roles/eventarc.eventReceiver
を付与する- 上記の権限を付与することで、Eventarc からのイベントを受信することができるようになります。
※ Cloud Functions の第1世代では、サービスアカウントを明記しない場合、App Engine のデフォルトサービスアカウントが使用されていましたが、第2世代からは、デフォルトのコンピューティングサービスアカウントが使用されます。
デプロイコマンドのオプションを変更する
主な変更点は以下の通りです
—gen2
オプションの追加--trigger-bucket
の指定方法の変更
弊社では、CIにデプロイコマンドを組み込んでいるため、対象ブランチにプッシュすると自動でデプロイされるようになっています。第2世代への移行に伴い、gcloud CLI からのデプロイコマンドも変更点があります。最終的には、以下のコマンドでデプロイしました。
// 第2世代 deploy-hello-functions: gcloud functions deploy HelloStorage \ --region asia-northeast1 \ --runtime go122 \ --source=. \ --entry-point=HelloStorage \ --trigger-bucket=YOUR_TRIGGER_BUCKET_NAME \ --project dev \ --memory=1024 \ --gen2 // 第1世代のデプロイ deploy-hello-functions: gcloud functions deploy HelloStorage \ --region asia-northeast1 \ --runtime go119 \ --trigger-resource YOUR_TRIGGER_BUCKET_NAME \ --trigger-event google.storage.object.finalize \ --project dev \ --memory=1024
第1世代は、 --trigger-resource
と --trigger-event
で指定していましたが、第2世代からは、 --trigger-bucket
でバケット名を指定するだけになっています。これは、オブジェクトのファイナライズでトリガーする設定になります。そのため、ファイナライズ以外をトリガーとしたい場合は、以下のように --trigger-event-filters
で指定する必要があります。イベントタイプはこちらを参照ください。
gcloud functions deploy HelloStorage \ --gen2 \ --trigger-event-filters="type=EVENT_TYPE" \ --trigger-event-filters="bucket=YOUR_STORAGE_BUCKET"
関数をデプロイする
今回の関数については、日次バッチ処理の一部だったため、第1世代の関数を削除してから、同名で第2世代の関数をデプロイしました。
まとめ
コンソールで作成すると、かなり丁寧な誘導(何に何の権限が足りていないのか、必要な情報などが直感的にわかるなど)があるので、CLIで試す前に一度コンソールで試してから整理しても良かったかもしれないと思いました。
第2世代から Cloud Run ベースで動いたり、デフォルトのサービスアカウントが変更されたり、CloudEvent, Eventarc など新キャラが登場したりと情報は多めでしたが、イベント駆動の実装がよりしやすくなったと思うので、今後も取り入れていきたいと思います!