ローカルのK8s上でairによるGoのアプリケーションのライブリロードを行いVSCodeからdelveでリモートデバッグする

kubernetesgolang

air によるライブリロード

air は Go のアプリケーションをライブリロードするツール。

$ go install github.com/cosmtrek/air@latest

air init すると設定ファイルが作成される。

$ air init
$ cat .air.toml 
root = "."
tmp_dir = "tmp"

[build]
  args_bin = []
  bin = "./tmp/main"
  cmd = "go build -o ./tmp/main ."
  delay = 1000
...

次のようなサーバーのコードに対して

package main

import (
	"context"
	"log"
	"net/http"
	"os/signal"
	"syscall"
)

func pingHandler(w http.ResponseWriter, r *http.Request) {
	_, err := w.Write([]byte("pong"))
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	http.HandleFunc("/ping", pingHandler)

	go func() {
		log.Print("Listening on port 5051")
		log.Fatal(http.ListenAndServe("0.0.0.0:5051", nil))
	}()

	ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	<-ctx.Done()
}

air を実行すると fsnotify でファイルの変更を検知してビルドし再起動してくれる。 inotify のサポートが完全でない WSL や Docker for Mac では、動かないツールや ENTR_INOTIFY_WORKAROUND のような設定が必要だったりすることもあるが、air は特に何かすることなく動いた。

$ air

  __    _   ___  
 / /\  | | | |_) 
/_/--\ |_| |_| \_ v1.49.0, built with Go go1.20.3

watching .
!exclude tmp
building...
running...
main.go has changed
building...
running...

コンテナで動かす場合はコードをマウントしてコンテナ内でairを実行すればよく、そのためのイメージも提供されている。

$ docker run -it --rm \
    -w "/app" \
    -e "air_wd=/app" \
    -v $(pwd):/app \
    -p 5051:5051 \
    cosmtrek/air

delve によるリモートデバッグ

最適化されないように -gcflags “all=-N -l” を渡してビルドしたバイナリを dlv exec で実行し listen のポートで待ち受ける。

$ go install -v github.com/go-delve/delve/cmd/dlv@latest
$ cat .air.toml
...
[build]
	cmd = "go build -gcflags \"all=-N -l\" -o tmp/main ."
  full_bin = "dlv exec --headless --listen=:12345 --accept-multiclient --continue ./tmp/main"
	...

これに対して dlv connect で接続し break でブレークポイントを設定したりできる。

$ dlv connect 127.0.0.1:12345
(dlv) break /app/main.go:12
Breakpoint 1 set at 0x308068 for main.pingHandler() /app/main.go:12

(dlv) breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x4d2a0,0x6473c,0x4d360 for (multiple functions)() <multiple locations>:0 (0)
Breakpoint unrecovered-panic (enabled) at 0x4d780 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1217 (0)
        print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x308068 for main.pingHandler() /app/main.go:12 (3)

(dlv) continue
> main.pingHandler() /app/main.go:12 (hits goroutine(82):2 total:2) (PC: 0x308068)

(dlv) args
w = net/http.ResponseWriter(*net/http.response) 0xbeef000000000008
r = ("*net/http.Request")(0x4000148b40)

Kubernetes での実行

air のイメージに delve をインストールする。

$ cat Dockerfile_dev
FROM cosmtrek/air:v1.50.0

RUN go install github.com/go-delve/delve/cmd/dlv@latest

$ docker build -t dev_testapp -f Dockerfile_dev .

ライブリロードできるようにカレントディレクトリをマウントする。コンテナ内でビルド時に埋め込まれるパスがワークスペースのパスと異なると VSCodeのエディタ上からブレークポイントを設定したときにファイルが見つからずエラーになるためマウント先を合わせている。起動設定の substitutePath を設定してもよい。

$ cat manifest.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: testapp-deployment
  labels:
    app: testapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: testapp
  template:
    metadata:
      labels:
        app: testapp
    spec:
      containers:
      - name: testapp
        image: dev_testapp
        imagePullPolicy: Never
        workingDir: PATH_PLACEHOLDER
        volumeMounts:
        - name: host-volume
          mountPath: PATH_PLACEHOLDER
        ports:
        - containerPort: 5051
        - containerPort: 12345 # delve
      volumes:
      - name: host-volume
        hostPath:
          path: PATH_PLACEHOLDER
          type: Directory
---
apiVersion: v1
kind: Service
metadata:
  name: testapp-service
spec:
  type: NodePort
  selector:
    app: testapp
  ports:
    - name: app
      port: 5051
      targetPort: 5051
      nodePort: 30010
    - name: delve
      port: 12345
      targetPort: 12345
      nodePort: 30011

$ sed "s|PATH_PLACEHOLDER|${PWD}|g" manifest.yaml | kubectl apply -f -

VSCode でのデバッグ

起動設定はデバッグ実行時に自動で作られた。

$ cat .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Connect to server",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "remotePath": "${workspaceFolder}",
            "port": 30011,
            "host": "127.0.0.1"
        }
    ]
}

参考

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