@shinyaz

EKS マネージドノードグループの Warm Pool を検証 — poolState 別スケールアウト時間と reuseOnScaleIn の実態

目次

はじめに

2026年4月8日、AWS は EKS マネージドノードグループが EC2 Auto Scaling ウォームプールをサポート したことを発表した。ウォームプールは、OS 初期化・ユーザーデータ実行・ソフトウェア設定が完了済みの EC2 インスタンスをプールとして保持し、スケールアウト時にコールドスタートなしでクラスターに参加させる機能である。

ウォームプールの効果は poolState の選択で大きく変わる。Running は最速だがインスタンス課金が継続し、Stopped はコスト効率が良いが起動に時間がかかる。この「速度 vs コスト」のトレードオフを実測データで定量化し、ワークロード特性に応じた poolState 選択の判断基準を提示する。

本記事では、3パターンのスケールアウト時間を実測し、poolState ごとの速度とコストのデータを提供する。読者はこのデータをもとに「自分のワークロードにウォームプールを導入すべきか」「どの poolState を選ぶべきか」を判断できる。公式ドキュメントは EKS マネージドノードグループのウォームプール を参照。

ウォームプールの仕組み

ウォームプールを有効にすると、EKS はノードグループの Auto Scaling グループにウォームプールを作成する。インスタンスはウォームプールで以下の3つの状態のいずれかで待機する。

poolState状態維持コスト(t3.medium/月)スケールアウト時の動作
Running起動中〜$32(インスタンス + EBS)そのままクラスターに参加
Stopped停止中〜$2(EBS のみ)起動 → クラスターに参加
Hibernated休止中〜$2(EBS + RAM)RAM 復元 → クラスターに参加

※ コスト概算は t3.medium(gp3 20GB)の ap-northeast-1 価格に基づく。インスタンスタイプやディスクサイズにより異なる。

前提条件:

  • AWS CLI v2.34+ 設定済み(eks:*, ec2:*, iam:*, autoscaling:* の権限)
  • kubectl v1.35+
  • 検証リージョン: ap-northeast-1(東京)

セットアップだけ見たい場合は検証環境のセットアップ、結果だけ見たい場合は検証 1に進んでほしい。

検証環境のセットアップ

EKS クラスター・ノードロール・ノードグループの作成手順

既存の EKS クラスター(v1.35)を使用した。Auto Mode で作成されたクラスターの場合、マネージドノードグループには vpc-cnikube-proxycoredns アドオンが必要である。

Terminal (アドオンのインストール)
REGION=ap-northeast-1
CLUSTER=eks-sandbox
 
aws eks create-addon --cluster-name $CLUSTER --addon-name vpc-cni --region $REGION
aws eks create-addon --cluster-name $CLUSTER --addon-name kube-proxy --region $REGION
aws eks create-addon --cluster-name $CLUSTER --addon-name coredns --region $REGION

ノードロールを作成し、必要なポリシーをアタッチする。

Terminal (ノードロール作成)
cat > node-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF
 
aws iam create-role \
  --role-name eks-warmpool-verify-node-role \
  --assume-role-policy-document file://node-trust-policy.json
 
aws iam attach-role-policy --role-name eks-warmpool-verify-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
aws iam attach-role-policy --role-name eks-warmpool-verify-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
aws iam attach-role-policy --role-name eks-warmpool-verify-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly

3つのノードグループを作成する。コールドスタート用(ウォームプールなし)、Stopped ウォームプール用、Running ウォームプール用。

Terminal (変数の準備)
# アカウント ID を取得
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
NODE_ROLE="arn:aws:iam::${ACCOUNT_ID}:role/eks-warmpool-verify-node-role"
 
# クラスターのプライベートサブネットを取得
SUBNETS=$(aws eks describe-cluster --name $CLUSTER --region $REGION \
  --query 'cluster.resourcesVpcConfig.subnetIds' --output text)
Terminal (ノードグループ作成)
 
# 1. コールドスタート(ウォームプールなし)
aws eks create-nodegroup \
  --cluster-name $CLUSTER --nodegroup-name ng-cold-start \
  --node-role $NODE_ROLE --subnets $SUBNETS \
  --instance-types t3.medium \
  --scaling-config minSize=2,maxSize=5,desiredSize=2 \
  --region $REGION
 
# 2. Stopped ウォームプール
aws eks create-nodegroup \
  --cluster-name $CLUSTER --nodegroup-name ng-warm-stopped \
  --node-role $NODE_ROLE --subnets $SUBNETS \
  --instance-types t3.medium \
  --scaling-config minSize=2,maxSize=5,desiredSize=2 \
  --warm-pool-config enabled=true,maxGroupPreparedCapacity=4,minSize=1,poolState=Stopped,reuseOnScaleIn=true \
  --region $REGION
 
# 3. Running ウォームプール
aws eks create-nodegroup \
  --cluster-name $CLUSTER --nodegroup-name ng-warm-running \
  --node-role $NODE_ROLE --subnets $SUBNETS \
  --instance-types t3.medium \
  --scaling-config minSize=2,maxSize=5,desiredSize=2 \
  --warm-pool-config enabled=true,maxGroupPreparedCapacity=4,minSize=1,poolState=Running,reuseOnScaleIn=false \
  --region $REGION

ウォームプールの設定パラメータ:

  • enabled=true — ウォームプールを有効化
  • maxGroupPreparedCapacity=4 — ウォームプール + ASG の合計インスタンス上限。desiredSize=2 の場合、ウォームプールには 4-2=2 台が配置される
  • minSize=1 — ウォームプールの最小インスタンス数
  • poolState=Stopped|Running — ウォームプール内のインスタンス状態
  • reuseOnScaleIn=true — スケールイン時にインスタンスをウォームプールに返却

ノードグループが ACTIVE になったら、ウォームプールのインスタンスが目的の状態(Stopped / Running)に遷移するまで待つ。

Terminal (ウォームプール状態確認)
# ASG 名を取得
ASG=$(aws eks describe-nodegroup --cluster-name $CLUSTER --nodegroup-name ng-warm-stopped \
  --region $REGION --query 'nodegroup.resources.autoScalingGroups[0].name' --output text)
 
# ウォームプールのインスタンス状態を確認
aws autoscaling describe-warm-pool --auto-scaling-group-name $ASG --region $REGION \
  --query 'Instances[].{InstanceId:InstanceId,State:LifecycleState}' --output table
Output
-------------------------------------------
|            DescribeWarmPool             |
+----------------------+------------------+
|      InstanceId      |      State       |
+----------------------+------------------+
|  i-0a546e64a73986140 |  Warmed:Stopped  |
|  i-0c0c0f4ad0652338a |  Warmed:Stopped  |
+----------------------+------------------+

検証 1: poolState 別のスケールアウト時間計測

3パターンのスケールアウト時間を計測する。各パターンで desiredSize を 2→3 に変更し、新ノードが kubectl get nodes で Ready になるまでの経過時間を記録した。

以下のポーリングスクリプトで計測を行った。NODEGROUP を各ノードグループ名に置き換えて使用する。

計測用ポーリングスクリプト
Terminal (計測スクリプト)
NODEGROUP="ng-cold-start"  # ng-warm-stopped / ng-warm-running に置き換え
START_TIME=$(date +%s)
 
while true; do
  READY=$(kubectl get nodes --selector="eks.amazonaws.com/nodegroup=$NODEGROUP" \
    --no-headers 2>/dev/null | grep " Ready " | wc -l)
  TOTAL=$(kubectl get nodes --selector="eks.amazonaws.com/nodegroup=$NODEGROUP" \
    --no-headers 2>/dev/null | wc -l)
  ELAPSED=$(( $(date +%s) - START_TIME ))
  echo "$(date +%T) [${ELAPSED}s] Nodes: $TOTAL total, $READY ready"
  [ "$READY" -ge 3 ] && echo "=== New node Ready! ===" && break
  sleep 5
done

(a) コールドスタート(ウォームプールなし)

Terminal
aws eks update-nodegroup-config \
  --cluster-name eks-sandbox --nodegroup-name ng-cold-start \
  --scaling-config minSize=2,maxSize=5,desiredSize=3 \
  --region ap-northeast-1
Output (5秒間隔でポーリング)
17:30:27 [3s]  Nodes: 2 total, 2 ready
17:30:40 [16s] Nodes: 2 total, 2 ready
17:30:54 [30s] Nodes: 2 total, 2 ready
17:31:06 [42s] Nodes: 3 total, 2 ready  ← ノード出現
17:31:19 [55s] Nodes: 3 total, 2 ready
17:31:26 [62s] Nodes: 3 total, 3 ready  ← Ready

結果: 62秒。ログの内訳を見ると、42秒でノードが出現(EC2 インスタンスの起動完了 + kubelet 起動開始)し、そこから20秒で Ready になった(CNI 設定、ノード登録完了)。

(b) Stopped ウォームプール

Terminal
aws eks update-nodegroup-config \
  --cluster-name eks-sandbox --nodegroup-name ng-warm-stopped \
  --scaling-config minSize=2,maxSize=5,desiredSize=3 \
  --region ap-northeast-1
Output
17:31:39 [2s]  Nodes: 2 total, 2 ready
17:31:59 [22s] Nodes: 2 total, 2 ready
17:32:05 [28s] Nodes: 3 total, 2 ready  ← ノード出現
17:32:18 [41s] Nodes: 3 total, 2 ready
17:32:25 [48s] Nodes: 3 total, 3 ready  ← Ready

結果: 48秒。コールドスタートより14秒(23%)短縮。ウォームプールのインスタンスは OS 初期化済みの状態で停止しているため、コールドスタートのようなフル起動シーケンスが不要になり、その分が短縮に寄与していると考えられる。

(c) Running ウォームプール

Terminal
aws eks update-nodegroup-config \
  --cluster-name eks-sandbox --nodegroup-name ng-warm-running \
  --scaling-config minSize=2,maxSize=5,desiredSize=3 \
  --region ap-northeast-1
Output
17:32:38 [2s]  Nodes: 2 total, 2 ready
17:32:51 [15s] Nodes: 3 total, 2 ready  ← ノード出現
17:33:04 [28s] Nodes: 3 total, 3 ready  ← Ready

結果: 28秒。コールドスタートより34秒(55%)短縮。Running インスタンスは既に起動済みのため、EC2 起動と OS 初期化のステップをスキップできる。残りの28秒は主に EKS ブートストラップ(クラスター参加 + CNI 設定)に費やされていると考えられる。

検証 2: reuseOnScaleIn の動作と再スケールアウト速度

検証 1 で Stopped ウォームプールのベースライン時間(48秒)を計測した。検証 2 では reuseOnScaleIn=true の環境でスケールイン→再スケールアウトのサイクルを確認し、再利用インスタンスの速度を比較する。

検証 1 で ng-warm-stopped の desiredSize は 3 になっている。ここから 3→2 にスケールインする。

スケールイン: インスタンスのウォームプール返却

desiredSize を 3→2 に変更してスケールインを実行した。

Terminal
aws eks update-nodegroup-config \
  --cluster-name eks-sandbox --nodegroup-name ng-warm-stopped \
  --scaling-config minSize=2,maxSize=5,desiredSize=2 \
  --region ap-northeast-1

スケールインの過程を観察すると、以下のステップで進行した。ウォームプールの状態遷移は以下のコマンドで確認できる。

Terminal (ウォームプール状態の観察)
# ASG 名を取得(セットアップ時に取得済みの場合は省略)
ASG=$(aws eks describe-nodegroup --cluster-name eks-sandbox --nodegroup-name ng-warm-stopped \
  --region ap-northeast-1 --query 'nodegroup.resources.autoScalingGroups[0].name' --output text)
 
# ノード状態とウォームプール状態を定期的に確認
while true; do
  echo "--- $(date +%T) ---"
  kubectl get nodes --selector='eks.amazonaws.com/nodegroup=ng-warm-stopped' --no-headers
  aws autoscaling describe-warm-pool --auto-scaling-group-name $ASG --region ap-northeast-1 \
    --query 'Instances[].{Id:InstanceId,State:LifecycleState}' --output table
  sleep 15
done
Output (スケールイン過程)
17:33:43 ノード: Ready,SchedulingDisabled  ← ドレイン開始
17:34:35 ノード: NotReady,SchedulingDisabled  ← kubelet 停止
17:49:09 ウォームプール: Warmed:Pending:Proceed  ← ライフサイクルフック完了
17:49:40 ウォームプール: Warmed:Stopped  ← 返却完了

スケールイン開始から返却完了まで約16分。EKS は ASG に Terminate-LC-Hook(ハートビートタイムアウト 1800秒 = 30分)を自動設定しており、ノードのドレイン自体は約1分で完了したにもかかわらず、ライフサイクルフックの処理完了まで待機が発生した。なお、ドキュメントでは「EKS API 経由でウォームプールを設定し、EC2 Auto Scaling API で直接変更しないこと」と記載されているため、このタイムアウト値をユーザーが調整することは推奨されていない。

Terminal (ライフサイクルフック確認)
aws autoscaling describe-lifecycle-hooks \
  --auto-scaling-group-name $ASG --region ap-northeast-1
Output
{
  "LifecycleHooks": [
    {
      "LifecycleHookName": "Launch-LC-Hook",
      "LifecycleTransition": "autoscaling:EC2_INSTANCE_LAUNCHING",
      "HeartbeatTimeout": 1800
    },
    {
      "LifecycleHookName": "Terminate-LC-Hook",
      "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING",
      "HeartbeatTimeout": 1800
    }
  ]
}

ASG のアクティビティログからも、インスタンスがウォームプールに返却されたことを確認できる。

Output (ASG アクティビティ)
Status: MidTerminatingLifecycleAction
Description: Scaling in EC2 instance into warm pool: i-0f44a906c76cdfe62

再スケールアウト: 再利用インスタンスの速度

ウォームプールに返却されたインスタンスを使って再スケールアウトした。

Terminal
aws eks update-nodegroup-config \
  --cluster-name eks-sandbox --nodegroup-name ng-warm-stopped \
  --scaling-config minSize=2,maxSize=5,desiredSize=3 \
  --region ap-northeast-1
Output
17:49:55 [2s]  Nodes: 2 total, 2 ready
17:50:22 [29s] Nodes: 2 total, 2 ready
17:50:28 [35s] Nodes: 3 total, 3 ready  ← Ready

結果: 35秒。初回の Stopped ウォームプール(48秒)より13秒速い。初回はインスタンス起動後に kubelet がクラスターに初めて参加するため、TLS ブートストラップ(証明書発行)やノードオブジェクトの新規登録が必要になる。再利用インスタンスではこれらがキャッシュされており、クラスター参加が高速化されたと考えられる。

検証 3: Pending Pod 起因のスケールアウト — Pod Running までの時間

検証 1-2 は手動で desiredSize を変更した。検証 3 では、実運用に近いシナリオとして Pending Pod が存在する状態でスケールアウトし、Pod が Running になるまでの時間を計測する。

検証 2 の再スケールアウトで ng-warm-stopped の desiredSize は 3 になっている。まず 2 に戻す。

Terminal (desiredSize を 2 に戻す)
aws eks update-nodegroup-config \
  --cluster-name eks-sandbox --nodegroup-name ng-warm-stopped \
  --scaling-config minSize=2,maxSize=5,desiredSize=2 \
  --region ap-northeast-1
# ウォームプールのインスタンスが Warmed:Stopped に戻るまで待つ(約16分)

desiredSize が 2 に戻り、ウォームプールに Stopped インスタンスが2つある状態で検証を開始する。

まず、既存の2ノードをリソース要求の大きい Pod で埋め、3つ目の Pod を Pending にした。

Pod マニフェスト(filler-pod × 2 + pending-pod × 1)
Terminal (Pod 作成)
# 既存ノードを埋める Pod × 2 + Pending になる Pod × 1
kubectl apply -f - << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: filler-pod-1
spec:
  nodeSelector:
    eks.amazonaws.com/nodegroup: ng-warm-stopped
  containers:
    - name: nginx
      image: nginx:alpine
      resources:
        requests:
          cpu: "1500m"
          memory: "2Gi"
---
apiVersion: v1
kind: Pod
metadata:
  name: filler-pod-2
spec:
  nodeSelector:
    eks.amazonaws.com/nodegroup: ng-warm-stopped
  containers:
    - name: nginx
      image: nginx:alpine
      resources:
        requests:
          cpu: "1500m"
          memory: "2Gi"
---
apiVersion: v1
kind: Pod
metadata:
  name: pending-pod
spec:
  nodeSelector:
    eks.amazonaws.com/nodegroup: ng-warm-stopped
  containers:
    - name: nginx
      image: nginx:alpine
      resources:
        requests:
          cpu: "1500m"
          memory: "2Gi"
EOF

t3.medium は 2 vCPU / 4 GiB で allocatable は約 1930m CPU だが、各ノードにはシステム Pod が動作しているため、1500m CPU を要求する Pod は1ノードに1つしか配置できない。結果として filler-pod-1 のみが Running になり、filler-pod-2 と pending-pod は Pending になった。

Output (Pod 状態)
NAME           READY   STATUS    AGE
filler-pod-1   1/1     Running   6s
filler-pod-2   0/1     Pending   6s
pending-pod    0/1     Pending   6s

Pending Pod が2つあるため、2ノードの追加が必要。Stopped ウォームプールから desiredSize を 2→4 に変更してスケールアウトを実行した。

Terminal
aws eks update-nodegroup-config \
  --cluster-name eks-sandbox --nodegroup-name ng-warm-stopped \
  --scaling-config minSize=2,maxSize=5,desiredSize=4 \
  --region ap-northeast-1
Output
17:53:29 [2s]  Running: 1/3
17:53:49 [22s] Running: 1/3
17:53:55 [28s] filler-pod-2: ContainerCreating  ← ノード Ready、Pod スケジュール
17:54:21 [54s] Running: 2/3
17:54:28 [61s] pending-pod: Running
17:54:34 [67s] Running: 3/3  ← 全 Pod Running

結果: 67秒(全 Pod Running まで)。内訳はノード Ready まで約48秒 + Pod スケジューリング + イメージプルに約19秒。Cluster Autoscaler を使う場合は、Pending Pod 検知→ASG desiredSize 変更のオーバーヘッドがさらに加わる(本記事では未計測)。

総合比較: poolState × スケーリング方式の組み合わせ

パターンノード Ready までの時間維持コスト概算(1インスタンス/月)備考
コールドスタート(ウォームプールなし)62秒$0ベースライン
Stopped ウォームプール(初回)48秒(-23%)〜$2(EBS のみ)OS 再起動のみ
Running ウォームプール(初回)28秒(-55%)〜$32(インスタンス + EBS)ブートストラップのみ
reuseOnScaleIn 再利用(Stopped)35秒(-44%)〜$2(EBS のみ)再利用による高速化(推測: ブートストラップのキャッシュ)
Stopped + Pod Running まで67秒〜$2(EBS のみ)イメージプル含む

poolState 選択の判断基準

計測結果から、以下の判断フレームワークを提示する。

  • Running を選ぶべきケース — スケールアウトの速度が最優先で、30秒以内にノードを追加したい場合。インスタンス課金が継続するため、ウォームプールのサイズは最小限に抑える。バースト頻度が高く、1回あたりの遅延コストが大きいワークロード向け
  • Stopped を選ぶべきケース — コスト効率を重視し、50秒程度のスケールアウト時間を許容できる場合。EBS 課金のみなので Running の約 1/16 のコスト。バースト頻度が低〜中程度のワークロード向け
  • reuseOnScaleIn を有効にすべきケース — スケールイン/アウトが頻繁に発生する場合。再利用インスタンスは初回 Stopped より13秒速い。ただし、スケールイン→ウォームプール返却に約16分かかるため、短時間のバーストが連続する場合は効果が薄い

ウォームプールを導入しないほうが良いケース

  • t3.medium のようなブート時間が短いインスタンスタイプでは、コールドスタートとの差が14-34秒にとどまる。ユーザーデータで初期化が重いワークロード(大量のパッケージインストールやデータダウンロード)ほど効果が大きい
  • ウォームプールはカスタム AMI をサポートしない(EKS 最適化 AMI のみ)。カスタム AMI を使用している場合は利用できない。初期化処理はユーザーデータ(launch template)で行う必要がある
  • Bottlerocket AMI では Hibernated 状態と reuseOnScaleIn が使えない

まとめ

  • Running は速いが高い — 28秒でノード追加できるが、インスタンス課金が継続する。速度要件が厳しいワークロードに限定して使うべき
  • Stopped はバランスが良い — 48秒で EBS 課金のみ。多くのワークロードではこれで十分
  • reuseOnScaleIn は16分のドレイン待ちがボトルネック — 再スケールアウトは35秒と速いが、ウォームプールへの返却に16分かかった。EKS が自動設定するライフサイクルフック(1800秒タイムアウト)が関与しており、ドキュメントではこの値の手動変更は推奨されていない
  • Pod Running までの時間はノード Ready + イメージプル — ノードが Ready になっても Pod が Running になるまで追加で約19秒かかる。イメージのプリプルやイメージサイズの最適化も合わせて検討すべき
クリーンアップ手順
Terminal (リソース削除)
REGION=ap-northeast-1
CLUSTER=eks-sandbox
 
# Pod 削除
kubectl delete pod filler-pod-1 filler-pod-2 pending-pod
 
# ノードグループ削除
for NG in ng-cold-start ng-warm-stopped ng-warm-running; do
  aws eks delete-nodegroup --cluster-name $CLUSTER --nodegroup-name $NG --region $REGION
done
 
# ノードグループ削除完了を待つ
for NG in ng-cold-start ng-warm-stopped ng-warm-running; do
  aws eks wait nodegroup-deleted --cluster-name $CLUSTER --nodegroup-name $NG --region $REGION
done
 
# アドオン削除(Auto Mode クラスターで追加した場合)
aws eks delete-addon --cluster-name $CLUSTER --addon-name vpc-cni --region $REGION
aws eks delete-addon --cluster-name $CLUSTER --addon-name kube-proxy --region $REGION
aws eks delete-addon --cluster-name $CLUSTER --addon-name coredns --region $REGION
 
# IAM ロール削除
aws iam detach-role-policy --role-name eks-warmpool-verify-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
aws iam detach-role-policy --role-name eks-warmpool-verify-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
aws iam detach-role-policy --role-name eks-warmpool-verify-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
aws iam delete-role-policy --role-name eks-warmpool-verify-node-role \
  --policy-name ClusterAutoscalerPolicy 2>/dev/null  # 追加した場合のみ
aws iam delete-role --role-name eks-warmpool-verify-node-role

共有する

田原 慎也

田原 慎也

ソリューションアーキテクト @ AWS

AWS ソリューションアーキテクトとして金融業界のお客様を中心に技術支援をしており、クラウドアーキテクチャや AI/ML に関する学びをこのサイトで発信しています。このサイトの内容は個人の見解であり、所属企業の公式な意見や見解を代表するものではありません。

関連記事