KubernetesクラスタをVMware Workstationでオンプレ想定で構築してみる (RHEL9) - (5) 自作したGoアプリのリモートデバッグ編 -

# はじめに
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クラスタ構成

以下の前回の記事で構築済みの環境を利用する。

* [KubernetesクラスタをVMware Workstationでオンプレ想定で構築してみる (RHEL9) - (4) 自作アプリのディプロイ/結合テスト編 -](https://qiita.com/rhpenguin/items/e724f1a821d2e85e1289)

![test_apps1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/4d9091a4-8bbb-4b57-a8c9-83947bd16996.png)

* [KubernetesクラスタをVMware Workstationでオンプレ想定で構築してみる (RHEL9) - (1) クラスタ構築編 -](https://qiita.com/rhpenguin/items/3347a41493a931953369)

![network1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/99b82e84-cac8-4c48-93ac-ded75fd4bc50.png)

# リモートデバッグ接続

```plantuml
@startuml

participant "VSCode (Host - Windows11)" as vscode
participant "開発VM (RHEL9.5)" as devvm
participant "Service (VIP)" as service
participant "test-k8s-app Pod (k8s-worker0)" as worker0
participant "test-k8s-app Pod (k8s-worker1)" as worker1
participant "test-k8s-app Pod (k8s-worker2)" as worker2

vscode -> devvm: Remote - SSH接続
devvm -> service: Go/Delve接続 (VIP)
service -> worker0: Go/Delve接続 (Pod IP)
service -> worker1: Go/Delve接続 (Pod IP)
service -> worker2: Go/Delve接続 (Pod IP)
@enduml
```

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<br /> (Ceph File System) | /mnt/cephfs/k8s-test-app/log/history.log |

# 開発VMのディレクトリ構成 - k8s-test-appアプリケーション

前回の記事で作成したk8s-test-appアプリを対象にしてみる。mng.test.k8s.local(RHEL9.5)で作業する。

```text: 開発ディレクトリツリー構成
/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を同梱したデバッグ用のビルドイメージを用意する。

```yaml: Contanerfile.dlv
#
# 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"]
```

```console: デバッグ用イメージをビルド
[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"タグをつけて区別するようにする。

```console: デバッグ用イメージでコンテナを起動
[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からの接続待ち...
```

```console: 接続前の状態を確認
[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               *:*
```

```yaml: VSCodeのlaunch.jsonを設定
{
    "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関数)でブレークポイント設定してみる。

![delve_vscode1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/9df5972a-e53e-4793-84f1-6d054842263f.png)


```console: アプリケーション起動確認
[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               *:*
```


```console: API呼び出し
[mng k8s-test-app]$ curl -k -H 'Authorization: TestApp my-super-secret-key' https://localhost:8443/ping | jq .
{
  "message": "pong"
}
```

```console: コンテナ実行画面
[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処理部分でブレークポイント設定してみる。

![delve_vscode2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/cee30084-737e-4a08-a12d-7e6da1ca1047.png)

```console: コンテナ停止とクリーンアップ
[mng k8s-test-app]$ podman stop lucid_swartz
[mng k8s-test-app]$ podman rm lucid_swartz
```


# Kubernetesクラスタ内のPodをリモートデバッグ

## プライベートレジストリ (Harbor) へPush

```console: コンテナイメージにdebugタグ付け
[mng k8s-test-app]$ podman tag k8s-test-app:debug harbor.test.k8s.local/k8s-test-app/k8s-test-app:debug
```

```console: ログイン
[mng k8s-test-app]$ podman login harbor.test.k8s.local
Username: admin
Password:
Login Succeeded!
```

```console: Push
[mng k8s-test-app]$ podman push k8s-test-app:debug harbor.test.k8s.local/k8s-test-app/k8s-test-app:debug
```

## Kubernetesクラスタへデバッグ用コンテナイメージで再ディプロイ

```yaml: k8s-test-app-deploy-dlv.yaml
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
```

```console: 適用
[mng k8s-test-app]$ kubectl apply -f k8s-test-app-deploy-dlv.yaml
```

```console: 確認
[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をデバッグ用に更新

```yaml: k8s-test-app-deploy-dlv.yaml
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
```

```console: 適用
[mng k8s-test-app]$ kubectl apply -f k8s-test-app-svc-dlv.yaml
```

```console: 確認
[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が追加されている。

```console: APIアクセス実行 (デバッグ開始前)
[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からリモートデバッグを実行

```yaml: VSCodeのlaunch.jsonを設定
{
    // 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からデバッグを開始しアプリケーションのソースコードにブレークポイント設定してみる。

```console: リモートからコンテナへシェルで接続し状態を確認
[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               *:*
```

```console: API呼び出し

# デバッガ接続した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
```

```console: Pod側のログを確認

[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処理部分でブレークポイント設定している。

![delve_vscode3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362125/5095de6c-6ad5-48e4-85c1-b8d9f2aaee96.png)

# 関連記事

https://qiita.com/rhpenguin/items/3347a41493a931953369

https://qiita.com/rhpenguin/items/5e996b25df07b59a8f43

https://qiita.com/rhpenguin/items/fdfaa6354efebac197b6