コーディング × インフラ複合 — GKE ヘルスチェック設計

2026-04-11 (Day 7) A+B: コーディング × インフラ ★★★☆☆ GKE / Flask / livenessProbe / readinessProbe

概要

❤️

livenessProbe

「Podを再起動すべきか」を判断。プロセス自体の生死のみ確認。依存サービスの状態は見ない(見るとcascading failureを起こす)。

readinessProbe

「トラフィックを流すか」を判断。依存サービス(DB・Cache)に接続できる場合のみ200を返す。失敗時は503でServiceから切り離し。

🔌

プロトコル一致

MySQLはTCP 3306(MySQL独自プロトコル)。RedisはRESP(TCP 6379)。requests.get() ではなく専用クライアントで確認する。

⚠️

ベアexceptの排除

except: はKeyboardInterruptやSystemExitも補足してしまう。必ず具体的な例外クラス(pymysql.Error 等)を指定する。

問題

GKE上で動くPythonサービスのヘルスチェックエンドポイントを実装しています。以下の「悪いコード」を読み、問題点を指摘した上で改善してください。

制約・前提条件

  • Python 3.12 / Flask 3.x
  • GKE上で動作(livenessProbe / readinessProbe 設定済み)
  • 依存サービス: MySQL(TCP接続)、Redis(TCP接続)
  • requests ではなく、実際のプロトコルに合わせた接続確認を行うこと
  • K8sの livenessProbe / readinessProbe の違いも踏まえてステータスコードの設計を説明すること
期待する回答形式: コード + 設計説明

悪いコード (Before)

このコードには 4つの設計上の問題 が隠れています。見つけてみてください。
import requests
from flask import Flask, jsonify

app = Flask(__name__)

DB_HOST = "10.0.0.1"
DB_PORT = 3306
CACHE_HOST = "10.0.0.2"
CACHE_PORT = 6379

@app.route("/health")
def health():
    # DBチェック
    try:
        r = requests.get(f"http://{DB_HOST}:{DB_PORT}/ping", timeout=1)
        db_ok = r.status_code == 200
    except:                          # ← ベアexcept(問題1)
        db_ok = False

    # Cacheチェック
    try:
        r = requests.get(f"http://{CACHE_HOST}:{CACHE_PORT}/ping", timeout=1)  # ← HTTP GETでRedis?(問題2)
        cache_ok = r.status_code == 200
    except:
        cache_ok = False

    if db_ok and cache_ok:
        return jsonify({"status": "ok"}), 200
    else:
        return jsonify({"status": "error"}), 200  # ← 常に200を返す(問題3)
        # ↑ livenessとreadinessの区別もない(問題4)

ヒント(段階的開示)

ヒント1 — 方向性
requests でMySQLやRedisにHTTP GETするのは根本的に間違い。それぞれのプロトコルで接続確認する方法を考えよう。また、ステータスコードが常に200なのはK8sにとって何が問題か?
ヒント2 — アプローチ
  • MySQL接続確認: pymysql でTCP 3306に接続してみる
  • Redis接続確認: redis-pyping() を使う
  • livenessProbe は「Podを再起動すべきか」を判断する。readinessProbe は「トラフィックを流すか」を判断する
  • 両者で /health/live/health/ready を分けるのが実践的
ヒント3 — 目指す構造
# liveness: プロセス自体が死んでいないかを確認(依存サービスは見ない)
@app.route("/health/live")
def liveness():
    return jsonify({"status": "ok"}), 200

# readiness: 依存サービスに接続できるかを確認(失敗時は503)
@app.route("/health/ready")
def readiness():
    checks = {}
    checks["db"] = _check_db()
    checks["cache"] = _check_cache()
    ok = all(checks.values())
    return jsonify({"status": "ok" if ok else "degraded", "checks": checks}), 200 if ok else 503

問題点分析

#問題点分類影響改善方法
1 requests.get() でMySQL/Redisに接続しようとしている プロトコル不一致 MySQL/RedisはHTTPではないため必ず失敗する(ハンドシェイクエラー) pymysql / redis-py の専用クライアントを使う
2 常に200を返す(障害時も200) ステータスコード設計 K8sのProbeは200-399を成功とみなすため「障害中でもPodが正常」と判断される readinessは失敗時に503を返す
3 ベアexcept(except:)の使用 例外設計 KeyboardInterruptやSystemExitも捕捉してしまう except pymysql.Error: / except redis.RedisError: と具体化
4 liveness/readinessの区別がない 設計ミス 依存サービス障害でlivenessをFailさせると、Pod再起動ループで障害を悪化させる /health/live/health/ready に分離

Probe設計図 — liveness vs readiness 分離

K8s Control livenessProbe /health/live → 失敗でPod再起動 readinessProbe /health/ready → 失敗でService外し Flask App (Pod) /health/live 常に200 OK(依存なし) /health/ready DB: pymysql.connect() Cache: redis.ping() MySQL TCP 3306 pymysql.connect() Redis TCP 6379 (RESP) redis.ping()

模範解答

"""GKE向けヘルスチェックエンドポイント。

liveness と readiness を分離し、K8s Probe の仕様に準拠した
ステータスコードを返す。依存サービスは各プロトコルで確認する。
"""

import socket
from http import HTTPStatus

import pymysql
import redis
from flask import Flask, jsonify

app = Flask(__name__)

# 接続設定(実運用では環境変数から取得)
DB_HOST = "10.0.0.1"
DB_PORT = 3306
DB_USER = "health_check"
DB_PASSWORD = "secret"

CACHE_HOST = "10.0.0.2"
CACHE_PORT = 6379

CONNECT_TIMEOUT_SEC = 2


def _check_db() -> bool:
    """MySQLにTCP接続できるかを確認する。"""
    try:
        conn = pymysql.connect(
            host=DB_HOST,
            port=DB_PORT,
            user=DB_USER,
            password=DB_PASSWORD,
            connect_timeout=CONNECT_TIMEOUT_SEC,
        )
        conn.close()
        return True
    except pymysql.Error:
        return False


def _check_cache() -> bool:
    """RedisにPINGを送って応答を確認する。"""
    try:
        client = redis.Redis(
            host=CACHE_HOST,
            port=CACHE_PORT,
            socket_connect_timeout=CONNECT_TIMEOUT_SEC,
        )
        return client.ping()
    except redis.RedisError:
        return False


@app.route("/health/live")
def liveness():
    """livenessProbe 用エンドポイント。

    プロセス自体が応答できるかだけを確認する。
    依存サービスの状態は見ない(依存サービス障害でPodを再起動しても無意味なため)。
    """
    return jsonify({"status": "ok"}), HTTPStatus.OK


@app.route("/health/ready")
def readiness():
    """readinessProbe 用エンドポイント。

    依存サービス(DB・Cache)に接続できる場合のみトラフィックを受け付ける。
    接続不可の場合は 503 を返し、Podをロードバランサから切り離す。
    """
    checks = {
        "db": _check_db(),
        "cache": _check_cache(),
    }
    all_healthy = all(checks.values())
    status_code = HTTPStatus.OK if all_healthy else HTTPStatus.SERVICE_UNAVAILABLE

    return (
        jsonify(
            {
                "status": "ok" if all_healthy else "degraded",
                "checks": checks,
            }
        ),
        status_code,
    )
livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 2

ポイント解説

1 ステータスコード設計
K8sのProbeは200-399を成功、それ以外を失敗とみなす。常に200を返すと「障害中でもPodが正常」と判断され、トラフィックが流れ続ける。readinessは失敗時に503を返すことでPodをServiceから切り離す。
2 liveness / readiness の分離
livenessはプロセスの生死のみ確認する。依存サービスの障害でlivenessをFailさせると、Pod再起動ループに入り障害を悪化させる(cascading failure)。readinessだけを503にすればPodは生き続け、依存サービス復旧後に自動復帰できる。
3 プロトコル一致
MySQLはHTTPではなくMySQL独自プロトコル(TCP 3306)。RedisもHTTPではなくRESP(TCP 6379)。requests.get() でこれらに接続しようとしてもハンドシェイクで必ず失敗する。
4 ベアexceptの排除
except: はKeyboardInterruptやSystemExitも補足してしまう。必ず具体的な例外クラスを指定する。

実務への応用

MOps/販促システムのArgo Workflowsジョブ

BigQueryやCloud Pub/Subに依存している場合、同様のヘルスチェック設計が活きる。WorkflowのPodに readinessProbe を設定しておくと、依存サービス障害時にジョブが途中でハングせずに早期失敗できる。また、DatadogのService Checkと組み合わせて、/health/ready の503をアラートトリガーにするのが実践的。

次のステップ

発展問題: /health/readyタイムアウト付き並列チェックconcurrent.futures.ThreadPoolExecutor)を実装し、複数依存サービスの応答を並列に確認する
  • 参考: Kubernetes公式ドキュメント「Configure Liveness, Readiness and Startup Probes」
  • 参考: Google Cloud「GKE best practices: health checks」

今日のまとめ

liveness(再起動判断)とreadiness(トラフィック制御)を分離し、障害時は503を返すことで、K8sのセルフヒーリングを正しく機能させる。

依存サービスの確認は必ずそのプロトコルで行う(MySQL → pymysql、Redis → redis.ping())。

except: ではなく except SpecificError: が例外処理の基本。

自己評価

自分の回答

気づき・メモ