詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Google App EngineでGoを動かすときに知っておくべきこと(ソースコード・ビルド編)

Google App Engine(GAE)で Go 製のウェブアプリを動かしたかった話。いっぺん動かしてみると GAE/Go はウェブアプリを動かす環境としてはとてもいい。ただ、中途半端な知識だけで始めると開発者としてはつまずくことが多かったので、分かりにくい点をまとめておく。


コンテキストとして、スタンドアロンで動くよう書いていた Go ウェブアプリを GAE で動かしたいと思った、しかし GAE の知識はほとんどない、という背景を想像してください。その際に壁となる以下のような点だと思うので、それぞれ自分がどうしたかを見ていく。

  • GAE/Go のデプロイの仕組み
  • GAE/Go 環境の制限

この記事の内容は、SDK のインストールと、goapp による Hello, world のデプロイくらいまでは済ませてある人むけ。そうでない人は、まずはチュートリアルをおさえると良いです。

Quickstart for Go App Engine Standard Environment  |  App Engine standard environment for Go  |  Google Cloud Platform

Google App Engine Go Standard Environment について

GAE は自分が知っていたころの知識では単体のサービスで、Python で書かれたアプリケーションを動かすことのできる代物だったが、今は Google Cloud Platform に吸収されて、そのいちコンポーネントとなっているらしい。2016年の今では、GAE 上で Go が動くようになっている。

App Engine - Platform as a Service  |  Google Cloud Platform

Getting Started with App Engine - YouTube より

ベータ版として Flexible Environment というものもあるらしいけど、ここでは触れません。

この記事で扱う goapp のバージョンは "go version go1.6.2 (appengine-1.9.40) darwin/amd64" 。

goapp は $GOPATH 以下もアプリケーションのソースとしてアップロード/コンパイルする

何を当たり前のことを、と思われるかもしれないし自分も今書いていてそりゃそうだとしか思えないのだけど、最初は「GAE って Heroku みたいなもんでしょ」というくらいの大雑把な認識しか持っていなかったので、ワークツリーの外に存在するソースコードが暗黙に利用されるというのに驚きがあった。

つまり、GAE/Go では

  • 手元でコンパイルしたアプリケーションのバイナリをアップロードするのではないし、
  • 主要なソースコードだけをアップロードしてリモートで go get するわけでもなく、

アプリケーションのビルドに必要なソースコードを $GOPATH 含めて探索し、すべてアップロードした上でリモートでコンパイルする。

この挙動は、一度デプロイしたアプリケーションのソースコードを appcfg.py download_app してダウンロードしてみるとわかる。たとえば github.com/pkg/errors をインポートしている場合、以下のようにソースコードが配置される(もちろん、この github.com というディレクトリはデプロイ時にはアプリケーションのソースコードに存在しなかったもの)。

.
|-- _go_manifest.txt
|-- app.yaml
|-- github.com
|   `-- pkg
|       `-- errors
|           |-- errors.go
|           `-- stack.go
`-- init.go

goapp はプロジェクトルート以下のソースコードをすべてコンパイルしようとする

これも驚きのあるところ。どういう経緯かは分からないけど、まあそういうものらしい。ここでプロジェクトルートというのは app.yaml が置いてあるディレクトリのことを指す。GAE/Go におけるアプリケーションのコンパイルはけっこうややこしいことになっていて、それを調べて書くだけで長い文章になりそうなのでやらないけれど、さしあたりこのルート以下のコードがすべてコンパイルされることを覚えておけば充分そう。以下に、このことによって発生するエラーメッセージを挙げる。

go-app-builder: Failed parsing input: parser: bad import "syscall" in ...

GAE/Go では syscallunsafe パッケージのインポートが許されていない。たとえばプロジェクトルート以下に vendor ディレクトリを作っていて、その下に置いたサードパーティ製のライブラリがこれら利用不可のパッケージを利用していた場合、アプリケーションから利用していなくても開発サーバの起動やデプロイができなくなることになる。

go-app-builder: Failed parsing input: app file xxx.go conflicts with same file imported from GOPATH

また、プロジェクトルート以下のファイルが $GOPATH や vendor ディレクトリに含まれていて、通常の方法でインポートされている場合も、アプリケーションの一部として自動的にコンパイルされるソースコードと、$GOPATH からインポートされるパッケージの一部としてのソースコードが重複するとみなされてエラーになる。

対処方法

アプリケーションからインポートされるソースコードは、プロジェクトルート以下に配置しない。プロジェクトルートの外にある $GOPATH からインポートするようにする。

その他の方法として、app.yamlnobuild_files なる項目を設定する、というやり方があるらしい。この項目、公式のドキュメントが見当たらない(のに巷のブログでは紹介されていたりする)のでやや不安がある。のだけど x/tools/cmd/present で紹介されてるのでまあ大丈夫じゃないかな……。

nobuild_files: vendor/

これは正規表現を指定するのだけど自動的に ^ が付与され、先頭一致となる模様。

API のドキュメント らしきものには Files that match this regular expression will not be built into the app. This directive is valid for Go only. と書いてありました。

ちなみにビルドされたくないソースコードに // +build !appengine なるビルドタグを付与することでコンパイルを避けることもできるが、現実的ではないだろう。

goapp は Python 製ツールのラッパー、コア部分は go-app-builder

これはまあ知らなくてもハマらないけど、細かい挙動を追いたくなったら必要になってくる。

goapp は挙動を見る限りでは Go 公式の go コマンドのフォークで、goapp deploy および goapp serve という GAE 専用のサブコマンドが追加されている。

% goapp help
Go is a tool for managing Go source code.

Usage:

        goapp command [arguments]

The commands are:

        serve       starts a local development App Engine server
        deploy      deploys your application to App Engine
...

で、

  • goapp servedev_appserver.py
  • goapp deployappcfg.py update の、

単なるラッパーになっている(それぞれのサブコマンドのヘルプに書いてある)。goapp にはログレベルを変更するオプションがないので、必要ならこれらを直接呼び出すとよい。

さらに、これら Python 製のツールは、アプリケーションのランタイムが Go である場合には go-app-builder というツールを呼び出す。これが前に述べた挙動の源になっている。これはソースコードが $(goapp env GOROOT)/src/cmd/go-app-builder にあるので興味あれば見てもらうとして、コメントだけ引用しておく。

go-app-builder is a program that builds Go App Engine apps.

It takes a list of source file names, loads and parses them, deduces their package structure, creates a synthetic main package, and finally compiles and links all these pieces.

いろいろやってます。

アプリケーションを構成するパッケージの置き場所とインポート

以上のような事情もあってか、GAE/Go ではプロジェクト内のパッケージは、デプロイ時のみ ルートからの相対パスによってインポートできることになっている。例えば import foo/bar と書かれたコードが goapp によってビルドされると、パッケージ foo/bar のコードは $GOPATH だけでなくプロジェクトルート以下の foo/bar ディレクトリからも探索される。

.
|-- app.yaml
|-- foo
|   `-- bar
|       `-- bar.go
`-- init.go # import "foo/bar"

しかしサブパッケージのインポートをこの挙動に頼るのは、あまりに GAE にバインドされてしまう点でよい方法とは言えないだろう。GAE と強く結びついているコードからこの形のインポートをするのはアリとしても、それ以外の、いわゆるドメイン層などアプリケーションのコアとなる部分までインフラの影響を受けてしまうのは一般的に避けたいはず。

そういうわけで、アプリケーションが複数パッケージ構成になる場合は、それらを $GOPATH から普通にインポートするように寄せるのがよい(いま説明したこの挙動は忘れたほうがいい)。プロジェクトに固有の $GOPATH を設定し、その中にサブパッケージを配置するようにする。go-app-builder は(普通の go と違い) 、$GOPATH 内のシンボリックリンクを辿るようになっているので、この構成も作りやすいはず。

このへんの話は公式ドキュメントにも触れられているんだけど、ちょっと分かりにくいかな……。

どういうソースコード構成にすればいいのか

で、結局どうしたらいいの、という話。ここは事実じゃなく意見の話なので、話半分で読んでください。

これまでの話を踏まえると、

  • デプロイされるアプリケーションが $GOPATH の状態に依存するので、
    • プロジェクト用の $GOPATH を設定し、それが開発者の環境に依存しないようにする(何らかの形でプロジェクトに含める)
    • 普通の vendoring を行う
  • プロジェクトルート以下のファイルがすべてコンパイル対象となってしまうので、
    • app.yaml のあるディレクトリ以下に依存ライブラリのソースコードを置かない
    • 置くなら nobuild_files を指定する

というオプションから好きなものを選べばよいということになる。

もともとスタンドアロンで動くものを書いていたこともあって、新たに appengine というディレクトリを作って、

$GOPATH/src/github.com/motemen/gae-app
|-- appengine/
|   |-- app/
|   |   |-- app.yaml
|   |   |-- init.go
|   |   ...
|   `-- gopath/
|       |-- src/github.com/motemen/gae-app/lib # lib への symlink
|       `-- vendor/src # vendor への symlink
|-- main.go # GAE に依存しないバイナリのエントリポイント
|-- lib/ # アプリケーションのコアロジック
|-- vendor/
...

のようにし、Makefile 中で $GOPATH を appengine/gopath に設定していた。この中にアプリケーションのサブパッケージや、vendor へ向いたシンボリックリンクを作っている。そうすることでアプリケーションにとっての $GOPATH をコントロール可能にしている。vendor の管理だけ、グローバルな $GOPATH の下で行っている。

まだ試行錯誤の数が少ないのでベストとは到底言えないけれど、自分は今は上記のようにしている。みなさんはどうしてますか? と丸投げして終わります。

みんなのGo言語【現場で使える実践テクニック】

みんなのGo言語【現場で使える実践テクニック】

その他、読んでおいたほうがいいもの

エンジニア立ち居振舞い: 属人性を減らす

お題「エンジニア立ち居振舞い」

おもしろそうなお題なので乗ってみる。自分は今は技術組織のとりまとめをしているけど、会社の古めのプロダクトの面倒を見る仕事もしてきた。時を経てサービスに携わる人が変遷し、コードの歴史も重層的で一筋縄ではいかないことが多い。仕事で触れるプロジェクトが多いので、ひとつのプロジェクトに関する知識を深めづらい面もある。

属人性を減らす

さまざまなタスクを通じてプロダクトに触れるうちにだんだんと自分の中に知識がついてきて、用件を聞いたときに「あ、それならあのプロジェクトのあのへんのコードだな」、というアタリがつけられるようになってくる。この地図や勘といったものは正直なところ外部化しづらく、ある程度を超えると個々人の中で養っていくしかないものだけれど、日々の仕事において、属人性を減らすように努力することはできる。

普段からやっていることは以下のようなところ。

  • 作業ログを残す。他の人が「とりあえず検索」したときに、周辺的な情報を掴めるようにする。Slackに書いて満足しない。
  • 単調作業はコード化する。簡単な手順でもスクリプト化してインターフェイスを狭めておくことで迷いを少なくする。
  • ドキュメントを書く。初見の人のためには、ふるまいや解決したいことを軸に記していくと入りやすい。

当たり前のことではあるけれど、先人がこれをやってくれているおかげで助けられることも多いです。

型と名前によるGoのコード探索 ― gofind

思いつきでツールを作ってはリスのように忘れ、再発見しては新鮮な気持ちで便利に使う日々です。

一般にプログラミングにおいては、ソースコードを読むことに意外とばかにならない時間を使うもの。特に Go ではデフォルトで標準ライブラリのソースコードが手元にあり、コードを書く際よい教科書になるので、これを読むことも多いはず。

Go は静的に型付けされる言語なのでその点コードは読みやすいけれど、データ構造が不変ではないので、ある構造体のフィールドがどこで書き換わるのかを知るには、処理を追っていくしかない。名前で grep するのもひとつの手ではあるけど、精度はあまり期待できない。

そこで gofind。簡単に言うと、型やパッケージを含めた名前でもって Go のソースコードを検索するツールです。

go get github.com/motemen/gofind/cmd/gofind

使い方は以下の通り。

gofind <pkg>.<name>.[<field>] <pkg>...

コードを読んで型付けまで行うので、実行には少し時間がかかる。

第一引数が、コード中の出現を見つけたい型や関数のパッケージ名で修飾された名前。型が構造体である場合には、フィールド名をオプショナルにつけることができる。指定できるものは以下のようなところ。

  • 型名
  • 関数名
  • (構造体型の)フィールド名
  • メソッド名

以降の引数は検索対象のパッケージで、普通にインポートパスを指定してもいいし(net/http とか)、ディレクトリでもいい(./lib/web とか)。

すると、指定された型や名前を持つ変数とかメソッドの出現をハイライトしてくれる。

利用例

実例を見るのが早いので、以下でいろいろ検索してみます。

構造体型のフィールド

このフィールド、どこで更新・参照されるんだろう? というときに。gofind のもともとの動機です。

% gofind crypto/tls.Config.ServerName net/http
...
net/http/transport.go:1015:             if cfg.ServerName == "" {
net/http/transport.go:1016:                     cfg.ServerName = cm.tlsHost()
net/http/transport.go:1039:                     if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
net/http/transport.go:2081:             ServerName:                  cfg.ServerName,
net/http/transport.go:2114:             ServerName:                  cfg.ServerName,

面白いのは、構造体リテラルのフィールド名も発見できること。画像で見るとわかりますが、ServerName: もハイライトされている。

f:id:motemen:20161028084521p:plain

構造体の無名初期化も検出できるのが便利。これは grep では発見しづらいところ。

% gofind go/ast.Package.Imports go/ast
go/ast/resolve.go:173:  return &Package{pkgName, pkgScope, imports, files}, p.errors.Err()

f:id:motemen:20161028084629p:plain

関数

go list std は標準パッケージの一覧。こうやって公式にどういう使い方がされているか発見できるので、生きたサンプルとして役立つ。

% gofind strings.Split $(go list std)
archive/tar/reader.go:781:      sparseMap := strings.Split(extHdrs[paxGNUSparseMap], ",")
crypto/tls/common.go:597:       labels := strings.Split(name, ".")
...

f:id:motemen:20161028084838p:plain

変数や構造体のフィールドで、指定された型を持つものを検索。

% gofind golang.org/x/tools/cmd/stringer.Package golang.org/x/tools/cmd/stringer
golang.org/x/tools/cmd/stringer/stringer.go:133:        g.Printf("package %s", g.pkg.name)
golang.org/x/tools/cmd/stringer/stringer.go:170:        pkg *Package     // Package we are scanning.
...

f:id:motemen:20161028084759p:plain

gofind sync.Mutex $(go list std) などとすれば、公式のソースコードでこの型がどういう名前付けをされているかわかるわけです(Go 言語における並行処理の構築部材 - 詩と創作・思索のひろば)。

メソッド

% gofind encoding/json.Encoder.Encode golang.org/x/tools/cmd/godoc
golang.org/x/tools/cmd/godoc/handlers.go:146:   json.NewEncoder(w).Encode(resp)

f:id:motemen:20161028084727p:plain

はてなで一緒に働きませんか?