Live reload Go application running on local K8s using air and remote debug using delve from VSCode

kubernetesgolang

Live reload using air

air is a tool that performs live reload for Go applications.

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

air init makes a settings file.

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

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

For server code as follows,

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()
}

running air, when it detects file changes with fsnotify, it builds code and restarts the process. With WSL and Docker for Mac, where support for inotify is not complete, some tools don’t work or some settings like ENTR_INOTIFY_WORKAROUND may be required, but air works without any special effort.

$ air

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

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

If you want to run it in a container, just mount the code and run air inside the container, and an image for that purpose is also provided.

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

Remote debug using delve

Build code with -gcflags “all=-N -l” to prevent from optimizing code, and run the binary with dlv exec to 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"
	...

Connecting to it with dlv connect, you can set breakpoints with break command.

$ 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)

Running on Kubernetes

Install delve in the air image.

$ 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 .

Mount the current directory for live reload. If the path embedded during the build in the container differs from the workspace path, the file will not be found and an error will occur when setting a breakpoint from the VSCode editor, so I set the mount destination same as the current directory. You may set substitutePath in the launch settings instead.

$ 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 -

Debugging on VSCode

Launch settings were generated on the first debug.

$ 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"
        }
    ]
}

References

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