Go+App Engine+Cloud SQLで始めるGo言語Webアプリケーション開発
こんにちは!先週末、GalaというGoogle主催のGo言語ハッカソンへ参加し、Webアプリケーションのベース開発とGoogle Cloud Platformの対応をしていました。
Go言語+Google Cloud Platform
さて、今回はそのハッカソンで用いたGoogle Cloud PlatformとGo言語によるWebアプリケーションの開発を紹介したいと思います。前半でGo+App Engine, Cloud SQLの解説をし、後半では実際にGCP環境を利用した開発について書かせていただきます。
既にGAEに詳しいが、Go言語でのWebアプリケーション開発にピンと来ない方は後半から記事を読むことをオススメします。
Google App Engine
Google App Engine はGAEと略されるPaaSで、サポートしている言語は Java, Python, PHP そして Go です。GAEの機能としては
- データの永続化とトランザクション機構
- オートスケールとロードバランシング
- 非同期にキューの処理を実行可能
- タスクスケジューリング
- 他のGoogleのサービスやAPIとの連携が可能
ただし、データストアにRDBMSを使用するには次に紹介するCloud SQLを使用しなくてはなりません。
Google Cloud SQL
Cloud SQLはDBにMySQLを採用したフルマネージドサービスです。GAEでは最初からデータの永続化に Google Cloud Datastore のフレームを提供していますが、Cloud DatastoreはKVSのため、RDBMSを使用するにはCloud SQLかGoogle Compute Engine上に構築するかが必要となります。
今回のハッカソンではなるべくインフラの構築に時間をかけたくなかったので、Cloud SQLを使用することにしました。
GCPとWebアプリケーションの設計
GCP (GAE) をWebアプリケーションで最大に活用するための設計はGoogle Cloud Platformのページでも紹介されています。
App Engineの機能にある「データの永続化」はCloud Datastoreで可能ですし、キャッシュ処理もMemcacheによって可能です。
これらの構築を全てGCP側で連携するだけで実現が可能となっています。
Go言語Webアプリケーション開発
GCPでアカウントを取得し、Go言語のApp Engineをインストールしてください。
$ goapp version
go version go1.4.2 (appengine-1.9.30) darwin/amd64
プロジェクト作成
今回はtagcloud
という名前でサンプルWebアプリケーションを作成することにします。プロジェクトの新規作成ですが、GAE/Goで開発するときは通常の $GOROOT
, $GOPATH
を使用せずに開発をします。
ここを気にせずに開発をしていると、通常のGo環境とGo App Engineの環境が混同して変なエラーに悩まされたことがあるので、各自が自分に合った構成を見つけておくとベストです。
私の例ですが、新規プロジェクトを作成するときは新しくディレクトリを作成し、そのディレクトリを直接$GOPATH
に通して開発を進めています。そして、プロジェクトのルートに app.yaml
と初期ロードファイルを置き、アプリケーションコードは app
以下に置いています。(もしくは別プロジェクトとしてgo get
しています。)
tagcloud/ # プロジェクト ($GOPATH)
|_ .envrc # 環境変数管理
|_ app/ # アプリケーションコード
| |_ app.yaml # GAE Manifest
| |_ init.go # 初期ロードファイル
|_ src/ # $GOPATH/src
|_ github.com/
|_ code.google.com/
|_ golang.org/
$GOPATH
などの環境変数を変更する必要があるためdirenvというツールを使用しています。direnvはディレクトリに.envrc
が存在するとディレクトリ移動時に読み込みを実行します。
# GoAppEngineの存在確認
goapp=`which goapp 2>&1`
if [[ ! "${?}" = "0" ]]; then
echo "Need to install `goapp` to execute this script."
exit 127
fi
GOAPPROOT="`dirname $goapp`/goroot"export GOROOT="${GOAPPROOT}" # GoAppEngineのGOROOTを設定する
export GOPATH="`pwd`" # プロジェクトルートをGOPATHに設定する
export GOBIN="${GOPATH}/bin"
# export PATH=${GOBIN}:${PATH} # 必要ならばパスを通しておく`
このスクリプトを.envrcに記述しておくことによって自分のGAE/Go環境の設定をシームレスに変更しています。Hello world!GAE/Goで「Hello world!」を出力させるサンプルはHello, World! in 5 minutesのページに書いてあります。
本来、ここの説明は公式ページや別のサイトを見るだけで済むのですが少しだけ解説をします。「Hello world!」の作成についてはGo標準のhttpパッケージを使用するだけで完成させることができるので初心者の方でも問題なく作成できます。init関数でハンドリングpackage tagcloudimport (
"fmt"
"net/http"
)func init() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
})
}httpパッケージの超基本的な記述だけでOKです。ここで必ず注意していただきたいのですがmain関数は使用せず、initで必要な処理をハンドルしておきます。main関数はGAE側が提供しています。間違えて使用してしまうと「404 page not found」が出力されます。このくらいの処置を書くなら下記のテンプレートファイルをVimから使用すれば簡単に記述することができますね。
app.yamlapp.yamlの設定に関しては「Configuring with app.yaml」にドキュメントがあります。初回ならば以下のようなフォーマットでapp.yamlを用意しておくことになります。version: 1
runtime: go
api_version: go1handlers:
- url: /.*
script: _go_app上記をそのままコピペして保存してください。一度でもGAEを触ったことがある方は application: [project ID] の存在を知っていると思いますが、ここに書かなくてもよい情報なので今回は割愛しています。アプリケーションの起動プロジェクトのルートにてapp.yamlが存在している場所を指定して起動します。今回だと goapp serve ./app とすればlocalhostでアプリケーションが立ち上がります。$ goapp serve ./app
INFO 2016-01-25 12:59:14,211 api_server.py:205] Starting API server at: http://localhost:57920
INFO 2016-01-25 12:59:14,215 dispatcher.py:197] Starting module "default" running at: http://localhost:8080
INFO 2016-01-25 12:59:14,217 admin_server.py:116] Starting admin server at: http://localhost:8000"default"で起動しているlocalhost:8080を叩けば Hello world! の文字が表示されます。$ curl -X GET "http://localhost:8080"
Hello, world!このserveコマンドはファイルの変更を検知して、ライブリロードを行ってくれるので非常に開発がやりやすいです。ファイルを変更してみると下記の様に表示されてビルド→起動を行います。INFO 2016-01-29 07:33:33,656 module.py:401] [default] Detected file changes:
/path/to/tagcloud/tagcloud.goアプリケーションのデプロイまだ「Hello world!」しか表示されませんがデプロイをします。GCPにて新規にプロジェクトを作成し、そのプロジェクトのproject IDを指定してコマンドを叩くだけでデプロイが完了します。project IDは真ん中くらいに記載されています。(少しモザイクを入れている部分です)$goapp deploy -application tagcloud-xxxxx ./appデプロイ完了後、http://[project ID].appspot.com へアクセスすると「Hello world!」が表示されていると思います。2. +Webアプリケーションフレームワーク先ほどの実装ではGo標準のhttpパッケージでしたが、Webアプリケーション開発をするなら何かのWebアプリケーションフレームワークを使用したいと思うはずです。
Go界隈で大体名前が上がってくるフレームワークと言ったら下記ですかね。(個人の偏見があります。)
各種フレームワークのベンチマークがginのリポジトリにあるので、そちらを参照してみるのも面白いです。
ref: BENCHMARKS.mdフレームワークとGAEGoのWebアプリケーションを使用した開発をした場合、どのようにGAEにハンドリングさせればいいのか微妙にわかりづらいかもしれません。
フレームワークのコアとなる機能は大抵 Handlerインタフェースを満たしているので、これによって起動させることが可能です。type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}・フレームワークのハンドリング独自のビルドシステムを持っているRevelとkochaについてはハンドリングさせるのが面倒(生成しなければいけない)なので今回は省略します。package gowafimport (
"fmt"
"net/http""github.com/gin-gonic/gin"
"github.com/go-martini/martini"
"github.com/zenazn/goji"
"github.com/zenazn/goji/web"
)func initNetHTTP() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
})
}func initGoji() {
goji.Get("/ping", func(c web.C, w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
})
http.Handle("/", goji.DefaultMux)
}func initGin() {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "Hello, world!")
})
http.Handle("/", router)
}func initMartini() {
m := martini.Classic()
m.Get("/ping", func() string {
return "Hello world!"
})
http.Handle("/", m)
}func init() {
// どれか一つだけコメントを解除
// initNetHTTP()
// initGoji()
initGin()
// initMartini()
}それぞれ、http.Handle(string, http.Handler)の第2引数に渡している値が http.Handlerインタフェースを満たしています。所感:Go+Webアプリケーション・フレームワーク個人的に、一番大好きなGoのWebアプリケーション・フレームワークはGinです。ただ、大好きと言っているくせに最大限の活用はせず、GinをラッピングしたフレームワークとしてGinを最大に活用しています。
Goの依存ポリシー上で全てをフレームワークに任せてしまうと細かくケアしたい場合にやり難さが生じるのである程度の関数オーバーヘッドを許容してラッパーを作成しています。
まだ重量級のRevelや他のフレームワークの開発も進行していますが、先週公開した「net/httpより10倍速いvalyala/fasthttpが面白そうなので調査してみた件」などの対応なども柔軟に進まないのでGo標準のnet/httpだけでWebアプリケーションを組むのもありだと思っています。(net/httpで実装していくと、結局俺々フレームワークが出来上がるんですけどね……)そもそも、GAEの場合はGAEが持っている appengine.Context がフレームワークの機能を提供するコンテキストなので、GAEの場合は本当にnet/httpで良いと思っています。そうせずに何かのフレームワークを使用すると Context が2つ存在したりするので、段々苦しくなってくると思います。3. Cloud SQLCloud SQLをGAE上で実行できるドライバーは2種類あり、 go-sql-driver/mysqlかziutek/mymysqlのどちらかとなります。GAE上ではlocalhostへの接続になるので、指定するユーザは % (any) ではなく localhost に接続できるようにしておきましょう。go-sql-driver/mysqlimport "database/sql"
import _ "github.com/go-sql-driver/mysql"func init() {
db, err := sql.Open("mysql", "user@cloudsql(project-id:instance-name)/dbname")
}ziutek/mymysql/godrvimport "database/sql"
import _ "github.com/ziutek/mymysql/godrv"func init() {
db, err := sql.Open("mymysql", "cloudsql:instance-name*dbname/user/password")
}接続後はsqlパッケージの通りに使用できますし、様々なORMが上記のドライバをサポートしていれば問題なく使用することが可能です。
非常に簡単なのですが、Cloud SQL+Goで接続するためのリファレンスを探し出すのに非常に苦労しました。色々なサイトを探したのですが何故か見つけられず…ref: The cloudsql packageO/RマッパーORMも色々転がっていますが、大体使うものが固定されてきた感じがします。
正直、インターフェースに関してはどれも似通っているので、速度の面や開発のし易さ(マイグレーション機能が組み込みであるか否か)とサポートがしっかり継続しているかで選定すれば良いと思います。(余談ですが、「ORM」ではなく「O/Rマッパー」が正式なんですよね。)おまけエッジキャッシュGoogle Cloud Platformの魅力として、強力なエッジキャッシュを利用できる点があります。詳しくは「GCP エッジキャッシュ」を参照するとよいでしょう。Go言語で、レスポンスをキャッシュにのせる方法は下記の様にすればOKです。tttttttjjj8uu
正直、インターフェースに関してはどれも似通っているので、速度の面や開発のし易さ(マイグレーション機能が組み込みであるか否か)とサポートがしっかり継続しているかで選定すれば良いと思います。
(余談ですが、「ORM」ではなく「O/Rマッパー」が正式なんですよね。)
おまけ
エッジキャッシュ
Google Cloud Platformの魅力として、強力なエッジキャッシュを利用できる点があります。詳しくは「GCP エッジキャッシュ」を参照するとよいでしょう。
Go言語で、レスポンスをキャッシュにのせる方法は下記の様にすればOKです。
import (
"fmt"
"net/http"
)func init() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Header().Set("Pragma", "Public")
fmt.Fprint(w, "Hello, world!")
})
}
これだけでGoogleのコンテンツと同じように自分のコンテンツもキャッシュされるようになります。
Cloud Datastore
前の方で少しだけ話題に出しましたが、GAE (appengine.Context) が標準で提供しているデータストアの Cloud Datastoreです。データオブジェクトを格納し、こちらもGAEと同様自動でスケールしてくれます。
RDBMSを使用する用途がない場合はDatastoreを利用するほうが手っ取り早いと思います。
Cloud Storage
AWSで言えばS3と同じものです。静的ファイルを置いておくこともできますし、作成したディレクトリをそのままHTMLとして配信することも可能です。(ドメインをあてることも可能)
まとめ
Go言語のWebアプリケーション・フレームワークやGCP (GAE, Cloud SQL, …) のことについて書きましたが、まだまだGCP全般の情報が少ないと感じています。
GAEやGloud SQL/Datastoreについて、構築も実装(使用)も非常に簡単なのでもっとGCPは盛り上がって良いんじゃないかなと思っています。
Go言語はかなり認知度があがってきたので、これからはGCPも含めてインプット・アウトプットを深めたいと思います。そして、もう少し新鮮なGoの情報を流すことができればなと。
また、今回は内部の話より表面的な話の方が多かったので、次回は実装の話をしようと思います。