🦍

コンテナで動くGoアプリをデバッグする方法

2021/11/27に公開

初めに

最近開発環境を整備していて、コンテナでGoアプリケーションをリモートでデバッグするのにちょっと面倒だったので、自分のメモも兼ねてやり方を残しておきます。

要件

プロジェクトによりますが、今の現場では主要エディタがVSCodeです。
もちろんVimユーザー(うち一人はぼく)がいるので、ターミナルでもデバッグできるように対応する必要があります。
また、コード変更を反映させるために都度イメージをビルドしてコンテナを再作成するは開発スピードが落ちるため、コンテナ再起動だけで反映されるようにする必要があります。

デバッガのしくみ

Goにはdelveというデバッガがあります。
delveはClient-Serverモデルになっていて、次の2つのプロトコルで通信が可能になります。

  1. JSON-RPC
  2. DAP

JSON-RPCdelveのCLIに使われています。
DAPはMicrosoftが策定しているデバッグ用プロトコルで、実際にVSCodeのGo拡張はこれを使っています。


https://github.com/golang/vscode-go/blob/master/docs/debugging.md#go-debug-extension-architecture-overview

dlv-dapについて

もともとdelveDAPに対応していなかったため、VSCodeDAPJSON-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の場合はDAPDEBUG=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