概要
以前もこの環境構築系の投稿をしたんですが、いずれも古いので今回また最新のGAE第二世代向けの構築をしてみることにしました。
なお、こちらの記事のソースコードはGitHubに公開しています。
https://github.com/brbranch/GaeGo112EchoSample/tree/gae_echo
2019/09/07
続編書きました。
mercari/datastoreのMemcache連携をGAEの代わりにRedis Cloudでしてみる【GAE/Go1.12】
https://qiita.com/br_branch/items/1b63b2c1dd9b4ff3931e
以前の記事
Golang1.8+echo+GoLandでGoogle App Engineのローカルサーバーを実行できる環境を作る
https://qiita.com/kazuked/items/a0e815fda868f302f4d1
Go言語初心者がGAE+Echo(v2)+goonでサーバーを構築する
https://qiita.com/kazuked/items/da656fd12a3177e60bb9
今回のゴール
- GAE/Go1.12 + echoサーバーを構築し、Datastoreにアクセスする。
- ローカルでの開発環境を構築する
- ローカルでDatastore含めたテストを行う
動作環境
- Mac OS 10.14
- go1.12.9
- Google Cloud SDK 253.0.0
- echo v3
- mercari/datastore v1.6.1
GAE第二世代からはAppEngine APIは利用できなくなる
以前はgoonを使ってましたが、今回からはmercari/datastorのboomに乗り換えてみます。
https://github.com/mercari/datastore
というのも、 GAE/go1.12(GAE第二世代) からはAppEngine APIは利用できません。
https://cloud.google.com/appengine/docs/standard/go112/go-differences?authuser=0&hl=ja#migrating-appengine-sdk
App Engine は、appengine パッケージを含めるように Go ツールチェーンを変更しません。appengine パッケージまたは google.golang.org/appengine パッケージを使用している場合は、Google Cloud クライアント ライブラリに移行する必要があります。
(省略)
App Engine で Memcache サービスを使用するには、App Engine Memcache の代わりに Redis Labs Memcached Cloud を使用します
しれっとひどいことやってきますね。
goonはAppengine APIを前提としてるっぽいので、どうも使えないようなんです。
ただ悪いことばかりでもないかもしれません。
go1.12からはApp Engineの第二世代を使うようになるらしく、何がよくなるのかよくわかってませんが、第二世代というくらいですから何かよくなってるのかもしれないです。
詳細:App Engine スタンダード環境のランタイム
https://cloud.google.com/appengine/docs/standard/runtimes
何が良くなりそうなのかはよくわからないけど、何か良くなってそうな気がするのは良かったですね。
よかったよかった。
(きっと自由度は上がってると思う。ちなみに、AppEngine Memcacheも使えなくなるっぽい)
まずは最小構成で動かしてみる
まずは、echoを導入してHello Worldをローカルで動かしてみるところまで作成します。
最初のフォルダ構成
以下の感じにしました。
backend (GOPATH)
├-- src (作業フォルダ)
├-- .envrc
└-- .go-version
Step1. まずは最低限で動くことを確認
まずは最低限動いてデプロイできることを確認するため、srcフォルダ内にGAEのクイックスタート(helloWorld)を入れます。
名前だけmain.go / main_test.go とかにしちゃいます。
backend (GOPATH)
├-- src (作業フォルダ)
│ ├ app.yaml
│ ├ main.go
│ └ main_test.go
├-- .envrc
└-- .go-version
あとは、普通にデプロイをしてみます。
$ cd ./backend/src
$ $ gcloud app deploy
Services to deploy:
descriptor: [/Users/xxxxx/git/my/xxxxxx/backend/src/app.yaml]
source: [/Users/xxxxx/git/my/xxxxxx/backend/src]
target project: [xxxxxx]
target service: [default]
target version: [20190906t203700]
target url: [https://xxxxxx.appspot.com]
Do you want to continue (Y/n)? Y
Beginning deployment of service [default]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 3 files to Google Cloud Storage ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://xxxxx.appspot.com]
You can stream logs from the command line by running:
$ gcloud app logs tail -s default
To view your application in the web browser run:
$ gcloud app browse
楽なもんですね!
Step2. ローカルでも動かしてみる
go1.12あたりからは、ローカルでは単純に go run
して動かせということです。
もうGAEローカルサーバーは使わないということですね。
$ go run main.go
2019/09/06 21:18:30 Defaulting to port 8080
2019/09/06 21:18:30 Listening on port 8080
なるほどねー。
Step3. Go Moduleを入れる
次は実際にGo Moduleを入れてデプロイをしてみます。
$ go mod init gaego112echosample
go: creating new go.mod: module gaego112echosample
$ ls
app.yaml go.mod main.go main_test.go
とりあえず、使わないけどechoを入れます。
ライブラリはどこにも入ってないんで赤字になります。
これでビルドしてみます。
$ go build
go: finding github.com/labstack/echo v3.3.10+incompatible
go: downloading github.com/labstack/echo v3.3.10+incompatible
go: extracting github.com/labstack/echo v3.3.10+incompatible
go: finding github.com/labstack/gommon/log latest
go: finding github.com/labstack/gommon/color latest
︙
すると、こんな感じになります。
※ 環境変数で GO111MODULE=on
を設定している場合は、 $GOPATH
で設定した場所ではなく、 go env GOPATH
で出力される場所にパッケージがダウンロードされます。
この状態で、同じようにデプロイをしてみました。以前はdepやglide使ってる時にはvendoringの問題があったりしてエラーが発生したりしてたのですが、これでも何の問題もなくデプロイして動かすこともできました。
Step4. Echoサーバーとして作成する。
じゃあ、次からはEchoを組み込んでいきます。
まずは最低限でいいですね。
package main
import (
"fmt"
"github.com/labstack/echo"
"log"
"net/http"
"os"
)
func main() {
e := echo.New()
http.Handle("/", e)
e.GET("/", func (e echo.Context) error {
return e.String(http.StatusOK, "Hello Echo!")
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
log.Printf("Listening on port %s", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
ローカルでもデプロイしても普通に動きます。
楽ですね。
Step5. logを出す
上記の通り、もう google.golang.org/appengine
は使えなくなりました。
代わりに、標準のlogパッケージを使えばいいみたいです。
それはつまり、どこからでも簡単に呼べるようになったということですね。
やってみましょう。
e.GET("/", func (e echo.Context) error {
log.Printf("Logger test") /// 追加
return e.String(http.StatusOK, "Hello Echo!")
})
デプロイしてアクセスすると、普通にLoggingに出てきます。
うん。こいつは楽ですね。
Step6. DataStoreを扱う
さて、いよいよDataStoreを扱ってみます。あとついでに処理をパッケージで分けてもみます。
最初の方にも書いたとおり、 第二世代のAppEngineではAppEngine APIを使えない という逆説的なことになってますので、今までお世話になってた google.golang.org/appengine/datastore
はもう使えません。
代わりに、 cloud.google.com/go/datastore
を利用し、 メルカリが作ってる boom
を使わせてもらいます。
package handler
import (
"cloud.google.com/go/datastore"
"github.com/labstack/echo"
"go.mercari.io/datastore/boom"
"go.mercari.io/datastore/clouddatastore"
"log"
"net/http"
"os"
)
type Post struct {
Kind string `datastore:"-" boom:"kind,post" json:"-"`
ID int64 `datastore:"-" boom:"id" json:"id"`
Content string `datastore:"content" json:"content"`
}
func HelloWorld(e echo.Context) error {
// app.yaml などにPROJECT_IDを設定しておく
projectId := os.Getenv("PROJECT_ID")
log.Printf("Project ID: %s", projectId)
// DataStore Clientの作成
ctx := e.Request().Context()
dataClient, err := datastore.NewClient(ctx, projectId)
if err != nil {
log.Fatalf("failed to get client (reason: %v)", err)
return e.String(http.StatusInternalServerError, "error")
}
// mercari.datastoreでラップする
client, err := clouddatastore.FromClient(ctx, dataClient)
if err != nil {
log.Fatalf("failed to get datastoreclient (reason: %v)", err)
return e.String(http.StatusInternalServerError, "error")
}
defer client.Close()
// boomを利用
b := boom.FromClient(ctx, client)
post := &Post{ID: 12345, Content:"test"}
// 保存
if _, err := b.Put(post); err != nil {
log.Fatalf("failed to put datastore (reason: %v)", err)
return e.String(http.StatusInternalServerError, "error")
}
// 取得
getPost := &Post{ID: 12345}
if err := b.Get(getPost); err != nil {
log.Fatalf("failed to get datastore (reason: %v)", err)
return e.String(http.StatusInternalServerError, "error")
}
return e.JSON(http.StatusOK, getPost)
}
// 抜粋
e.GET("/", handler.HelloWorld)
これでローカル環境でも、GAEの環境でもGCP/DataStoreに保存 / 取得することができました。
どうも、 gloudでログインしているユーザーにアクセス権限があれば、ローカル環境からも自動でつなぐみたいですね。
Step7. ローカルのDataStore Emulator にアクセスする
これでも開発はできるんですが、せっかくならローカルのエミュレーターにつなぎたいですよね。
というわけで、それの設定もしてみます。
ローカルのエミュレーターを起動
以下のコマンドでできます。
--project
の部分は [a-z][a-z0-9\-]+
の命名規則であれば任意でも問題ないようです。指定しなければ gcloud
で設定している current project id が入ります。
$ gcloud beta emulators datastore start --host-port localhost:8059 --project test-project
WARNING: Reusing existing data in [/Users/xxxx/.config/gcloud/emulators/datastore].
Executing: /Users/xxxx/tools/google-cloud-sdk/platform/cloud-datastore-emulator/cloud_datastore_emulator start --host=localhost --port=8059 --store_on_disk=True --consistency=0.9 --allow_remote_shutdown /Users/xxxx/.config/gcloud/emulators/datastore
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.CloudDatastore$FakeDatastoreAction$9 apply
[datastore] 情報: Provided --allow_remote_shutdown to start command which is no longer necessary.
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub <init>
[datastore] 情報: Local Datastore initialized:
[datastore] Type: High Replication
[datastore] Storage: /Users/xxxx/.config/gcloud/emulators/datastore/WEB-INF/appengine-generated/local_db.bin
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub load
[datastore] 情報: The backing store, /Users/xxxx/.config/gcloud/emulators/datastore/WEB-INF/appengine-generated/local_db.bin, does not exist. It will be created.
[datastore] API endpoint: http://localhost:8059
[datastore] If you are using a library that supports the DATASTORE_EMULATOR_HOST environment variable, run:
[datastore]
[datastore] export DATASTORE_EMULATOR_HOST=localhost:8059
[datastore]
[datastore] Dev App Server is now running.
[datastore]
[datastore] The previous line was printed for backwards compatibility only.
[datastore] If your tests rely on it to confirm emulator startup,
[datastore] please migrate to the emulator health check endpoint (/). Thank you!
実際に動かしてみる
上記のエミュレーターを起動した状態で、ローカルで連携をしてみます。
以下のコマンドを付与して run
します。
$ env DATASTORE_EMULATOR_HOST=localhost:8059 DATASTORE_PROJECT_ID=test-project go run main.go
そして localhost:8080
にアクセスすると正常に表示され、エミュレーターを起動した側のターミナル上にログが出力されます。
[datastore] The health check endpoint for this emulator instance is http://localhost:8059/9 07, 2019 2:06:34 午後 io.gapi.emulators.grpc.GrpcServer$3 operationComplete
[datastore] 情報: Adding handler(s) to newly registered Channel.
[datastore] 9 07, 2019 2:06:34 午後 io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
[datastore] 情報: Detected HTTP/2 connection.
[datastore]
[datastore]
[datastore] 9 07, 2019 2:06:48 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub lambda$persist$7
[datastore] 情報: Time to persist datastore: 27 ms
これでローカル環境での開発も捗りそうです(`・ω・´)
ローカルのエミュレーターの中を確認(Google Cloud GUI)
ただ、エミュレーターの場合今までのように Appengine Local Serverの管理画面からDataStoreの中を見るということができません。ちょっと気になったりすることもあるんで、それも見れるようにします。
すごい人が作ってたので、それを利用します。
Google Cloud GUI
https://github.com/GabiAxel/google-cloud-gui
$ npm i -g google-cloud-gui
$ google-cloud-gui
上記を実行すると、ブラウザが自動で立ち上がります。
あとは「Projects」の左にある+ボタンを押してプロジェクトを追加すると、確認することができました。
素敵。
(ターミナル見ると何やらたくさんエラーはでてるけど、使えてるのでOKですね)
Step8. テストケースを書く
いよいよゴール間近です。
今回はとりあえず、DataStore Emulatorを使って、実際にDataStoreに接続する形でのテストを記述してみます。
func TestHelloWorld(t *testing.T) {
// 環境変数をセット
os.Setenv("DATASTORE_PROJECT_ID", "test-project")
os.Setenv("DATASTORE_EMULATOR_HOST", "localhost:8059")
e := echo.New()
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
c := e.NewContext(req, rr)
err = HelloWorld(c)
if err != nil {
t.Errorf("unexpeced handler reponse (err: %v)", err)
return
}
if status := rr.Code; status != http.StatusOK {
t.Errorf(
"unexpected status: got (%v) want (%v)",
status,
http.StatusOK,
)
}
expected := `{"id":12345,"content":"test"}`
if strings.TrimSpace(rr.Body.String()) != expected {
t.Errorf(
"unexpected body: got (%v) want (%v)",
rr.Body.String(),
expected,
)
}
}
DataStore Emulatorを起動した状態で実行すると、無事テストはパスしました。
=== RUN TestHelloWorld
2019/09/07 14:51:33 Project ID:
--- PASS: TestHelloWorld (0.02s)
PASS
やってみた感想
とりあえず、構築自体はそんなに引っかかることなくできました。
Go Moduleでvendoringから開放されるのは嬉しいです。
ただ、最初にも書いた GAE/go1.12 からはAppEngine APIは利用できないって結構えげつないですね。
既存プロジェクトの変更の影響かなり大きそう・・・。
まだできてないこと
上記の感じなので、今はDataStoreとmemcacheの連携はできてないはず。
(mercari/datastoreがすごく優秀でローカルキャッシュもやってくれてるみたいな記事をどこかで読んだんだけど、その動作の確認はできてないです)
「今後は Appengine Memcache ではなく Redis Cloudを使え」とGCPもおっしゃってるんで、次回はその連携もしてみようと思います。
Redis Cloud は30MBまでは無料らしいです。
30MBかぁ…(´・_・`)
個人でやるとしても、実際に運用する際には多少の課金は必要になってきそう。
トラブルシューティング
構築中に発生したエラーをまとめておきます。
goenvでgo1.12をインストールできない
goのバージョン管理で goenv
を使っているのですが、ぼくの環境では install --list
でも表示されず、最初うまく入れることができませんでした。
$ goenv install 1.12.9
go-build: definition not found: 1.12.9
See all available versions with `goenv install --list'.
If the version you need is missing, try upgrading goenv:
brew update && brew upgrade goenv
解決方法
以下のようにしてアップデートをしました。
$ brew update # brew自体をアップデートする
$ $ brew install --HEAD goenv
Error: Xcode alone is not sufficient on Mojave.
Install the Command Line Tools:
xcode-select --install
# 怒られたんで、言われるままにやります(というか入ってなかったのか…)
$ xcode-select --install
xcode-select: note: install requested for command line developer tools
$ brew install --HEAD goenv
Error: goenv 1.23.3 is already installed
To install HEAD, first run `brew unlink goenv`.
# また怒られた。言われた通りにunlinkする
$ $ brew unlink goenv
Unlinking /usr/local/Cellar/goenv/1.23.3... 4 symlinks removed
$ brew install --HEAD goenv
# 成功!
Mac OS 10.14 だとgo1.12でテストできない
なぜか、go test をやろうとするとこんなエラーがでちゃいます。
$ go test
go: creating new go.mod: module github.com/brbranch/JankenShogiOnline
# runtime/cgo
In file included from gcc_libinit.c:8:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:232:66: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:249:43: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:256:66: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:524:1: error: unknown type name 'size_t'
gcc_libinit.c:97:18: error: variable has incomplete type 'struct timespec'
gcc_libinit.c:97:9: note: forward declaration of 'struct timespec'
gcc_libinit.c:110:3: error: implicit declaration of function 'nanosleep' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
FAIL github.com/xxx/xxxx/backend/src [build failed]
解決方法
https://github.com/golang/go/issues/30072 に載ってました。
よくわかってないけど、 以下の方法で実行したらテストできました。
$ env CGO_ENABLED=0 go test
PASS
ok github.com/xxxx/xxxx/backend/src 0.013s
なんかなっとくいかないけど、動いてるから正義ってことで(´・ω・`)
参考
App Engine スタンダード環境のランタイム
https://cloud.google.com/appengine/docs/standard/runtimes
Migrating your App Engine app to Go 1.12
https://cloud.google.com/appengine/docs/standard/go112/go-differences
Google Cloud GUI
https://github.com/GabiAxel/google-cloud-gui
mercari/datastore の知見を語っておく
https://qiita.com/vvakame/items/f24444267a0ce786dffc
GAE+GO+Echoでgo1.11対応した
https://qiita.com/nyappa/items/dccac12e6332d311e37b