1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】個人アカウントの料金が大変なことになってきたので棚卸ししてみる

1
Posted at

はじめに

ここ最近、AWS のサービスを色々使って遊んできましたが、いい加減そろそろ利用料金がヤバくなってきたので現実をみてみることにしました。

Kiro に AWS MCP 経由で Cost Explorer を叩いてもらい、様々な角度で棚卸しをしています。総額だけみていたら分からなかったことも明らかになり、AI とコスト分析の相性の良さを感じることができました。

また、すぐに削減できそうな影響の小さい施策もいくつか実行しておきました。
早くやっておけば良かった。。

全体像

まず月次の総額推移を Cost Explorer で確認してみます。
AWSCostPast1Y.png

分かりやすく7月から一気に跳ね上がっています。8月も高止まりの状態です。
表に書き表わすと次のような感じ。

総額(USD) 状態
1〜5月 357〜398 定常
6月 441 定常(やや上昇)
7月 908 急増
8月 890 高止まり

定常時の平均は約\$390。そこから7-8月は月+\$500前後です。
ほぼ倍増。こうしてみるとすごい金額・・・

全体像が把握できたので、もう少し分解していきます。

リージョンで分解

このアカウントでは、大まかにリージョンごとに利用用途を分けています。

  • 東京(ap-northeast-1)= 本番系:常時稼働のインフラ(Lambda / RDS / ECS など)
  • オレゴン(us-west-2)= 検証系:普段は小さいが、新しいワークロードを流すお試し環境
  • バージニア(us-east-1):Kiro のサブスク専用

リージョン別に月次料金を並べると、増加の出どころが一目で分かります。
AWSCostExplorerByRegion.png

リージョン 5月 6月 7月 8月 役割
東京 290 280 291 469 本番系
オレゴン 13 58 316 116 検証系
バージニア 41 45 201 207 Kiro枠

全体的に自分の感覚とおおよそ一致していますが、東京リージョンの8月高騰が想定外。
8月分が高止まりになっていた原因がここにありました。

続いて、サービス別に増加要因を深堀りしていきます。

増加要因の分解

サービス別の内訳は、リージョンごとに分解することで具体的な状況が分かりますので、順に追ってみます。

バージニア(us-east-1)

AWSCostExplorerByService-us-east-1.png

サービス 5月 6月 7月 8月
Kiro \$40.00 \$43.91 \$200.00 \$205.96
その他(KMS / CloudWatch 等) \$1.03 \$1.00 \$1.41 \$1.23

バージニアはほぼ Kiro 枠で想定通りです。
7-8月は夏休みの自由研究のために Kiro Power にしてました。後悔はないです笑

オレゴン(us-west-2)

AWSCostExplorerByService-us-west-2.png

サービス 5月 6月 7月 8月
DynamoDB \$0.02 \$42.22 \$268.81 \$14.38
OpenSearch \$0 \$0 \$0 \$26.80
Bedrock \$1.75 \$4.10 \$30.19 \$58.47
WAF \$6.00 \$6.00 \$6.00 \$6.00
Amplify \$5.31 \$4.09 \$3.42 \$0.81
Lambda \$0.01 \$0.01 \$2.22 \$4.31
その他 \$0.44 \$1.69 \$5.05 \$5.11

7月と8月で、大きく3つの要因が見て取れます。

7月: DynamoDBの負荷テスト

7月の DynamoDB \$268.81 を使用タイプ別に見ると、WriteCapacityUnit-Hrs(プロビジョンドの書き込みキャパシティ)が主な内訳でした。

使用タイプ 6月 7月 8月
WriteCapacityUnit-Hrs(コスト) \$34.47 \$245.11 \$11.31
WriteCapacityUnit-Hrs(時間) 71,630h 395,690h 36,000h

これは「DynamoDB でホットパーティションを起こしてみよう!」と試行錯誤した結果ですね。
詳細は以下の検証記事にまとめていますが、複数のプロビジョンドテーブル(WCU 10,000)に高RPSの負荷を投入しました。

ただ万事が無駄なく検証に回せていたかと言うとそんなことはなく、実際のところは、検証後にキャパシティを戻し忘れていた分も混在しています。
検証したらやりっ放しにせず、お片付けまでしっかりと」基本ですね。

8月: ベクトル検索検証のOpenSearch

8月の OpenSearch \$26.80 は、ベクトル検索を試すために立てたクラスターです。こちらも DynamoDB ベクトル検索の対比用に立てたものになりますが、この時はある程度コストを見積もってから実施したので、おおよそ計画通りです。検証が終わった時点で削除済みなので、今後は発生しません。また、同じ検証で使った DynamoDB はオンデマンドモードで回したため、ここでの費用はほとんど発生していません。

7-8月: Bedrock の増加分

Bedrock は5月の\$1.75から8月の \$58.47 まで右肩上がりです。主要因となっているモデルは Claude Sonnet 5 と見られますが、ここは正直、具体的な原因が特定できていません
AI エージェントもポコポコ立てて試しているので、もう少し様子を見ながら必要に応じて見える化も考えてみます。

東京(ap-northeast-1)

AWSCostExplorerByService-ap-northeast-1.png

サービス 5月 6月 7月 8月
RDS \$95.83 \$93.72 \$95.83 \$274.39
EC2(その他 + Compute) \$94.53 \$91.40 \$94.64 \$94.30
ElastiCache \$19.34 \$18.72 \$19.34 \$19.34
Lambda \$19.47 \$17.95 \$19.36 \$19.37
ECS \$11.46 \$11.09 \$11.46 \$11.46
VPC \$11.16 \$10.80 \$11.16 \$11.16
その他(SES / Secrets / StepFunctions等) \$37.96 \$36.63 \$39.28 \$38.56

東京リージョンは7月まで横ばいで、8月だけ RDS が跳ね上がっています。
使用タイプ別に見ると原因は明確になりました。

使用タイプ 7月 8月
InstanceUsage: db.t3.micro \$38.69 \$38.69
RDS: GP2-Storage \$30.36 \$30.36
RDS: ProxyUsage \$26.78 \$26.78
ExtendedSupport: Yr1-Yr2: MySQL8.0 \$0 \$178.56

AWSCostExplorerByService-ap-northeast-1-RDS.png

見慣れぬ ExtendedSupport:Yr1-Yr2:MySQL8.0 という使用タイプが8月から新規に発生し、毎日 \$5.76 が発生しています。他の使用タイプはそのままです。
これは MySQL 8.0 系の標準サポート終了に伴う、有償の Extended Support への自動移行によるものでした。

自動以降に関しては事前連絡があったはずですが、全然認識出来ていませんでした。このタイミングで気付けたのは運が良かったかもしれません

セキュリティ的にもこちらの対策は必須と考えていますが、影響が大きいので別途対応方法を検討することにします。

コスト削減施策

ということで、リージョンごとに分解した結果を踏まえ、上述のリソース以外も含めてコスト削減施策をいくつか Kiro に挙げてもらいました。

基準となる定常コスト

削減率を測るための基準(ベースライン)を、今後も続くAWSインフラの月額として定義します。

  • 定常インフラ(1〜6月の平均):\$390/月
  • RDS MySQL 有償サポート(8月から恒常化):+\$178/月
  • 合計ベースライン\$568/月

Kiro枠と検証系(オレゴン)の一時的な増加分(DynamoDB 他)は、ベースラインには含めません。

施策と削減率

こちらが、削減率と影響度合いから実行する価値があると判断した施策の一覧です。

# 施策 月次削減 影響の重さ 扱い
1 ECS踏み台の常時稼働をやめる -\$11 軽(インフラのみ) 本記事で実施
2 RDS ストレージを GP3 化 -\$6 軽(ほぼ無停止) 本記事で実施
3 使っていないElastiCacheの停止 -\$19 中(アプリコード要確認) 本記事で実施
4 RDS 有償サポートの回避(8.0→8.4) -\$178 重(本番DB・不可逆) 別途検討
5 NAT Gateway の見直し -\$45〜92 重(ネットワーク構成) 別途検討

施策を積み上げると、最終的な目標コストは月\$262〜309。ベースライン \$568 から見て約46〜54%の削減を目指してみます。

ただ、本番系が絡むところは影響が大きいので、本記事では軽くてすぐ効く3つの施策(施策1〜3)から先に片付けます。

施策1: ECS踏み台の常時稼働をやめる

現状把握

東京リージョンの ECS に常時1タスクで稼働しているサービスを Kiro に見つけられました。

中身は次の通り。

  • mysql:8.0 イメージを command: ["sleep", "infinity"] で起動しているだけ
  • enableExecuteCommand: true → ECS Exec で中に入り、mysqlクライアントでRDSに繋ぐ

要するにRDSに入るための踏み台です。

これは当時、RDS に繋いでメンテナンス作業をする際の手順を簡略化するために構築したものになります。「使い終わったら desiredCount=0 に戻すつもりが、面倒で放置していた」というのが実態です。

結局、24時間365日起動が状態化しており、0.25 vCPU 常時稼働で月約 \$11 です。メンテナンス作業自体は今後も 週1〜2回で各20〜40分 程度必要となるため、自動で止める処理を追加します。

システム設計

最終接続から2時間で自動停止させるためのシステム設計を行います。

最初は「接続イベント(SSMのStartSession)をEventBridgeで捕まえてイベント駆動で停止のタイマー予約を入れる」方式を検討しましたが、本アカウントでは CloudTrail を利用していないためイベントを拾えず、断念。

そこで今回は、CloudTrailに依存しないポーリング方式で進めることにしました。

EventBridge Scheduler(15分ごと)
  └→ Lambda が判定
       1. サービスの desiredCount が 0 なら何もしない
       2. アクティブな踏み台セッションがあれば止めない
       3. 「最終セッションの切断時刻」と「タスクの起動時刻」の
          遅い方から2時間経過していれば desiredCount=0

アイドルの起点を 「最終接続」と「タスク起動」の遅い方にしているのがポイントです。最終接続は SSM DescribeSessions(History)の EndDate から、タスク起動は ECS の DescribeTasksstartedAt から取ります。

Lambda 本体

判定ロジックはこれだけです。_latest_task_started_at() で実行中タスクの起動時刻を取り、最終接続時刻との遅い方をアイドルの起点にしています。

import os
import boto3
from datetime import datetime, timezone, timedelta

CLUSTER = os.environ.get("CLUSTER", "rds_ope_cluster")
SERVICE = os.environ.get("SERVICE", "rds_ope_service")
# 踏み台タスクの SSM ターゲットは "ecs:<cluster>_..." 形式
TARGET_PREFIX = os.environ.get("TARGET_PREFIX", "ecs:rds_ope_cluster")
IDLE_MINUTES = int(os.environ.get("IDLE_MINUTES", "120"))

ecs = boto3.client("ecs")
ssm = boto3.client("ssm")


def _is_bastion(target: str) -> bool:
    return bool(target) and target.startswith(TARGET_PREFIX)


def _latest_task_started_at(now):
    """実行中タスクの最新 startedAt を返す。起動途中なら now を返して保護側に倒す。"""
    arns = ecs.list_tasks(
        cluster=CLUSTER, serviceName=SERVICE, desiredStatus="RUNNING"
    ).get("taskArns", [])
    if not arns:
        return None
    tasks = ecs.describe_tasks(cluster=CLUSTER, tasks=arns).get("tasks", [])
    started = [t.get("startedAt") for t in tasks if t.get("startedAt")]
    # startedAt が未設定のタスク(起動途中)があれば now を返して停止を防ぐ
    if len(started) < len(tasks):
        return now
    return max(started) if started else now


def handler(event, context):
    # 1. desiredCount が 0 なら何もしない
    desc = ecs.describe_services(cluster=CLUSTER, services=[SERVICE])
    if not desc.get("services"):
        return {"action": "skip", "reason": "service not found"}
    if desc["services"][0].get("desiredCount", 0) == 0:
        return {"action": "skip", "reason": "already stopped"}

    # 2. アクティブな踏み台セッションがあれば止めない
    active = ssm.describe_sessions(State="Active", MaxResults=50).get("Sessions", [])
    if any(_is_bastion(s.get("Target", "")) for s in active):
        return {"action": "keep", "reason": "active bastion session exists"}

    now = datetime.now(timezone.utc)

    # 3. 「最終接続」と「タスク起動」の遅い方からのアイドルで判定
    history = ssm.describe_sessions(State="History", MaxResults=50).get("Sessions", [])
    ends = [s.get("EndDate") for s in history
            if _is_bastion(s.get("Target", "")) and s.get("EndDate")]
    last_end = max(ends) if ends else None
    task_started = _latest_task_started_at(now)

    references = [t for t in (last_end, task_started) if t is not None]
    if not references:
        return {"action": "keep", "reason": "no reference time yet"}

    idle_from = max(references)
    idle = now - idle_from
    if idle >= timedelta(minutes=IDLE_MINUTES):
        ecs.update_service(cluster=CLUSTER, service=SERVICE, desiredCount=0)
        return {"action": "stopped", "idle_from": idle_from.isoformat(),
                "idle_minutes": round(idle.total_seconds() / 60, 1)}
    return {"action": "keep", "idle_from": idle_from.isoformat(),
            "idle_minutes": round(idle.total_seconds() / 60, 1)}

IAMロールは最小権限にしています。ECS操作は対象クラスタに限定し、DescribeTasks はクラスタ配下のタスクだけに絞りました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["ecs:UpdateService", "ecs:DescribeServices"],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "ecs:cluster": "arn:aws:ecs:ap-northeast-1:<ACCOUNT_ID>:cluster/rds_ope_cluster"
        }
      }
    },
    { "Effect": "Allow", "Action": ["ecs:ListTasks"], "Resource": "*" },
    {
      "Effect": "Allow",
      "Action": ["ecs:DescribeTasks"],
      "Resource": "arn:aws:ecs:ap-northeast-1:<ACCOUNT_ID>:task/rds_ope_cluster/*"
    },
    { "Effect": "Allow", "Action": ["ssm:DescribeSessions"], "Resource": "*" }
  ]
}

スケジュールは15分間隔のポーリングです。

aws scheduler create-schedule \
  --name ecs-bastion-autostop-poll \
  --schedule-expression "rate(15 minutes)" \
  --flexible-time-window '{"Mode":"OFF"}' \
  --target '{"Arn":"<LAMBDA_ARN>","RoleArn":"<SCHEDULER_ROLE_ARN>","Input":"{}"}'

結果

実際に止まることを確認できました。
これで無駄な起動時間を抑えることができます。

施策2: RDS ストレージを GP2 から GP3 へ

なぜ安くなるのか

RDS のストレージは、古い世代の gp2(汎用SSD)のままでした。後継の gp3 は容量あたりの単価が約20%安く、しかも 3,000 IOPS までは追加料金なしで付いてきます。gp2 → gp3 に変えるだけで、性能はそのままか向上して、料金だけ下がるという「やらない理由がない」変更です。削減効果は -\$6 ほどですが、停止を伴わない変更でリスクもないので実施しておきます。

コンソールでの手順

RDSコンソールから対象のDBインスタンスを選び、右上の「変更」を開きます。
AWSCostExplorerRDSSetting1.png

「ストレージ」セクションで、ストレージタイプを 汎用SSD(gp2) から 汎用SSD(gp3) に変更します。
AWSCostExplorerRDSSetting2.png

適用タイミングを選びます。すぐに反映したい場合は「すぐに適用」を選択します。
AWSCostExplorerRDSSetting3.png

適用後、ストレージタイプが gp3 になっていることを確認します。
AWSCostExplorerRDSSetting4.png

以上です。簡単ですね。

施策3: 使っていない ElastiCache を止める

利用状況

コスト調査の中で、東京リージョンに ElastiCache(Redis / cache.t3.micro / 1ノード)が見つかりました。月約\$19です。

こちらの利用状況をメトリクスで確認したところ、

  • Get/Set などコマンド系メトリクスがすべて0(読み書きゼロ)
  • CacheHits / CacheMisses / CurrItems もすべて0、メモリ使用率は最大1.5%程度
  • 一方で接続数(CurrConnections)は平均4〜5ある

となっており、キャッシュとしては一切使っていないことが分かりました。

さらに詳細を確認すると、過去作成したアプリの拡張用に作ったものの、実際には使われなかった機能であることが分かりました。作成から約9ヶ月、\$19/月をずっと垂れ流していました

これは単純にリソースを削除しておこうと思います。

コンソールでの手順

ElastiCache コンソールで対象クラスターを開き、メトリクスから「本当に使われていない」ことを最終確認します。新しい接続が全く発生していないことなどがグラフからも見て取れます。

AWSCostExplorerElastiCache1.png

裏取りできたら、クラスターを選択して「削除」します。バックアップの要否を確認するダイアログが出るので、必要に応じて最終バックアップを取ります。今回はそのまま捨てます。

AWSCostExplorerElastiCache2.png

削除後、クラスターが一覧から消えたことを確認します。あわせて、アプリが接続エラーを出していないかログでチェックします。

AWSCostExplorerElastiCache3.png

以上となります。

おまけ

最後に予約購入(RI・Savings Plans)の効果も確認しておきます。

ここまでの施策は、無駄なものは止めることでしたが、AWS では予約購入することで利用料を抑えることも可能です。

Cost Explorer の購入推奨機能(実際の使用状況ベース)で、Kiro に一通り確認してもらいました。

対象 推奨
RDS Reserved Instances 1年前払いなしで-\$8.47/月(22%)、3年全前払いで-\$19.35/月(51%)
Compute Savings Plans 推奨なし(コミットする価値がある使用量ではないと判定)

やはり、RDS のリザーブドインスタンスを購入するのが一定の効果を見込めそうです。
Extended Support を抜けるためにメジャーアップグレードを実施してから、実際に購入する検討を進めたいと思います。

まとめ

今回は、いい加減ヤバくなってきた利用料金に正面から向き合ってみました。

Kiro も上手く活用することで、現状の見える化だけでなく削減施策や影響度調査も合わせて進められました。
無駄に動き続けていたリソースやサポート切れなども見つかり、単純なコスト削減以上の効果があったかなと思います。

次は残る大物への対応を進めて、更なるコスト削減とセキュリティ向上を図っていきたいです。

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?