はじめに
Kubernetes のノードのオートスケーラーである Karpenter は,Amazon EKS クラスタでの利用を中心に普及しつつあります。
Karpenter を調べてみた・使ってみた系記事はたくさんあるので,この記事では実際の運用を主眼におき,様々な観点に沿って情報を整理したいと思います。
この記事で以下の目標が達成できればと思います。
- 読者が円滑に導入・運用できるようにする。
- 起きたトラブルと対処方法がわかる。
各項目は続きものではなく独立しているので,目次を見て気になる項目だけ読むのでもかまいません。
この記事では v0.29.2 で動作検証した内容を含んでいます。v0.31 まではカスタムリソースが同じなので記載内容はほぼ通用しますが,それ以降は適宜読み替えていただく必要があります。
分類 | v0.31 以前 | v0.32 以降 |
---|---|---|
カスタムリソース | Provisioner | NodePool |
カスタムリソース | AWSNodeTemplate | EC2NodeClass |
カスタムリソース | Machine | NodeClaim |
アノテーション | karpenter.sh/do-not-evict | karpenter.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
- ノードのオートスケーラーを新規に導入したい場合
- スポットインスタンス中断時,Pod の退避が2分以内に終わる場合
- 負荷変動が大きい場合
MNG
- すでに Cluster Autoscaler を使っていて,使い続けたい場合
- スポットインスタンス中断時,Pod の退避が2分以内に終わらない場合
- キャパシティリバランシング機能で,中断しそうになると代替ノードを起動して早めに退避する
- https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/managed-node-groups.html#managed-node-group-capacity-types
- https://team-blog.mitene.us/karpenter-b48ca7cdc22a
- 負荷変動が比較的落ち着いている場合(同じインスタンスタイプのスケールで問題ない場合)
- システム系リソースなど
- Karpenter のコントローラーをホストするには適する
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 は再調整の推奨通知を受け取ったら,代替のインスタンスを立ち上げて早めに退避する挙動なので,問題になりにくいです(参考事例)。
スポットインスタンスの中断の挙動は以下の通りです。
- 再調整の推奨通知が出る。
- スポット中断される可能性が高まったという意味。
- ASG・MNG はこのタイミングで退避を始める。
- のスポット中断通知より前に出ることになっているが,保証はされていないので,同時に出ることがある。
- スポット中断通知が出る。
- 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 によらず,小さいサイズのノードに集約することで効率化を図り,コストを削減することができます。
- AWS ブログ
- ハンズオン
以下の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 のレプリカ数を変化させ,ノードがどのように変化するかを観察しました。
項番 | 観点 | キャパシティ | 時刻 | 変化前の値 | 変化後の値 | 挙動 | ノード構成 | 知見 |
---|---|---|---|---|---|---|---|---|
1 | 2の準備 | スポット | 14:17:00 | 0 | 10 | 約40秒でスケジュールされた。 | c5.4xlarge (16, 27.696Gi) x 1 | |
2 | 要求増加時にスケールアップするか | スポット | 14:26:00 | 10 | 20 | ノードが追加され,追加 Pod が約40秒でスケジュールされた。 | c5.4xlarge (16, 27.696Gi) x 1 m5d.2xlarge (7.9, 29.916Gi) x 1 | 別ノードが追加される。スケールアップではなくスケールアウトになる。 |
3 | ノード過剰時にスケールダウンするか | スポット | 14:30:00 | 20 | 10 | 2で追加されたノードが約20秒で削除された。 | c5.4xlarge (16, 27.696Gi) x 1 | 減らせるノードがある場合は減る。スケールダウンではなくスケールインになる。 |
4 | ゼロスケール | スポット | 14:32:00 | 10 | 0 | 1で追加されたノードが約20秒で削除された。 | – | ゼロスケールする。 |
5 | 6の準備 | スポット | 14:35:00 | 0 | 20 | 約50秒でスケジュールされた。 | m5zn.6xlarge (24, 89.337Gi) x 1 | |
6 | ノード過剰時にスケールダウンするか | スポット | 14:38:00 | 20 | 2 | 数秒で Pod 数が変化した。ノードは変わらなかった。 | m5zn.6xlarge (24, 89.337Gi) x 1 | 減らせるノードがない場合はスケールダウンしない。 |
7 | 8の準備 | スポット | 14:46:30 | 2 | 0 | 5で追加されたノードが約20秒で削除された。 | – | |
オンデマンド | 14:51:25 | 0 | 20 | 約50秒でスケジュールされた。 | c5a.8xlarge (32, 59.253Gi) x 1 | |||
8 | ノード過剰時にスケールダウンするか | オンデマンド | 14:54:10 | 20 | 2 | 数秒で Pod 数が変化した。約50秒でノードがスケールダウンした。 | c5a.xlarge (3.9, 6.681Gi) x 1 | オンデマンドインスタンスではスケールダウンする。 |
9 | 10の準備 | オンデマンド | 15:00:00 | 2 | 0 | 8で追加されたノードが約20秒で削除された。 | – | |
オンデマンド | 16:51:00 | 0 | 10 | 約50秒でスケジュールされた。 | c5a.4xlarge (16, 28.131Gi) x 1 | |||
10 | 要求増加時にスケールアップするか | オンデマンド | 16:54:30 | 10 | 20 | ノードが追加され,追加 Pod が約40秒でスケジュールされた。 | c5a.4xlarge (16, 28.131Gi) x 1 c5a.2xlarge (7.9, 14.461Gi) x 1 | 別ノードが追加される。スケールアップではなくスケールアウトになる。 |
11 | 後片付け | オンデマンド | 17:07:00 | 20 | 0 | 9, 10で追加されたノードが約20秒で削除された。 | – |
わかったことを以下にまとめます。
- 要求増加時にはスケールアップではなくスケールアウトになる。
- スポット・オンデマンドのどちらも別ノードが追加される。
- スポットインスタンスではリプレイスされない。
- ノード過剰時にはスケールダウンしない。★
- スポットインスタンスは最安値のインスタンスタイプにすると中断の可能性が高くなるのでリプレイスしない仕様になっている。
- https://karpenter.sh/docs/concepts/disruption/#consolidation の Note に書かれている。
- オンデマンドインスタンスではリプレイスされる。
- ノード過剰時にはスケールダウンして,ちょうどよいインスタンスタイプになる。
- 要求増加時にはノードを追加する。スケールアップはしない。
★の挙動から,スポットインスタンスの場合は過剰スペックのままになることがあります。
対策として,ノード停止の 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個ずつ入れ替わるような設定です。
項番 | 観点 | キャパシティ | 時刻 | 変化前の値 | 変化後の値 | 挙動 | ノード構成 | 知見 |
---|---|---|---|---|---|---|---|---|
1 | 2の準備 | オンデマンド | 18:19:30 | 0 | 20 | 約50秒でスケジュールされた。 | c5a.8xlarge (32, 59.253Gi) x 1 | |
2 | ノード過剰時にスケールダウンするか | オンデマンド | 18:21:00 | 20 | 2 | 18個の Pod が約50秒かけて一斉に終了した。 約120秒でノードのスケールダウンが完了した。 ・Pod は1個ずつ入れ替わった。 ・旧 Pod の preStop フックの終了を待たずに新 Pod が開始された。 | c5a.xlarge (3.9, 6.681Gi) x 1 | PDB を満たしながらリバランシングが行われる。 代替 Pod は旧 Pod の preStop フック終了までは待たない。 |
3 | 後片付け | オンデマンド | 18:27:00 | 20 | 0 | 約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 が起こりません。
項番 | 観点 | キャパシティ | 時刻 | 変化前の値 | 変化後の値 | 挙動 | ノード構成 | 知見 |
---|---|---|---|---|---|---|---|---|
1 | 1ノードにする | オンデマンド | 14:42:00 | 0 | 5 | 約50秒でスケジュールされた。 | c5a.2xlarge (7.9, 14.461Gi) x 1 | |
2 | 2ノードにする | オンデマンド | 14:44:15 | 5 | 10 | 約40秒でスケジュールされた。 | c5a.2xlarge (7.9, 14.461Gi) x 2 | |
3 | do-not-evict があっても1ノードに戻るか | オンデマンド | 14:46:00 | 10 | 5 | 約50秒で Pod が削除された。 2ノードに(2個・3個の配置で)分散したまま減少した。 | c5a.2xlarge (7.9, 14.461Gi) x 2 | Pod がノード間に分散したままレプリカ数が減少すると,Consolidation が行われない。 |
したがって,do-not-evict アノテーションを追加する際は consolidation が行われない可能性があることに留意しましょう。追加する場合は,ジョブなど一定時間に終了するものに限定すれば,consolidation の効果を活かせます。
まとめ
今回は Karpenter を実際に運用する上での留意事項を,資料調査内容および検証結果とともにまとめました。
Karpenter を導入する際にどれか一つでも参考になれば幸いです。
今回の記事で取り上げられなかった論点については,別の記事で取り上げられたらと思います。