はじめに
Kubernetesクラスタに自分で開発したGoアプリケーションをディプロイし、それに対してVisual Studio Codeからリモートデバッグする際の備忘録 (小ネタ)。つまりコンテナ内で実行されるGoアプリケーションをDelveにより起動しリモートからデバッグ作業を行う。
例えばKubernetesクラスタ環境内に既に動作している他のコンポーネントとの連携動作を含めてデバッグしたい場合やDeoployment定義により構成された環境(ボリュームのマウント構成など)でデバッグを実施したい場合などを想定している。
利用するソフトウェア
- VMware Workstation 17 Pro (Windows 11 / X86_64)
- RHEL 9.5 (VM)
- Kubernetes (v1.32)
- Harbor (2.13.0)
- Rook-Ceph (v1.17.1)
(参考) テストアプリケーションの作成環境
- go (1.23.6)
- Delve Debugger (1.24.1)
- Podman (5.2.2)
- Visual Studio Code (1.100.0)
- Visual Studio Code拡張: Go (0.46.1)
- Visual Studio Code拡張: Remote Development (0.26.0)
- Visual Studio Code拡張: Remote Explorer (0.5.0)
- Visual Studio Code拡張: Dev Containers (0.413.0)
- Visual Studio Code拡張: YAML (1.18.0)
- コンテナ: ubi9/go-toolset (1.23)
- コンテナ: ubi9 (latest)
Kubernetesクラスタ構成
以下の前回の記事で構築済みの環境を利用する。
リモートデバッグ接続
VMWare WorkstationのホストOS (Windows11)上のVisual Studio Code (Remote Development/Remote - SSH拡張)から作業する。そこより各ワーカーノード上のGoアプリケーションPod上のDelveへ(Kubernetes ServiceのVIP経由で)アクセスしリモートデバッグする。
利用するテストアプリケーション
テストアプリ名 | エンドポイント | API | 説明 |
---|---|---|---|
k8s-test-app | 前回作成したテストアプリケーション。単純なAPIをエキスポートする | ||
hxxps://k8s-test-app.test.k8s.local | GET /ping | ping要求に対してpong応答(json)を返すAPI |
k8s-test-appの構成定義
項目 | タイプ | Volumeマウント先 |
---|---|---|
TLS: サーバ証明書 | Secret | /app/certs/tls.crt |
TLS: サーバ秘密鍵 | Secret | /app/certs/tls.key |
API-KEY | Secret | /app/secret/testapp-secret.yaml |
アクセスログ | PersistentVolumeClaim (Ceph File System) |
/mnt/cephfs/k8s-test-app/log/history.log |
開発VMのディレクトリ構成 - k8s-test-appアプリケーション
前回の記事で作成したk8s-test-appアプリを対象にしてみる。mng.test.k8s.local(RHEL9.5)で作業する。
/home/hoge/k8s-test-app/
├── Containerfile # 通常ビルド用
├── Containerfile.dlv # デバッグビルド用
│
├── main.go # Goソースコード
├── test-app # Exeファイル
├── go.mod
├── go.sum
│
├── openssl.cnf # TLS証明書設定 (OpenSSL)
│
├── k8s-test-app-deploy.yaml # kubernetes: Deployment定義
├── k8s-test-app-deploy-dlv.yaml # kubernetes: Deployment定義 (デバッグ用)
├── k8s-test-app-svc.yaml # kubernetes: Service定義
├── k8s-test-app-svc-dlv.yaml # kubernetes: Service定義 (デバッグ用)
├── k8s-test-app-api-key.yaml # kubernetes: Secret定義
├── k8s-test-app-tls-secret.yaml # kubernetes: Secret定義
│
└── podman # Podmanランタイム用のローカルボリューム
│
├── certs/ # TLS証明書(読み取り専用でマウント)
│ ├── tls.crt # サーバ証明書(PEM形式)
│ └── tls.key # サーバ秘密鍵(PEM形式)
│
├── secret/ # 認証用APIキー(読み取り専用でマウント
│ └── testapp-secret.yaml # APIキー定義ファイル
│
└── cephfs/
└── k8s-test-app/
└── log/ # アクセスログ保存先(書き込み可能でマウント)
準備: ローカルでデバッグ - Podman runtime
作成したGoアプリケーションのコンテナイメージをPodmanによりビルドおよびそれをローカルのコンテナランタイムで起動し、リモート接続でデバッガーを利用して作業してみる。
リモートからデバッグするためにDelveを同梱したデバッグ用のビルドイメージを用意する。
#
# Stage 1: Build
#
FROM registry.access.redhat.com/ubi9/go-toolset:1.23 AS builder
USER root
WORKDIR /app
# ビルドに必要な依存モジュールを取得
COPY go.mod go.sum ./
RUN go mod download
# ソースコードなどをコピー
COPY . .
# Delveをビルド環境へインストール
RUN go install github.com/go-delve/delve/cmd/dlv@latest
# RUN find / -print |grep dlv
# RUN echo "==== /usr/bin/dlv ====" && ls -l /usr/bin/dlv
# デバッグ用にアプリケーションをビルド
RUN go build -gcflags="all=-N -l" -o app .
#
# Stage 2: Runtime
#
FROM registry.access.redhat.com/ubi9
# トラブルシューティング用ツールを入れておく
RUN dnf install -y \
shadow-utils \
ca-certificates \
procps-ng \
iproute \
iputils \
vim \
bind-utils \
tcpdump \
nc \
jq \
&& dnf clean all
WORKDIR /app
# ビルドしたアプリケーション実行ファイルをコピー
COPY --from=builder /app/app .
# Delve実行ファイルをコピー
COPY --from=builder /usr/bin/dlv /usr/local/bin/dlv
EXPOSE 8443 # API用ポート (/ping)
EXPOSE 2345 # Delveポート
# Delveを起動
CMD ["dlv", "exec", "/app/app", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient"]
[mng k8s-test-app]$ podman build -t k8s-test-app:debug -f Containerfile.dlv .
[mng k8s-test-app]$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
...
localhost/k8s-test-app debug 71dad901fadf 3 hours ago 320 MB
...
※ イメージ名に"debug"タグをつけて区別するようにする。
[mng k8s-test-app]$ podman run -it -p 8443:8443 -p 2345:2345 -v /home/hoge/k8s-test-app/podman/certs:/app/certs:ro -v /home/hoge/k8s-test-app/podman/secret:/app/secret:ro -v /home/hoge/k8s-test-app/podman/cephfs/k8s-test-app/log:/mnt/cephfs/k8s-test-app/log k8s-test-app:debug
API server listening at: [::]:2345
2025-05-05T10:43:56Z warn layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
VSCodeからの接続待ち...
[mng k8s-test-app]$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8292797caf0c localhost/k8s-test-app:debug dlv exec /app/app... 2 minutes ago Up 2 minutes 0.0.0.0:2345->2345/tcp, 0.0.0.0:8443->8443/tcp, 2345/tcp, 8443/tcp lucid_swartz
# リモートからコンテナへシェルで接続
[mng k8s-test-app]$ podman exec -it lucid_swartz /bin/bash
[root@8292797caf0c app]#
[root@8292797caf0c app]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 2.9 6104168 52428 pts/0 Ssl 10:43 0:00 dlv exec /app/app --headless --listen=:2345 --api-version=2 --accept-multiclient
...
root 14 0.0 0.0 10536 128 pts/0 t+ 10:43 0:00 /app/app
...
# まだAPI用のポート(8443)がオープンでない = アプリケーションが開始していない
[root@8292797caf0c app]# ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 *:2345 *:*
{
"version": "0.2.0",
"configurations": [
{
"name": "Remote Attach to dlv on Podman",
"type": "go",
"request": "attach",
"mode": "remote",
"host": "127.0.0.1", # コンテナ側のIP
"port": 2345, # コンテナ側のDelveポート
"cwd": "/home/hoge/k8s-test-app", # ソースコードのディレクトリ (ホスト側)
"remotePath": "/app", # コンテナ側の実行ファイルのディレクトリ
"apiVersion": 2,
"trace": "verbose", # DelveとVSCode間の通信ログを表示
"showLog": true # デバッグアダプタのログ出力を有効化
}
]
}
デバッガーを開始しアプリケーションの起動部分(main関数)でブレークポイント設定してみる。
[root@8292797caf0c app]# ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 *:8443 *:* # API用のポート(8443)がオープン
LISTEN 0 128 *:2345 *:*
[mng k8s-test-app]$ curl -k -H 'Authorization: TestApp my-super-secret-key' https://localhost:8443/ping | jq .
{
"message": "pong"
}
[mng k8s-test-app]$ podman run -it -p 8443:8443 -p 2345:2345 -v /home/hoge/k8s-test-app/podman/certs:/app/certs:ro -v /home/hoge/k8s-test-app/podman/secret:/app/secret:ro -v /home/hoge/k8s-test-app/podman/cephfs/k8s-test-app/log:/mnt/cephfs/k8s-test-app/log k8s-test-app:debug
API server listening at: [::]:2345
2025-05-05T10:43:56Z warn layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
2025/05/05 10:55:18 INFO: API key successfully loaded
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> main.handlePing (4 handlers)
2025/05/05 10:59:47 INFO: Starting HTTPS server on :8443
2025/05/05 11:00:09 INFO: /ping request
[GIN] 2025/05/05 - 11:01:08 | 200 | 58.801611862s | 127.0.0.1 | GET "/ping" ★
API処理部分でブレークポイント設定してみる。
[mng k8s-test-app]$ podman stop lucid_swartz
[mng k8s-test-app]$ podman rm lucid_swartz
Kubernetesクラスタ内のPodをリモートデバッグ
プライベートレジストリ (Harbor) へPush
[mng k8s-test-app]$ podman tag k8s-test-app:debug harbor.test.k8s.local/k8s-test-app/k8s-test-app:debug
[mng k8s-test-app]$ podman login harbor.test.k8s.local
Username: admin
Password:
Login Succeeded!
[mng k8s-test-app]$ podman push k8s-test-app:debug harbor.test.k8s.local/k8s-test-app/k8s-test-app:debug
Kubernetesクラスタへデバッグ用コンテナイメージで再ディプロイ
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-test-app
labels:
app: k8s-test-app
spec:
replicas: 3
selector:
matchLabels:
app: k8s-test-app
template:
metadata:
labels:
app: k8s-test-app
spec:
imagePullSecrets:
- name: harbor-creds
initContainers:
- name: k8s-test-app-init
image: busybox
command:
- sh
- -c
- |
echo "[INIT] Creating log directory...";
if mkdir -p /mnt/cephfs/k8s-test-app/log; then
chmod -R 777 /mnt/cephfs/k8s-test-app;
else
echo "[ERROR] Failed to create /mnt/cephfs/k8s-test-app/log" >&2;
exit 1;
fi
volumeMounts:
- name: history-vol
mountPath: /mnt/cephfs
containers:
- name: k8s-test-app
image: harbor.test.k8s.local/k8s-test-app/k8s-test-app:debug # ★debug用のイメージを指定
imagePullPolicy: Always
ports:
- containerPort: 8443
- containerPort: 2345 # ★Delveポートを追加
volumeMounts:
- name: certs
mountPath: /app/certs
readOnly: true
- name: api-key
mountPath: /app/secret
readOnly: true
- name: history-vol
mountPath: /mnt/cephfs
volumes:
- name: certs
secret:
secretName: k8s-test-app-tls
- name: api-key
secret:
secretName: k8s-test-app-api-key
- name: history-vol
persistentVolumeClaim:
claimName: cephfs-pvc
[mng k8s-test-app]$ kubectl apply -f k8s-test-app-deploy-dlv.yaml
[mng k8s-test-app]$ k8s-test-app]$ kubectl get pods
NAME READY STATUS RESTARTS AGE
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
k8s-test-app-789d7fc76f-2t47l 1/1 Running 0 97s 172.20.194.71 k8s-worker1 <none> <none>
k8s-test-app-789d7fc76f-j9d8z 1/1 Running 0 97s 172.23.229.146 k8s-worker0 <none> <none>
k8s-test-app-789d7fc76f-jlhlm 1/1 Running 0 98s 172.30.126.31 k8s-worker2 <none> <none>
...
Serviceをデバッグ用に更新
apiVersion: v1
kind: Service
metadata:
name: k8s-test-app
labels:
app: k8s-test-app
spec:
type: LoadBalancer
selector:
app: k8s-test-app
ports:
- name: test-app
protocol: TCP
port: 443
targetPort: 8443
- name: dlv # ★Delveポート定義を追加
protocol: TCP
port: 2345
targetPort: 2345
[mng k8s-test-app]$ kubectl apply -f k8s-test-app-svc-dlv.yaml
[mng k8s-test-app]$ kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
k8s-test-app LoadBalancer 10.100.133.21 10.0.0.64 443:32278/TCP,2345:31387/TCP 2d9h app=k8s-test-app
...
※ ポート2345が追加されている。
[mng k8s-test-app]$ curl --cacert ../ceph-test/tls/private_ca.crt -H 'Authorization: TestApp a9b2F0c8d1E7f6A4b3C5d2E8f7A5b4C6' https://k8
s-test-app.test.k8s.local/ping | jq .
* Failed to connect to k8s-test-app.test.k8s.local port 443: Connection refused
※ デバッガーでアプリケーションを開始していないので接続エラー。
Visual Studio Codeからリモートデバッグを実行
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Remote Attach to dlv on k8s - k8s-test-app",
"type": "go",
"request": "attach",
"mode": "remote",
"host": "k8s-test-app.test.k8s.local", # Goアプリケーションへの接続ホスト名 (VIP)
"port": 2345, # コンテナ側のDelveポート
"cwd": "/home/hoge/k8s-test-app", # ソースコードのディレクトリ (ホスト側)
"remotePath": "/app", # コンテナ側の実行ファイルのディレクトリ
"apiVersion": 2,
"trace": "verbose", # DelveとVSCode間の通信ログを表示
"showLog": true # デバッグアダプタのログ出力を有効化
}
]
}
VSCodeからデバッグを開始しアプリケーションのソースコードにブレークポイント設定してみる。
[mng k8s-test-app]$ kubectl exec -it k8s-test-app-789d7fc76f-2t47l -- bash
Defaulted container "k8s-test-app" out of: k8s-test-app, k8s-test-app-init (init)
[root@k8s-test-app-789d7fc76f-2t47l app]# ps ax
PID TTY STAT TIME COMMAND
1 ? Ssl 0:00 dlv exec /app/app --headless --listen=:2345 --api-version=2 --accept-multiclient
7 ? Ssl 0:00 /usr/local/bin/dlv ** telemetry **
8 ? Sl 0:00 /app/app
51 pts/0 Ss 0:00 bash
62 pts/0 R+ 0:00 ps ax
[root@k8s-test-app-789d7fc76f-2t47l app]# ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 *:8443 *:* # API用のポート(8443)がオープン
LISTEN 0 128 *:2345 *:*
# デバッガ接続したPodに分散された場合には成功
[mng k8s-test-app]$ curl --cacert ../tls/private_ca.crt -H 'Authorization: TestApp a9b2F0c8d1E7f6A4b3C5d2E8f7A5b4C6' https://k8
s-test-app.test.k8s.local/ping | jq .
{
"message": "pong"
}
# デバッガ未接続のPodに分散された場合には接続エラー
[mng k8s-test-app]$ curl --cacert ../tls/private_ca.crt -H 'Authorization: TestApp a9b2F0c8d1E7f6A4b3C5d2E8f7A5b4C6' https://k8s-test-app.test.k8s.local/ping | jq .
curl: (7) Failed to connect to k8s-test-app.test.k8s.local port 443: Connection refused
[mng k8s-test-app]$ kubectl get pods
NAME READY STATUS RESTARTS AGE
k8s-test-app-789d7fc76f-2t47l 1/1 Running 0 61m
...
[mng k8s-test-app]$ kubectl logs k8s-test-app-789d7fc76f-2t47l
Defaulted container "k8s-test-app" out of: k8s-test-app, k8s-test-app-init (init)
API server listening at: [::]:2345 ★デバッガーでアプリケーション開始
2025-05-05T13:14:44Z warn layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
2025/05/05 13:20:10 INFO: API key successfully loaded
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> main.handlePing (4 handlers)
2025/05/05 13:20:19 INFO: Starting HTTPS server on :8443
2025/05/05 13:23:36 INFO: /ping request
[GIN] 2025/05/05 - 13:23:43 | 200 | 7.461114369s | 10.0.0.157 | GET "/ping" ★OK
2025/05/05 13:24:26 INFO: /ping request
[GIN] 2025/05/05 - 13:24:29 | 200 | 3.397720163s | 10.0.0.157 | GET "/ping"
API処理部分でブレークポイント設定している。
関連記事