RIを3年間失敗し続けた話と、月160万円削減できた購入戦略

「とりあえず1年Standard」でRI費用を垂れ流した経験はありませんか?月530万円の請求書をきっかけに見直したカバレッジ分析と購入手順を具体的に共有します。

RI購入で失敗し続けた話から始める

正直に言うと、RIをちゃんと運用できるようになるまでに3年かかった。最初の頃は「とりあえず1年のStandardで買っておけばいいか」みたいなノリで購入して、気づいたら全然使われていないRIが毎月費用を垂れ流している状態になっていた。あれは本当に痛かった。

きっかけは去年の夏、月次コストレビューで請求書が530万円を突破したこと。マネージャーから「さすがに調べてくれ」と言われて、本腰を入れてRI戦略を見直した。結果的に3ヶ月で約160万円/月削減できたので、今回はその具体的な手順を共有したい。

ちなみに月額500万円の請求書を見て動いたAWS費用削減の記録という記事でもコスト削減全体の話は書いたけど、今回はRIに絞ってもっと詳細に書く。

RIカバレッジ分析:まず「見える化」から始める

RI購入の前に必ずやるべきなのが現状のカバレッジ分析だ。これを飛ばして購入すると高確率で無駄になる。順番を守るだけで、後の失敗がかなり減る。

AWS Cost Explorer でのカバレッジ確認

まず Cost Explorer の「RI Coverage」レポートを開く。2026年現在、インターフェースが少し変わって「Reservation Coverage」として統合されているので注意。

# AWS CLIでカバレッジを取得する場合
aws ce get-reservation-coverage \
  --time-period Start=2026-04-01,End=2026-05-01 \
  --granularity MONTHLY \
  --group-by Type=DIMENSION,Key=SERVICE \
  --metrics "CoverageHours"

実際に出力されるJSONはこんな感じ:

{
  "CoveragesByTime": [
    {
      "TimePeriod": {
        "Start": "2026-04-01",
        "End": "2026-05-01"
      },
      "Groups": [
        {
          "Attributes": {
            "SERVICE": "Amazon EC2"
          },
          "Coverage": {
            "CoverageHours": {
              "OnDemandHours": "12450",
              "ReservedHours": "8760",
              "TotalRunningHours": "21210",
              "CoverageHoursPercentage": "41.30"
            },
            "CoverageNormalizedUnits": {
              "CoverageNormalizedUnitsPercentage": "38.72"
            }
          }
        },
        {
          "Attributes": {
            "SERVICE": "Amazon RDS"
          },
          "Coverage": {
            "CoverageHours": {
              "OnDemandHours": "3500",
              "ReservedHours": "5256",
              "TotalRunningHours": "8756",
              "CoverageHoursPercentage": "60.03"
            }
          }
        }
      ]
    }
  ]
}

これを見ると、EC2のカバレッジが41%しかない。目標は70〜80%なので、かなり改善余地がある状態だった。RDSは60%でまあまあだけど、もう少し上げたい。

カバレッジの可視化

分析したデータをグラフにするとこんな感じになった:

xychart-beta
  title "サービス別RIカバレッジ率(2026年4月)"
  x-axis ["EC2", "RDS", "ElastiCache", "Redshift", "OpenSearch"]
  y-axis "カバレッジ率 (%)" 0 --> 100
  bar [41, 60, 25, 80, 15]
  line [75, 75, 75, 75, 75]

このグラフで一目瞭然だけど、ElastiCacheとOpenSearchのカバレッジが壊滅的だった。特にOpenSearchは15%しかカバーされておらず、ほぼオンデマンドで動いていた。個人的にはこれが一番の盲点で、「RI = EC2のもの」という思い込みがずっとあったのが正直なところ。EC2ばかり意識してDB系を完全に放置していたのはちょっと恥ずかしい。

どのインスタンスが「RI購入対象」か絞り込む

カバレッジを見た後は、具体的にどのインスタンスを買うべきか分析する。重要なのは稼働時間の安定性だ。「常時起動しているか」をちゃんと数字で確認しないと、後で後悔することになる。

import boto3
from datetime import datetime, timedelta

def analyze_instance_stability(instance_id: str, days: int = 90) -> dict:
    """
    過去90日間の稼働パターンを分析してRI購入適合性を判定
    """
    ce = boto3.client('ce', region_name='ap-northeast-1')
    
    end_date = datetime.now().strftime('%Y-%m-%d')
    start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
    
    response = ce.get_cost_and_usage(
        TimePeriod={'Start': start_date, 'End': end_date},
        Granularity='DAILY',
        Filter={
            'Dimensions': {
                'Key': 'RESOURCE_ID',
                'Values': [instance_id]
            }
        },
        Metrics=['UsageQuantity'],
        GroupBy=[{'Type': 'DIMENSION', 'Key': 'USAGE_TYPE'}]
    )
    
    daily_hours = []
    for result in response['ResultsByTime']:
        for group in result['Groups']:
            if 'BoxUsage' in group['Keys'][0]:
                hours = float(group['Metrics']['UsageQuantity']['Amount'])
                daily_hours.append(hours)
    
    if not daily_hours:
        return {'suitable_for_ri': False, 'reason': 'データ不足'}
    
    avg_hours = sum(daily_hours) / len(daily_hours)
    min_hours = min(daily_hours)
    utilization_rate = min_hours / 24  # 最悪日でも何割稼働しているか
    
    return {
        'instance_id': instance_id,
        'avg_daily_hours': round(avg_hours, 2),
        'min_daily_hours': round(min_hours, 2),
        'utilization_rate': round(utilization_rate * 100, 1),
        'suitable_for_ri': utilization_rate >= 0.7,  # 70%以上なら購入対象
        'recommended_type': 'Standard 1yr' if utilization_rate >= 0.9 else 'Convertible 1yr'
    }

# 使い方
result = analyze_instance_stability('i-0abc123def456789')
print(result)
# 出力例:
# {
#   'instance_id': 'i-0abc123def456789',
#   'avg_daily_hours': 23.8,
#   'min_daily_hours': 22.1,
#   'utilization_rate': 92.1,
#   'suitable_for_ri': True,
#   'recommended_type': 'Standard 1yr'
# }

このスクリプトを全インスタンスに実行して、RI購入候補リストを作った。地味な作業だけど、これがないとホントに当てずっぽうになる。数十台あると結構な時間がかかるが、ここをサボると後で必ず後悔する。

Savings Plans vs RI:2026年時点での判断基準

「Savings PlansとRIって結局どっちがいいの?」という質問、チームでも何度も出た。先に結論を言うと、使い分けが正解で、どちらか一方に全振りするのは危険だと思っている。

Savings Plans vs Reserved Instances の比較記事でも詳しく書いているけど、改めて自分たちの判断基準を整理しておく。

比較項目Compute Savings PlansEC2 Instance SPStandard RIConvertible RI
割引率(最大)〜66%〜72%〜72%〜66%
柔軟性最高(OS・ファミリー変更可)高(同ファミリー内)低(厳格に固定)中(変換可能)
対象サービスEC2・Lambda・FargateEC2のみEC2/RDS/ElastiCache等EC2のみ
Marketplace販売不可不可不可
購入期間1年 / 3年1年 / 3年1年 / 3年1年 / 3年
向いているケース変化が多い環境ファミリー固定の大量EC2RDS・ElastiCache成長フェーズのEC2

うちのチームでの実際の使い分けはこうなっている:

  • EC2の安定ワークロード:Standard RI(最も割引率が高い)
  • Lambda・Fargate混在環境:Compute Savings Plans
  • RDS・ElastiCache:サービス別RI一択(Savings Plansが対応していない)
  • 新規サービス・不確定なワークロード:Convertible RI(最悪変換できる)

正直まだ検証中なのが「3年 vs 1年」の選択。3年だと割引率は上がるけど、インフラの変化速度を考えると1年の方がリスクが低い。うちは今のところ1年で統一しているけど、3年にして得をしたケースがあれば教えてほしい。

購入戦略:段階的アプローチと自動化

RI購入で大事なのは「一気に買わない」こと。これを最初に失敗して学んだ教訓なので、ここは強調しておきたい。

段階的購入のフロー

flowchart TD
    A[現状分析<br/>Cost Explorerで<br/>カバレッジ確認] --> B{カバレッジ率は?}
    B -->|70%以上| C[現状維持<br/>期限切れRIの<br/>更新のみ]
    B -->|40〜70%| D[中程度不足<br/>段階的補充]
    B -->|40%未満| E[大幅不足<br/>緊急対応フェーズ]
    
    D --> F[Phase1: 安定インスタンス特定<br/>過去90日 稼働率90%以上]
    E --> F
    
    F --> G[Phase2: RI推奨ツール実行<br/>Cost Explorer Recommendations]
    G --> H[Phase3: 人間レビュー<br/>将来の構成変更を考慮]
    H --> I{購入承認?}
    I -->|Yes| J[Standard RI購入<br/>最大割引を狙う]
    I -->|条件付き| K[Convertible RI購入<br/>変換オプション確保]
    I -->|No| L[Savings Plans検討<br/>柔軟性優先]
    
    J --> M[月次モニタリング設定<br/>RI使用率アラート]
    K --> M
    L --> M
    M --> N[四半期レビュー<br/>次の購入計画]
    N --> A

フローで見ると整然としているが、実際は「Phase3の人間レビュー」が一番時間がかかる。将来の構成変更を把握するには、インフラチームだけでなくプロダクトチームとの会話も必要になるので、そこは覚悟しておくといい。

Cost Explorer の推奨機能をちゃんと使う

2026年現在、Cost Explorerの推奨エンジンがかなり賢くなっている。特に「lookback period」を90日に設定すると、季節変動も考慮した提案をしてくれるようになった。地味に便利な機能で、以前は自分でスクリプトを書いていたのが不要になりつつある。

# RI購入推奨を取得
aws ce get-reservation-purchase-recommendation \
  --service "Amazon EC2" \
  --lookback-period-in-days SIXTY_DAYS \
  --term-in-years ONE_YEAR \
  --payment-option PARTIAL_UPFRONT \
  --account-scope PAYER \
  --page-size 10

出力結果の一部:

{
  "Recommendations": [
    {
      "AccountScope": "Payer",
      "LookbackPeriodInDays": "SIXTY_DAYS",
      "TermInYears": "ONE_YEAR",
      "PaymentOption": "PARTIAL_UPFRONT",
      "ServiceSpecification": {
        "EC2Specification": {
          "OfferingClass": "STANDARD"
        }
      },
      "RecommendationDetails": [
        {
          "InstanceDetails": {
            "EC2InstanceDetails": {
              "InstanceType": "m6i.2xlarge",
              "Region": "ap-northeast-1",
              "Tenancy": "Shared",
              "Platform": "Linux/UNIX",
              "CurrentGeneration": true,
              "SizeFlexEligible": true
            }
          },
          "RecommendedNumberOfInstancesToPurchase": "4",
          "EstimatedMonthlyOnDemandCost": "3200.50",
          "EstimatedReservationCostForLookbackPeriod": "4100.20",
          "UpfrontCost": "2100.00",
          "RecurringStandardMonthlyCost": "180.50",
          "EstimatedMonthlySavingsAmount": "960.15",
          "EstimatedMonthlySavingsPercentage": "30.0"
        }
      ]
    }
  ]
}

「m6i.2xlargeを4台、月$960削減できる」というのが読み取れる。ただ、このAPIの推奨をそのまま鵜呑みにしてはいけない。来月にインスタンスタイプを変える予定があるなら無意味だし、直近でサービス縮小の話があるなら過剰投資になる。必ず人間のレビューを挟むこと。これは本当に大事なので繰り返しておく。

実際の購入量の決め方

うちが使っているルールは「推奨量の70%を購入する」だ。最初はもったいない気がしたけど、これが結果的に一番安全だった。

def calculate_ri_purchase_quantity(
    recommended_count: int,
    confidence: str = 'MEDIUM',
    planned_scale_down: bool = False
) -> dict:
    """
    推奨数量から実際の購入数量を計算
    
    confidence: LOW, MEDIUM, HIGH
    """
    safety_factor = {
        'HIGH': 0.85,    # 稼働実績が安定している
        'MEDIUM': 0.70,  # 通常ケース
        'LOW': 0.50,     # 不確実性が高い
    }
    
    factor = safety_factor.get(confidence, 0.70)
    
    if planned_scale_down:
        factor *= 0.8  # 縮小予定があればさらに保守的に
    
    purchase_count = int(recommended_count * factor)
    
    return {
        'recommended': recommended_count,
        'purchase': max(1, purchase_count),
        'coverage_gap': recommended_count - purchase_count,
        'suggestion': 'Savings Plans で残りをカバーすることを推奨'
    }

# 例: 推奨10台, 信頼度MEDIUM, 縮小予定なし
result = calculate_ri_purchase_quantity(10, 'MEDIUM', False)
print(result)
# {'recommended': 10, 'purchase': 7, 'coverage_gap': 3, 
#  'suggestion': 'Savings Plans で残りをカバーすることを推奨'}

残りの30%をSavings Plansで補完する戦略で、柔軟性を確保しながら一定の割引を維持している。「100%RIで埋めたい」という気持ちはわかるが、そこを我慢するのがポイントだ。

AWS構成:RI管理の実装アーキテクチャ

RI購入後は「使われているか」の継続的な監視が超重要だ。うちで構築したモニタリング構成はこんな感じ:

graph TB
    subgraph Management Account
        CE[Cost Explorer API]
        CUR[Cost & Usage Report<br/>S3バケット]
        EVT[EventBridge<br/>Scheduler<br/>毎日 09:00 JST]
    end
    
    subgraph Lambda Functions
        ANALYZER[ri-coverage-analyzer<br/>Python 3.13]
        RECOMMENDER[ri-purchase-recommender<br/>Python 3.13]
        ALERTER[ri-utilization-alerter<br/>Python 3.13]
    end
    
    subgraph データストア
        DYNAMO[(DynamoDB<br/>ri-analysis-history)]
        S3_REPORT[(S3<br/>ri-reports)]
    end
    
    subgraph 通知
        SNS[SNS Topic]
        SLACK[Slack<br/>コスト最適化チャンネル]
        EMAIL[メール通知<br/>週次サマリー]
    end
    
    subgraph ダッシュボード
        CW_DASH[CloudWatch<br/>Dashboard]
        QS[QuickSight<br/>月次レポート]
    end
    
    EVT -->|日次トリガー| ANALYZER
    EVT -->|週次トリガー| RECOMMENDER
    CE -->|カバレッジデータ| ANALYZER
    CUR -->|詳細使用量| ANALYZER
    
    ANALYZER -->|分析結果保存| DYNAMO
    ANALYZER -->|レポートPDF| S3_REPORT
    ANALYZER -->|使用率低下検知| ALERTER
    
    RECOMMENDER -->|推奨購入リスト| DYNAMO
    RECOMMENDER -->|承認依頼| SNS
    
    ALERTER -->|アラート発火| SNS
    SNS --> SLACK
    SNS --> EMAIL
    
    DYNAMO -->|可視化データ| CW_DASH
    S3_REPORT -->|集計データ| QS
    
    style Management Account fill:#FF9900,stroke:#FF9900,color:#fff
    style Lambda Functions fill:#FF9900,stroke:#FF9900,color:#fff
    style データストア fill:#3F8624,stroke:#3F8624,color:#fff
    style 通知 fill:#E7157B,stroke:#E7157B,color:#fff
    style ダッシュボード fill:#1A73E8,stroke:#1A73E8,color:#fff

このシステムのポイントは、RI使用率が80%を下回ったらSlackにアラートを出す部分だ。放置しがちなRI管理を、プッシュ通知で強制的に意識させる仕組みにしている。「管理画面を毎日チェックする」という運用は絶対に続かないので、アラートで引っ張ってくる形にするのが現実的だと思う。

アラート用のLambdaコードの核心部分はこんな感じ:

import boto3
import json
import os
from datetime import datetime, timedelta

def lambda_handler(event, context):
    ce = boto3.client('ce')
    sns = boto3.client('sns')
    
    # 直近7日のRI使用率を取得
    end_date = datetime.now().strftime('%Y-%m-%d')
    start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
    
    response = ce.get_reservation_utilization(
        TimePeriod={'Start': start_date, 'End': end_date},
        Granularity='DAILY',
        GroupBy=[{'Type': 'DIMENSION', 'Key': 'INSTANCE_TYPE'}]
    )
    
    low_utilization_ris = []
    
    for time_result in response['UtilizationsByTime']:
        for group in time_result['Groups']:
            utilization_pct = float(
                group['Utilization']['UtilizationPercentage']
            )
            instance_type = group['Attributes'].get('instanceType', 'unknown')
            
            if utilization_pct < 80.0:
                low_utilization_ris.append({
                    'date': time_result['TimePeriod']['Start'],
                    'instance_type': instance_type,
                    'utilization': f"{utilization_pct:.1f}%",
                    'wasted_cost_estimate': group['Utilization'].get(
                        'UnusedAmortizedUpfrontCostForRI', '0'
                    )
                })
    
    if low_utilization_ris:
        message = {
            'alert_type': 'RI_LOW_UTILIZATION',
            'severity': 'WARNING',
            'details': low_utilization_ris,
            'action_required': 'RIの利用状況を確認してください。Convertible RIへの変換またはRI Marketplaceでの売却を検討してください。'
        }
        
        sns.publish(
            TopicArn=os.environ['SNS_TOPIC_ARN'],
            Subject='【コスト最適化】RI使用率低下アラート',
            Message=json.dumps(message, ensure_ascii=False, indent=2)
        )
    
    return {'statusCode': 200, 'low_utilization_count': len(low_utilization_ris)}

このアラートのおかげで、今は使用率80%を下回るRIがほぼゼロの状態を維持できている。インシデント対応と同じで、早期検知の仕組みを作るかどうかで運用の質が全然変わると実感している。

3ヶ月実施した結果と学んだこと

実際に分析から購入・運用まで3ヶ月走らせた結果をまとめる。数字で見ると改善のペースが体感よりもはっきりわかって、これはなかなか気持ちよかった。

xychart-beta
  title "月次AWSコスト推移(RIカバレッジ改善前後)"
  x-axis ["2025-Dec", "2026-Jan", "2026-Feb", "2026-Mar", "2026-Apr", "2026-May"]
  y-axis "月次コスト (万円)" 300 --> 600
  bar [530, 520, 490, 430, 390, 370]
  line [530, 520, 490, 430, 390, 370]

530万円から370万円、約160万円/月の削減。年換算だと1,920万円。我ながらまあまあの成果だと思っている。

ただ、RIカバレッジだけで全部達成したわけじゃない。内訳を正直に言うと:

施策削減額(月次)
RI最適化約90万円
不要リソースの削除(停止インスタンス・未使用EBSなど)約40万円
Savings Plansの追加購入約30万円
合計約160万円

RIが一番インパクト大きかったのは間違いないが、AWS Compute Optimizerの活用と組み合わせることで効果が倍増した感じだった。どれか一つだけじゃなく、複数の施策を並行して回したのがよかったんだと思う。

失敗から学んだ教訓

3ヶ月でいくつか失敗もした。特に痛かったのを正直に書いておく。

失敗1:ElastiCacheのRIをファミリー固定で買いすぎた

valkey移行の話が出ていたのに、cache.r7g.2xlargeのStandard RIを12ヶ月分購入してしまった。3ヶ月後にvalkeyへの移行が決まって、RIが無駄になりそうになった。幸いConvertible RIにしていた分は変換できたが、Standard分は泣く泣くRI Marketplaceで売却(当然損した)。ロードマップを確認せずに買うのは本当に危ない。

失敗2:アカウント単位ではなく請求統合で分析すべきだった

Organizationsで複数アカウントを管理しているのに、最初は1アカウントずつ分析していた。本来は管理アカウントから全体を見るべきで、これに気づくまで1ヶ月無駄にした。あの1ヶ月は取り返しがつかないので、マルチアカウント環境の人は最初から管理アカウントで分析してほしい。

# 正しい方法:管理アカウントから全体のカバレッジを確認
aws ce get-reservation-coverage \
  --time-period Start=2026-04-01,End=2026-05-01 \
  --granularity MONTHLY \
  --group-by Type=DIMENSION,Key=LINKED_ACCOUNT \
  --metrics "CoverageHours"
  # --filter は指定しないことで全アカウントが対象になる

マルチアカウント管理の詳細はAWS Organizationsの設計記事が参考になるので合わせて見てほしい。

まとめ

RI戦略で本当に重要なのは「一度買って終わり」じゃなくて継続的な分析と調整のサイクルを回すことだと3年間で痛感している。最初の購入よりも、買った後に使われ続けているかを追いかける仕組みを作る方が、長期的なインパクトはずっと大きい。

要点をまとめるとこうなる:

  1. 分析から始める:まずCost ExplorerでサービスごとのRIカバレッジを確認。70%未満なら改善余地あり。EC2以外(ElastiCache・OpenSearch等)も忘れずに確認する

  2. 稼働安定性で購入対象を絞る:過去90日間の稼働率が70%以上のインスタンスのみRI対象にする。それ以下はSavings Plansで対応

  3. 段階的に買う・全量は買わない:推奨数量の70〜85%をRI購入し、残りをSavings Plansで補完するハイブリッド戦略が安全

  4. RI使用率の継続監視を自動化する:80%を下回ったらアラートを出す仕組みを作る。人間が毎月手動で確認するのは確実に漏れる

  5. RDSとElastiCacheはRI必須:Savings Plansが対応していないため、安定したDBインスタンスは積極的にRI購入すると大きく効く

今すぐできるアクションとしては、まずCost Explorerで自分のサービスのRIカバレッジを確認するところから始めてみてほしい。稼働率分析スクリプトを実行して購入候補リストを作り、RI使用率のCloudWatchアラートを設定する——この3ステップだけで、3ヶ月後の請求書はかなり変わるはずだ。

U

Untanbaby

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

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

関連記事