1. TOP
  2. BLOG
  3. TECH ARTICLE:連載 第9回
    karpenterの安全な使用方法
TECH ARTICLE

2024年08月19日

連載 第9回
karpenterの安全な使用方法

karpenterはノードのコストを最適化するために、ノードの数を積極的に変更します。新規のPodを生成するために新しいノードが実行すると、Podに設定されたリクエスト容量で最もコストが安いノードを新規に生成し、既存ノードの中で使用量が低いノードは削除し、既存ノードで実行中のPodを新しいノードへ移します。この作業で、既存のPodは削除されて新しいPodが開始します。Podが再開しても顧客サービスに影響を与えないためには、以下のオプションが必要です。

TerminationGracePeriodSeconds、PodDisruptionBudget、PriorityClass

今回はこのオプションを確認します。

1. Graceful Shutdown (安定的な終了)

ユーザの増減によりPodオートスケールが発生してノード数が増減すると、karpenterはコストを最適化するようにノードの数とタイプを変更します。

karpenterによる実サービスのノード数の変化の例

従来のクラスターオートスケールに比べてより頻繫にノード数を変更します。karpenterが自動的にノードを変更し、EC2ノードコストが以前に比べて約30%減少しました。しかし、ノードが終了するとノードで実行中のPodも同時に終了させられるので、サービスに影響を与える可能性があります。そのため、Podが安全に終了する設定が必要です。

クバネティスはノード(又はユーザ)が終了(以下Terminating)信号を受信すると、Podは下記のプロセスによりPodを終了します。

https://wangwei1237.github.io/Kubernetes-in-Action-Second-Edition/docs/Understanding_the_pod_lifecycle.html

① ユーザ、ノードなどでterminating信号を受信すると、Podはサービスエンドポイント(endpoint)で除外されて新しいリクエストを受信しません。そして、コントロールプレーン(Control plane)の etcdデータベースにもノードの状態はterminating状態に変更されます。

② Podがterminating状態に変わると、クバネティスはPod内の全てのコンテナにpreStopハンドラーを実行できるようにリクエストします。preStopハンドラーはPodが終了する前に、コンテナが実行すべき整理処理を実行するときに使用されます。例えば、コンテナがデータベースに変更事項を保存するなどの終了準備ができます。preStopの設定はマニュフェストに指定する必要があります。

③ preStopハンドラーの処理が完了すると、クバネティスはPodの全てのコンテナへSIGTERMシグナルを送ります。SIGTERMはgraceful shutdownのためのシグナルで、コンテナにプロセスを終了するようにリクエストします。このシグナルを受信したコンテナはpreStop整理処理を終えて自然に終了します。

④ Podはこの後でterminationGracePeriodSecondsに指定された時間待機します。この待機の間にコンテナは終了準備の処理をします。時間内に終了できないとPodは強制的に終了されます。PodのterminationGracePeriodSecondsのデフォルトの設定時間は30秒で、それ以上の時間が必要であればYAMLファイルの設定で変更します。筆者は4時間を設定する場合もあります。

⑤ terminationGracePeriodSeconds時間が経過後もコンテナが終了しない場合は、クバネティスはSIGKILLシグナルを送って強制的にコンテナを終了します。SIGKILLは強制terminatingシグナルで、コンテナが決まった時間に終了できない場合に使用されます。

既存のセッションが維持すべきVoIPなどの電話サービスの場合、セッションを正常終了する必要があります。もし、karpenterによりノードが終了されてPodが異常終了すると、ユーザの電話が切断されるため障害となります。電話中の顧客のサービスセッションは最後まで維持する必要があります。そのため、このようなサービスの設定は、VoIPなどの電話と関連Podは別の専用ノードで実行し、Podが終了する待機時間をterminationGracePeriodSecondsで設定し、例えば、4時間(14,400s)と設定し、全てのセッションを正常終了させてPodが終了できるようにします。この設定により、Podがterminating信号を受信しても4時間の追加時間を置いて、その時間に既存の通話が終了できるように待機します。

例で確認します。

busybox-graceful-shutdown-pod.yaml

1.	apiVersion: v1
2.	kind: Pod
3.	metadata:
4.	    name: pods-termination-grace-period-seconds
5.	spec:
6.	    containers:
7.	    - command:
8.	        - sleep
9.	        - "3600"
10.	        image: busybox
11.	        name: pods-termination-grace-period-seconds
12.	    terminationGracePeriodSeconds: 5 # Time to wait before moving from a TERM signal to the pod's main process to a KILL signal.

terminationGracePeriodSeconds時間を5秒に設定しました。Podは終了リクエストを受信すると直ちに削除できず5秒間、正常終了できるまで待機した後で削除されます。

Podを終了して5秒間維持しているかを確認します。busybox Podを実行します。

1.	(jerry-test:default)k8s-class:jerry$ cd karpenter/
2.	(jerry-test:default)k8s-class:jerry$ k apply -f busybox-graceful-shutdown-pod.yaml
3.	pod/pods-termination-grace-period-seconds created

Podを終了して別ウィンドウで ‘k get pod -w(wait)’ コマンドを使用して、リアルタイムでPodの変更事項を確認します。

1.	(jerry-test:default)k8s-class:jerry$ k delete pod pods-termination-grace-period-seconds
2.	pod "pods-termination-grace-period-seconds" deleted
1.	(jerry-test:default)k8s-class:jerry$ k get pod -w
2.	NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
3.	pods-termination-grace-period-seconds 1/1 Running 0 3s 10.110.42.118 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
4.	
5.	pods-termination-grace-period-seconds 1/1 Terminating 0 14s 10.110.42.118 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
6.	
7.	pods-termination-grace-period-seconds 0/1 Terminating 0 20s 10.110.42.118 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>

上記で確認できるようにTerminating(1/1)コマンドを受信して5秒後に完全に終了、Terminating(0/1)されました。つまり、直ちに終了せずに5秒間の猶予時間を維持しました。上記のような既存セッションを維持すべきサービスにはterminationGracePeriodSecondsオプションを設定し、既存のサービスが完全に正常終了するのに必要な時間を待ちます。

筆者はPodが終了しても既存セッションを維持すべき全サービスのPodに、Graceful Shutdownの設定を追加しました。

2.PodDisruptionBudgetの設定

karpenterがノードを停止すると、ノードで実行中のPodは終了して、他のノードで新しいPodが実行されます。terminatingコマンドを受信したPodは先にTerminating状態に入り、次の実行可能な他のノードで実行(Running)されます。先にTerminatingして次のPodを実行するため、既存のPodが終了して新しいPodが実行する(Terminating -> Running)間にサービス停止現象が発生することがあります。単純ですがサービス停止は障害ですので、サービス停止が発生しないようにクバネティスでPodDisruptionBudget(Pod停止状態の予算)の設定を追加できます。

PodDisruptionBudget(以下、PDB)はPodの停止を制御するポリシーです。PDBを使用するとクラスター内のPodの停止時点における制限を設定して、特定のPodが停止するまでの間でアプリケーションの可用性と安定性を維持できます。

PDBを理解するため次のようなシナリオを考えて見ます。

仮定する状況は以下の通りです。

  • クラスターにWebサービスをサポートする3つのPodがあります。
  • このサービスでは可用性を高めるために3つのPodが全て稼働中です。
  • もし、2つ以上の Podが同時に停止すると、サービス停止又は障害が発生することがあります。

上記の問題の発生を防止するためにPDBを設定して次の規則を定義できます。

  • MinAvailable:維持すべきPodの最小数を指定します。例えば、’MinAvailable: 2’を設定すると、最小2つのPodが常に稼働します。
  • MaxUnavailable:同時に停止する可能性があるPodの最大数を指定します。例えば、 ‘MaxUnavailable: 1’を設定すると、同時に停止できるPodは1つだけです。

上記のようにサービス特性に合わせてPDBを適切に設定すると、安定したサービスを提供できます。

実習で確認します。実習のシナリオは1つのノードで2つのPodを実行し、それらのPodにPDBを設定します。ノードをドレイン(Drain、他のノードへ移動)、つまり、ノードのPodを他のノードへ移動します。このときにPDBを設定すると1つ(minAvailable)のPodは移動しないで、元のノードで継続的に実行されます。

実習用のDeploymentリソースを以下の様に定義します。

busybox-nodeSelector.yaml

1.	apiVersion: apps/v1
2.	kind: Deployment
3.	metadata:
4.	    name: busybox
5.	    labels:
6.	        app: busybox
7.	spec:
8.	    replicas: 2
9.	    selector:
10.	        matchLabels:
11.	            app: busybox
12.	    template:
13.	        metadata:
14.	            labels:
15.	                app: busybox
16.	        spec:
17.	            containers:
18.	            - command:
19.	                - sleep
20.	                - "3600"
21.	                image: busybox
22.	                name: busybox
23.	            nodeSelector:
24.	                kubernetes.io/hostname: ip-10-110-47-109.ap-southeast-1.compute.internal

前に使用したBusybox Deployment Manifestと同じですが、nodeSelectorオプションを追加しました。nodeSelectorは名前から推測できるように、指定したノードでPodを実行するオプションです。上記の例は特定ホスト名のノード(ip-10-110-47-109.ap-southeast-1.compute.internal)でPodを実行する設定です。各EKSノードのホスト名を入力してマニュフェストファイルを変更します。

PDBもクバネティスリソースで、YAMLファイルで実行できます。設定は下記の様に非常に簡単です。

busybox-pdb.yaml

1.	apiVersion: policy/v1
2.	kind: PodDisruptionBudget
3.	metadata:
4.	    name: busybox
5.	    namespace: default
6.	spec:
7.	    minAvailable: 1
8.	    selector:
9.	        matchLabels:
10.	            app: busybox

. spec.minAvailable: 1

Podが終了しても実行すべきPodの最小数です。アプリケーションの特性により適切に数を調整します。

. spec.selector.matchLabels

PDBを適用するPodを指定します。サービスリソースがPodを指定するときselectorを使用することと同じLabel基準でTarget Podを指定します。

上記の2つのリソースを配布します。

1.	(jerry-test:default)k8s-class:jerry$ cd podDisruptionBudget/
2.	(jerry-test:default)k8s-class:jerry$ k apply -f busybox-pdb.yaml -f busybox-nodeSelector-deploy.yaml 
3.	poddisruptionbudget.policy/busybox created
4.	deployment.apps/busybox created

PodDisruptionBudgetはクバネティスリソースで下記のコマンドで確認できます。

1.	(jerry-test:default)k8s-class:jerry$ k get pdb
2.	NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
3.	busybox 1 N/A 

更に、詳細な情報を確認します。

1.	(jerry-test:default)k8s-class:jerry$ k describe pdb busybox 
2.	Name: busybox
3.	Namespace: default
4.	Min available: 1
5.	Selector: app=busybox
6.	Status:
7.	    Allowed disruptions: 1
8.	    Current: 2
9.	    Desired: 1
10.	    Total: 2
11.	(省略)

. Selector

Label(app=busybox)基準でPDBを適用するリソースを選択しました。

. Status: Allowed disruptions: 1

停止(disruptions)可能なPodの数が‘1’です。Target対象のbusybox Podを1つ(まで)終了できます。

PDB、デプロイメントリソースが実行中です。ノードを終了してPDBが正常動作するかを確認します。ノードで実行中のPodを他のノードへ強制移動するdrainコマンドを使用します。

karpenterはノードの数を最適化するconsolidation(統合)の処理を進めると、ノードにPodを実行できないようにして(cordon)、ノードにあるPodを他のノードへ移動(drain)するステップを実行します。その状況を再現するためにbusybox Podが実行中のノードを移動します。

Podが実行中のノードの情報を確認します。

1.	(jerry-test:default)k8s-class:jerry$ k get pod -o wide
2.	NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
3.	busybox-5784bfcbcd-2p9cj 1/1 Running 0 8m31s 10.110.37.91 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
4.	busybox-5784bfcbcd-gnf9l 1/1 Running 0 8m31s 10.110.39.167 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>

nodeSelector設定で2つのPodが全てip-10-110-47-109.ap-southeast-1.compute.internalのノードで実行中です。このPodのノードを移動します。k drainコマンド実行時に–ignore-daemonsetsと–delete-emptydir-dataのオプションを追加します。

1.	(jerry-test:default)k8s-class:jerry$ k drain ip-10-110-47-109.ap-southeast-1.compute.internal --ignore-daemonsets --delete-emptydir-data 
2.	node/ip-10-110-47-109.ap-southeast-1.compute.internal cordoned
3.	(省略)
4.	error when evicting pods/"busybox-5784bfcbcd-2p9cj" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
5.	(省略)
6.	error when evicting pods/"busybox-5784bfcbcd-2p9cj" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
7.	(省略)

–ignore-daemonsetsはノードで実行中のDaemonSet Podを終了するオプションで、–delete-emptydir-dataは特定のPodがノードの一時ディレクトリを使用させます。ノードを削除すると、データを使用できない警告メッセージを表示しました。他のノードでPodが再開可能なPodであることを確認して、そのオプションを追加してコマンドを実行します。

drainコマンドの実行時に出力されたメッセージを確認すると ‘busybox-5784bfcbcd-2p9cj’ のPodがEvict(ノードのリソースを確保するために kubeletがPodを停止)で、他のノードへ移動できません。つまり、PDBの設定で1つのPodは必ず実行しなければならないため、他のノードへPodが移動できないということです。

他のターミナルウィンドウで確認すると、1つのbusybox Podは実行中です。

1.	(jerry-test:default)k8s-class:jerry$ k get pod -o wide
2.	NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
3.	busybox-5784bfcbcd-2p9cj 1/1 Running 0 12m 10.110.37.91 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
4.	busybox-5784bfcbcd-5d4cc 0/1 Pending 0 2m24s <none> <none> <none> <none>

このようにPDBを設定すると、karpenterがノードを終了してもminAvailableの設定で該当する Podは必ず実行されます。サービスを安定的に運用するためには必要なデフォルト設定で、筆者はこの設定を実運用の環境サービスでも使用しています。Karpenterの使用環境で必ず設定をお勧めするオプションです。次は実習に使用したdrainなどのノードスケジューリングのコマンドの詳細を確認します。

3.ノードスケジューリングの設定 (drain、cordon、uncordon)

クバネティスはクラスターのノードを柔軟に管理し、Podの移動とスケジューリングを調整して安定性と可用性を保証できるようにdrainとcordon、uncordonコマンドを提供します。

drainコマンドはノードを安全に非活性化し、ノードの Podを他のノードへ移動(マイグレーション)するときに使用します。これはノードの整備や、クラスターのバージョンアップ、ノードの障害などの状況に対して有用です。実務で特定のノードに障害が発生して、ノードのPodを他のノードへ移動するときや、クラスターのバージョンをアップグレードするときに使用します (drainの実習は以上で終了です)。

次にcordon/uncordonコマンドはノードのスケジューリング状態を調節するために使用するコマンドです。ノードのスケジューリング状態を変更して、新しいPodのスケジューリングを許可したり、ブロックしたりできます。ノードをcordonすると、ノードにこれ以上新しいPodはスケジューリングされません。cordonは主にノードの障害や、整備、アップグレードなどで、ノードを一時的に使用できないようにするときに使用します。

Cordonの作業が終了すると uncordonコマンドを実行します。Cordonでスケジューリングが停止されたノードを再度活性化して、ノードに新しいPodのスケジューリングを許可します。cordon状態でノードを障害から回復するときと、整備が完了して再度使用可能にするためにuncordonコマンドを使用します。

上記のPDBのテストを終了して、ノードで新しいPodがスケジューリングできるようにするために uncordonコマンドを実行します。

先ず、ノードの状態を確認します。ip-10-110-47-109-*のノードは drainコマンド実行後で、 SchedulingDisabled状態です。

1.	(jerry-test:default)k8s-class:jerry$ k get nodes -o wide ip-10-110-47-109.ap-southeast-1.compute.internal
2.	k NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
3.	ip-10-110-47-109.ap-southeast-1.compute.internal Ready,SchedulingDisabled <none> 8d v1.26.4-eks-0a21954 10.110.47.109 <none> Amazon Linux 2 5.10.178-162.673.amzn2.x86_64 containerd://1.6.19

テストが完了したため、uncordonコマンドでノードのcordon状態を解除します。

1.	(jerry-test:default)k8s-class:jerry$ k uncordon ip-10-110-47-109.ap-southeast-1.compute.internal
2.	node/ip-10-110-47-109.ap-southeast-1.compute.internal uncordoned

ノードを確認すると、再度Podを実行できる状態(Ready)になりました。

1.	(jerry-test:default)k8s-class:jerry$ k get nodes -o wide ip-10-110-47-109.ap-southeast-1.compute.internal
2.	NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
3.	ip-10-110-47-109.ap-southeast-1.compute.internal Ready <none> 8d v1.26.4-eks-0a21954 10.110.47.109 <none> Amazon Linux 2 5.10.178-162.673.amzn2.x86_64 containerd://1.6.19

uncordonコマンドが正常に実行されました。

今回はkarpenterを適用した状態で安全にPodを終了できるGraceful Shutdown、Pod Disruption Budgetと追加でノードのdrainとcordon/uncordonを確認しました。

PDBのテストが完了したため、デプロイメントとPDBの設定を削除します。

1.	(jerry-test:default)k8s-class:jerry$ k delete deployments.apps busybox
2.	k ddeployment.apps "busybox" deleted
3.	
4.	(jerry-test:default)k8s-class:jerry$ k delete pdb busybox
5.	poddisruptionbudget.policy "busybox" deleted

1. 参考までに、‘kubectl rollout restart {deploy_name}’ コマンドを実行すると、先ず、Podを実行(running)し、実行が完了すると既存のPodを終了(terminating)状態に変更します。

2. PDBの詳細は次のURLを参照してください。https://kubernetes.io/docs/tasks/run-application/configure-pdb/

3. nodeSelectorに関する詳細は次回のadvancedSchedulingで説明します。

お気軽にお問い合わせください

製品に関する事やご不明な点など、お気軽にご相談ください。
また、ジェニファーソフトでは2週間の無償トライアルを提供しています。

詳しく知りたい方は
導入や費用のご相談は