コンテナで動くGoアプリをデバッグする方法
初めに
最近開発環境を整備していて、コンテナでGoアプリケーションをリモートでデバッグするのにちょっと面倒だったので、自分のメモも兼ねてやり方を残しておきます。
要件
プロジェクトによりますが、今の現場では主要エディタがVSCode
です。
もちろんVim
ユーザー(うち一人はぼく)がいるので、ターミナルでもデバッグできるように対応する必要があります。
また、コード変更を反映させるために都度イメージをビルドしてコンテナを再作成するは開発スピードが落ちるため、コンテナ再起動だけで反映されるようにする必要があります。
デバッガのしくみ
Goにはdelveというデバッガがあります。
delve
はClient-Serverモデルになっていて、次の2つのプロトコルで通信が可能になります。
JSON-RPC
はdelve
のCLIに使われています。
DAP
はMicrosoftが策定しているデバッグ用プロトコルで、実際にVSCode
のGo拡張はこれを使っています。
dlv-dapについて
もともとdelve
はDAP
に対応していなかったため、VSCode
はDAP
とJSON-RPC
を変換してくれるdlv-dap
でプロトコルの変換をしていました。
今はdelve
が標準でDAP
を対応しているため不要になりましたが、まだ実験的な段階のため不安定の場合は従来の構成を使うとよいと思います。
delve
の詳細なしくみは公式ドキュメントに記載されているこちらのスライドを参照してください。
アーキテクチャレベルで詳しく書かれています。
プロジェクト構成
簡単な例ですが、次のような構成とします。
skanehira@godzilla github.com/skanehira/go-remote-debug $ tree .
.
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yaml
├── go.mod
├── main.go
└── start.sh
Goのバージョンは次のとおりです。
skanehira@godzilla github.com/skanehira/go-remote-debug $ go version
go version go1.17.2 darwin/arm64
リモートデバッグ
デバッガの概要で説明したとおり、実際にVSCode
でコンテナにあるアプリケーションをデバッグするにはDAP
プロトコルを使う必要があります。
しかしVim
ユーザーもいるため、必要に応じて使うプロトコルを切り替えられるしくみを用意する必要があります。
次のようなスクリプトを用意して、それを含めたベースのコンテナイメージを作ります。
それぞれのファイルの中身は次のようになっています。
start.sh
#!/bin/bash
echo -e "mode: ${DEBUG}\nport: ${DEBUG_PORT}"
if [ "${DEBUG}" = "dap" ];then
go build -gcflags='all=-N -l' -o app
dlv dap -l 0.0.0.0:${DEBUG_PORT} --log --check-go-version=false
elif [ "${DEBUG}" = "rpc" ]; then
dlv debug --continue --check-go-version=false --accept-multiclient --headless -l 0.0.0.0:${DEBUG_PORT} main.go
else
go run main.go
fi
Dockerfile
FROM golang:1.17.3
COPY start.sh /usr/local/bin/start.sh
RUN chmod 775 /usr/local/bin/start.sh && go install github.com/go-delve/delve/cmd/dlv@latest
CMD ["start.sh"]
docker-compose.yaml
version: '3.8'
services:
app:
build:
context: .
# dlvをヘッドレスで起動する場合
# SIGINTで終了できないため強制終了させる
stop_signal: SIGKILL
ports:
- "8080:8080"
- "${DEBUG_PORT:-9998}:${DEBUG_PORT:-9998}"
volumes:
- $PWD:/dev/go-remote-debug
- type: volume
source: go_module_cache
target: /go/pkg/mod
working_dir: /dev/go-remote-debug
environment:
- "DEBUG=${DEBUG:-}"
- "DEBUG_PORT=${DEBUG_PORT:-9998}"
volumes:
go_module_cache:
main.go
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello Gorilla"))
})
log.Println("start http server :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
デバッガのポートとプロトコルを環境変数で切り替えられるようにしています。DEBUG=dap
の場合はDAP
、DEBUG=rpc
の場合はJSON-RPC
でサーバが起動します。
なお、ポートはコンテナ作成後に変えられないので、変更するときはコンテナの作り直しが必要です。
skanehira@godzilla github.com/skanehira/go-remote-debug $ DEBUG=dap docker compose up
[+] Running 1/0
⠿ Container go-remote-debug-app-1 Created 0.0s
Attaching to go-remote-debug-app-1
go-remote-debug-app-1 | mode: dap
go-remote-debug-app-1 | port: 9998
go-remote-debug-app-1 | 2021-11-26T15:12:56Z warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
go-remote-debug-app-1 | DAP server listening at: [::]:9998
VSCode
の場合、launch.json
は次のように設定する必要があります。
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug API Container",
"type": "go",
"debugAdapter": "dlv-dap",
"request": "launch",
"port": 9998,
"host": "localhost",
"mode": "exec",
"program": "/dev/go-remote-debug/app",
"substitutePath": [
{
"from": "${workspaceFolder}",
"to": "/dev/go-remote-debug"
}
]
}
]
}
ポイントとなる項目は次のとおりです。
項目 | 概要 |
---|---|
port |
デバッガのポート |
program |
コンテナ側にある実行ファイルのフルパス |
exec |
事前ビルドされたprogram を実行する |
substitutePath |
VSCode側のパスをコンテナ側のパスに置換 |
VSCode
でデバッグを開始するとコンテナ側のアプリケーションが起動します。
あとはブレークポイントを設置するなり好きにデバッグできます。
CLIでデバッグする場合はdlv connect host:port
でデバッグが可能になります。
コンテナにアタッチまたローカルから接続のどちらも利用可能です。
次のデモはコンテナにアタッチしてからdlv connect
してデバッグする例です。
最後に
DAP
でのデバッグはちょっとだけ手間ですが、これで要件は満たせるかなと思います。
同じことで困った方の助けになれればと思います。
記事で使用したコードはこちらにおいておきますので、ご自由に参照ください。
Discussion