Kubernetes上で動くアプリのバージョンアップ、裏側でなにがおこっているのか
最近業務でお手伝いさせていただく案件のなかでも、Java on Kubernetesなパターンが増えてきています。Kubernetesに限らずクラウドのマネージドPaaSで動かすにはアプリケーション側でもクラウドネイティブを考慮した実装が必要になり、特に耐障害性や回復性などを意識した実装が大事になってきます。
アプリケーションのリリース方式も、オンプレでのWeb3層システムとは考え方が異なる部分やKubernetesのしくみを正しく理解しておく必要があるので、あらためて整理したいと思います。
アプリのバージョンアップ
アプリケーション開発の世界では、1度リリース完了したらそれで終わりではなく、新機能追加やバグ修正などによりバージョンアップが行われます。特にビジネス要件の変更が頻繁なケースでは、小さな単位でアプリケーションの機能追加/修正し、短いタイミングでリリースする手法が使われています。
しかしながら、アプリケーションのバージョンアップには危険を伴います。ちょっとした設定ミスによりシステムエラーをおこし、場合によってはサービス停止に至ることもあるでしょう。したがって、テスト済みの安全なものを、なるべく迅速に本番環境にデプロイできるしくみを整えることが大事です。
アプリケーションを本番環境にデプロイする手法はいくつかありますが、代表的なものは次の2つです。
ローリングアップデート
アプリケーションをバージョンアップする際に、まとめて一気に変更するのではなく、稼動状態のまま少しずつ順番に更新する手法です。同じアプリケーションが複数並列に動いている場合に徐々に入れ替えていくので、バージョンアップ中は新旧のアプリケーションが混在することになります。そのためアプリケーションがローリングアップデートに対応している必要があります。
ブルー/グリーンデプロイメント
バージョンの異なる新旧2つのアプリケーションを同時に起動させておき、ネットワークの設定変更で変更する方法です。ブルー(旧)とグリーン(新)を切り替えることから、ブルー/グリーンデプロイメントと呼ばれます。 ブルーが本番としてサービス提供しているときには、グリーンは待機している状態となります。新機能は、待機系であるグリーン側に追加して、こちらで事前テストを実施します。そしてテストをクリアしたことを確認したうえでグリーンを本番に切り替えます。この方式はもし切り替えたグリーンのアプリケーションで障害があったときに、即座にブルーに切り戻せるというメリットがあります。
このほかにも、一部の利用者にのみ新機能を提供し、問題がないことを確認してから全ユーザに大規模展開するカナリアリリースなどもあります。
Deploymentを使ったアプリのバージョンアップ
Kubernetesには、安全にアプリをアップデートするしくみが備わっていて「Deployment」と呼ばれています。
この、Deploymentリソースには、大きく分けて次の2つのアップデートの処理方式があります。これは、マニフェストファイルのDeploymentのspec
-strategy
-type
で設定し、デフォルトはRollingUpdate
です。
spec:
strategy:
type: Recreate
Recrate いったん古いPodをすべて停止し、新しいPodを再作成する方式です。シンプルで高速に動きますが、ダウンタイムが発生します。開発環境やダウンタイムが許容できるシステムなどで使います。
RollingUpdate クラスターで動くPodを少しずつアップデートしていく方式です。古いPodが動いている状態で、新しいPodを起動し、新しいPodの起動が確認出来たら古いPodを停止するという動きをします。一時的に新旧のバージョンが混在するので処理方法は複雑になりますが、ダウンタイムなしで移行できるのが特徴です。本番環境ではこちらを採用するのが良いでしょう。
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はそれぞれOldReplicaSets
とNewReplicaSet
フィールドで確認できます。
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
に入れ替わっているのが分かります。
詳細にログをみると、以下が行われているのが分かります。
- 新しいReplicaSetを作成
- 新しいReplicaSetのPodを増やす
- 古いReplicaSetのPodを減らす
- 古いReplicaSetのPodが0になるまで手順2と3を繰り返す
このようにサービスが停止しないように、内部で整合性をとりながら徐々にロールアウトしているのが分かります。
ロールバック
コンテナーイメージを変更し、アプリケーションのバージョンを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のマニフェストでrevisionHistoryLimit
1を設定しましょう。
ローリングアップデートの制御
アプリケーションによってはアップデート中の性能縮退を最小に抑えたい場合もあります。Deploymentではこのローリングの処理を細かく制御できます。
使用できないPodの制御(maxUnavailable)
アップデート中に使用できなくなってもよいPodの最大数を指定するときはDeploymentマニフェストのspec
-strategy
-rollingUpdate
-maxUnavailable
を設定します。設定できる値はPodの数または、Podの割合(%)です。例えば10個のPodを起動したシステムで、maxUnavailable
を30%に設定すると、アップデート中でも、常に利用可能なPodの総数が希望のPodの70%以上、つまり7個以上のPodがクラスター内で常に利用できる状態になります。
クラスターでPodを作成できる最大数(maxSurge)
ロールアウトをするときにどのぐらいの追加リソースを作成できるかを制御したいときは、Deploymentマニフェストのspec
-strategy
-rollingUpdate
-maxSurge
を設定します。たとえば、この値を30%に設定すると、ローリングアップデートが開始されるとすぐに新しいレプリカセットを拡大して、古いポッドと新しいポッドの合計数が130%を超えないように調整します。この値を100%にすると、クラスターの中にReplicasで指定した数だけ、新しいPodがすべて立ち上がってから、古いPodをスケールダウンさせます。これは、新旧のPodが共存することになりますので、ブルー/グリーンデプロイメントになります。安全性は高まりますが、一方2倍のPodを稼働させるため、Worker Nodeのコンピューティングリソースを多く用意しておく必要があります。
apiVersion: apps/v1
kind: Deployment
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 30%
maxUnavailable: 10%
なお、maxUnavailable
とmaxSurge
はいずれもレプリカ数を指定できますが、特別な理由がない限り全体のレプリカ数に対する割合で指定するのが良いでしょう。なぜなら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のしくみや世界観も含めて理解しておくとよいなとおもいます。
以上