Dockerを使ったGolang開発環境

しばらくiOSアプリのクライアントサイドばかり開発していてサーバサイドプログラムにご無沙汰だったのだけど、 最近またGoでアプリのサーバサイドを書くようになった。

ちょうど xhyve が話題になっているのもあって、OS X の仮想環境がアツい感じだったので、 ひさしぶりに Docker で開発してみよう、と思いたち、Dockerを使ったアプリ開発をやってみている。

docker-compose を使って依存ミドルウェアも一緒に立ち上げる

docker-compose [1] というのを使うと、複数のコンテナを同時に立てられ、それぞれにリンクも良い感じにやってくれる。 開発環境を作るにはもってこいのツールだ。

GoのWebアプリ

サンプルとして以下のようなアプリを考える。

// main.go
package main

import (
    "log"
    "fmt"
    "net/http"

    "github.com/garyburd/redigo/redis"
)

func main() {
    redi, err := redis.Dial("tcp", "redis:6379")
    if err != nil {
        log.Fatal(err)
    }

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        res, err := redi.Do("incr", "counter")
        if err != nil {
            w.WriteHeader(500)
            w.Write([]byte(err.Error()))
            return
        }

        if res, ok := res.(int64); ok {
            w.Write([]byte(fmt.Sprintf("counter: %d", res)))
        } else {
            w.WriteHeader(500)
            w.Write([]byte("unexpected value"))
        }
    })
    log.Fatal(http.ListenAndServe(":5000", nil))
}

HTTPでリクエストを受けて、Redisに保存してるカウントを +1 して返す、というアプリだ。

まず、このアプリ自体のコンテナを作る。これは以下のようなDockerfileを書くだけで良い。

# Dockerfile
FROM golang:onbuild
EXPOSE 5000

こいつを docker-compose から使うには、さらに以下のように docker-compose.yml を書く

# docker-compose.yml
app:
  build: .
  ports:
    - "5000:5000"
  links:
    - redis

redis:
  image: redis

内容としては、 app と、 redis という二つのコンテナを定義し、 app はさきほど作った Dockerfile を指定、5000ポートでアクセスできるようにして、redisコンテナにリンク、という感じだ。

これで、 docker-compose up とすると、コンテナたちが立ち上がり、 127.0.0.1:5000[2] でGoのwebアプリにアクセスできる。 何回かリロードすればredisに保存された値が増えていくのが分かるはずだ。

docker-compose がよしなに設定してくれるので、アプリからは redis というホストでredisコンテナにアクセスできているのも楽で良い。[3]

Goのコードを変更した時の対応

このままだと、Goのコードを書き換えた時にアプリのDockerイメージ自体をrebuildしないと変更が反映されない。 開発時に毎回ビルドするのは面倒だし、時間もかかるので、手元のコードをコンテナにマウントし、それを起動するようにする。

そうするには以下のように docker-compose.yml を書き換えれば良い。

app:
  build: .
  ports:
    - "5000:5000"
  links:
    - redis
  volumes:
    - ".:/go/src/app"
  command: go run main.go

redis:
  image: redis

こうすればコードを書き換えたら docker-compose up しなおせば手元のコードがrunされるので、イメージをrebuildしなくても変更に追従できる

依存ライブラリーが変わった時の対応

それだけだと、 go get するライブラリが増えた時に結局rebuildする必要があってめんどくさい。また、その場合 go get を全部やりなおすことになって時間もかかる。

なので、開発時には $GOPATH を格納する専用のデータコンテナを作ってやると良い。

まず、

$ docker run -itd --name myapp-gopath -v /go busybox

のようにして、 myapp-gopath という名前[4]で、 /go 以下[5]を共有するデータコンテナを作る。[6]

そして、 docker-compose-yml を以下のようにする

app:
  build: .
  ports:
    - "5000:5000"
  links:
    - redis
  volumes:
    - ".:/go/src/app"
  volumes_from:
    - myapp-gopath
  command: go run main.go

redis:
  image: redis

これで、appのGOPATHはmyapp-gopathコンテナのデータが使われるようになる。

go get をこのデータコンテナに対しておこなうのは、わかりにくいが、

$ docker run --rm --volumes-from myapp-gopath -v $PWD:/go/src/app golang:onbuild go-wrapper download

こんな感じにすればappイメージをrebuildすることなく、GOPATHを更新できる。

まとめ

以上で、go getするコマンドが長いのを除けば、わりと不満ない開発環境になった。

以前からもVMを使った開発環境というのは作っていたが、それに比べ、Dockerは

  • ミドルウェア単体でシンプルなコンテナがあるので分かりやすい
  • docker-composeでの簡単にコンテナ連携
  • aufsなどのDockerそもそものメリット

と、開発環境としてうれしいメリットがおおい。

ホストマシンにもGoの環境はあるので、Goのアプリは手元で動かし、 ミドルウェアだけをDockerで、というパターンも考えられそうだが、そちらはコンテナへの接続がめんどくさそうで一長一短か。

とりあえず、しばらくこの環境で開発してみようと思う。 プロダクションへの導入も追々考えたい。