Compute Optimizerを3ヶ月本番運用して見えてきた、推奨値の正しい受け取り方

「EC2でかいのはわかるけど、どれをどう変えれば?」そんな悩みにCompute Optimizerを3ヶ月使い込んで向き合った話。推奨値の落とし穴と月120万円削減の実態を正直に書きます。

Compute Optimizerを3ヶ月本番運用して見えてきた、推奨値の正しい受け取り方

月の請求書を見て「あれ、また増えてる」ってなる経験、みなさんありませんか。うちのチームでも半年くらい前まで、それが続いていた。Cost Explorerを眺めるたびに「EC2がでかい」というのはわかるんだけど、どのインスタンスをどう変えたらいいのか具体的な根拠が欲しくて、改めてCompute Optimizerをちゃんと使い込んでみることにした。

結論から言うと、月換算で約120万円のコスト削減につながった。ただ、「推奨通りに変えたら解決」という話ではなくて、推奨値の裏側にある前提条件を理解しないと踏み抜く地雷がある。今日はそのあたりを含めて、3ヶ月運用して見えてきたことを正直に書く。

なお、RIやSavings Plansとの組み合わせについてはSavings Plans vs Reserved Instances|2026年AWSコスト最適化判断基準に詳しく書いたので、興味ある方はそちらも参照してほしい。EKS側のコスト最適化はEKS コスト最適化2026|Spot/On-Demand戦略とKarpenter活用でまとめている。

Compute Optimizerの現状と2026年時点の対応サービス

2026年5月時点でCompute Optimizerが推奨を提供しているのは以下の通り。

サービス推奨タイプ必要な有効化オプション
EC2インスタンスサイズ変更・タイプ変更Enhanced Infrastructure Metrics(有料)
EC2 Auto Scaling Groupスケーリングポリシー・インスタンスタイプ同上
Lambda関数メモリサイズCloudWatchメトリクス連携
ECSサービス(Fargate)CPU/メモリ設定Container Insights必須
EBSボリュームボリュームタイプ・サイズデフォルト有効
RDS(Aurora/PostgreSQL)インスタンスタイプ2025年GA・要オプトイン

RDSの推奨が2025年にGAしたのが地味に大きくて、うちの環境ではAuroraのインスタンスがオーバースペックになっていたケースをここで発見できた。「なんとなく大きめにしておこう」でずっと放置されていたやつが、ちゃんと数値で可視化されると一気に動きやすくなる。

全体のアーキテクチャでいうと、データ収集から推奨表示までの流れはこんな感じになっている。

flowchart TB
  subgraph AWS_Account["AWS アカウント(本番)"]
    subgraph VPC["VPC"]
      subgraph AZ_A["Availability Zone A"]
        EC2A["EC2 Instances"]
        ECS_A["ECS Fargate Services"]
      end
      subgraph AZ_B["Availability Zone B"]
        EC2B["EC2 Instances"]
        ECS_B["ECS Fargate Services"]
      end
    end
    Lambda["Lambda Functions"]
    RDS["Aurora PostgreSQL"]
    EBS["EBS Volumes"]
    CW["CloudWatch Metrics\n+ Container Insights"]
    CT["CloudTrail"]
  end

  subgraph Compute_Optimizer["AWS Compute Optimizer"]
    ML["MLモデル\n(14日〜93日分析)"]
    REC["推奨エンジン"]
    RISK["リスク分類\n(Over/Under)"]
  end

  subgraph Output["推奨出力"]
    Console["Consoleダッシュボード"]
    API["GetRecommendations API"]
    S3Export["S3エクスポート\n(CSV/JSON)"]
    Notification["EventBridge通知"]
  end

  EC2A --> CW
  EC2B --> CW
  ECS_A --> CW
  ECS_B --> CW
  Lambda --> CW
  RDS --> CW
  EBS --> CT
  CW --> ML
  CT --> ML
  ML --> REC
  REC --> RISK
  RISK --> Console
  RISK --> API
  RISK --> S3Export
  RISK --> Notification

この図で重要なのは、推奨の精度が「何日分のメトリクスを見ているか」に直結しているという点。デフォルトは14日間だけど、Enhanced Infrastructure Metricsを有効にすると最大93日間まで遡れる。月額リソースあたり数ドルかかるけど、季節変動があるサービスではこれを有効にしないと的外れな推奨が出やすい。個人的には、大規模な環境ほど迷わず有効にしたほうがいいと思っている。

実際に運用してわかった、推奨値の受け取り方

最初に試したのは一番ボリュームのでかいEC2インスタンス群だった。開発初期から「とりあえずm5.2xlarge」が積み重なって、あまり見直されていない状態。Compute OptimizerはこのうちいくつかをOver-provisionedと判定して、m5.largeやm7i.largeへの変更を推奨してきた。

正直、最初は「本当に大丈夫か?」という感覚があった。推奨値をそのまま適用するのが怖くて、最初の1ヶ月はほぼ観察だけしていた。そこで気づいたのが、Compute Optimizerの推奨には前提条件がちゃんと埋め込まれているという点。

たとえば「99th percentileのCPU使用率が15%以下だからダウンサイズ推奨」という判断をしているとき、バッチ処理で月1回だけCPUが跳ね上がるパターンは当然考慮されていない。観察期間が14日だと月次バッチが入っていないことがある。実際うちでもこれを見落として、ステージング環境でダウンサイズしてバッチ実行時にCPUが張り付くという事態を起こした。これは地味に焦った。

対策として採用したのは、推奨を出す前にCloudWatchで最大値ではなくパーセンタイル分布を確認するステップを挟む運用。具体的には以下のAWS CLIスクリプトを毎週実行して確認している。

#!/bin/bash
# Compute Optimizer推奨インスタンスのCPU p99/p95/p50を確認するスクリプト

INSTANCE_ID="i-0abc123def456789"
START_TIME=$(date -d '93 days ago' --utc +%Y-%m-%dT%H:%M:%SZ)
END_TIME=$(date --utc +%Y-%m-%dT%H:%M:%SZ)

echo "=== CPU使用率パーセンタイル分析 ==="

for STAT in p50 p95 p99; do
  VALUE=$(aws cloudwatch get-metric-statistics \
    --namespace AWS/EC2 \
    --metric-name CPUUtilization \
    --dimensions Name=InstanceId,Value=$INSTANCE_ID \
    --start-time $START_TIME \
    --end-time $END_TIME \
    --period 86400 \
    --extended-statistics ExtendedStatistics=$STAT \
    --query "Datapoints[*].ExtendedStatistics.$STAT" \
    --output text | sort -n | tail -1)
  echo "$STAT: ${VALUE}%"
done

# Compute Optimizerの推奨を取得
echo ""
echo "=== Compute Optimizer推奨 ==="
aws compute-optimizer get-ec2-instance-recommendations \
  --instance-arns "arn:aws:ec2:ap-northeast-1:123456789012:instance/$INSTANCE_ID" \
  --query 'instanceRecommendations[0].{Current:currentInstanceType,Finding:finding,Recommendations:recommendationOptions[0].instanceType}' \
  --output table

実行結果のイメージはこんな感じ:

=== CPU使用率パーセンタイル分析 ===
p50: 8.2%
p95: 42.7%
p99: 78.3%

=== Compute Optimizer推奨 ===
----------------------------------------------------------------------------------------
|                       GetEc2InstanceRecommendations                                  |
+------------------+-------------------+-----------------------------------------------+
|     Current      |      Finding      |              Recommendations                  |
+------------------+-------------------+-----------------------------------------------+
|  m5.2xlarge      |  OVER_PROVISIONED |  m5.xlarge                                    |
+------------------+-------------------+-----------------------------------------------+

p99が78.3%出ているインスタンスに対してm5.xlargeへのダウンサイズ推奨が出てきた場合、これは要注意。このケースでは推奨を見送ることにした。一方でp99が20%未満のインスタンスはほぼ安心してダウンサイズできることがわかってきた。この「p99を自分で確認してから判断する」習慣を身につけたのが、3ヶ月で一番よかった変化だと思っている。

Lambda・ECS Fargate推奨の活用、正直ここが一番効いた

EC2よりもはるかに楽に適用できたのがLambdaとECS Fargateの推奨だった。理由はシンプルで、最悪すぐにロールバックできるから。心理的なハードルが全然違う。

Lambdaのメモリ推奨は地味に面白くて、「メモリを減らす」推奨と「メモリを増やす」推奨の両方が出る。増やす推奨のほうが最初は直感に反したんだけど、Lambda Power Tuningの考え方と同じで、メモリを増やすとCPUも増えて実行時間が短縮されてトータルコストが下がるケースがある。

これについてはLambda Power Tuningを半年本番で使って分かったメモリ最適化の現実でも詳しく書いたけど、Compute Optimizerの推奨と組み合わせると根拠がより強くなる感覚がある。「なんとなく増やした」じゃなくて「MLモデルが分析した結果として増やした」と言えるのは、チーム内の説得でも助かった。

実際に適用した結果をリソース別にまとめるとこんな数値になった。

xychart-beta
  title "Compute Optimizer適用前後のリソース別コスト削減率(%)"
  x-axis ["EC2", "ECS Fargate", "Lambda", "EBS", "Aurora RDS"]
  y-axis "コスト削減率 (%)" 0 --> 50
  bar [18, 32, 28, 15, 22]

ECS Fargateが一番削減率が高かったのは意外だった。開発チームがFargate定義を書くとき「とりあえず1vCPU/2GB」みたいな設定が多くて、実際の使用率を見るとCPUは10%以下、メモリも40%以下というタスクがごろごろいた。これをCompute Optimizerが0.5vCPU/1GBに変えろと推奨してくれて、適用したら思ったよりすんなり動いた。TaskDefinitionを変えてデプロイするだけなので、EC2と違ってインスタンス停止もない。

Fargateの推奨を適用するときのコード例。TaskDefinitionの修正はCDKで管理しているので、推奨値を受けてコードを変更するだけ。

// Before: 過剰なFargate設定
const taskDef = new ecs.FargateTaskDefinition(this, 'ApiTaskDef', {
  memoryLimitMiB: 2048,  // Compute Optimizerが「1024で十分」と推奨
  cpu: 1024,             // Compute Optimizerが「512で十分」と推奨
});

// After: Compute Optimizer推奨値に合わせて修正
const taskDef = new ecs.FargateTaskDefinition(this, 'ApiTaskDef', {
  memoryLimitMiB: 1024,  // 推奨: 1024(実際のp99使用率: 380MB)
  cpu: 512,              // 推奨: 512(実際のp99使用率: 15%)
});

デプロイして1週間様子を見たけど特に問題なし。Fargateはスペックを下げても起動速度への影響がほぼないので、試しやすい。EC2の前にFargateで感触をつかむ、という順序は本当におすすめしたい。

S3エクスポートとEventBridgeで推奨を自動追跡する仕組み

3ヶ月で一番「作っておいてよかった」と思ったのが、推奨をS3に定期エクスポートしてSlackに飛ばす仕組みだった。Consoleをポチポチ見に行くのは最初の2週間で飽きる。自動でSlackに流れてきて、担当者がそれを見て判断するフローが現実的に続く。

構成はこう。

flowchart LR
  subgraph Compute_Optimizer["Compute Optimizer"]
    REC_ENGINE["推奨エンジン"]
  end

  subgraph Automation["自動化レイヤー"]
    EB["EventBridge\nScheduler\n(毎週月曜9時)"]
    LAMBDA_EX["Lambda\nExporter"]
    S3["S3 Bucket\nrecommendations/"]
    LAMBDA_NOTIFY["Lambda\nNotifier"]
    SLACK["Slack\n#aws-cost-alert"]
  end

  subgraph Review["レビュープロセス"]
    ATHENA["Athena\n傾向分析クエリ"]
    TICKET["Jiraチケット\n自動作成"]
    APPLY["担当者レビュー\n→適用判断"]
  end

  REC_ENGINE -->|推奨生成| EB
  EB -->|トリガー| LAMBDA_EX
  LAMBDA_EX -->|GetRecommendations API| REC_ENGINE
  LAMBDA_EX -->|CSV保存| S3
  S3 -->|S3イベント通知| LAMBDA_NOTIFY
  LAMBDA_NOTIFY -->|週次サマリー| SLACK
  S3 -->|クエリ| ATHENA
  SLACK -->|アクション| TICKET
  TICKET --> APPLY

Lambda Exporterの中身はこんな感じで動いている。

import boto3
import json
import csv
import io
from datetime import datetime

def lambda_handler(event, context):
    optimizer = boto3.client('compute-optimizer', region_name='ap-northeast-1')
    s3 = boto3.client('s3')
    
    recommendations = []
    
    # EC2推奨を取得
    paginator = optimizer.get_paginator('get_ec2_instance_recommendations')
    for page in paginator.paginate():
        for rec in page['instanceRecommendations']:
            if rec['finding'] in ['OVER_PROVISIONED', 'UNDER_PROVISIONED']:
                # 削減可能コストを計算
                current_price = rec.get('currentPerformanceRisk', 'UNKNOWN')
                top_rec = rec['recommendationOptions'][0] if rec['recommendationOptions'] else {}
                
                recommendations.append({
                    'service': 'EC2',
                    'resource_id': rec['instanceArn'].split('/')[-1],
                    'current_type': rec['currentInstanceType'],
                    'recommended_type': top_rec.get('instanceType', 'N/A'),
                    'finding': rec['finding'],
                    'savings_opportunity': top_rec.get('savingsOpportunity', {}).get('estimatedMonthlySavings', {}).get('value', 0),
                    'cpu_p99': next((m['value'] for m in rec.get('utilizationMetrics', []) 
                                   if m['name'] == 'CPU' and m['statistic'] == 'MAXIMUM'), 0),
                })
    
    # Lambda推奨を取得
    paginator = optimizer.get_paginator('get_lambda_function_recommendations')
    for page in paginator.paginate():
        for rec in page['lambdaFunctionRecommendations']:
            if rec['finding'] in ['OVER_PROVISIONED', 'MEMORY_UNDER_PROVISIONED']:
                top_rec = rec['memorySizeRecommendationOptions'][0] if rec['memorySizeRecommendationOptions'] else {}
                recommendations.append({
                    'service': 'Lambda',
                    'resource_id': rec['functionArn'].split(':')[-2],
                    'current_type': f"{rec['currentMemorySize']}MB",
                    'recommended_type': f"{top_rec.get('memorySize', 'N/A')}MB",
                    'finding': rec['finding'],
                    'savings_opportunity': top_rec.get('savingsOpportunity', {}).get('estimatedMonthlySavings', {}).get('value', 0),
                    'cpu_p99': 0,
                })
    
    # S3にCSVとして保存
    output = io.StringIO()
    if recommendations:
        writer = csv.DictWriter(output, fieldnames=recommendations[0].keys())
        writer.writeheader()
        writer.writerows(recommendations)
        
        date_str = datetime.now().strftime('%Y-%m-%d')
        s3.put_object(
            Bucket='my-cost-optimization-bucket',
            Key=f'recommendations/{date_str}/compute_optimizer.csv',
            Body=output.getvalue().encode('utf-8'),
            ContentType='text/csv'
        )
        
        total_savings = sum(r['savings_opportunity'] for r in recommendations)
        print(f"推奨件数: {len(recommendations)}, 月次削減可能額合計: ${total_savings:.2f}")
    
    return {'statusCode': 200, 'recommendations_count': len(recommendations)}

これを週1回動かして、Slackに「今週の推奨: 18件、削減可能額: $3,420/月」みたいなサマリーが流れるようにした。最初は「また通知か」ってなりそうで怖かったけど、週1回だと適度に気になる頻度でちょうどよかった。毎日だと絶対スルーされる。これは本当にそう。

3ヶ月の実績と、正直まだ解決できていないこと

3ヶ月で変えたリソースとその結果を振り返ると、月次コストはこんな推移になった。

xychart-beta
  title "月次EC2+ECS+Lambda費用推移(万円)"
  x-axis ["2025-11月", "2025-12月", "2026-1月", "2026-2月", "2026-3月"]
  y-axis "費用(万円)" 0 --> 500
  line [420, 415, 390, 360, 298]

5ヶ月で420万円→298万円、月120万円ほど削減できた。ただ正直に言うと、この削減の30%くらいはCompute Optimizerとは関係なく、Savings Plansの見直しと並行して進めていたので、純粋にCompute Optimizerの推奨適用だけの効果ではない。数字を盛りたい気持ちはあるけど、正直に書いておく。

まだ解決できていないことも残っている。

ひとつはECSのContainer InsightsコストとCompute Optimizer推奨によるFargate削減のバランス問題。Container InsightsはECSの推奨を出すために必要なんだけど、それ自体がそこそこコストかかる。うちの環境だと月3〜4万円かかっていて、Fargate削減で得られるメリットと天秤にかけると微妙なラインのクラスターがある。ここは正直まだ計算中。小規模なクラスターには向かないかもしれない。

もうひとつは、推奨値の「Savings Opportunity」という数値が実際の削減額と乖離することがある点。特にRI・Savings Plansで購入済みのインスタンスでは、ダウンサイズしても購入コストが固定されているため実際の削減効果が推奨値ほど出ないケースがある。オンデマンド比で計算されていることが多いので、自分の購入状況を加味して判断する必要がある。最初にこれを知らずに「すごい削減効果だ!」と期待して拍子抜けした経験があるので、先に言っておきたかった。

インシデント対応との兼ね合いも一応頭に置いておきたい。ダウンサイズ後にパフォーマンス問題が起きたとき、それがCompute Optimizer推奨適用の影響かどうかをすぐに判断できる記録を残しておくことが大事で、インシデント対応の最新ベストプラクティス2026|DevOps・SRE必読に書いたようなタイムライン記録と組み合わせると後から追いやすい。

まとめ

3ヶ月Compute Optimizerをちゃんと使い込んでわかった要点をまとめる。

  1. Enhanced Infrastructure Metricsは有効にする価値がある。 特に季節変動があるサービスでは93日間の分析がないと的外れな推奨が出やすい。月次コストはかかるけど、大規模環境なら投資対効果は高い。

  2. 推奨値をそのまま適用する前に、必ずパーセンタイル分布を確認する。 p99が高いインスタンスへの推奨は見送り判断が多い。スクリプト化して確認を習慣にすると楽になる。

  3. LambdaとECS Fargateの推奨は比較的安全に適用できる。 ロールバックが容易なので、EC2より先に試すのがおすすめ。

  4. S3エクスポート+Slack通知の自動化が運用継続のカギ。 Consoleを能動的に見に行く運用は1ヶ月持たない。週次で自動サマリーが流れる仕組みを作るとずっと続く。

  5. RI・Savings Plans購入済みリソースへの推奨は鵜呑みにしない。 実際の削減効果が推奨値と乖離する。オンデマンド比で計算されている場合が多いため、自分の購入状況を加味して判断する必要がある。

次のアクション: まずEnhanced Infrastructure Metricsを全アカウントで有効化して、1週間後に推奨一覧をS3エクスポートしてみるところから始めるといい。最初からすべての推奨を適用しようとせず、Lambda・Fargateから始めて感覚をつかんでから、EC2に進む順序がリスクが少ない。

皆さんのチームでCompute Optimizerを使っていて「ここがうまくいった」「ここでハマった」という話があればぜひ聞かせてほしい。

U

Untanbaby

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

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

関連記事