RIカバレッジ分析、3年間ずっと間違えてた話と2026年の正解

RI利用率とカバレッジを混同したまま2年間失敗し続け、月160万円削減にたどり着くまでの試行錯誤。Savings Plans併用の判断基準や実際のコードも公開しています。

RIカバレッジ分析と購入戦略2026|3年間の失敗から学んだ実践ガイド

正直に言うと、RIを買い始めた最初の2年間、ずっと失敗していた。

「使用率が高いインスタンスにRI買えばいい」という雑な理解でやっていたら、気づいたら期限切れ間際のRIが余りまくって、フリマで売るはめになったり、逆にRI未購入のインスタンスがOn-Demandで動き続けていたり。チームで運用するようになって、ようやくちゃんとカバレッジを分析するフローを作り、月160万円ほどのコスト削減に落ち着いた経緯は以前の記事で書いた。

今回はその後の話、つまり2026年時点でどうやってRIカバレッジ分析を日常的に回しているか、そして購入タイミングと種別の判断基準をどう整理したかを書く。Savings PlansとRIの使い分けも含めて、実際に動かしているコードと判断フローを共有したい。


RIカバレッジとは何を見ているのか、改めて整理する

これ、意外と「カバレッジ率」の定義を誤解している人が多い。使用率(Utilization)とカバレッジ(Coverage)は別物で、最初はここで混乱した。

指標定義目標値の目安
RI利用率(Utilization)購入したRIのうち実際に使われた割合90%以上
RIカバレッジ(Coverage)On-Demand実行時間のうちRIで賄った割合70〜80%以上(ワークロード次第)

RI利用率が高くても、カバレッジが低ければ意味がない。逆にカバレッジを上げすぎると、RI余りが発生して利用率が落ちる。この二つを同時に見ながらバランスを取るのが本当のRI管理で、片方だけ見ていた頃は毎月どこかしらズレていた。

うちのチームでは2026年からCost Optimization Hubを正式に導入して、ここで両方の指標をダッシュボード化している。以前はCost Explorerを手動でポチポチ見ていたが、これが地味に時間かかるし、何より「見る頻度が落ちる」原因になっていた。ツールを変えるだけで習慣が変わる、というのを身をもって体験した。

import boto3
import pandas as pd
from datetime import datetime, timedelta

def get_ri_coverage_report(days: int = 30) -> dict:
    """
    過去N日間のRIカバレッジレポートを取得する
    """
    ce = boto3.client('ce', region_name='us-east-1')
    
    end_date = datetime.now().strftime('%Y-%m-%d')
    start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
    
    response = ce.get_reservation_coverage(
        TimePeriod={
            'Start': start_date,
            'End': end_date
        },
        GroupBy=[
            {'Type': 'DIMENSION', 'Key': 'INSTANCE_TYPE'},
            {'Type': 'DIMENSION', 'Key': 'REGION'},
        ],
        Granularity='MONTHLY',
        Metrics=['Hour']
    )
    
    coverage_data = []
    for result in response['CoveragesByTime']:
        period = result['TimePeriod']
        for group in result['Groups']:
            instance_type = group['Attributes']['instanceType']
            region = group['Attributes']['region']
            covered_hours = float(group['Coverage']['CoverageHours']['CoveredHours'])
            total_hours = float(group['Coverage']['CoverageHours']['TotalRunningHours'])
            coverage_pct = float(group['Coverage']['CoverageHours']['CoverageHoursPercentage'])
            
            coverage_data.append({
                'period': period['Start'],
                'instance_type': instance_type,
                'region': region,
                'covered_hours': covered_hours,
                'total_hours': total_hours,
                'coverage_pct': coverage_pct
            })
    
    df = pd.DataFrame(coverage_data)
    
    # カバレッジが低い(50%未満)かつ実行時間が多いインスタンスを抽出
    low_coverage = df[
        (df['coverage_pct'] < 50) & 
        (df['total_hours'] > 100)
    ].sort_values('total_hours', ascending=False)
    
    return {
        'full_data': df,
        'low_coverage_candidates': low_coverage
    }

if __name__ == '__main__':
    report = get_ri_coverage_report(days=30)
    print('=== RI購入候補(カバレッジ50%未満・100時間超)===')
    print(report['low_coverage_candidates'][[
        'instance_type', 'region', 'coverage_pct', 'total_hours'
    ]].to_string(index=False))

これを毎週月曜朝にLambdaで実行して、結果をSlackに飛ばすようにしている。「見なきゃいけない」をやめて「勝手に見える」状態にしたのが正直一番大きかった。

実行結果の例:

=== RI購入候補(カバレッジ50%未満・100時間超)===
 instance_type      region  coverage_pct  total_hours
  m7i.4xlarge   us-east-1          12.5       2160.0
  r7g.2xlarge   us-west-2          28.3        744.0
  c7i.xlarge    ap-northeast-1     41.7        528.0

RI vs Savings Plans、2026年時点での使い分け判断

これはSavings Plans vs Reserved Instancesの比較記事でも詳しく書いたが、実際の購入判断フローをここで改めて整理したい。

結論から言うと、うちのチームではCompute Savings Plansを基盤として使い、その上でEC2 RIを補完的に使うという形に落ち着いた。「全部RIで買えばいいじゃん」と思っていた時期もあったが、インスタンスタイプの変更が読めないワークロードに対してそれをやると、後で泣くことになる。

flowchart TD
    A[コスト削減対象の特定] --> B{インスタンスタイプが
固定されているか?}
    B -->|Yes| C{使用期間の予測は
1年以上確実か?}
    B -->|No| D[Compute Savings Plans]
    C -->|Yes| E{使用率の変動は
20%以内か?}
    C -->|No| D
    E -->|Yes| F[EC2 RI (1年 Full Upfront)]
    E -->|No| G{変動の原因は?}
    G -->|スケールアップ予定| H[EC2 RI (Convertible)]
    G -->|季節変動・予測困難| D
    F --> I[RIカバレッジ80%目標]
    H --> I
    D --> J[Compute SP カバレッジ70%目標]
    I --> K[月次レビュー]
    J --> K
    K --> L{余剰RI発生?}
    L -->|Yes| M[AWS Marketplace出品 or
Convertible交換]
    L -->|No| K

Convertible RIを積極的に使い始めたのが2025年後半で、これが思いのほか良かった。Standard RIよりディスカウント率は下がるものの、インスタンスファミリーの変更ができるのが大きい。うちのワークロードはM系からR系に移行する可能性があって、ずっと「移行したらRI無駄になる」と躊躇していたが、ConvertibleならR系に交換できるので踏み切れた。

ただ正直、まだ全体最適できているかは微妙なところもある。季節変動があるバッチ処理系はSavings Plansで逃がしているが、そのコミット量の閾値設定がいまいち自信がない。皆さんはどうやってSPのコミット量を決めていますか?


実際の購入プロセスと自動化フロー

RI購入を「なんとなくタイミングで買う」からルーティン化したのが一番の改善だった。今はこういう構成で動いている。

graph TB
    subgraph "AWS アカウント(Management)"
        CUR[Cost & Usage Report<br/>S3バケット]
        CE[Cost Explorer API]
        COH[Cost Optimization Hub]
    end
    
    subgraph "分析基盤 VPC"
        subgraph "AZ-a"
            LAMBDA[Lambda<br/>週次分析関数]
        end
        subgraph "AZ-b"
            RDS[(Aurora PostgreSQL<br/>コスト履歴DB)]
        end
        STEP[Step Functions<br/>購入承認ワークフロー]
    end
    
    subgraph "通知・承認"
        SLACK[Slack<br/>購入候補通知]
        APPR[承認フォーム<br/>Google Forms]
        MAIL[SES<br/>CFO承認メール]
    end
    
    subgraph "購入実行"
        PURCH[Lambda<br/>RI購入実行]
        AUDIT[CloudTrail<br/>購入監査ログ]
    end
    
    CUR --> LAMBDA
    CE --> LAMBDA
    COH --> LAMBDA
    LAMBDA --> RDS
    LAMBDA --> STEP
    STEP --> SLACK
    STEP --> MAIL
    SLACK --> APPR
    APPR --> PURCH
    PURCH --> AUDIT
    AUDIT --> RDS

承認フローが入っているのがミソで、一定金額(うちは月間コスト削減効果が50万円超のRI購入)はCFOの承認が必要にしている。最初は「承認フロー重い」と思っていたが、これのおかげで「なんとなく買った微妙なRI」が消えた。手間をかけると人間は真剣に考えるようになる、という当たり前の話なんだけど。

購入実行のLambda関数の核心部分はこんな感じ:

import boto3
import json
from dataclasses import dataclass
from typing import Optional

@dataclass
class RIPurchaseRequest:
    instance_type: str
    availability_zone: Optional[str]  # Noneならリージョンスコープ
    offering_class: str  # 'standard' or 'convertible'
    payment_option: str  # 'AllUpfront', 'PartialUpfront', 'NoUpfront'
    term_length: int  # 1 or 3 (年)
    instance_count: int
    estimated_monthly_savings: float

def calculate_optimal_ri_count(
    total_running_hours: float,
    current_ri_hours: float,
    target_coverage: float = 0.80,
    buffer_ratio: float = 0.90  # 100%買わずに余裕を持たせる
) -> int:
    """
    目標カバレッジに達するために必要なRI数を計算する
    buffer_ratioで購入過多を防ぐ
    """
    # 1インスタンスあたりの月間時間(31日換算)
    hours_per_instance_per_month = 31 * 24
    
    # 目標のRI適用時間
    target_ri_hours = total_running_hours * target_coverage
    
    # 追加で必要なRI時間
    additional_hours_needed = max(0, target_ri_hours - current_ri_hours)
    
    # 必要なRI数(buffer込み)
    raw_count = additional_hours_needed / hours_per_instance_per_month
    buffered_count = raw_count * buffer_ratio
    
    return max(0, int(buffered_count))

def get_ri_offering_price(
    ec2_client,
    instance_type: str,
    offering_class: str,
    payment_option: str,
    term_length: int
) -> dict:
    """
    RI価格を取得する
    """
    term_str = 'one-year' if term_length == 1 else 'three-years'
    
    offerings = ec2_client.describe_reserved_instances_offerings(
        InstanceType=instance_type,
        ProductDescription='Linux/UNIX',
        OfferingClass=offering_class,
        OfferingType=payment_option,
        Scope='Region',
        Filters=[{
            'Name': 'duration',
            'Values': ['31536000' if term_length == 1 else '94608000']
        }]
    )
    
    if not offerings['ReservedInstancesOfferings']:
        return {}
    
    offering = offerings['ReservedInstancesOfferings'][0]
    return {
        'offering_id': offering['ReservedInstancesOfferingId'],
        'fixed_price': offering.get('FixedPrice', 0),
        'recurring_charges': offering.get('RecurringCharges', []),
        'usage_price': offering.get('UsagePrice', 0)
    }

buffer_ratio = 0.90 というのがポイントで、目標カバレッジまで100%買い切らずに少し余裕を持たせている。ワークロードは予測通りにいかないことがほとんどで、完全にカバレッジ目標を達成しようとすると余剰RIが出やすい。この0.10のバッファが地味に効く。


カバレッジ推移の可視化と月次レビュー運用

数値で見るだけだと正直ピンとこないので、可視化を整備した。以下が週次のSlack報告に貼り付けているものと同じ形式だ。

xychart-beta
    title "RIカバレッジ推移(2025年7月〜2026年4月)"
    x-axis ["2025-07", "2025-08", "2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03", "2026-04"]
    y-axis "カバレッジ率 (%)" 0 --> 100
    bar [42, 48, 55, 61, 68, 72, 75, 78, 81, 83]
    line [42, 48, 55, 61, 68, 72, 75, 78, 81, 83]

2025年7月時点で42%だったのが、2026年4月で83%まで来た。目標の80%を超えたのが2026年3月で、ここまで8ヶ月かかった。急に買い増ししなかったのは正解で、段階的に積み上げることで「余剰RI」がほぼ発生しなかった。焦って一気に買っていたらまた失敗していたと思う。

コスト削減額の推移も合わせて見ている:

xychart-beta
    title "月間コスト削減額推移(万円)"
    x-axis ["2025-07", "2025-08", "2025-09", "2025-10", "2025-11", "2025-12", "2026-01", "2026-02", "2026-03", "2026-04"]
    y-axis "削減額(万円)" 0 --> 200
    bar [38, 52, 71, 89, 112, 128, 143, 151, 162, 168]

月次レビューの場では、この2つのグラフと「来月の購入候補リスト」を3分で説明する形にしている。FinOpsを全員が理解している必要はないが、少なくとも意思決定者が「なぜ今買うのか」を数字で判断できる場を作ることが重要だと思っている。「なんとなく担当者に任せる」から「数字で判断できる経営層」に変わると、承認が格段に取りやすくなるのもメリットだ。

インシデント対応との絡みもあって、コスト最適化の取り組みが運用負荷を増やさないようにするのが課題になることもある。インシデント対応のベストプラクティスも参考にしながら、RI管理の対応フローをランブックに組み込んだ。


購入種別と支払いオプションの現実的な判断基準

「Full Upfrontが一番お得」というのは理論上正しいが、キャッシュフロー的に常に最善とは限らない。2026年時点の割引率を整理するとこうなる:

支払いオプション割引率(1年・Standard)割引率(3年・Standard)特徴
Full Upfront約40%約60%初期費用大・最大割引
Partial Upfront約38%約58%バランス型
No Upfront約35%約55%初期費用なし
Compute SP(1年)約30〜35%相当柔軟性最大
EC2 SP(1年)約35〜40%相当インスタンスファミリー固定

※上記はリージョン・インスタンスタイプにより変動あり

うちのチームでは1年Full Upfrontを基本にしている。3年だとインスタンス世代の変化についていけなくなるリスクが高いからで、実際2年前に3年RIでm5系を買ったチームが、今はm7iに移行したくても身動き取れない状態になっているのを目撃している。ConvertibleならOKだが、Standard 3年はよほど確信がない限り今はやめたほうがいいと個人的に思っている。

No Upfrontを選ぶケースは、財務部門から「今期の設備投資を抑えてほしい」と言われた時だけだ。コスト的にはもったいないが、そういう制約がある時にはNo Upfrontでカバレッジを上げる方が、On-Demandで払い続けるよりまだマシ。「最適解」より「実現可能な次善策」を取るのが大事だと学んだ。

EKSを使っている場合はEKSコスト最適化のKarpenter活用も組み合わせると効果的で、Spot Instanceの活用とRI/SPを組み合わせることで全体の最適化が進む。


まとめ

RI管理を「なんとなく」から「仕組み化」した3年間で、ようやく再現性のある運用に辿り着いた。

要点をまとめると:

  1. 利用率とカバレッジは別物 — 両方を週次で監視する仕組みを先に作る
  2. Compute Savings Plans を基盤、EC2 RI を補完 — 柔軟性と割引率のバランスで使い分ける
  3. 段階的に購入してカバレッジを積み上げる — 一度に目標まで買い切ると余剰が出やすい
  4. 3年 Standard RIは慎重に — インスタンス世代の変化を考えると Convertible か1年が安全
  5. 承認フローを自動化する — 「見なきゃいけない」をやめて「勝手に見える・判断できる」状態を作る

次のアクション:

  • まずCost Explorerで現在のカバレッジ率を確認する(理想は70%以上)
  • カバレッジ50%未満かつ月間100時間超のインスタンスをリストアップする
  • Compute Savings Plansのコミット量を、過去3ヶ月の最低On-Demand消費額を基準に設定する

正直、ここまで整備するのに時間がかかりすぎた。でも一度仕組みができてしまえば、後は月1のレビューで回るようになる。RI管理をまだ手動でやっているチームは、まず週次自動レポートの仕組みを作るところから始めてみてほしい。最初の一歩が一番しんどいが、そこさえ越えれば後はかなりラクになる。

U

Untanbaby

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

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

関連記事