Goの公式dockerイメージを使ってみた

## Goの公式dockerイメージとは？
* これの事 : [golang Repository | Docker Hub Registry - Repositories of Docker Images](https://registry.hub.docker.com/_/golang/)
    * アナウンス記事 : [Docker Hub Official Repos: Announcing Language Stacks | Docker Blog](http://blog.docker.com/2014/09/docker-hub-official-repos-announcing-language-stacks/)
    * 「各言語の実行環境が整ったコンテナ を動かすためのイメージ」を公式に公開したぜ、という話
    * ということはつまり、 __Goアプリの実行・テスト・プロファイリング・ビルド といった作業を「公式のお墨付き且つ使い捨ての環境」で行うことが出来る__ という事。素敵
    * 言語のバージョンも揃っているので、バージョン間の挙動差異を確認したい時にサクっと環境をGETすることが出来る

## 使い方の例

### 実行環境として使う (Start a Go instance in your app)
Goで作成したcliツールを公式イメージのコンテナで動かすケースを試してみる（[拙作](https://github.com/goldeneggg/ipcl) で試してみる）

#### 事前準備
* 対象goアプリをgit cloneする（ローカルにあればそこにcd）

```
$ git clone https://github.com/goldeneggg/ipcl.git
$ cd ipcl
```

* 公式記事にあるサンプルの形式でDockerfileを用意する
    * `FROM`で指定するベースイメージは、tagに`-onbuild`というsuffixが付いているものを選択
        * このイメージには、`ONBUILD`処理が複数定義されている
    * buildする際、この`ONBUILD`で定義されている処理も動く
        * `ONBUILD`で処理を定義した場合、 __下流の/派生先の Dockerfileのcontextで処理が実行される__
            * 派生先Dockerfileの`FROM`句 直後に処理を埋め込むイメージ 
        * 公式のサンプルではコンテナ上に`app`という名前で自・Goアプリがインストールされるけど、`ONBUILD`を使う事によりbuildを実行したディレクトリの内容次第でこの`app`の実体が変わる。という仕組み
            * つまり、Dockerfile自体に手を加えること無く使い回しが効く

```
$ vi Dockerfile

FROM golang:1.3.1-onbuild

# 対象goアプリ/ツールがコンテナ上では app という名前でインストールされるので、その名前を指定して実行
## 実行時引数が無いケース
# CMD ["app"]
## 実行時引数があるケース
CMD ["app", "192.168.56.24/0"]
```

* このDockerfileを(任意のtagを付けて)buildする
    * build時に行われる処理の流れ
        * [golang/1.3.1/onbuild](https://github.com/docker-library/golang/tree/master/1.3.1/onbuild) イメージの親(FROMで指定されてる)イメージの [golang/1.3.1](https://github.com/docker-library/golang/tree/master/1.3.1) がpull＆buildされる
            * Dockerfileは[これ](https://github.com/docker-library/golang/blob/master/1.3.1/Dockerfile)。goバイナリのダウンロード・`GOPATH`等の環境変数設定・buildで使うwrapperスクリプト [go-wrapper](https://github.com/docker-library/golang/blob/master/1.3.1/go-wrapper) のコピー・などなどが実行される
        * [golang/1.3.1/onbuild](https://github.com/docker-library/golang/tree/master/1.3.1/onbuild) イメージがpull&buildされる。`ONBUILD`により下記処理がビルド時に実行される
            * カレントディレクトリのファイル群をコンテナの`/go/src/app`にコピー
            * `go-wrapper download`の実行
            * `go-wrapper install`の実行

```
$ docker build -t my-ipcl .

:
(ONBUILD ここから)
# Executing 3 build triggers
Step onbuild-0 : COPY . /go/src/app
 ---> 370cb70d6c73
Step onbuild-1 : RUN go-wrapper download
 ---> Running in c6751d9cb63b
+ exec go get -v -d
github.com/goldeneggg/ipcl (download)
github.com/jessevdk/go-flags (download)
 ---> bc940f23da94
Step onbuild-2 : RUN go-wrapper install
 ---> Running in 04625b77395b
+ exec go install -v
github.com/goldeneggg/ipcl/parser
github.com/goldeneggg/ipcl/writer
github.com/jessevdk/go-flags
app
 ---> b6115dc12578
 ---> b6115dc12578
(ONBUILD ここまで)
:
```

#### goアプリの実行
* 実行すると、CMDで指定したコマンド（と引数）の結果が表示される

```
$ docker run -it --rm --name my-running-ipcl my-ipcl

source_cidr : 192.168.56.0/24
network     : 192.168.56.0
mask        : 255.255.255.0
host_num    : 254
min_address : 192.168.56.1
max_address : 192.168.56.254
broadcast   : 192.168.56.255
```

* `app`に渡す引数を動的に指定したい場合は、Dockerfileの`CMD`を`ENTRYPOINT`にすれば良い

```
FROM golang:1.3.1-onbuild

# 実行時引数は docker run 時に指定する
ENTRYPOINT ["app"]
```
```
$ docker build -t my-ipcl .
```
```
$ docker run -it --rm --name my-running-ipcl my-ipcl 192.168.56.0/24  # <- 実行時引数指定

source_cidr : 192.168.56.0/24
network     : 192.168.56.0
mask        : 255.255.255.0
host_num    : 254
min_address : 192.168.56.1
max_address : 192.168.56.254
broadcast   : 192.168.56.255

# 引数が無い場合Usageを表示する想定だが、その通り動くか？ -> OK
$ docker run -it --rm --name my-running-ipcl my-golang-ipcl

Target CIDR(or CIDR list file) is not assigned


Usage:
  ipcl [OPTIONS] <CIDR TEXT | -f <FILE>>

Application Options:
  -f, --file=    Filepath listed target CIDR
  -c, --csv=     Output format is csv
  -t, --tsv=     Output format is tsv
  -v, --version  Print version

Help Options:
  -h, --help     Show this help message
```

### コンパイル環境として使う
自作Goアプリをコンテナ上でビルドしたい場合

#### 事前準備
* 対象goアプリをgit cloneする（ローカルにあればそこにcd）

```
$ git clone https://github.com/goldeneggg/ipcl.git
$ cd ipcl
```

#### ※公式記事のサンプル通りでは動かない
* 公式記事にあるサンプルの形式で`go build` や `make` してみる
    * `-v`や`-w`のpathは`$GOPATH/src/<対象appのpackage path>`に置き換える
        * `GOPATH`は`/go` 
* ___ちょっとハマった...___
    * 外部ライブラリに依存している場合、事前に`go get`するなりして依存解決しておかないとbuildエラーになる
    * makeコマンドが無い（公式Dockerfileではapt-get installしているはずなのだが）、github上のDockerfileとdockerhubに公開されてるイメージに差分があるのかしら。。。

```
$ docker run --rm -v "$(pwd)":/go/src/github.com/goldeneggg/ipcl -w /go/src/github.com/goldeneggg/ipcl golang:1.3.1 go build -v

main.go:11:2: cannot find package "github.com/jessevdk/go-flags" in any of:
        /usr/src/go/src/pkg/github.com/jessevdk/go-flags (from $GOROOT)
        /go/src/github.com/jessevdk/go-flags (from $GOPATH)
```
```
$ docker run --rm -v "$(pwd)":/go/src/github.com/goldeneggg/ipcl -w /go/src/github.com/goldeneggg/ipcl golang:1.3.1 make

exec: "make": executable file not found in $PATH
```

* 対策として、
    * `go build`する前に、依存パッケージの`go get`を行うようにする
    * makeが動かなかった公式の`golang:1.3.1`用Dockerfileをローカルでbuildして使う
        * __折角公開されているイメージをローカルでbuildし直して使う、というのは本来やるべきではないと思うが__

```
$ wget https://raw.githubusercontent.com/docker-library/golang/master/1.3.1/go-wrapper
$ wget https://raw.githubusercontent.com/docker-library/golang/master/1.3.1/Dockerfile

$ docker build -t golang:1.3.1 .
```

#### 通常コンパイル (Compile your app inside the Docker container)

* 普通に`go build`する場合
    * `-v`で __カレントディレクトリ(=git cloneしたディレクトリ) と コンテナのGOPATH配下(`/go`配下)__ をマウント
    * `-w`でコンテナ側の作業ディレクトリを同じGOPATH配下にしており、ビルドの成果物はここに出力される __＝マウントしているサーバー側のカレントディレクトリに置かれる__

```
$ docker run --rm -v "$(pwd)":/go/src/github.com/goldeneggg/ipcl -w /go/src/github.com/goldeneggg/ipcl golang:1.3.1 bash -c 'go get -d ./... && go build -v'

github.com/goldeneggg/ipcl/parser
github.com/goldeneggg/ipcl/writer
github.com/goldeneggg/ipcl
```

* コンパイルで生成された実行ファイルをサーバー側で確認

```
$ ls ipcl
ipcl
```

* Makefileを用意していて`make`を実行する場合

```
$ docker run --rm -v "$(pwd)":/go/src/github.com/goldeneggg/ipcl -w /go/src/github.com/goldeneggg/ipcl golang:1.3.1 make

go get github.com/jessevdk/go-flags
go test ./parser
ok      github.com/goldeneggg/ipcl/parser       0.004s
go build -o /ipcl
```

#### クロスコンパイル (Cross-compile your app inside the Docker container)
* 通常コンパイルと異なるのは下記の2点
    * 使用するオフィシャルイメージはtagに`-cross`というsuffixの付いたもの
        * 通常コンパイル同様、ローカルでbuildして使う
            * __これもベースイメージをローカルでbuildしたからであって、ローカルでbuildし直して使う のは本来やるべきではない__
    * docker runの際に`-e`でプラットフォーム環境変数を指定する
        * `GOOS`
        * `GOARCH`
* ベースとなっている`golang:1.3.1`イメージをローカルでbuildしたので、`golang:1.3.1-cross`もローカルでbuildする
    * Dockerfileは[これ](https://github.com/docker-library/golang/blob/master/1.3.1/cross/Dockerfile)。go自体をクロスプラットフォームでbuildする処理を行っている

```
$ wget https://raw.githubusercontent.com/docker-library/golang/master/1.3.1/cross/Dockerfile
$ docker build -t golang:1.3.1-cross .
```

* クロスコンパイル実行 (`go build`で)
    * `-e`でプラットフォーム環境変数指定。例としてwindowsを
        * windowsを指定しているので実行ファイルは`.exe`形式で生成される

```
$ docker run --rm -v "$(pwd)":/go/src/github.com/goldeneggg/ipcl -w /go/src/github.com/goldeneggg/ipcl -e GOOS=windows -e GOARCH=386 golang:1.3.1-cross bash -c 'go get -d ./... && go build -v'

github.com/goldeneggg/ipcl/parser
github.com/goldeneggg/ipcl/writer
github.com/jessevdk/go-flags
github.com/goldeneggg/ipcl
```

* コンパイルで生成された実行ファイルをサーバー側で確認

```
$ ls ipcl.exe
ipcl.exe
```

* こんな感じのスクリプトを用意しとけば、複数プラットフォーム向けのクロスコンパイル → 実行ファイル取得 までを __常に同じGo環境を使って一括実施する__ ことができる

```
$ vi cross_compiles.sh
```
```bash
#!/bin/sh

PKG_PATH=$1
P=`basename ${PKG_PATH}`
PKG_NAME=${P%.*}

OS_S=("linux" "windows")
ARCH_S=("386" "amd64")

for os in ${OS_S[@]}
do
  for arch in ${ARCH_S[@]}
  do
    bin=${PKG_NAME}_${os}_${arch}
    if [ ${os} = "windows" ]
    then
        bin=${bin}.exe
    fi

    docker run --rm -v "$(pwd)":/go/src/${PKG_PATH} -w /go/src/${PKG_PATH} -e GOOS=${os} -e GOARCH=${arch} golang:1.3.1-cross bash -c "go get -d ./... && go build -o ${bin} -v"
  done
done
```
```
$ ./cross_compiles.sh github.com/goldeneggg/ipcl

github.com/goldeneggg/ipcl/parser
github.com/goldeneggg/ipcl/writer
github.com/jessevdk/go-flags
github.com/goldeneggg/ipcl
github.com/goldeneggg/ipcl/parser
github.com/goldeneggg/ipcl/writer
github.com/jessevdk/go-flags
github.com/goldeneggg/ipcl
github.com/goldeneggg/ipcl/parser
github.com/goldeneggg/ipcl/writer
github.com/jessevdk/go-flags
github.com/goldeneggg/ipcl
github.com/goldeneggg/ipcl/parser
github.com/goldeneggg/ipcl/writer
github.com/jessevdk/go-flags
github.com/goldeneggg/ipcl

$ ls ipcl*
ipcl_linux_386  ipcl_linux_amd64  ipcl_windows_386.exe  ipcl_windows_amd64.exe
```

## 所感
* これまで言語の実行環境用イメージは自前でDockerfileを書いて用意していたが、「dockerの公式」という箔が付いたイメージが公開された事で、種々の作業について合意形成が行い易くなったと思う 
    * イメージの内容について理解する必要がない。勉強して理解したい人だけがやればいい
    * イメージの内容を保証する必要がない
    * オレオレ文化の抑止効果
        * 個人・部署・会社を跨いだノウハウ共有のハードルが下がった
* とはいえ、公式イメージも随時修正されるもの（という想定）で考えておかねばならないわけで、現場で公式イメージを使用する運用を行うのであれば「どの / いつ時点の commitのイメージを使用したか？」はチーム内で管理・共有しておくべきである気がする
    * 管理・共有が面倒であれば「社内では、ある時点で公式をforkして、docker buildし直したイメージを使うようにする」とか良しなにルールを決めるなり何なり