Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Cloud Storage トリガーによって呼び出している Go の Cloud Functions 関数を第2世代へ移行する

はじめに

こんにちは、サーバーサイドエンジニアの加味(@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 など新キャラが登場したりと情報は多めでしたが、イベント駆動の実装がよりしやすくなったと思うので、今後も取り入れていきたいと思います!

参考