詩と創作・思索のひろば

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

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言語【現場で使える実践テクニック】

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

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