ここ数日でDocker関連とGo言語の開発環境について書いてきました。
今回はそれらを組み合わせて、Go言語の開発をDockerコンテナ上で行いたいと思います。
ということで、今回は今までの記事を読んでいることが前提です。
目次
今回目指すところ
- Dockerコンテナ内でGoのWebアプリをビルドしてMacから動作確認できるようにする。
- ソースを保存するだけで自動的にビルド&起動されるようにする。
HotReloadのしくみ
PHPなどのスクリプト言語ではコンパイルが不要なので保存すればすぐに動作の確認ができますが、GoやJavaなどの言語は一旦コンパイルしないと動作させることができません。
そのような場合、gulpやGruntのようなタスクランナーを使ってファイルの変更を監視し、ビルドタスクを走らせることで動作確認の際のビルド&再起動を自動化することができます。
今回はGo製のタスクランナー、godoを使います。
godoの使い方
godoはGoでできているので、go getでインストールできます。
$ go get -u gopkg.in/godo.v2/cmd/godo
godoは、実行させたディレクトリ配下のGododirというディレクトリ内のmain.goを実行します。
例えば以下のようにGododir/main.goを作るとカレントディレクトリ以下の*.goファイルを監視し、変更があればmain.goをリビルド&再起動できるようになります。
package main import ( do "gopkg.in/godo.v2" ) func tasks(p *do.Project) { p.Task("server", nil, func(c *do.Context) { // rebuilds and restarts when a watched file changes c.Start("main.go", do.M{"$in": "./"}) }).Src("*.go", "**/*.go"). Debounce(3000) } func main() { do.Godo(tasks) }
実行方法は、
$ godo server --watch
のようにします。
Dockerfileを作る
今回は公式のGoのDockerイメージを使いたいと思います。
昨日1.6がリリースされたようなので、試しに使ってみます。
ただし、今回はHotReloadできるようにしたいので、このイメージをそのまま使うのではなく、これをもとにDockerfileを作ります。
まず、公式イメージのDockerfileを見てみましょう。
FROM buildpack-deps:jessie-scm # gcc for cgo RUN apt-get update && apt-get install -y --no-install-recommends \ g++ \ gcc \ libc6-dev \ make \ && rm -rf /var/lib/apt/lists/* ENV GOLANG_VERSION 1.6 ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz ENV GOLANG_DOWNLOAD_SHA256 5470eac05d273c74ff8bac7bef5bad0b5abbd1c4052efbdbc8db45332e836b0b RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \ && echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \ && tar -C /usr/local -xzf golang.tar.gz \ && rm golang.tar.gz ENV GOPATH /go ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" WORKDIR $GOPATH COPY go-wrapper /usr/local/bin/
これを見ると、環境変数GOPATHが /go に設定されているということがわかります。
ですから、/go/src/packagename のような構成にする必要があります。
また、先ほどのgodoをインストールしておかなければいけません。
それを踏まえて、Dockerfileを作ります。
FROM golang:1.6 MAINTAINER d-abe <abe@flup.jp> RUN go get -u gopkg.in/godo.v2/cmd/godo WORKDIR /go/src/app EXPOSE 8080 CMD ["/go/bin/godo", "server", "--watch"]
GOPATHが/goなので、go get すると/go/binにgodoがインストールされるので、CMDの実行パスは/go/bin/godoとなっています。
イメージにはGododirなどは入れていません。また、WORKDIRですが、とりあえずイメージでは /go/src/app としています。
この辺りは、コンテナ起動時のパラメータで調整を行います。
docker-compose.yaml
次に、コンテナ起動時のパラメータなどをdocker-compose.yamlに設定します。
ファイルの位置関係は
workspace ├── docker │ ├── compose │ │ └── docker-compose.yaml │ └── golang │ └── Dockerfile └── go └── src └── myapp ├── Gododir │ └── main.go ├── glide.lock ├── glide.yaml └── main.go
のような感じとすると、myappをDockerコンテナ上の/go/src/myappにマウントさせれば良さそうです。
# postgres postgresql: image: postgres:9.5 ports: - "5432:5432" environment: POSTGRES_USER: docker POSTGRES_PASSWORD: docker POSTGRES_DB: docker # golang golang: build: ../golang volumes: - "${ROOT_DIR}/go/src/myapp:/go/src/myapp" links: - postgresql:db working_dir: /go/src/myapp ports: - "80:8080"
ここで、${ROOT_DIR}というのがありますが、これはdirenvを使って環境変数を設定しておきます。
上の例ではworkspaceの中で、
export ROOT_DIR=$(pwd)
とでもしておけば良いかと思います。
サンプルのmain.go
今回はとりあえずGinフレームワークのサンプルを動かしてみたいと思います。
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and server on 0.0.0.0:8080 }
main.goを作ったら、
$ glide create $ glide up
として、vendor以下に外部パッケージを入れておきます。
また、試しにローカルで動くかどうか実験します。
$ go run main.go [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /ping --> main.main.func1 (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080
うまく起動できました。正常に動作するかどうかも確認します。
$ curl -l http://localhost:8080/ping {"message":"pong"}
問題ないようです。
Docker Composeを動かす
ではいよいよ、Docker Composeを動かしてみます。
$ cd docker/compose $ docker-compose up -d Creating golang_postgresql_1 Creating golang_golang_1 $ docker-compose logs golang Attaching to golang_golang_1 golang_1 | godo Godo tasks changed. Rebuilding /go/src/myapp/Gododir/godobin-2.0.0-pre... golang_1 | godo watching /go/src/myapp/Gododir ... golang_1 | server rebuilding with -a to ensure clean build (might take awhile) golang_1 | server 23385ms golang_1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. golang_1 | - using env: export GIN_MODE=release golang_1 | - using code: gin.SetMode(gin.ReleaseMode) golang_1 | golang_1 | [GIN-debug] GET /ping --> main.main.func1 (3 handlers) golang_1 | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default golang_1 | [GIN-debug] Listening and serving HTTP on :8080 golang_1 | server watching . ...
docker-compose logs で、ログを見ることができます。
これを見ると、正常に先ほどのGinのサンプルが動いていることがわかります。
先ほどと同様に動作確認を行います。
$ docker-machine ip default 10.211.55.25 $ curl -l http://10.211.55.25/ping {"message":"pong"}
次に、main.goを少し変更してみます。
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ - "message": "pong", + "message": "hoge", }) }) r.Run() // listen and server on 0.0.0.0:8080 }
pongをhogeに変えただけですがw
これで保存すると・・・・
ログの赤い枠の部分を見ると、rebuiltされ再起動されたことがわかります。
実際に、再度curlしてみると・・・
$ curl -l http://10.211.55.25/ping {"message":"hoge"}
ちゃんと反映されていますね!!
これで、ソースを保存→ブラウザをリロード、というスタイルで開発できるようになります。
VMWare Fusionでのトラブル
このやり方で本格的に開発していたら、VMWare Fusionのせいなのか分かりませんが、godoで実行させようとするとpanic: runtime error: invalid memory address or nil pointer dereferenceというエラーが出て失敗してしまいました。
さらに、コンテナを再起動させると、transport endpoint is not connectedというエラーが出てコンテナが起動しない...。
調べてみると、boot2dockerから/Usersへのhgfsのマウントが解除されてしまっているという・・・。
原因は全く不明で困っていたのですが、もうバッサリVMWareの共有機能を使うことを諦めてNFS経由にしたところ解決しました。
# 解決に至るまで、godoが悪いのか?とかいろいろ考えてgulp.jsでgulp-goを使って動かしたりしてみましたが、同じエラーが出てホント困りました。。
やり方は、まずdocker-machineを作るところからやり直します。
$ docker-machine create --driver vmwarefusion --vmwarefusion-no-share default
これで、VMWareでのシェア機能が無効化されます。
次に、docker-machine-nfsというツールを使います。
説明通り、インストールします。
$ curl -s https://raw.githubusercontent.com/adlogix/docker-machine-nfs/master/docker-machine-nfs.sh | sudo tee /usr/local/bin/docker-machine-nfs > /dev/null && \ sudo chmod +x /usr/local/bin/docker-machine-nfs
インストールが完了したら、NFSで/Usersを共有させます。
$ docker-machine-nfs default
途中でパスワードを聞かれることがあります。その場合、Macでの管理者パスワードを入れればOKです。
このツールはVirtualBoxを使っている場合でも有用(VirtualBoxのファイル共有は速度が遅すぎる・・)なので参考にしてください。
【追記】VMWareのシェア機能
--vmwarefusion-noshareを入れても、なぜか無効になっていませんでした!!
そこで、docker-machine-nfsを入れると生成されるbootlocal.shを少しいじります。
#!/bin/sh sudo umount /Users + rm /usr/local/bin/vmhgfs-fuse + sudo killall vmhgfs-fuse sudo mkdir -p /Users sudo /usr/local/etc/init.d/nfs-client start sudo mount -t nfs -o noacl,async 192.168.10.40:/Users /Users
勝手に起動するので削除しました。ご利用は自己責任でお願いします(追加したところを消して再起動したら戻ります)。