EKSを運用していると、必ずここで悩みます。
ノードを多めに用意すれば安心ですが、使っていない時間の料金が積み上がります。
かといって減らすと、Podがスケジュールできずに詰まります。
この「多すぎ」と「足りない」の間を自動で埋めるのが Karpenter です。
AWSが公開しているオープンソースのオートスケーラで、必要になった瞬間に必要な分だけEC2を起動し、要らなくなったら削除します。
この記事は、すでに稼働しているEKSクラスタに後から Karpenter を追加した際の作業メモです。
■ 導入すると何が変わるか
やることは2つだけです。
・Podがスケジュールできない状態になったら、ノードを新しく起動する
・そのノードが空になったら、自動で削除する
シンプルですが、この2つが自動で回るかどうかで、月末の請求額はかなり変わります。
■ 事前に用意するもの
以下の4つを先に入れておきます。どれか欠けると途中で止まります。
・AWS CLI … AWSの操作全般
・kubectl … Kubernetesの操作
・eksctl … EKS専用のCLI
・helm … Kubernetesのパッケージ管理
Karpenter は自分でEC2を起動するため、AWS側の権限が必要になります。
その権限の渡し方が IRSA、正式には IAM Roles for Service Accounts という仕組みです。
ざっくり言うと、KubernetesのサービスアカウントにIAMロールを紐づけて、アクセスキーを書かずに権限を渡す方式です。
認証情報をファイルに直書きしなくて済むので、これを使います。
■ 手順1 環境変数を先に用意する
以降のコマンドで何度も使う値を、先に変数化しておきます。
設定する主な値は次のとおりです。
・クラスタ名
・Karpenter を入れる名前空間(通常は kube-system)
・Karpenter のバージョン
・AWSのリージョン
・AWSアカウントID
・EKSのバージョン
・クラスタのOIDCエンドポイント
・使用するAMIのエイリアスバージョン
ここで一つ注意点があります。
ネット上の手順書では、Karpenter のバージョンを変数として使っているのに、その変数を設定する行が抜けているものがあります。
その状態で先に進むと、後のダウンロード処理でパスが壊れ、ファイルが取得できずに止まります。
バージョンは必ず明示的に設定してから進めてください。
■ 手順2 Karpenter が使うAWSリソースを作成する
IAMロール、ポリシー、SQSキュー、EventBridgeのルール。
このあたりを Karpenter 公式が配布している CloudFormation テンプレートでまとめて作成します。
テンプレートは Karpenter のリポジトリ内、getting-started 配下に cloudformation.yaml という名前で置かれています。
これを取得し、クラスタ名をパラメータとして渡してデプロイします。
IAMリソースを作るため、CAPABILITY_NAMED_IAM の指定が必要です。
ここで SQSキューが必要になる理由を書いておきます。
スポットインスタンスの中断通知を受け取るためです。
AWS側から「このインスタンスを2分後に停止します」という通知が飛んでくるので、それを受けて先にPodを他へ退避させます。
この仕組みがあるおかげで、スポットインスタンスを実運用で使えます。
続いて、クラスタにOIDCプロバイダを関連付けます。
eksctl の utils associate-iam-oidc-provider コマンドで実行できます。
これをやらないと IRSA が機能しません。
■ 手順3 サブネットとセキュリティグループにタグを付ける
ここが地味ですが最重要です。
Karpenter は「どのサブネットにノードを起動してよいか」をタグで判断します。
設定ファイルに書くのではなく、AWS側のタグを見に行く方式です。
つまりタグが無ければ、Karpenter は何もできずに黙り込みます。
付けるタグは次の形です。
キー : karpenter.sh/discovery
値 : クラスタ名
既存のノードグループが使用しているサブネットに対して、まとめて付与します。
ノードグループ一覧を取得し、それぞれのサブネットIDを引き出して create-tags を実行する流れになります。
セキュリティグループも同じ考え方です。
クラスタのセキュリティグループIDを取得し、同じタグを付けます。
補足として書いておきます。
「ノードが1台も起動しない」というトラブルの原因は、経験上ほとんどがこのタグ漏れです。
動かないときは、まずタグを確認してください。
■ 手順4 起動したノードがクラスタに参加できるようにする
Karpenter が起動したEC2は、そのままではクラスタのメンバーとして認められません。
「このIAMロールはノードとして扱ってよい」と、EKS側に登録する必要があります。
方法は2つあります。
【従来の方法】aws-auth ConfigMap
kube-system 名前空間の aws-auth という ConfigMap を編集し、mapRoles に Karpenter 用のノードロールを追記します。
username には EC2のプライベートDNS名を展開する変数を指定し、groups には system:bootstrappers と system:nodes を指定します。
【現在推奨される方法】EKS Access Entry
aws-auth ConfigMap は非推奨の方向に動いています。
AWSは代わりに EKS Access Entry を推奨しており、こちらは create-access-entry コマンド一つで完了します。
タイプには EC2_LINUX を指定します。
既存クラスタが ConfigMap 運用であれば、無理に変更しなくても動作します。
ただし、これから新規に組むのであれば Access Entry を選んでおいた方が後々の運用が楽になります。
■ 手順5 Helm でマニフェストを生成する
Helm で直接インストールせず、いったんYAMLファイルに書き出します。
この後で1箇所だけ手を入れる必要があるためです。
helm template コマンドに、バージョン、名前空間、クラスタ名、中断通知キュー名、そして Karpenter コントローラ用IAMロールのARNを渡します。
コントローラのCPUとメモリのリクエストおよびリミットも、ここで指定しておきます。
生成されたファイルの affinity セクションを書き換え、Karpenter 自身が既存のノードグループ上で動くように固定します。
nodeAffinity の requiredDuringSchedulingIgnoredDuringExecution で、既存ノードグループのラベルを指定する形です。
なぜ固定するのか。
Karpenter 自身が、Karpenter の作ったノードの上で動いていると、そのノードが縮退の対象になったときに自分ごと消えます。
消えてしまうと、誰もノードを立て直せません。
鍵を部屋の中に置いたままドアを閉めるのと同じ状態になります。
管理する側は、管理される側の外に置く。これが原則です。
■ 手順6 CRD を導入する
Karpenter は Kubernetes 標準に無い独自リソースを使うため、その定義であるCRDを先に入れます。
必要なものは3種類です。
NodePool
どんなノードを、どこまで、どういう条件で起動するかを定義します。EKSのノードグループに相当する役割です。
EC2NodeClass
IAMロール、サブネット、セキュリティグループ、AMI。AWS寄りの設定はこちらに書きます。
NodeClaim
Karpenter が内部で使う作業伝票のようなものです。
1台起動する必要があると判断したときに自分で作成し、不要になったら自分で削除します。人間が直接触るものではありません。
これら3つのCRDは、Karpenter リポジトリの pkg/apis/crds 配下に、それぞれ karpenter.sh_nodepools、karpenter.k8s.aws_ec2nodeclasses、karpenter.sh_nodeclaims という名前のYAMLとして置かれています。
バージョンを合わせて取得し、適用してください。
■ 手順7 Karpenter 本体をデプロイする
手順5で生成し、affinity を修正したYAMLを適用します。
これで Karpenter コントローラが起動します。
■ 手順8 NodePool と EC2NodeClass を作成する
ここでようやく「どんなノードを立てるか」を決めます。
NodePool の主な設定内容は次のとおりです。
・アーキテクチャ … amd64
・OS … linux
・キャパシティ種別 … spot
・インスタンス系統 … c系、m系、r系
・インスタンス世代 … 第3世代以降
・ノードの有効期限 … 720時間
・CPU上限 … 1000
・縮退ポリシー … 空、または使用率が低い状態になったら整理
・縮退までの待ち時間 … 1分
EC2NodeClass 側では、ノード用IAMロール名、使用するAMIのエイリアス、そしてサブネットとセキュリティグループの選択条件を指定します。
選択条件には、手順3で付けた karpenter.sh/discovery タグを使います。
読み方のポイントを4つだけ挙げます。
インスタンスタイプを指定していない
「c系、m系、r系の第3世代以降」という条件だけを渡し、あとは Karpenter に選ばせます。
これが従来のノードグループとの最大の違いです。
キャパシティ種別を spot のみにしている
料金は下がりますが、中断されます。
本番で使うのであれば、spot と on-demand の両方を指定し、スポットが確保できないときはオンデマンドに逃げる形が現実的です。
CPU上限は必ず入れる
設定ミスやPodの暴走で、ノードが延々と増え続ける事故を防ぐための保険です。
これが無いと、事故が起きたときに青天井で課金されます。省略しないでください。
縮退までの待ち時間
短くすればコストは下がりますが、ノードの出入りが激しくなります。
1分はあくまで検証用の値です。
■ 動作確認
まず、Karpenter コントローラのログを流しておきます。
kubectl logs にコントローラのラベルを指定して追跡できます。
【スケールアウトの確認】
CPU 500m を要求するPodを10個投入します。既存ノードでは足りない量です。
nginx イメージで replicas を10に設定した Deployment を用意し、適用します。
うまくいけば、ログに launched nodeclaim という行が出て、選ばれたインスタンスタイプとアベイラビリティゾーンが表示されます。
おおよそ5コア必要な状況に対して8コア級のインスタンスが1台、といった形で、必要量から逆算して1台にまとめて詰め込む動きをします。
【スケールインの確認】
先ほどの Deployment を削除します。
Podが消え、設定した縮退待ち時間が経過すると、ノードが削除されます。
■ 導入前に知っておくべき注意点
正直に書いておきます。
スポットインスタンスは落ちます
料金は下がりますが、AWS側の都合で停止します。
ステートフルなワークロードや、途中で切れると困るバッチ処理には向きません。
使うのであれば PodDisruptionBudget とセットで設計してください。
CPU上限を入れ忘れると危険です
設定ミスやPodの暴走で、ノードが増え続けます。
上限設定は保険ではなく必須項目と考えてください。
縮退待ち時間を短くしすぎない
1分は検証向けの値です。
本番でこの値にすると、ノードの生成と削除を繰り返し、Podが落ち着かなくなることがあります。
5分から10分程度で始めて、様子を見ながら調整するのが無難です。
バージョンは固定する
常に最新版を取得する書き方にすると、ある日CRDの仕様変更で動かなくなります。
バージョンは明示し、上げるときは意図的に上げてください。
既存のノードグループは残す
すべてを Karpenter に寄せたくなりますが、Karpenter 自身を載せる場所が必要です。
小さめのノードグループを1つ残しておくのが安全です。
■ まとめ
Karpenter を導入すると、ノードグループを事前に設計するという作業自体が不要になります。
Podの要求から逆算して、その時点でもっとも都合のよいインスタンスを選んで起動し、使い終わったら削除する。
事前設計をやめて、実行時の判断に任せる。考え方としてはそれだけです。
ただし、任せる範囲を決めるのは人間の仕事です。
CPU上限、縮退待ち時間、キャパシティ種別。
この3つの設定が、そのままコストと安定性のトレードオフになります。
まずは検証用クラスタで、この3つを動かして挙動を確認することをおすすめします。