Go1.12＋go modでDockerコンテナ化

# お題
k8sの勉強しがてら、GoのアプリでもGKEに乗せてみようと思いつつ、そういえば、表題の組み合わせでDockerコンテナ作ったことないなと気づく。
前回最後にDockerコンテナ作った時は、Go1.9＋dep だった、たしか。
というわけで、知識をアップデートしておく。

# 開発環境
### # OS
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
```

### # Docker
```
$ sudo docker version
Client:
 Version:           18.09.2
 API version:       1.39
 Go version:        go1.10.4
 Git commit:        6247962
 Built:             Tue Feb 26 23:52:23 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.09.2
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.4
  Git commit:       6247962
  Built:            Wed Feb 13 00:24:14 2019
  OS/Arch:          linux/amd64
  Experimental:     false
```

# 実践

## Goアプリ

まあ、アプリの内容は何でもいいので、とりあえず `http://localhost:8080/sample` でアクセスしたら `200 OK` をJSON形式で返すのを書いておく。

```go:[main.go]
package main

import (
	"net/http"

	"github.com/labstack/echo"
)

func main() {
	e := echo.New()
	e.GET("/sample", func(c echo.Context) error {
		return c.JSON(
			http.StatusOK,
			struct {
				Code int    `json:"code"`
				Text string `json:"text"`
			}{
				Code: http.StatusOK,
				Text: http.StatusText(http.StatusOK),
			},
		)
	})
	e.Logger.Fatal(e.Start(":8080"))
}
```

ローカル環境で実行すると↓のように`8080`ポートで待ち受ける。

```
$ go run main.go 

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v3.3.10-dev
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080
```


ちなみに、`go.mod` はこんな感じ。

```text:[go.mod]
module t01

require (
	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
	github.com/labstack/echo v3.3.10+incompatible
	github.com/labstack/gommon v0.2.8 // indirect
	github.com/mattn/go-colorable v0.1.1 // indirect
	github.com/mattn/go-isatty v0.0.7 // indirect
	github.com/valyala/fasttemplate v1.0.1 // indirect
	golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 // indirect
)
```

## Dockerfile

以下を参考に。
https://medium.com/@petomalina/using-go-mod-download-to-speed-up-golang-docker-builds-707591336888

```dockerfile
# step 1: build
FROM golang:1.12.4-alpine3.9 as build-step

# for go mod download
RUN apk add --update --no-cache ca-certificates git

RUN mkdir /go-app
WORKDIR /go-app
COPY go.mod .
COPY go.sum .

RUN go mod download
COPY . .

RUN CGO_ENABLED=0 go build -o /go/bin/go-app

# -----------------------------------------------------------------------------
# step 2: exec
FROM scratch
COPY --from=build-step /go/bin/go-app /go/bin/go-app
ENTRYPOINT ["/go/bin/go-app"]
```

### 特記事項

#### ■ベースイメージを「[Docker Hub](https://hub.docker.com)」から取得しているので`docker login`が必要

してないと、こうなる。

```
$ sudo docker build -t sky0621/go-app:v0.1 .
Sending build context to Docker daemon  7.168kB
Step 1/8 : FROM "1.11.9-alpine3.9" AS builder
pull access denied for 1.11.9-alpine3.9, repository does not exist or may require 'docker login'
```

なので、[docker login](https://docs.docker.com/engine/reference/commandline/login/)する。
【参考】
http://docs.docker.jp/docker-hub/accounts.html

```
$ sudo docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: sky0621dhub
Password: 
WARNING! Your password will be stored unencrypted in /home/sky0621/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
```

#### ■「`RUN apk add --update --no-cache ca-certificates git`」がないと、`RUN go mod download`が失敗する。

```
Step 6/11 : RUN go mod download
 ---> Running in 1b5ecc7774b5
go: github.com/dgrijalva/jwt-go@v3.2.0+incompatible: git init --bare in /go/pkg/mod/cache/vcs/11fb0427c36fe2d22a58c95464426a2d142ff24c438e1248eeb9ff450be5af41: exec: "git": executable file not found in $PATH
　　〜〜　省略　〜〜
go: golang.org/x/crypto@v0.0.0-20190418165655-df01cb2cc480: git init --bare in /go/pkg/mod/cache/vcs/de5fd3af413a4f3f969455ae522b4002fcb7bb4c158f339396dfc77710c9007d: exec: "git": executable file not found in $PATH
go: error loading module requirements
The command '/bin/sh -c go mod download' returned a non-zero code: 1
```

#### ■「`CGO_ENABLED=0`」がないと、`docker run`が失敗する。

```
$ sudo docker run -p 80:8080 sky0621/go-app:v0.1
standard_init_linux.go:207: exec user process caused "no such file or directory"
```

`cgo`というのは、GoからCのコードを呼び出すコマンドらしい。
それを有効にする（`=1`）か無効にする（`=0`）かの指定が「`CGO_ENABLED`」でデフォルトでは有効とのこと。
（ただし、クロスコンパイル時はデフォルトで無効）
有効の場合、ビルド制約として`cgo`がセットされる。
【参考】
https://golang.org/cmd/cgo/

`standard_init_linux.go`の中身は追っていないので、ここでエラーになった原因であると断定は出来ないのだけど、ビルド制約`cgo`を外すことで`docker run`時の失敗は回避された。

## ビルド

事前にdockerプロセスもイメージもないことを確認。

```
$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
$
$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
```

では、実行。

```
$ ls -l
total 16
-rw-r--r-- 1 sky0621 sky0621  464 Apr 20 08:52 Dockerfile
-rw-r--r-- 1 sky0621 sky0621  420 Apr 19 08:00 go.mod
-rw-r--r-- 1 sky0621 sky0621 1895 Apr 19 08:00 go.sum
-rw-r--r-- 1 sky0621 sky0621  375 Apr 19 09:23 main.go
$
$ date
Sat Apr 20 09:11:41 JST 2019
$
$ sudo docker build -t sky0621/go-app:v0.1 .
Sending build context to Docker daemon  6.656kB
Step 1/12 : FROM golang:1.12.4-alpine3.9 as build-step
1.12.4-alpine3.9: Pulling from library/golang
bdf0201b3a05: Pull complete 
38f114998adb: Pull complete 
21134b1a9e68: Pull complete 
cff36310a8c5: Pull complete 
02f293f0b51b: Pull complete 
Digest: sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Status: Downloaded newer image for golang:1.12.4-alpine3.9
 ---> b97a72b8e97d
Step 2/12 : RUN apk add --update --no-cache ca-certificates git
 ---> Running in 0c5bb1e980c9
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz
(1/6) Installing nghttp2-libs (1.35.1-r0)
(2/6) Installing libssh2 (1.8.2-r0)
(3/6) Installing libcurl (7.64.0-r1)
(4/6) Installing expat (2.2.6-r0)
(5/6) Installing pcre2 (10.32-r1)
(6/6) Installing git (2.20.1-r0)
Executing busybox-1.29.3-r10.trigger
OK: 20 MiB in 21 packages
Removing intermediate container 0c5bb1e980c9
 ---> 0625379ca405
Step 3/12 : RUN mkdir /go-app
 ---> Running in 82c9e6be294d
Removing intermediate container 82c9e6be294d
 ---> 0e99542d29eb
Step 4/12 : WORKDIR /go-app
 ---> Running in 7929d16e6ce4
Removing intermediate container 7929d16e6ce4
 ---> 2bb8099a90aa
Step 5/12 : COPY go.mod .
 ---> 77a7fb906751
Step 6/12 : COPY go.sum .
 ---> da9177c9aaa1
Step 7/12 : RUN go mod download
 ---> Running in a60a8f304669
go: finding github.com/labstack/echo v3.3.10+incompatible
go: finding github.com/mattn/go-colorable v0.1.1
go: finding github.com/dgrijalva/jwt-go v3.2.0+incompatible
go: finding github.com/labstack/gommon v0.2.8
go: finding github.com/mattn/go-isatty v0.0.7
go: finding github.com/valyala/fasttemplate v1.0.1
go: finding golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480
go: finding github.com/mattn/go-isatty v0.0.5
go: finding github.com/valyala/bytebufferpool v1.0.0
go: finding golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e
go: finding golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223
Removing intermediate container a60a8f304669
 ---> 44992184e9e2
Step 8/12 : COPY . .
 ---> 8e8041d6ff86
Step 9/12 : RUN CGO_ENABLED=0 go build -o /go/bin/go-app
 ---> Running in 1baafdb9189b
Removing intermediate container 1baafdb9189b
 ---> 51d15d46907b
Step 10/12 : FROM scratch
 ---> 
Step 11/12 : COPY --from=build-step /go/bin/go-app /go/bin/go-app
 ---> 6031f71f0586
Step 12/12 : ENTRYPOINT ["/go/bin/go-app"]
 ---> Running in a27a508d1102
Removing intermediate container a27a508d1102
 ---> 1ecb02037408
Successfully built 1ecb02037408
Successfully tagged sky0621/go-app:v0.1
$
$ date
Sat Apr 20 09:12:23 JST 2019
```

かかった時間は、ざっと「42」秒。けっこうかかるね。。

```
Sat Apr 20 09:11:41 JST 2019
Sat Apr 20 09:12:23 JST 2019
```

## イメージの確認

Goのビルドなどに使ったイメージはそれなりのサイズではあるものの、実際に動かすGoアプリのイメージは「8.43MB」と軽量。

```
$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
sky0621/go-app      v0.1                1ecb02037408        13 minutes ago      8.43MB
<none>              <none>              51d15d46907b        13 minutes ago      413MB
golang              1.12.4-alpine3.9    b97a72b8e97d        7 days ago          350MB
```

## 実行

```
$ sudo docker run -p 80:8080 sky0621/go-app:v0.1

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v3.3.10-dev
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080
```

起動した。
では、アクセス。
![screenshot-localhost-2019.04.20-09-32-43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/00cf5e9b-3855-234c-e3b3-8eebaf050cca.png)

OK。

## キャッシュ確認

Dockerfileのつくりによるキャッシュの効果を確認するために、Goアプリを微修正して再ビルドしてみる。
ビルド時間が短くなることを期待。

### Goアプリを微修正

受け付けるパスを「`/sample`」→「`/sample2`」に変えただけ。

```
$ git diff
diff --git a/chXX_try/app/main.go b/chXX_try/app/main.go
index acb93c1..522a0e4 100644
--- a/chXX_try/app/main.go
+++ b/chXX_try/app/main.go
@@ -8,7 +8,7 @@ import (
 
 func main() {
        e := echo.New()
-       e.GET("/sample", func(c echo.Context) error {
+       e.GET("/sample2", func(c echo.Context) error {
                return c.JSON(
                        http.StatusOK,
                        struct {
```

### 再ビルド

```
$ date
Sat Apr 20 09:43:21 JST 2019
$
$ sudo docker build -t sky0621/go-app:v0.2 .
Sending build context to Docker daemon  6.656kB
Step 1/12 : FROM golang:1.12.4-alpine3.9 as build-step
 ---> b97a72b8e97d
Step 2/12 : RUN apk add --update --no-cache ca-certificates git
 ---> Using cache
 ---> 0625379ca405
Step 3/12 : RUN mkdir /go-app
 ---> Using cache
 ---> 0e99542d29eb
Step 4/12 : WORKDIR /go-app
 ---> Using cache
 ---> 2bb8099a90aa
Step 5/12 : COPY go.mod .
 ---> Using cache
 ---> 77a7fb906751
Step 6/12 : COPY go.sum .
 ---> Using cache
 ---> da9177c9aaa1
Step 7/12 : RUN go mod download
 ---> Using cache
 ---> 44992184e9e2
Step 8/12 : COPY . .
 ---> c9a3ca188d54
Step 9/12 : RUN CGO_ENABLED=0 go build -o /go/bin/go-app
 ---> Running in 4aaa5be99f06
Removing intermediate container 4aaa5be99f06
 ---> f3ab3206497a
Step 10/12 : FROM scratch
 ---> 
Step 11/12 : COPY --from=build-step /go/bin/go-app /go/bin/go-app
 ---> fa6b7e4787a8
Step 12/12 : ENTRYPOINT ["/go/bin/go-app"]
 ---> Running in d17c0c300e5b
Removing intermediate container d17c0c300e5b
 ---> 0b607262dca1
Successfully built 0b607262dca1
Successfully tagged sky0621/go-app:v0.2
$
$ date
Sat Apr 20 09:43:33 JST 2019
```

早くなってる。「12」秒。（初回は「42」秒だった。）
キャッシュ効果、大きいね。

```
Sat Apr 20 09:43:21 JST 2019
Sat Apr 20 09:43:33 JST 2019
```

# まとめ
簡素なアプリで試しただけではあるものの、ひとまずGo1.12＋go modでのDockerコンテナ化は完了。
Dockerfileに関しては定期的に↓など再確認しないとなぁと思った。
http://docs.docker.jp/v1.9/engine/articles/dockerfile_best-practice.html