EKS コスト最適化2026|Spot/On-Demand戦略とKarpenter活用
EKSのコスト30~50%削減を実現する最新戦略。Spot/On-Demand混合戦略、Karpenterによる動的スケーリング、リソース最適化を実装レベルで解説。2026年ベストプラクティス完全ガイド。
EKS コンテナコスト最適化2026|Spot/On-Demand戦略とリソース自動調整
はじめに:2026年EKSコスト課題の現実
2026年4月時点で、企業のEKS運用コストは引き続き大きな課題となっています。オンデマンドインスタンスのみで運用すると月額コストが30~50%割高になるという課題は依然解決されておらず、実装レベルでの工夫が求められています。
2025年から2026年にかけての動向:
- karpenter v0.36以降での動的スケーリング機能強化
- AWS Spot Instance割引率の最適化(最大90%削減)
- リソースリクエストの自動推奨ツール成熟化
- Mixed Instance Policy活用による自動フェイルオーバーの安定化
このガイドでは、実装レベルでのコスト最適化戦略と、2026年時点の最新ベストプラクティスを、設定ファイルとコード例を交えて解説します。
EKS コスト最適化の全体戦略
EKSのコスト最適化は、大きく3つのレイヤーで進めます:
- インスタンスレベル:Spot/On-Demand の最適なミックス
- ワークロードレベル:リソースリクエストの精密化
- クラスタレベル:ノード自動スケーリングと廃止ノード削除
flowchart TD
A["EKS Cluster<br/>運用開始"] --> B["1️⃣ Instance Mix Strategy<br/>Spot 70% + On-Demand 30%"]
B --> C["2️⃣ Resource Request<br/>最適化・自動推奨"]
C --> D["3️⃣ Node Autoscaling<br/>karpenter活用"]
D --> E["Cost Monitoring<br/>CloudWatch + Kubecost"]
E --> F["目標達成<br/>30~50%削減"]
style A fill:#e1f5ff
style F fill:#c8e6c9
2026年時点での現実的なコスト構造
pie title "2026年 EKS月額コスト内訳(最適化前)"
"EC2 On-Demand" : 55
"NAT Gateway + Data Transfer" : 20
"Storage (EBS)" : 15
"Load Balancer" : 7
"その他" : 3
EC2コストが55%を占めるため、ここへの施策がROIが最高になります。
AWS構成図:最適化されたEKS環境
以下は、Spot/On-Demand混合構成の推奨アーキテクチャです:
- Karpenter Controller:2つのAZにデプロイされたSpot/On-Demandノードプールを自動管理
- Spot Instances(70%):コスト効率の高いSpotインスタンスを優先配置
- On-Demand(30%):ワークロード中断時のフェイルオーバー用
- EBS + NAT最適化:gp3ストレージとNAT Gateway高可用性設定
- Kubecost Monitoring:コスト可視化と推奨値提示
戦略1:Spot Instance活用による30~50%削減
2026年のSpot Instanceの現状と割引率
| インスタンスタイプ | On-Demand価格 | Spot価格 | 割引率 | 利用推奨度 |
|---|---|---|---|---|
| t3.xlarge | $0.1664/h | $0.0332/h | 80% | ⭐⭐⭐⭐⭐ |
| m5.2xlarge | $0.384/h | $0.0960/h | 75% | ⭐⭐⭐⭐⭐ |
| c5.2xlarge | $0.34/h | $0.0750/h | 78% | ⭐⭐⭐⭐ |
| r5.2xlarge | $0.504/h | $0.1260/h | 75% | ⭐⭐⭐⭐ |
| i3.2xlarge | $1.824/h | $0.3648/h | 80% | ⭐⭐⭐ |
*2026年4月時点の東京リージョン(ap-northeast-1)の相場
Karpenter v0.36以降の設定例
2026年時点でのkarpenterは、Mixed Instance Policyをより直感的に定義できるようになっています。
# karpenter/values.yaml - 2026年推奨設定
apiVersion: helm.sh/v1
name: karpenter
namespace: karpenter
---
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: spot-optimized
spec:
# テンプレート定義
template:
spec:
requirements:
# インスタンスファミリーの多様化(割込み対策)
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: node.kubernetes.io/instance-type
operator: In
values:
- "t3.xlarge"
- "t3a.xlarge" # Spot割込み時の代替
- "m5.xlarge"
- "m5a.xlarge"
- "c5.xlarge"
- "c5a.xlarge"
- key: karpenter.sh/weighted-priority
operator: In
values:
- "100" # t3系を優先
- "90" # t3a系
- "80" # m5系
nodeClassRef:
name: default
# スケーリング定義
limits:
cpu: 1000
memory: 1000Gi
# 拡張・縮約ポリシー
disruption:
consolidateAfter: 30s
expireAfter: 2592000s # 30日で強制更新
budgets:
- nodes: "10%" # 一度に削除できるノード数
duration: 5m
- nodes: "0"
duration: 9h-17h
schedule: "0 9 * * mon-fri"
timezone: "Asia/Tokyo"
ttlSecondsAfterEmpty: 30
---
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: on-demand-fallback
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: node.kubernetes.io/instance-type
operator: In
values:
- "t3.xlarge"
- "m5.xlarge"
- "c5.xlarge"
nodeClassRef:
name: default
limits:
cpu: 200
memory: 200Gi
# オンデマンドはSpot割込み時のみ起動
weight: 50 # Spotプールが優先
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2
role: "KarpenterNodeRole"
subnetSelector:
karpenter.sh/discovery: "true"
securityGroupSelector:
karpenter.sh/discovery: "true"
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3 # gp2より20%コスト削減
deleteOnTermination: true
userData: |
#!/bin/bash
# 2026年推奨:起動時の最適化
echo "vm.max_map_count=262144" >> /etc/sysctl.conf
sysctl -p
tags:
Environment: production
ManagedBy: karpenter
リソースリクエストの適切な設定
Spot割込み対策として、**リソースリクエストは実測値の120~130%**に設定します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
# Spot割込みに強いPodDisruptionBudgetを設定
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- api
topologyKey: kubernetes.io/hostname
# Spot割込み時の準備期間:60秒
terminationGracePeriodSeconds: 60
containers:
- name: api
image: myregistry.azurecr.io/api:1.2.3
resources:
# 実測値:CPU 0.5, Memory 512Mi → 130%で設定
requests:
cpu: 650m # 500m * 1.3
memory: 665Mi # 512Mi * 1.3
limits:
cpu: 1000m
memory: 1Gi
# リソース利用監視
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
戦略2:リソースリクエストの自動推奨と最適化
Kubeflow + Prometheus メトリクス分析
2026年時点で、リソースリクエストの自動推奨ツールが成熟化しています。
# resource_optimizer.py - 2026年版
import boto3
import prometheus_client
from typing import Dict, Tuple
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ResourceOptimizer:
def __init__(self, prometheus_url: str, namespace: str):
self.prom = prometheus_client.PrometheusConnect(
url=prometheus_url,
disable_ssl=True
)
self.namespace = namespace
self.cloudwatch = boto3.client('cloudwatch')
def analyze_pod_usage(
self,
pod_name: str,
lookback_hours: int = 168
) -> Dict[str, float]:
"""Prometheusメトリクスから実測値を取得"""
# CPU使用率の99パーセンタイル値を取得
cpu_query = f'''
histogram_quantile(
0.99,
rate(container_cpu_usage_seconds_total{{
pod="{pod_name}",
namespace="{self.namespace}"
}}[5m])
)
'''
# メモリ使用量の99パーセンタイル値を取得
memory_query = f'''
histogram_quantile(
0.99,
container_memory_working_set_bytes{{
pod="{pod_name}",
namespace="{self.namespace}"
}}
)
'''
try:
cpu_result = self.prom.custom_query(cpu_query)
mem_result = self.prom.custom_query(memory_query)
cpu_cores = float(cpu_result[0]['value'][1]) if cpu_result else 0.5
memory_bytes = float(mem_result[0]['value'][1]) if mem_result else 512e6
return {
'cpu_cores': cpu_cores,
'memory_mi': memory_bytes / 1e6,
'p99_cpu': cpu_cores,
'p99_memory': memory_bytes / 1e6
}
except Exception as e:
logger.error(f"Failed to query metrics: {e}")
return {'cpu_cores': 0.5, 'memory_mi': 512}
def calculate_optimal_request(
self,
measured: Dict[str, float],
safety_margin: float = 1.3
) -> Tuple[str, str]:
"""実測値から推奨リクエストを算出(30%マージン)"""
cpu_request = measured['p99_cpu'] * safety_margin
memory_request = measured['p99_memory'] * safety_margin
# 標準値に丸める(karpenterフレンドリー)
cpu_normalized = self._normalize_cpu(cpu_request)
memory_normalized = self._normalize_memory(memory_request)
return cpu_normalized, memory_normalized
def _normalize_cpu(self, cpu: float) -> str:
"""CPUを正規化(250mずつ)"""
import math
normalized = math.ceil(cpu * 1000 / 250) * 250
return f"{normalized}m"
def _normalize_memory(self, memory_mi: float) -> str:
"""メモリを正規化(128Miずつ)"""
import math
normalized = math.ceil(memory_mi / 128) * 128
return f"{int(normalized)}Mi"
def publish_to_cloudwatch(
self,
pod_name: str,
cpu_request: str,
memory_request: str
):
"""推奨値をCloudWatchカスタムメトリクスに発行"""
cpu_value = float(cpu_request.replace('m', '')) / 1000
memory_value = float(memory_request.replace('Mi', ''))
self.cloudwatch.put_metric_data(
Namespace='EKS/ResourceOptimization',
MetricData=[
{
'MetricName': 'RecommendedCPURequest',
'Value': cpu_value,
'Unit': 'Count',
'Dimensions': [
{'Name': 'PodName', 'Value': pod_name},
{'Name': 'Namespace', 'Value': self.namespace}
]
},
{
'MetricName': 'RecommendedMemoryRequest',
'Value': memory_value,
'Unit': 'Megabytes',
'Dimensions': [
{'Name': 'PodName', 'Value': pod_name},
{'Name': 'Namespace', 'Value': self.namespace}
]
}
]
)
# 使用例
if __name__ == "__main__":
optimizer = ResourceOptimizer(
prometheus_url="http://prometheus.monitoring:9090",
namespace="default"
)
# api-service ポッドの分析
measured = optimizer.analyze_pod_usage("api-service-abc123", lookback_hours=168)
cpu_req, mem_req = optimizer.calculate_optimal_request(measured)
logger.info(f"Pod: api-service")
logger.info(f"実測値(P99): CPU {measured['p99_cpu']:.3f} cores, Memory {measured['p99_memory']:.0f}Mi")
logger.info(f"推奨リクエスト: CPU {cpu_req}, Memory {mem_req}")
optimizer.publish_to_cloudwatch("api-service", cpu_req, mem_req)
Kubecostダッシュボードの活用
2026年時点で、Kubecostはkarpenterと完全に統合されており、リアルタイムでコスト推奨値を提示します。
# Kubecostをインストール
helm repo add kubecost https://kubecost.github.io/cost-analyzer/
helm install kubecost kubecost/cost-analyzer \
--namespace kubecost \
--create-namespace \
--values - <<EOF
kubecostModel:
warmCache: true
warmSavingsCache: true
promptail:
enabled: true
prometheus:
server:
persistentVolume:
enabled: true
size: 100Gi
storageClass: gp3 # gp2より安い
EOF
# Kubecostダッシュボードへのアクセス
kubectl port-forward -n kubecost svc/kubecost-cost-analyzer 9090:9090
# http://localhost:9090 で右上「Savings」タブから推奨値を確認
戦略3:ノード自動スケーリング最適化
Karpenterの拡張・縮約スケジュール
2026年4月の本番運用では、営業時間/非営業時間でのスケーリングポリシーが必須です。
# karpenter-disruption-schedule.yaml - スケジュール付き拡張・縮約
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: business-hours-pool
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["t3.xlarge", "m5.xlarge"]
nodeClassRef:
name: default
limits:
cpu: 500
memory: 500Gi
# 営業時間スケジュール
disruption:
consolidateAfter: 1m
expireAfter: 720h # 30日
budgets:
# 営業時間(9:00-17:00 月-金):保守作業をしない
- nodes: "0"
duration: 9h-17h
schedule: "0 9 * * mon-fri"
timezone: "Asia/Tokyo"
# 営業外(17:00-21:00 月-金):最大20%のノードを削除可能
- nodes: "20%"
duration: 5h
schedule: "0 17 * * mon-fri"
timezone: "Asia/Tokyo"
# 夜間(21:00-9:00):最大50%削除可能
- nodes: "50%"
duration: 12h
schedule: "0 21 * * *"
timezone: "Asia/Tokyo"
# 週末:最大100%削除可能(最小稼働ノードのみ残す)
- nodes: "100%"
duration: 48h
schedule: "0 0 * * sat"
timezone: "Asia/Tokyo"
ttlSecondsAfterEmpty: 30
ttlSecondsUntilExpired: 2592000
コスト監視と最適化ループ
EKSコスト最適化は継続的なプロセスです。月次で以下のサイクルを実施してください。
flowchart LR
A["1. Kubecost<br/>レポート取得"] --> B["2. 異常値検知<br/>(閾値比較)"]
B --> C["3. ワークロード<br/>分析"]
C --> D["4. リソース<br/>リクエスト調整"]
D --> E["5. Karpenter<br/>設定最適化"]
E --> F["6. コスト<br/>検証"]
F --> |満足| G["✓ 改善完了"]
F --> |未改善| A
style G fill:#c8e6c9
CloudWatch Alarmの設定
# cloudwatch-alarms.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: eks-cost-alarms
namespace: monitoring
data:
create_alarms.py: |
#!/usr/bin/env python3
import boto3
import json
cloudwatch = boto3.client('cloudwatch')
# ECS月額コスト閾値アラーム
cloudwatch.put_metric_alarm(
AlarmName='EKS-Monthly-Cost-Threshold',
ComparisonOperator='GreaterThanThreshold',
EvaluationPeriods=1,
MetricName='EstimatedCharges',
Namespace='AWS/Billing',
Period=3600,
Statistic='Maximum',
Threshold=50000, # 月50万円
ActionsEnabled=True,
AlarmActions=['arn:aws:sns:ap-northeast-1:123456789:eks-alerts'],
Dimensions=[
{
'Name': 'Currency',
'Value': 'JPY'
}
]
)
# Spot割込み率アラーム
cloudwatch.put_metric_alarm(
AlarmName='EKS-Spot-Interruption-Rate-High',
ComparisonOperator='GreaterThanThreshold',
EvaluationPeriods=3,
MetricName='SpotInstanceInterruptionRate',
Namespace='EKS/Karpenter',
Period=300,
Statistic='Average',
Threshold=5.0, # 5%以上の中断率
ActionsEnabled=True,
AlarmActions=['arn:aws:sns:ap-northeast-1:123456789:eks-alerts']
)
print("Alarms created successfully")
実装チェックリスト
以下を確認して本番環境へのロールアウトを進めてください:
checklist title "EKS コスト最適化 実装チェックリスト"
- [ ] Karpenter v0.36以降をインストール
- [ ] Spot/On-Demand混合プール設定(70:30比率)
- [ ] Podリソースリクエスト監査完了
- [ ] リソース推奨値ツール(Kubecost)導入
- [ ] CloudWatch Alarmとログ設定
- [ ] Spot割込み対応(PodDisruptionBudget設定)
- [ ] 本番環境での1週間テスト運用
- [ ] チーム教育・ドキュメント作成
- [ ] 月次監視ダッシュボード構築
- [ ] 予算最適化レビュー(月1回)
期待されるコスト削減効果
bar
title "EKS最適化による月額コスト削減(概算)"
x-axis [現状, Spot導入, リソース最適化, 全施策適用]
y-axis "月額コスト(万円)" 0 100
bar [100, 70, 50, 35]
line [100, 70, 50, 35]
| 施策 | 削減率 | 実装期間 | 効果実感 |
|---|---|---|---|
| Spot Instance活用(70%) | 20~25% | 2-3週間 | 即座 |
| リソースリクエスト最適化 | 10~15% | 4週間 | 段階的 |
| ノード自動スケーリング | 5~10% | 2週間 | 継続的 |
| 合計削減 | 35~50% | 6-8週間 | 顕著 |
まとめ
2026年のEKSコスト最適化は、以下3つの柱で実現できます:
- Spot Instance + karpenter:70%のコスト削減効果
- リソース推奨ツール(Kubecost):実測値ベースの精密化
- 自動スケーリング + スケジュール:非営業時間の自動縮約
**月額コスト削減目標:30~50%**は、適切な実装で十分達成可能です。重要なのは、単なる「安さ」ではなく「安定性とのバランス」をとることです。本ガイドの実装例を参考に、段階的にロールアウトしてください。