📖

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