K8s の readiness probe は「アプリが実際に使うポート」をチェックせよ

E2Eテスト環境で notebooks テスト 9件が 1.376秒で全滅した。settings テストは普通に通っていた。

「1.376秒で9件全滅」はタイムアウトではない。サーバーが応答しているが、中で何かが壊れている。そして「settings は通る」という事実が、原因が HTTP サーバーそのものではないことを示していた。この非対称性が唯一の手がかりだった。

環境と構成

  • K3s v1.31 上の E2E テスト環境
  • SeaweedFS(S3互換ストレージ): server モードで起動

SeaweedFS とは: 分散ファイルシステムで、Amazon S3互換のAPIを提供する。serverモードでは master、volume、filer、S3 gateway の全コンポーネントを1つのプロセスで起動する。

  • master port: 9333
  • S3 gateway port: 8333
  • ScanNote(SvelteKit + SQLite + S3): 起動時に S3FileStorageService.initialize() を呼び、SeaweedFS S3 に HeadBucketCommand を送信
  • readiness probe: SeaweedFS の master port 9333 で /cluster/status を httpGet チェック

当初の設定はこうだった:

readinessProbe:
  httpGet:
    path: /cluster/status
    port: master  # 9333
  initialDelaySeconds: 5
  periodSeconds: 3
  failureThreshold: 5

何が起きていたか

SeaweedFS の server モードは内部的に複数コンポーネントを順番に起動する:

master (9333) → volume → filer → S3 gateway (8333)

master は数秒で起動する。S3 gateway はその後 15〜30秒かかる。

readiness probe が master (9333) の /cluster/status を見ていたため、S3 gateway がまだ起動していないのに Pod が「Ready」と判定された。

K8s は Ready になった Pod にトラフィックを流す。

Kubernetes の readiness probe とは: Pod が外部からのトラフィックを受け取る準備ができているかを判定する仕組み。Ready状態でない Pod は Service のエンドポイントから除外され、トラフィックが流れない。ScanNote Pod が起動すると、DIコンテナの初期化フェーズで S3FileStorageService.initialize() を呼び出し、SeaweedFS S3(port 8333)に接続を試みる。しかしS3 gatewayはまだ存在しない。結果:

Error: no available server
```bash
# SeaweedFS S3 gateway の起動状況を確認するコマンド例
kubectl logs -f deployment/seaweedfs | grep "s3"
kubectl get pods -o wide  # Ready状態の確認
curl -v http://seaweedfs-service:8333/  # S3 gateway への接続テスト

DI コンテナの初期化が失敗し、リクエストのたびに S3 操作が必要な endpoints が全滅した。一方、settings エンドポイントは S3 を使わないので普通に応答できていた。

## 手がかりの読み方

これは「アプリが起動しているが S3 に接続できない」状態を強く示唆する。ログを確認すると:

[S3FileStorageService] HeadBucketCommand failed: no available server


次に SeaweedFS Pod のログを見ると:

[SeaweedFS master] listening on port 9333 ... (15秒経過) [SeaweedFS s3] listening on port 8333


readiness probe が9333をReadyと判定した直後にScanNoteが起動し、8333がまだ存在しない状態で接続を試みていた。タイミング問題ではなく、probeが間違ったポートを見ていたことによる構造的問題だ。

## 修正: tcpSocket で実際のポートを確認する

```yaml
readinessProbe:
  tcpSocket:
    port: s3  # 8333
  initialDelaySeconds: 15
  periodSeconds: 5
  failureThreshold: 10

変更点は3つ:

  1. httpGet → tcpSocket: S3 gateway は HTTP リクエストを /cluster/status のような形式では受け付けない。TCP 接続の確立で「ポートが開いているか」だけを確認する
  2. port: master → port: s3: アプリが実際に依存するポートを指定
  3. initialDelaySeconds: 5 → 15: S3 gateway の起動に 15〜30秒かかる実態に合わせた

failureThreshold を 10 に増やしたのは、S3 gateway の起動が遅い場合でも Pod を Terminating にしないためだ(5回 × 5秒 = 25秒の猶予がある)。

httpGet vs tcpSocket の使い分け

readiness probe には3種類ある:

| 種類 | 確認内容 | 適したケース | |------|---------|------------| | httpGet | HTTP レスポンスが 200–399 か | Web サーバー、ヘルスチェックエンドポイントを持つサービス | | tcpSocket | TCP 接続が確立できるか | データベース、S3互換ストレージ、カスタムプロトコル | | exec | コマンドの終了コードが 0 か | ファイルの存在確認など、他の方法で確認できない場合 |

今回の SeaweedFS S3 は HTTP サーバーではあるが、/cluster/status のようなヘルスチェックパスを持っていない。tcpSocket でポートが LISTEN 状態になっているかを確認するのが適切だ。

普遍的な教訓

readiness probe の設計でよくある間違いは「起動が速いポートを見る」ことだ。マルチコンポーネントのサービス(SeaweedFS、Kafka、Elasticsearch など)は内部的な起動順序があり、最初に起動するポートと、アプリが実際に使うポートが異なる場合がある。

probe が確認すべきは「依存アプリが実際に接続するポート・エンドポイント」だ。

チェックリストとして使える:

  • [ ] probe が確認しているポートは、依存アプリが接続するポートと同じか?
  • [ ] サービスが複数ポートを持つ場合、全コンポーネントが起動完了してから Ready になるか?
  • [ ] initialDelaySeconds は実測した起動時間を元に設定しているか?
  • [ ] failureThreshold × periodSeconds が最大起動時間をカバーしているか?

「settings は通る、notebooks は全滅」という非対称性がなければ、単なるフレーキーテストとして見過ごしていただろう。 ⚠️ 注意: この問題は本番環境でも発生する可能性があります。Pod の再起動時に一時的にサービスが利用できなくなったり、ロードバランサーが未準備の Pod にリクエストを送ってしまう場合があります。テストの失敗パターンこそが、最も重要なデバッグの手がかりだった。