Kubernetes上で動くアプリのバージョンアップ、裏側でなにがおこっているのか


最近業務でお手伝いさせていただく案件のなかでも、Java on Kubernetesなパターンが増えてきています。Kubernetesに限らずクラウドのマネージドPaaSで動かすにはアプリケーション側でもクラウドネイティブを考慮した実装が必要になり、特に耐障害性や回復性などを意識した実装が大事になってきます。

アプリケーションのリリース方式も、オンプレでのWeb3層システムとは考え方が異なる部分やKubernetesのしくみを正しく理解しておく必要があるので、あらためて整理したいと思います。


アプリのバージョンアップ

アプリケーション開発の世界では、1度リリース完了したらそれで終わりではなく、新機能追加やバグ修正などによりバージョンアップが行われます。特にビジネス要件の変更が頻繁なケースでは、小さな単位でアプリケーションの機能追加/修正し、短いタイミングでリリースする手法が使われています。

しかしながら、アプリケーションのバージョンアップには危険を伴います。ちょっとした設定ミスによりシステムエラーをおこし、場合によってはサービス停止に至ることもあるでしょう。したがって、テスト済みの安全なものを、なるべく迅速に本番環境にデプロイできるしくみを整えることが大事です。

アプリケーションを本番環境にデプロイする手法はいくつかありますが、代表的なものは次の2つです。

ローリングアップデート

アプリケーションをバージョンアップする際に、まとめて一気に変更するのではなく、稼動状態のまま少しずつ順番に更新する手法です。同じアプリケーションが複数並列に動いている場合に徐々に入れ替えていくので、バージョンアップ中は新旧のアプリケーションが混在することになります。そのためアプリケーションがローリングアップデートに対応している必要があります。

rolling-update

ブルー/グリーンデプロイメント

バージョンの異なる新旧2つのアプリケーションを同時に起動させておき、ネットワークの設定変更で変更する方法です。ブルー(旧)とグリーン(新)を切り替えることから、ブルー/グリーンデプロイメントと呼ばれます。 ブルーが本番としてサービス提供しているときには、グリーンは待機している状態となります。新機能は、待機系であるグリーン側に追加して、こちらで事前テストを実施します。そしてテストをクリアしたことを確認したうえでグリーンを本番に切り替えます。この方式はもし切り替えたグリーンのアプリケーションで障害があったときに、即座にブルーに切り戻せるというメリットがあります。

bluegreen

このほかにも、一部の利用者にのみ新機能を提供し、問題がないことを確認してから全ユーザに大規模展開するカナリアリリースなどもあります。

Deploymentを使ったアプリのバージョンアップ

Kubernetesには、安全にアプリをアップデートするしくみが備わっていて「Deployment」と呼ばれています。

この、Deploymentリソースには、大きく分けて次の2つのアップデートの処理方式があります。これは、マニフェストファイルのDeploymentのspec-strategy-typeで設定し、デフォルトはRollingUpdateです。

spec:
  strategy:
    type: Recreate

kubernetes-update

DeploymentによるRollingUpdate

ここではDeploymentリソースのデフォルトになっているRollingUpdateによるバージョンアップの挙動をみていきます。

ロールアウト

ロールアウトとは、アプリケーションをクラスター内にデプロイし、サービスを稼働させることです。

ここで、次のDeploymentを作成します。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: front
  name: front
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: front
  template:
    metadata:
      labels:
        app: front
    spec:
      containers:
        - image: xxx/frontend:v1.0.0
          name: front

次のコマンドを実行すると、クラスタにfrontend:v1.0.0がデプロイされます。

kubectl apply -f deployment.yaml

ロールアウトの状況を確認するはkubectl rollout statusコマンド、詳細を確認するにはkubectl describe deployコマンドを実行します。

kubectl rollout status deploy front
Waiting for deployment "front" rollout to finish: 0 of 3 updated replicas are available...
Waiting for deployment "front" rollout to finish: 1 of 3 updated replicas are available...
Waiting for deployment "front" rollout to finish: 2 of 3 updated replicas are available...
deployment "front" successfully rolled out

ロールアウトできているのが分かります。次に詳細をみてみます。

kubectl describe deploy front

するとDeploymentの詳細が表示されます。Annotationsフィールドにrevision=1が自動で付加されているのがわかりますが、この値は、ローリングアップデートのたびに増えていきます。

Name:                   front
Namespace:              default
CreationTimestamp:      Sun, 10 Oct 2021 09:02:34 +0900
Labels:                 app=front
Annotations:            deployment.kubernetes.io/revision: 1

以下のフィールドをみると、ローリングアップデートの状況が確認できます。

Selector:               app=front
Replicas:               3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  10% max unavailable, 30% max surge

Deploymentの状態はConditionsフィールド、ログはEventsフィールドで確認できます。Deploymentが切り替えた新旧のReplicaSetはそれぞれOldReplicaSetsNewReplicaSetフィールドで確認できます。

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

Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  39s   deployment-controller  Scaled up replica set front-7876d7bd66 to 3

次に、アプリケーションのバージョンアップをします。Deploymentの内容を変更します。ここでは、コンテナーのイメージを変更します。これによりDeploymentが新しいReplicaSetを作成し、アプリケーションのバージョンアップを行います。

~変更前~
      - image: xxx/frontend:v2.0.0
~変更後~
      - image: xxx/frontend:v1.0.0

次のコマンドを実行してDeploymentを 更新 します。

kubectl apply -f deployment.yaml 
deployment.apps/front configured

デプロイの状況は次のコマンドで確認できます。

kubectl get deploy
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
front   3/3     2            3           2m55s

kubectl get rs
NAME               DESIRED   CURRENT   READY   AGE
front-7876d7bd66   0         0         0       3m21s
front-7b95f674dc   3         3         3       42s

内部でどのように入れ替わっているのかEventsフィールドでログを見ていきましょう。

kubectl describe deploy front

Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  3m43s  deployment-controller  Scaled up replica set front-7876d7bd66 to 3
  Normal  ScalingReplicaSet  64s    deployment-controller  Scaled up replica set front-7b95f674dc to 1
  Normal  ScalingReplicaSet  54s    deployment-controller  Scaled down replica set front-7876d7bd66 to 2
  Normal  ScalingReplicaSet  54s    deployment-controller  Scaled up replica set front-7b95f674dc to 2
  Normal  ScalingReplicaSet  43s    deployment-controller  Scaled down replica set front-7876d7bd66 to 1
  Normal  ScalingReplicaSet  43s    deployment-controller  Scaled up replica set front-7b95f674dc to 3
  Normal  ScalingReplicaSet  33s    deployment-controller  Scaled down replica set front-7876d7bd66 to 0

古いReplicaSetfront-7876d7bd66から、新しいReplicaSetfront-7b95f674dcに入れ替わっているのが分かります。

詳細にログをみると、以下が行われているのが分かります。

  1. 新しいReplicaSetを作成
  2. 新しいReplicaSetのPodを増やす
  3. 古いReplicaSetのPodを減らす
  4. 古いReplicaSetのPodが0になるまで手順2と3を繰り返す

kubernetes-update-describe

このようにサービスが停止しないように、内部で整合性をとりながら徐々にロールアウトしているのが分かります。

ロールバック

コンテナーイメージを変更し、アプリケーションのバージョンをv1からv2にバージョンアップできました。これをふたたびv1に戻したらどのような動きをするのでしょうか。マニフェストを修正し、kubectl applyコマンドを実行します。

~変更前~
      - image: xxx/frontend:v2.0.0
~変更後~
      - image: xxx/frontend:v1.0.0
apply -f deployment.yaml 
deployment.apps/front configured

再度kubectl describeコマンドで詳細を確認するとAnnotationsフィールドがrevision=3に上がっているのがわかります。

kubectl describe deploy front

Name:                   front
Namespace:              default
CreationTimestamp:      Sun, 10 Oct 2021 09:02:34 +0900
Labels:                 app=front
Annotations:            deployment.kubernetes.io/revision: 3                      

なお、Deploymentのロールアウトは「Podを変更する」ためのしくみです。Podのspec-template以外の更新は、リビジョンが上がりません。たとえばreplicasフィールドの値を変更してもPodの数が増えるだけでrevisionの値は同じままです。

また、Deploymentでは履歴を管理しますので、長期間運用していると履歴情報が大きくなります。そのためロールバックの世代を指定しておくことも検討しましょう。履歴はデフォルトで10世代を管理しますが、これを変更するときはDeploymentのマニフェストでrevisionHistoryLimit1を設定しましょう。

ローリングアップデートの制御

アプリケーションによってはアップデート中の性能縮退を最小に抑えたい場合もあります。Deploymentではこのローリングの処理を細かく制御できます。

使用できないPodの制御(maxUnavailable)

アップデート中に使用できなくなってもよいPodの最大数を指定するときはDeploymentマニフェストのspec-strategy-rollingUpdate-maxUnavailableを設定します。設定できる値はPodの数または、Podの割合(%)です。例えば10個のPodを起動したシステムで、maxUnavailableを30%に設定すると、アップデート中でも、常に利用可能なPodの総数が希望のPodの70%以上、つまり7個以上のPodがクラスター内で常に利用できる状態になります。

kubernetes-update-maxunavailable

クラスターでPodを作成できる最大数(maxSurge)

ロールアウトをするときにどのぐらいの追加リソースを作成できるかを制御したいときは、Deploymentマニフェストのspec-strategy-rollingUpdate-maxSurgeを設定します。たとえば、この値を30%に設定すると、ローリングアップデートが開始されるとすぐに新しいレプリカセットを拡大して、古いポッドと新しいポッドの合計数が130%を超えないように調整します。この値を100%にすると、クラスターの中にReplicasで指定した数だけ、新しいPodがすべて立ち上がってから、古いPodをスケールダウンさせます。これは、新旧のPodが共存することになりますので、ブルー/グリーンデプロイメントになります。安全性は高まりますが、一方2倍のPodを稼働させるため、Worker Nodeのコンピューティングリソースを多く用意しておく必要があります。

kubernetes-update-maxsurge

apiVersion: apps/v1
kind: Deployment
  strategy:
    type: RollingUpdate
    rollingUpdate: 
      maxSurge: 30%
      maxUnavailable: 10%

なお、maxUnavailablemaxSurgeはいずれもレプリカ数を指定できますが、特別な理由がない限り全体のレプリカ数に対する割合で指定するのが良いでしょう。なぜならPodのレプリカ数はAutoScaleなどの設定で負荷に応じて動的に変化したとき、固定値にしておくと運用時に意図せぬ動作になってしまう可能性があるためです。

Kubernetes に設定変更を宣言的に反映するには

余談にはなりますが、Kubernetesにクラスタの設定情報を反映させるkubernetes applyコマンドは、該当のKubernetesリソースが存在しなければ新規作成し、存在すれば差分を反映するコマンドです。

Kubernetesを使う上でとても大事な概念である、宣言的な管理 – Declarative object configurationができます。

しかしながら、次のコマンドを実行してアプリケーションをリリースするとどうなるでしょうか?

kubectl delete -f deployment.yaml
kubectl apply -f deployment.yaml

これは、「すでにあるDeploymentリソースをいったん削除」 したあとで 「Deploymentリソースを新規に作成している」 ということになります。

この方式だとダウンタイムが発生します。そして、本ブログで説明したKubernetesが持つローリングアップデートの機能なども有効に働きません。

また、Podの終了処理(iptablesの書き換え/kubelet=>container runtimeによるContainerの削除)のタイミングの関係で通信エラーになる可能性も考えられます。

詳細については、Spring WebFlux + KuberntesでアプリのGraceful Shutdownを実装するで、Podの終了処理の詳細についてまとめましたので、あわせて読んでいただけるといいかもしれません。

なお、本番環境ではこのエントリのようにkubectlコマンドを実行してアプリを更新するのではなく、CI/CD環境を整備し、デプロイの処理を自動化することで事故を防ぐよう検討する必要があります。最近のトレンドとしては、GitOpsなどを導入しているケースが多くなっています。

たとえば私が今お仕事でセールスを担当しているAzure Arc Enabled Kubernetesでは、GitOpsのしくみとして CNCFのIncubating projectであるオープンソースのFluxを内部で使っていたりします。

まとめ

KubernetesのDeploymentは、Kubernetesリソースの中でも最も重要かつ奥深いリソースなので、今一度基本に立ち返り、使い方だけでなくKubernetesのしくみや世界観も含めて理解しておくとよいなとおもいます。

以上


  1. revisionHistoryLimit ↩︎