Ritolabo
  1. Home
  2. Kubernetes
  3. Rolling Update / Rollback: Kubernetes でアプリを止めずに Pod を入れ替える

Rolling Update / Rollback: Kubernetes でアプリを止めずに Pod を入れ替える

  • 公開日
  • カテゴリ:Kubernetes
  • タグ:Kubernetes,学習メモ
Rolling Update / Rollback: Kubernetes でアプリを止めずに Pod を入れ替える

動いているアプリを、アクセスを止めずに新しいバージョンへ入れ替えたい。Kubernetes では、Deployment の spec.template(Pod の定義)の image を書き換えて apply すると、入れ替えは Kubernetes がやる。どう入れ替えるかは Deployment の strategy で決まる。

この記事では、同じ nginx の Deployment(Pod 3 個)で、次の 3 つを確認する。

  • 入れ替え(Rolling Update): image を nginx:1.16.1 から nginx:1.17.10 に変え、古い Pod が 1 つずつ新しい Pod に入れ替わる様子
  • 失敗した入れ替え: 存在しない image に変えて、入れ替えが途中で止まる様子と、それでもアプリが止まらないこと
  • 戻す(Rollback): kubectl rollout undo で前の revision に戻す

この記事でやること:

  • Deployment・Service と、Service に 1 秒ごとにアクセスし続ける Pod を作り、入れ替えの前のバージョンを確認
  • YAML の image を書き換えて apply し、kubectl get rs -w で新旧 2 つの ReplicaSet の数が交互に動くことと、アクセスが途切れないことの確認
  • Deployment の Events: と strategy(maxSurge / maxUnavailable)で、入れ替えの順番が決まる仕組みの確認
  • kubectl rollout history で revision を見て、その実体が数 0 で残った古い ReplicaSet であることの確認
  • 存在しない image で ImagePullBackOff になり、入れ替えが「新しい Pod 1 つ」で止まることの確認
  • kubectl rollout undo と --to-revision=N で戻し、revision の番号がどう変わるかの確認
  • Recreate、maxSurge: 0、progressDeadlineSeconds、CHANGE-CAUSE、pause / resume の確認

contents

  1. 環境
  2. Rolling Update と Rollback
    1. Rolling Update はどこで起きているか
  3. Deployment・Service・アクセスし続ける Pod を作る
  4. image を書き換えて apply する
    1. kubectl get rs -w で入れ替わりを見る
    2. kubectl rollout status で待つ
    3. アクセスが途切れていないことを見る
  5. 入れ替えの順番と strategy
    1. 終わったあとの形
    2. Events で順番を見る
    3. 順番を決めている strategy
  6. 履歴を見る(kubectl rollout history)
    1. revision の実体は ReplicaSet
  7. 失敗する入れ替え:存在しない image に変える
    1. kubectl set image で変える
    2. 止まっているのを見る
    3. Pod の Events で原因を見る
    4. アクセスは途切れていない
  8. 1 つ前に戻す(kubectl rollout undo)
    1. Warning の意味:YAML ファイルとのずれ
  9. 特定の revision に戻す(--to-revision)
    1. YAML を apply すると、YAML の image に進む
  10. 消す
  11. 全部消してから作る(strategy.type: Recreate)
  12. 消してから作る順にする(maxSurge: 0)
  13. 止まった入れ替えを失敗と判定させる(progressDeadlineSeconds)
  14. CHANGE-CAUSE に理由を残す
  15. 入れ替えを一時停止する(kubectl rollout pause / resume)
  16. 用語メモ
  17. 参考 URL

環境

kind で作ったクラスタ(Control Plane 1台 + Worker 2台)を使う。(作り方は「Kubernetes の Pod とは?コンテナを動かす最小単位を整理する」を参照)

  • 環境: macOS(Apple Silicon)、Docker Desktop 29.8.0、kind v0.33.0、Kubernetes v1.37.0、kubectl v1.36.1

Rolling Update と Rollback

同じ nginx の Deployment で、「入れ替え」「失敗した入れ替え」「戻す」の 3 つを順に見る。

入れ替え失敗した入れ替え戻す
やることimage を nginx:1.16.1 → nginx:1.17.10 に変える存在しない image(nginx:1.17.10-typo)に変えるkubectl rollout undo で 1 つ前に戻す
何が起きるか新しい ReplicaSet ができ、古い ReplicaSet と Pod の数が 1 つずつ入れ替わる新しい Pod が 1 つ ImagePullBackOff のまま止まる1 つ前の ReplicaSet の数が 3 に戻り、失敗した ReplicaSet は 0 になる
アクセス途切れない(古い Pod と新しい Pod が混ざって返事する)途切れない(古い Pod 3 つがそのまま動いている)途切れない
rollout historyREVISION が 1 → 23 が増える戻した版が新しい番号(4)になる

この記事の本文と実行結果に出てくる言葉は次のとおり。

名前内容
Deployment → ReplicaSet → PodDeployment を作ると ReplicaSet が作られ、ReplicaSet が Pod を作る 3 段。ReplicaSet は「この Pod の定義(template)を N 個」というデータで、Pod の定義が変わると新しい ReplicaSet が作られる。古い ReplicaSet は消されず、数 0 で残る(「Deployment: Kubernetes で Pod の数と更新を管理する」を参照)
rolloutDeployment の Pod を、新しい定義の Pod に入れ替えること。入れ替えの一連の流れ全体を指す。kubectl rollout というコマンドの名前にもなっている
Rolling Updaterollout のやり方の 1 つ。古い Pod を全部消してから作るのではなく、少しずつ(rolling)入れ替える。Deployment の既定(strategy.type: RollingUpdate)
revisionPod の定義が変わるたびに 1 増える番号。ReplicaSet に付く。kubectl rollout history で見える
Rollback / kubectl rollout undo前の revision の Pod の定義に戻すこと。戻すときも「Pod の定義を変える」なので、Rolling Update で入れ替わる
ImagePullBackOffkubelet が image を取ってこられず(存在しない、名前の間違い、レジストリに届かない)、間をあけて取り直しを繰り返している、という Pod の STATUS。取り直しに失敗した瞬間は ErrImagePull

今回は nginx:1.16.1 を nginx:1.17.10 に変える。nginx としての動きは同じ(HTML を返すだけ)なので、違いはバージョン番号そのもので見る。

見るものnginx:1.16.1nginx:1.17.10
kubectl exec deploy/demo-app -- nginx -vnginx version: nginx/1.16.1nginx version: nginx/1.17.10
HTTP の返事の Server ヘッダーServer: nginx/1.16.1Server: nginx/1.17.10
kubectl get rs の IMAGES 列と、ReplicaSet の名前のハッシュnginx:1.16.1 / demo-app-86547cd9f8nginx:1.17.10 / demo-app-799468465
Pod の名前の真ん中(ReplicaSet のハッシュ)demo-app-86547cd9f8-xxxxxdemo-app-799468465-xxxxx

Rolling Update はどこで起きているか

Deployment も ReplicaSet も、Control Plane で動いている etcd に保存されるデータ。入れ替えを進めるプログラムは kube-controller-manager(Control Plane の Pod)の中の deployment-controller と replicaset-controller。新しい image を取ってきてコンテナを起動するのは、Pod が置かれた Node の kubelet。

図を描画しています…
  • 角が二重の箱(kube-controller-manager、kubelet)は動いているプログラム。それ以外は etcd に保存されたデータと、Node の上の Pod
  • 自分で書き換えるのは Deployment の spec.template だけ。deployment-controller が新しい ReplicaSet を作り、古い ReplicaSet と新しい ReplicaSet の DESIRED を 1 つずつ入れ替える。DESIRED に合わせて Pod を作る・消すのは replicaset-controller。replicas を手で変えたときと、ReplicaSet から下は同じ流れ(「HPA: Kubernetes で負荷に応じて Pod を自動でスケールする」を参照)
  • image を取ってくるのは Control Plane ではなく、Pod が置かれた Node の kubelet。image が取れなければ、その Pod が ImagePullBackOff になる
  • 古い Pod は入れ替えが進むにつれて消えていく。図は入れ替えが終わったあとの Pod の配置

Deployment・Service・アクセスし続ける Pod を作る

replicas: 3 の Deployment を作る。この記事で書き換えるのは image の行だけ。

# demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
        - name: nginx
          image: nginx:1.16.1
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 100m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
フィールド意味
spec.replicas: 3Pod を 3 個。1 個だと「少しずつ入れ替える」が見えないので 3 個にする
spec.templatePod の定義。ここが変わると、新しい ReplicaSet が作られて入れ替えが始まる。replicas を変えても入れ替えは起きない
spec.template.spec.containers[0].image今回変える行。nginx:1.16.1 を nginx:1.17.10 にする
spec.strategy書いていない。書かないと RollingUpdate(後の節で確認する)

demo-service.yaml は、app: demo-app の Pod に振り分ける Service。(Service は「Service: Kubernetes で Pod への宛先を固定する」を参照)

# demo-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-service
spec:
  selector:
    app: demo-app
  ports:
    - port: 80
      targetPort: 80

client.yaml は、入れ替えの間もアクセスが途切れないことを見るための Pod。busybox の wget で demo-service に 1 秒ごとにアクセスし、返事の Server ヘッダー(nginx が自分のバージョンを名乗る行)を時刻付きで出力し続ける。

nginx(demo-app。アクセスされる側)client(アクセスする側)
何かWeb サーバー1 秒に 1 回 demo-service に HTTP でアクセスし、返事の Server: の行だけを記録するコマンド
出力なし04:12:36 Server: nginx/1.16.1 のような行が 1 秒ごとに増える。返事がなければ FAIL
入れ替えるかするしない(別の Pod。最後まで同じものが動き続ける)
# client.yaml
apiVersion: v1
kind: Pod
metadata:
  name: client
spec:
  restartPolicy: Never
  containers:
    - name: client
      image: busybox:1.36
      command:
        - sh
        - -c
        - |
          while true; do
            r=$(wget -S -q -O /dev/null -T 2 http://demo-service 2>&1 | grep -i 'Server:')
            echo "$(date +%T) ${r:-FAIL}"
            sleep 1
          done
部分意味
wget -S -q -O /dev/null -T 2 http://demo-servicedemo-service にアクセスして、本文は捨て(-O /dev/null)、返事のヘッダーを表示する(-S)。2 秒返事がなければあきらめる(-T 2)
grep -i 'Server:'ヘッダーのうち Server: nginx/1.16.1 の行だけ残す
echo "$(date +%T) ${r:-FAIL}"時刻と一緒に出力する。Server: の行が取れなかった(返事がなかった)ときは FAIL と出す
時刻コンテナの中の date は UTC。日本時間より 9 時間前の値が出る
$ kubectl apply -f demo-deployment.yaml -f demo-service.yaml -f client.yaml

deployment.apps/demo-app created
service/demo-service created
pod/client created

$ kubectl get deploy,rs,pods -l app=demo-app -o wide

NAME                                  DESIRED   CURRENT   READY   AGE   CONTAINERS   IMAGES         SELECTOR
replicaset.apps/demo-app-86547cd9f8   3         3         3       5s    nginx        nginx:1.16.1   app=demo-app,pod-template-hash=86547cd9f8

NAME                            READY   STATUS    RESTARTS   AGE   IP            NODE                NOMINATED NODE   READINESS GATES
pod/demo-app-86547cd9f8-9hntt   1/1     Running   0          5s    10.244.1.17   k8s-study-worker    <none>           <none>
pod/demo-app-86547cd9f8-rtgkv   1/1     Running   0          5s    10.244.2.19   k8s-study-worker2   <none>           <none>
pod/demo-app-86547cd9f8-ttx62   1/1     Running   0          5s    10.244.1.18   k8s-study-worker    <none>           <none>

$ kubectl get pod client

NAME     READY   STATUS    RESTARTS   AGE
client   1/1     Running   0          15s
  • ReplicaSet は 1 つ。IMAGES が nginx:1.16.1。名前は demo-app-<Pod の定義から計算したハッシュ>
  • Pod は 3 つとも名前の真ん中が ReplicaSet と同じハッシュ 86547cd9f8
  • -l app=demo-app で Deployment の Pod だけに絞っている。client はラベルが違うので別に見る

今のバージョンを 2 つの方法で見ておく。

$ kubectl exec deploy/demo-app -- nginx -v

# => nginx version: nginx/1.16.1

$ kubectl logs client --tail=5

04:12:36   Server: nginx/1.16.1
04:12:37   Server: nginx/1.16.1
04:12:38   Server: nginx/1.16.1
04:12:39   Server: nginx/1.16.1
04:12:40   Server: nginx/1.16.1
コマンド意味
kubectl exec deploy/demo-app -- nginx -vDeployment の Pod のどれか 1 つの中で nginx -v(バージョン表示)を実行する
kubectl logs client --tail=5client の出力の最後の 5 行。1 秒ごとに Server: nginx/1.16.1。3 つの Pod のどれが返事しても、今は全部 1.16.1

入れ替えの前に、kubectl rollout history deployment/<Deployment 名> で、この Deployment の「Pod の定義の履歴」を見ておく。

$ kubectl rollout history deployment/demo-app

deployment.apps/demo-app
REVISION  CHANGE-CAUSE
1         <none>
列意味
REVISIONPod の定義の版の番号。作った直後は 1 だけ。Pod の定義が変わるたびに 1 つ増える
CHANGE-CAUSE「この revision は何のために変えたか」を書いておく欄。自動では何も入らない(後の節で確認する)

image を書き換えて apply する

demo-deployment.yaml の image の行を書き換える。変えるのはこの 1 行だけ。

        - name: nginx
          image: nginx:1.17.10

kubectl get rs -w で入れ替わりを見る

apply のすぐあとに kubectl get rs -w を続けて打つ。-w を付けると、ReplicaSet の数が変わるたびに行が追加される。古い ReplicaSet が 0 0 0、新しい ReplicaSet が 3 3 3 になったら Ctrl+C で止める。

$ kubectl apply -f demo-deployment.yaml; kubectl get rs -l app=demo-app -w

deployment.apps/demo-app configured
NAME                  DESIRED   CURRENT   READY   AGE
demo-app-799468465    1         1         0       0s
demo-app-86547cd9f8   3         3         3       17m
demo-app-799468465    1         1         1       1s
demo-app-86547cd9f8   2         3         3       17m
demo-app-86547cd9f8   2         3         3       17m
demo-app-86547cd9f8   2         2         2       17m
demo-app-799468465    2         1         1       1s
demo-app-799468465    2         1         1       1s
demo-app-799468465    2         2         1       1s
demo-app-86547cd9f8   2         2         2       17m
demo-app-799468465    2         2         2       2s
demo-app-86547cd9f8   1         2         2       17m
demo-app-799468465    3         2         2       2s
demo-app-86547cd9f8   1         2         2       17m
demo-app-86547cd9f8   1         1         1       17m
demo-app-799468465    3         2         2       2s
demo-app-799468465    3         3         2       2s
demo-app-86547cd9f8   1         1         1       17m
demo-app-799468465    3         3         3       3s
demo-app-86547cd9f8   0         1         1       17m
demo-app-86547cd9f8   0         1         1       17m
demo-app-86547cd9f8   0         0         0       17m
demo-app-86547cd9f8   0         0         0       17m

同じ内容の行が 2 回出ることがある。列の意味は次のとおり。すべて「その ReplicaSet が持つ Pod の数」で、DESIRED だけが目標、あとの 2 つは今の実際の数。

列意味
NAMEReplicaSet の名前。<Deployment 名>-<ハッシュ> で、ハッシュは Pod の定義(image など)から決まる。定義が違えば名前も違う
DESIREDdeployment-controller が「この ReplicaSet に持たせたい Pod の数」として書いた値。入れ替え中はこの数が 1 ずつ増えたり減ったりする
CURRENT実際に存在している Pod の数(起動中・終了中も含む)。DESIRED に合わせて replicaset-controller が Pod を作ったり消したりするので、少し遅れて追いつく
READYそのうち、起動が終わってリクエストを受けられる Pod の数。image を取ってきて nginx が立ち上がるまでは CURRENT より小さい
AGEその ReplicaSet が作られてからの時間

行を上から追うと、2 つの ReplicaSet の DESIRED がこう動いている。

順古い demo-app-86547cd9f8(1.16.1)新しい demo-app-799468465(1.17.10)何が起きたか
03(なし)入れ替えの前
131新しい ReplicaSet が作られ、Pod が 1 つ作られる。READY が 1 になるまで 1 秒
221新しい Pod が準備できたので、古い Pod を 1 つ消す
322新しい Pod をもう 1 つ作る
412準備できたら、古い Pod をもう 1 つ消す
513新しい Pod を 3 つ目
603最後の古い Pod を消す。入れ替え完了
  • 古い Pod を消すのは、新しい Pod が READY になってから。新しい Pod が準備できるまでは、古い 3 つがそのまま動いている
  • 同時に存在する Pod は最大 4 つ(古い 3 + 新しい 1)、最低 3 つ。この「最大 4・最低 3」を決めているのが strategy の maxSurge / maxUnavailable(後の節で確認する)
  • 古い ReplicaSet は 0 0 0 になっても消えない。一覧に残り続ける
  • 1.17.10 の image はこのクラスタに一度も取ってきていなかったが、READY まで 1 秒で済んだ。ここは image の大きさと回線で変わる

kubectl rollout status で待つ

入れ替えが終わるのを待つコマンドが kubectl rollout status deployment/<Deployment 名>。終わっていれば successfully rolled out とだけ出る。

$ kubectl rollout status deployment/demo-app

# => deployment "demo-app" successfully rolled out

apply の直後に打つと、終わるまで進み具合を出し続ける。

$ kubectl rollout status deployment/demo-app

Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
deployment "demo-app" successfully rolled out
行意味
1 out of 3 new replicas have been updated...3 つのうち 1 つが新しい定義になった
1 old replicas are pending termination...新しい Pod は 3 つそろい、古い Pod が 1 つ消えるのを待っている
successfully rolled out入れ替え完了。このコマンドは終了する。止まった入れ替えでは、ここに来ない(後の節で確認する)

アクセスが途切れていないことを見る

$ kubectl logs client | grep -m1 -B5 -A3 '1.17.10'

04:29:10   Server: nginx/1.16.1
04:29:11   Server: nginx/1.16.1
04:29:12   Server: nginx/1.16.1
04:29:13   Server: nginx/1.16.1
04:29:14   Server: nginx/1.16.1
04:29:15   Server: nginx/1.17.10
04:29:16   Server: nginx/1.17.10
04:29:17   Server: nginx/1.17.10
04:29:18   Server: nginx/1.17.10

$ kubectl logs client | grep -c FAIL

# => 0

$ kubectl exec deploy/demo-app -- nginx -v

# => nginx version: nginx/1.17.10
コマンド意味
grep -m1 -B5 -A3 '1.17.10'最初に 1.17.10 が出た行(-m1)の前 5 行(-B5)と後 3 行(-A3)。返事が 1.16.1 から 1.17.10 に切り替わった瞬間
grep -c FAIL返事がなかった回数。0 なら、1 秒に 1 回のアクセスは一度も失敗していない
  • 入れ替え中も毎秒返事が来ていて、FAIL は 0。アプリは止まっていない
  • 入れ替え中は古い Pod と新しい Pod が混ざって動いているので、1.16.1 と 1.17.10 が交互に出ることもある(Service はそのとき準備できている Pod に振り分ける)。今回は 04:29:15 からきれいに 1.17.10 だけになった

入れ替えの順番と strategy

終わったあとの形

$ kubectl get deploy,rs,pods -l app=demo-app -o wide

NAME                                  DESIRED   CURRENT   READY   AGE    CONTAINERS   IMAGES          SELECTOR
replicaset.apps/demo-app-799468465    3         3         3       7m8s   nginx        nginx:1.17.10   app=demo-app,pod-template-hash=799468465
replicaset.apps/demo-app-86547cd9f8   0         0         0       24m    nginx        nginx:1.16.1    app=demo-app,pod-template-hash=86547cd9f8

NAME                           READY   STATUS    RESTARTS   AGE    IP            NODE                NOMINATED NODE   READINESS GATES
pod/demo-app-799468465-khxct   1/1     Running   0          7m7s   10.244.1.19   k8s-study-worker    <none>           <none>
pod/demo-app-799468465-kkc5b   1/1     Running   0          7m6s   10.244.2.22   k8s-study-worker2   <none>           <none>
pod/demo-app-799468465-kkrk2   1/1     Running   0          7m8s   10.244.2.21   k8s-study-worker2   <none>           <none>
  • ReplicaSet が 2 つ。古い 86547cd9f8(nginx:1.16.1)は 0 0 0、新しい 799468465(nginx:1.17.10)が 3 3 3
  • Pod は 3 つとも新しいハッシュ。最初に作った Pod は 1 つも残っていない。「image を変える」は「Pod を作り直す」ことで、動いている Pod のコンテナを差し替えるのではない
  • Pod の AGE が 7m8s / 7m7s / 7m6s と 1 秒ずつずれている。1 つずつ順に作られた

Events で順番を見る

$ kubectl describe deploy demo-app | grep -A 10 'Events:'

Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  24m    deployment-controller  Scaled up replica set demo-app-86547cd9f8 from 0 to 3
  Normal  ScalingReplicaSet  7m37s  deployment-controller  Scaled up replica set demo-app-799468465 from 0 to 1
  Normal  ScalingReplicaSet  7m36s  deployment-controller  Scaled down replica set demo-app-86547cd9f8 from 3 to 2
  Normal  ScalingReplicaSet  7m36s  deployment-controller  Scaled up replica set demo-app-799468465 from 1 to 2
  Normal  ScalingReplicaSet  7m35s  deployment-controller  Scaled down replica set demo-app-86547cd9f8 from 2 to 1
  Normal  ScalingReplicaSet  7m35s  deployment-controller  Scaled up replica set demo-app-799468465 from 2 to 3
  Normal  ScalingReplicaSet  7m34s  deployment-controller  Scaled down replica set demo-app-86547cd9f8 from 1 to 0
  • 1 行目は Deployment を作ったときの記録。2 行目からが入れ替え
  • Scaled up ... 799468465 from 0 to 1(新しいのを 1 つ)→ Scaled down ... 86547cd9f8 from 3 to 2(古いのを 1 つ)→ up ... 1 to 2 → down ... 2 to 1 → up ... 2 to 3 → down ... 1 to 0。-w で見た動きと同じ
  • From はすべて deployment-controller。ReplicaSet の数を書き換えているのは deployment-controller で、違いは「ReplicaSet が 2 つあって、片方を増やし、もう片方を減らす」こと。Pod を実際に作る・消すのは replicaset-controller

順番を決めている strategy

kubectl describe deploy の上のほうに、入れ替えのやり方が出ている。

$ kubectl describe deploy demo-app | grep -E 'Replicas:|StrategyType|RollingUpdateStrategy'

Replicas:               3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType:           RollingUpdate
RollingUpdateStrategy:  25% max unavailable, 25% max surge
行意味
StrategyType: RollingUpdate入れ替えのやり方。YAML の spec.strategy.type。書いていないので既定の RollingUpdate(少しずつ入れ替える)。もう 1 つの値は Recreate(全部消してから作る。後の節で確認する)
25% max surgereplicas を超えて作ってよい Pod の数。YAML の spec.strategy.rollingUpdate.maxSurge。既定 25%。3 個の 25% = 0.75 → 切り上げて 1。だから最大 3 + 1 = 4 個
25% max unavailable入れ替え中に足りなくてよい Pod の数。同じく maxUnavailable。既定 25%。3 個の 25% = 0.75 → 切り捨てて 0。だから最低 3 個は動いていないといけない
Replicas: 3 desired / 3 updated / 3 total / 3 available欲しい数 / 新しい定義になった数 / 今ある数 / 使える数。入れ替え中は 1 updated / 4 total のように動く

「最大 4、最低 3」なので、1 つ作って 1 つ消す、しかできない。これが上で見た順番の理由。deployment-controller は順番そのものを設定で持っているわけではなく、この上限と下限を守って「作れるなら作る、作れなければ消せるだけ消す」を繰り返している。

YAML に書くなら、spec に replicas と並べて書く(今回は書いていないので既定値)。

spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%

履歴を見る(kubectl rollout history)

入れ替えのあと、REVISION が 2 つになっている。それぞれの revision がどの image かを --revision=N で見る。

$ kubectl rollout history deployment/demo-app

deployment.apps/demo-app
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

$ kubectl rollout history deployment/demo-app --revision=1

deployment.apps/demo-app with revision #1
Pod Template:
  Labels:	app=demo-app
	pod-template-hash=86547cd9f8
  Containers:
   nginx:
    Image:	nginx:1.16.1
    Port:	80/TCP
    Host Port:	0/TCP
    Limits:
      cpu:	200m
      memory:	128Mi
    Requests:
      cpu:	100m
      memory:	64Mi
    Environment:	<none>
    Mounts:	<none>
  Volumes:	<none>
  Node-Selectors:	<none>
  Tolerations:	<none>
表示意味
REVISION 1 / 2作ったときの Pod の定義が 1、image を変えたあとの定義が 2
--revision=1 の Pod Template:revision 1 の Pod の定義そのもの。Image: nginx:1.16.1、pod-template-hash=86547cd9f8(古い ReplicaSet のハッシュ)

revision の実体は ReplicaSet

revision は Deployment が別に持っているデータではなく、各 ReplicaSet に付いたアノテーション deployment.kubernetes.io/revision の値。kubectl rollout history は、Deployment の ReplicaSet を全部集めて、このアノテーションの番号順に並べているだけ。

$ kubectl get rs -l app=demo-app -o custom-columns='NAME:.metadata.name,REVISION:.metadata.annotations.deployment\.kubernetes\.io/revision,DESIRED:.spec.replicas,IMAGE:.spec.template.spec.containers[0].image'

NAME                  REVISION   DESIRED   IMAGE
demo-app-799468465    2          3         nginx:1.17.10
demo-app-86547cd9f8   1          0         nginx:1.16.1

-o custom-columns= で、表示する列を自分で決めている。アノテーションの名前の . は \. と書く。

  • 古い ReplicaSet 86547cd9f8 が DESIRED 0 で残っているのは、この revision 1 の定義を取っておくため。kubectl rollout undo は、この ReplicaSet の定義を使って戻す
  • 残す数は Deployment の spec.revisionHistoryLimit で決まり、既定は 10。それを超えた古い ReplicaSet から消される(消えた revision には戻れない)
  • replicas だけ変えても revision は増えない。増えるのは spec.template(Pod の定義)が変わったときだけ

失敗する入れ替え:存在しない image に変える

kubectl set image で変える

YAML を書き換えずに image だけを変える近道が kubectl set image deployment/<Deployment 名> <コンテナ名>=<image>。ここでは、わざと存在しない tag(nginx:1.17.10-typo)を指定する。

$ kubectl set image deployment/demo-app nginx=nginx:1.17.10-typo

# => deployment.apps/demo-app image updated
部分意味
nginx=nginx:1.17.10-typoコンテナ nginx(YAML の containers[].name)の image を nginx:1.17.10-typo にする。Deployment の spec.template.spec.containers[0].image を書き換えている。YAML を書き換えて apply したのと、変わるフィールドは同じ
image updated書き換えた。入れ替えが始まる

kubectl scale と同じ位置づけで、YAML ファイルの image: nginx:1.17.10 はそのまま残る。

止まっているのを見る

$ kubectl rollout status deployment/demo-app

Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...

1 out of 3 のまま進まない。このコマンドは入れ替えが終わるまで終了しないので、Ctrl+C で止める。

$ kubectl get deploy,rs,pods -l app=demo-app

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/demo-app-6c4dcbb5b8   1         1         0       27s
replicaset.apps/demo-app-799468465    3         3         3       11m
replicaset.apps/demo-app-86547cd9f8   0         0         0       28m

NAME                            READY   STATUS             RESTARTS   AGE
pod/demo-app-6c4dcbb5b8-7q8pj   0/1     ImagePullBackOff   0          27s
pod/demo-app-799468465-khxct    1/1     Running            0          11m
pod/demo-app-799468465-kkc5b    1/1     Running            0          11m
pod/demo-app-799468465-kkrk2    1/1     Running            0          11m

Pod の STATUS は、打つタイミングで ErrImagePull と ImagePullBackOff のどちらかが出る。

行見るところ
ReplicaSet3 つ目の ReplicaSet 6c4dcbb5b8(typo の定義)が DESIRED 1 / READY 0。2 つ目の 799468465(1.17.10)は 3 3 3 のまま
Pod新しい Pod が 1 つ 0/1、ImagePullBackOff。1.17.10 の Pod 3 つはそのまま Running

なぜ 1 つで止まるか。maxSurge 1 / maxUnavailable 0 なので、「新しい Pod を 1 つ作る」までは進めるが、その Pod が READY にならないので「古い Pod を 1 つ消す」に進めない。作ってよい数も 1 つが上限なので、2 つ目も作れない。deployment-controller はこの状態で待ち続け、自分では戻さない。

Pod の Events で原因を見る

止まっている Pod の名前を、上の kubectl get pods から取って describe する。

$ kubectl describe pod demo-app-6c4dcbb5b8-7q8pj | grep -A 10 'Events:'

Events:
  Type     Reason     Age                  From               Message
  ----     ------     ----                 ----               -------
  Normal   Scheduled  2m17s                default-scheduler  Successfully assigned default/demo-app-6c4dcbb5b8-7q8pj to k8s-study-worker
  Normal   Pulling    45s (x4 over 2m16s)  kubelet            spec.containers{nginx}: Pulling image "nginx:1.17.10-typo"
  Warning  Failed     44s (x4 over 2m15s)  kubelet            spec.containers{nginx}: Failed to pull image "nginx:1.17.10-typo": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/library/nginx:1.17.10-typo": failed to resolve reference "docker.io/library/nginx:1.17.10-typo": docker.io/library/nginx:1.17.10-typo: not found
  Warning  Failed     44s (x4 over 2m15s)  kubelet            spec.containers{nginx}: Error: ErrImagePull
  Normal   BackOff    5s (x8 over 2m15s)   kubelet            spec.containers{nginx}: Back-off pulling image "nginx:1.17.10-typo"
  Warning  Failed     5s (x8 over 2m15s)   kubelet            spec.containers{nginx}: Error: ImagePullBackOff
行意味
Pulling image "nginx:1.17.10-typo"(From: kubelet)Node の kubelet が Docker Hub から image を取ろうとした
Failed to pull image ... not foundDocker Hub に nginx:1.17.10-typo という tag はない、と言われた
Error: ErrImagePull取れなかった。STATUS が ErrImagePull になる瞬間
Back-off pulling image / Error: ImagePullBackOff間をあけて取り直す(待ち時間は失敗するたびに延びる)。待っている間の STATUS が ImagePullBackOff
(x4 over 2m16s)同じことが 2 分 16 秒の間に 4 回起きた

image を取るのは、Control Plane ではなく Pod が置かれた Node の kubelet。だから、その Node から Docker Hub に届かないときも同じ ImagePullBackOff になる。

アクセスは途切れていない

$ kubectl logs client --tail=3

04:43:07   Server: nginx/1.17.10
04:43:08   Server: nginx/1.17.10
04:43:09   Server: nginx/1.17.10

$ kubectl logs client | grep -c FAIL

# => 0

$ kubectl describe deploy demo-app | grep -E -A4 'Replicas:|Conditions:'

Replicas:               3 desired | 1 updated | 4 total | 3 available | 1 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
--
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    ReplicaSetUpdated

$ kubectl rollout history deployment/demo-app

deployment.apps/demo-app
REVISION  CHANGE-CAUSE
1         <none>
2         <none>
3         <none>
  • client は 1.17.10 から返事をもらい続けている。FAIL は 0。入れ替えが失敗しても、アプリは止まっていない
  • Replicas: 3 desired | 1 updated | 4 total | 3 available | 1 unavailable。Pod は 4 つあって(古い 3 + 新しい 1)、使えるのは 3 つ
  • Conditions: の Progressing True ReplicaSetUpdated。「入れ替えは進行中」のまま。Kubernetes から見ると「失敗」ではなく「まだ終わっていない」(既定では 10 分たつと False / ProgressDeadlineExceeded に変わる。後の節で確認する)
  • typo の定義も REVISION 3 として履歴に入っている

アプリが止まらなかったのは、maxUnavailable が 0 で「使える Pod の下限が 3」だったため。古い Pod は、新しい Pod が使えるようになるまで 1 つも消されない。入れ替えに失敗しても古いほうがそのまま動き続けるので、人が気づいて戻すまでの時間がある。

1 つ前に戻す(kubectl rollout undo)

kubectl rollout undo deployment/<Deployment 名> で、1 つ前の revision に戻す。

$ kubectl rollout undo deployment/demo-app

Warning: resource deployments/demo-app was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation, which may cause unexpected behavior on future 'kubectl apply' operations. Consider using 'kubectl apply' with your previous configuration file instead.
deployment.apps/demo-app rolled back

$ kubectl rollout status deployment/demo-app

# => deployment "demo-app" successfully rolled out
行意味
rolled back戻した。--to-revision を付けないと、今の revision の 1 つ前(今は 3 なので 2)に戻る。最初の状態に戻るのではない
Warning: ... previously managed with 'kubectl apply'「この Deployment は kubectl apply で管理されているので、YAML のほうを直して apply するやり方も考えて」という注意。この節の最後で確認する
successfully rolled out がすぐ出る戻り先の ReplicaSet 799468465 は止まっている間も 3 3 3 のまま動いていたので、新しく作る Pod がない。typo の Pod を消すだけ
$ kubectl get rs,pods -l app=demo-app

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/demo-app-6c4dcbb5b8   0         0         0       4m33s
replicaset.apps/demo-app-799468465    3         3         3       15m
replicaset.apps/demo-app-86547cd9f8   0         0         0       32m

NAME                           READY   STATUS    RESTARTS   AGE
pod/demo-app-799468465-khxct   1/1     Running   0          15m
pod/demo-app-799468465-kkc5b   1/1     Running   0          15m
pod/demo-app-799468465-kkrk2   1/1     Running   0          15m

$ kubectl rollout history deployment/demo-app

deployment.apps/demo-app
REVISION  CHANGE-CAUSE
1         <none>
3         <none>
4         <none>

$ kubectl get rs -l app=demo-app -o custom-columns='NAME:.metadata.name,REVISION:.metadata.annotations.deployment\.kubernetes\.io/revision,DESIRED:.spec.replicas,IMAGE:.spec.template.spec.containers[0].image'

NAME                  REVISION   DESIRED   IMAGE
demo-app-6c4dcbb5b8   3          0         nginx:1.17.10-typo
demo-app-799468465    4          3         nginx:1.17.10
demo-app-86547cd9f8   1          0         nginx:1.16.1

$ kubectl describe deploy demo-app | grep -A 12 'Events:' | tail -2

  Normal  ScalingReplicaSet  4m53s  deployment-controller  Scaled up replica set demo-app-6c4dcbb5b8 from 0 to 1
  Normal  ScalingReplicaSet  73s    deployment-controller  Scaled down replica set demo-app-6c4dcbb5b8 from 1 to 0

$ kubectl exec deploy/demo-app -- nginx -v

# => nginx version: nginx/1.17.10
  • typo の ReplicaSet 6c4dcbb5b8 が 0 0 0 になり、ImagePullBackOff の Pod は消えた。1.17.10 の Pod 3 つはそのまま(名前も変わっていない)
  • REVISION が 1, 3, 4 になった。2 が消えて 4 が増えている。戻し先の ReplicaSet 799468465 のアノテーションが 2 → 4 に書き換えられた。「revision 2 に戻る」のではなく、「revision 2 と同じ定義を、新しい revision 4 として出し直す」。履歴の番号は減らない
  • Events: は Scaled down replica set demo-app-6c4dcbb5b8 from 1 to 0 だけ。戻すときも deployment-controller が ReplicaSet の数を変えているだけで、入れ替えと同じ仕組み
  • rollout undo がやっていることは、1 つ前の ReplicaSet の Pod の定義を取り出して、Deployment の spec.template に書き戻すこと。あとは「template が変わった」として、いつもの入れ替えが動く。戻し先の Pod が動いていなければ、Rolling Update で入れ替わる(次の節で確認する)

Warning の意味:YAML ファイルとのずれ

rollout undo も kubectl set image も、YAML ファイルを書き換えない。今は、

  • YAML ファイルの image: nginx:1.17.10(書き換えたまま)
  • 実物の Deployment の image: nginx:1.17.10(typo にして、戻した)

でたまたま一致している。確かめるために、YAML を apply してみる。

$ kubectl apply -f demo-deployment.yaml

# => deployment.apps/demo-app unchanged

unchanged。YAML と実物が同じなので、何も起きない。もし typo を YAML に書いて apply していたら、YAML は typo のままで、undo しても YAML は直らない。YAML で管理しているなら、戻すときも YAML を直して apply する、というのが Warning の言っていること。rollout undo は「今すぐ戻したい」ときの近道。

特定の revision に戻す(--to-revision)

--to-revision=1 で最初の定義(1.16.1)に戻す。今度は 1.16.1 の Pod が動いていないので、Rolling Update で入れ替わる。

$ kubectl rollout undo deployment/demo-app --to-revision=1; kubectl rollout status deployment/demo-app

Warning: resource deployments/demo-app was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation, which may cause unexpected behavior on future 'kubectl apply' operations. Consider using 'kubectl apply' with your previous configuration file instead.
deployment.apps/demo-app rolled back
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
deployment "demo-app" successfully rolled out

$ kubectl rollout history deployment/demo-app

deployment.apps/demo-app
REVISION  CHANGE-CAUSE
3         <none>
4         <none>
5         <none>

$ kubectl get rs -l app=demo-app -o custom-columns='NAME:.metadata.name,REVISION:.metadata.annotations.deployment\.kubernetes\.io/revision,DESIRED:.spec.replicas,IMAGE:.spec.template.spec.containers[0].image'

NAME                  REVISION   DESIRED   IMAGE
demo-app-6c4dcbb5b8   3          0         nginx:1.17.10-typo
demo-app-799468465    4          0         nginx:1.17.10
demo-app-86547cd9f8   5          3         nginx:1.16.1

$ kubectl exec deploy/demo-app -- nginx -v

# => nginx version: nginx/1.16.1
  • --to-revision=1 で、1.16.1 の定義(最初の ReplicaSet 86547cd9f8)に戻した。今度は 1.16.1 の Pod が 1 つも動いていなかったので、rollout status に 1 out of 3 ... が出て、Rolling Update で入れ替わった
  • REVISION は 3, 4, 5。1 が 5 になった(ReplicaSet 86547cd9f8 のアノテーションが 1 → 5)
  • ReplicaSet は作り直されず、同じ 86547cd9f8 が再利用された。同じ Pod の定義なら同じハッシュになるため。Pod は新しく作られる
  • ない番号を指定すると error: unable to find specified revision N in history になる

YAML を apply すると、YAML の image に進む

今、YAML ファイルは nginx:1.17.10、実物は nginx:1.16.1。ずれている。この状態で apply すると、YAML が正になって 1.17.10 に入れ替わる。

$ kubectl apply -f demo-deployment.yaml; kubectl rollout status deployment/demo-app

deployment.apps/demo-app configured
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
deployment "demo-app" successfully rolled out

$ kubectl rollout history deployment/demo-app

deployment.apps/demo-app
REVISION  CHANGE-CAUSE
3         <none>
5         <none>
6         <none>

$ kubectl exec deploy/demo-app -- nginx -v

# => nginx version: nginx/1.17.10
  • configured。rollout undo で戻した結果が、apply で上書きされた。kubectl scale の結果が apply で上書きされるのと同じ
  • 1.17.10 の ReplicaSet 799468465 が再利用され、revision が 4 → 6 になった

image を変える方法と戻す方法の関係はこうなる。

YAML を書き換えて applykubectl set imagekubectl rollout undo
変えるフィールドDeployment の spec.template(の image)同じ同じ(前の ReplicaSet の定義を書き戻す)
そのあとの動き新しい ReplicaSet を作って Rolling Update同じ同じ(戻し先の ReplicaSet は再利用)
YAML ファイルファイルの値が正変わらない(ずれる)変わらない(ずれる)。Warning が出る
向いている場面ふだんの更新・戻し(Git に残る)今すぐ試したい今すぐ戻したい。そのあと YAML も直して apply

消す

$ kubectl delete -f demo-deployment.yaml -f demo-service.yaml -f client.yaml

deployment.apps "demo-app" deleted from default namespace
service "demo-service" deleted from default namespace
pod "client" deleted from default namespace

$ kubectl get all

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   14d

数 0 で残っていた古い ReplicaSet(履歴)も、Deployment と一緒に消える。

全部消してから作る(strategy.type: Recreate)

Recreate にすると、古い Pod 3 つを全部消してから新しい Pod を作る。その間、アプリは止まる。

spec に replicas と並べて strategy を足し、image を nginx:1.17.10 にする。

# demo-deployment-recreate.yaml(spec の部分。ほかは demo-deployment.yaml と同じ)
spec:
  replicas: 3
  strategy:
    type: Recreate
  ...
        - name: nginx
          image: nginx:1.17.10

image: nginx:1.16.1 の Deployment・Service・client を作り直してから、Recreate の YAML を apply する。

$ kubectl apply -f demo-deployment.yaml -f demo-service.yaml -f client.yaml

deployment.apps/demo-app created
service/demo-service created
pod/client created

$ kubectl rollout status deployment/demo-app

# => deployment "demo-app" successfully rolled out

$ kubectl apply -f demo-deployment-recreate.yaml; kubectl rollout status deployment/demo-app

deployment.apps/demo-app configured
Waiting for deployment "demo-app" rollout to finish: 0 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 0 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 0 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 0 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 0 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 0 of 3 updated replicas are available...
Waiting for deployment "demo-app" rollout to finish: 1 of 3 updated replicas are available...
Waiting for deployment "demo-app" rollout to finish: 2 of 3 updated replicas are available...
deployment "demo-app" successfully rolled out

$ kubectl describe deploy demo-app | grep -E 'StrategyType'

# => StrategyType:       Recreate

$ kubectl describe deploy demo-app | grep -A 12 'Events:' | tail -2

  Normal  ScalingReplicaSet  13s   deployment-controller  Scaled down replica set demo-app-86547cd9f8 from 3 to 0
  Normal  ScalingReplicaSet  12s   deployment-controller  Scaled up replica set demo-app-799468465 from 0 to 3

$ kubectl logs client | grep -B2 -A2 FAIL

04:59:13   Server: nginx/1.16.1
04:59:14   Server: nginx/1.16.1
04:59:17 FAIL
04:59:18   Server: nginx/1.17.10
04:59:19   Server: nginx/1.17.10
  • rollout status が 0 out of 3 new replicas have been updated... から始まる。新しい Pod が 1 つもできる前に、古い Pod を全部消している
  • Events: は Scaled down ... from 3 to 0 → Scaled up ... from 0 to 3 の 2 行だけ。Rolling Update の 6 行(1 つずつ)と違う
  • client に FAIL が出た。04:59:14 の返事のあと 04:59:17 まで返事がなく、3 秒ほど止まった。nginx の起動が速いので短いが、起動に 30 秒かかるアプリなら 30 秒止まる
  • Recreate を使うのは、古いバージョンと新しいバージョンが同時に動いてはいけないとき(例: 同じデータを別の形式で書く)

消してから作る順にする(maxSurge: 0)

maxSurge: 0(replicas を超えて作らない)にすると、「古いのを 1 つ消してから、新しいのを 1 つ作る」順になる。

spec に strategy を足し、image は nginx:1.16.1(1.17.10 から戻す形)。

# demo-deployment-surge.yaml(spec の部分。ほかは demo-deployment.yaml と同じ)
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 0
      maxUnavailable: 1
  ...
        - name: nginx
          image: nginx:1.16.1
設定3 個のとき順番
既定(maxSurge: 25% / maxUnavailable: 25%)最大 4・最低 3作ってから消す
maxSurge: 0 / maxUnavailable: 1最大 3・最低 2消してから作る

1.17.10 が動いている状態から apply し、kubectl get deploy -w で見る。UP-TO-DATE が 3、READY が 3/3 になったら Ctrl+C。

$ kubectl apply -f demo-deployment-surge.yaml; kubectl get deploy demo-app -w

deployment.apps/demo-app configured
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
demo-app   2/3     1            2           94s
demo-app   2/3     1            2           94s
demo-app   3/3     1            3           95s
demo-app   2/3     1            2           95s
demo-app   2/3     2            2           95s
demo-app   2/3     2            2           95s
demo-app   3/3     2            3           95s
demo-app   2/3     2            2           95s
demo-app   2/3     3            2           95s
demo-app   2/3     3            2           95s
demo-app   3/3     3            3           96s

$ kubectl describe deploy demo-app | grep -E 'RollingUpdateStrategy'

# => RollingUpdateStrategy:  1 max unavailable, 0 max surge

$ kubectl describe deploy demo-app | grep -A 14 'Events:' | tail -6

  Normal  ScalingReplicaSet  20s   deployment-controller  Scaled down replica set demo-app-799468465 from 3 to 2
  Normal  ScalingReplicaSet  20s   deployment-controller  Scaled up replica set demo-app-86547cd9f8 from 0 to 1
  Normal  ScalingReplicaSet  19s   deployment-controller  Scaled down replica set demo-app-799468465 from 2 to 1
  Normal  ScalingReplicaSet  19s   deployment-controller  Scaled up replica set demo-app-86547cd9f8 from 1 to 2
  Normal  ScalingReplicaSet  19s   deployment-controller  Scaled down replica set demo-app-799468465 from 1 to 0
  Normal  ScalingReplicaSet  19s   deployment-controller  Scaled up replica set demo-app-86547cd9f8 from 2 to 3
  • READY が 2/3 → 3/3 → 2/3 と上下する。先に 1 つ消すので、一瞬 2 個になる
  • Events: が Scaled down → Scaled up の順。既定の Rolling Update では Scaled up → Scaled down だった。1 つずつ入れ替えるのは同じで、作ると消すの順番だけが逆
  • 順番が逆になるのは、上限が 3 + 0 = 3 で「3 個そろった状態からは作れない」ため。deployment-controller は「作れるなら作る、作れなければ消せるだけ消す」を繰り返すだけで、設定を変えると同じ手順から逆の順番が出てくる
  • 使いどころは、Node に Pod を増やす余裕がないとき。逆に、1 つも減らしたくないなら maxUnavailable: 0(そのとき maxSurge は 1 以上が必要。両方 0 にはできない)

止まった入れ替えを失敗と判定させる(progressDeadlineSeconds)

止まった入れ替えを放っておくと、Conditions: の Progressing が False / ProgressDeadlineExceeded になり、kubectl rollout status がエラー終了する。それでも Kubernetes は戻さない。

spec に replicas と並べて progressDeadlineSeconds: 60 を足し、image を存在しない nginx:1.17.10-typo にする。

# demo-deployment-deadline.yaml(spec の部分。ほかは demo-deployment.yaml と同じ)
spec:
  replicas: 3
  progressDeadlineSeconds: 60
  ...
        - name: nginx
          image: nginx:1.17.10-typo
フィールド意味
progressDeadlineSeconds: 60この秒数進まなかったら「進んでいない」と status に書く。既定は 600(10 分)

apply して、20 秒後と 75 秒後に Conditions: を見る。

$ kubectl apply -f demo-deployment-deadline.yaml

# => deployment.apps/demo-app configured

$ kubectl describe deploy demo-app | grep -A 4 'Conditions:'

Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    ReplicaSetUpdated

$ kubectl describe deploy demo-app | grep -A 4 'Conditions:'

Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    False   ProgressDeadlineExceeded

$ kubectl get deploy demo-app -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}'

Available	True	MinimumReplicasAvailable	Deployment has minimum availability.
Progressing	False	ProgressDeadlineExceeded	ReplicaSet "demo-app-6c4dcbb5b8" has timed out progressing.

$ kubectl rollout status deployment/demo-app

# => error: deployment "demo-app" exceeded its progress deadline

$ echo $?

# => 1

$ kubectl get pods -l app=demo-app

NAME                        READY   STATUS             RESTARTS   AGE
demo-app-6c4dcbb5b8-qjf9k   0/1     ImagePullBackOff   0          2m50s
demo-app-86547cd9f8-h7ffg   1/1     Running            0          6m13s
demo-app-86547cd9f8-pzlpg   1/1     Running            0          6m13s
demo-app-86547cd9f8-qmpgk   1/1     Running            0          6m14s
  • 20 秒後は Progressing True ReplicaSetUpdated(まだ進行中扱い)。60 秒を過ぎると Progressing False ProgressDeadlineExceeded、message が ReplicaSet "..." has timed out progressing.
  • kubectl rollout status が error: deployment "demo-app" exceeded its progress deadline で終了し、終了コードが 1。期限なしのときは終わらず待ち続けていたのと違う。CI などで「更新が失敗したら止める」に使えるのはこの終了コード
  • それでも Pod はそのまま。古い 3 つが動き、typo の 1 つが ImagePullBackOff のまま。Available True も変わらない。公式ドキュメントは、Kubernetes は status を書く以外のことはしない、としている。戻すのは人か上位のツール

戻して終わる。

$ kubectl rollout undo deployment/demo-app

Warning: resource deployments/demo-app was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation, which may cause unexpected behavior on future 'kubectl apply' operations. Consider using 'kubectl apply' with your previous configuration file instead.
deployment.apps/demo-app rolled back

$ kubectl describe deploy demo-app | grep -A 4 'Conditions:'

Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable

Progressing True NewReplicaSetAvailable が「入れ替えが完了している」状態。

CHANGE-CAUSE に理由を残す

kubectl rollout history の CHANGE-CAUSE は、ずっと <none> だった。Deployment の metadata.annotations(リソースに付ける自由なメモ。キー: 値 の形)に kubernetes.io/change-cause というキーで文を書いて apply すると、その文がこの欄に入る。

demo-deployment.yaml の metadata に、name と並べて annotations を足す(image は nginx:1.16.1 に戻す)。

metadata:
  name: demo-app
  annotations:
    kubernetes.io/change-cause: "nginx を 1.16.1 に戻す"
$ kubectl apply -f demo-deployment.yaml; kubectl rollout status deployment/demo-app

deployment.apps/demo-app configured
deployment "demo-app" successfully rolled out

$ kubectl rollout history deployment/demo-app

deployment.apps/demo-app
REVISION  CHANGE-CAUSE
2         <none>
4         <none>
5         nginx を 1.16.1 に戻す
  • アノテーションは Deployment に書くが、deployment-controller がそのときの ReplicaSet にコピーする。rollout history が読んでいるのは ReplicaSet のほう
  • revision の番号は、それまでに何回変えたかで変わる
  • 公式ドキュメントでは、この値に kubectl set image ... のようなコマンドの文字列を入れる例を載せている。kubectl annotate deployment/<名前> kubernetes.io/change-cause="..." --overwrite で付け直すと、今の revision の CHANGE-CAUSE が書き換わる(新しい revision は増えない)

入れ替えを一時停止する(kubectl rollout pause / resume)

pause しておくと、image を変えても入れ替えが始まらない。resume で始まる。複数の変更をまとめて 1 回の入れ替えにしたいとき用。

$ kubectl rollout pause deployment/demo-app

# => deployment.apps/demo-app paused

$ kubectl set image deployment/demo-app nginx=nginx:1.17.10

# => deployment.apps/demo-app image updated

$ kubectl get rs -l app=demo-app

NAME                  DESIRED   CURRENT   READY   AGE
demo-app-6c4dcbb5b8   0         0         0       7m33s
demo-app-799468465    0         0         0       12m
demo-app-86547cd9f8   3         3         3       12m

$ kubectl describe deploy demo-app | grep -A 4 'Conditions:'

Conditions:
  Type           Status   Reason
  ----           ------   ------
  Available      True     MinimumReplicasAvailable
  Progressing    Unknown  DeploymentPaused

$ kubectl rollout resume deployment/demo-app; kubectl rollout status deployment/demo-app

deployment.apps/demo-app resumed
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "demo-app" rollout to finish: 1 old replicas are pending termination...
deployment "demo-app" successfully rolled out
  • pause 中に image を変えても、ReplicaSet の数は動かない(1.16.1 の 86547cd9f8 が 3 のまま)。Conditions: は Progressing Unknown DeploymentPaused
  • resume した瞬間に、いつもの Rolling Update が始まる
  • 止めている間は、progressDeadlineSeconds の時間も数えられない

用語メモ

用語読み方意味
rolloutロールアウトDeployment の Pod を、新しい定義(spec.template)の Pod に入れ替えること。kubectl rollout コマンドの名前
Rolling Updateローリング アップデート古い Pod を少しずつ新しい Pod に入れ替える rollout のやり方。Deployment の strategy.type の既定値 RollingUpdate
Recreateリクリエイト古い Pod を全部消してから新しい Pod を作るやり方。strategy.type: Recreate。止まる時間ができる
strategyストラテジーDeployment のフィールド spec.strategy。入れ替えのやり方(type)と、その細かい設定(rollingUpdate)
maxSurgeマックス サージspec.strategy.rollingUpdate.maxSurge。replicas を超えて作ってよい Pod の数。既定 25%(切り上げ)
maxUnavailableマックス アンアベイラブルspec.strategy.rollingUpdate.maxUnavailable。入れ替え中に足りなくてよい Pod の数。既定 25%(切り捨て)
revisionリビジョンPod の定義の版の番号。ReplicaSet のアノテーション deployment.kubernetes.io/revision。spec.template が変わるたびに 1 増える
revisionHistoryLimitリビジョン ヒストリー リミットspec.revisionHistoryLimit。数 0 で残す古い ReplicaSet の数。既定 10
Rollbackロールバック前の revision の定義に戻すこと。kubectl rollout undo
kubectl rollout statusキューブ コントロール ロールアウト ステータス入れ替えが終わるまで進み具合を表示して待つコマンド。終わっていれば successfully rolled out
kubectl rollout historyキューブ コントロール ロールアウト ヒストリーrevision の一覧。--revision=N でその定義
kubectl rollout undoキューブ コントロール ロールアウト アンドゥ1 つ前(--to-revision=N で指定の revision)の定義を Deployment に書き戻すコマンド。出力は rolled back
kubectl rollout pause / resumeキューブ コントロール ロールアウト ポーズ / リジューム入れ替えを止める / 再開する
kubectl set imageキューブ コントロール セット イメージYAML を書き換えずに、Deployment の image だけを変える近道のコマンド。出力は image updated
UP-TO-DATEアップ トゥ デートkubectl get deploy の列。今の spec.template で動いている Pod の数
ImagePullBackOffイメージ プル バックオフkubelet が image を取れず、間をあけて取り直しを待っている Pod の STATUS。失敗した瞬間は ErrImagePull
ProgressingプログレッシングDeployment の Conditions: の 1 つ。True / ReplicaSetUpdated は入れ替え中、True / NewReplicaSetAvailable は完了、False / ProgressDeadlineExceeded は期限切れ、Unknown / DeploymentPaused は一時停止中
progressDeadlineSecondsプログレス デッドライン セカンズspec.progressDeadlineSeconds。入れ替えが進まないとき、Progressing False にするまでの秒数。既定 600
change-causeチェンジ コーズアノテーション kubernetes.io/change-cause。rollout history の CHANGE-CAUSE 列に出るメモ
deployment-controllerデプロイメント コントローラーkube-controller-manager の中の係。spec.template が変わったら新しい ReplicaSet を作り、古い ReplicaSet と新しい ReplicaSet の DESIRED を入れ替える
last-applied-configurationラスト アプライド コンフィギュレーションアノテーション kubectl.kubernetes.io/last-applied-configuration。kubectl apply が「前回 apply した YAML の中身」を残しているもの。rollout undo はこれを更新しないので Warning が出る

まとめ

  • アプリの更新は、Deployment の spec.template(Pod の定義)を変えること。YAML を書き換えて apply しても、kubectl set image でも、変わるフィールドは同じ。replicas を変えるだけでは更新は起きない
  • template が変わると、deployment-controller が新しい ReplicaSet を作り、「新しいのを 1 つ増やす → READY になったら古いのを 1 つ減らす」を繰り返す(Rolling Update。strategy.type の既定)。入れ替え中は古い Pod と新しい Pod が混ざって動き、Service からのアクセスは途切れない
  • 一度に増やせる数・減らせる数は maxSurge / maxUnavailable(既定 25%)で決まる。3 個なら最大 4・最低 3 → 1 つずつ。maxSurge: 0 にすると消してから作る順になり、Recreate にすると全部消してから作る(止まる時間ができる)
  • 古い ReplicaSet は数 0 で残り、それが revision(履歴)。kubectl rollout history で一覧、--revision=N で中身。残す数は revisionHistoryLimit(既定 10)
  • 新しい Pod が動かない(ImagePullBackOff など)と、入れ替えは「1 つ作った」ところで止まる。古い Pod は消されないのでアプリは止まらない。Kubernetes は自分では戻さない(progressDeadlineSeconds を過ぎると Progressing False になるだけ)
  • kubectl rollout undo は、1 つ前の ReplicaSet の定義を Deployment に書き戻すコマンド。戻した定義は新しい revision 番号になる。--to-revision=N で指定の revision に戻せる
  • kubectl set image も rollout undo も YAML ファイルを変えない。YAML を apply すると YAML の値に戻る(上書きされる)。ふだんは YAML を直して apply する

参考 URL


[Prev] HPA: Kubernetes で負荷に応じて Pod を自動でスケールする

Author

rito

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級