プログラマのための IT 教科書
第5部 サービスをつくる

コンテナと Kubernetes

読了目安 55

この章を読むとできるようになること
  • コンテナと VM の違いを説明できる
  • Pod が起動しない原因を kubectl で特定できる
  • Service に繋がらない時の調べ方を知っている

書いたコードは、最終的にどこかで動きます。その「どこか」が Kubernetes です。

新人が Kubernetes に求められるのは、クラスタを構築することではありません。 自分のサービスが動かない時に、原因を特定できることです。 この章はそこに絞ります。

コンテナとは何か

仮想マシンとの違い

仮想マシン                        コンテナ
┌──────────┬──────────┐          ┌──────────┬──────────┐
│  アプリ   │  アプリ   │          │  アプリ   │  アプリ   │
│  ライブラリ│  ライブラリ│          │ ライブラリ │ ライブラリ │
│  ゲストOS │  ゲストOS │  ← 重い  ├──────────┴──────────┤
├──────────┴──────────┤          │   コンテナランタイム   │
│   ハイパーバイザ      │          ├─────────────────────┤
├─────────────────────┤          │      ホストOS        │
│      ホストOS        │          └─────────────────────┘
└─────────────────────┘

コンテナはOS を丸ごと持ちません。ホストのカーネルを共有し、 プロセスから見える世界だけを分離します。だから軽く、起動が速いのです。

コンテナの中身は Linux

第1章で触れた話に戻ります。あなたが macOS で開発していても、 コンテナの中は Linuxです。

そのため次の差が出ます。

  • シェルスクリプトの挙動(BSD と GNU)
  • ファイルパスの大文字小文字の扱い(macOS は区別しないことが多い)
  • ネイティブモジュールのビルド対象

「ローカルで動くのにコンテナで動かない」の大半がこれです。

イメージとレイヤー

Dockerfile の各命令が1つのレイヤーになり、積み重なってイメージができます。

FROM node:22-alpine        # レイヤー1
WORKDIR /app
COPY package.json pnpm-lock.yaml ./   # レイヤー2
RUN pnpm install --frozen-lockfile    # レイヤー3
COPY . .                              # レイヤー4
RUN pnpm build                        # レイヤー5
CMD ["node", "dist/main.js"]

変わっていないレイヤーはキャッシュが使われます。 だから「変わりにくいものを先に、変わりやすいものを後に」書きます。

上の例で COPY . . を先頭に持ってくると、 コードを1文字直すたびに pnpm install からやり直しになります。

イメージは小さく

node:22 は約1GB、node:22-alpine は約150MB です。

イメージが小さいと、デプロイが速くなり、 攻撃対象になりうるパッケージも減ります。

本番用にはマルチステージビルドで、ビルド用と実行用を分けるのが定石です。

なぜオーケストレータが必要か

コンテナを1つ動かすだけなら docker run で足ります。 しかし実際には、次のことが必要になります。

  • 落ちたら自動で再起動する
  • 負荷に応じて数を増やす
  • どのマシンに配置するか決める
  • 新バージョンに順次入れ替える(無停止で)
  • アクセス先を1つの窓口にまとめる

これを自動でやるのが Kubernetes です。

宣言的であること

Kubernetes の考え方の核心です。

命令的: 「コンテナを3つ起動しろ」
宣言的: 「常に3つ動いている状態にしろ」   ← Kubernetes はこちら

1つ落ちたら、Kubernetes が勝手に1つ起動して3つに戻します。 あなたは「あるべき状態」を書くだけで、そこへ近づけるのは Kubernetes の仕事です。

主要なリソース

リソース役割
Podコンテナを動かす最小単位。通常1 Pod に1コンテナ
Deployment「Pod を何個動かすか」を宣言する。更新も担当
ServicePod への安定したアクセス口。ラベルで対象を選ぶ
Ingress外部からの HTTP を Service に振り分ける
ConfigMap設定値(非機密)
Secret認証情報など(機密)

関係

Ingress(外からの入口)
    ↓
Service(安定した窓口。Pod が入れ替わっても変わらない)
    ↓ セレクタでラベルが一致する Pod を選ぶ
Pod  Pod  Pod
    ↑ Deployment が「3つ」を維持している

Pod は使い捨てです。落ちれば新しいものが作られ、IP も変わります。 だから直接 Pod を指定せず、Service を通します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api          # ← このラベルの Pod を管理する
  template:
    metadata:
      labels:
        app: api        # ← Pod に付けるラベル
    spec:
      containers:
        - name: api
          image: asia-northeast1-docker.pkg.dev/proj/api:v1.2.3
          ports:
            - containerPort: 8080

Probe — 生きているかの確認

Kubernetes は、Pod が正常かどうかを自分で判断できません。 アプリ側が答える必要があります

Probe質問失敗すると
readinessProbe今リクエストを受けられる?Service から外される(再起動はしない)
livenessProbe生きてる?コンテナが再起動される
startupProbe「起動終わった?」起動が遅いアプリ用
readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
livenessProbe に DB チェックを入れない

「DB に繋がるか」を livenessProbe に入れると、 DB が一時的に不調になった時に、全 Pod が一斉に再起動します

アプリは生きているのに再起動を繰り返し、障害が拡大します。

  • liveness — 自分自身が壊れているか(デッドロックなど)だけを見る
  • readiness — 依存先を含めて、今リクエストを処理できるかを見る

障害の読み方

この節が実務で最も使います。

kubectl get pods                    # まず全体を見る
kubectl describe pod <pod-name>     # イベントと状態の詳細
kubectl logs <pod-name>             # アプリのログ
kubectl logs <pod-name> --previous  # 再起動前のログ(クラッシュの原因)

よくある症状と原因

状態意味見るところ
ImagePullBackOffイメージを取得できないタグの typo、レジストリの権限
CrashLoopBackOff起動しては落ちるの繰り返しlogs --previous
OOMKilledメモリ上限を超えて強制終了resources.limits.memory
Pendingどのノードにも置けないリソース不足、describe のイベント
Running だが繋がらないService のセレクタ不一致などkubectl get endpoints

診断の順番

# 1. 状態を見る
kubectl get pods
# api-7d9f8-x2k4l   0/1   CrashLoopBackOff   5   3m
 
# 2. イベントを見る(describe の一番下)
kubectl describe pod api-7d9f8-x2k4l
 
# 3. 落ちる前のログを見る(ここに答えがあることが多い)
kubectl logs api-7d9f8-x2k4l --previous
--previous が鍵

CrashLoopBackOff の Pod に kubectl logs を実行しても、 再起動直後の新しいログしか出ません

落ちた理由は前のコンテナのログにあります。 --previous-p)を付けてください。これを知っているかどうかで 調査時間が10分と1時間くらい変わります。

Service に繋がらない — Endpoints が空

最も気づきにくい障害です。

kubectl get endpoints api
# NAME   ENDPOINTS   AGE
# api    <none>      5m      ← 空!

Service はラベルのセレクタで Pod を選びます。 これが一致していないと、Pod は元気に動いているのにどこにも繋がりません

# Service
spec:
  selector:
    app: api-server     # ← こちらは api-server
 
# Deployment の Pod
metadata:
  labels:
    app: api            # ← こちらは api。一致していない

エラーもログも出ません。Endpoints が空かどうかを見るのが唯一の手がかりです。

Pod が CrashLoopBackOff になっています。原因を調べる最初の一手は?

リソース制限

resources:
  requests:              # 最低これだけ確保してほしい(配置の判断に使う)
    cpu: 100m            # 0.1 コア
    memory: 128Mi
  limits:                # これを超えたら制限する
    cpu: 500m
    memory: 512Mi
超えるとどうなるか
CPU の limit遅くなる(スロットリング)。落ちはしない
memory の limit強制終了(OOMKilled)。落ちる
requests を書かないと危険

requests は「配置を決めるための申告」です。書かないと、 Kubernetes は「リソースを使わない」と見なして詰め込みます。

結果、1つのノードに Pod が集まりすぎてメモリ不足になり、 巻き込まれて他の Pod まで落ちます

新人が触ることは少ない設定ですが、 「OOMKilled が出たら limit を疑う」 ことだけ覚えておいてください。

ConfigMap と Secret

設定をイメージから切り離すための仕組みです。

env:
  - name: LOG_LEVEL
    valueFrom:
      configMapKeyRef: { name: api-config, key: log_level }
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef: { name: api-secret, key: db_password }
Secret は暗号化ではない

Kubernetes の Secret は、Base64 エンコードされているだけです。 暗号化ではありません。

kubectl get secret api-secret -o jsonpath='{.data.db_password}' | base64 -d

これで誰でも読めます。本番では Secret Manager などの 外部の仕組みと連携させるのが一般的です。

少なくとも、Secret の YAML を Git にコミットしてはいけません。

デプロイの流れ

1. コードを push
2. CI がイメージをビルドしてレジストリへ push
3. マニフェストの image タグを新しいものに更新
4. kubectl apply(または Argo CD などが自動で適用)
5. Kubernetes が Pod を順次入れ替える(ローリングアップデート)
6. readinessProbe が OK になった Pod から Service に入る

問題があれば戻します。

kubectl rollout undo deployment/api      # 直前のバージョンに戻す
kubectl rollout status deployment/api    # 進行状況を見る
kubectl edit で直した変更は消える

第3章でも触れましたが、重要なので再掲します。

kubectl editkubectl scale での変更は、クラスタの状態だけを変えます。 Git のマニフェストは変わらないので、次のデプロイで上書きされて消えます

調査のための一時的な変更には便利ですが、 恒久的な変更は必ずマニフェストを直して PR を出してください。

実務の落とし穴まとめ

  1. logs だけ見て --previous を忘れる — CrashLoopBackOff の原因が見えない
  2. Service のセレクタ不一致 — Endpoints が空。エラーが出ないので気づきにくい
  3. livenessProbe に依存先チェック — DB 不調で全 Pod が再起動する
  4. Dockerfile の COPY の順序 — キャッシュが効かずビルドが毎回遅い
  5. Secret を暗号化だと思う — Base64 なので誰でも読める
  6. kubectl edit で直す — 次のデプロイで消える

まとめ

  • コンテナは OS を持たない。中身は Linux
  • Dockerfile は変わりにくいものを先に書く(キャッシュ)
  • Kubernetes は宣言的。「あるべき状態」を書けば、そこへ近づけてくれる
  • Pod は使い捨て。アクセスは Service を通す
  • readiness は Service から外れる、liveness は再起動。依存先チェックは readiness へ
  • 障害調査は get podsdescribelogs --previous
  • 繋がらない時は kubectl get endpoints が空でないか
  • OOMKilled は memory limit 超過

章末問題

Pod は Running なのに、Service 経由でアクセスすると繋がりません。最初に確認することは?

DB が数分間不調になった際、全ての Pod が再起動を繰り返して障害が拡大しました。設定上の問題は?

Dockerfile で COPY . . を pnpm install より前に書くと何が起きますか。

次の章では、こうして動かした複数のサービスを、 どう安全に繋ぐかを扱います。

読み終わったら記録しておくと、目次で進み具合が分かります。