この記事はKarpenterのCluster Autoscaler(CA)との違いや実践して感じたことについて書いたメモです。
結論
- CA:基本は ASG(ノードグループ)を増減。要件が増えるほどASGを増やすことになる。
- Karpenter:NodePool(ポリシー) とPod要求から 最適なノードを直接作る。この場ではKarpenterをFargateで動かす
- ノード入れ替えの際にpodを強制停止できないクリティカルな領域は、do-not-disruptアノテーションとKarpenterのTerminationGracePeriodをなくし、マニュアルでノードの入れ替えを行う
Karpenterの登場人物(一部)
- NodePool:どんなノードを作ってよいか(requirements / taints / limits / disruption budgets など)を定義するポリシー。Karpenterは NodePoolを順に評価し、PodがtolerateできないtaintがあるNodePoolは使わない、複数マッチならweightが高いNodePoolを優先します。
- EC2NodeClass:AWS依存設定(サブネット/SG/AMI/Role 等)をまとめる部品。
- Disruption(縮退/置換):consolidation(集約)、drift(ズレの置換)などを “どれだけ・いつ” 実行していいかを budgets で制御できる。budgetsの既定(10%)や、どの種類のdisruptionをブロックするかも定義されている。
- Karpenter Controller(コントローラ)
Karpenter本体。Helmチャートで controller がDeploymentとして入ります。
Podのスケジューリング監視・Pod要件を評価・NodePool/NodeClassと突き合わせ・NodeClaimを作ってノードを起動し、不要ならスケールイン(deprovision)などを行う
コントローラはFargate上に置くことが出来ます。KarpenterのスケールインにKarpenterコントローラが巻き込まれるという自分で自分を削除することがないようにするためです。 - NodeClaim(CRD / “ノード作成のチケット”)
Karpenterが実際にノードを起動する時に作る中核リソースです。
Karpenterは Pending Podの要求を評価 → 互換なNodePool + NodeClassを選ぶ → NodeClaimを作る
(1) PodがPending(置けない)
|
v
(2) Karpenter ControllerがPod要求を評価 ← webhookも有効
|
v
(3) NodePool(ポリシー) + EC2NodeClass(AWS設定)を選ぶ
|
v
(4) NodeClaim(起動チケット)を作る(基本immutable)
|
v
(5) AWSにEC2起動(Subnet/SG/AMI/IAM Role / OD or Spot)
|
v
(6) Nodeとしてクラスタに登録 → 予定のPodがスケジュール
|
v
(7) Disruption(集約/ドリフト/割り込み対応)で縮退や置換
CAとKarpenterの違い
ノードグループ
CAはノードグループ(ASG)単位で増減します。つまり要件や制約が増えると、
- 常駐On-demand
- 常駐On-demand(高性能)
- バッチSpot
- GPU
- AZ固定 etc..
みたいに ノードグループを増やして運用するため複雑になります。
KarpenterはNodePoolに「許可する範囲」を持たせ、Pod需要に合わせて最適ノードを作るので、要件増加=ノードグループ増加になりにくい
スケーリング速度はCAの場合複数リソースを経由してEC2起動になりますが、karpenterはノードグループなどが不要で直接EC2を起動するので速度が速いです。
インスタンスタイプとSpotインスタンス
CAでもkarpenterでも複数のインスタンスタイプを使用できます。
CAもkarpenterもSpotとon-demandインスタンス両方が使えますが、karpenterはReservedインスタンスを使用することができます。なお、最適なインスタンスをkarpenterは自動で選ぶことが出来ます。
スケールイン方法
オートスケールで事故るポイントはだいたいスケールインです。Karpenterでは安全性を高めるため、
KarpenterはNodePoolの disruption.budgets で、
- 同時に何ノードまで縮退して良いか(% / 数)
- 平日日中は0にする などのスケジュール制御(土日の利用が多かったり障害対応に人員がいないとかだと土日を0にしたり)
ができます。
更新(AMI/世代更新)
CAはスケール自体は担いますが、AMI更新や世代更新はASG/LT更新・ローリングなどを運用側で握ることが多く、「いつ・どれだけ置き換えるか」をオートスケールの枠内で表現しづらいです。
KarpenterはNodeClass(EC2NodeClass)でAMIをpinし、更新すると drift(ズレ)として扱われ、置換が進みます。disruption.budgets と組み合わせることで「平日日中は置換しない」「夜間に少しずつ進める」をポリシーとして表現できます。
まとめ
運用対象が増える・スケール速度
CAはノードグループ(ASG/MNG)単位で増減するので、要件が増えると ノードグループが増えがちです。
KarpenterはNodePool/NodeClassで制約を集約しやすい。
またスケール速度はkarpenterのほうが速く、karpeneterはReservedインスタンスが使える
更新(AMI/世代更新)を“オートスケールの枠内”で扱えるか
CAはスケールは担当しますが、AMI更新はASG/LT更新・ローリング等を運用が握りがち。
KarpenterはNodeClassの変更が drift として扱われ、置換が進む(=更新がオートスケールの延長線に乗る)という思想です。
推奨アーキテクチャ
前提はこうです。
- EKSは セルフマネージドASG運用
- ワークロードは 常駐多め
- 重要系/ステートフルは 止めないと縮退できない
- Karpenterコントローラ(controller + webhook)はFargate上で稼働
この条件なら、最初におすすめなのはこの分割です。
- 固定ASG(既存運用を維持):system / critical / stateful(止めないと縮退できない領域)
- Karpenter:一般的なstateless常駐 + たまにバッチ
KarpenterコントローラをFargateに載せている場合、「Karpenterを動かすための小さなノードグループ(ASG/MNG)」は必須ではありません。EKS公式でも、通常は小さなノードグループを推奨しつつ、代替として karpenter namespaceをFargate profile対象にしてFargateで動かせると説明されています。
Karpenter on Fargate の前提条件(最小)
Fargate profile の作り方(namespaceを対象にする)
Fargate profile は selector で「どのPodをFargateに載せるか」を決めます。selectorは namespace必須で、namespaceだけ指定すると そのnamespaceのPodは全部Fargate対象になります。
eksctlの最小例:
kubectl create namespace karpentereksctl create fargateprofile \
--cluster <CLUSTER_NAME> \
--name karpenter \
--namespace karpenter
実装例
プレースホルダ(<CLUSTER_NAME>など)は環境に合わせて置換してください。
固定ASG側:ラベル/taintをノード参加時に付ける
重要系・止めないと縮退できない系を 固定ASGへ隔離するため、ノード参加時にラベル/taintを付与します。
ASGのUserData例(/etc/eks/bootstrap.sh)
#!/bin/bash -xe
/etc/eks/bootstrap.sh <CLUSTER_NAME> \
--kubelet-extra-args "\
--node-labels=nodepool=static-critical,workload-tier=critical \
--register-with-taints=dedicated=critical:NoSchedule"
nodepool=static-critical:固定ASG領域だと識別するラベルdedicated=critical:NoSchedule:重要系以外が誤って載らないためのtaint
Karpenter側:EC2NodeClass(AMI pin推奨)+ NodePool 2つ
EC2NodeClass
本番はAMIを @latest にせず pin するのが推奨です。EKSベストプラクティスでも本番クラスタではAMI pinを強く推奨しています。
またEKS公式は「v1 APIではカスタムLaunch Templateをサポートしないため、必要なカスタムはEC2NodeClass側(AMI/user dataなど)で扱う」前提を明記しています。
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
role: "KarpenterNodeRole-<cluster-name>"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "<cluster-name>"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "<cluster-name>"
amiSelectorTerms:
- alias: al2023@v20240807 # 例:本番は pin 推奨
karpenter.sh/discovery タグは、Karpenterがクラスタ関連リソースを見つける定番パターンです(環境によってタグ方針は調整してください)。
NodePool:縮退スケジュールを決める
常駐中心は「勝手に集約される」ことがストレスになりやすいので、最初は WhenEmpty で始めるのが無難です。(WhenEmptyはすべてのpodがノードから退避したらノードを削除する)
また disruption.budgets で「平日日中は縮退0」などの制御ができます。
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: resident-ondemand
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: kubernetes.io/os
operator: In
values: ["linux"]
limits:
cpu: "2000"
memory: 2000Gi
disruption:
consolidationPolicy: WhenEmpty
budgets:
- nodes: "5%"
- schedule: "0 9 * * mon-fri"
duration: 8h
nodes: "0"
ポイント:
limits:暴走防止(上限に当たると新規プロビジョニングが止まる)budgets:縮退スピード制御(平日日中は0など)
NodePool:taintで住み分ける
バッチが “たまに” なら、Spot用のNodePoolを用意して、バッチだけ toleration で寄せると運用が綺麗になります。
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: batch-spot
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
taints:
- key: workload
value: batch
effect: NoSchedule
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
limits:
cpu: "500"
memory: 500Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
budgets:
- nodes: "20%"
ワークロード側:固定ASGに寄せる / Spotに寄せる
critical/stateful:固定ASGへ寄せる(nodeAffinity + toleration)
固定ASGに付けた nodepool=static-critical と、dedicated=critical taint を使います。
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nodepool
operator: In
values: ["static-critical"]
tolerations:
- key: "dedicated"
operator: "Equal"
value: "critical"
effect: "NoSchedule"
バッチ:Spot NodePoolへ寄せる(toleration)
tolerations:
- key: workload
operator: Equal
value: batch
effect: NoSchedule
AMI更新運用:セルフマネージドの“重さ”をKarpenter置換に寄せる
セルフマネASGだとAMI更新は「LT更新→ローリング」で、手順も影響範囲も読みにくくなりがちです。
KarpenterはAMIのpin更新がdrift(ズレ)として扱われ、budgets が許す範囲で置換が進む(または抑制される)設計です。
推奨:段階的AMI更新(実務の手順)
- 検証環境で先にpinを更新
- EC2NodeClassの
amiSelectorTermsを新pinへ
(例:al2023@v20240807→al2023@v20241001)
- EC2NodeClassの
- 本番は先にbudgetsを“守り”にする
- 平日日中は
nodes: "0" - 夜間のみ
nodes: "5%"など
- 平日日中は
- 本番EC2NodeClassのpinを更新
- いきなり置換が怖い場合は、先に スケールアップで新ノードを混ぜて、夜間に置換を進める
運用トラブルシューティング
「Pendingなのに増えない」(スケールアップ不発)
まずは Pendingの理由を見ます。
kubectl get pods -A --field-selector=status.phase=Pending
kubectl describe pod -n <ns> <pod>
kubectl get events -A --sort-by='.lastTimestamp' | tail -n 80
よくある原因:
- Pod側:nodeSelector / affinity / topology / tolerations
- NodePool側:requirements / taints / limits(上限到達)
- クラウド側:Spot枯渇、Subnet IP枯渇、クォータ
次に NodePool/NodeClass を確認:
kubectl get nodepools
kubectl describe nodepool resident-ondemand
kubectl get ec2nodeclasses
kubectl describe ec2nodeclass default
特に limits は「上限超えで新規作成が止まる」ため、原因になりやすいです。
(Fargate前提の落とし穴)
Fargate profile のselectorにマッチしないPodを「Fargateに載る前提」で出してしまうと、PodがPendingのままになることがあります。selectorはnamespace必須で、namespaceのみ指定するとそのnamespace内Podは全部Fargate対象になる点を先に確認します。
「ノードが減らない」(縮退が進まない)
まず意図せず縮退を止めていないか確認(平日日中0など)。
kubectl get nodepool resident-ondemand -o yaml | sed -n '/disruption:/,/limits:/p'
次に “退避(drain)をブロックするもの” を疑います(CAでも同じ):
- PDBが厳しすぎる / レプリカが足りない
- DaemonSetが多い
- ローカルストレージ依存が強い
「更新で揺れた / 怖い」
- 本番は
@latestを避けて pin(検証→本番の段階適用) - 置換速度は
budgetsで抑える(特に常駐中心は“夜間のみ”が効く)
