|
1 |
| -# debug-k8s |
2 |
| -How to debug a go-service in kubernetes |
| 1 | +# Preface |
| 2 | + |
| 3 | +In a perfect world every written service will work smooth, your test coverage is on top and there are no bugs in the implementation of your API. But we all know, that we can't achieve this world, sadly. It's not unusual that there's a bug and you have to debug this problem in a production environment. We have faced this problem with our go services in our Kubernetes cluster, and we want to show you how it's possible to remote debug a go service in a Kubernetes cluster. |
| 4 | + |
| 5 | +## Software Prerequisites |
| 6 | + |
| 7 | +For this case we create a kubernetes cluster running locally on our system. Therefore we need the following software: |
| 8 | + |
| 9 | +* [Docker Desktop](https://docs.docker.com/get-docker) (used version: 19.03.8) |
| 10 | +* [kind (Kubernetes in Docker)](https://kind.sigs.k8s.io) (used version: v0.7.0) |
| 11 | +* [Kubectl](https://kubernetes.io/de/docs/tasks/tools/install-kubectl) (used version: 1.17.2) |
| 12 | +* [Visual Studio Code](https://code.visualstudio.com/download) (used version: 1.32.3) |
| 13 | + |
| 14 | +We decided to use `kind` instead of `minikube`, since it's a very good tool for testing Kubernetes locally, and we can use our docker images without a docker registry. |
| 15 | + |
| 16 | +## Big Picture |
| 17 | + |
| 18 | +First we will briefly explain how it works. We start by creating a new Kubernetes cluster `local-debug-k8s` on our local system. |
| 19 | + |
| 20 | +* You need a docker container with [delve](https://github.com/go-delve/delve) (the go debugger) as the main process. |
| 21 | +* The debugger delve needs access to the path with the project data. This is done by mounting `$GOPATH/src` on the pod which is running in the Kubernetes cluster. |
| 22 | +* We start the delve container on port 30123 and bind this port to localhost, so that only our local debugger can communicate with delve. |
| 23 | +* To debug an API with delve, it's necessary to set up an ingress network. For this we use port 8090. |
| 24 | + |
| 25 | +A picture serves to illustrate the communication: |
| 26 | + |
| 27 | + |
| 28 | + |
| 29 | +### Creating a Kubernetes cluster |
| 30 | + |
| 31 | +`kind` unfortunately doesn't use the environment variable `GOPATH`, so we have to update this in [config.yaml](cluster/config.yaml#L21): |
| 32 | + |
| 33 | +```sh |
| 34 | +sed -i.bak 's|'{GOPATH}'|'${GOPATH}'|g' cluster/config.yaml |
| 35 | +``` |
| 36 | + |
| 37 | +You can also open [config.yaml](cluster/config.yaml#L21) and replace `{GOPATH}` with the absolute path manually. If you already installed kind (Kubernetes in Docker) on your local system, you can create the cluster with this command: |
| 38 | + |
| 39 | +```sh |
| 40 | +kind create cluster --config cluster/config.yaml --name=local-debug-k8s |
| 41 | +``` |
| 42 | + |
| 43 | +Ensure that port 8090 and 30123 are not used on your local system. The newly created cluster has the name `local-debug-k8s` and has been created with custom configuration ( `--config cluster/config.yaml`). The following is a brief explanation: |
| 44 | + |
| 45 | +```yml |
| 46 | +kind: Cluster |
| 47 | +apiVersion: kind.x-k8s.io/v1alpha4 |
| 48 | +nodes: |
| 49 | +- role: control-plane |
| 50 | + kubeadmConfigPatches: # necessary, since we are going to install an ingress network in the cluster |
| 51 | + - | |
| 52 | + kind: InitConfiguration |
| 53 | + nodeRegistration: |
| 54 | + kubeletExtraArgs: |
| 55 | + node-labels: "ingress-ready=true" |
| 56 | + authorization-mode: "AlwaysAllow" |
| 57 | + extraPortMappings: |
| 58 | + - containerPort: 80 # http endpoint of ingress runs on the port 80 |
| 59 | + hostPort: 8090 # port on your host machine to call API's of the service |
| 60 | + protocol: TCP |
| 61 | + - containerPort: 30123 # node port for the delve server |
| 62 | + hostPort: 30123 # port on your host machine to communicate with the delve server |
| 63 | + protocol: TCP |
| 64 | +- role: worker |
| 65 | + extraMounts: |
| 66 | + - hostPath: {GOPATH}/src # ATTENTION: you might want to replace this path with your ${GOPATH}/src manually |
| 67 | + containerPath: /go/src # path to the project folder inside the worker node |
| 68 | +``` |
| 69 | +
|
| 70 | +Expected result: |
| 71 | +
|
| 72 | +```sh |
| 73 | +Creating cluster "local-debug-k8s" ... |
| 74 | +✓ Ensuring node image (kindest/node:v1.17.0) 🖼 |
| 75 | +✓ Preparing nodes 📦 📦 |
| 76 | +✓ Writing configuration 📜 |
| 77 | +✓ Starting control-plane 🕹️ |
| 78 | +✓ Installing CNI 🔌 |
| 79 | +✓ Installing StorageClass 💾 |
| 80 | +✓ Joining worker nodes 🚜 |
| 81 | + |
| 82 | +Set kubectl context to "kind-local-debug-k8s" |
| 83 | +You can now use your cluster with: |
| 84 | + |
| 85 | +kubectl cluster-info --context kind-local-debug-k8s |
| 86 | + |
| 87 | +Have a nice day! 👋 |
| 88 | +``` |
| 89 | + |
| 90 | +Activate the kube-context for `kubectl` to communicate with the new cluster: |
| 91 | + |
| 92 | +```sh |
| 93 | +kubectl cluster-info --context kind-local-debug-k8s |
| 94 | +``` |
| 95 | + |
| 96 | +#### Install nginx-ingress |
| 97 | + |
| 98 | +For both ports (8090 and 30123) to work, it is necessary to deploy an nginx controller as an ingress controller: |
| 99 | + |
| 100 | +```sh |
| 101 | +kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/kind/deploy.yaml |
| 102 | +``` |
| 103 | + |
| 104 | +Source: [kind documentation](https://kind.sigs.k8s.io/docs/user/ingress/#ingress-nginx>) |
| 105 | + |
| 106 | +to observe the current status the following command can be executed: |
| 107 | + |
| 108 | +```sh |
| 109 | +kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=120s |
| 110 | +``` |
| 111 | + |
| 112 | +#### Labelling the node |
| 113 | + |
| 114 | +We know that by default a kubernetes cluster will deploy a pod on a node which has enough ressources for this workload. Our docker image must be pulled on all nodes in our kubernetes cluster in order to be ready as quickly as possible. This process may take a long time. If the docker image isn't pulled on a node and a new pod will provisioned on this node, it will take more time to get ready and healthy. |
| 115 | +For our use case we will label a node in our kubernetes cluster so that always this node will be used. |
| 116 | + |
| 117 | +We label a node with _debug=true_: |
| 118 | + |
| 119 | +```sh |
| 120 | +kubectl label nodes local-debug-k8s-worker debug=true |
| 121 | +``` |
| 122 | + |
| 123 | +### Creating a docker image |
| 124 | + |
| 125 | +Our service has only one endpoint `/hello` and writes just a few logs. Let's checkout the Dockerfile for delve: |
| 126 | + |
| 127 | +```Dockerfile |
| 128 | +FROM golang:1.13-alpine |
| 129 | + |
| 130 | +# compile gcc statically |
| 131 | +ENV CGO_ENABLED=0 |
| 132 | +ENV GOROOT=/usr/local/go |
| 133 | +# this path will be mounted in deploy-service.yaml |
| 134 | +ENV GOPATH=${HOME}/go |
| 135 | +ENV PATH=$PATH:${GOROOT}/bin |
| 136 | + |
| 137 | +# Install git and get the latest version of delve via go |
| 138 | +RUN apk update && apk add --no-cache \ |
| 139 | + git && \ |
| 140 | + go get github.com/go-delve/delve/cmd/dlv |
| 141 | + |
| 142 | +# ATTENTION: you want to check, if the path to the project folder is the right one here |
| 143 | +WORKDIR /go/src/github.com/setlog/debug-k8s |
| 144 | + |
| 145 | +# 30123 for delve and 8090 for API calls |
| 146 | +EXPOSE 30123 8090 |
| 147 | + |
| 148 | +# let's start delve as the entrypoint |
| 149 | +ENTRYPOINT ["/go/bin/dlv", "debug", ".", "--listen=:30123", "--accept-multiclient", "--headless=true", "--api-version=2"] |
| 150 | +``` |
| 151 | + |
| 152 | +So, let's build build our docker image from our [Dockerfile](Dockerfile): |
| 153 | + |
| 154 | +```sh |
| 155 | +docker build -t setlog/debug-k8s ./Dockerfile |
| 156 | +``` |
| 157 | + |
| 158 | +After the build is done, we load the image `setlog/debug-k8s:latest` on the node _local-debug-k8s-worker_: |
| 159 | + |
| 160 | +```sh |
| 161 | +kind load docker-image setlog/debug-k8s:latest --name=local-debug-k8s --nodes=local-debug-k8s-worker |
| 162 | +``` |
| 163 | + |
| 164 | +A message appears indicating that the docker image did not exist before: |
| 165 | + |
| 166 | +```sh |
| 167 | + Image: "setlog/debug-k8s:latest" with ID "sha256:944baa03d49698b9ca1f22e1ce87b801a20ce5aa52ccfc648a6c82cf8708a783" not present on node "local-debug-k8s-worker" |
| 168 | +``` |
| 169 | + |
| 170 | +### Deploy the delve container in our cluster |
| 171 | + |
| 172 | +First of all we need a persistent volume and its claim in order to mount the project path into the node: |
| 173 | + |
| 174 | +```sh |
| 175 | +kubectl create -f cluster/persistent-volume.yaml |
| 176 | +``` |
| 177 | + |
| 178 | +The interesting part here is: |
| 179 | + |
| 180 | +```yaml |
| 181 | + hostPath: |
| 182 | + path: /go/src |
| 183 | +``` |
| 184 | +
|
| 185 | +Below is an image that shows the configurations through which our local path is mounted. In your environment it could be another path: |
| 186 | +
|
| 187 | + |
| 188 | +
|
| 189 | +Check, if your persistent volume claim has been successfully created (STATUS must be Bound): |
| 190 | +
|
| 191 | +```sh |
| 192 | +kubectl get pvc |
| 193 | + |
| 194 | + NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE |
| 195 | + go-pvc Bound go-pv 256Mi RWO hostpath 51s |
| 196 | +``` |
| 197 | + |
| 198 | +Now we are ready to deploy all our services in debug mode: |
| 199 | + |
| 200 | +```sh |
| 201 | +kubectl create -f cluster/deploy-service.yaml |
| 202 | +``` |
| 203 | + |
| 204 | +Let's go through the pod manifest: |
| 205 | + |
| 206 | +* _image_ is the previously built and loaded image into the kind cluster with `kind load image...` |
| 207 | +* _imagePullPolicy_ must be set to _IfNotPresent_ because it's already loaded and we don't want Kubernetes to try it again |
| 208 | + |
| 209 | +```yaml |
| 210 | + image: setlog/debug-k8s:latest |
| 211 | + imagePullPolicy: IfNotPresent |
| 212 | +``` |
| 213 | +
|
| 214 | +* We use the persistent volume claim to mount the project path into the pod, so that `/go/src` will be linked to `${GOPATH}/src` on your local system |
| 215 | + |
| 216 | +```yaml |
| 217 | + containers: |
| 218 | + - name: debug-k8s |
| 219 | + ... |
| 220 | + volumeMounts: |
| 221 | + - mountPath: /go/src |
| 222 | + name: go-volume |
| 223 | + volumes: |
| 224 | + - name: go-volume |
| 225 | + persistentVolumeClaim: |
| 226 | + claimName: go-pvc |
| 227 | +``` |
| 228 | + |
| 229 | +* As there might be several nodes in your kubernetes cluster, we deploy the pod on the node, that is labelled with _debug=true_. The docker image _setlog/debug-k8s_ was already loaded on this node. |
| 230 | + |
| 231 | +```yaml |
| 232 | + nodeSelector: |
| 233 | + debug: "true" |
| 234 | +``` |
| 235 | + |
| 236 | +* Service _service-debug_ has the type _NodePort_ and is mounted to the node. This port 30123 is equal to the parameter _--listen=:30123_ in the Dockerfile, which makes it possible to send debug commands to the delve server. |
| 237 | + |
| 238 | +* Service _debug-k8s_ will be connected to the ingress server in the final step. It serves for exposing the API endpoints we are going to debug. |
| 239 | + |
| 240 | +If you did all steps correctly, your pod should be up and running. Check it with `kubectl get pod`. You should see the output with the pod status _Running_ and two additional services _debug-k8s_ and _service-debug_: |
| 241 | + |
| 242 | +```sh |
| 243 | +NAME READY STATUS RESTARTS AGE |
| 244 | +pod/debug-k8s-6d69b65cf-4fl6t 1/1 Running 0 1h |
| 245 | +
|
| 246 | +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE |
| 247 | +service/debug-k8s ClusterIP 10.96.80.193 <none> 8090/TCP 1h |
| 248 | +service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 1h |
| 249 | +service/service-debug NodePort 10.96.219.86 <none> 30123:30123/TCP 1h |
| 250 | +``` |
| 251 | + |
| 252 | +_Hint: create a new variable to store the pod name using `PODNAME=$(kubectl get pod -o jsonpath='{.items[0].metadata.name}')`. It can be helpful, if you repeatedly debug the pod._ |
| 253 | + |
| 254 | +Usually it takes a couple of seconds to start the debugging process with delve. If your paths are mounted in the proper way, you will find the file `__debug_bin` in the project path on your computer. That is an executable which has been created by delve. |
| 255 | + |
| 256 | +Also, you can output logs of the pod by performing `kubectl logs $PODNAME` in order to make sure the delve API server is listening at 30123. |
| 257 | + |
| 258 | +Output: |
| 259 | + |
| 260 | +```sh |
| 261 | + API server listening at: [::]:30123 |
| 262 | +``` |
| 263 | + |
| 264 | +_Hint: always wait until this log message is shown for this pod before you start the debugging process. Otherwise, the delve server is not up yet and cannot answer to the debugger._ |
| 265 | + |
| 266 | +### Starting the debug process via launch.json |
| 267 | + |
| 268 | +Now we need a debug configuration in Visual Studio Code. This can be done in `.vscode/launch.json`: |
| 269 | + |
| 270 | +```json |
| 271 | +{ |
| 272 | + "version": "0.2.0", |
| 273 | + "configurations": [ |
| 274 | + { |
| 275 | + "name": "Remote debug in Kubernetes", |
| 276 | + "type": "go", |
| 277 | + "request": "attach", |
| 278 | + "mode":"remote", |
| 279 | + "remotePath": "/go/src/github.com/setlog/debug-k8s", |
| 280 | + "port": 30123, |
| 281 | + "host": "127.0.0.1", |
| 282 | + "showLog": true |
| 283 | + } |
| 284 | + ] |
| 285 | +} |
| 286 | +``` |
| 287 | + |
| 288 | +Where `remotePath` is the path to the project path inside the pod, `port` the local port to send the debug commands to, and `host` the host to send the debug commands to. |
| 289 | + |
| 290 | +You can find the new configuration in Visual Studio Code here: |
| 291 | + |
| 292 | + |
| 293 | + |
| 294 | +After starting the debug process there is a new log created by the go service: |
| 295 | + |
| 296 | +```sh |
| 297 | + 2020/05/28 15:38:53 I am going to start... |
| 298 | +``` |
| 299 | + |
| 300 | +Finally we are ready to debug the service, but we have to trigger the API functions through the ingress service. Deploy it with kubectl: |
| 301 | + |
| 302 | +```sh |
| 303 | +kubectl create -f cluster/ingress.yaml |
| 304 | +``` |
| 305 | + |
| 306 | +And try accessing it now: |
| 307 | + |
| 308 | +```sh |
| 309 | +curl http://localhost:8090/hello |
| 310 | +``` |
| 311 | + |
| 312 | +Which should trigger the debugger: |
| 313 | + |
| 314 | + |
| 315 | + |
| 316 | +Happy debugging! |
| 317 | + |
| 318 | +### Cleaning up |
| 319 | + |
| 320 | +If you don't need your kind cluster anymore, you can remove this with the following command: |
| 321 | + |
| 322 | +```sh |
| 323 | +kind delete cluster --name=local-debug-k8s |
| 324 | +``` |
0 commit comments