SavingsPlansとRI、3年混乱し続けた僕がやっと整理できた2026年の選び方

「RIを買い続けたのにコスト削減されていない…」そんな経験ありませんか?3年間の失敗と棚卸しを経て気づいた、Savings PlansとRIの本当の使い分け基準をまとめました。

先日、うちのチームのAWS費用レビューをしていたら「RIとSPが混在していて誰も全体像を把握していない」という地獄のような状況に気づいた。RIを3年間買い続けたのに、RIカバレッジ分析を自動化した話でも書いたけど、カバレッジが高くてもコスト削減効果が出ていないという謎現象が起きていた。

今回はその経験を踏まえて、2026年7月時点でどういう基準でSavings PlansとReserved Instancesを選ぶべきか、実際に運用してきた結果から整理してみる。正直まだ検証中の部分もあるけど、迷っているなら参考になると思う。

まず「混乱した理由」を整理する

2023〜2024年ごろ、自分はこういう選び方をしていた。

  • EC2は1年前払いのStandard RI
  • Lambdaは…あ、忘れてた
  • FargateはSavings Plansが使えると知らなかった
  • RDSはMulti-AZ構成なのにSingle-AZ RIを購入して後で後悔

要するに「なんとなくRI買っておけばいい」という感覚で動いていた。これは今振り返ると完全に間違いで、ワークロードの特性を無視していた。

2025年末に月160万円削減できたRIの購入戦略を参考にしながらチームで棚卸しをしたとき、初めてSavings Plansの柔軟性の本当の意味を理解できた気がする。

Savings PlansとReserved Instancesの本質的な違い

2026年7月時点の情報でテーブルにまとめた。

比較軸Savings PlansReserved Instances
適用範囲Compute SP: EC2+Fargate+Lambda、EC2 SP: EC2のみ購入したサービス・リージョン・インスタンスタイプに固定
柔軟性高い(インスタンスファミリー・OS・リージョン変更可)Standard RIは低い、Convertible RIは中程度
最大割引率Compute SPで最大66%、EC2 SPで最大72%Standard RIで最大72%
コミットメント時間あたりの使用金額($/hour)特定インスタンスの予約
Marketplace売却不可Standard RIは可能
1年 vs 3年両方あり両方あり
前払い全前払い/部分前払い/前払いなし全前払い/部分前払い/前払いなし
RDS対応非対応(RDS専用RIが必要)対応
ElastiCache対応非対応対応
Redshift対応非対応対応

これを見て真っ先に気づくのは「Savings Plansは万能じゃない」ということだ。RDS・ElastiCache・Redshiftにはまだ専用RIしかない。これを知らずにSPで全部カバーしようとして失敗した人、結構多いんじゃないかと思う。自分もそのひとりだった。

アーキテクチャ視点での適用対象

実際に運用しているシステムの構成で、どのサービスにSPが効いてどこにRIが必要かを色分けするとこうなる。

graph TB
    subgraph AWS_Account["AWSアカウント"]
        subgraph SP_Coverage["Savings Plans適用範囲"]
            subgraph VPC_Main["VPC (ap-northeast-1)"]
                subgraph AZ_A["AZ-a"]
                    EC2_A["EC2\nc5.2xlarge"]
                    Fargate_A["ECS Fargate\napi-service"]
                end
                subgraph AZ_C["AZ-c"]
                    EC2_C["EC2\nc5.2xlarge"]
                    Fargate_C["ECS Fargate\nworker-service"]
                end
            end
            Lambda["Lambda\n非同期処理"]
        end
        subgraph RI_Coverage["Reserved Instances適用範囲"]
            subgraph DB_Subnet["DBサブネット"]
                RDS_Primary["RDS Aurora\ndb.r6g.2xlarge\n(Primary)"]
                RDS_Replica["RDS Aurora\ndb.r6g.2xlarge\n(Replica)"]
            end
            ElastiCache["ElastiCache Redis\ncache.r6g.large"]
            Redshift["Redshift\nra3.xlplus"]
        end
        S3["S3\nデータレイク"]
    end
    EC2_A --> RDS_Primary
    EC2_C --> RDS_Replica
    Fargate_A --> ElastiCache
    Lambda --> S3
    RDS_Primary --> Redshift

この構成だと「SPでEC2+Fargate+Lambdaをカバー、RIでRDS+ElastiCache+Redshiftをカバー」という二本柱になる。最初にこの整理ができていれば3年間の混乱はなかったと思う。

2026年時点の判断フロー

うちのチームで実際に使っている意思決定フローチャートを公開する。

flowchart TD
    A["コスト最適化対象の\nサービスを特定"] --> B{"対象サービスは?"}
    B --> |"RDS / ElastiCache\n/ Redshift / OpenSearch"| C["RI一択\n(SPは非対応)"]
    B --> |"EC2 / Fargate\n/ Lambda"| D{"ワークロードの\n変更頻度は?"}
    D --> |"高い\n(インスタンスタイプ変更、\nリージョン移行可能性)"| E["Compute Savings Plans\n(最大柔軟性)"]
    D --> |"中程度\n(同一ファミリー内\nのサイズ変更)"| F{"EC2のみ?\nそれともFargate/Lambdaも?"}
    F --> |"EC2のみ"| G["EC2 Instance Savings Plans\n(最大割引)"]
    F --> |"混在"| E
    D --> |"低い\n(3年間固定確実)"| H{"Convertible RIと\nEC2 SPどちらが得か?\n試算する"}
    H --> |"RI有利"| I["Convertible RI\n(売却不要なら)"]
    H --> |"SP有利"| G
    C --> J["期間・前払い方式\nの選定"]
    E --> J
    G --> J
    I --> J
    J --> K{"コミット期間"}
    K --> |"ワークロード安定\n3年以上継続確実"| L["3年・全前払い\n(最大割引)"]
    K --> |"1〜2年程度\nの見通し"| M["1年・部分前払い\n(バランス型)"]
    K --> |"不確実"| N["1年・前払いなし\n(柔軟性優先)"]

ポイントは「柔軟性が必要かどうか」を最初に判断することだ。インスタンスタイプを将来変えるかもしれないならCompute SP一択。変えないと確信があるならEC2 Instance SPかStandard RIで最大割引を狙う。

実際のコスト試算:具体的なシナリオ比較

実際に動かしているc5.2xlarge(ap-northeast-1、Linux)をオンデマンドで動かしている前提で試算してみた。

# コスト比較スクリプト(Boto3 + Pricing API使用)
import boto3
import json

def get_ec2_pricing_comparison():
    """
    c5.2xlarge (ap-northeast-1) のコスト比較
    2026年7月時点の参考値
    """
    # オンデマンド価格(1時間あたり)
    on_demand_hourly = 0.428  # USD/hour (ap-northeast-1, Linux)
    
    # 年間コスト
    hours_per_year = 8760
    annual_on_demand = on_demand_hourly * hours_per_year
    
    pricing = {
        "on_demand": {
            "hourly": on_demand_hourly,
            "annual": annual_on_demand,
            "discount": 0
        },
        "compute_sp_1yr_no_upfront": {
            "hourly": 0.272,  # 約36%割引
            "annual": 0.272 * hours_per_year,
            "discount": 36.4
        },
        "compute_sp_1yr_partial_upfront": {
            "effective_hourly": 0.258,  # 約40%割引
            "annual": 0.258 * hours_per_year,
            "discount": 39.7
        },
        "ec2_sp_1yr_no_upfront": {
            "hourly": 0.244,  # 約43%割引
            "annual": 0.244 * hours_per_year,
            "discount": 43.0
        },
        "standard_ri_1yr_full_upfront": {
            "effective_hourly": 0.238,  # 約44%割引
            "annual": 0.238 * hours_per_year,
            "upfront": 2085,
            "discount": 44.4
        },
        "standard_ri_3yr_full_upfront": {
            "effective_hourly": 0.171,  # 約60%割引
            "annual": 0.171 * hours_per_year,
            "upfront": 4490,
            "discount": 60.0
        },
        "compute_sp_3yr_no_upfront": {
            "hourly": 0.182,  # 約57%割引
            "annual": 0.182 * hours_per_year,
            "discount": 57.5
        }
    }
    
    print(f"オンデマンド年間コスト: ${annual_on_demand:,.0f}")
    print("\n--- 割引後年間コスト比較 ---")
    for plan, data in pricing.items():
        if plan == "on_demand":
            continue
        effective_annual = data.get("annual", data.get("effective_hourly", 0) * hours_per_year)
        savings = annual_on_demand - effective_annual
        print(f"{plan:45s}: ${effective_annual:,.0f}/年 (削減: ${savings:,.0f}, {data['discount']}%割引)")
    
    return pricing

result = get_ec2_pricing_comparison()

実行するとこうなる。

オンデマンド年間コスト: $3,749

--- 割引後年間コスト比較 ---
compute_sp_1yr_no_upfront            : $2,382/年 (削減: $1,367, 36.4%割引)
compute_sp_1yr_partial_upfront       : $2,260/年 (削減: $1,489, 39.7%割引)
ec2_sp_1yr_no_upfront               : $2,137/年 (削減: $1,612, 43.0%割引)
standard_ri_1yr_full_upfront         : $2,085/年 (削減: $1,664, 44.4%割引)
standard_ri_3yr_full_upfront         : $1,497/年 (削減: $2,252, 60.0%割引)
compute_sp_3yr_no_upfront           : $1,594/年 (削減: $2,155, 57.5%割引)

ここで気づくのは「Compute SP 3年 vs Standard RI 3年の差は約5%」という事実だ。柔軟性が欲しいなら3年Compute SPで十分だし、インスタンスタイプを絶対変えないと言い切れるなら3年Standard RIで最大割引を取りに行く、という判断になる。個人的には「5%の差より将来の自由度」派なのでCompute SPを選んでいる。

割引率の視覚化

xychart-beta
    title "c5.2xlarge コスト削減率比較(ap-northeast-1)"
    x-axis ["Compute SP\n1yr NoUp", "Compute SP\n1yr Partial", "EC2 SP\n1yr NoUp", "Standard RI\n1yr Full", "Standard RI\n3yr Full", "Compute SP\n3yr NoUp"]
    y-axis "割引率(%)" 0 --> 70
    bar [36.4, 39.7, 43.0, 44.4, 60.0, 57.5]

実際にやらかした失敗パターンと対策

3年間で踏んだ地雷を正直に書く。

失敗1: Fargate用にRI購入しようとしてECS RIが存在しないと気づく

2023年当時、ECS Fargateのコストを下げようとしてRIを探したが、そもそも存在しない。Fargateに使えるのはCompute Savings Plansだけだ。これを知らずに半年間オンデマンドで動かし続けた。半年分は完全に無駄だった。

失敗2: Compute SPのcommitment金額の計算ミス

Savings Plansは「時間あたりの使用金額をコミットする」仕組みで、EC2台数ではなくドル金額でコミットする。最初これがわからなくて、AWS Cost ExplorerのSP購入推奨を見ずに手で計算しようとして盛大にズレた。Cost ExplorerのAPIで推奨値を取ってくるのが一番確実で、手計算は信用しないほうがいい。

# SP推奨値を取得するコード
import boto3
from datetime import datetime, timedelta

def get_savings_plans_recommendations():
    client = boto3.client('ce', region_name='us-east-1')
    
    response = client.get_savings_plans_purchase_recommendation(
        SavingsPlansType='COMPUTE_SP',
        TermInYears='ONE_YEAR',
        PaymentOption='NO_UPFRONT',
        LookbackPeriodInDays='SIXTY_DAYS'  # 60日間の使用データを元に推奨
    )
    
    rec = response['SavingsPlansPurchaseRecommendation']
    summary = rec['SavingsPlansPurchaseRecommendationSummary']
    
    print(f"推奨コミット金額: ${float(summary['HourlyCommitmentToPurchase']):.4f}/hour")
    print(f"予想年間削減額: ${float(summary['EstimatedSavingsAmount']):,.0f}")
    print(f"予想削減率: {float(summary['EstimatedSavingsPercentage']):.1f}%")
    print(f"推奨カバレッジ: {float(summary['EstimatedOnDemandCostWithCurrentCommitment']):,.0f}")
    
    return summary

get_savings_plans_recommendations()

失敗3: RIの期限切れを見逃す

1年RIを買って、1年後に切れたことに2ヶ月気づかなかった。Cost Anomaly Detectionを設定していたのに、コストが徐々に上がる形だったので検出されなかった。これは地味に痛かった。

対策としてEventBridge + Lambdaで期限90日前にSlack通知する仕組みを入れた。

import boto3
import json
from datetime import datetime, timezone

def check_ri_expiration(event, context):
    """RI期限90日前通知Lambda"""
    client = boto3.client('ec2', region_name='ap-northeast-1')
    
    response = client.describe_reserved_instances(
        Filters=[{'Name': 'state', 'Values': ['active']}]
    )
    
    now = datetime.now(timezone.utc)
    warnings = []
    
    for ri in response['ReservedInstances']:
        end_time = ri['End']
        days_remaining = (end_time - now).days
        
        if days_remaining <= 90:
            warnings.append({
                'ri_id': ri['ReservedInstancesId'],
                'instance_type': ri['InstanceType'],
                'count': ri['InstanceCount'],
                'end_date': end_time.strftime('%Y-%m-%d'),
                'days_remaining': days_remaining
            })
    
    if warnings:
        message = "🚨 RI期限切れ警告\n\n"
        for w in warnings:
            message += f"• {w['instance_type']} x{w['count']}\n"
            message += f"  期限: {w['end_date']} (残{w['days_remaining']}日)\n"
            message += f"  RI ID: {w['ri_id']}\n\n"
        
        # Slack通知(webhook URL は環境変数から)
        import urllib.request
        import os
        
        payload = json.dumps({'text': message}).encode('utf-8')
        req = urllib.request.Request(
            os.environ['SLACK_WEBHOOK_URL'],
            data=payload,
            headers={'Content-Type': 'application/json'}
        )
        urllib.request.urlopen(req)
    
    return {'warnings': len(warnings)}

これで見逃しはゼロになった。地味だけど本当に助かっている。

2026年時点の推奨戦略

最終的にうちのチームが落ち着いた戦略はこうだ。導入前後のコスト推移から先に見てもらうと、効果の規模感が伝わると思う。

xychart-beta
    title "月次コスト推移(SP+RI戦略導入前後)"
    x-axis ["2025-07", "2025-08", "2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06"]
    y-axis "月次費用(万円)" 0 --> 200
    line [168, 172, 175, 170, 165, 142, 128, 122, 118, 115, 112, 110]

戦略導入(2025年12月)から半年で約35%削減できた。内訳はこんな感じだ。

Layer 1: ベースラインコミット(Compute SP 3年・部分前払い)

  • EC2 + Fargate + Lambdaの安定した使用量の70%をカバー
  • 3年コミットで約56〜57%割引
  • 前払い金額と流動性のバランスを取るため「部分前払い」を選択

Layer 2: 変動対応(オンデマンド + Spot)

  • 残り30%はオンデマンドまたはSpot
  • Spot比率が高いワークロードにはEKSのSpot/Karpenter戦略を組み合わせ

Layer 3: マネージドサービス(サービス別RI)

  • RDS: Multi-AZを考慮してインスタンスタイプを慎重に選んで1年RI
  • ElastiCache: 1年RI(キャッシュ要件が変わりやすいので3年はリスク)
  • Redshift: ra3系で3年RI(データウェアハウスは変更頻度が低め)

ここで重要なのは「Compute SPを70%でカバーして30%はバッファにする」という考え方だ。100%コミットすると柔軟性がなくなる。うちは最初100%コミットして、インスタンスタイプ変更のたびに後悔した。「70%ルール」は経験則として今でも守っている。

どちらを選ぶかのシンプルな判断基準

現場では判断が分かれるケースが多いので、最終的な判断軸を整理しておく。

Compute Savings Plansを選ぶケース:

  • FargateやLambdaが混在している
  • 1〜2年以内にインスタンスタイプやリージョンを変更する可能性がある
  • マイクロサービスで多種類のインスタンスを使っている
  • 経験が浅くて「安全牌を選びたい」チーム

EC2 Instance Savings Plansを選ぶケース:

  • EC2のみで、インスタンスファミリーを固定できる
  • Compute SPより数%でも割引率を上げたい
  • 運用が成熟していてインスタンス変更の予定がない

Standard RIを選ぶケース:

  • RDS・ElastiCache・Redshift・OpenSearch(これは必須)
  • EC2で3年間インスタンスタイプを絶対変えないと確信している
  • 将来的にRI Marketplaceでの売却オプションを残しておきたい

Convertible RIを選ぶケース:

  • 正直、Compute SPが登場してからは用途がかなり限られる
  • Standard RIより柔軟だが、Compute SPより割引率が低いケースが多い
  • EC2の一部でどうしてもRI形式で管理したい特殊な事情がある場合

コスト管理自動化の観点ではAWS夜中のコストアラートで飛び起きた話で紹介したBudgetsとAnomaly Detectionとの組み合わせが効く。SPやRIの購入判断だけでなく、実際に効いているかの検証にも必須のツールだ。

まとめ

3年間の失敗から得た教訓をまとめる。

  1. サービスで分類するのが最初の一歩 → RDS/ElastiCache/RedshiftはRI一択。EC2/Fargate/LambdaはSPの検討から始める
  2. Compute SPは「損をしにくい」選択 → 柔軟性が高く、特にFargate・Lambda混在環境では実質一択。割引率がEC2 SPより数%低いのは「保険料」だと思えば納得できる
  3. コミット量は70〜80%に抑える → 100%コミットはリスクが高い。残り20〜30%をバッファにしてSpotやオンデマンドで対応する
  4. RI期限管理は自動化が必須 → EventBridge + Lambdaで90日前通知を入れないと確実に見逃す。これは本当に痛い目を見た
  5. Cost ExplorerのSP推奨値を信用する → 60日間の使用データから算出される推奨値は精度が高い。手計算より正確だし、手計算を信じると盛大にズレる

次のアクションとしては、まずCost ExplorerでSP/RI購入推奨レポートを確認して、現在のカバレッジ率を把握することから始めてほしい。「なんとなくRI買ってた」から「設計してSPを入れる」に変えるだけで、コストとストレスの両方が下がる。

U

Untanbaby

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

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

関連記事