docker_golang

Docker内のGo製Webアプリをソース変更後リロードするだけで確認できるようにする

ここ数日で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
これで保存すると・・・・

スクリーンショット 2016-02-18 22.28.00

ログの赤い枠の部分を見ると、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

勝手に起動するので削除しました。ご利用は自己責任でお願いします(追加したところを消して再起動したら戻ります)。

LINEで送る
Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です