Karpenter を Amazon EKS で使う

Toshiki Shimomura

2023.12.14

はじめに

Kubernetes のノードのオートスケーラーである Karpenter は,Amazon EKS クラスタでの利用を中心に普及しつつあります。

Karpenter を調べてみた・使ってみた系記事はたくさんあるので,この記事では実際の運用を主眼におき,様々な観点に沿って情報を整理したいと思います。

この記事で以下の目標が達成できればと思います。

  • 読者が円滑に導入・運用できるようにする。
  • 起きたトラブルと対処方法がわかる。

各項目は続きものではなく独立しているので,目次を見て気になる項目だけ読むのでもかまいません。

この記事では v0.29.2 で動作検証した内容を含んでいます。v0.31 まではカスタムリソースが同じなので記載内容はほぼ通用しますが,それ以降は適宜読み替えていただく必要があります。

分類v0.31 以前v0.32 以降
カスタムリソースProvisionerNodePool
カスタムリソースAWSNodeTemplateEC2NodeClass
カスタムリソースMachineNodeClaim
アノテーションkarpenter.sh/do-not-evictkarpenter.sh/do-not-disrupt

理解しておくべき考え方

リリースペース

まばらですが,だいたい1ヶ月に1回くらいのペースでマイナーバージョンがリリースされています。

機能や仕様が少しずつ変わり,以前のバージョンで作ったカスタムリソースがそのまま使えないことがあります。(この記事の内容も近いうちに古くなると思われます。)

採用したバージョンのまま塩漬けすると,新しいバージョンがどんどんリリースされて設定方法も変わるので,定期的にアップデートする運用を検討したほうがよさそうです。

Karpenter 自体のアップデート

Karpenter のコントローラー本体と CRD は別のインストール単位(Helm チャート)で提供されています(Helm 公式の Method 2)。

従来はコントローラーの Helm チャートに CRD が含まれていましたが,CRD のみ初回インストール以降の変更は反映されず,手動で最新の CRD を適用する必要がありました(同 Method 1)。v0.26.1 以降は CRD のみの Helm チャートが導入され,CRD の更新は Helm で行えるようになりました。

Upgrade Guide を読み,以下の点については,各バージョンでアップデートの検証をしたほうがよいです。

  • アップデート時にすでに起動しているノードは新バージョンでも整合性が取れるか?
  • CRD 更新が必要なバージョンにアップデートする場合,古いカスタムリソースが引き続き有効かどうか,その場合は一時的に新しいカスタムリソースと共存して不具合が発生することにならないか?

対応クラウド

特定のクラウドプロバイダには依存しない設計になっています。

ただし,AWS の専門チームが開発を主導しているので,EKS が主眼に置かれていて,ドキュメントもそれを前提とした書き方をしているところがあります。

GKE には現状対応していませんが,対応させようという動きはあります。

AKS にはつい先日(2023年11月)対応が発表されました。https://github.com/Azure/karpenter

バルーニング

スケールアウトの際,ノードの起動・追加にはどうしても時間がかかってしまいます。

その対策として,あらかじめダミーの Pod(バルーン)をデプロイしておき,ワークロードの Pod がスケジュールされたらバルーンと入れ替えることで,ノードの起動待ちを減らせます。

具体的な方法の説明とマニフェストは,以前発表したスライドに載せています。

なぜ Cluster Autoscaler より速いの?

ノードのオートスケーラーとして Cluster Autoscaler が従来から使われていました。

Karpenter のほうがスケールが速い理由を理解しておき,採用する場合は選んだ理由を説明できるようにしましょう。

  • EC2 インスタンスの作成を Auto Scaling グループではなく Fleet で実現するので,オーバーヘッドが小さい。
  • EC2 インスタンスの起動完了を待たずにノード登録・Pod のスケジュールまで終わらせる。

AWS OSS製の高速Cluster Autoscaler Karpenter | PSYENCE:MEDIA

ベストプラクティス

EKS ベストプラクティスに Karpenter のページがあるので,一読しておきましょう。以下に要点を挙げておきます。

  • Karpenter 自体は Karpenter 管理外のノードにデプロイする。EKS Fargate でも通常のワーカーノードでもよい。
  • 起動テンプレートを使わない。AWSNodeTemplate を使う。
  • ワークロードに合わないインスタンスタイプを除外する。
  • スポットインスタンスの中断について
    • Interruption Handling を有効にする。
    • ワークロードが通知を受け取ったら2分以内に終了できるようにしておく。
    • イベント通知が重複するので Node Termination Handler を併用しない。

マネージドノードグループとの使い分け

すでに EKS でクラスタを構築している方は,マネージドノードグループ(MNG)や Auto Scaling グループ(ASG)を使ってノードを展開しているかと思います。

その状況で Karpenter を導入したほうがいいか,そうでないかを判断するため,それぞれが適している場合を列挙しました。

Karpenter

MNG

Karpenter と ASG・MNG は併用もできます。スケール対象のワークロードによって使い分けるのもよいでしょう。Karpenter と Cluster Autoscaler は競合してしまうので併用できません。

ベストプラクティスでも書かれていますが,Karpenter 自体は Karpenter 管理ノードにデプロイしないように注意しましょう。MNG でも EKS Fargate でもよいです。

導入方法

基本的に Getting Started に沿って導入すれば問題ありません。

AWS リソース

公式 Getting Started

Getting Started では Cloud Formation スタックと eksctl コマンドで作っています。

実際のプロジェクトでは,公式サイト上にある特定バージョンの cloudformation.yaml ファイルに依存するのは好ましくないと判断したので,まとめて Terraform で書き直しました。

EKS Blueprints

EKS Blueprints にも公式とは別の文脈で Karpenter の導入例が紹介されています。こちらはコントローラーを EKS Fargate 上で動かします。Kubernetes のリソースも Terraform で作られています。

(参考)IRSA モジュール

terraform-aws-modules に Karpenter の IRSA を作れるモジュールがあります。

ただし,こちらは対応バージョンが書かれておらず,ポリシーが公式と微妙に違って権限エラーで動作しないことがあるので,使わないほうがよいです。

Kubernetes リソース

続いて Kubernetes 側で作成すべきリソースについて説明します。

Provisioner

どのような条件のときにどのようなノードをスケールするかを Provisioner で指定します。ノードとして使うコンピュートリソース(AWS では EC2 インスタンス)は次で作る NodeTemplate を参照します。

設定できる項目例:

  • インスタンスタイプ・サイズ・シリーズ
    • 対象のワークロードの性質上,明らかに当てはまらない(選ばれると困る)シリーズ・アーキテクチャ・サイズは除外しましょう。
    • その上で,なるべく広めに取ったほうがワークロードに合わせたコスト最適なインスタンス選択の観点で効果的です。
  • ゾーン(AZ)
  • キャパシティタイプ:スポットかオンデマンド

AWSNodeTemplate

古いバージョンでは指定できた起動テンプレートが使えなくなり,起動テンプレート相当の内容を AWSNodeTemplate に書くことになりました。

ノード起動のたびに一時的な起動テンプレートが作成・削除されます。

設定できる項目例:

  • サブネット
  • セキュリティグループ
  • AMI
  • ユーザースクリプト

Pod

カスタムリソースからは外れますが,ノードのスケールに関わるワークロードの定義があるので,所望のノードオートスケールになるように適切に設定しましょう。

ノードのオートスケールに影響する項目例:

  • nodeSelector:Provisioner のラベルに対応
  • affinity
  • topologySpreadConstraints:ゾーン分散

ケーススタディ

実際の運用上で起こりうるいくつかのケースとその対処法について紹介します。

インスタンスタイプが大きい

一時的にスケジュール待ちの Pod が多くなると,選択されるインスタンスタイプが大きくなることがあります。実際に 32xlarge のサイズが選択されることもありました。

対処法としては,Provisioner でインスタンスタイプ(サイズ)を絞り込めるので,あまりに大きいサイズは除外しておけば,大きめのノード1台ではなく小さめのノード複数台が選択されます。

スポット中断の退避が2分で終わらない

PDB が設定されている場合は,スポット中断通知から実行までの制限時間2分以内に退避が終わらないことがあります。現バージョン(v0.30)でも状況は変わっていません。

ASG・MNG は再調整の推奨通知を受け取ったら,代替のインスタンスを立ち上げて早めに退避する挙動なので,問題になりにくいです(参考事例)。

スポットインスタンスの中断の挙動は以下の通りです。

  1. 再調整の推奨通知が出る。
    • スポット中断される可能性が高まったという意味。
    • ASG・MNG はこのタイミングで退避を始める。
      1. のスポット中断通知より前に出ることになっているが,保証はされていないので,同時に出ることがある。
  2. スポット中断通知が出る。
  3. 2分後に中断される。

Pod の終了に時間がかかる要件で,再調整の推奨通知を必要とする場合は,Node Termination Handler (NTH) を組み合わせる必要があります。ただし,NTH と Karpenter 側の中断ハンドリングネイティブサポートの併用は,通知を二重処理する可能性があるため非推奨とされています。

スポットインスタンスの在庫がない

選択されたインスタンスタイプでスポットインスタンスの在庫が一時的にない場合,オンデマンドにフォールバックしてスケールが完了するかについて考えます。

ドキュメントを読む限り, karpenter.sh/capacity-type の値に [spot, on-demand] の両方を指定すれば,フォールバックすることが期待されます。
またこちらの記事では,当時のバージョンで EC2 API での在庫不足を再現したものをビルドして検証しています。

そもそもインスタンスタイプを広めに設定しておけば,在庫のあるインスタンスタイプが選ばれるので,あまり心配しなくてよいともいえます。

検証

最近追加された consolidation 機能について検証します。

Consolidation モード

従来はノードごとに TTL(ttlSecondsAfterEmpty)を設定し,DaemonSet 以外の Pod がスケジュールされなくなってから TTL の時間が経過すればそのノードを落とすという挙動でした。このため,ノードが空になっても Pod が新たにスケジュールされると残り時間がリセットされ,ワークロードに合わないサイズのノードでも使い続けられるという欠点がありました。

新たに追加された Consolidation モードでは,ノードの TTL によらず,小さいサイズのノードに集約することで効率化を図り,コストを削減することができます。

以下の3観点について,Consolidation モードで動作させてみました。

シンプルにレプリカ数を減らした場合の挙動

以下のマニフェストを用意します。

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0  # レプリカ数を変化させる。
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      nodeSelector:
        intent: apps
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.2
          resources:
            requests:
              cpu: "1"
              memory: 1.5Gi

Provisioner

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  labels:
    intent: apps
  requirements:
    - key: karpenter.sh/capacity-type
      operator: In
      values: [spot]  # spot と on-demand を切り替える。
    - key: kubernetes.io/arch
      operator: In
      values: [amd64]
  limits:
    resources:
      cpu: 1000
      memory: 1000Gi
  consolidaton: true
  providerRef:
    name: default

AWSNodeTemplate

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  labels:
    intent: apps
  requirements:
    - key: karpenter.sh/capacity-type
      operator: In
      values: [spot]  # spot と on-demand を切り替える。
    - key: kubernetes.io/arch
      operator: In
      values: [amd64]
  limits:
    resources:
      cpu: 1000
      memory: 1000Gi
  consolidaton: true
  providerRef:
    name: default

以下の条件にしてあります。

  • AWS 公式のダミーコンテナイメージを使う。
  • リソース要求を固定で設定する。
  • その他制約は指定しない。

Deployment のレプリカ数を変化させ,ノードがどのように変化するかを観察しました。

項番観点キャパシティ時刻変化前の値変化後の値挙動ノード構成知見
12の準備スポット14:17:00010約40秒でスケジュールされた。c5.4xlarge (16, 27.696Gi) x 1
2要求増加時にスケールアップするかスポット14:26:001020ノードが追加され,追加 Pod が約40秒でスケジュールされた。c5.4xlarge (16, 27.696Gi) x 1
m5d.2xlarge (7.9, 29.916Gi) x 1
別ノードが追加される。スケールアップではなくスケールアウトになる。
3ノード過剰時にスケールダウンするかスポット14:30:0020102で追加されたノードが約20秒で削除された。c5.4xlarge (16, 27.696Gi) x 1減らせるノードがある場合は減る。スケールダウンではなくスケールインになる。
4ゼロスケールスポット14:32:001001で追加されたノードが約20秒で削除された。ゼロスケールする。
56の準備スポット14:35:00020約50秒でスケジュールされた。m5zn.6xlarge (24, 89.337Gi) x 1
6ノード過剰時にスケールダウンするかスポット14:38:00202数秒で Pod 数が変化した。ノードは変わらなかった。m5zn.6xlarge (24, 89.337Gi) x 1減らせるノードがない場合はスケールダウンしない。
78の準備スポット14:46:30205で追加されたノードが約20秒で削除された。
オンデマンド14:51:25020約50秒でスケジュールされた。c5a.8xlarge (32, 59.253Gi) x 1
8ノード過剰時にスケールダウンするかオンデマンド14:54:10202数秒で Pod 数が変化した。約50秒でノードがスケールダウンした。c5a.xlarge (3.9, 6.681Gi) x 1オンデマンドインスタンスではスケールダウンする。
910の準備オンデマンド15:00:00208で追加されたノードが約20秒で削除された。
オンデマンド16:51:00010約50秒でスケジュールされた。c5a.4xlarge (16, 28.131Gi) x 1
10要求増加時にスケールアップするかオンデマンド16:54:301020ノードが追加され,追加 Pod が約40秒でスケジュールされた。c5a.4xlarge (16, 28.131Gi) x 1
c5a.2xlarge (7.9, 14.461Gi) x 1
別ノードが追加される。スケールアップではなくスケールアウトになる。
11後片付けオンデマンド17:07:002009, 10で追加されたノードが約20秒で削除された。

わかったことを以下にまとめます。

  • 要求増加時にはスケールアップではなくスケールアウトになる。
    • スポット・オンデマンドのどちらも別ノードが追加される。
  • スポットインスタンスではリプレイスされない。
    • ノード過剰時にはスケールダウンしない。★
    • スポットインスタンスは最安値のインスタンスタイプにすると中断の可能性が高くなるのでリプレイスしない仕様になっている。
  • オンデマンドインスタンスではリプレイスされる。
    • ノード過剰時にはスケールダウンして,ちょうどよいインスタンスタイプになる。
    • 要求増加時にはノードを追加する。スケールアップはしない。

★の挙動から,スポットインスタンスの場合は過剰スペックのままになることがあります。

対策として,ノード停止の TTL である ttlSecondsUntilExpired を設定することで,稼働時間を打ち切り,適切なサイズの新しいノードを起動することができます。

PDB の影響

リバランシングされるノード上の Pod が PDB の影響を受ける場合,リバランシングはどれくらい遅くなるでしょうか?

オンデマンドインスタンスで要求減少時にスケールダウンする場合に PDB の影響を受けるので,先程と同様に検証します。

既存の Deployment に以下のマニフェストを追加します。

Deployment

# /spec/template/spec/containers
      containers:
        - name: inflate
          image: ubuntu:latest
          command: ["/bin/sh", "-c", "sleep infinity"]
          resources:
            requests:
              cpu: "1"
              memory: 1.5Gi
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 20"]

以下の PDB のマニフェストを追加で用意します。

PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: inflate
spec:
  selector:
    matchLabels:
      app: inflate
  maxUnavailable: 1

1個ずつ入れ替わるような設定です。

項番観点キャパシティ時刻変化前の値変化後の値挙動ノード構成知見
12の準備オンデマンド18:19:30020約50秒でスケジュールされた。c5a.8xlarge (32, 59.253Gi) x 1
2ノード過剰時にスケールダウンするかオンデマンド18:21:0020218個の Pod が約50秒かけて一斉に終了した。
約120秒でノードのスケールダウンが完了した。
・Pod は1個ずつ入れ替わった。
・旧 Pod の preStop フックの終了を待たずに新 Pod が開始された。
c5a.xlarge (3.9, 6.681Gi) x 1PDB を満たしながらリバランシングが行われる。
代替 Pod は旧 Pod の preStop フック終了までは待たない。
3後片付けオンデマンド18:27:00200約50秒で Pod が削除された。
2で追加されたノードが約70秒で削除された。
preStop フックおよび terminationGracePeriodSeconds を待ってからノードが削除される。

PDB を満たしながら Pod の退避が行われていることがわかります。
PDB による退避の長期化が問題ないかを確認しておきましょう。

do-not-evict の影響

Pod に karpenter.sh/do-not-evict: "true" のアノテーションを追加すると,その Pod は終了まで evict されないことになっています。

https://karpenter.sh/docs/concepts/disruption/#pod-level-controls

Pod にアノテーションが追加されている場合,Pod の配置によって consolidation が行われるかどうかが決まります。(ただし,evict 不可の Pod をスケールインする用途はあまりないと思われます。)

下の例では,レプリカ数を減らした際にノード間の分散を保ったままだったため,evict できないため consolidation が起こりません。

項番観点キャパシティ時刻変化前の値変化後の値挙動ノード構成知見
11ノードにするオンデマンド14:42:0005約50秒でスケジュールされた。c5a.2xlarge (7.9, 14.461Gi) x 1
22ノードにするオンデマンド14:44:15510約40秒でスケジュールされた。c5a.2xlarge (7.9, 14.461Gi) x 2
3do-not-evict があっても1ノードに戻るかオンデマンド14:46:00105約50秒で Pod が削除された。
2ノードに(2個・3個の配置で)分散したまま減少した。
c5a.2xlarge (7.9, 14.461Gi) x 2Pod がノード間に分散したままレプリカ数が減少すると,Consolidation が行われない。

したがって,do-not-evict アノテーションを追加する際は consolidation が行われない可能性があることに留意しましょう。追加する場合は,ジョブなど一定時間に終了するものに限定すれば,consolidation の効果を活かせます。

まとめ

今回は Karpenter を実際に運用する上での留意事項を,資料調査内容および検証結果とともにまとめました。

Karpenter を導入する際にどれか一つでも参考になれば幸いです。

今回の記事で取り上げられなかった論点については,別の記事で取り上げられたらと思います。

ブログ一覧へ戻る

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

SREの設計・技術支援から、
SRE運用内で使用する
ツールの導入など、
SRE全般についてご支援しています。

資料請求・お問い合わせ