目次
4 章

Deployment と ReplicaSet

宣言型デプロイとローリングアップデートを扱います。Deployment / ReplicaSet / Pod の三段の関係をつかみ、replicas: 3 の self-healing、RollingUpdate の maxSurge / maxUnavailable、rollout undo によるロールバック、Deployment が解かないワークロード (StatefulSet・DaemonSet・Job) までを一連の流れで整理します。

第3章 kubectl と最初の Pod の最後で確認した一行 — Pod は mortal なので直接起動すると消えるだけだ — が本章の出発点になります。本章ではその空白を自動で埋める最初のコントローラである Deployment と、その下の ReplicaSet を扱います。replicas: 3 を宣言して Pod を維持する方法、Pod 1個を消したときに自動復旧される原理、イメージタグを変えたときにローリングアップデートとロールバックがどう動作するのかまでを一連の流れで整理します。

本章の終わりには Pod を人がいちいち起動せずコントローラに任せる最初のマニフェスト が手に入ります。ここからが事実上、運用で人が書くマニフェストの基本形です。

Deployment、ReplicaSet、Pod — 三段の関係 #

本章の主役は3つのリソースですが、人が直接書くのは一番上の1層だけです。頭の中の図はこうつかんでおくと楽です。

三段の構造
   ┌──────────────────────┐
   │     Deployment       │  ← 人が書くマニフェスト
   │  (web)               │
   └──────────┬───────────┘
              │ 作る/管理する
   ┌──────────────────────┐
   │     ReplicaSet       │  ← Deployment が自動で作る
   │  (web-abc123)        │
   └──────────┬───────────┘
              │ 作る/維持する
   ┌──────────┬──────────┬──────────┐
   │   Pod    │   Pod    │   Pod    │  ← 実際のワークロード
   │ web-...  │ web-...  │ web-...  │
   └──────────┴──────────┴──────────┘

各段の責任を一行ずつ整理すると次の通りです。

  • Deployment — 人が書くマニフェストです。「この Pod テンプレートで N 個を起動しておき、新バージョンへどう変えるか (ローリングアップデート)」まで書きます。事実上、運用で最もよく触るリソースです。
  • ReplicaSet — Deployment が自動で作り出す中間オブジェクトです。責任は1つ — 「この Pod テンプレートで N 個を常に維持する」 です。人が直接 ReplicaSet マニフェストを書くことはほとんどありません。
  • Pod — 実際のワークロードです。ReplicaSet が作り出し、死ねば ReplicaSet が再び作ります。第3章 で手で起動していたあの Pod ですが、今は誰が殺しても勝手に再び起動してきます。

なぜ2層なのか #

最初に見ると Deployment 1層で十分に見えます。ReplicaSet はなぜ別に存在するのかと問いたくなります。理由は新バージョンのデプロイにあります。

新バージョンをデプロイするとき Deployment は 新しい ReplicaSet をもう1つ 作ります。その新しい ReplicaSet の replicas を1、2、3 と段階的に上げながら、古い ReplicaSet の replicas を3、2、1 と段階的に下げます。その間に2つの ReplicaSet の Pod たちが一緒に起動している短い区間が生まれます — これがローリングアップデートの本体です。終わると古い ReplicaSet は0個で空になりつつもオブジェクトとしては残り、ロールバックが必要なときにそちらを再び N 個に増やす形で戻します。

要するに Deployment は「バージョン間の切り替えを扱う層」 であり ReplicaSet は「1つのバージョンを N 個で維持する層」 です。両者が分かれているので、同じクラスタの中に古いバージョンと新しいバージョンが一瞬共存できます。

最初の Deployment マニフェスト #

同じ nginx:1.27 を今度は Pod ではなく Deployment で書きます。ファイル名は web.yaml にして、次のように書きます。

web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80

第3章 で見た Pod マニフェストと比べると、新しく入ってきた部分が3つあります。

  • apiVersion: apps/v1 — Pod は v1 でしたが Deployment は apps/v1 API グループに入っています。コントローラ系列のリソース (Deployment、StatefulSet、DaemonSet、ReplicaSet) はすべて同じグループに属します。
  • spec.replicas: 3 — この Pod テンプレートが常に3個起動していなければならないという宣言です。
  • spec.selector.matchLabels + spec.template — Deployment が自分が管理する Pod を見つけるラベル条件と、その Pod がどんな形であるべきかのテンプレートです。template の下の形は 第3章 で見た Pod の metadata + spec とまったく同じです。

1つのルール — selector と template のラベルが一致しなければならない #

最初にマニフェストを書くとき最もよく起きるミスがここです。spec.selector.matchLabelsspec.template.metadata.labels は互いに一致しなければなりません。 一致しないと K8s がマニフェストを拒否します。単なる推奨規約ではなく apiserver が持っている検証ルールです。

上のマニフェストで両方を app: web に合わせておいたのがその理由です。もし selector を app: web にして template のラベルだけ app: nginx に変えたなら、kubectl apply が次のようなエラーを吐きます。

ラベルが食い違ったとき
The Deployment "web" is invalid: spec.template.metadata.labels:
  Invalid value: map[string]string{"app":"nginx"}:
  `selector` does not match template `labels`

頭の中のシンプルなモデルは次の通りです — selector は「自分が管理する Pod をどう見分けるか」であり、template は「自分が作る Pod のラベル」 です。両者が一致してこそ、自分が作った Pod を再び自分が見分けられるという、ほぼ同語反復に近いルールです。

適用してみる #

web.yaml をクラスタに反映します。

Deployment を作る
kubectl apply -f web.yaml
出力例
deployment.apps/web created

3種類のリソースを一度に見ましょう。kubectl get はカンマで複数のリソース種類を一度に受け取れます。

三段を一度に
kubectl get deploy,rs,pods
出力例
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web   3/3     3            3           20s

NAME                            DESIRED   CURRENT   READY   AGE
replicaset.apps/web-abc123      3         3         3       20s

NAME                  READY   STATUS    RESTARTS   AGE
pod/web-abc123-aa11   1/1     Running   0          20s
pod/web-abc123-bb22   1/1     Running   0          20s
pod/web-abc123-cc33   1/1     Running   0          20s

読み方は次の通りです。

  • Deployment の行READY 3/3 は desired 3個がすべて ready、UP-TO-DATE 3 は現在のテンプレートで更新された Pod 数、AVAILABLE 3 は minReadySeconds まで生きていてトラフィックを受けられる Pod 数です。
  • ReplicaSet の行 — 名前が web-abc123 の形です。後ろの abc123 は K8s がテンプレートハッシュで自動生成した任意値です。DESIRED 3 / CURRENT 3 / READY 3 が核心のカラムです。
  • Pod の行 — 名前が web-abc123-aa11 のような二段の任意値を付けています。前の部分 (web-abc123) が ReplicaSet 名と一致しているのが見えます。誰が自分を作ったかが名前にそのまま現れます。

名前のパターンを一行で押さえておくと — <deployment>-<replicaset-hash>-<pod-suffix> です。この形は本書の最後までよく出会います。

Pod を殺してみる — self-healing #

本章のマニフェストの最初の効果を確認する番です。第3章 では Pod を消すとただ消えました。今回はどう違うか確認します。

Pod を1個強制削除
kubectl delete pod web-abc123-aa11
出力例
pod "web-abc123-aa11" deleted

すぐに Pod 一覧を再び取得してみましょう。

再び見る
kubectl get pods
出力例
NAME                  READY   STATUS    RESTARTS   AGE
web-abc123-bb22       1/1     Running   0          2m
web-abc123-cc33       1/1     Running   0          2m
web-abc123-dd44       1/1     Running   0          5s

3個がそのまま起動しています。ただ1行を詳しく見ると違いが見えます — bb22cc33AGE 2m なのに、新しく見える dd44AGE 5s です。たった今新しく起動した Pod です。名前の後ろの任意値が変わったのも新しい Pod という手がかりです。

これが 第1章 Kubernetes とは で図で見た reconcile loop が働く様子です。ReplicaSet は「Pod 3個があるべき」を持っていて、人が1個消した瞬間 desired (3) と actual (2) が食い違いました。controller-manager の中の ReplicaSet コントローラがその差を感知し、Pod を1個追加で作れと apiserver に要求します。scheduler がノードを決め、kubelet がコンテナを起動し、再び3個に合わせられます。人が別途やったことはありません。

同じことがノード単位でも起こります。ある Pod が起動していたノードが死ぬと、K8s はその Pod たちを別の生きているノードへ移して再び起動します。第1章 で見た「ノードが死んでもサービスは生きていなければならない」が実はこの ReplicaSet コントローラが解いてくれる問題です。

replicas の調整 #

3個では足りなかったり多すぎたりするとき、個数を調節する道は2通りあります。

宣言型 — マニフェストの数字を変えて再び apply します。最もきれいな道です。

web.yaml — replicas だけ5に
spec:
  replicas: 5
  ...
再び適用
kubectl apply -f web.yaml
出力例
deployment.apps/web configured

kubectl get pods で見るとまもなく5個に増えています。減らすときも同じやり方です — マニフェストの数字を減らして apply します。

命令型 — 速いですが一時的です。

命令型スケール
kubectl scale deploy/web --replicas=5
出力例
deployment.apps/web scaled

瞬間的に増やしたり減らしたりするには軽いです。ただし短所が明らかです — マニフェストの replicas 値とクラスタの実際の状態が食い違います。 マニフェストには依然として replicas: 3 が書かれているのにクラスタには5個が起動している状態になるからです。次に誰かが何も考えずに kubectl apply -f web.yaml をもう一度呼ぶと、5個が再び3個に減ってしまいます。

だから一行で整理しておくと — 宣言型マニフェストが常に信頼できる情報源 (source of truth) です。kubectl scale はデバッグ中に素早く手を入れなければならないときや、マニフェストをまもなく再び同期するつもりのときだけ使い、正常な流れはマニフェストを直して apply です。この原則が 第1章 で見た desired state モデル全体の土台であり、第20章 GitOps で ArgoCD / Flux でもう一度本格的に扱います。

replicas を人が決めずに負荷に応じて自動で増やしたり減らしたりするオプションは 第13章 オートスケーリング で HPA・VPA・Cluster Autoscaler として扱います。

ローリングアップデート — 無停止デプロイの基本動作 #

ではいよいよ本書で初めて 新バージョンをデプロイ します。イメージタグを nginx:1.27 から nginx:1.28 へ1文字だけ変えます。

web.yaml — イメージタグだけ 1.28 に
      containers:
        - name: web
          image: nginx:1.28
          ports:
            - containerPort: 80
新バージョン適用
kubectl apply -f web.yaml
出力例
deployment.apps/web configured

表面に落ちたメッセージは1行ですが、中ではかなり大きなことが起きています。

中で起きること #

Deployment コントローラはテンプレートが変わったのを見て、その新しいテンプレート用の 新しい ReplicaSet を1つ作ります。それから新 RS の replicas は0から1、2、3へ増やし、古い RS の replicas は3から2、1、0へ減らしていきます。1段階ごとに新しい Pod が Ready 状態に入ってこそ次の段階へ進みます。

進行中に kubectl get rs で見ると ReplicaSet が2行見えます。

ロールアウトの途中
kubectl get rs
出力例 — 真っ最中
NAME             DESIRED   CURRENT   READY   AGE
web-abc123       2         2         2       10m   ← 旧 RS (1.27)
web-def456       2         2         1       30s   ← 新 RS (1.28)

古い RS は3から2に減っていて、新しい RS は0から2に上がってきています。この1コマがローリングアップデートの本体です。2つの RS の Pod が一瞬一緒に起動しています — その間のトラフィックは 第5章 Service で扱う Service が満遍なく分配します。

進行のモニタリング #

ロールアウトの進行状況を一行で見たいなら、次のコマンドが最も楽です。

ロールアウトの進行状況
kubectl rollout status deploy/web
出力例
Waiting for deployment "web" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 1 old replicas are pending termination...
deployment "web" successfully rolled out

画面に進行段階が一行ずつ落ちてきて、最後に成功が出たら新バージョンのデプロイが終わったということです。終わったあと kubectl get rs を再び見ると、古い RS が DESIRED 0 で空になりつつもオブジェクトとしては残っています。この構造が次の節のロールバックを可能にします。

デフォルトの strategy を一行 #

上の流れが起こる正確な理由は、Deployment の spec.strategy のデフォルト値が RollingUpdate であり、その中の2つのパラメータが次の通りだからです。

  • maxSurge: 25% — desired 個数より一時的に多く起動してもよい上限です。3個基準で1個まで追加が許容されます。
  • maxUnavailable: 25% — desired 個数より一時的に不足してもよい上限です。3個基準で1個まで不足が許容されます。

別のオプションは Recreate です — 古い Pod を全部殺して新しい Pod を起動する方式です。無停止にはなりませんが、2つのバージョンが同時に起動してはいけないステートフルなワークロード (例: 同じボリュームを占有する DB マイグレーション) でたまに使います。一般的な Web サーバーならデフォルト値の RollingUpdate で十分です。

失敗したらどうなるか #

イメージタグをわざと間違って書いてみましょう — たとえば nginx:1.99-not-real のような具合です。

存在しないタグで適用
kubectl apply -f web.yaml

kubectl rollout status deploy/web がしばらく止まっていて、kubectl get pods で見ると新しく作られた Pod が1つ ImagePullBackOff 状態にあります。

出力例
NAME                  READY   STATUS             RESTARTS   AGE
web-abc123-aa11       1/1     Running            0          15m
web-abc123-bb22       1/1     Running            0          15m
web-abc123-cc33       1/1     Running            0          15m
web-ghi789-zz99       0/1     ImagePullBackOff   0          40s

興味深いのは 古い Pod 3個がそのまま生きている という点です。新しい Pod が Ready に入ってこられないと Deployment は次の段階へ進みません。つまり古い RS を0に減らしません。ロールアウトが止まっている間も古いバージョンはトラフィックを正常に受けます。 無停止の核心がここにあります。

このときのデバッグの順序は 第3章 の最後で整理したそのままです。

止まったロールアウトを覗き込む
kubectl describe deploy/web
kubectl describe pod web-ghi789-zz99

describe deploy の Events には ReplicaSet ... has timed out progressing のようなメッセージが、describe pod の Events には Failed to pull image "nginx:1.99-not-real" が書かれています。答えはほぼ常にこの2つの出力の中にあります。診断ツリーの完成版は 第27章 kubectl デバッグパターン で整理します。

ロールバック #

新バージョンが間違って上がったのを見つけたら、古いバージョンへ戻す道が一行で用意されています。

ロールアウト履歴
kubectl rollout history deploy/web
出力例
deployment.apps/web
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

リビジョン一覧が見えます。1番が最初の nginx:1.27、2番がたった今上げた nginx:1.28 です。直前のリビジョンへ戻すには次のようにします。

直前のリビジョンへ戻す
kubectl rollout undo deploy/web
出力例
deployment.apps/web rolled back

特定のリビジョンを指定したいなら --to-revision フラグを使います。

特定のリビジョンへ
kubectl rollout undo deploy/web --to-revision=1

これが可能な理由は一行で整理されます — リビジョンは ReplicaSet の別の姿 です。新バージョンをデプロイするとき古い ReplicaSet は消えずに replicas: 0 で空にされたまま残っていたからです。undo はその古い RS を再び N 個に増やす作業です。だからほぼ即座に古いバージョンが再びトラフィックを受けます。

デフォルトで K8s はリビジョンを10個まで保管します。マニフェストの spec.revisionHistoryLimit で増やしたり減らしたりできます。長すぎると古い ReplicaSet が登録簿に積み上がって整理が煩雑になり、短すぎるとずっと前のバージョンへ一度に戻れないので、運用環境のデプロイ頻度に合わせて決めます — 一般的な Web サービスならデフォルト値の10が無難です。

Deployment が解いてくれないもの #

ここまでの Deployment がすべてのワークロードの形を扱うわけではありません。筋が異なる場合を一段落で押さえておきます。

  • ステートフル (stateful) ワークロード — データベースのように各インスタンスが自分の名前と自分のディスクを持たなければならないワークロードは StatefulSet が合います。Pod 名が web-0web-1 のように安定して付けられ、それぞれにマニフェストで定義した PVC が1:1で対応します。起動順序も 012 で保証されます。Deployment は Pod の名前とディスクの両方を任意値で扱うため DB には適しません。
  • ノードごとに1個ずつ起動すべきワークロード — ログ収集器 (Fluent Bit、Filebeat)、ノードモニタ (Node Exporter)、CNI エージェントといったワークロードは DaemonSet が適します。新しいノードがクラスタに合流すると自動でそのノードにも1個が起動してきます。
  • 使い捨ての作業 — マイグレーション、バックアップ、バッチジョブのように一度回って終わる作業は Job (即時実行) または CronJob (スケジュール実行) を使います。Pod の Phase が Succeeded へ入るのが自然なワークロードです。

この3つは 第8章 StatefulSet / DaemonSet / Job / CronJob で本格的に扱います。本章では最もよく触る Deployment にだけ集中します。ただし頭の中の分類はあらかじめつかんでおくとよいです — 状態がなければ Deployment、状態があれば StatefulSet、ノードごとに1つなら DaemonSet、使い捨てなら Job です。

後片付け・クリーンアップ #

今日作ったリソースをきれいに消します。Deployment を1個消すとその下の ReplicaSet と Pod が一緒に整理されます。K8s が親子関係 (owner reference) を通じてガベージコレクションをしてくれる部分です。

マニフェストでクリーンアップ
kubectl delete -f web.yaml
出力例
deployment.apps "web" deleted
本当に空になったか
kubectl get deploy,rs,pods
出力例
No resources found in default namespace.

名前で直接消す道もあります。

名前でクリーンアップ
kubectl delete deploy web

Deployment だけ消したのに ReplicaSet と Pod まで一度に消えることを確認しておいてください。この owner reference モデルは本書の後半でも同じ方式で動作します。

練習問題 #

  1. 上の本文どおり web.yamlreplicas: 3 で起動したあと、kubectl delete pod <name> で Pod を1個強制削除してみてください。kubectl get podsAGE カラムがどう変わるかを時間順に記録し、新しい Pod の名前の後ろの任意値がどう変わったかをメモします。ReplicaSet コントローラの reconcile loop がどこで差を埋めたかを一段落で書きます。
  2. nginx:1.27 から nginx:1.28 へ一度ロールアウトしてみたあと、わざと nginx:1.99-not-real のような存在しないタグでもう一度 apply してみてください。kubectl get rskubectl get pods がどんな形で止まるか、古い Pod 3個がそのままトラフィックを受けられる状態で残っていることが §「失敗したらどうなるか」の無停止の核心とどうつながるかを整理します。kubectl rollout undo deploy/web できれいに戻すまでの全流れを一連の流れで記録します。
  3. kubectl scale deploy/web --replicas=5 で命令型に増やしたあと、マニフェスト (replicas: 3) の kubectl apply をもう一度実行して5個が再び3個に減ることを確認してみてください。「宣言型マニフェストが常に信頼できる情報源」という §「replicas の調整」の結論が 第20章 GitOps のメンタルモデルとどうつながるかを一段落でメモします。

一行まとめ: Deployment は「この Pod テンプレートで N 個維持 + 新バージョンへのローリング切り替え」を扱うマニフェストであり、その下の ReplicaSet が1つのバージョンの N 個維持を、Pod が実際の実行を担う。Pod を消しても ReplicaSet が新しく起動して self-healing が自動で動作する。新バージョンのデプロイは新 RS を段階的に増やして古い RS を段階的に減らすローリングアップデートで、失敗時は古い RS がそのままトラフィックを受け、kubectl rollout undo 一行で古い RS を再び増やして戻せる。

次の章 #

ここまで来ても1つがまだ解けていません — 外部からクラスタの中の Pod へどうトラフィックを送るか です。今私たちが作った nginx Pod 3個にはクラスタ内部 IP が付いているだけで、その IP は Pod が死んで新しく起動するたびに変わります。ReplicaSet が Pod を自動で生かしてはくれますが、そうして生き返った Pod の IP が毎回違うので、クライアントがどこへ接続すればよいかが曖昧です。

第5章 Service では、Service が Pod の前段に安定した仮想 IP / DNS 名をどう付けてくれるのか、クラスタ内部通信用の ClusterIP、ノードポートで外部に開く NodePort、クラウドロードバランサを付ける LoadBalancer の3種類の違い、そして本章で起動した app: web Pod の前段に Service を付けて最初の外部接続を作る流れまで扱います。本章の Pod 3個がそこで初めて「アドレスを持つサービス」になります。

X