Docker + VSCodeでGoの環境構築(ホットリロード対応)

## はじめに

個人開発で最近Go言語を使う機会が増えてきました。
Go言語で開発するにあたり私が感じたことは、

- Dockerで動かしたい
- ホットリロード機能が欲しい
- 開発用・本番用でコンテナを分けたい
- 開発時は、VSCode上でコンテナ内とリモート接続して作業したい

色々な方々のDocker環境を参考にDockerでGo環境を作りました。
よろしければ参考にしてください。

## ホスト環境
- Docker:　ver.19.03.8
- docker-compose:　ver.1.25.4
- VSCode:　ver.1.43.0

## ホットリロード
Go言語でホットリロードをする際、調べていたら、

- fresh
- realize

があるのを知りました。
しかし、筆者は**realize** の方はエラー(`error: returned a non-zero code: 1`)が発生して`go get` ができませんでした。


一度コンテナを立ち上げて、コンテナ内で`go get` したらうまく行くのですが解決出来ませんでした...

ですので今回は**fresh** を使用します。

## 手順
1. Dockerfile(開発用&本番用) + docker-compose.yml の作成
1. Remote Containers の設定
1. ホットリロードの設定
1. 開発環境の起動
1. 開発環境の動作確認
1. 本番環境の起動

## 開発環境
はじめに作業ディレクトリを作ります。

``` shell:/
# 作業ディレクトリ作成
$ mkdir go-work
# 作業ディレクトリへ移動
$ cd go-work
```

### １. Dockerfile(開発用&本番用) + docker-compose.yml の作成
続いて**Dockerfile** と**docker-compose.yml** を書いていきます。
下記コマンドからファイルを作成してください。

``` shell:/go-work
$ touch docker-compose.yml
$ mkdir -p docker/golang
$ touch docker/golang/Dockerfile.dev
$ touch docker/golang/Dockerfile.dep
```

#### ※ Dockerfileの言語サポート

Dockerfile.dev(.dep) はVSCodeの標準では言語サポートされていないので設定を追加します。
設定されている方は次へ飛ばしてください。

1. [Code] -> [基本設定] -> [設定]
1. `Files: Associations` で検索
1. `settings.json` で編集

``` json:settings.json
{
  "files.associations": { 
    "Dockerfile.dev": "dockerfile",
    "Dockerfile.dep": "dockerfile"
  }
}
```

#### Docker + docker-composeの設定

- 開発用Dockerfile

``` dockerfile:/go-work/docker/golang/Dockerfile.dev
FROM golang:1.14.0-alpine3.11

SHELL ["/bin/ash", "-c"]
WORKDIR /go/src/app
COPY ./app ./
EXPOSE 8080
ENV GO111MODULE=on

RUN apk add --no-cache alpine-sdk

# Golang ホットリロード(freshのインストール)
RUN go get github.com/pilu/fresh

# Golang 環境構築(任意)
RUN go get github.com/go-delve/delve/cmd/dlv \
    github.com/rogpeppe/godef \ 
    golang.org/x/tools/cmd/goimports \
    golang.org/x/tools/cmd/gorename \
    sourcegraph.com/sqs/goreturns \
    github.com/ramya-rao-a/go-outline \
    golang.org/x/tools/gopls@latest
```

- 本番用Dockerfile

``` dockerfile:/go-work/docker/golang/Dockerfile.dep
FROM golang:1.14.0-alpine3.11 as builder

WORKDIR /go/src/app
COPY ./app ./
ENV GO111MODULE=on 
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/app

FROM alpine:latest

RUN apk --no-cache add ca-certificates
COPY --from=builder /go/bin/app /go/bin/app
ENTRYPOINT [ "/go/bin/app" ]
EXPOSE 8080
```

- docker-compose

``` yml:/go-work/docker-compose.yml
version: '3.7'

services:
  app:
    container_name: go-app
    build:
      context: .
      # Remote Containerが立ち上がるまで待機
      dockerfile: docker/golang/Dockerfile.dev
    ports:
      - 8080:8080
    volumes:
      - ./app:/go/src/app:cached
    command: /bin/ash -c "while sleep 1000; do :; done"
```

### ２. Remote Containers の設定

#### インストール
インストールしていない方は、 [Remote-Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) からインストールしてください。

#### 準備

まずはじめにVSCode の左下のアイコンをクリックしてください。
<img width="150" alt="01-icon.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381808/8cdcba90-878f-27c5-0ea4-1b016a004698.png">
すると、コマンドパレッドが開きますので、
`Remote-Containers: Add Development Container Configuration Files...`
と検索して選択してください。

<img width="500" alt="02-remote-containers.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381808/7f078464-ab9d-ff50-2819-3f0543bb3eb5.png">

選択したら、
`From 'docker-compose.yml'`
を選択してください。

<img width="500" alt="03-remote-containers.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381808/3cc9d46e-ac82-54f0-119b-1283849e3cd8.png">

選択後`.devcontainer` ディレクトリが作成され、設定ファイルが格納されています。

#### 設定

``` shell
.devcontainer
    ├── devcontainer.json
    └── docker-compose.yml
```

まずはじめに、docker-compose.yml はもう作成してあるので、
.devcontainer 内のdocker-compose.yml は削除してください。

下記のコマンドを実行してファイルを削除してください。

``` shell:/go-work
$ rm .devcontainer/docker-compose.yml
```

次にdevcontainer.json を編集します。

``` json:/go-work/.devcontainer/devcontainer.json
{
  "name": "go",
  "dockerComposeFile": [
    "../docker-compose.yml"
  ],
  "service": "app",
  "workspaceFolder": "/go/src/app",
  "settings": { 
    "terminal.integrated.shell.linux": "/bin/ash",
    "go.gopath": "/go"
  },
  "extensions": [
    "ms-vscode.go",
  ],
  "shutdownAction": "stopCompose"
}
```

### ３. ホットリロードの設定

#### 準備
まずはGoアプリを開発する作業ディレクトリを作成します。
その後ホットリロードを行うための**fresh** の設定ファイルを作成します。

ちなみに設定ファイルを作成しなくても大丈夫ですのでこちらは任意で！

下記のコマンドを実行してください。

``` shell:/go-work
$ mkdir app
$ cd app
# /go-work/app
$ touch .fresh.conf
```

#### 設定

``` conf:/go-work/app/.fresh.conf
root:              .
tmp_path:          ./tmp
build_name:        runner-build
build_log:         runner-build-errors.log
valid_ext:         .go, .tpl, .tmpl, .html
no_rebuild_ext:    .tpl, .tmpl, .html
ignored:           assets, tmp
build_delay:       600
colors:            1
log_color_main:    cyan
log_color_build:   yellow
log_color_runner:  green
log_color_watcher: magenta
log_color_app:
```
上記の設定はテンプレートから持ってきています。
細かい設定は任意で！

### ４. 開発環境の起動

さて、いよいよコンテナを起動します。
VSCode から左下のアイコンをクリックしてください。

<img width="150" alt="01-icon.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381808/e15c6c0f-23a7-3e4f-69da-7e17a6032c58.png">

すると、コマンドパレッドが開きますので、
`Remote-Containers: Reopen in Container`
を検索して選択してください。

<img width="500" alt="04-remote-containers.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381808/a0c445f0-d8ec-6b59-b95e-f2aa3d8ee1d2.png">

すると、画面が切り替わります。
この時点でDockerイメージが作成され、コンテナが自動で起動します。
起動するまでしばらく時間がかかりますので待機...

しばらくするとコンテナとリモートで繋がり作業ディレクトリに移ります。

<img width="200" alt="05-remote-containers.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381808/9d8ed527-c6b9-e9d2-d16d-b947eb2cca24.png">

VSCode の左下のアイコンも変わっているはずです。
VSCode 上でターミナルを開くと、コンテナ内のターミナルに切り替わっています。

``` shell:コンテナ内
/go/src/app #
```

### ５. 開発環境の動作確認

**ここからはコンテナ内で作業していきます。**
ホットリロードの動作を確認するためにGo のプログラムを開発していきます。

#### main.go の作成

開発するためのファイルを作成します。

``` shell:/go/src/app
touch main.go
go mod init main
```

つづいてmain.go に簡単なWebサーバを作成します。

``` go:/go/src/app/main.go
// ...省略
func init() {
  http.HandleFunc("/", index)
}

func index(w http.ResponseWriter, r *http.Request) {
  fmt.Fprint(w, "Hello World")
}

func main() {
  fmt.Print("Server Start")
  if err := http.ListenAndServe(":8080", nil); err != nil {
    panic(err)
  }
}
```

#### 実行

main.go を実行するのですが今回は**fresh** を使って実行します。
VSCode 内のコンテナで実行しても良いのですが、こちらは開発時で利用したいので今回は別ターミナルで実行します。

別ターミナルでコンテナ内に入り、ホットリロードを起動させます。

``` shell:別ターミナル
$ docker exec -it go-app /bin/ash

# コンテナ内: /go/src/app
fresh -c .fresh.conf
```
main.go が実行されますので確認作業に移ります。

#### 確認

確認は、ブラウザ または、ターミナル で確認します。

- ブラウザの場合は、[http://localhost:8080](http://localhost:8080) にアクセス
- ターミナルの場合は、`curl http://localhost:8080`  を実行

`Hello World` が返ってくれば起動しています。

#### 変更

つづいてホットリロードが正常に起動しているかを確認したいので、
**fresh を起動したままVSCode に戻ってください**。

VSCode上でmain.go を変更し、保存してください。

``` go:/go/src/app/main.go
func index(w http.ResponseWriter, r *http.Request) {
  - fmt.Fprint(w, "Hello World")
  + fmt.Fprint(w, "Hello Golang")
}
```

保存したら再度、確認作業を行ってください。
`Hello Golang` が返ってくればホットリロードは正常に起動しています。

### ６. 本番環境の起動

本番環境はDockerをビルドして実行すれば良いだけです。

#### イメージのビルド

``` shell:/go-work
$ docker build -t go-app -f ./docker/golang/Dockerfile.dep .
```

#### コンテナの実行

``` shell:/go-work
$ docker run --name go-app go-app -p 8080:8080 -d . /bin/ash
```