Google App Engine(GAE)で Go 製のウェブアプリを動かしたかった話。いっぺん動かしてみると GAE/Go はウェブアプリを動かす環境としてはとてもいい。ただ、中途半端な知識だけで始めると開発者としてはつまずくことが多かったので、分かりにくい点をまとめておく。
- Google App Engine Go Standard Environment について
- goapp は $GOPATH 以下もアプリケーションのソースとしてアップロード/コンパイルする
- goapp はプロジェクトルート以下のソースコードをすべてコンパイルしようとする
- goapp は Python 製ツールのラッパー、コア部分は go-app-builder
- アプリケーションを構成するパッケージの置き場所とインポート
- どういうソースコード構成にすればいいのか
- その他、読んでおいたほうがいいもの
コンテキストとして、スタンドアロンで動くよう書いていた Go ウェブアプリを GAE で動かしたいと思った、しかし GAE の知識はほとんどない、という背景を想像してください。その際に壁となる以下のような点だと思うので、それぞれ自分がどうしたかを見ていく。
- GAE/Go のデプロイの仕組み
- GAE/Go 環境の制限
この記事の内容は、SDK のインストールと、goapp
による Hello, world のデプロイくらいまでは済ませてある人むけ。そうでない人は、まずはチュートリアルをおさえると良いです。
Google App Engine Go Standard Environment について
GAE は自分が知っていたころの知識では単体のサービスで、Python で書かれたアプリケーションを動かすことのできる代物だったが、今は Google Cloud Platform に吸収されて、そのいちコンポーネントとなっているらしい。2016年の今では、GAE 上で Go が動くようになっている。
App Engine - Platform as a Service | Google Cloud Platform
ベータ版として 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 では syscall
や unsafe
パッケージのインポートが許されていない。たとえばプロジェクトルート以下に vendor ディレクトリを作っていて、その下に置いたサードパーティ製のライブラリがこれら利用不可のパッケージを利用していた場合、アプリケーションから利用していなくても開発サーバの起動やデプロイができなくなることになる。
go-app-builder: Failed parsing input: app file xxx.go conflicts with same file imported from GOPATH
また、プロジェクトルート以下のファイルが $GOPATH や vendor ディレクトリに含まれていて、通常の方法でインポートされている場合も、アプリケーションの一部として自動的にコンパイルされるソースコードと、$GOPATH からインポートされるパッケージの一部としてのソースコードが重複するとみなされてエラーになる。
対処方法
アプリケーションからインポートされるソースコードは、プロジェクトルート以下に配置しない。プロジェクトルートの外にある $GOPATH からインポートするようにする。
その他の方法として、app.yaml
に nobuild_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 serve
はdev_appserver.py
のgoapp deploy
はappcfg.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 の下で行っている。
まだ試行錯誤の数が少ないのでベストとは到底言えないけれど、自分は今は上記のようにしている。みなさんはどうしてますか? と丸投げして終わります。
- 作者: 松木雅幸,mattn,藤原俊一郎,中島大一,牧大輔,鈴木健太,稲葉貴洋
- 出版社/メーカー: 技術評論社
- 発売日: 2016/09/09
- メディア: 大型本
- この商品を含むブログ (1件) を見る