EKSの月120万円請求に震えた話|1年かけてコストを半分以下にした実装記録
「このまま年1400万円かかる」と試算が出て本気で向き合うことに。Karpenter・Spot・VPA・Kubecostなど、失敗も含めた1年分のEKSコスト最適化の記録です。
先日、うちのチームのEKSクラスターで月120万円を超えた請求書が届いて、正直かなり焦った。プロジェクト初期はとにかく動かすことが優先で、ノードサイジングもリソースリクエストもざっくり設定のまま放置してきた。その結果が3桁万円の請求だった。
「このまま行けば年間1400万円のコンピュートコスト」という試算を出したら、上から「半分以下にしろ」と言われて本気で向き合うことになった。それから約1年かけて実装した内容を、失敗も含めて整理したい。ちなみに月100万円が消える恐怖から始まったEKSコスト最適化の実装記でも同じ文脈の話を書いたけど、今回はより具体的なコードと設定ファイルを中心に書く。
現状把握:どこにお金が消えているかを見える化する
まず「どこが無駄なのか」を特定しないと何も始まらない。EKSのコスト分析は意外と面倒で、EC2コストとEKSクラスター管理費(1クラスター$0.10/時)を分けて考える必要がある。
うちのチームで最初にやったのは kubecost の導入だ。2026年時点ではKubecost 2.xが出ていて、Namespace・Deployment・Podレベルのコスト可視化が一段と使いやすくなっている。
# Kubecost 2.x をHelmで導入
helm repo add kubecost https://kubecost.github.io/cost-analyzer/
helm repo update
helm install kubecost kubecost/cost-analyzer \
--namespace kubecost \
--create-namespace \
--set kubecostToken="your-token" \
--set prometheus.nodeExporter.enabled=true \
--set persistentVolume.size=32Gi \
--version 2.3.0
導入して1週間眺めてわかったのは、主な無駄の原因は大きく4つに絞られていた。
| 問題 | 月額コスト影響 | 原因 |
|---|---|---|
| ノードのリソース使用率が平均23% | 約40万円 | リクエスト過剰設定 |
| 深夜バッチ用ノードが常時起動 | 約25万円 | スケールダウン未設定 |
| dev/staging環境が本番同スペック | 約30万円 | 環境差分なし |
| データ転送コスト | 約15万円 | AZ跨ぎ通信 |
使用率23%というのはかなりひどかった。開発初期に「メモリが足りなくなると困る」という恐怖から全Podに requests: memory: 1Gi を付けた結果だ。皆さんのチームでも心当たりありませんか?
# 現在のリソース使用率を確認するワンライナー
kubectl top pods --all-namespaces --sort-by=memory | head -20
# Node別使用率の確認
kubectl describe nodes | grep -A 5 "Allocated resources"
Karpenter v1.xへの移行とSpot/On-Demand混在戦略
2026年現在、KarpenterはV1.1まで進んでいる。以前 Karpenterノードプロビジョニング最適化|2026年EKS完全ガイド でも書いたけど、v1.0以降で NodePool リソースの安定版APIが使えるようになって設計がかなり楽になった。
うちのクラスターでは以下の構成でNodePoolを3種類定義している。
# 本番Webサービス用:On-Demand優先、Spot混在
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: production-web
spec:
template:
metadata:
labels:
workload-type: web
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand", "spot"]
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["m6i", "m6g", "m7i", "m7g", "c6i", "c7i"]
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["xlarge", "2xlarge", "4xlarge"]
nodeClassRef:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenUnderutilized
consolidateAfter: 30s
budgets:
- nodes: "20%" # 同時disruption上限
limits:
cpu: 1000
memory: 4000Gi
---
# バッチ処理用:Spot特化
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: batch-spot
spec:
template:
metadata:
labels:
workload-type: batch
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["c6i", "c7i", "c6g", "c7g", "m6i", "m7i"]
taints:
- key: workload-type
value: batch
effect: NoSchedule
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 60s
Spotインスタンスの選択肢を広げることが重要で、1〜2ファミリーに絞るとSpot中断率が上がる。うちではm6i/m6g/m7i/m7gを候補にすることで中断率を月1〜2回程度に抑えられている。
arm64(Graviton)との混在は最初は怖かったけど、実際に試してみると特定のPythonライブラリ(C拡張系)以外はほぼ問題なかった。Graviton3はx86比でコストパフォーマンスが約40%良いので、試す価値は絶対にある。
# Graviton優先のアフィニティ設定
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 70
preference:
matchExpressions:
- key: kubernetes.io/arch
operator: In
values: ["arm64"]
VPA(Vertical Pod Autoscaler)とリソースリクエストの最適化
これが地味に一番効いた。最初のリソース使用率23%という数字を改善するためにVPAを導入した。2026年現在、VPA 1.2がリリースされていてRecommenderの精度が上がっている。
# VPA: Recommendモードで2週間様子見
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-server-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
updatePolicy:
updateMode: "Off" # まずRecommendのみ
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
cpu: 100m
memory: 128Mi
maxAllowed:
cpu: 4
memory: 8Gi
controlledResources: ["cpu", "memory"]
updateMode: "Off" で2週間観察してから updateMode: "Auto" に切り替えた。推奨値を見て最初はびっくりした。requests: memory: 1Gi で設定していたサービスが実態は 130Mi 程度しか使っていないケースがいくつもあったからだ。
ただしVPAにはHPAと組み合わせる場合の制約がある。CPU/Memoryの両方をVPAで管理しつつHPAでスケールアウトもしたい場合、2026年現在では KEDA + VPA の組み合わせが有力だ。正直まだ検証中の部分もあるけど、KEDAのScaledObjectにVPA設定を組み合わせると干渉を避けやすい。
# VPA推奨値の確認コマンド
kubectl get vpa api-server-vpa -n production -o jsonpath='{.status.recommendation}' | jq .
実際の結果がこちら:
{
"containerRecommendations": [
{
"containerName": "api-server",
"lowerBound": {"cpu": "87m", "memory": "128Mi"},
"target": {"cpu": "145m", "memory": "256Mi"},
"upperBound": {"cpu": "523m", "memory": "512Mi"}
}
]
}
target 値を参考に全サービスのリクエストを見直した結果、ノード使用率が23%→61%まで改善した。
AWS構成図:コスト最適化後のEKSクラスター構成
graph TB
subgraph AWS_Account["AWS Account"]
subgraph VPC["VPC (10.0.0.0/16)"]
subgraph AZ_A["AZ: ap-northeast-1a"]
subgraph NG_A["Node Group A"]
OD_A["On-Demand Nodes\nm7i.xlarge / m7g.xlarge"]
SPOT_A["Spot Nodes\nMulti-instance types"]
end
end
subgraph AZ_B["AZ: ap-northeast-1c"]
subgraph NG_B["Node Group B"]
OD_B["On-Demand Nodes\nm7i.xlarge / m7g.xlarge"]
SPOT_B["Spot Nodes\nMulti-instance types"]
end
end
subgraph ControlPlane["EKS Control Plane"]
KARPENTER["Karpenter v1.1\nNodePool管理"]
VPA["VPA 1.2\nリソース最適化"]
KEDA["KEDA 2.14\nイベント駆動スケール"]
end
subgraph CostTools["コスト管理"]
KUBECOST["Kubecost 2.x\nNamespace別コスト"]
CW["CloudWatch\nContainer Insights"]
end
end
subgraph Managed["マネージドサービス"]
ECR["ECR\nコンテナレジストリ"]
S3_CACHE["S3\nBuildキャッシュ"]
SAVINGS["Compute Savings Plans\n1年コミット"]
end
subgraph CostOptimize["コスト最適化設定"]
SP["Spot Interruption Handler\naws-node-termination-handler"]
CRON["KEDA CronScaler\n深夜スケールダウン"]
end
end
KARPENTER --> OD_A
KARPENTER --> SPOT_A
KARPENTER --> OD_B
KARPENTER --> SPOT_B
VPA --> ControlPlane
KEDA --> CRON
SP --> SPOT_A
SP --> SPOT_B
KUBECOST --> CW
SAVINGS --> OD_A
SAVINGS --> OD_B
KEDA + CronScalerで非本番環境を夜間シャットダウン
月25万円を消費していた「深夜に誰も使っていないバッチ用ノード」の問題は、KEDAのCronScalerで解決した。
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: batch-worker-scaler
namespace: production
spec:
scaleTargetRef:
name: batch-worker
minReplicaCount: 0 # ゼロスケール可能
maxReplicaCount: 20
triggers:
- type: cron
metadata:
timezone: Asia/Tokyo
start: "0 8 * * 1-5" # 平日朝8時スタート
end: "0 22 * * 1-5" # 平日夜10時終了
desiredReplicas: "3"
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.ap-northeast-1.amazonaws.com/xxxx/batch-jobs
queueLength: "10"
awsRegion: ap-northeast-1
dev/staging環境はさらに積極的にゼロスケールしている。
# staging環境: 平日9-19時のみ起動
triggers:
- type: cron
metadata:
timezone: Asia/Tokyo
start: "0 9 * * 1-5"
end: "0 19 * * 1-5"
desiredReplicas: "2"
これだけでdev/stagingのEC2コストが月30万円→8万円に落ちた。夜間や週末に誰も使っていなかったのに常時起動していたというのは、改めて見ると本当にもったいなかった。「ずっとやろうと思っていた」案件をすぐやるべきだった、と心底思った瞬間だ。
コスト削減の成果と1年後の数字
各施策の効果を時系列で見ると:
xychart-beta
title "EKSクラスター月額コスト推移(万円)"
x-axis ["2025-07", "2025-09", "2025-11", "2026-01", "2026-03", "2026-05", "2026-07"]
y-axis "月額コスト(万円)" 0 --> 130
line [120, 105, 87, 68, 55, 51, 48]
120万円→48万円という結果になった。年換算で864万円の削減だ。施策別の内訳はこう:
| 施策 | 削減額(月) | 工数 | 難易度 |
|---|---|---|---|
| VPAによるリソース最適化 | 約25万円 | 1週間 | 低 |
| Spot混在比率向上(30%→65%) | 約20万円 | 2週間 | 中 |
| 非本番環境夜間停止 | 約22万円 | 3日 | 低 |
| Graviton3移行(一部) | 約10万円 | 3週間 | 中 |
| Karpenter consolidation最適化 | 約8万円 | 1週間 | 中 |
| AZ間通信削減 | 約7万円 | 2週間 | 高 |
この表を見ると、難易度「低」の施策だけで月47万円削減できる計算になる。まず難易度の低いところから手をつけるのが正解で、個人的にはVPAと夜間停止を最初の2週間でやり切れたのが大きかった。
AZ間通信の削減については、サービスのアフィニティ設定とTopology Spread Constraintsを組み合わせて、同一AZ内通信を優先するようにした。これは実装が地味に複雑で、インシデント対応の最新ベストプラクティス2026|DevOps・SRE必読で触れたような本番停止リスクも考慮しながら段階的に進める必要があった。
# Topology Spread Constraints で同一AZ優先
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api-server
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: ["api-server"]
topologyKey: topology.kubernetes.io/zone
もう一点、コンテナセキュリティ完全ガイド2026|eBPF・SBOM・サプライチェーン対策で書いたSBOM・脆弱性スキャンのコスト面も見直した。ECRのスキャンを全イメージ毎日実行していたのをPUSH時のみに変更し、Inspector v2の対象をCritical/High限定にすることで月3万円ほど削減できた。小さいけど、こういう積み重ねが地味に効く。
Compute Savings PlansについてはOn-Demand比30%程度を1年コミットで購入している。Savings Plans vs Reserved Instances|2026年AWSコスト最適化判断基準でも書いたけど、Karpenterで動的プロビジョニングしている環境ではReserved InstancesよりCompute Savings Plansの柔軟性が効く。Graviton移行を進めているので特にそれが重要だった。
まとめ
1年かけたEKSコスト最適化で見えた要点を整理する。
- まずKubecostで現状可視化:何が無駄かわからないまま最適化しようとすると空振りする。最初の2週間はひたすら現状把握に使って正解だった
- VPAは最も費用対効果が高い:設定コストが低いわりに削減額が大きく、全チームに最初に勧めたい施策。
updateMode: "Off"で安全に始められる - Spotはインスタンスファミリーを4種類以上広げる:選択肢が少ないと中断率が上がる。m6i/m6g/m7i/m7gの4種類で安定した
- 非本番環境の夜間停止は即効性あり:KEDA CronScalerで1日の実装。「ずっとやろうと思っていた」案件はすぐやるべきだった
- Graviton移行は段階的に:一部のPython/Javaライブラリでarm64非対応があるため、全移行前に必ずCI環境で動作確認する
次のアクションとしては、プロファイリングベースのリソース自動調整(VPA Auto + KEDA組み合わせ)の本番化と、EKS Auto Modeへの移行評価を進める予定だ。EKS Auto Mode完全ガイド2026|運用負荷90%削減のサーバーレスK8sで書いた内容を自分たちの環境でどこまで適用できるか、正直まだ検証中だけど年内には結論を出したい。