Karpenter でステートフルな Pod を安全に再配置する方法
背景
私が担当しているプロダクトでは、Karpenter によるノードスケーリングを導入しています。
当初、クラスタ上ではステートレスなワークロードだけが動いているという前提で運用していました。
ところが、しばらく経ってから一部にステートフルな処理が含まれていたことが分かり、Karpenter の自動的なノード削除( Disruption )が原因で、思わぬ影響が出る可能性があることに気付きました。根本的にはステートフルな処理へ改修すべきなのですが、それと同時に暫定的な対処方法を模索しました。
今回は、do-not-disrupt
アノテーションを使って Karpenter の自動ドレインを防ぎつつ、Pod の再配置をこちらのタイミングで安全に行う方法を紹介します。
Karpenter の導入を検討している方や、すでに導入していてステートフルなワークロードとの共存に課題を感じている方に向けて、本記事がヒントになれば幸いです。
前提
- Amazon EKS 1.29 ( →1.30 )
- Karpenter 0.37.7
Karpenter の Drift と Disruption の抑制
Karpenter では、ノードの設定が現在の NodePool(またはNodeClassなど)の定義とズレている場合、自動的に Drift を検知し、ノードを置き換えるために Disruption(ノード削除とPodのドレイン) を行おうとします。
例えば以下のようなケースが対象になります。
- AMI のバージョンが古くなった
- ラベルや taint の構成が変更された
- NodePool の定義を変更したが、既存ノードが更新されていない
こうした Drift による自動削除は便利な反面、ステートフルな Pod がそのノード上にいる場合、意図しないタイミングで Pod が消えるという問題に繋がります。
これを防ぐために使えるのが、次のアノテーションです。
apiVersion: karpenter.sh/v1beta1
kind: NodePool
### <略> ###
metadata:
annotations:
karpenter.sh/do-not-disrupt: "true"
このアノテーションを NodePool や Pod に付与しておくと、Karpenter はそれらを含むノードに対して Disruption を実行しなくなります(例えば EKS アップグレードにより AMI が変更された場合等)。
つまり、ステートフルな Pod を守りつつ、再配置のタイミングをこちらで制御することが可能になります。
自身のタイミングでノード再配置を行う手順
アノテーションを付与したことにより、 Drift による Disruption を抑制することができました。次の手順により、Karpenter 管理下のノードにおいても、自身のタイミングで Pod を再配置することができます。
1. 対象ノードを cordon する
まず、Podが再スケジュールされないようにノードをスケジューリング不可(cordon)にします。
kubectl cordon <MY_NODE_NAME>
2. 新しいノードを手動で作成する
次に、対象のワークロードを再配置するための新しいノードを作成します。
ダミーの Pod を意図的に起動することでノードを明示的に立てることができます。
ここでは、1台だけノードを立てる方法と、複数台に分散させる場合の例を紹介します。
1台のノードを用意する
ノードを1台立ち上げます。
kubectl run --rm -it tmp-debian --image=debian:bullseye-slim --restart=Never --pod-running-timeout=5m0s --overrides='{ "apiVersion": "v1", "spec": {"nodeSelector": { "NodeGroupName": "<MY_NODE_GROUP_NAME>" }}}' -- /bin/bash
複数台のノードを用意する
ノードが1台に収まりきらない場合は、以下のマニフェストを作成して複数台のノードを立ち上げることができます。
apiVersion: apps/v1
kind: Deployment
metadata:
name: prepare-nodes
spec:
replicas: 3
selector:
matchLabels:
app: prepare-nodes
template:
metadata:
labels:
app: prepare-nodes
spec:
. # Podをできるだけ別々のノードに分散させる
topologySpreadConstraints:
- maxSkew: 1
topologyKey: "kubernetes.io/hostname"
whenUnsatisfiable: "DoNotSchedule"
labelSelector:
matchLabels:
app: prepare-nodes
affinity:
# 同じノードに同じラベルのPodを置かない
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: prepare-nodes
topologyKey: "kubernetes.io/hostname"
# 特定のインスタンスタイプとノードグループにのみ配置する
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "node.kubernetes.io/instance-type"
operator: "In"
values: ["t3a.medium"]
- key: "NodeGroupName"
operator: "In"
values: ["<MY_NODE_GROUP_NAME>"]
containers:
- name: prepare-nodes
image: debian:bullseye-slim
command: ["/bin/bash", "-c", "sleep 3600"]
resources:
limits:
cpu: "100m"
memory: "64Mi"
以下のように apply
します。
kubectl apply -f prepare-nodes.yaml
3. rollout restart を実行する
再配置したい Pod を持つリソースに対して、rollout restart
を行います。
kubectl rollout restart deployment <MY_DEPLOYMENT>
これにより、Pod が再スケジュールされ、新たに作成したノードに移動します。
4. DaemonSet のみが残っていることを確認し、古いノードを drain
Podの移動が完了し、DaemonSet以外のPodが存在しないことを確認したうえで、古いノードを drain します。
kubectl drain <MY_NODE_NAME> --ignore-daemonsets --delete-emptydir-data
まとめ
-
karpenter.sh/do-not-disrupt: "true"
アノテーションにより、Karpenter による自動ドレインから Pod を保護できる - cordon → ノード作成 → rollout restart → drain の手順で、安全にノード再配置が可能
- ステートフルなワークロードでも、Karpenter の自動管理と共存可能な運用が実現できる
終わりに
久しぶりに EKS アップグレードを行う機会があり、Karpenter の細かい部分を知るきっかけとなりました。まだまだ学びの過程ですが、これからも有用な情報があれば発信していきたいと思います。もしより良い方法があれば教えていただけると幸いです。
Discussion