フューチャー技術ブログ

Go Cloud#6 GCPのローカルエミュレータを活用する

はじめに

こんにちは、TIG DXチームの真野です。Go Cloud記事の第6弾です。


本記事では、Go CloudでGCPのエミュレータに接続してみます。同様の企画であるAWS LocalStack版は第5弾でまとめていますので、詳細はそちらを確認ください

GCPのローカルエミュレータとは

GCPのリソースを管理するためのコマンドラインインターフェースとして gcloud があり、このgcloudコマンド経由で各種エミュレータを実行することができます。

gcloudはAWSでいうAWS CLIに相当しますが、AWS CLIにはもちろんそのような仕組みは備わっていません。代わりにLocalStackというツールを利用することが多いです。それとは異なりGCPは標準コマンドに組み込まれているということで、思想の違いのようなものを感じられ興味深いですね。

エミューレータ種類について

gcloud alpha emulators --help で確認すると、2019年11月時点でエミュレートできるサービスは4つです。

  1. bigtable
  2. pubsub
  3. datastore
  4. firestore

現時点ではGo CloudのBlobに相当するCloud Storageのエミュレータは存在しないようです 1

存在しない理由は注釈1のStackOverflowの回答にもありますが、各Cloud StorageのSDKに、ローカルストレージに書き込むオプションが存在するため、エミュレータレベルでは存在しないのでは?と推測します。純製ではないですが fsouza/fake-gcs-serverというツールもあるので、こちらを利用することも検討に値するかもしれません。

本記事では、gcloud emulator で実行できる、 pubsubとfirestoreに対してGo Cloudでアクセスしていきます。

Cloud Pub/Sub

Cloud Pub/Sub は送信者と受信者を切り離す多対多の非同期メッセージングを提供することによって、別々に開発されたアプリケーションの間で安全かつ高可用な通信を実現するGCPのプロダクトです。送信側がPublisher、受信側がSubscriberと呼ばれ、1:Nのメッセージのやり取りを行えます。
https://cloud.google.com/pubsub/docs/overview

ドキュメントに従いgcloud pubsubエミュレータをインストールします。

Install
gcloud components install pubsub-emulator

インストールが終わったら、以下のコマンドでエミュレータを起動します。

pubsubエミュレータの起動
gcloud beta emulators pubsub start --project=dummy

--project で指定するPJは実際に存在しないダミー値でOKです。ポートはデフォルト 8085 で起動します。 --port オプションで変更も可能です。

アプリケーションにエミュレータを認識させるためには、PUBSUB_EMULATOR_HOSTPUBSUB_PROJECT_ID という2つの環境変数を設定する必要があるので設定します。

環境変数の設定
export PUBSUB_EMULATOR_HOST=localhost:8085
export PUBSUB_PROJECT_ID=dummy

このまま、gcloudコマンドで起動したpub/subのエミュレータに対してトピックを作成します。…と言いたいところですが、 実はgcloudはエミュレータに非対応です。

The emulator does not support GCP Console or gcloud pubsub commands.
(エミュレータはGCPコンソールやgcloud pubsubコマンドに対応していません)
https://cloud.google.com/pubsub/docs/emulator#using_the_emulator

え、どうやってトピック作るの?って疑問に思いましたが、GCPのSDK経由であれば操作できるようです(そうでないと意味無いので当然ですが)。ドキュメントにPython製のツールがいくつかおいてあるので、そちらをgcloudコマンドの代わりに利用してトピックを作成します。

# ツールの取得
$ git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
$ cd python-docs-samples/pubsub/cloud-client

# インストール
$ pip install -r requirements.txt

# トピックの作成
$ python publisher.py dummy create future-example

Topic created: name: "projects/dummy/topics/future-example" といったメッセージが出力されれば成功です。

ではGo Cloudでさきほど作成した future-example というトピックに対してPublishしてみます。

サンプルPublish(実はNGケース)
package main

import (
"context"
"gocloud.dev/pubsub"
_ "gocloud.dev/pubsub/gcppubsub"
"log"
)

func main() {
ctx := context.Background()

topic, err := pubsub.OpenTopic(ctx, "gcppubsub://projects/myproject/topics/mytopic")
if err != nil {
log.Fatal(err)
}
defer topic.Shutdown(ctx)

err = topic.Send(ctx, &pubsub.Message{Body: []byte("Hello, World!")})
if err != nil {
log.Fatal(err)
}
}

実は上記のコードは2019/11時点のGo CDK 0.1.0では上手く動かないようです。理由は、PUBSUB_EMULATOR_HOST環境変数は読んでくれているようです が、gRPCのトランスポートがCredentialsがついたたままで、 Insecureオプションへの切り替えが無いからだと思います。PullRequstチャンスだと思いますが、現時点のコードベースだと、gcppubsubパッケージでガンバってInsecureオプションを付ける処理を入れる必要がある気がします。

ということで、例えば以下のようなコードでWithInsecureで切り替えが必要すれば一応動かすことができます。ちょっと長いですがGCPのCredentailsを環境変数ではなく 自前で設定する場合のコード に、エミュレータの切り替えを追加しただけです。

無理やり通すパターン
var endPoint = "pubsub.googleapis.com:443"

func main() {
ctx := context.Background()

conn, err := dial(ctx) // ★エミュレータ対応
if err != nil{
log.Fatalf("dial error: %v", err)
}
defer conn.Close()

pubClient, err := gcppubsub.PublisherClient(ctx, conn)
if err != nil {
log.Fatalf("publisher client error: %v", err)
}
defer pubClient.Close()

topic, err := gcppubsub.OpenTopicByPath(pubClient, "projects/dummy/topics/future-example", nil)
if err != nil {
log.Fatalf("open topic error: %v", err)
}
defer topic.Shutdown(ctx)

if err = topic.Send(ctx, &pubsub.Message{Body: []byte("Hello, World!")}); err != nil {
log.Fatalf("publish error: %v", err)
}
fmt.Println("done")
}

// ★エミュレータ対応して関数切り出し
func dial(ctx context.Context) (*grpc.ClientConn, error) {
emulatorEndPoint := os.Getenv("PUBSUB_EMULATOR_HOST")
if emulatorEndPoint != "" {
endPoint = emulatorEndPoint
return grpc.DialContext(ctx, endPoint,
grpc.WithInsecure(), // ★追加
grpc.WithUserAgent(fmt.Sprintf("%s/%s/%s", "pubsub", "go-cdk", "0.1.0")),
)
}

creds, err := gcp.DefaultCredentials(ctx)
if err != nil {
return nil, err
}

return grpc.DialContext(ctx, endPoint,
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: creds.TokenSource}),
grpc.WithUserAgent(fmt.Sprintf("%s/%s/%s", "pubsub", "go-cloud", "0.1.0")),
}

続いて、Subscribeです。先程のPython製ツールでSubscriptionのエンドポイントを作成します。ProjectID, トピック名を指定し、gocdk-example1 としたエンドポイントを指定しました。

python subscriber.py dummy create future-example gocdk-example1

この gocdk-example1 に対してGo Cloudでアクセスします。

func main() {
ctx := context.Background()

conn, err := dial(ctx) // ★先程のエミュレータ対応した関数
if err != nil{
log.Fatalf("dial error: %v", err)
}
defer conn.Close()

subClient, err := gcppubsub.SubscriberClient(ctx, conn)
if err != nil {
log.Fatalf("subscriber client error: %v", err)
}
defer subClient.Close()

sub, err := gcppubsub.OpenSubscriptionByPath(subClient, "projects/dummy/subscriptions/gocdk-example1", nil) // ★さきほど取得したエンドポイントを指定する
if err != nil {
log.Fatalf("subscription error: %v", err)
}
defer sub.Shutdown(ctx)
for {
msg, err := sub.Receive(ctx)
if err != nil {
log.Fatalf("Receiving message: %v", err)
}
fmt.Printf("Got message: %q\n", msg.Body)
msg.Ack()
}
}

Subscribeのテストをしたい場合は、先程のPythonライブラリにサンプルでPublishするツールも付いているのでそちらで検証すると良いと思います。

python publisher.py dummy publish future-example

上記は、1から10までpublishする簡単なツールですが、検証用途ととしてはお手軽なのでオススメです。実行すると、Subscription側に標準出力されることが確認できると思います。

Cloud Firestore

Cloud Firestore は、高速でサーバーレスなフルマネージドのクラウド ネイティブ NoSQL ドキュメント データベースです。
https://cloud.google.com/firestore/

エミュレータのインストールはこちら を参考にできますが、今回はgcloudコマンドを直接叩いて起動させます。

Install
gcloud components install cloud-firestore-emulator

インストールが終わったら、以下のコマンドでエミュレータを起動します。オプションはこちら を参考ください。

pubsubエミュレータの起動
gcloud beta emulators firestore start --project dummy --host-port=localhost:8080

--host-port オプションでポートを固定できます(指定しないと起動のたびに違うポートが用いられました)。localhost:8080 でアクセスすると Ok が返ってくれば起動成功です。

これをGo Cloudでアクセスします。その前にCloud Pub/Subの場合と同様に、エミュレータの環境変数を設定します。

export FIRESTORE_EMULATOR_HOST=localhost:8080
export FIRESTORE_PROJECT_ID=dummy

続いてコードはこちらです。FirestoreもPub/Subと同様に、エミュレータ対応する関数を用意してスイッチさせています。

type Record struct {
ID string `docstore:"id"`
Name string `docstore:"name"`
}

var endPoint = "firesotore.googleapis.com:443"

func main() {
ctx := context.Background()

client, err := dial(ctx) // ★エミュレータ対応のため関数切り出し
if err != nil {
log.Fatalf("dial error: %v", err)
}

resourceID := gcpfirestore.CollectionResourceID("dummy", "example-collection")
coll, err := gcpfirestore.OpenCollection(client, resourceID, "id", nil)
if err != nil {
log.Fatalf("open collection error: %v", err)
}
defer coll.Close()

// 書き込み
row := Record{
ID: "1",
Name: "hoge",
}
if err := coll.Create(ctx, &row); err != nil {
log.Fatalf("create error: %v", err)
}

// 読み込み
rowToRead := Record{
ID: "1",
}
if err := coll.Get(ctx, &rowToRead); err != nil {
log.Fatalf("get error: %v", err)
}
fmt.Printf("get: %+v\n", rowToRead)
}

// 0.1.0時点でエミュレータで動かすため自前で切り替え
func dial(ctx context.Context) (*vkit.Client, error) {
emulatorEndPoint := os.Getenv("FIRESTORE_EMULATOR_HOST")
if emulatorEndPoint != "" {
endPoint = emulatorEndPoint

conn, err := grpc.DialContext(ctx, endPoint, grpc.WithInsecure())
if err != nil {
return nil, err
}
return vkit.NewClient(ctx,
option.WithEndpoint(endPoint),
option.WithGRPCConn(conn),
option.WithUserAgent(fmt.Sprintf("%s/%s/%s", "firestore", "go-cloud", "0.1.0")),
)
}

creds, err := gcp.DefaultCredentials(ctx)
if err != nil {
return nil, err
}

client, _, err := gcpfirestore.Dial(ctx, creds.TokenSource)
return client, err
}

コードの中でCreate(登録)とGet(取得)ができました。後はエミュレータを意識すること無く操作することができます。

まとめ

GCPのローカルエミュレータのFirestore, PubSubに対して多少前処理が必要ですがGo Cloudから十分アクセスができます。同時にまだ実装が枯れきっていないので色々PullRequestチャンスだと思います。

エミュレータを使っていてよかったなと思うポイントは、gcloudこそ利用できませんでしたが、PubSubで登場したPythonツールのように、既存のエコシステムを活用出来る点で、コードの動作確認ではとてもお世話になると思います。

Go Cloud自体の filemem のような機能と合わせてエミュレータも上手く使い分けて、より生産性と品質を高められるようにしていきたいですね。