0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

EC2 2台の環境を EKS に移行するとき、コンテナ数はどう決めるのか【初学者向けサイジング入門】

0
Last updated at Posted at 2026-07-27

はじめに

「EC2 で動いていた仮想マシン 2 台を EKS に移行する。じゃあコンテナ(Pod)は何個にすればいいのか?」

この問いに対する答えは 「2 個ではない」 だ。

EC2 と Kubernetes では「台数」という数字の意味そのものが違う。ここを理解しないまま移行すると、リソースを盛りすぎて無駄なコストを払うか、逆に足りずに障害を起こすかのどちらかになる。

この記事では、Kubernetes をこれから触る人向けに、以下を順番に説明する。

  • EC2 の「台数」と Kubernetes の「Pod 数」が別物である理由
  • 実測値から Pod 数を算出する具体的な手順(計算例つき)
  • requests / limits という、すべての基準になる設定値
  • HPA / VPA / Karpenter という 3 つのオートスケーラーの役割分担
  • EKS でも EC2 は必要なのか、インスタンスタイプはどう選ぶのか
  • クラスター作成時にインスタンスタイプを聞かれるのか
  • 初学者がハマりやすい罠

第 0 章:まず用語を揃える

計算に入る前に、最低限の用語を整理する。ここが曖昧だと以降がすべて曖昧になる。

用語 読み ざっくり言うと EC2 で言うと
Node(ノード) ノード コンテナを動かすサーバ EC2 インスタンスそのもの
Pod(ポッド) ポッド コンテナを 1 個以上まとめた最小単位 サーバ上で動くアプリのプロセス群
Deployment デプロイメント 「この Pod を N 個動かして」という宣言 Auto Scaling Group に近い
ReplicaSet レプリカセット Deployment が裏で作る、Pod 数を維持する係 ASG の希望台数の維持機能
replicas(レプリカ数) レプリカ Pod を何個動かすかの数 インスタンス台数
requests リクエスト 「最低これだけ確保して」という予約値 (EC2 には対応概念なし)
limits リミット 「これ以上は使わせない」という上限値 (EC2 には対応概念なし)

重要なのは Node と Pod は階層が違う ということだ。

[ EKS クラスタ ]
   ├─ [ Node(EC2 インスタンス)#1 ]
   │      ├─ Pod A
   │      ├─ Pod A
   │      └─ Pod B
   └─ [ Node(EC2 インスタンス)#2 ]
          ├─ Pod A
          └─ Pod B

1 台の Node の上に複数の Pod が乗る。だから「EC2 2 台 = Pod 2 個」という対応にはならない。


第 1 章:なぜ「VM 2 台 → Pod 2 個」ではないのか

EC2 で 2 台構成にしていた理由を思い出してほしい。ほとんどの場合、理由は次の 2 つのどちらかだ。

理由A:冗長性のための 2 台

1 台が落ちてもサービスを止めないための構成。この場合、2 という数字は「必要な処理能力」ではなく「壊れてもいい余裕」を表している。実際の負荷は 1 台分にも満たないことが多い。

理由B:処理能力が足りないための 2 台

1 台では捌ききれないから増やした構成。この場合の 2 は処理能力の数字だが、それは 「そのインスタンスタイプ 1 台分」という粗い粒度 でしか表現されていない。

いずれにせよ、EC2 の台数は次の 2 つの要素が混ざった数字になっている。

EC2 の台数 = 必要な処理能力 ÷ インスタンス 1 台の能力 + 冗長分
                (細かく刻めない)              (HA 用)

Kubernetes ではこの 2 つを 分離して考える

Pod 数 = 必要な処理能力 ÷ Pod 1 個の能力     ← 能力の話
         ただし HA 下限(最低 2〜3)を下回らない  ← 冗長性の話

コンテナは EC2 より遥かに細かい粒度(CPU 0.1 個分など)で刻めるため、「必要な処理能力」をより正確に表現できる。これが移行によるコスト削減の主な源泉になる。

※ 逆に言うと、実測せずに移行してもコストは下がらない。EC2 のスペックをそのまま Pod の requests に書き写すと、無駄がそのまま引き継がれるだけになる。


第 2 章:サイジングの全体像

具体的な手順は次の 6 ステップだ。

Step 1  現行 EC2 の実測値を取る          ← 最重要
   ↓
Step 2  「スペック」ではなく「実消費」に変換する
   ↓
Step 3  Pod 1 個のサイズ(requests / limits)を決める
   ↓
Step 4  Pod 数を計算する
   ↓
Step 5  HA の下限をかぶせる
   ↓
Step 6  Pod を載せる Node 側をサイジングする

この 6 ステップで「初期値」が決まる。その後、次の 3 つを重ねていくと本番構成になる。

   ↓
Step 7  オートスケール(HPA / Karpenter)で変動に追従させる   → 第 9 章
   ↓
Step 8  Node のインスタンスタイプの範囲を宣言する              → 第 10〜12 章
   ↓
Step 9  負荷試験で実測し、初期値を修正する                     → 第 13 章

以降、この例を使って説明する。

想定環境
m5.large(2 vCPU / 8 GiB)× 2 台で Web API を運用中。ALB 配下。


第 3 章:Step 1 — 実測値を取る

サイジングの精度は、ここで取るデータの質でほぼ決まる。最低 2 週間分 のメトリクスを取得する。1 週間だと週次のピーク(月曜朝など)を取りこぼす可能性がある。

CPU 使用率を取る

aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 \
  --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --start-time 2026-07-13T00:00:00Z \
  --end-time 2026-07-27T00:00:00Z \
  --period 3600 \
  --statistics Average Maximum \
  --profile workload_a

メモリ使用率を取る

⚠️ メモリは EC2 の標準メトリクスに存在しない。 CloudWatch エージェントを入れていない場合、CloudWatch を見ても取得できない。

エージェントが入っている場合は CWAgent 名前空間を見る。

aws cloudwatch get-metric-statistics \
  --namespace CWAgent \
  --metric-name mem_used_percent \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --start-time 2026-07-13T00:00:00Z \
  --end-time 2026-07-27T00:00:00Z \
  --period 3600 \
  --statistics Average Maximum \
  --profile workload_a

入っていない場合の代替手段は次のとおり。

手段 内容
CloudWatch エージェント導入 2 週間待つ必要があるが、最も正確
sar / sysstat すでに導入済みなら過去データが残っている可能性がある
手動サンプリング free -m を cron で記録する。数日でも無いよりはるかにマシ

取るべき統計値

平均値だけを見るのは危険だ。次の 3 つをセットで取る。

統計値 何に使うか
Average(平均) 平常時の必要キャパの算出
P90 / P95(上位パーセンタイル) requests の基準値。外れ値に引きずられない
Maximum(最大) limits の基準値、および HPA の maxReplicas の算出

なぜ P95 を使うのか
Maximum は「デプロイ時に一瞬跳ねただけ」のような外れ値を拾ってしまう。その値で requests を設定すると、常時その分のリソースを予約し続けることになり無駄が大きい。逆に Average だけだと日常的なピークで性能が出ない。その中間として P90〜P95 が実務的な落としどころになる。


第 4 章:Step 2 — 実消費に変換する

ここが 2 つ目の関門だ。インスタンスのスペックではなく、実際に使っている量を基準にする。

実測結果が次のようになったとする。

項目 測定値
CPU 平均 30%
CPU P95 55%
CPU 最大 60%
メモリ平均 40%
メモリ P95 52%
メモリ最大 55%

これを実消費量に換算する。m5.large は 2 vCPU / 8 GiB、それが 2 台なので合計 4 vCPU / 16 GiB。

項目 計算 結果
スペック合計 2 vCPU × 2 台 4 vCPU / 16 GiB
CPU 平常時 4 × 30% 1.2 vCPU
CPU ピーク 4 × 60% 2.4 vCPU
メモリ平常時 16 × 40% 6.4 GiB
メモリピーク 16 × 55% 8.8 GiB

移行後に本当に必要なのは、4 vCPU / 16 GiB ではなく 2.4 vCPU / 8.8 GiB のほうだ。

ここでスペック合計(4 vCPU / 16 GiB)をそのまま移行先の見積もりに使うと、1.7〜2 倍のオーバープロビジョニング になる。EKS 移行でコストが下がらない典型的な失敗パターンがこれだ。


第 5 章:Step 3 — Pod 1 個のサイズを決める

requests と limits の役割の違い

この 2 つの違いが Kubernetes 最大のつまずきポイントなので、丁寧に説明する。

requests limits
意味 「最低これだけ確保して」という予約 「これ以上使わせない」という上限
誰が見るか スケジューラ(Pod の配置先を決める係) コンテナランタイム(実行時の制限係)
効く場面 Pod をどの Node に置くかの判断 実際にリソースを使いすぎたとき

決定的に重要な事実

スケジューラは limits を一切見ない。requests だけを見て配置先を決める。

つまり requests: 100m と書いておきながら実際は 1000m 使うアプリがあると、スケジューラは「この Pod は小さいから」と大量に同じ Node に詰め込み、結果としてその Node が過負荷になる。

逆に requests を実際より大きく書くと、使われないまま予約された領域が増え、Node の空きが減ってコストだけ増える。

requests は実測に基づいた正直な値にする。これが鉄則だ。

CPU と メモリの決定的な違い

CPU メモリ
性質 圧縮可能(Compressible) 圧縮不可能(Incompressible)
上限に達すると スロットリング(遅くなるだけ) OOMKilled(プロセスが強制終了)
超過時の深刻度 低い(性能劣化) 高い(落ちる

CPU は「順番待ちで遅くなる」だけで済むが、メモリは他のプロセスに分け与えられないため、超えた瞬間に殺される。この非対称性が、次の設定方針につながる。

設定例と、その理由

resources:
  requests:
    cpu: 500m       # 「500 ミリコア」= CPU 0.5 個分
    memory: 1Gi
  limits:
    memory: 1Gi     # メモリの上限は requests と同じ値にする
    # cpu の limits は、あえて設定しない

なぜメモリの limitsrequests と同じ値にするのか

メモリの limitsrequests より大きくすると、「普段は 1Gi だが、たまに 2Gi まで使える」という状態になる。一見よさそうだが、この余剰分は Node に空きがあるときしか使えない ため、混雑時に限って OOMKilled が発生する、という再現性の低い障害を生む。同じ値にしておけば挙動が常に一定になり、トラブルシュートが容易になる。

なぜ CPU の limits は設定しないのか

CPU の limits を設定すると、Linux の CFS(Completely Fair Scheduler)によるスロットリングが発生する。これは 100ms ごとの周期で「割り当てを使い切ったら残り時間は強制的に停止」という動作をするため、平均使用率が limits に達していなくても、瞬間的なバーストがある処理ではレイテンシが悪化する

Node に CPU の空きがあるなら使わせたほうが得なので、Web API のようなレイテンシ重視のワークロードでは CPU の limits を付けないのが定番だ。

⚠️ ただし、マルチテナントのクラスタ(他チームと Node を共有する等)では、暴走したアプリが Node 全体を巻き込むのを防ぐため、CPU limits を付ける運用もある。セキュリティ/安定性を取るか、レイテンシを取るかのトレードオフとして理解しておく。

QoS クラスについての補足

Kubernetes は requestslimits の設定内容から、Pod に QoS クラス を自動で割り当てる。これは Node のメモリが逼迫したとき、どの Pod から先に追い出されるかの優先順位になる。

QoS クラス 条件 追い出されやすさ
Guaranteed すべてのコンテナで CPU とメモリの両方requests == limits 最後まで残る
Burstable requests は設定済みだが Guaranteed の条件を満たさない 中間
BestEffort requestslimits も未設定 最初に殺される

⚠️ 注意点として、メモリだけ requests == limits にしても Guaranteed にはならない。CPU も同値にする必要がある。

前述のとおり CPU の limits を付けない構成では QoS は Burstable になる。これは意図的な選択だ。Guaranteed を狙って CPU limits を付けると、今度はスロットリングでレイテンシが悪化する。

最低限守るべきは「BestEffort にしないこと」requests を書いていない Pod は、Node がメモリ不足になった瞬間に真っ先に殺される。ここだけは必ず避ける。


第 6 章:Step 4 — Pod 数を計算する

計算式はシンプルだ。

Pod 数 = 必要な総キャパ ÷ Pod 1 個の requests

Step 2 の実消費と、Step 3 で決めた cpu: 500m / memory: 1Gi で計算する。

CPU 基準の計算

場面 計算 必要 Pod 数
平常時 1.2 vCPU ÷ 0.5 2.4 → 3 個
ピーク時 2.4 vCPU ÷ 0.5 4.8 → 5 個

メモリ基準の計算

場面 計算 必要 Pod 数
平常時 6.4 GiB ÷ 1 GiB 6.4 → 7 個
ピーク時 8.8 GiB ÷ 1 GiB 8.8 → 9 個

⚠️ 大きいほうが答えになる

CPU では 5 個、メモリでは 9 個。必要 Pod 数は常に大きいほう(この例では 9 個)を採用する。

なぜなら、Pod 数を 5 個にするとメモリが足りずに OOMKilled が発生するからだ。片方だけ見て決めてはいけない。

ボトルネックが偏っているときは Pod のサイズを見直す

この例では CPU:メモリ の必要比率が 2.4 : 8.8、およそ 1 vCPU あたり 3.7 GiB となっている。一方で設定した Pod のサイズは 0.5 vCPU : 1 GiB = 1 vCPU あたり 2 GiB で、比率が合っていない。

比率が合っていないと、どちらか一方が必ず余る。そこで Pod のサイズをワークロードの実際の比率に近づける。

# 修正後:メモリ寄りのワークロードに合わせる
resources:
  requests:
    cpu: 250m      # 500m → 250m に縮小
    memory: 1Gi
  limits:
    memory: 1Gi
場面 CPU 基準 メモリ基準 採用
平常時 1.2 ÷ 0.25 = 4.8 → 5 個 6.4 ÷ 1 = 6.4 → 7 個 7 個
ピーク時 2.4 ÷ 0.25 = 9.6 → 10 個 8.8 ÷ 1 = 8.8 → 9 個 10 個

CPU とメモリの必要 Pod 数が近づき、無駄が減った。

💡 Pod は小さく刻むほうが有利
Pod 1 個を小さくすると、① スケールの粒度が細かくなり過剰なスケールアウトが減る、② Node への詰め込み(ビンパッキング)効率が上がる、というメリットがある。
ただし刻みすぎると、コンテナごとのオーバーヘッド(サイドカー、JVM のヒープ外領域、コネクションプール等)の割合が増えて逆効果になる。まずは「EC2 1 台分の 1/2〜1/4」くらいから始めて調整するのが現実的だ。


第 7 章:Step 5 — HA の下限をかぶせる

計算結果が 1 個や 2 個になったとしても、本番環境では 最低 3 レプリカ を推奨する。理由は 3 つある。

理由1:AZ 分散

3 つの AZ に均等に配置するには、最低 3 個必要になる。

topologySpreadConstraints:
  - maxSkew: 1                                    # AZ 間の Pod 数の差を 1 個以内にする
    topologyKey: topology.kubernetes.io/zone      # AZ 単位で分散
    whenUnsatisfiable: ScheduleAnyway             # 満たせなくても起動はさせる
    labelSelector:
      matchLabels:
        app: my-api

whenUnsatisfiableDoNotSchedule にすると条件を満たせないとき Pod が起動しなくなる。厳密な分散が必要な場合以外は ScheduleAnyway から始めるのが安全だ。

理由2:PodDisruptionBudget(PDB)が成立する

PDB は「メンテナンス等で Pod を退避させるとき、最低何個は残すか」を宣言する仕組みだ。

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-api-pdb
spec:
  minAvailable: 2          # 最低 2 個は常に稼働させる
  selector:
    matchLabels:
      app: my-api

⚠️ レプリカ数が 2 で minAvailable: 2 にすると、1 個も退避できなくなり Node のアップデートが永久にブロックされる。レプリカ 3 に対して minAvailable: 2(または maxUnavailable: 1)が扱いやすい。

理由3:ローリングアップデート中も定足数を維持できる

デプロイ中は一時的に古い Pod が減る。レプリカ 2 だと、更新中は実質 1 個で全トラフィックを受けることになる。

最終的なレプリカ数

項目
計算上の平常時 7 個
計算上のピーク 10 個
HA 下限 3 個
採用する minReplicas 7 個(計算値 > HA 下限)
採用する maxReplicas 15〜20 個(ピーク × 1.5〜2)

第 8 章:Step 6 — Node 側のサイジング

Pod のサイズが決まったら、それを載せる Node を決める。ここで初学者が必ずハマるのが 「Node のスペック全部は使えない」 という事実だ。

差し引かれるもの

差し引かれる要素 内容 目安
kube-reserved kubelet 等の Kubernetes 自身の予約 CPU 数十 m、メモリ数百 MiB
system-reserved OS 自体の予約 同上
eviction-threshold 追い出し発動の閾値として空けておく分 メモリ 100 MiB 程度
DaemonSet 全 Node に 1 個ずつ強制配置される Pod CPU 0.3〜0.5 / メモリ 0.5〜1 GiB

DaemonSet の代表例は次のとおり。EKS では最初から何個か動いている。

  • aws-node(VPC CNI・ネットワーク)
  • kube-proxy
  • CloudWatch エージェント / Fluent Bit(ログ収集)
  • セキュリティエージェント(GuardDuty Agent 等)

実際に使える量を確認する

推測せず、コマンドで確認するのが確実だ。

kubectl describe node <node-name>

出力の中に次のようなセクションがある。

Capacity:
  cpu:                2          # ← インスタンスのスペック
  memory:             7906908Ki
  pods:               29         # ← 載せられる Pod 数の上限
Allocatable:
  cpu:                1930m      # ← Pod が実際に使える量(ここを見る)
  memory:             7150748Ki
  pods:               29

Capacity ではなく Allocatable を見る。 さらにここから DaemonSet の消費分を引いたものが、アプリ用に使える実質容量になる。

Pod 数の上限(max-pods)に注意

EKS のデフォルト(VPC CNI)では、Node に載せられる Pod 数が ENI とセカンダリ IP の数で制限される。CPU やメモリが余っていても、この上限で Pod が起動できなくなる。

インスタンスタイプ 最大 Pod 数(デフォルト)
t3.small 11
m5.large 29
m5.xlarge 58
m5.2xlarge 58

⚠️ 小さいインスタンスで Pod を細かく刻むと、この上限に先に当たる。

回避策:プレフィックス委任(Prefix Delegation)
VPC CNI の ENABLE_PREFIX_DELEGATION=true を有効にすると、IP を 1 個ずつではなく /28 のブロック単位で ENI に割り当てるようになり、Node あたりの Pod 数上限が大幅に増える(m5.large なら 110 まで)。小さい Pod を多数動かす構成では、ほぼ必須の設定になる。


第 9 章:オートスケール入門 — HPA / VPA / Karpenter とその仕組み

ここまでは「初期値をどう決めるか」の話だった。しかし負荷は時間帯で変わる。そこで自動調整の仕組みが必要になる。

Kubernetes のオートスケーリングは 3 階層 で考える。

┌─────────────────────────────────────────────┐
│ アプリのレイヤー                              │
│   HPA  … Pod の「数」を増減する               │
│   VPA  … Pod の「サイズ」を調整する            │
├─────────────────────────────────────────────┤
│ インフラのレイヤー                            │
│   Karpenter / Cluster Autoscaler             │
│        … Node(EC2)の「数」を増減する         │
└─────────────────────────────────────────────┘

この 3 つが揃って初めて「勝手にスケールするクラスタ」になる。HPA だけ入れても、Pod を載せる Node に空きがなければ Pod は Pending のまま起動しない。


9-1. HPA(Horizontal Pod Autoscaler)— 横に増やす

H = Horizontal(水平)= 横に増やす。

before:  [Pod]                     ← 1 個では処理しきれない
after:   [Pod] [Pod] [Pod]         ← 数を増やして分担する

レジに例えると「客が増えたのでレジの台数を増やす」に相当する。

設定例

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-api
  minReplicas: 7                                # 平常時の必要数
  maxReplicas: 15                               # ピーク × 1.5〜2
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60                # ← 目標使用率

⚠️ 最大の誤解ポイント:60% は何に対する 60% か

averageUtilization: 60マシンの CPU 全体に対する 60% ではない。

requests.cpu に対する 60% である。

具体例で確認する。

requests.cpu: 250m、targetUtilization: 60%
  → 目標は 250m × 60% = 150m
  → 「1 Pod が 150m 使っている状態」を維持したい

現在 7 Pod が動いていて、平均 210m 使っていた場合の計算:

  必要 Pod 数 = 現在の Pod 数 × (現在の使用量 ÷ 目標使用量)
             = 7 × (210 ÷ 150)
             = 9.8
             → 切り上げて 10 Pod にスケールアウト

この式(ceil[currentReplicas × (currentMetric ÷ targetMetric)])が HPA の全てだ。requests が基準になっているため、requests が実態とズレていると HPA も正しく動かない。

目標値の決め方

設定値 挙動
80〜90% スケールが間に合わず、増える前に性能劣化する
60〜70% 実務的な推奨レンジ
30〜40% 常に Pod が余り、コストが増える

新しい Pod が起動して実際にトラフィックを受けるまでには、コンテナイメージの Pull、アプリの起動、ヘルスチェック通過などで数十秒〜数分かかる。その間の余裕として 30〜40% を空けておく、という考え方だ。

前提条件:Metrics Server

⚠️ HPA を動かすには Metrics Server がクラスタに必要になる。Pod の CPU 使用量を測定してくれるコンポーネントで、EKS にはデフォルトでは入っていない

# インストール
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# 動作確認(数値が出れば OK)
kubectl top pods

kubectl top pods がエラーになる状態では HPA も動かない。まずここを確認する。

スケールイン(減らす方)は慎重に動く

HPA には stabilization window という仕組みがあり、デフォルトでは スケールインは 5 分間の様子見をしてから実行される。負荷が一時的に下がっただけで Pod を減らし、すぐまた増やす、という振動(フラッピング)を防ぐためだ。

一方スケールアウトは即座に反応する。「増えるのは速く、減るのは遅い」 が HPA の基本挙動と覚えておく。

CPU 以外でスケールさせたい場合

CPU 使用率がボトルネックでないアプリ(I/O 待ちが主な処理など)では、CPU で測っても意味がない。その場合は次のようなメトリクスを使う。

メトリクス 用途
ALB のリクエスト数 Web API の実負荷に最も近い
SQS のキュー滞留数 ワーカー系の処理
アプリ独自のメトリクス 同時接続数など

これらを使うには KEDAPrometheus Adapter といったアダプタを追加導入する。初学者はまず CPU で始め、必要になったら移行するとよい。


9-2. VPA(Vertical Pod Autoscaler)— 縦に太らせる

V = Vertical(垂直)= サイズを変える。

before:  [ Pod (小) ]        ← 1 個あたりのメモリが足りない
after:   [ Pod (大) ]        ← サイズを大きくして解決

レジの例では「レジの台数はそのままで、店員をベテランに替えて処理を速くする」に相当する。

VPA の本質は requests の値が実態と合っているかを診断してくれる係」 だ。

⚠️ VPA は原則「提案モード」で使う

VPA には 4 つの動作モードがある。

モード 挙動 初学者向けか
Off 提案するだけ。何も変更しない まずこれ
Initial 新しく作られる Pod にのみ適用。既存は触らない
Recreate 既存 Pod を削除して作り直す
Auto 自動で書き換える(現状は実質 Recreate 相当)

Auto を本番でいきなり使ってはいけない。 VPA が値を変更するたびに Pod が再起動される ため、サービス稼働中に Pod が落ちては立ち上がる状態になる。

💡 補足:Kubernetes 1.33 以降では Pod を再起動せずにリソースを変更する「In-Place Pod Resize」が使えるようになりつつあり、VPA 側もこれに対応しつつある。ただし機能の成熟度とクラスタバージョンに依存するため、現時点では Off から始める方針を推奨する。

使い方

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-api-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-api
  updatePolicy:
    updateMode: "Off"        # ← 提案のみ。何も変更されない

1〜2 週間放置してから推奨値を確認する。

kubectl describe vpa my-api-vpa
Recommendation:
  Container Recommendations:
    Container Name:  my-api
    Lower Bound:                  # これ未満だと性能が出ない下限
      Cpu:     180m
      Memory:  220Mi
    Target:                       # ← 推奨値。これを採用する
      Cpu:     237m
      Memory:  262Mi
    Upper Bound:                  # これ以上は明らかに過剰
      Cpu:     420m
      Memory:  580Mi

この Target の値を 人間が確認したうえで手動で YAML に反映する、というのが最も安全な運用になる。

⚠️ VPA も EKS にはデフォルトで入っていないため、別途インストールが必要だ。

同種のツール

VPA 以外にも requests の適正値を教えてくれるツールがある。

ツール 特徴
Goldilocks VPA をラップして Web UI で推奨値を一覧表示する。見やすい
KRR(Robusta) Prometheus のデータから推奨値を算出。VPA 不要で導入が軽い
Kubecost コスト可視化がメイン。金額換算で無駄が見える

9-3. HPA と VPA を同時に使うときの罠

⚠️ 同じメトリクス(CPU)に対して HPA と VPA を両方 Auto で動かすと、システムが振動する。

① VPA「CPU が足りないので requests を 250m → 500m に増やす」
        ↓  requests(=使用率の分母)が 2 倍になった
② HPA「使用率が 60% → 30% に下がった。Pod を減らそう」
        ↓  Pod が減り、1 個あたりの負荷が上がった
③ VPA「CPU が足りないので requests をさらに増やす」
        ↓
④  ①に戻る(無限ループ)

原因は明快で、HPA の判断基準(使用率)の分母を、VPA が勝手に書き換えてしまうためだ。

安全な組み合わせ

構成 内容 推奨度
HPA(CPU)+ VPA(Off HPA で数を自動調整し、VPA からは提案だけもらう 王道。まずこれ
HPA(カスタムメトリクス)+ VPA(メモリのみ) 競合するメトリクスを分離する △ 上級者向け
HPA(CPU)+ VPA(Auto, CPU) 競合する ❌ 絶対に避ける

9-4. Karpenter — Node を増やす

HPA が Pod を増やしても、載せる Node に空きがなければ Pod は Pending のまま起動しない。この「Node が足りない」を解決するのが Karpenter だ。

# Pending の Pod を確認する
kubectl get pods
# NAME                      READY   STATUS    RESTARTS   AGE
# my-api-7d9f8b6c5d-x2k4p   0/1     Pending   0          3m   ← Node に空きがない

kubectl describe pod my-api-7d9f8b6c5d-x2k4p
# Events:
#   Warning  FailedScheduling  ... 0/2 nodes are available: 2 Insufficient cpu.

動作の流れ

1. HPA が Pod を 7 個 → 10 個に増やす
2. Node に空きがなく、3 個が Pending になる
3. Karpenter が Pending Pod の requests を合計する
4. 「この 3 個をちょうど載せられるインスタンスタイプ」を選定して EC2 を起動
5. Pod が起動する
6. 負荷が下がって Pod が減ったら、空いた Node を自動で削除(Consolidation)

Cluster Autoscaler との違い

Cluster Autoscaler Karpenter
仕組み 事前定義した Auto Scaling Group の希望台数を増減 必要なスペックの EC2 を直接起動
インスタンスタイプ ASG ごとに固定 Pod の要件に応じて自動選択
起動速度 相対的に遅い 速い
無駄の削減 ASG の粒度に縛られる Consolidation で継続的に最適化

現在の EKS では Karpenter が推奨 される。「Pod の要求に対してちょうどいいインスタンスを選んでくれる」点が、サイジングの手間を大きく減らしてくれる。


9-5. オートスケールは実際どう動いているのか

「HPA が Pod を増やす」と一言で書いたが、内部では複数のコンポーネントが連携している。ここを理解しておくと、スケールしないときの切り分けが一気に楽になる。

全体の登場人物

 ① kubelet / cAdvisor      各 Node で Pod の使用量を計測している
        │  (15秒ごとに収集)
        ▼
 ② Metrics Server          全 Node から使用量を集めて API として提供
        │  (metrics.k8s.io という API になる)
        ▼
 ③ HPA コントローラー       15秒ごとに「今の使用率」を問い合わせ、必要な数を計算
        │  (Deployment の replicas を書き換える)
        ▼
 ④ ReplicaSet コントローラー  replicas の数に合わせて Pod オブジェクトを作る
        │
        ▼
 ⑤ スケジューラー           作られた Pod を、requests が収まる Node に割り当てる
        │  (空きがなければ Pending のまま)
        ▼
 ⑥ Karpenter               Pending の Pod を見つけて EC2 を起動する
        │
        ▼
 ⑦ kubelet                 Node が Ready になり、イメージを Pull して Pod を起動

⚠️ 重要なのは、これらが「命令」ではなく「それぞれが独立したループ」として動いている点だ。HPA は Karpenter に「Node を作れ」と命令していない。HPA は replicas の数字を書き換えるだけで、Karpenter は「Pending の Pod があるな」と自分で気づいて動く。

この「宣言的な状態を各コンポーネントが勝手に収束させる」考え方が Kubernetes の中核であり、だからこそ、どこか 1 つが欠けると静かに止まる

スケールしないときの切り分け表

上の流れのどこで詰まっているかは、コマンドで特定できる。

症状 確認コマンド 疑うべき箇所
HPA の TARGETS<unknown> kubectl get hpa ② Metrics Server が未導入/異常
kubectl top pods がエラー kubectl top pods ② Metrics Server
HPA が反応しない kubectl describe hpa <name> requests 未設定の可能性が高い
Pod が Pending のまま kubectl describe pod <name> ⑤⑥ Node の空き不足 or Karpenter
Node は増えたのに Pod が起動しない kubectl describe node ⑦ イメージ Pull 失敗、taint の不一致

⚠️ requests を書いていない Pod は、HPA が絶対に動かない。
HPA は「使用量 ÷ requests」で使用率を計算するため、requests が無いと割り算が成立しない。kubectl describe hpamissing request for cpu と出ていたらこれが原因だ。前章までで「requests がすべての基準」と繰り返してきた理由がここにもある。

時間軸で見るスケールアウト

実際に負荷が急増してから、Pod がトラフィックを受け始めるまでの流れを秒単位で追う。

 0秒    負荷が急増する
        │
 〜15秒 kubelet / Metrics Server がメトリクスを更新(収集間隔ぶんの遅れ)
        │
 〜30秒 HPA が変化を検知し、replicas を 7 → 10 に書き換える
        │
 〜31秒 Pod が 3 個作られる。ただし Node に空きがなく Pending
        │
 〜35秒 Karpenter が Pending を検知し、EC2 を起動
        │
 〜75秒 Node が Ready になる(EC2 起動 + kubelet 登録で 30〜60 秒)
        │
 〜95秒 コンテナイメージを Pull(イメージサイズ次第で 10〜120 秒)
        │
 〜110秒 アプリが起動し、Readiness Probe が通る
        │
 〜111秒 ようやくトラフィックを受け始める

合計で 2 分前後かかる。 「HPA を入れたのに急なアクセス増で落ちた」という事故は、ほぼこの遅延が原因だ。

遅延を短くする打ち手

打ち手 効果 難易度
HPA の目標値を下げる(70% → 60%) 早めに増え始める。最も手軽
コンテナイメージを小さくする Pull 時間を短縮。マルチステージビルド等 ★★
minReplicas に余裕を持たせる そもそも Pending を発生させない
予測スケール(時間帯で minReplicas を変える) 朝のピーク等、事前に分かる負荷に有効 ★★
オーバープロビジョニング Pod ↓ 次項で解説 ★★

裏技:オーバープロビジョニング Pod

「何もしない、優先度が最低の Pod」をあえて動かしておき、Node の空き枠を事前に確保しておくテクニックがある。

通常時:  [ 実 Pod ][ 実 Pod ][ ダミー Pod ]   ← ダミーが席取りをしている
                                 ↓
急増時:  [ 実 Pod ][ 実 Pod ][ 実 Pod ]      ← ダミーが即座に追い出され、
                              + ダミーは Pending へ    実 Pod がすぐ起動できる
                                 ↓
         Karpenter がダミーのために Node を増やす(この間もサービスは正常)

ダミー Pod に PriorityClass の低い値(負の値)を設定しておくと、実際の Pod が来たときに即座に追い出される。EC2 の起動時間をサービスのクリティカルパスから外せるのがポイントだ。

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: overprovisioning
value: -1                    # ← 負の値。通常の Pod(0)より必ず低い
globalDefault: false
description: "席取り用のダミー Pod"

9-6. HPA の挙動を細かく制御する(behavior)

デフォルトのスケール速度が合わない場合、behavior セクションで調整できる。ここは初学者がいきなり触る必要はないが、「調整できる」と知っておくと詰まったときに助かる。

spec:
  behavior:
    scaleUp:                             # 増やすときの挙動
      stabilizationWindowSeconds: 0      # 様子見なし(デフォルト。即座に増やす)
      policies:
        - type: Percent
          value: 100                     # 一度に最大 100%(=倍)まで増やせる
          periodSeconds: 15
        - type: Pods
          value: 4                       # または一度に最大 4 個まで
          periodSeconds: 15
      selectPolicy: Max                  # 上の2つのうち「多いほう」を採用

    scaleDown:                           # 減らすときの挙動
      stabilizationWindowSeconds: 300    # 5分間様子を見てから減らす(デフォルト)
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15

なぜ増やすのは速く、減らすのは遅いのか

間違えたときの被害が非対称だからだ。

判断ミス 結果
増やしすぎた 少し余分にお金がかかる(後から減らせる)
減らしすぎた サービスが落ちる(取り返しがつかない)

だから Kubernetes は「迷ったら増やす」側に倒すデフォルトになっている。

振動(フラッピング)への対処

負荷が閾値の前後で揺れると、増やす → 減らす → 増やす、を繰り返して Pod が落ち着かないことがある。

負荷:  ▁▇▁▇▁▇▁▇        ← 閾値付近で揺れている
Pod:   7 →10→ 7 →10→ 7   ← 起動と終了を繰り返し、かえって不安定になる

この場合は scaleDown.stabilizationWindowSeconds を 300 → 600 のように延ばす。「減らす判断をより慎重にする」ことで振動が収まる。


9-7. Node のオートスケールの仕組み(Karpenter の中身)

Karpenter が「Pending の Pod を見て EC2 を起動する」までの流れを、もう少し細かく見る。

スケールアウト(Provisioning)

1. Pending の Pod を収集する
       ↓
2. それぞれの Pod の requests・nodeSelector・taint/toleration・
   topologySpreadConstraints をすべて満たす組み合わせを計算する
       ↓
3. 「この Pod 群を最も安く収容できるインスタンスタイプ」の候補を多数リストアップ
       ↓
4. EC2 Fleet API に候補リストを丸ごと渡す
       ↓
5. EC2 側が、在庫と価格から最適な 1 つを選んで起動する

ログを見ると、この様子が実際に確認できる。

kubectl -n karpenter logs -l app.kubernetes.io/name=karpenter
"message":"created nodeclaim", "instance-types":"m5.2xlarge, m6g.4xlarge, c5.xlarge ... and 55 other(s)"
"message":"launched nodeclaim", "instance-type":"m6g.2xlarge", "capacity-type":"spot"

候補を 60 個近く挙げたうえで 1 つが選ばれている点に注目してほしい。前章で「候補を絞りすぎるな」と書いた理由がこれだ。候補が少ないと、在庫切れのときに起動できるものが無くなる。

スケールイン(Consolidation)

Karpenter は Node を減らすとき、2 つの方法を使う。

方法 内容
Node 削除 その Node の Pod が全部、他の Node の空きに収まるなら Node ごと消す
Node 置換 大きい Node 1 台を、より安い小さい Node 1 台に置き換える
Before:  [ m5.2xlarge (半分空き) ][ m5.large (ほぼ空き) ]
                    ↓  Consolidation
After:   [ m5.xlarge (ちょうど埋まる) ]        ← 安く詰め直された

⚠️ この置き換えの際に Pod は必ず退避(drain)される。ここで PodDisruptionBudget を設定していないと、レプリカが一時的に 0 になる可能性がある。PDB は Karpenter を使うなら必須だと考えてよい。

disruption:
  consolidationPolicy: WhenEmptyOrUnderutilized
  consolidateAfter: 1m
  budgets:
    - nodes: "10%"          # 一度に触る Node は全体の 10% まで

Node の有効期限(Drift / Expiry)

Karpenter には「Node を一定期間で作り直す」機能もある。

目的 内容
セキュリティパッチの適用 新しい AMI が出たら自動で入れ替える(Drift)
設定変更の反映 NodePool を変更したら、古い設定の Node を置き換える
長期稼働の回避 expireAfter で指定期間後に作り直す

EC2 を「大事に長く使う資産」ではなく「使い捨ての家畜」として扱うのが Kubernetes の思想であり、これを自動化するのが Karpenter の役目になる。

Cluster Autoscaler の仕組みとの違い

Cluster Autoscaler Karpenter
見ているもの ASG の希望台数 Pending Pod そのもの
増やし方 ASG の DesiredCapacity を +1 する EC2 Fleet API で直接起動
インスタンスの種類 ASG に定義済みのものだけ リージョン内のほぼ全種類から選択
減らし方 使用率が低い Node を削除 Consolidation で継続的に詰め直す
反応速度 ASG 経由のぶん遅い 速い

Cluster Autoscaler は「ASG という箱を操作する」のに対し、Karpenter は「EC2 を直接操作する」。この違いが、柔軟性と速度の差になっている。


9-8. スケールの土台になる設定

オートスケールを正しく動かすには、スケーラー自体の設定以外に前提となる設定がある。ここが抜けていると、スケールしたのにエラーが出る、という状態になる。

Readiness Probe(起動完了の判定)

これが無いと、まだ起動途中の Pod にトラフィックが流れてエラーになる。

readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5      # 起動後、最初のチェックまでの待ち時間
  periodSeconds: 5            # チェック間隔
  failureThreshold: 3         # 3回失敗したらトラフィックを止める
Probe の種類 失敗したときの動作
Readiness トラフィックを流さない(Pod は生かしておく)
Liveness Pod を再起動する
Startup 起動が遅いアプリ用。これが通るまで他の Probe を待たせる

⚠️ Liveness Probe を厳しくしすぎると、負荷が高いときに「応答が遅い → 再起動 → さらに残りに負荷が集中 → 全滅」という連鎖を起こす。Liveness は緩く、Readiness は適切にが原則になる。

graceful shutdown(終了処理)

スケールインで Pod が消えるとき、処理中のリクエストを取りこぼさない設定も要る。

spec:
  terminationGracePeriodSeconds: 30    # 強制終了までの猶予
  containers:
    - name: app
      lifecycle:
        preStop:
          exec:
            command: ["sleep", "5"]    # ← ロードバランサーの切り離しを待つ

preStop で数秒待つ理由は、Pod の削除通知とロードバランサーからの切り離しが並行して進むためだ。この待ち時間がないと、すでに終了処理に入った Pod に新規リクエストが届いてエラーになる。


第 10 章:EKS でも EC2 は必要なのか

ここまでで「Node」という言葉を何度も使ってきたが、その正体は EC2 インスタンスだ。ではクラスターを作ると必ず EC2 が必要になるのか。答えは 「基本的に必要。ただし自分で管理する量は選べる」 になる。

EKS の 2 つの層

┌─ コントロールプレーン(API Server, etcd, スケジューラー)─┐
│   AWS が完全に管理する。EC2 は一切見えない                │  ← 固定費のみ
└──────────────────────────────────────────────────────────┘
┌─ データプレーン(Pod が実際に動く場所)───────────────────┐
│   ここが EC2。自分の VPC の中に立つ                       │  ← サイジングの対象
└──────────────────────────────────────────────────────────┘

「EKS クラスターを作る」= 上のコントロールプレーンを作る、という意味になる。この時点では EC2 は 1 台も存在しない。

コンピュートの選択肢は 4 つ

方式 インスタンスタイプを決めるのは 向いている場面
EKS Auto Mode AWS(Karpenter 内蔵) ✅ 新規構築ならこれが第一候補
Karpenter 自前運用 Karpenter(設定は自分で細かく) 大規模、細かい制御が必要
マネージドノードグループ 自分で明示的に指定 既存 ASG 運用からの移行
セルフマネージドノード 自分(すべて手動) 特殊要件がある場合
AWS Fargate 存在しない(Pod 単位でサーバレス) 現在は非推奨寄り ↓

EKS Auto Mode とは

コントロールプレーンだけでなく データプレーンまで AWS に任せる モードだ。Karpenter・VPC CNI・ロードバランサー連携・ストレージドライバといった、通常は自分で導入・運用するコンポーネントが最初から組み込まれている。

  • Node の起動・終了・スケールを自動で実行する
  • OS の更新も、PodDisruptionBudget を尊重しながら自動で適用する
  • 使われている AMI は Bottlerocket ベースのイミュータブルなもの

Fargate についての注意

AWS の公式 FAQ では、「EKS Auto Mode が今後の推奨アプローチである」 と明言されている。Fargate には次の制約があるためだ。

Fargate の制約
DaemonSet が使えない(ログ収集エージェント等の構成が変わる)
Istio 等のプラットフォームツールが動かない
Spot インスタンスや GPU が使えない
RI / Savings Plans による割引が効かない

新規で構築するなら、Fargate ではなく Auto Mode を選ぶのが無難だ。

⚠️ Auto Mode のコスト

Auto Mode は EC2 の料金に加えて 管理手数料 が上乗せされる(インスタンスの料金に対する一定割合)。

判断軸 選択
小〜中規模、運用工数を減らしたい Auto Mode(手数料 < 運用にかかる人件費)
大規模でコストを極限まで詰めたい Karpenter 自前(手数料ゼロ。ただし運用は自分)

第 11 章:インスタンスタイプをどう選ぶか

Auto Mode や Karpenter を使う場合、「インスタンスタイプを 1 つ決める」のではなく「候補の範囲を宣言する」 のが現在の考え方になる。

# Karpenter / Auto Mode の NodePool
requirements:
  - key: eks.amazonaws.com/instance-category
    operator: In
    values: ["c", "m", "r"]            # ← ファミリーの範囲
  - key: kubernetes.io/arch
    operator: In
    values: ["arm64", "amd64"]         # ← アーキテクチャ
  - key: karpenter.sh/capacity-type
    operator: In
    values: ["spot", "on-demand"]      # ← 購入オプション

⚠️ 絞りすぎるのが最大のアンチパターンになる。特に Spot を使う場合、候補が少ないとキャパシティ枯渇時に Pod が Pending のまま復旧しなくなる。公式のベストプラクティスでも「候補は広く取る」ことが推奨されている。

以下、決めるべき 6 つの観点を挙げる。

① インスタンスファミリー ← requests と直結する

Pod の CPU : メモリ比率と、インスタンスの比率を合わせる。 ここが最も重要になる。

ファミリー vCPU : メモリ 合うワークロード
c 系(Compute 最適化) 1 : 2 CPU 重視(エンコード、数値計算)
m 系(汎用) 1 : 4 一般的な Web / API
r 系(メモリ最適化) 1 : 8 キャッシュ、JVM、インメモリ処理

第 6 章で決めた requests: cpu 250m / memory 1Gi1 vCPU : 4 GiB、つまり m 系がちょうど合う。ここで r 系を選ぶと CPU が大量に余り、c 系を選ぶとメモリが先に枯渇する。

Pod の比率が、そのままノードの比率になる。
requests を決めた時点で、実はインスタンスファミリーも半分決まっている。

② アーキテクチャ(Graviton / ARM)

同等スペックで最大 40% 価格性能が良い。 アプリケーションが arm64 で動作するなら、使わない理由はほぼない。

m5.large (x86)  →  m7g.large (Graviton)

⚠️ 事前に確認すべき点は次のとおり。

  • コンテナイメージが arm64 に対応しているか(マルチアーキテクチャビルドが必要)
  • ネイティブ拡張を含むライブラリが動作するか(Python の一部、Node.js の node-gyp 系など)

③ 購入オプション(Spot / On-Demand)

Spot は最大 90% 割引になる一方、2 分前の通知で強制的に回収される

ワークロード 推奨
ステートレスな Web / API(レプリカ多数) Spot 中心 + On-Demand を一定数混ぜる
バッチ、非同期ワーカー Spot
ステートフル、シングルトン On-Demand

④ ノードの「大きさ」のトレードオフ

見落とされがちだが、同じ総容量でも「大きい Node を少数」か「小さい Node を多数」かで性質が変わる。

大きい Node(例:m7g.4xlarge 小さい Node(例:m7g.large
ビンパッキング効率 ✅ 高い(詰めやすい) ❌ 端数が出やすい
DaemonSet のオーバーヘッド ✅ 台数が少なく割安 ❌ 台数ぶん必要で割高
障害時の影響範囲 ❌ 1 台落ちると大量の Pod が死ぬ ✅ 影響が小さい
スケールの粒度 ❌ 粗い(1 台増が大きすぎる) ✅ 細かい
max-pods 上限 ✅ 余裕がある ❌ 先に上限に当たる

実務的な落としどころは xlarge2xlarge 前後になる。第 8 章で触れた DaemonSet のオーバーヘッド(CPU 0.3〜0.5 / メモリ 0.5〜1 GiB)は、large サイズだと全体の 20〜25% を占めてしまう。これが「小さすぎる Node は割に合わない」の正体だ。

⑤ max-pods(ENI 上限)

第 8 章で触れたとおり、Node あたりの Pod 数には上限がある。Pod を細かく刻む戦略と、小さい Node は相性が悪い。プレフィックス委任の有効化がセットで必要になる。

EKS Auto Mode ではプレフィックス委任がデフォルトで有効なため、この考慮は不要になる。

⑥ 特殊要件の有無

要件 必要なファミリー
GPU(推論・学習) g / p
ローカル NVMe(高速ディスク) d が付くもの(m6id, c6id 等)
高ネットワーク帯域 n が付くもの(c6in 等)

これらは 専用の NodePool を分けて taint を設定する のがセオリーになる。汎用の Pod が高価な GPU ノードに載ってしまう事故を防げる。

この記事の例に当てはめると

必要量は ピーク 2.4 vCPU / 8.8 GiB(+ DaemonSet 分)だった。推奨する宣言は次のようになる。

requirements:
  - key: eks.amazonaws.com/instance-category
    operator: In
    values: ["m", "c", "r"]                    # 広めに取る
  - key: eks.amazonaws.com/instance-size
    operator: In
    values: ["large", "xlarge", "2xlarge"]     # 極端なサイズだけ除外
  - key: kubernetes.io/arch
    operator: In
    values: ["arm64", "amd64"]                 # Graviton を許可
  - key: karpenter.sh/capacity-type
    operator: In
    values: ["spot", "on-demand"]

これで Karpenter が「m7g.xlarge × 2 台(AZ 分散)」といった構成を自動的に選ぶ。人間が m5.large と書き込む必要はもうない。


第 12 章:インスタンスタイプはいつ聞かれるのか

「クラスターを作るときにインスタンスタイプを聞かれるのか」は、初学者が最初に抱く疑問だ。答えは 「クラスター作成では聞かれない」

手順は 2 段階に分かれている

Step 1: クラスター作成       → コントロールプレーン。インスタンスタイプは聞かれない
Step 2: ノードグループ作成    → ★ここで初めて聞かれる★

昔の EKS はこの 2 段階が完全に別作業だったため、「クラスターを作ったのに Pod が Pending から動かない」というつまずきが定番だった。

作成方法ごとの違い

作成方法 インスタンスタイプを聞かれるか
コンソール:Quick configuration ❌ 聞かれない(Auto Mode 込みで自動設定)
コンソール:Custom + Auto Mode 有効 ❌ 聞かれない(NodePool の有効/無効のみ)
コンソール:Custom + Auto Mode 無効 ⭕ クラスター作成、ノードグループ追加時に聞かれる
eksctl(フラグ指定なし) ⚠️ 聞かれないが、デフォルト値が自動採用される
eksctl(--node-type 指定) ⭕ 自分で指定する
CDK aws-eks-v2 ❌ Auto Mode がデフォルトのため不要
CDK(NODEGROUP 指定) defaultCapacityInstance で指定
Terraform(eks module) instance_types の明示指定が必要

① Auto Mode の場合:最後まで聞かれない

コンソールの Auto Mode Compute セクションで入力するのは次の 2 つだけになる。

  • Node pools — 組み込みの general-purpose / system を使うか
  • Node IAM role — 起動する Node に付与する IAM ロール

インスタンスタイプの入力欄がそもそも存在しない。 第 11 章で説明した「範囲の宣言」は、必要になった時点で NodePool の YAML として追加する形になる。

② eksctl の場合:黙って決まる

eksctl create cluster --name my-cluster --region ap-northeast-1
# → 何も聞かれずにノードグループが作られる(デフォルトは m5.large)

⚠️ これが最大の罠になる。 「聞かれなかった」は「考えなくてよい」ではなく、デフォルト値が自動的に採用されただけだ。検証用途なら問題ないが、本番でこれをやると、ここまで説明したサイジングがすべて無意味になる。

明示するなら次のように指定する。

eksctl create cluster \
  --name my-cluster \
  --region ap-northeast-1 \
  --node-type m7g.xlarge \
  --nodes 2 --nodes-min 2 --nodes-max 6

③ マネージドノードグループの場合:詳細に聞かれる

コンソールから「ノードグループを追加」すると、次の項目を入力することになる。

項目 内容
AMI タイプ Amazon Linux 2023 / Bottlerocket / Windows など
インスタンスタイプ 複数選択できる(Spot 利用時は必ず複数入れる)
キャパシティタイプ On-Demand / Spot
ディスクサイズ デフォルトは 20 GiB
希望 / 最小 / 最大サイズ Auto Scaling Group と同じ考え方

💡 インスタンスタイプを 1 つしか指定しないと、Spot 枯渇時に Node が起動できなくなる。第 11 章の「候補は広く取る」がここでも効いてくる。


第 13 章:最も精度の高いサイジング方法 — 負荷試験

ここまで説明した CPU / メモリからの逆算は、あくまで 初期値 を出すための方法だ。本命は負荷試験になる。

飽和点(Saturation Point)を測る

Pod を 1 個だけ動かした状態で徐々に負荷をかけ、レスポンスタイムが劣化し始める点を探す。

リクエスト数を増やしていくと…

 rps  50 → レイテンシ  20ms   ✅ 余裕
 rps 100 → レイテンシ  25ms   ✅ 余裕
 rps 150 → レイテンシ  35ms   ✅ まだ大丈夫
 rps 200 → レイテンシ 120ms   ⚠️ 急激に悪化 ← ここが飽和点
 rps 250 → レイテンシ 800ms   ❌ 破綻

この例なら 1 Pod = 150 rps が実力値になる。

飽和点から Pod 数を出す

必要 Pod 数 = ピーク時の rps ÷ 1 Pod の rps × 安全係数

例)ピーク 1,200 rps、1 Pod = 150 rps、安全係数 1.3 の場合
    1200 ÷ 150 × 1.3 = 10.4 → 11 Pod

CPU / メモリからの逆算より圧倒的に精度が高い。理由は、実際のボトルネックが CPU やメモリとは限らないからだ。

隠れたボトルネックの例
DB のコネクションプール上限
外部 API のレート制限
スレッドプールの枯渇
ディスク I/O

これらは CPU 使用率を見ても分からない。負荷試験なら「レスポンスが遅くなる」という結果として必ず現れる。

ツール

ツール 特徴
k6 JavaScript でシナリオを書く。学習コストが低い
Vegeta CLI で手軽。単純な負荷なら最速
Locust Python でシナリオを書く。Web UI 付き
Distributed Load Testing on AWS AWS のソリューション。大規模負荷向け

第 14 章:移行チェックリスト

サイジングして本番投入する前に確認する項目をまとめる。

計測フェーズ

  • EC2 の CPU メトリクスを 2 週間以上取得した
  • メモリメトリクスを取得した(CloudWatch エージェントの有無を確認した)
  • Average だけでなく P95 と Maximum も取得した
  • スペック値ではなく実消費量に換算した

設定フェーズ

  • すべてのコンテナに requests を設定した(BestEffort をなくした
  • サイドカーコンテナの分も requests に含めた
  • メモリの limitsrequests と同値にした
  • CPU / メモリ両方で Pod 数を計算し、大きいほうを採用した
  • レプリカ数が 3 以上ある(または HA 要件を満たしている)
  • PodDisruptionBudget を設定し、レプリカ数と矛盾していないことを確認した
  • topologySpreadConstraints で AZ 分散を設定した
  • Liveness / Readiness Probe を設定した

オートスケーリング

  • Metrics Server を導入し、kubectl top pods が動作する
  • HPA の TARGETS<unknown> になっていない
  • HPA の minReplicas / maxReplicas を実測から決めた
  • HPA と VPA を同じメトリクスで競合させていない(VPA は Off
  • Karpenter または Cluster Autoscaler を導入した
  • Node の max-pods 上限に引っかからないことを確認した
  • preStopterminationGracePeriodSeconds を設定し、スケールイン時の取りこぼしを防いだ

Node / インスタンスタイプ

  • コンピュート方式を選んだ(Auto Mode / Karpenter / マネージドノードグループ)
  • インスタンスファミリーが Pod の CPU : メモリ比率と合っている
  • インスタンスタイプの候補を絞りすぎていない(特に Spot 利用時)
  • Graviton(arm64)を検討した
  • eksctl のデフォルト値(m5.large)をそのまま本番に使っていない
  • GPU 等の特殊要件がある場合、NodePool を分けて taint を設定した

検証

  • 負荷試験で 1 Pod の飽和点を測定した
  • スケールアウトが間に合うことを確認した(急増から実際に処理を受けるまで約 2 分かかる前提)
  • Pod を強制削除しても、サービスが継続することを確認した
  • Node を強制終了しても、サービスが継続することを確認した

まとめ

論点 結論
EC2 2 台 → Pod は何個? 2 個ではない。実測から算出する
何を基準に計算する? インスタンスのスペックではなく 実消費量
すべての基準になる値は? requests。これがズレると全部ズレる
CPU とメモリ、どちらで決める? 両方計算して 大きいほう
最低レプリカ数は? 3(AZ 分散 + PDB + ローリングアップデート)
HPA と VPA の違いは? HPA = を増やす / VPA = サイズを直す
VPA はどう使う? Off モードで提案だけもらうAuto は使わない
スケールにかかる時間は? 急増から処理開始まで約 2 分。目標使用率で余裕を作る
Node はどうする? Karpenter / EKS Auto Mode に任せる
EKS でも EC2 は必要? 必要(データプレーン)。ただし管理量は選べる
インスタンスタイプは? 1 つ決めるのではなく、候補の範囲を宣言する
どのファミリーを選ぶ? Pod の CPU : メモリ比率に合わせる(汎用なら m 系)
クラスター作成時に聞かれる? 聞かれない。ノードグループ / NodePool の話
最も精度が高い方法は? 負荷試験で 1 Pod の飽和点を測る

EKS 移行でコストが下がるのは、EKS が安いからではない。「実際に必要な量」を細かい粒度で表現できるようになるからだ。その表現方法が requests であり、その値の根拠が実測値になる。

そして面白いことに、requests を決めた時点で連鎖的に多くのことが決まる。

requests を決める
   ├→ Pod 数が決まる(総キャパ ÷ requests)
   ├→ HPA の挙動が決まる(使用率の分母が requests のため)
   ├→ スケジューラーの配置が決まる(requests しか見ないため)
   ├→ Karpenter が選ぶインスタンスが決まる(Pending Pod の requests を合計するため)
   └→ インスタンスファミリーが決まる(CPU : メモリ比率が一致するため)

裏を返せば、実測せずに移行しても何も改善しない。まずは現行環境のメトリクスを取るところから始めるとよい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?