はじめに
「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 は、あえて設定しない
なぜメモリの limits は requests と同じ値にするのか
メモリの limits を requests より大きくすると、「普段は 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 は requests と limits の設定内容から、Pod に QoS クラス を自動で割り当てる。これは Node のメモリが逼迫したとき、どの Pod から先に追い出されるかの優先順位になる。
| QoS クラス | 条件 | 追い出されやすさ |
|---|---|---|
| Guaranteed | すべてのコンテナで CPU とメモリの両方 が requests == limits
|
最後まで残る |
| Burstable |
requests は設定済みだが Guaranteed の条件を満たさない |
中間 |
| BestEffort |
requests も limits も未設定 |
最初に殺される |
⚠️ 注意点として、メモリだけ 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
whenUnsatisfiableをDoNotScheduleにすると条件を満たせないとき 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 のキュー滞留数 | ワーカー系の処理 |
| アプリ独自のメトリクス | 同時接続数など |
これらを使うには KEDA や Prometheus 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 hpaにmissing 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 1Gi は 1 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 上限 |
✅ 余裕がある | ❌ 先に上限に当たる |
実務的な落としどころは xlarge 〜 2xlarge 前後になる。第 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に含めた -
メモリの
limitsをrequestsと同値にした - 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上限に引っかからないことを確認した -
preStopとterminationGracePeriodSecondsを設定し、スケールイン時の取りこぼしを防いだ
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 : メモリ比率が一致するため)
裏を返せば、実測せずに移行しても何も改善しない。まずは現行環境のメトリクスを取るところから始めるとよい。