Cloud Functions（2nd Gen）を使い始める上で知っておきたいことメモ（Go）

Cloud Functions で非同期ワーカーをサクッと作ろうと着手し始めたら、第二世代になって色々と変わっていて「あ、ここに書いてあったの？」というものが多かった…。

そこで当記事では、「チュートリアル以上のことをしようとした時に、ドキュメントを読み始める前に知っておくとつまづきが減るかもしれない前提知識」 を整理していきたいと思います。

:::note info
* 当記事では Go 言語を前提として説明していきます
* 2024年8月時点の情報ですので、最新情報はドキュメントをご確認ください
:::

## Cloud Functions 2nd Gen. のアーキテクチャ
ドキュメントを読んでいくと、Go 言語での実装は以下のように案内されている。

**・HTTP関数のサンプル**
```go
package myhttpfunction

import (
    "fmt"
    "net/http"

    "github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

func init() {
    // Register an HTTP function with the Functions Framework
    functions.HTTP("MyHTTPFunction", myHTTPFunction)
}

// Function myHTTPFunction is an HTTP handler
func myHTTPFunction(w http.ResponseWriter, r *http.Request) {
    // Your code here

    // Send an HTTP response
    fmt.Fprintln(w, "OK")
}
```
[HTTP 関数を作成する  |  Cloud Functions Documentation  |  Google Cloud](https://cloud.google.com/functions/docs/writing/write-http-functions?hl=ja#implementation) より
<br />

**・イベントドリブン関数のサンプル**
``` go
package mycloudeventfunction

import (
    "context"

    "github.com/GoogleCloudPlatform/functions-framework-go/functions"
    "github.com/cloudevents/sdk-go/v2/event"
)

func init() {
    // Register a CloudEvent function with the Functions Framework
    functions.CloudEvent("MyCloudEventFunction", myCloudEventFunction)
}

// Function myCloudEventFunction accepts and handles a CloudEvent object
func myCloudEventFunction(ctx context.Context, e event.Event) error {
    // Your code here
    // Access the CloudEvent data payload via e.Data() or e.DataAs(...)

    // Returning an error causes its message to be logged.
    // Example:
    err := myInternalFunction() // may return an error
    if err != nil {
        // Append error message to log
        return err
    }

    // Return nil if no error occurred
    return nil
}
```
[イベント ドリブン関数を作成する  |  Cloud Functions Documentation  |  Google Cloud](https://cloud.google.com/functions/docs/writing/write-event-driven-functions?hl=ja#cloudevent-functions) より

<br />

初見だと「あれ、これだけで良いの？」「どう実行されるの？」と思うかもしれない。

実際、この関数がどのように呼び出されるのか。それについて理解するには、[ローカルでの開発  |  Cloud Functions Documentation  |  Google Cloud](https://cloud.google.com/functions/docs/running/overview?hl=ja#abstraction_layers) ページにある以下のイメージ図が役に立つ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115077/d52aef7d-8804-0e2c-4b25-98756e98a9c9.png)

<br />

前述のコードは、この図の "Your Function" のところに該当しており、それを 「Functions Framework」 という存在がHTTPサービスに変換してくれている。

> [Functions Framework](https://cloud.google.com/functions/docs/functions-framework?hl=ja) は、受信した HTTP リクエストを言語固有の関数呼び出しにアンマーシャルするために Cloud Functions 内で使用される一連のオープンソース ライブラリです。これにより、関数をローカルで実行可能な HTTP サービスに変換します。
> 
> （[ローカルでの開発  |  Cloud Functions Documentation  |  Google Cloud](https://cloud.google.com/functions/docs/running/overview?hl=ja#abstraction_layers) より）

<br />

また、この Functions Framework などはオープンソースとなっており、それを利用して同様の関数を別環境にもホスティングすることができる。

> Cloud Functions 自体がマルチレイヤ アーキテクチャを使用しています。また、その大半はオープンソースです。これらのオープンソース コンポーネントを使用すると、Cloud Functions 用に設計されたコードを他のプラットフォームで実行できます。
> 
> （[ローカルでの開発  |  Cloud Functions Documentation  |  Google Cloud](https://cloud.google.com/functions/docs/running/overview?hl=ja#abstraction_layers) より）

<br />

これは、ローカルで動作を検証する際にも言えるため
* Functions Framework を使ったエントリーポイントとなる実装追加し go run する
* 同上 をさらにラップした Functions エミュレータを使って起動

のどちらかを行い、 [curl でコールすることで](https://cloud.google.com/functions/docs/running/calling?hl=ja) HTTP関数、イベントドリブン関数 のどちらも動作を確認することができるようになる。

## イベントのハンドリングは？ = Eventarc がやってくれてる
とここまで読んで、イベントドリブン関数を使おうとした人は 「イベントをキューから Pull してくるんじゃないの？ Push方式なの？」と疑問に思う方もいるはず。

実は Cloud Functions 第二世代からは、イベント周りのハンドリングは全て Eventarc がやってくれている。
![image 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115077/62852bb6-dcad-bf4c-1b3a-2949a43d8012.png)

> Cloud Functions（第 2 世代）のすべてのイベント ドリブン関数は、Eventarc トリガーを使用してイベントを配信します。Eventarc トリガーを使用すると、Eventarc でサポートされている任意のイベントタイプで関数をトリガーできます。
>  （[Eventarc の概要  |  Google Cloud](https://cloud.google.com/eventarc/docs/overview?hl=ja#functions) より）

> Eventarc は、プロバイダに関係なく、バイナリ コンテンツ モードで HTTP リクエストを使用して、ターゲットの宛先に [CloudEvents](https://cloudevents.io/) 形式でイベントを配信します
> （[Eventarc の概要  |  Google Cloud](https://cloud.google.com/eventarc/docs/overview?hl=ja#event-format) より）


これらの情報が頭に入っていると、ドキュメントも読み進めやすくなるように思う。


## デプロイ方法 （仕組みを知っておこう）
さて、動作の仕組みがざっくりと分かったところで、次はデプロイについて知っておきたいことを整理していく。

ドキュメントとしては、Cloud Functions 関数のデプロイ方法は、以下のページにまとまっている。
[Cloud Functions の関数をデプロイする  |  Cloud Functions Documentation  |  Google Cloud](https://cloud.google.com/functions/docs/deploy?hl=ja)

<br />

大きく方法としては "gcloud CLI" と ”コンソール" があり、その際に利用するソースコードの参照元は "ローカルから" と "Cloud Storageから" がある。

ここでは CI/CD に組み込むことも考えて、 gcloud CLI について触れていこう。

<br />

**・HTTP関数のデプロイコマンド（ローカルから）**
``` sh
gcloud functions deploy my-http-function \
  --gen2 \
  --region=us-central1 \
  --runtime=nodejs20 \
  --source=. \
  --entry-point=myHttpFunction \
  --trigger-http
```
（[Cloud Functions の関数をデプロイする  |  Cloud Functions Documentation  |  Google Cloud](https://cloud.google.com/functions/docs/deploy?hl=ja#example-http-from-local) より）

<br />

**・イベントドリブン関数のデプロイコマンド（Cloud Storage から）**
```sh
gcloud functions deploy my-pubsub-function \
  --gen2 \
  --region=europe-west1 \
  --runtime=python312 \
  --source=gs://my-bucket/my_function_source.zip \
  --entry-point=pubsub_handler \
  --trigger-topic=my-topic
```
（[Cloud Functions の関数をデプロイする  |  Cloud Functions Documentation  |  Google Cloud](https://cloud.google.com/functions/docs/deploy?hl=ja#example-storage-from-local) より）

<br />

この時点でおさえておきたいことを、まずはいくつか列挙しておく。

* `--source` に指定するのは init を含む関数 func が置かれたディレクトリであること
  * ローカル検証などを目的として、function framework を用いたエントリーポイントとなる main 関数を用意している場合には、間違えがちなので注意が必要
  * このディレクトリには、go.mod・go.sum か vendor ディレクトリが必要となる（後述する）
* `--entry-point` に指定するのは、`functions.CloudEvent` の第一引数で指定している name である
* イベントドリブン関数の場合、このコマンドによって「サブスクライバ」「Eventarc」「Cloud Functions関数」が自動作成される
  * オプションから、すでに存在する Eventarc の設定を利用することもできるようだ（なお、delete コマンドを実行すると、この時に自動作成されたものは削除してくれる）
* オプションで 環境変数 を設定することができる
  * その際、直接 KEY=VALUE で指定する方法と、yaml ファイルを指定してまとめて設定することができる
* オプションで Secret Manager も利用することは可能
  * ただ、環境変数とは異なり、特定のシークレットをボリュームマウントする方法か、KEY=SecretManagerIDを指定する方法があり、ファイルでまとめて設定することはできなそう。（筆者は shell スクリプトでまとめて設定できるようにした）

<br />

さてここで、このコマンドを実行すると Cloud Build が実行される。ぜひ、何をしているのかを確認しておこう。

Cloud Build のステップを見ると以下のようになっている。

![image 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115077/a90ec484-cd8f-c3ad-836b-87dba4d28481.png)

* ステップ `0: fetch` 
  * asia-northeast1-docker.pkg.dev/serverless-runtimes/utilities/gcs-fetcher というイメージを pull して、Cloud Storage からソースコード群を取得している。なお、`--source` でローカルパスを指定した場合も同様であるため、指定されたディレクトリ配下のファイル・ディレクトリをローカルで zip にまとめて Cloud Storage へとアップロードしているようだ。（ここを覚えておきたい）
なお、初回実行時に `.gcloudignore` ファイルが作成され、ここで指定したファイルはアップロードされないようになっている。
* ステップ `1: pre-buildpack`
  * 筆者が実行した際には、asia-northeast1-docker.pkg.dev/serverless-runtimes/google-22-full/builder/go というイメージを pull し、必要なディレクトリの準備や環境変数設定が行われる。
* ステップ `2: build`
  * ステップ2 で pull したイメージによって、`go list -m` や `go mod tidy` が実行され、build が実行される


このステップが頭に入れておかないと、デプロイのトラブルが発生した際に、新旧入り混じったインターネット上の情報に振り回されてしまうことになる。（筆者体験済み）

特に、依存関係の解決で頭を悩ませることが多い。

## Go の依存関係指定について
公式ドキュメントでも、以下のように [依存関係 についてのページ](https://cloud.google.com/functions/docs/writing/specifying-dependencies-go?hl=ja) が設けられており、 Go モジュール（go.mod + go.sum） か `vendor` ディレクトリを使用できることが示されている。

前述まで読んできてもらえたのであれば、すんなり頭に入る内容だと思われる。


* Private なパッケージがなければ、素直に Go モジュールを利用するのが良いと思われる
* そうではない場合は、`go mod vendor` で vendor ディレクトリにパッケージを持ってきた上でアップロードする必要がある。（そうでないと、前述の ステップ `2: build`での `go mod tidy` で取得できずエラーになる）

<br />

また、Go のバージョンが 1.16 より前の Go バージョン の場合に `vendor` ディレクトリを利用する際には、`.gcloudignore` に go.mod と go.sum を追記する旨が記載されているが、これはあくまで 1.15 以前のバージョン向けの記載である。

エラーを Perplexity に相談したり、インターネットの海に答えを求めて漂っていると、バージョンの前提条件を抜かしてこの対応を提示してくる場合がある。（筆者体験済み） Cloud Functions のコンソールからは、ソースファイルZipをダウンロードできるので、「あるはずのものが無い」系のエラーが起きた時には、Zip　と `.gcloudignore` を見比べて、適宜確認もしていこう。


## まとめ
使い始めのフェーズで、色々と分かりづらい・つまづきがちだなと思った情報をまとめてみました。

サービスでの本番利用を考えると、より色々とまとめたい情報もあるので、適宜記事にしていきたいと思います。