Help us understand the problem. What is going on with this article?

GAE第二世代+Go v1.12でechoサーバーを構築する(2019年度版)

概要

以前もこの環境構築系の投稿をしたんですが、いずれも古いので今回また最新の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)を入れます。

https://github.com/GoogleCloudPlatform/golang-samples/tree/master/appengine/go11x/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

あとは実際に動くか確認
image.png

楽なもんですね!

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

image.png

なるほどねー。

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を入れます。

image.png

ライブラリはどこにも入ってないんで赤字になります。
これでビルドしてみます。

$ 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
︙

すると、こんな感じになります。

image.png

※ 環境変数で GO111MODULE=on を設定している場合は、 $GOPATH で設定した場所ではなく、 go env GOPATH で出力される場所にパッケージがダウンロードされます。

この状態で、同じようにデプロイをしてみました。以前はdepやglide使ってる時にはvendoringの問題があったりしてエラーが発生したりしてたのですが、これでも何の問題もなくデプロイして動かすこともできました。

image.png

Step4. Echoサーバーとして作成する。

じゃあ、次からはEchoを組み込んでいきます。
まずは最低限でいいですね。

main.go
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))
}

ローカルでもデプロイしても普通に動きます。

image.png

image.png

楽ですね。

Step5. logを出す

上記の通り、もう google.golang.org/appengine は使えなくなりました。
代わりに、標準のlogパッケージを使えばいいみたいです。

それはつまり、どこからでも簡単に呼べるようになったということですね。
やってみましょう。

main.go(一部抜粋)
    e.GET("/", func (e echo.Context) error {
        log.Printf("Logger test") /// 追加
        return e.String(http.StatusOK, "Hello Echo!")
    })

デプロイしてアクセスすると、普通にLoggingに出てきます。

image.png

うん。こいつは楽ですね。

Step6. DataStoreを扱う

さて、いよいよDataStoreを扱ってみます。あとついでに処理をパッケージで分けてもみます。
最初の方にも書いたとおり、 第二世代のAppEngineではAppEngine APIを使えない という逆説的なことになってますので、今までお世話になってた google.golang.org/appengine/datastore はもう使えません。
代わりに、 cloud.google.com/go/datastore を利用し、 メルカリが作ってる boom を使わせてもらいます。

src/handler/hello_handler.go
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)
}
main.go
// 抜粋
e.GET("/", handler.HelloWorld)

これでローカル環境でも、GAEの環境でもGCP/DataStoreに保存 / 取得することができました。

image.png

どうも、 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

上記を実行すると、ブラウザが自動で立ち上がります。

image.png

あとは「Projects」の左にある+ボタンを押してプロジェクトを追加すると、確認することができました。

image.png

素敵。
(ターミナル見ると何やらたくさんエラーはでてるけど、使えてるのでOKですね)

Step8. テストケースを書く

いよいよゴール間近です。
今回はとりあえず、DataStore Emulatorを使って、実際にDataStoreに接続する形でのテストを記述してみます。

handler/hello_handler_test.go
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 に載ってました。

image.png

よくわかってないけど、 以下の方法で実行したらテストできました。

$ 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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away