Cloud Functions/GoでGCPの課金情報をSlackに通知する

Cloud FunctionsでGo 1.11が[βリリース](https://cloud.google.com/blog/products/application-development/cloud-functions-go-1-11-is-now-a-supported-language
)されたので試したときのメモです。
GCPの課金情報をCloud StorageにJSON形式でexportするようにし、
Cloud Functions/GoでSlackに通知するコードを書いて試してみました。

## 関数の実装方法
関数の実装方法は以下の2種類に分けられるようです。
新しく覚えることはほとんどなくどちらも非常に簡単です。

### HTTP functions
HTTPリクエストをトリガーにして実行される関数です。
実装は簡単で一般的なWebアプリケーションと同様にハンドラを定義すればよいです。

```golang
// function.go
package function

import "net/http"

func F(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        w.Write([]byte(r.Header.Get("X-Forwarded-For")))
}
```

### background functions
今回はこちらを使いました。
イベントをトリガーにして実行される関数です。
利用可能なイベントは、公式ドキュメントに記載があるので見てみてください。

- [Cloud Storage](https://cloud.google.com/functions/docs/calling/storage)
- [Cloud Pub/Sub](https://cloud.google.com/functions/docs/calling/pubsub)
- [Cloud Firestore](https://cloud.google.com/functions/docs/calling/cloud-firestore)
- [Google Analytics for Firebase](https://cloud.google.com/functions/docs/calling/google-analytics-firebase)
- [Firebase Realtime Database](https://cloud.google.com/functions/docs/calling/realtime-database)
- [Firebase Authentication](https://cloud.google.com/functions/docs/calling/firebase-auth)

第二引数で各イベントに対応した構造体を受け取るように実装する必要があります。
現時点では構造体は自前で定義する必要がありますが、上記ドキュメントに記載されているものを参考にすれば良さそうです。

```golang
// function.go

// Package function includes an example of processing a GCS event.
package function

import (
        "context"
        "log"
)

// GCSEvent holds event data from a Google Cloud Storage Event.
type GCSEvent struct {
        Bucket      string `json:"bucket"`
        Name        string `json:"name"`
}

func F(ctx context.Context, e GCSEvent) error {
        log.Printf("Processing file: %s", e.Name)
        return nil
}
```

## 事前準備

- GCP
  - IAMでCloud FunctionsのAPIの有効化
  - Cloud SDKを最新にする  
    `gcloud components update &&
gcloud components install beta`
  - Cloud Storageに適当なバケットを作成
  - 課金情報をJSON形式で作成したバケットに出力するように設定
- その他
  - SlackのWebhook URLをメモしておく

## 実装してみよう
作成したコードは[こちら](https://github.com/monmaru/billing-notify-function/blob/master/go/function.go)です。

### 概要
あまり難しいコードではないので要点だけ説明します。
GCPの[サンプルコード](https://github.com/GoogleCloudPlatform/golang-samples/tree/master/functions)も参考になるので併せて参照してください。

まず、Cloud Storageからのイベント情報を構造体で定義します。
こちらは[GCPのサンプルコード](https://github.com/GoogleCloudPlatform/golang-samples/blob/master/functions/helloworld/storage_generic/hello_storage_generic.go)を参考にしました。

```golang
type GCSEvent struct {
	Bucket         string    `json:"bucket"`
	Name           string    `json:"name"`
	Metageneration string    `json:"metageneration"`
	ResourceState  string    `json:"resourceState"`
	TimeCreated    time.Time `json:"timeCreated"`
	Updated        time.Time `json:"updated"`
}
```

あとは、第一引数に`context.Context`と第二引数に上記で定義した構造体を受け取る関数を定義するだけです。
簡単ですね！！

```golang
func F(ctx context.Context, e GCSEvent) error {
	// contextからメタ情報を取得することができる
	meta, err := metadata.FromContext(ctx)
	if err != nil {
		return fmt.Errorf("metadata.FromContext: %v", err)
	}
	log.Printf("Event ID: %v\n", meta.EventID)
	log.Printf("Event type: %v\n", meta.EventType)
	// ...省略

	// Slackにwebhookで通知
	return webhook(ctx, webhookURL, buildMessage(e.Name, b))
}
```

第二引数で受け取ったイベント情報（バケット名とファイル名）を使ってCloud Storageからファイルの内容を読み取ります。
今回は、JSON形式で課金情報が記載されているので、必要な情報を抜き出してSlackに通知しています。

```golang
func readFromGCS(ctx context.Context, bucket, name string) (io.ReadCloser, error) {
	return storageClient.
		Bucket(bucket).
		Object(name).
		NewReader(ctx)
}
```

### 依存ライブラリ
https://cloud.google.com/functions/docs/concepts/go-runtime?hl=en#specifying_dependencies

依存ライブラリの管理には、`go modules`か`vendoring`が利用可能です。
両方同時に利用することができないと記載があるので注意してください。

### One-time initialization
[初期化の方法](https://cloud.google.com/functions/docs/concepts/go-runtime?hl=en#one-time_initialization)が紹介されています。
DBのコネクションなど一度初期化しておけばよいものはまとめて実装しておきましょう。
`func init()`と`sync.Once.Do()`の2パターンが紹介されていますので、それぞれ用途に合わせて利用しましょう。
私は、Cloud Storageのクライアント初期化や正規表現のコンパイルを`func init()`で行いました。


```golang
func init() {
	var err error
	storageClient, err = storage.NewClient(context.Background())
	if err != nil {
		log.Fatalf("storage.NewClient: %v", err)
	}

	webhookURL = os.Getenv("WEBHOOK")
	regexB = regexp.MustCompile(`billing-(.*).json`)
}
```

## デプロイしてみよう
[ドキュメント](https://cloud.google.com/functions/docs/deploying/filesystem)を参考にしてgcloud cliでデプロイします。

```
gcloud functions deploy FUNCTION_NAME --runtime go111 --entry-point F --trigger-resource \
TRIGGER_BUCKET_NAME --trigger-event google.storage.object.finalize \
--region asia-northeast1 --project=PROJECT_ID --set-env-vars WEBHOOK=WEBHOOK_URL
```

`FUNCTION_NAME`は、実装とは関係なくデプロイした関数の名前になるので好きなものを指定してください。
`--entry-point`に呼び出して欲しい関数名を指定します。
`--trigger-resource`にバケット名を指定します。
`--trigger-event`には、イベントのタイプを指定します。
今回は、バケットにファイルが生成されたタイミングで関数を実行して欲しいので`google.storage.object.finalize`を指定しています。
`google.storage.object.finalize`は、以下のように説明されています。
> This event is sent when a new object is created (or an existing object is overwritten, and a new generation of that object is created) in the bucket.

### 環境変数の設定
設定には環境変数を利用したいですよね。
[Using Environment Variables](https://cloud.google.com/functions/docs/env-var)に記載があります。
先のコマンド例では、`--set-env-vars WEBHOOK=WEBHOOK_URL`のようにデプロイ時にwebhook先のURLを環境変数として設定しています。

扱い方の注意事項などいろいろ記載があるので読んでみてください。
例えば、`Variable lifecycle`には、環境変数はデプロイ時のみ設定が可能とあります。
デプロイ後にUI等で変更することはできないようです。
> Environment variables are bound to a deployment of a Cloud Function, and can only be set or changed with a deployment. If a deployment fails for any reason, any changes to environment variables will not be applied. Environment variable changes require a successful deployment.

## まとめ
Cloud Functions/Goを試してみました。
今後SDKが拡充されてもっと実装しやすくなることを期待します。

普段はApp Engineでサクッと済ませることが多いのですが、Cloud StorageやFirestoreなどで発生したイベントをもとに処理したいときには便利そうです。
また、Cloud Pub/SubやCloud Schedulerと組み合わせることでいろいろとおもしろい使い方ができそうです。
みなさんも試してみてください！！