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つのレイヤーで進めます:

  1. インスタンスレベル:Spot/On-Demand の最適なミックス
  2. ワークロードレベル:リソースリクエストの精密化
  3. クラスタレベル:ノード自動スケーリングと廃止ノード削除
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/h80%⭐⭐⭐⭐⭐
m5.2xlarge$0.384/h$0.0960/h75%⭐⭐⭐⭐⭐
c5.2xlarge$0.34/h$0.0750/h78%⭐⭐⭐⭐
r5.2xlarge$0.504/h$0.1260/h75%⭐⭐⭐⭐
i3.2xlarge$1.824/h$0.3648/h80%⭐⭐⭐

*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つの柱で実現できます:

  1. Spot Instance + karpenter:70%のコスト削減効果
  2. リソース推奨ツール(Kubecost):実測値ベースの精密化
  3. 自動スケーリング + スケジュール:非営業時間の自動縮約

**月額コスト削減目標:30~50%**は、適切な実装で十分達成可能です。重要なのは、単なる「安さ」ではなく「安定性とのバランス」をとることです。本ガイドの実装例を参考に、段階的にロールアウトしてください。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事