Live reload Go application running on local K8s using air and remote debug using delve from VSCode
kubernetesgolangLive 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"
}
]
}