Google AppEngine 2Genのgo111 standardランタイム ベータ公開に伴って、それまでDatastoreとCloudSQL(MySQL5.7)で開発していたアプリケーションをSpannerでリプレースすることがあったので、その際にやったこと等をつらつらと書いていきます。
前提
- サーバーアプリケーションは原則としてAppEngineで動作する前提
- CIにはCloudBuildを使用する
- 開発者それぞれが個別に開発用のGCP Projectを利用している(AppEngineはProjectに1つなので)
ローカルエミュレーターが無い
Cloud Spannerには現状ローカルエミュレーターが無いですが、ちょいとあれこれしたら割となんとかなりました。
Datastore + MySQL版
Datastore + MySQLの場合、Datastoreはgcloud sdkにエミュレーターが存在するのでそれを使用、MySQLはDocker経由で各自のマシンで起動という具合でローカル環境を構築していました。
多分そんなに変わった構成じゃないかなと。
Spanner版
素直にSpannerに接続するようにしています。
開発者レベルでの環境分離は、SpannerのデータベースをGCP Project名と同じにする という素朴なルールで特に困ることなく回っています。
みんなで同じプロジェクトの同じインスタンスを使いまわして、データベースだけ分けるという具合です。
GAE上のGoアプリケーションからSpannerに接続する場合、例えば以下のようなコードになるのですが
package main
import (
"fmt"
"cloud.google.com/go/spanner"
"google.golang.org/api/option"
)
func main() {
ctx := context.Background()
// 接続先
// project instance databaseにはそれぞれの環境に合わせたものにする必要がある
// プロジェクト名とインスタンス名は固定でなんとかなるが、データベース名が開発者ごとに違う変数になる
dsn := fmt.Sprintf("projects/%s/instances/%s/databases/%s", "project", "instance", "database")
tokenSource, _ := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/spanner.admin")
client, _ := spanner.NewClient(ctx, dsn, option.WithTokenSource(tokenSource))
}
この際、DSNを組み立てるためのデータベース名が開発者固有の変数として残ってしまいます。
環境変数でos.Getenvするというのが素朴で良かったんですが、app.yamlの数は減らしたい(開発者ごとにenv_variablesを作らないといけないのは嫌だ)という気持ちになり、AppEngineにデプロイするアプリ書いてるんだから、みんなgcloud initはしてるっしょ!という発想から、
- ローカルではgcloud sdkのコンフィグファイルをパースして、アクティブなプロジェクトをデータベース名として採用する
- AppEngineで動作している場合は、メタデータサーバーに問い合わせて自分がデプロイされているプロジェクトをデータベース名として採用する
というライブラリをエイヤッと書いて対応しています。
自分が今ローカル動作なのか、GAE動作なのかは、GAEはこちらから何も指定していなくても、以下のような環境変数を持っているので、このあたりを見て判断しています
GAE_MEMORY_MB=128
GAE_INSTANCE={インスタンスのID GAEが勝手に決めてると思う}
PORT=8080
HOME=/root
GAE_SERVICE={デプロイしたサービス名 app.yamlのserviceに書いたもの}
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GAE_DEPLOYMENT_ID={デプロイID GAEが勝手に決めてると思う}
DEBIAN_FRONTEND=noninteractive
GOOGLE_CLOUD_PROJECT={自分がデプロイされているプロジェクト}
GAE_ENV=standard
GAE_APPLICATION={b~の後ろにGOOGLE_CLOUD_PROJECTの中身がくっついたようなやつ}
PWD=/srv
GAE_RUNTIME=go111
GAE_VERSION=20181031t131554
これ書いてて思いましたけど、メタデータサーバーまで行かなくてもGOOGLE_CLOUD_PROJECTの値見れば良い感ありますね。そっと修正しておこう。
もうちょっといいやり方あれば誰か教えてほしいです。
ユニットテストに組み込みたい
Datastore + MySQL版
Datastore Emulatorはなんかよしなに繋がるので特に意識せずに使ってました。
MySQLもDockerでホスト側ポートが全員同じポートで固定されていたので、そこに素直にアクセスしていました
Spanner版
大変詳しい記事があったのでこちらを参考にしていただけたら良いと思います。
私もだいたい同じ感じで、テストケース毎にSpannerのDBを使い捨てています。
func TestABC(t *testing.T) {
databaseInit()
defer databaseDrop()
// テストケースつらつら書いていく
}
要するにこんな感じです。
ユニットテストの場合はDB名は各テストが走るたびにUUIDベースで生成して、30文字で切り捨てるようなことをやっています。(Spannerのデータベースは最大で30文字なので)
デバッガから落としたりするとdeferが実行されずにDROPが走りませんが、開発用のSpannerインスタンスは毎晩自動で落とすようにしているので、その際データベースも消えるしまぁいっかな というお気楽運用にしています。
データベースのセットアップが終わらない
Datastore + MySQL版
MySQLのマイグレーションにgo-migrateというライブラリを使用していました。
Spanner版
go-migrateはspannerにも対応しているのですが、これがパフォーマンス的にあまり良くなかったです。
大変詳しい記事があったのでこちらを参考にしていただけるとよく分かるのですが、SpannerはCREATE DATABASE作成と同時にDDLを渡してやらないと、CREATE TABLEするだけでもかなりの時間がかかります。
go-migrateはDDLを一気に渡すような挙動ではなかったため、このあたりは開発用に簡単なコマンドを作成して対処しています。
といっても、上に書いたような判断で接続先のデータベースを勝手に見分けて、所定の位置からDDLとDMLをかき集めて実行しているだけなのですが。
ユニットテストの場合は、databaseInit的な関数の中身がCREATE DATABASEと同時にDDLを投げつけるように実装してある感じです。