本記事には広告(アフィリエイトリンク)が含まれます。

Kubernetes応用編 第08回

Kubernetes応用編 第08回
自動スケーリングとヘルスチェック設計 — HPA / Probe詳細

8.1 はじめに

前回(第7回)では、SecurityContextとPod Security Standardsを使ってTaskBoardの全コンテナを「最小権限の実行環境」に強化しました。非root実行、readOnlyRootFilesystem、権限昇格の禁止、ケーパビリティの全剥奪と、コンテナ内部のセキュリティを固めています。

現在のTaskBoardの状態を確認しておきましょう。

[app Namespace]
  Nginx (Deployment, replicas: 2) + Service (ClusterIP, targetPort: 8080)
    ※ 第7回で非root化済み(listen 8080、SecurityContext適用)
  TaskBoard API (Deployment, replicas: 2, MySQL接続版) + Service (ClusterIP)
    ※ SecurityContext適用済み(runAsNonRoot, readOnlyRootFilesystem等)
  Gateway (taskboard-gateway) + HTTPRoute (taskboard-route)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み
  + NetworkPolicy適用済み
  + Pod Security Admission: warn=restricted

[db Namespace]
  MySQL (StatefulSet, replicas: 1) + Headless Service + PVC
  + DB初期化Job(Completed)
  + DBバックアップCronJob(稼働中)
  + Secret(MySQL認証情報)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み
  + NetworkPolicy適用済み
  + Pod Security Admission: warn=baseline

[monitoring Namespace]
  ログ収集DaemonSet(全Worker Nodeに配置)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み

[クラスタ全体]
  Calico CNI
  Metrics Server 稼働中
  Gateway API 稼働中(/ → Nginx, /api → TaskBoard API)
  マルチノード構成(CP 1 + Worker 3)

セキュリティは固まりました。しかし、運用の観点から2つの課題が残っています。

1つ目はスケーリングです。TaskBoardのNginxもAPIもreplicas: 2で固定されています。アクセスが急増したらどうするか。深夜に手動でkubectl scaleを打つのでしょうか。VMの世界では、vCenterのアラームを設定して自動対応するか、結局は深夜に電話が鳴って手動でVM追加する、そんな運用でした。

2つ目はヘルスチェックです。入門編でProbeの存在は知りました。しかし、initialDelaySecondsを何秒にするか、failureThresholdを何回にするか、根拠を持って設定できているでしょうか。VMの世界でZabbixの監視テンプレートを設定するとき、ポーリング間隔やリトライ回数を「なんとなく」で決めていた方も多いはずです。

BeforeAfter
replicasは手動で変えるもの。Probeは入門で触ったが、パラメータの設計根拠がないHPAで自動スケーリングを実装し、Probeの各パラメータの効果を体験的に理解している

今回は前半でHPA(Horizontal Pod Autoscaler)による自動スケーリングを実装し、後半でProbeの各パラメータの効果を体験します。特に後半では、Payara Microの起動時間(15〜20秒)を活かして「startupProbeがないとどうなるか」を身体で理解してもらいます。

なお、今回はProbeの「仕組み」と「各パラメータの効果」を体験することに集中します。「TaskBoardの各コンポーネントに対してどのProbeをどの値で設定するか」という設計判断は、実践編第3回(詳細設計)で行います。まずは道具の使い方をしっかり覚えましょう。

8.2 VMの世界との対比 — DRSとZabbix監視

8.2.1 vSphere DRSとアラームベースの自動アクション → HPA

VMware環境でリソース使用率に応じた自動対応をするには、主に2つの方法がありました。

1つ目はvSphere DRS(Distributed Resource Scheduler)です。DRSはクラスタ内のESXiホスト間でVMを自動移行(vMotion)して、リソース使用率のバランスを取ります。ただし、DRSが行うのは「既存VMの配置最適化」であり、「VMの台数を増やす」わけではありません。

2つ目はvCenterのアラーム + 自動アクション設定です。「CPU使用率が80%を超えたらメール通知」は多くの環境で設定されていたでしょう。しかし「CPU使用率が80%を超えたらVMを自動追加」という自動アクションを実現するには、vRealize Automationなどの追加製品が必要でした。多くの現場では結局、アラームメールを受けて人間がVMを追加していたはずです。

KubernetesのHPA(Horizontal Pod Autoscaler)は、この「メトリクスに基づいてPod数を自動調整する」機能を標準で提供します。追加製品は不要です。CPU使用率が閾値を超えればPodが増え、下がれば減る。深夜の電話は鳴りません。

VMの世界Kubernetes
DRS — VMの配置最適化(台数は変わらない)Scheduler — Podの配置最適化(Node選択)
アラーム通知 → 手動でVM追加HPA — CPU/メモリに基づいてPod数を自動調整
vRealize Automation(別製品)で自動化HPAはKubernetes標準機能(追加費用なし)
スケールアウトに10〜30分(VM起動 + OS設定)スケールアウトに数秒〜数十秒(Pod追加)

8.2.2 Zabbixの死活監視 / サービス監視 → Probe

VMの世界でサーバーの健全性を確認するには、Zabbixのような外部監視ツールを使っていました。典型的な監視設計は以下のようなものです。

ICMP Ping(死活監視)はサーバーが生きているかを確認します。ポート監視(TCP接続確認)はサービスが待ち受けているかを確認します。HTTPコンテンツ監視はアプリケーションが正常にレスポンスを返すかを確認します。それぞれ、監視間隔やリトライ回数、タイムアウトを設定しましたよね。

KubernetesのProbeは、この外部監視に相当する機能がkubeletに組み込まれています。しかも、監視結果に基づいて「Podの再起動」や「トラフィックの除外」を自動で行います。Zabbixのアクション設定で「サービス停止検知 → スクリプト実行 → サービス再起動」を組んでいた方には、Probeの自動化レベルの高さが実感できるでしょう。

Zabbixでの監視設計KubernetesのProbe
ICMP Ping — サーバーが生きているかlivenessProbe — プロセスが生きているか(失敗 → 再起動)
ポート/HTTP監視 — サービスが応答するかreadinessProbe — リクエストを受け入れ可能か(失敗 → トラフィック除外)
(相当する機能なし)startupProbe — 起動が完了したか(完了までliveness/readinessを抑制)
監視間隔: 60秒、リトライ: 3回 → 手動設定periodSeconds, failureThreshold → マニフェストで宣言的に設定
検知後のアクション: スクリプト実行(要設定)検知後のアクション: 自動再起動 / 自動除外(組み込み)

Zabbixとの大きな違いはstartupProbeの存在です。VMではOSの起動完了を待つのは当然で、OS起動中にサービス監視が失敗するからといってサーバーを再起動したりはしません。しかしKubernetesでは、起動中のコンテナにlivenessProbeが走ると「応答しない → 異常 → 再起動」という判断がなされてしまいます。startupProbeはこの問題を解決するために存在します。今回のハンズオンで、その必要性を身体で理解しましょう。

8.3 HPAで負荷に応じた自動スケーリングを実装する

8.3.1 HPAの仕組み — Metrics Serverとの連携

HPA(Horizontal Pod Autoscaler)は、メトリクスの値に基づいてDeploymentやStatefulSetのreplicasを自動で増減させるKubernetesリソースです。動作の流れは次の通りです。

まず、各NodeのkubeletがPodのCPU/メモリ使用量を収集します。Metrics Serverがそのデータを集約してAPIとして公開します。HPAコントローラーが定期的(デフォルト15秒間隔)にMetrics ServerからPodのメトリクスを取得します。現在の使用率と目標値を比較し、必要なreplicas数を計算します。計算結果に基づいてDeploymentのreplicasを更新します。

HPAの動作にはMetrics Serverが必須です。第1回でMetrics Serverを導入したのは、まさにこの回のためです。まず正常に動作していることを確認しましょう。

[Execution User: developer]

kubectl top nodes
NAME                        CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
k8s-applied-control-plane   210m         5%     780Mi           19%
k8s-applied-worker          85m          2%     420Mi           10%
k8s-applied-worker2         78m          1%     395Mi           9%
k8s-applied-worker3         72m          1%     380Mi           9%

各NodeのCPU/メモリ使用量が表示されれば、Metrics Serverは正常に動作しています。もしerror: Metrics API not availableと表示される場合は、第1回のMetrics Server導入手順を再確認してください。

[Execution User: developer]

kubectl top pods -n app
NAME                             CPU(cores)   MEMORY(bytes)
nginx-7d8b4f6c9-k2m5x           1m           8Mi
nginx-7d8b4f6c9-t9r3p           1m           7Mi
taskboard-api-5c8d7e6f4-h4n2j   12m          310Mi
taskboard-api-5c8d7e6f4-w7k9m   11m          305Mi

Podレベルのメトリクスも取得できています。NginxはCPU 1m程度と非常に軽量ですが、Payara Micro(TaskBoard API)はJVMが動いているためメモリを300Mi前後消費しています。この差は第1回でも確認しましたが、HPAの閾値設計でも重要なポイントになります。

8.3.2 NginxにHPAを作成する

まずはNginx DeploymentにHPAを適用します。CPU使用率が70%を超えたらPodを増やし、最小2、最大6のレンジでスケーリングさせます。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/hpa-nginx.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nginx-hpa
  namespace: app
  labels:
    app: taskboard
    component: frontend
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx
  minReplicas: 2
  maxReplicas: 6
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70   # CPU使用率の平均が70%を超えたらスケールアウト
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # スケールイン前に5分間の安定期間を確保
      policies:
        - type: Pods
          value: 1                      # 一度に最大1 Podずつ減らす
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0     # スケールアウトは即座に実行
      policies:
        - type: Pods
          value: 2                      # 一度に最大2 Podずつ増やす
          periodSeconds: 60
EOF

各設定の意味を整理します。

scaleTargetRefはスケーリング対象のリソースを指定します。ここではDeployment/nginxです。minReplicas: 2はスケールインの下限で、第7回からのreplicas: 2と同じ値です。maxReplicas: 6はスケールアウトの上限です。averageUtilization: 70はCPU requests(50m)に対する使用率の平均が70%(つまり35m)を超えたらスケールアウトが開始されるという意味です。

behaviorセクションはスケーリングの振る舞いを細かく制御します。scaleDown.stabilizationWindowSeconds: 300は、スケールインする前に5分間メトリクスが安定していることを確認します。一時的な負荷低下でPodが減らされ、すぐに負荷が戻ってまた増やす、という「フラッピング」を防ぎます。scaleUp.stabilizationWindowSeconds: 0は、スケールアウトは待たずに即座に実行します。負荷急増時は素早い対応が必要だからです。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/hpa-nginx.yaml
horizontalpodautoscaler.autoscaling/nginx-hpa created

HPAの状態を確認します。

[Execution User: developer]

kubectl get hpa -n app
NAME        REFERENCE          TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
nginx-hpa   Deployment/nginx   2%/70%    2         6         2          30s

TARGETS列に2%/70%と表示されています。左が現在のCPU使用率の平均(2%)、右が目標値(70%)です。現在は負荷がほとんどないため2%です。REPLICASは2で、最小値のまま維持されています。

もしTARGETS<unknown>/70%と表示される場合は、Metrics Serverがまだメトリクスを収集できていません。1〜2分待ってから再度kubectl get hpa -n appを実行してください。

8.3.3 stress-ngで負荷をかけてスケールアウトを観察する

HPAの動作を確認するため、Nginx Podに負荷をかけます。stress-ngをホストにインストールするのではなく、一時的なPodを使ってNginxに対してHTTPリクエストの負荷をかけます。

まず、ターミナルを2つ用意します。1つ目のターミナルでHPAの状態をリアルタイム監視します。

[ターミナル1 — 監視用]

[Execution User: developer]

kubectl get hpa -n app --watch

2つ目のターミナルで負荷をかけます。Nginx Podに対してServiceを経由した大量のHTTPリクエストを送ることでCPU負荷を発生させます。

[ターミナル2 — 負荷用]

[Execution User: developer]

# busyboxの一時Podから、Nginx Serviceに対して大量のHTTPリクエストを送信する
kubectl run stress-test --image=busybox:1.36 -n app --rm -it --restart=Never -- \
  /bin/sh -c "while true; do wget -q -O /dev/null http://nginx.app.svc.cluster.local; done"

このコマンドはbusyboxの一時Podを作成し、Nginx Service(nginx.app.svc.cluster.local)に対してHTTPリクエストを連続送信します。Nginx PodのCPU使用率が上昇していきます。

負荷をかけ始めてから1〜2分後、ターミナル1でHPAの変化を観察してください。

[ターミナル1 — 監視出力]

NAME        REFERENCE          TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
nginx-hpa   Deployment/nginx   2%/70%    2         6         2          3m
nginx-hpa   Deployment/nginx   45%/70%   2         6         2          4m
nginx-hpa   Deployment/nginx   82%/70%   2         6         2          5m
nginx-hpa   Deployment/nginx   82%/70%   2         6         3          5m30s
nginx-hpa   Deployment/nginx   68%/70%   2         6         3          6m
nginx-hpa   Deployment/nginx   75%/70%   2         6         4          7m

CPU使用率が70%を超えた時点でREPLICASが2から3に、さらに負荷が続くと4に増えています。HPAが自動でDeploymentのreplicasを増やし、Podが追加されています。

別のターミナルでPodの状態も確認してみましょう。

[Execution User: developer]

kubectl get pods -n app -l component=frontend
NAME                     READY   STATUS    RESTARTS   AGE
nginx-7d8b4f6c9-k2m5x   1/1     Running   0          45m
nginx-7d8b4f6c9-t9r3p   1/1     Running   0          45m
nginx-7d8b4f6c9-b8h3c   1/1     Running   0          2m15s
nginx-7d8b4f6c9-j5v7d   1/1     Running   0          75s

Podが4つに増えています。新しいPodはAGEが若いことから、HPAによって追加されたものだとわかります。

kubectl top pods -n app -l component=frontendでリアルタイムのCPU使用量も確認できます。負荷が4つのPodに分散されていることが見えるはずです。

📝 HPAとResourceQuotaの関係

HPAがスケールアウトしてPodが増えると、app NamespaceのResourceQuota(第1回で設定:CPU requests合計2コア、Pod数上限20)の消費量も増えます。もしHPAのmaxReplicasが大きすぎてQuotaの上限に達すると、新しいPodの作成が拒否されてスケールアウトが止まります。つまり、ResourceQuotaはHPAの安全弁として機能します。Nginx(requests: 50m)× 6 Pod = 300m ですので、今回の設定ではQuota(2コア = 2000m)には十分な余裕があります。

また、LimitRange(第1回設定)のデフォルト値は、HPAで新規作成されるPodにも適用されます。ただし、今回のNginx Deploymentは明示的にresourcesを指定しているため、LimitRangeのデフォルト値ではなくマニフェストの値が使われます。

8.3.4 負荷停止後のスケールインを観察する — クールダウン期間

ターミナル2でCtrl+Cを押して負荷テストを停止します。stressテスト用のPodが終了します。

ターミナル1のHPA監視はそのまま続けてください。負荷が止まってもすぐにはPod数は減りません。

[ターミナル1 — 監視出力(負荷停止後)]

NAME        REFERENCE          TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
nginx-hpa   Deployment/nginx   75%/70%   2         6         4          8m
nginx-hpa   Deployment/nginx   25%/70%   2         6         4          9m
nginx-hpa   Deployment/nginx   3%/70%    2         6         4          10m
nginx-hpa   Deployment/nginx   2%/70%    2         6         4          12m
nginx-hpa   Deployment/nginx   2%/70%    2         6         3          14m
nginx-hpa   Deployment/nginx   2%/70%    2         6         2          16m

負荷が下がってもすぐにPodが減らないことに注目してください。HPAマニフェストでscaleDown.stabilizationWindowSeconds: 300(5分間)を設定したためです。CPU使用率が閾値を下回ってから5分間安定していることを確認してから、1 Podずつ段階的に減らしています。

このクールダウン(安定化ウィンドウ)がないと、「負荷が一時的に下がる → Podを減らす → また負荷が上がる → Podを増やす」というフラッピングが発生します。VMの世界でDRSの移行閾値を保守的に設定していたのと同じ考え方です。

Ctrl+Cで監視を停止してください。

8.3.5 TaskBoard APIにもHPAを適用する

フロントエンドと同様に、TaskBoard API(Payara Micro)にもHPAを適用します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/hpa-taskboard-api.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: taskboard-api-hpa
  namespace: app
  labels:
    app: taskboard
    component: api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: taskboard-api
  minReplicas: 2
  maxReplicas: 4
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70   # CPU使用率の平均が70%を超えたらスケールアウト
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Pods
          value: 1                     # APIは1 Podずつ慎重に増やす
          periodSeconds: 60
EOF

NginxのHPAとの違いは2点です。maxReplicas: 4はNginxの6より少なくしています。Payara MicroはJVMベースでNginxよりリソース消費が大きい(requests: 200m/384Mi)ため、4 Pod(requests合計: 800m/1536Mi)でもResourceQuotaへの影響が大きくなります。scaleUp.policies.value: 1で、1 Podずつ慎重にスケールアウトさせます。JVMの起動時にCPU負荷が一時的に高くなるため、一度に複数Podを起動するとNode全体の負荷が上がりすぎる可能性があるためです。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/hpa-taskboard-api.yaml
horizontalpodautoscaler.autoscaling/taskboard-api-hpa created

[Execution User: developer]

kubectl get hpa -n app
NAME                REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
nginx-hpa           Deployment/nginx       2%/70%    2         6         2          20m
taskboard-api-hpa   Deployment/taskboard-api   6%/70%    2         4         2          15s

2つのHPAが稼働しています。TaskBoard APIのCPU使用率は6%で、通常時はスケールアウトしません。HPAの動作確認は前半ここまでです。次は後半、Probeの設計に移ります。

8.4 startupProbeの必要性を身体で理解する

ここからがProbeのハンズオンです。まず、startupProbeの必要性を「身体で理解する」ところから始めます。

8.4.1 startupProbeなしでPayara Microをデプロイする — CrashLoopBackOffを体験する

Payara Microの起動には15〜20秒かかります。startupProbeなしでlivenessProbeだけを設定するとどうなるか、体験してみましょう。

以下のマニフェストは、startupProbeを意図的に省略し、livenessProbeを厳しめの設定にしたものです。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api-deployment-v4-no-startup.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: taskboard-api
  namespace: app
  labels:
    app: taskboard
    component: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: taskboard
      component: api
  template:
    metadata:
      labels:
        app: taskboard
        component: api
    spec:
      containers:
        - name: taskboard-api
          image: taskboard-api:2.0.0
          imagePullPolicy: Never
          ports:
            - containerPort: 8080
          env:
            - name: DB_HOST
              value: "mysql-0.mysql-headless.db.svc.cluster.local"
            - name: DB_NAME
              value: "taskboard"
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_USER
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_PASSWORD
          resources:
            requests:
              cpu: "200m"
              memory: "384Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          # ▼ startupProbeなし — livenessProbeだけ設定
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 0      # 起動直後からチェック開始
            periodSeconds: 5            # 5秒ごとにチェック
            timeoutSeconds: 3           # 3秒以内に応答がなければ失敗
            failureThreshold: 2         # 2回連続失敗で再起動
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 0
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: payara-config
              mountPath: /opt/payara/config
      volumes:
        - name: tmp
          emptyDir: {}
        - name: payara-config
          emptyDir: {}
EOF

意図的に厳しくした設定のポイントは、initialDelaySeconds: 0(起動直後からチェック開始)とfailureThreshold: 2(2回失敗で再起動)です。Payara Microの起動に15〜20秒かかるのに、起動0秒からlivenessProbeが走り、5秒間隔で2回失敗したら再起動される設定です。起動が完了する前にlivenessProbeに殺されることは確実です。

では、デプロイしてみましょう。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/taskboard-api-deployment-v4-no-startup.yaml

Podの状態をリアルタイムで観察します。

[Execution User: developer]

kubectl get pods -n app -l component=api --watch
NAME                             READY   STATUS    RESTARTS   AGE
taskboard-api-6a9b8c7d5-m3k4j   0/1     Running   0          5s
taskboard-api-6a9b8c7d5-p8r2w   0/1     Running   0          5s
taskboard-api-6a9b8c7d5-m3k4j   0/1     Running   1 (1s ago)   12s
taskboard-api-6a9b8c7d5-p8r2w   0/1     Running   1 (1s ago)   13s
taskboard-api-6a9b8c7d5-m3k4j   0/1     Running   2 (1s ago)   25s
taskboard-api-6a9b8c7d5-p8r2w   0/1     Running   2 (1s ago)   26s
taskboard-api-6a9b8c7d5-m3k4j   0/1     CrashLoopBackOff   2 (1s ago)   38s
taskboard-api-6a9b8c7d5-p8r2w   0/1     CrashLoopBackOff   2 (1s ago)   39s

RESTARTSが増え続け、最終的にCrashLoopBackOffに陥りました。Ctrl+Cで監視を停止してください。

8.4.2 なぜ殺されるのか — Payara Microの起動時間とlivenessProbeの関係

何が起きているか、時系列で整理します。

0秒   : コンテナ起動
0秒   : livenessProbe開始(initialDelaySeconds: 0)
         → Payara Microはまだ起動処理中 → /health/live に応答できない → 失敗 (1回目)
5秒   : livenessProbe再実行(periodSeconds: 5)
         → まだ起動処理中 → 失敗 (2回目)
10秒  : failureThreshold: 2 に達する → kubeletがコンテナを再起動(kill)
10秒  : コンテナ再起動
         → 再び同じことが繰り返される
         → CrashLoopBackOff

Payara Microは起動に15〜20秒かかります。しかし、livenessProbeは起動0秒からチェックを開始し、10秒で「応答しない → 異常」と判定してコンテナを再起動してしまいます。起動完了を待てずに殺される、いわば「産まれる前に死亡確認される」状態です。

Podのイベントで確認してみましょう。

[Execution User: developer]

kubectl describe pod -n app -l component=api | grep -A 5 "Liveness"
    Liveness:       http-get http://:8080/health/live delay=0s timeout=3s period=5s #success=1 #failure=2
    ...
  Warning  Unhealthy  12s (x6 over 72s)  kubelet  Liveness probe failed: Get "http://10.244.2.15:8080/health/live": dial tcp 10.244.2.15:8080: connect: connection refused

connection refusedが出ています。Payara Microがまだ起動しておらず、8080ポートでリッスンしていないため、TCP接続自体が拒否されています。

initialDelaySecondsを20秒に設定すれば解決するのでは?」と思うかもしれません。確かにそれでもある程度は動きます。しかし、起動時間は環境やリソース状況によって変動します。Node負荷が高い日は25秒かかるかもしれません。initialDelaySecondsはあくまで「固定の待ち時間」であり、実際の起動完了とは連動しません。

これがstartupProbeの出番です。

8.4.3 startupProbeを追加して正常起動を確認する

startupProbeは「起動が完了するまでの間、livenessProbeとreadinessProbeを抑制する」ための仕組みです。startupProbeが成功するまで、livenessProbeは実行されません。

まず、startupProbeを追加した正式版のDeploymentを作成します。ここではまだカスタムHealthCheckクラスを追加する前の状態です。Payara Microの標準MicroProfile Healthエンドポイントを使います。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api-deployment-v4.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: taskboard-api
  namespace: app
  labels:
    app: taskboard
    component: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: taskboard
      component: api
  template:
    metadata:
      labels:
        app: taskboard
        component: api
    spec:
      containers:
        - name: taskboard-api
          image: taskboard-api:3.0.0
          imagePullPolicy: Never
          ports:
            - containerPort: 8080
          env:
            - name: DB_HOST
              value: "mysql-0.mysql-headless.db.svc.cluster.local"
            - name: DB_NAME
              value: "taskboard"
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_USER
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_PASSWORD
          resources:
            requests:
              cpu: "200m"
              memory: "384Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          # ▼ startupProbe — 起動完了を待つ(最大60秒)
          startupProbe:
            httpGet:
              path: /health/started
              port: 8080
            initialDelaySeconds: 0      # 即座にチェック開始
            periodSeconds: 2            # 2秒ごとにチェック
            timeoutSeconds: 3           # 3秒のタイムアウト
            failureThreshold: 30        # 30回まで失敗を許容 = 最大60秒待ち
            # 根拠: Payara Micro起動15-20秒。余裕を持って最大60秒
          # ▼ livenessProbe — プロセスが生きているか
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 0      # startupProbe通過後に開始
            periodSeconds: 10           # 10秒ごとにチェック
            timeoutSeconds: 3           # 3秒のタイムアウト
            failureThreshold: 3         # 3回連続失敗で再起動 = 30秒間の無応答
            # 根拠: startupProbe通過後は安定動作前提。30秒間無応答で再起動は妥当
          # ▼ readinessProbe — リクエストを受け入れ可能か
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 0      # startupProbe通過後に開始
            periodSeconds: 5            # 5秒ごとにチェック(livenessより高頻度)
            timeoutSeconds: 3           # 3秒のタイムアウト
            failureThreshold: 3         # 3回連続失敗でトラフィック除外 = 15秒間の無応答
            successThreshold: 1         # 1回成功でトラフィック復帰
            # 根拠: DB接続確認を含む。15秒間無応答でServiceから除外。復帰は迅速に
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: payara-config
              mountPath: /opt/payara/config
      volumes:
        - name: tmp
          emptyDir: {}
        - name: payara-config
          emptyDir: {}
EOF

Probe設定の設計根拠をまとめます。

Probeエンドポイントperiodtimeoutfailure最大検知時間根拠
startup/health/started2秒3秒30回60秒Payara Micro起動15-20秒。余裕を持って最大60秒まで待つ
liveness/health/live10秒3秒3回30秒startupProbe通過後。30秒間無応答はプロセス異常と判断して再起動
readiness/health/ready5秒3秒3回15秒DB接続確認を含む。15秒間無応答でServiceから除外。トラフィック制御は迅速に

まだtaskboard-api:3.0.0イメージを作成していないため、ここではまず一旦imagetaskboard-api:2.0.0のまま適用して動作確認します(カスタムHealthCheckの追加は8.6で行います)。

[Execution User: developer]

# 一旦既存イメージで動作確認するため、imageをtaskboard-api:2.0.0に変更して適用
sed 's/taskboard-api:3.0.0/taskboard-api:2.0.0/' ~/k8s-applied/taskboard-api-deployment-v4.yaml | kubectl apply -f -

Podの起動を観察します。

[Execution User: developer]

kubectl get pods -n app -l component=api --watch
NAME                             READY   STATUS    RESTARTS   AGE
taskboard-api-7b2c9d8e6-f5g3h   0/1     Running   0          3s
taskboard-api-7b2c9d8e6-n4p6r   0/1     Running   0          3s
taskboard-api-7b2c9d8e6-f5g3h   1/1     Running   0          18s
taskboard-api-7b2c9d8e6-n4p6r   1/1     Running   0          19s

今度はCrashLoopBackOffにならず、約18〜19秒でREADY 1/1になりました。startupProbeが2秒間隔でチェックし、Payara Microの起動完了を検知してから、livenessProbeとreadinessProbeのチェックが開始されます。RESTARTSも0のままです。Ctrl+Cで監視を停止してください。

先ほどのCrashLoopBackOffとの対比です。

startupProbeなしstartupProbeあり
起動中のlivenessProbe実行される → 失敗 → 再起動抑制される → 失敗しない
結果CrashLoopBackOff正常起動
RESTARTS増え続ける0

Payara MicroのようなJVMベースのアプリケーションや、初期化処理に時間がかかるアプリケーションでは、startupProbeは必須です。

8.5 liveness / readiness / startupの使い分け

8.5.1 3つのProbeの役割の違い

3つのProbeはそれぞれ異なる問いに答えます。

Probe問い失敗時のアクション実行タイミング
startupProbe起動は完了したか?failureThresholdに達するとコンテナを再起動コンテナ起動直後から。成功するまで繰り返し
livenessProbeプロセスは生きているか?コンテナを再起動(kill → restart)startupProbe成功後から継続的に
readinessProbeリクエストを受け入れ可能か?Serviceのエンドポイントから除外(トラフィックを送らない)startupProbe成功後から継続的に

重要な違いは、livenessProbeの失敗は「コンテナの再起動」、readinessProbeの失敗は「トラフィックの除外」という点です。livenessProbeは「もう治らない」場合に使い、readinessProbeは「一時的に受け入れ不可」の場合に使います。

たとえば、アプリケーションがデッドロックに陥ってリクエストを処理できなくなった場合、livenessProbeが失敗してコンテナが再起動されます。一方、バックエンドのDB接続が一時的に切れた場合は、readinessProbeが失敗してServiceからルーティングが外れ、DB接続が復旧すれば自動で復帰します。再起動する必要はありません。

8.5.2 /health/live と /health/ready で異なるエンドポイントを使う意味

Payara Micro(MicroProfile Health)は、/health/live/health/readyという2つのエンドポイントを提供しています。なぜ同じ/healthではなく、別々のエンドポイントを使うのでしょうか。

/health/liveは「プロセスが生きているか」だけを確認します。デフォルトでは、Payara Microのランタイムが正常に動作していればUPを返します。外部依存(DBやキャッシュ)の状態は含みません。このエンドポイントが失敗するのは、JVMレベルの深刻な問題(デッドロック、メモリ破壊など)が発生した場合です。

/health/readyは「リクエストを受け入れ可能か」を確認します。ここには外部依存の状態を含めることができます。たとえば、DB接続が切れている場合はDOWNを返すことで、「プロセスは生きているが、リクエストを処理できない」状態をKubernetesに伝えられます。

もし/health(統合エンドポイント)をlivenessProbeに使ってしまうと、DB接続が一時的に切れただけでコンテナが再起動されてしまいます。DBの問題なのにアプリケーションを再起動しても解決しません。むしろ、再起動中にリクエストを処理できるPodが減り、状況が悪化します。

livenessProbeには/health/liveを、readinessProbeには/health/readyを使う。この使い分けが重要です。

8.6 カスタムHealthCheckでDB接続状態をProbeに反映する

デフォルトのMicroProfile Healthエンドポイントは、「Payara Microランタイムが正常に動作しているか」だけを確認します。TaskBoard APIにとって、DB(MySQL)に接続できるかどうかも重要な健全性指標です。カスタムHealthCheckクラスを追加して、readinessProbeでDB接続状態を確認できるようにしましょう。

8.6.1 HealthCheckReady.java — Readinessチェックにdb接続確認を追加する

MicroProfile Health APIの@Readinessアノテーションを付けたクラスを作成すると、/health/readyエンドポイントに自動登録されます。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api/src/main/java/com/taskboard/HealthCheckReady.java
package com.taskboard;

import jakarta.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;

/**
 * カスタムReadinessチェック。
 * DB接続が有効かどうかを確認し、/health/ready に反映する。
 *
 * DB接続が切れている場合はDOWNを返す → readinessProbeが失敗
 * → ServiceのEndpointsから除外 → このPodにトラフィックが送られなくなる
 */
@Readiness
public class HealthCheckReady implements HealthCheck {

    // DataSourceConfig.java で定義したJNDI名と一致させる
    @Resource(lookup = "java:app/jdbc/taskboard")
    private DataSource dataSource;

    @Override
    public HealthCheckResponse call() {
        try (Connection conn = dataSource.getConnection()) {
            // isValid(5) : 5秒以内にDB接続の有効性を確認する
            boolean valid = conn.isValid(5);
            if (valid) {
                return HealthCheckResponse.named("db-connection")
                        .up()
                        .withData("database", "taskboard")
                        .build();
            } else {
                return HealthCheckResponse.named("db-connection")
                        .down()
                        .withData("reason", "Connection validation failed")
                        .build();
            }
        } catch (Exception e) {
            return HealthCheckResponse.named("db-connection")
                    .down()
                    .withData("reason", e.getMessage())
                    .build();
        }
    }
}
EOF

このクラスのポイントを整理します。@Readinessアノテーションにより、このHealthCheckは/health/readyエンドポイントに自動登録されます。@Resource(lookup = "java:app/jdbc/taskboard")で、第3回でDataSourceConfig.javaに定義したデータソースのJNDI名と一致させています。Connection.isValid(5)で5秒以内にDB接続の有効性を確認します。接続が有効ならUP、無効ならDOWNを返します。

8.6.2 HealthCheckLive.java — カスタムLivenessチェック

livenessProbeに対応するカスタムチェックも追加します。livenessはDB接続に依存せず、アプリケーションプロセス自体の健全性だけを確認します。

[Execution User: developer]

cat <<'EOF' > ~/k8s-applied/taskboard-api/src/main/java/com/taskboard/HealthCheckLive.java
package com.taskboard;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;

/**
 * カスタムLivenessチェック。
 * アプリケーションプロセスが正常に動作しているかを確認する。
 *
 * DB接続の状態は含めない(それはReadinessの責務)。
 * livenessが失敗するのはプロセスレベルの深刻な問題のみ。
 */
@Liveness
public class HealthCheckLive implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        // プロセスが正常に動作していればUPを返す
        // 将来的にはメモリ使用率やスレッドデッドロック検知などを追加可能
        return HealthCheckResponse.named("taskboard-api-live")
                .up()
                .withData("version", "3.0.0")
                .build();
    }
}
EOF

@Livenessアノテーションにより、/health/liveエンドポイントに登録されます。現在のカスタムLivenessは常にUPを返すシンプルな実装です。これで十分なのかと疑問に思うかもしれませんが、Payara Microのデフォルトランタイムチェックに加えてアプリケーション層のチェックが走る構造になります。将来的に「メモリ使用率が閾値を超えたらDOWN」「特定のデッドロック検知でDOWN」といったロジックを追加する拡張点としても機能します。

8.6.3 コンテナイメージを再ビルドしてデプロイする

カスタムHealthCheckクラスを追加したので、コンテナイメージを再ビルドします。まず、pom.xmlが既にMicroProfile Health APIの依存を含んでいるか確認します。

[Execution User: developer]

grep -A 4 "microprofile" ~/k8s-applied/taskboard-api/pom.xml
        <groupId>org.eclipse.microprofile</groupId>
        <artifactId>microprofile</artifactId>
        <version>6.1</version>
        <type>pom</type>
        <scope>provided</scope>

MicroProfile 6.1にはMicroProfile Health 4.0が含まれているため、@Liveness@ReadinessアノテーションやHealthCheckインタフェースはそのまま使えます。追加の依存は不要です。

pom.xmlのバージョンを3.0.0に更新します。

[Execution User: developer]

sed -i 's/<version>2.0.0<\/version>/<version>3.0.0<\/version>/' ~/k8s-applied/taskboard-api/pom.xml

ファイル構成を確認します。

[Execution User: developer]

find ~/k8s-applied/taskboard-api -type f | sort
/home/developer/k8s-applied/taskboard-api/Dockerfile
/home/developer/k8s-applied/taskboard-api/pom.xml
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/DataSourceConfig.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/HealthCheckLive.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/HealthCheckReady.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/Task.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskBoardApplication.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskResource.java
/home/developer/k8s-applied/taskboard-api/src/main/java/com/taskboard/TaskService.java
/home/developer/k8s-applied/taskboard-api/src/main/resources/META-INF/persistence.xml

第3回からHealthCheckLive.javaHealthCheckReady.javaの2ファイルが追加されています。Dockerイメージをビルドしましょう。

[Execution User: developer]

cd ~/k8s-applied/taskboard-api
docker build -t taskboard-api:3.0.0 .
[+] Building 45.2s (12/12) FINISHED
 ...
 => => naming to docker.io/library/taskboard-api:3.0.0

[Execution User: developer]

kind load docker-image taskboard-api:3.0.0 --name k8s-applied
Image: "taskboard-api:3.0.0" with ID "sha256:c3d4e5f6..." not yet present on node "k8s-applied-worker", loading...
Image: "taskboard-api:3.0.0" with ID "sha256:c3d4e5f6..." not yet present on node "k8s-applied-worker2", loading...
Image: "taskboard-api:3.0.0" with ID "sha256:c3d4e5f6..." not yet present on node "k8s-applied-worker3", loading...
Image: "taskboard-api:3.0.0" with ID "sha256:c3d4e5f6..." not yet present on node "k8s-applied-control-plane", loading...

イメージがkindクラスタにロードされました。正式版のDeployment(taskboard-api:3.0.0)を適用します。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/taskboard-api-deployment-v4.yaml
deployment.apps/taskboard-api configured

[Execution User: developer]

kubectl rollout status deployment/taskboard-api -n app
deployment "taskboard-api" successfully rolled out

カスタムHealthCheckが正常に動作しているか確認します。

[Execution User: developer]

kubectl exec -n app deploy/taskboard-api -- curl -s http://localhost:8080/health/ready | python3 -m json.tool
{
    "status": "UP",
    "checks": [
        {
            "name": "db-connection",
            "status": "UP",
            "data": {
                "database": "taskboard"
            }
        }
    ]
}

db-connectionチェックがUPになっています。HealthCheckReady.javaが正常に動作し、MySQLへの接続が確認できています。

[Execution User: developer]

kubectl exec -n app deploy/taskboard-api -- curl -s http://localhost:8080/health/live | python3 -m json.tool
{
    "status": "UP",
    "checks": [
        {
            "name": "taskboard-api-live",
            "status": "UP",
            "data": {
                "version": "3.0.0"
            }
        }
    ]
}

livenessも正常です。バージョン情報が3.0.0になっており、新しいイメージが稼働していることが確認できます。

8.6.4 DBを停止してreadinessがDOWNになることを確認する

カスタムReadinessの真価は、DBが停止したときに発揮されます。MySQLのPodを意図的に停止して、TaskBoard APIのreadinessがDOWNになり、Serviceからルーティングが外れることを確認しましょう。

まず、現在のAPIのEndpoints(Serviceに登録されているPod)を確認します。

[Execution User: developer]

kubectl get endpoints taskboard-api -n app
NAME            ENDPOINTS                           AGE
taskboard-api   10.244.1.12:8080,10.244.2.18:8080   5m

2つのPod IPがEndpointsに登録されています。次に、MySQLのPodをスケールダウンしてDB接続を切断します。

[Execution User: developer]

kubectl scale statefulset mysql -n db --replicas=0
statefulset.apps/mysql scaled

15〜20秒ほど待ってから、TaskBoard APIのreadiness状態を確認します。

[Execution User: developer]

kubectl exec -n app deploy/taskboard-api -- curl -s http://localhost:8080/health/ready | python3 -m json.tool
{
    "status": "DOWN",
    "checks": [
        {
            "name": "db-connection",
            "status": "DOWN",
            "data": {
                "reason": "Communications link failure..."
            }
        }
    ]
}

readinessがDOWNになりました。DB接続が失敗しているためです。Podの状態も確認しましょう。

[Execution User: developer]

kubectl get pods -n app -l component=api
NAME                             READY   STATUS    RESTARTS   AGE
taskboard-api-7b2c9d8e6-f5g3h   0/1     Running   0          8m
taskboard-api-7b2c9d8e6-n4p6r   0/1     Running   0          8m

READY0/1になっています。readinessProbeが失敗しているため、Readyではありません。しかしRESTARTSは0のまま — livenessProbe(/health/live)はDB接続を含まないので成功し続けており、コンテナは再起動されていません。Endpointsも確認します。

[Execution User: developer]

kubectl get endpoints taskboard-api -n app
NAME            ENDPOINTS   AGE
taskboard-api   <none>      10m

Endpointsが<none>になりました。ServiceからTaskBoard API Podへのルーティングが完全に外れています。Gateway API経由でアクセスしても、このPodにはトラフィックが送られません。

これがreadinessProbeの効果です。「プロセスは生きているが、リクエストを処理できない」状態を検知し、自動でトラフィックから除外する。DBが復旧すれば自動で復帰します。やってみましょう。

[Execution User: developer]

kubectl scale statefulset mysql -n db --replicas=1
statefulset.apps/mysql scaled

MySQLの起動を待ち、30秒〜1分ほどしてからPodの状態を確認します。

[Execution User: developer]

kubectl get pods -n app -l component=api
NAME                             READY   STATUS    RESTARTS   AGE
taskboard-api-7b2c9d8e6-f5g3h   1/1     Running   0          12m
taskboard-api-7b2c9d8e6-n4p6r   1/1     Running   0          12m

READY1/1に復帰しました。DB接続が回復し、readinessProbeが成功するようになったためです。Endpointsも復帰しているか確認できます。Podの再起動なしに、トラフィックの除外と復帰が自動で行われました。

8.7 Probeパラメータの効果を体験する

8.7.1 failureThresholdを1にする — 一時的な高負荷でPodが再起動される体験

Probeパラメータの設定によってPodの挙動がどう変わるか、もう一つ体験してみましょう。livenessProbeのfailureThresholdを1に変更します。これは「1回でもlivenessProbeが失敗したら即座に再起動」という設定です。

[Execution User: developer]

# failureThresholdを1に変更したDeploymentを適用
cat ~/k8s-applied/taskboard-api-deployment-v4.yaml | \
  sed 's/failureThreshold: 3         # 3回連続失敗で再起動/failureThreshold: 1         # 1回失敗で即再起動(検証用)/' | \
  kubectl apply -f -

Podが更新されたら、一時的な負荷をかけてProbeのタイムアウトを誘発してみます。

[Execution User: developer]

# APIに対して大量の同時リクエストを送信し、一時的にレスポンスが遅延する状況を作る
kubectl run api-stress --image=busybox:1.36 -n app --rm -it --restart=Never -- \
  /bin/sh -c "for i in \$(seq 1 100); do wget -q -O /dev/null http://taskboard-api.app.svc.cluster.local:8080/api/tasks & done; wait"

負荷後にPodの状態を確認します。

[Execution User: developer]

kubectl get pods -n app -l component=api
NAME                             READY   STATUS    RESTARTS      AGE
taskboard-api-8c3d9e7f5-a2b4c   1/1     Running   1 (30s ago)   3m
taskboard-api-8c3d9e7f5-d6e8f   1/1     Running   1 (25s ago)   3m

RESTARTSが1になっています。一時的な高負荷でlivenessProbeのレスポンスが3秒のタイムアウトを超え、failureThreshold: 1のため即座に再起動されてしまいました。

これがfailureThreshold: 1の問題点です。一時的なスパイクでProbeが1回タイムアウトしただけで再起動が発生する。本番環境でこの設定を使うと、「負荷が高い → Pod再起動 → 起動中は処理できない → 残りのPodに負荷集中 → それも再起動」というカスケード障害に発展するリスクがあります。

元の設定(failureThreshold: 3)に戻しましょう。

[Execution User: developer]

kubectl apply -f ~/k8s-applied/taskboard-api-deployment-v4.yaml
deployment.apps/taskboard-api configured

8.7.2 各パラメータがどう効くかのまとめ

今回体験したProbeパラメータの効果をまとめます。

パラメータ効果小さすぎると大きすぎると
initialDelaySecondsProbe開始までの待ち時間起動中にProbeが失敗する(startupProbeで代替可能)障害検知が遅れる
periodSecondsチェックの間隔kubeletの負荷が増える障害検知が遅れる
timeoutSeconds1回のチェックのタイムアウト一時的な遅延で失敗と誤判定する本当の障害検知が遅れる
failureThreshold連続失敗の許容回数一時的なスパイクで再起動される(今回体験済み)本当の障害時に再起動が遅れる
successThreshold成功と判定する連続成功回数(readinessのみ)-(最小値は1)復帰が遅れる

「最大検知時間」はperiodSeconds × failureThresholdで計算できます。livenessProbeの場合、10秒 × 3回 = 30秒で、最大30秒間の無応答で再起動が発生します。readinessProbeの場合、5秒 × 3回 = 15秒で、最大15秒間の無応答でトラフィック除外が発生します。

readinessProbeのperiodSecondsをlivenessProbeより短く(5秒 vs 10秒)設定しているのは、トラフィック制御は迅速に行いたいためです。異常なPodにリクエストが送られ続けるとエラーレスポンスがユーザーに返ってしまいます。一方、livenessProbeは再起動という重いアクションであるため、慎重に判断させています。

Probeパラメータの「最適な値」は、アプリケーションの特性やSLAの要件によって変わります。今回体験した「各パラメータがどう効くか」の理解を持った上で、実践編第3回(詳細設計)でTaskBoardの各コンポーネントに最適な値を設計判断します。

8.8 この回のまとめ

8.8.1 TaskBoardの現在地

今回の作業で、TaskBoardに以下が追加されました。

[app Namespace]
  Nginx (Deployment, replicas: 2) + Service (ClusterIP, targetPort: 8080)
    ★ HPA適用(CPU 70%, min:2, max:6)
    SecurityContext適用済み
  TaskBoard API (Deployment, replicas: 2, MySQL接続版) + Service (ClusterIP)
    ★ HPA適用(CPU 70%, min:2, max:4)
    ★ Probe詳細設定(startup / liveness / readiness)
    ★ カスタムHealthCheck(DB接続確認付き)
    ★ イメージをtaskboard-api:3.0.0に更新
    SecurityContext適用済み
  Gateway (taskboard-gateway) + HTTPRoute (taskboard-route)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み
  + NetworkPolicy適用済み
  + Pod Security Admission: warn=restricted

[db Namespace]
  MySQL (StatefulSet, replicas: 1) + Headless Service + PVC
  + DB初期化Job(Completed)
  + DBバックアップCronJob(稼働中)
  + Secret(MySQL認証情報)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み
  + NetworkPolicy適用済み
  + Pod Security Admission: warn=baseline

[monitoring Namespace]
  ログ収集DaemonSet(全Worker Nodeに配置)
  + ResourceQuota / LimitRange 適用済み
  + RBAC適用済み

[クラスタ全体]
  Calico CNI
  Metrics Server 稼働中
  Gateway API 稼働中(/ → Nginx, /api → TaskBoard API)
  マルチノード構成(CP 1 + Worker 3)

8.8.2 HPA / Probe設計の判断基準 — いつ使う / いつ使わない

判断ケース
HPAを使うべき負荷が時間帯やイベントで変動するワークロード。ステートレスなフロントエンド/APIサーバー
HPAを使わないStatefulSet(DBなど)。レプリカ数が固定の要件があるワークロード。負荷が安定しているバッチ処理
startupProbeが必須起動に10秒以上かかるアプリケーション。JVMベース(Payara Micro, Spring Boot等)。初期化処理が重いワークロード
startupProbeが不要起動が1〜2秒で完了するアプリケーション(Nginx、busybox等)
livenessProbeを設定すべきデッドロックやメモリリークで応答不能になる可能性があるアプリケーション
readinessProbeを設定すべき外部依存(DB、キャッシュ、外部API)がありトラフィック制御が必要なアプリケーション

🔧 トラブルシュートTips

HPAのTARGETS<unknown>のまま変わらない場合は、Metrics Serverの状態を確認してください。kubectl get apiservice v1beta1.metrics.k8s.ioでSTATUSがTrueであることを確認します。Falseの場合はMetrics Serverが正常に動作していません。

startupProbeが成功しないためPodが繰り返し再起動される場合は、kubectl logs -n app -l component=api --previousで前回のコンテナログを確認し、起動中のエラーを特定してください。failureThreshold × periodSecondsが実際の起動時間より短い可能性があります。

readinessProbeが失敗し続ける場合は、kubectl execでPod内から直接ヘルスチェックエンドポイントにアクセスして、レスポンスの内容を確認してください。DB接続エラーの場合はkubectl get pods -n dbでMySQLが稼働しているか確認します。

8.8.3 実践編への橋渡し

今回学んだHPAとProbeの知識は、実践編で以下の場面で使います。

  • 実践編 第3回(詳細設計): 今回体験したProbeパラメータの効果を基に、TaskBoardの各コンポーネント(Nginx、Payara Micro、MySQL)に対して「なぜこの値にしたか」の設計根拠とともにProbe設定を設計書に落とし込みます。今回は「道具の使い方」を覚えましたが、実践編では「この道具をどう使うか」を設計判断します
  • 実践編 第7回(運用設計): HPAの閾値(70%)の妥当性を負荷テストで検証し、スケーリング設計を運用設計書に記録します。クールダウン期間の設定も、サービスのSLA要件に基づいて判断します
  • 実践編 第9回(障害対応): Probeの設定ミスによるCrashLoopBackOffや、readinessProbe失敗によるトラフィック除外は、障害の切り分けで頻出するパターンです。今回の体験が障害原因の特定に直結します

8.8.4 次回予告

HPAとProbeの実装が完了し、TaskBoardには自動スケーリングとヘルスチェックの仕組みが備わりました。次回(第9回)は応用編の最終回です。ここまで1つずつ手作業でkubectl applyしてきたマニフェストファイルが相当な数になっているはずです。「開発環境と本番環境でreplicas数やresourcesだけ変えたい」という課題をHelmで解決します。TaskBoardのフロントエンドマニフェストをHelmチャート化し、values.yamlで環境差分を吸収する方法を学びます。

AIコラム — Probeパラメータの設計をAIに相談する

Probeの各パラメータに「どの値を設定するか」は、アプリケーションの特性(起動時間、外部依存、障害復旧パターン)を考慮して決める必要があります。この設計判断はAIとの壁打ちに向いています。

AIに「Payara Micro 7.2026.1でMySQLに接続するREST APIを動かしている。起動時間は15〜20秒。livenessProbeとreadinessProbeのパラメータを提案して。根拠も示して」と伝えると、アプリケーション特性に基づいたパラメータ案を短時間で得られます。

ただし、AIの提案はあくまで出発点です。以下を自分で検証してください。

  • 提案されたfailureThreshold × periodSecondsが、実際の起動時間(kubectl logsで確認)に対して十分な余裕があるか
  • readinessProbeに含める外部依存チェック(DB接続など)のタイムアウトが、timeoutSecondsの範囲内に収まるか
  • 本番環境の負荷状況で、livenessProbeが誤って失敗しないか(今回体験したfailureThreshold: 1の教訓)

実践編第3回では、AIの提案を批判的にレビューしながらProbe設計を完成させるプロセスを実践します。