Docker上のGo製Webアプリケーションをリモートデバッグする

# 概要
Docker上で動作するGo製Webアプリケーションをデバッグする方法を解説します。

## 2020-05-03 追記

以前は `oxequa/realize` を利用していましたが、 2020年5月3日現在 `oxequa/realize` はメンテナンスが止まっています。

例えば [去年私が追加したissue](https://github.com/oxequa/realize/issues/253) は閉じられていませんし、その他のPRもマージされていません。

その為 [cosmtrek/air](https://github.com/cosmtrek/air) を使った形に記事を大幅に書き換えました。

[cosmtrek/air](https://github.com/cosmtrek/air) はメンテナンスが継続されており、GoModuleにも対応しているので、`oxequa/realize` の代わりは十分に果たせると思っています。

# 環境

- ホストOS(macOS Catalina バージョン 10.15.4)
- GoLand 2020.1.1
- Docker Desktop for Mac(Docker version 19.03.8)
- 利用するDockerイメージ(golang:1.14-alpine3.11)

この記事ではホストPC上で利用するツールとしてGoLandを利用しますが、[Visual Studio Code](https://code.visualstudio.com/) 等でも同じことは実現出来ると思います。

# Docker側の設定

## Dockerfileの内容

```Docker:Dockerfile
FROM golang:1.14-alpine3.11 as build

LABEL maintainer="https://github.com/nekochans"

WORKDIR /go/app

COPY . .

ENV GO111MODULE=off

RUN set -eux && \
  apk update && \
  apk add --no-cache git curl && \
  go get -u github.com/cosmtrek/air && \
  go build -o /go/bin/air github.com/cosmtrek/air && \
  go get -u github.com/go-delve/delve/cmd/dlv && \
  go build -o /go/bin/dlv github.com/go-delve/delve/cmd/dlv

ENV GO111MODULE on

RUN set -eux && \
  go build -o portfolio-backend ./cmd/rest/main.go

FROM alpine:3.11

WORKDIR /app

COPY --from=build /go/app/portfolio-backend .

RUN set -x && \
  addgroup go && \
  adduser -D -G go go && \
  chown -R go:go /app/portfolio-backend

CMD ["./portfolio-backend"]
```

`go get -u github.com/go-delve/delve/cmd/dlv` と `go build -o /go/bin/dlv github.com/go-delve/delve/cmd/dlv` の部分が重要になります。

`delve` というGoLang用のDebuggerをインストールしています。

開発時にホットリロードを有効にしたいので `go get -u github.com/cosmtrek/air` で[cosmtrek/air](https://github.com/cosmtrek/air) をインストールしています。

## docker-composeの設定

以下のように設定します。

```yaml:docker-compose.yml
version: '3.7'

services:
  go:
    build:
      context: .
      dockerfile: docker/go/Dockerfile
      target: build
    volumes:
      - ./:/go/app
    command: air
    ports:
      - 8888:8888
      - 2345:2345
    security_opt:
      - apparmor:unconfined
    cap_add:
      - SYS_PTRACE
```

`command` で `air` を起動させています。

`ports` で `8888`（WebServer用のポート）と `2345`（デバッグ用のポート）を開放します。

`security_opt`, `cap_add` の2つはDocker上で `delve`を利用する為の設定です。

```yaml
    security_opt:
      - apparmor:unconfined
```

Dockerが行うシステムコールを制限するオプションです。

（参考）https://docs.docker.com/engine/security/seccomp/

```yaml
    cap_add:
      - SYS_PTRACE
```

コンテナの capabilities の追加を行います。

（参考）https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities

（参考）[Docker コンテナ上で Go のプロジェクトを delve でデバッグするには ptrace の許可が必要](http://please-sleep.cou929.nu/debug-go-project-with-delve-on-docker.html)

## airの設定

下記の通りです。

```.air.conf
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o tmp/portfolio-backend ./cmd/rest/main.go"
# Binary file yields from `cmd`.
bin = "tmp/portfolio-backend"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air /go/bin/dlv exec ./tmp/portfolio-backend --headless=true --listen=:2345 --api-version=2 --accept-multiclient"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms

[log]
# Show log time
time = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true
```

重要なのは下記の部分です。

```
root = "."
tmp_dir = "tmp"

cmd = "go build -o tmp/portfolio-backend ./cmd/rest/main.go"
bin = "tmp/portfolio-backend"
full_bin = "APP_ENV=dev APP_USER=air /go/bin/dlv exec ./tmp/portfolio-backend --headless=true --listen=:2345 --api-version=2 --accept-multiclient"
```

ファイルの変更が行われる度に `cmd` に設定されているBuildを実行し `delve` を再読み込みします。

# GoLand側の設定

## GoLandデバッグのデバッグ設定を作成する

`Run → Debug → 0. Edit Configurations...` から設定を作成します。

<img width="326" alt="createDebug1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71899/661784ba-071f-2ebd-4221-2414d6766ba8.png">

この記事の通りにやれば `delve` での利用ポートはデフォルトの2345なので `Go Remote` のTemplateを使って作成すれば良いです。

<img width="1067" alt="createDebug2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71899/df1f2e50-6d17-5e7a-b349-582f10dea405.png">

`Name` はこの記事では `app` としていますが、わかり易ければ何でも構いません。

## GoLand側からDockerコンテナに接続する

`Run → Debug →` から先程作成したデバッグ設定を開きます。

<img width="448" alt="ConnectRemoteServer1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71899/2a2ca027-2a39-d059-4a55-0736a0c62ccc.png">

下記のように表示されていれば、接続が完了しています。

<img width="704" alt="ConnectRemoteServer2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71899/13fe48cb-ba9f-3a3c-3b1a-c54d84743c83.png">

ちなみにDockerのログを見ると下記のようにアプリケーションの起動を確認出来るかと思います。

```
❯❯❯ docker logs -f bd1438fb171f

  __    _   ___
 / /\  | | | |_)
/_/--\ |_| |_| \_ v1.12.1 // live reload for Go apps, with Go1.14.0

mkdir /go/app/tmp
watching .
watching _sql
watching application
watching cmd
watching cmd/rest
watching config
watching docker
watching docker/go
watching docker/mysql
watching docker/mysql/config
watching docker/nginx
watching docker/nginx/config
watching domain
watching infrastructure
watching infrastructure/repository
watching k8s
watching test
watching test/data
watching test/data/memberscenario
watching test/data/memberscenario/fetchallfrommysql
watching test/data/memberscenario/fetchallfrommysql/succeed
watching test/data/memberscenario/fetchfrommysql
watching test/data/memberscenario/fetchfrommysql/succeed
watching test/data/webservicescenario
watching test/data/webservicescenario/fetchallfrommysql
watching test/data/webservicescenario/fetchallfrommysql/succeed
!exclude tmp
building...
running...
API server listening at: [::]:2345
2020/05/03 06:54:28 env: develop
2020/05/03 06:54:28 Starting app
```

## GoLandでブレークポイントを設定してデバッグ

ブレークポイントを設定してアプリケーションのURLにアクセスしましょう。

ブレークポイントでアプリケーションが一時停止し変数の中身等が確認出来るかと思います。

<img width="1067" alt="ConnectRemoteServer3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71899/9599e1d6-4964-52ff-843a-f3660b98612b.png">

※ アプリケーションのコードを修正した場合、GoLand側でDebuggerとのコネクションを再接続する必要があります。

<img width="666" alt="ReDebug.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71899/2c9ad636-8199-7af8-12cf-c8b59121df30.png">

# あとがき

ここまでのコードはGitHubに公開してあります。

https://github.com/nekochans/portfolio-backend

Debuggerが使えると開発効率が上がりますので、少々時間をかけてでも設定を行うことをオススメします。

以上になります。最後まで読んで頂きありがとうございます。