アプリケーションの更新とロールバック

KubernetesBeginner
オンラインで実践に進む

はじめに

これで、アプリケーションのデプロイ、公開、スケーリングができるようになりました。次に考えるべき運用上の課題は、サービス全体を停止せずにアプリケーションのバージョンを置き換える方法です。

Kubernetes の Deployment は、これにローリングアップデートで対応します。Pod テンプレートが変更されると、Deployment は新しい ReplicaSet を作成し、新しい Pod を段階的に起動します。そして、置き換え先が利用可能になった時点で、古い Pod を削除します。この間、Service は同じ安定したアドレスを維持します。

この実験では、NGINX アプリケーションを固定されたイメージから別のイメージへ移行し、Deployment のリビジョンと ReplicaSet、Pod の関係を確認します。さらに、意図的に壊れたリリースを試し、最後に正常だったリビジョンへロールバックします。また、保存済みマニフェストを復旧後の稼働状態と一致させます。これは、後でkubectl applyを実行した際に障害を再発させないための重要な習慣です。

開始時の環境を確認する

環境の起動: この実験では、完全な Kubernetes クラスターが起動します。コントロールプレーン、ノード、ネットワークコンポーネントの構成には通常 2~3 分かかります。環境の読み込みが完了するまで、そのままお待ちください。

このステップでは、クラスターへの接続を確認し、クラスター内から HTTP リクエストを送信するクライアントを準備します。

このコース環境には、すでに稼働中の Kubernetes クラスターが用意されています。そのため、クラスターを作成する必要はありません。まず、kubectlがコマンドをどこへ送信するのかを確認します。config current-contextはアクティブな接続先を示し、get nodeはノードの状態を確認し、versionはローカルクライアントと接続可能な API サーバーのバージョンを表示します。

kubectl config current-context
kubectl get node
kubectl version

コンテキストとノードの名前はlabex-v135です。ノードはReadyで、サーバーは Kubernetes v1.35.x を報告します。コンテキストとは、kubectlを特定のクラスター、ユーザー、デフォルト名前空間に接続するための kubeconfig の選択項目です。

ここで小さなクライアント Pod を作成します。後ほど、この Pod を使って Service 経由でアプリケーションへアクセスします。kubectl runは Pod を作成し、--imageは BusyBox を選択します。--restart=Neverは Pod を単独の Pod として維持し、--区切り記号以降で長時間実行するsleep 3600コマンドを指定します。その後、kubectl waitが最大 30 秒間、Ready条件になるのを待ちます。

kubectl run release-client --image=busybox:1.36 --image-pull-policy=IfNotPresent \
  --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/release-client --timeout=30s

これにより、呼び出し元と Web Pod を分離できます。これは、クラスター内であるワークロードが別のワークロードを呼び出す構成に近い方法です。

安定した基準状態をデプロイする

このステップでは、変更を加える前に、正常であることが確認されたアプリケーションリビジョンを確立します。

更新の練習を始める前に、正常なリビジョンを用意します。3 つのレプリカを持つ Deployment と、安定した ClusterIP Service を 1 つのマニフェストに定義して作成します。

cdは準備済みの作業ディレクトリへ移動します。ヒアドキュメント構文cat <<'EOF' > release-web.yamlは、閉じるEOFまでの内容をファイルに書き込みます。---行は、1 つの YAML ファイル内にある 2 つの Kubernetes オブジェクトを区切ります。

cd /home/labex/project/update-lab
cat <<'EOF' > release-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: release-web
  annotations:
    kubernetes.io/change-cause: "Initial release: nginx 1.26"
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: release-web
  template:
    metadata:
      labels:
        app: release-web
    spec:
      containers:
        - name: nginx
          image: nginx:1.26-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: release-web
spec:
  selector:
    app: release-web
  ports:
    - name: http
      port: 80
      targetPort: http
EOF
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment,service,pods -l app=release-web

Deployment は Pod を管理し、Service はラベルに基づいて独立して Pod を選択します。Deployment を更新しても、Service のアドレスは変わりません。

クライアントから基準状態を確認します。kubectl exec POD -- COMMANDでは、--kubectlの引数とコンテナ内で実行するコマンドを区切ります。wget -qO-は静かにデータを取得して標準出力へ出力し、パイプでレスポンスをheadに渡して先頭部分だけを表示します。

kubectl exec release-client -- wget -qO- http://release-web | head

新しいイメージを宣言的にリリースする

このステップでは、保存済みの Pod テンプレートを変更し、Deployment にローリングアップデートを実行させます。

Deployment は、Pod テンプレートが変更されたときにロールアウトを開始します。イメージは Pod テンプレート内にあるため、イメージを変更すると新しいリビジョンと新しい ReplicaSet が作成されます。

保存済みマニフェストのイメージと、人が読める変更理由の両方を更新します。各sed -i 's/old/new/' file置換は、ファイルを直接編集します。その後、grep -nEで拡張正規表現のいずれかの項目(change-causeまたはimage:)を表示し、適用前に両方の変更を確認します。

cd /home/labex/project/update-lab
sed -i 's/Initial release: nginx 1.26/Release nginx 1.27/' release-web.yaml
sed -i 's/nginx:1.26-alpine/nginx:1.27-alpine/' release-web.yaml
grep -nE 'change-cause|image:' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s

kubectl applyは望ましい状態を変更します。その後、Deployment コントローラーが非同期に動作し、3 つの更新済みレプリカがすべて利用可能になるまで処理を続けます。Pod に絞った一覧を表示するには、-lでアプリケーションラベルを選択し、-o custom-columnsで見出しとオブジェクトのフィールドパスを対応付けます。

kubectl get deployment release-web
kubectl get pods -l app=release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready'

現在の 3 つのレプリカはすべて新しいイメージを使用しています。ロールアウトが成功した後も、一時的に古いイメージの Pod がTerminating状態で表示されることがあります。この Pod はもはや望ましいレプリカではありません。Kubernetes がバックグラウンドで正常な終了処理を完了させています。

リビジョン、ReplicaSet、Pod の関係を確認する

このステップでは、成功したロールアウトの背後にあるオブジェクトを確認し、Service が利用可能な状態を維持していることを確かめます。

ロールアウトは完了していますが、単に「成功」と表示されるだけでなく、何が変わったのかを理解することが重要です。kubectl rollout historyは、保存された Deployment のリビジョンを読み取ります。次のget replicasetsコマンドでは、-lで関連するオブジェクトに絞り込み、custom-columnsでレプリカ数とイメージを比較します。

kubectl rollout history deployment/release-web
kubectl get replicasets -l app=release-web \
  -o custom-columns='NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas,IMAGE:.spec.template.spec.containers[0].image'

2 つの ReplicaSet が表示されるはずです。新しい ReplicaSet は 3 つの Pod を管理し、古い ReplicaSet はロールバックに備えて Pod テンプレートを利用できるよう、レプリカ数 0 のまま残ります。ReplicaSet の名前には Pod テンプレートから生成されたハッシュが含まれます。そのため、イメージを変更すると Pod 名も変わります。

稼働中のイメージと Service のバックエンド数を確認します。どちらの JSONPath 式も、リスト内の各要素に対して出力テンプレートを繰り返すためにrangeを使用しています。リテラルの空白と{"\n"}によって、Pod またはエンドポイントごとに読みやすい 1 行が表示されます。

kubectl get pods -l app=release-web \
  -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.spec.containers[0].image}{"\n"}{end}'
kubectl get endpointslices -l kubernetes.io/service-name=release-web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
kubectl exec release-client -- wget -qO- http://release-web | head

リビジョンは変わりましたが、Service には引き続き 3 つの準備完了バックエンドがあり、安定した名前も維持されています。

壊れたリリースを診断する

このステップでは、意図的にイメージエラーを発生させ、Pod イベントを使ってロールアウトが完了できない理由を特定します。

ここでは、よくあるリリースミスを再現します。存在しないコンテナイメージタグを指定します。この失敗を同じ Deployment 上で発生させることで、ロールアウト機構の重要な安全性を確認できます。2 つのsed -iコマンドは、アノテーションとイメージの文字列を直接編集します。ロールアウトのタイムアウトでは通常、失敗ステータスが返されますが、|| trueによって意図的にレッスンを続行できるようにしています。

cd /home/labex/project/update-lab
sed -i 's/Release nginx 1.27/Broken release: missing image/' release-web.yaml
sed -i 's/nginx:1.27-alpine/nginx:does-not-exist-course/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=20s || true
kubectl get deployment release-web
kubectl get pods -l app=release-web

新しい Pod がイメージを取得できないため、ロールアウトは完了しません。|| trueによって、予想されるタイムアウトの後もレッスンを続行できます。

失敗した Pod を見つけ、そのイベントを確認します。$(...)はコマンドの出力をBAD_PODに保存します。最初のパイプで Kubernetes の JSON をjqに渡し、select(...)で壊れたイメージを持つ Pod だけを選択します。-rは Pod 名をプレーンテキストで返します。2 つ目のパイプでhead -n1に渡し、1 つの名前だけを取得します。最後に、sed -n '/Events:/,$p'describeの出力からEvents:以降を最後まで表示します。

BAD_POD=$(kubectl get pods -l app=release-web \
  -o json | jq -r '.items[] | select(.spec.containers[0].image == "nginx:does-not-exist-course") | .metadata.name' | head -n1)
echo "$BAD_POD"
kubectl describe pod "$BAD_POD" | sed -n '/Events:/,$p'
kubectl get replicasets -l app=release-web

ErrImagePullまたはImagePullBackOffを探してください。以前の正常な ReplicaSet には引き続き利用可能な Pod があるため、Service が応答を継続できることにも注目してください。

kubectl exec release-client -- wget -qO- http://release-web | head

これは、ロールアウト失敗中もサービスの可用性が保たれていることを示すものであり、新しいリリースが正常に動作する証拠ではありません。

ロールバックしてマニフェストを整合させる

このステップでは、以前の正常なリビジョンを復元し、その後で保存済みマニフェストを修正します。

稼働中の Deployment にはリビジョン履歴があるため、Kubernetes は以前の Pod テンプレートを復元できます。rollout undoは、Deployment コントローラーに以前のリビジョンを再利用するよう指示します。rollout statusは復旧を待機し、custom-columnsは復元されたイメージと準備完了数を表示します。

kubectl rollout history deployment/release-web
kubectl rollout undo deployment/release-web
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.template.spec.containers[0].image,READY:.status.readyReplicas'

イメージは再び正常になり、3 つのレプリカが準備完了になります。rollout undoは古い Pod テンプレートから新しいリビジョンを作成するものであり、リビジョン番号を巻き戻すわけではありません。

もう 1 つ作業が残っています。YAML ファイルにはまだ壊れたイメージが記述されているため、後でkubectl applyを実行すると、Deployment が再び壊れてしまいます。保存済みの望ましい状態を、復旧後の稼働状態と一致させます。置換コマンドでファイルを修正し、applyで同期します。最後の履歴コマンドで、更新後のリビジョン履歴を確認します。

cd /home/labex/project/update-lab
sed -i 's/Broken release: missing image/Rollback to nginx 1.27/' release-web.yaml
sed -i 's/nginx:does-not-exist-course/nginx:1.27-alpine/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
grep -nE 'change-cause|image:' release-web.yaml
kubectl rollout history deployment/release-web

これで、クラスターとファイルの両方が、同じ正常なリリースを示す状態になりました。

より安全な更新予算を設定する

このステップでは、Deployment のロールアウト時に許容する一時的なキャパシティと可用性の上限を明示的に設定します。

Deployment の更新戦略設定は、今後の更新中に許容される一時的なキャパシティを制御します。

  • maxUnavailableは、ロールアウト中に利用できなくなってもよい望ましいレプリカ数です。
  • maxSurgeは、望ましいレプリカ数を超えて一時的に作成できる追加 Pod 数です。

この 3 レプリカの小規模なアプリケーションでは、3 つすべての望ましいレプリカを利用可能な状態に保ち、追加 Pod を 1 つまで許可します。

sedコマンドでは、/type: RollingUpdate/というアドレスを使って戦略の行を検索します。a\アクションは、必要なインデントを含む、改行で区切られた次の YAML を追加します。適用後、JSONPath で両方の戦略値を抽出し、オブジェクト全体を確認せずに編集結果を検証します。

cd /home/labex/project/update-lab
sed -i '/type: RollingUpdate/a\    rollingUpdate:\n      maxUnavailable: 0\n      maxSurge: 1' release-web.yaml
kubectl apply -f release-web.yaml
kubectl get deployment release-web \
  -o jsonpath='maxUnavailable={.spec.strategy.rollingUpdate.maxUnavailable}{"\n"}maxSurge={.spec.strategy.rollingUpdate.maxSurge}{"\n"}'
kubectl get deployment release-web

戦略フィールドだけを変更しても、Pod テンプレートは変わらないため、Pod は置き換えられません。今後イメージを更新すると、Kubernetes は追加 Pod を 1 つ作成でき、利用可能なレプリカ数を意図的に 3 未満へ減らさないように動作します。

この設定は可用性を優先しますが、クラスターに余分なキャパシティが必要です。万能な最適値はありません。大規模なアプリケーションではパーセンテージを使用することも多く、実際の選択はキャパシティ、起動時間、許容できる中断の程度によって決まります。

まとめ

アプリケーションのリリースについて、運用上のライフサイクル全体を確認しました。

  • 正常な Deployment と安定した Service の基準状態を構築した。
  • 固定されたイメージを宣言的に変更し、ロールアウトの完了を待った。
  • Deployment のリビジョンと、古い ReplicaSet および新しい ReplicaSet の関係を確認した。
  • 以前のリリースがサービスを提供し続ける中で、イメージ取得失敗を診断した。
  • 最後に正常だった Pod テンプレートへロールバックした。
  • 後続の適用操作で壊れたイメージが復元されないよう、マニフェストを整合させた。
  • 今後の更新に備え、可用性と追加 Pod 数の上限を明示的に設定した。

重要なのは、Deployment が時間の経過に伴う望ましい状態を管理するという点です。ロールアウトは単なるイメージの変更ではありません。これは ReplicaSet 間を制御しながら移行するプロセスであり、状況を観察して検証し、必要に応じて元に戻せるよう準備しておく必要があります。