goland + air + delveでdocker環境のリモートデバッグ

こんにちは、トレタでサーバーサイドエンジニアをしている神山と申します。

こちらはトレタアドベントカレンダー12日目の記事です。
この記事ではdocker-compose環境におけるgolangのリモートデバッグについて記載したいと思います。

# 概要
docker-composeを使ってgolangの環境を構築し、そこでapplicationを動作させています。
既にairのホットリロードにより差分をすぐ反映することができるため、そこまで気にせず開発を進めていました。
ですがサービスのコード量や複雑な要件が増えるにつれて、デバッグが徐々に辛くなってきました。
そこでデバッグできるようにdelveを導入してみました。

# 環境
- MacBook Pro(Apple M1)
- go version go1.19.4 linux/arm64
- Docker Compose version v2.13.0
- air v1.40.4
- delve v1.20.1

# docker-compose.yml

docker-composeファイルはこのように書きました。

```yml
version: '3.7'

services:
  app:
    image: 'golang:1.19'
    working_dir: '/app'
    volumes:
      - type: bind
        source: '.'
        target: '/app'
    ports:
      - '8080:8080'
      - '2345:2345'
    command: >
      sh -c '
        go install github.com/go-delve/delve/cmd/dlv@latest
        go install github.com/cosmtrek/air@latest
        air -c .air.toml
      '
```

- imageはそのままgolangの1.19を指定しています。
- working_dirにソースコードをbindしています。
- portsは二箇所開けています。
    - api用の8080
    - リモートデバッグ接続用の2345
- commandでdocker-composeをbuildするタイミングでdelveとairををinstall、airを起動するようにしています。
    - 本記事では触れませんが、本番環境用に別でDockerfileを用意しています。
    - 本番環境ではdelve, airともにinstallしたくないのでここに記載する形にしました。

# .air.toml

airを使っている場合、tomlファイルを作成しているかと思います。
編集したのは下記に記載の部分です。他は変えていません。

```toml
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -gcflags \"all=-N -l\" -o ./.air/main ./cmd/main.go"
# Binary file yields from `cmd`.
bin = "./.air/main"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air dlv exec ./.air/main --headless=true --listen=:2345 --api-version=2 --accept-multiclient"
```

- **cmd**では、`./cmd/main.go`をbuildして、`./.air/main`にバイナリを配置しています。
- **bin**では、あらためてバイナリの位置を記載しています。
- **full_bin**では、airコマンドを使ってdelve用のserverを起動します。

この状態で`docker-compose up`をするとターミナルに下記のようなログが表示されるはずです。

```bash
app  | API server listening at: [::]:2345
app  | 2022-12-19T10:16:59Z warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
```

# リモートデバッグの接続
- golandの右上から`Edit Configuration`を選択します。
- `+`ボタンからGo Remoteを選択してください。
- Nameは適当な名前を入れてください。
- Hostはlocalhost, Portは2345を指定してください。
- `Leave it runnning`にチェックを入れて保存してください。
 
![スクリーンショット 2022-12-19 19.24.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434850/b05fac79-b57d-c806-e93f-d0eec3d3a636.png)

- 下記のような表示になっているはずなので、デバッグマークを押してください。

![スクリーンショット 2022-12-19 19.27.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434850/208f4424-2bd9-a7b3-e322-ca854aa831b7.png)

- 問題なければ下記のように`Connected`が表示されるはずです。

![スクリーンショット 2022-12-19 19.28.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434850/0e67fe17-91b2-f9a9-26c7-9a50ac51819c.png)

# デバッグを試してみる

本番のソースコードとは全く違うのですが、こんなmain関数を用意しました。
`GET http://localhost:8080/hello`でアクセスするとレスポンスが返ってくるだけです。


```go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", helloHandler)
	http.ListenAndServe(":8080", nil)
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	test := "Hello Remote Debug!!"
	fmt.Fprint(w, test)
}

```

実際にブレイクポイントを置いてAPIを叩いてみます。
下部のデバッグコンソールで、testの変数値などが見えているかと思います。
ソースコードを変更して保存すれば、ホットリロードも有効になっています。

![スクリーンショット 2022-12-19 19.31.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434850/c20a2140-1655-17a2-6c02-5d9116606126.png)

# 注意点

どうやら**M1 Macでplatformにlinux/amd64を指定したdocker環境ではdelveは使えない**ようです。
どういうことかというと、docker-compose.ymlを下記のように書き換えました。
`golang1.19`は`linux/amd64`に対応していないimageのようだったので、`golang:1.19-buster`に書き換えています。

```diff_yaml
version: '3.7'

services:
  app:
+   platform: 'linux/amd64'
+   image: 'golang:1.19-buster'
-   image: 'golang:1.19'
    working_dir: '/app'
    volumes:
      - type: bind
        source: '.'
        target: '/app'
    ports:
      - '8080:8080'
      - '2345:2345'
    command: >
      sh -c '
        go install github.com/go-delve/delve/cmd/dlv@latest
        go install github.com/cosmtrek/air@latest
        air -c .air.toml
      '
```

他の設定は全てそのままに`docker-compose up`を実行します。
起動はできるものの `could not launch process: fork/exec /app/.air/main: function not implemented`と言われてしまいました。

```bash
app  | API server listening at: [::]:2345
app  | 2022-12-19T10:37:43Z warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
app  | could not launch process: fork/exec /app/.air/main: function not implemented
```

delveのissueを調べてみたらありました。
https://github.com/go-delve/delve/issues/2910

> Nothing we can do anything about.

コメントのラリーもなくcloseされていました・・。無慈悲。

# さいごに
やや中途半端ですが、delveを入れてリモートデバッグを実現しました。
本番環境のDockerfileでは`linux/amd64`で`golang:1.19-buster`を使用しているので、このデバッグ環境100%信用して使うべきではないと思っています。
とはいえ、例えばチームメンバーからレビューが上がってきたときや複雑なロジックを書くときなどはデバッグがないと辛い・・・
そんな時は少しだけdocker-compose.ymlと.air.tomlを書き換えるだけでリモートデバッグ環境を構築することができるので、重宝しています。

特にgolangはfor文など愚直に書くことが多い言語なので、開発体験を整備すると効率があがりますね。。

なお、トレタではエンジニアの募集を行なっております。
興味がある方はぜひご連絡ください。

https://corp.toreta.in/recruit/engineer/