プログラマのための IT 教科書

コンテナと Kubernetes

この部の 9 / 13 章 ・ 全体で 51 / 76 章 ・ 読了目安 55 分

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

サーバー1台にアプリを置いて動かす——最初はこれで十分です。 しかし、サービスが育つと次が起きます。

□ 深夜にプロセスが落ちた。誰も気づかず、朝まで停止していた
□ アクセスが増えたので台数を増やしたい。手作業で10台に同じ設定を入れる
□ デプロイのたびに、数分間サービスが止まる
□ 「このサーバーだけ設定が違う」が、誰も理由を知らないまま残っている
□ 1台が壊れた。復旧手順は担当者の頭の中にしかない

これらはすべて「人間が手で管理している」ことが原因です。 台数が増えるほど、手作業は破綻します。

そこで、「あるべき状態」を宣言しておけば、 機械が勝手にその状態を保ち続ける仕組みが求められました。 落ちたら再起動し、増やせと言えば増やし、更新は少しずつ入れ替える。

それがコンテナオーケストレータであり、その事実上の標準が Kubernetes です。

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

コンテナのおさらい

Docker を使いこなすで扱ったとおり、コンテナは軽い仮想マシンではなく、隔離されたプロセスです。 ホストのカーネルを共有し、名前空間で見える範囲を、cgroup で使える量を区切っています。

ここで効いてくるのは次の2点です。

  • 軽いから、たくさん動かせる。 1台に何十個も載せられるからこそ、機械が配置を決める意味が出る
  • cgroup で上限を決められる。 だから Kubernetes は「このコンテナには CPU 0.5、メモリ 512Mi」と宣言できる

イメージとレイヤー

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 の仕事です。

用語: 宣言的 / オーケストレーション
命令的(手順を書く)  「コンテナを3つ起動しろ」「1つ落ちたら起動しろ」
宣言的(状態を書く)  「コンテナが常に3つある状態にしておいて」

宣言的だと、現状との差分をシステムが埋め続けてくれます。 1つ落ちれば自動で1つ起動されます。手順ではなく、あるべき姿を書く—— これは Terraform(Terraform と Infrastructure as Code)とも共通する考え方です。

オーケストレーションは、多数のコンテナの配置・起動・停止・ 入れ替えを自動で調整すること。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 で直した変更は消える

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

kubectl edit や kubectl 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 pods → describe → logs --previous
  • 繋がらない時は kubectl get endpoints が空でないか
  • OOMKilled は memory limit 超過

公式ドキュメント

迷ったら一次情報に戻ってください。

章末問題

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

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

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

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

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