AWSの検証アカウントでうっかり予算を超えてしまったので原因を振り返りつつカイゼンしてみた

うっかりAuroraのDBクラスターを立ち上げ放置してしまいました。二度と同じことが起きない様に、StepFunctionsでEC2・RDSへの滅びのステートマシンを作ったりしました。取り扱いは本当に要注意です。
2022.12.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。

みなさん、検証用のAWSアカウントで各種リソースを立ち上げたまま放置してしまい、意図しない課金を発生させたことはありませんか?

私はあります。(泣)

弊社では個人に検証用のAWSアカウントを付与するのですが、その環境でAurora DBクラスターを立ち上げたまま土日を迎えてしまい、数日で85USD程の課金を発生させてしまいました。

今後同じ轍は踏まない様に課金が発生するまでにやっていたことをまとめつつ、課金後に改善したみたことをまとめてみました。

課金前にやっていたこと

Budgetsによる予算の監視

AWSの利用日が予算を超えた、または超える予測の際に会社のメールアドレスに通知する様に設定していました。

基本的かつ最も有効な設定ですね。

基本的にBudgetsを設定することででうっかりミスに気付けるのですが、私の場合 金曜日に立ち上げたDBクラスターを土日の間稼働させてしまいました。土日はもちろん会社のメールはみません。

Slackに毎日使用額を通知

以下ブログで紹介されている方法でSlackに利用額を通知していました。

私の場合、EメールよりもSlackの方が圧倒的に見やすいので、基本的にこちらで利用額を確認していました。

ただし、私の場合 その土日は珍しく私用に追われていてSlack通知をちゃんと確認していませんでした。こんな時に限って。

ここまでを踏まえて対策を整理

  • 金曜日に検証用のAurora DBクラスターを立ち上げて放置してしまった
  • 土日にメールやSlackで通知は来ていたが気づかない。もしくは流し見してしまった
  • 問答無用でリソースを停止・削除するジョブなどを導入していなかった

せっかく通知を入れているのに「どうせ大丈夫だろうというバイアスから見ない」とか「そもそも見てない」というのはよくあることですね(今回は土日という要素もありますが)。

これらを踏まえて以下のようなカイゼンをしてみることにしました。

カイゼンしたこと

今の予算を意識しやすくする

比較的最近ですが、マネジメントコンソールのホーム画面がカスタマイズできるようになっています。

同僚から「コストと使用状況(Cost and Usage)」のウィジェットを見やすくした方がおすすめというアドバイスをいただいたので、下画像のように目に入りやすいように一番上に配置する様にしました。

20221212_custom_dashboard_imaizumi

今回のように「AWS環境を見ることができない数日間の間に課金」パターンに直接的には有効ではありませんが、普段からコストを意識できるようになる上、お手軽です。

また、Slackへの通知の文言を若干変更しました。 変更前はこちら。

20221212_before_slack_notification

変更後は価格を強調表示にして、自分にとって見やすい配置にしました。

20221212_after_slack_notification

検証環境のRDSインスタンスとEC2インスタンスを削除するジョブを導入(取扱注意)

検証環境のRDSとEC2のリソースを問答無用で削除する処理を導入しました。

方法はいくつかあると思うのですが、自分でカスタマイズも柔軟にでき、各種APIをサーバレスで実行できるAWS Step Functionsを利用しました。

ステートマシンの流れは以下のようにして、EC2のインスタンス・RDSのDBインスタンス・DBクラスターを削除します。

20221212_sfn_workflow_image

現在はさっと起動しがちで放置しがちなサービスのみ削除の対象にしていますが、自分がよく利用するリソースの増加に伴ってステートマシンに追加していく予定です。

EventBridge Schedulerで毎日深夜に呼び出して、問答無用でリソースの削除を行なっています。(そもそも検証環境であるため)

今は検証環境の特定リージョンのインスタンス全てを対象にしていますが、「特定のタグづけされたインスタンスは対象にしない」といった対策をした方が良いかもしれません。

20221212_eventbridge_scheduler_sfn_call

Step Functionsのステートマシン実行失敗時には、EventBridgeを利用してSlackへ通知します。

今回はシンプルにEventBridgeのターゲットにSlackのウェブフックのエンドポイントを指定してメッセージの通知を実装しています。

方式について知りたい方は以下記事がわかりやすいのでご参照ください。

なお、Step FunctionsのステートマシンについてはAWS CDK(AWS クラウド開発キット)で実装しています。

実装コードについては、思ったよりも大きくなったので別途記事にしたいと思います。

さっとステートマシンの定義を参考にしたい場合、以下をご確認ください。

※ 注意: 以下の削除処理はアカウント内の特定リージョンの全てのEC2インスタンス・RDSのDBインスタンス・DBクラスターを対象とします。カスタマイズして利用するか、ご理解の上そのままご利用ください。

{
  "StartAt": "Parallel executtion of delete operations",
  "States": {
    "Parallel executtion of delete operations": {
      "Type": "Parallel",
      "End": true,
      "Branches": [
        {
          "StartAt": "Describe EC2 instances",
          "States": {
            "Describe EC2 instances": {
              "Next": "has terminate targets?",
              "Type": "Task",
              "ResultSelector": {
                "InstanceIDs.$": "$.Reservations[*].Instances[*].InstanceId",
                "length.$": "States.ArrayLength($.Reservations[*].Instances[*].InstanceId)"
              },
              "Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
              "Parameters": {
                "Filters": [
                  {
                    "Name": "instance-state-name",
                    "Values": [
                      "running"
                    ]
                  }
                ]
              }
            },
            "has terminate targets?": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.length",
                  "NumericGreaterThan": 0,
                  "Next": "Delete Terminate Instances"
                }
              ],
              "Default": "no instances"
            },
            "no instances": {
              "Type": "Succeed",
              "Comment": "exit because no target instances"
            },
            "Delete Terminate Instances": {
              "End": true,
              "Type": "Task",
              "Resource": "arn:aws:states:::aws-sdk:ec2:terminateInstances",
              "Parameters": {
                "InstanceIds.$": "$.InstanceIDs"
              }
            }
          }
        },
        {
          "StartAt": "Describe RDS instances",
          "States": {
            "Describe RDS instances": {
              "Next": "Iterete delete DB Instances",
              "Type": "Task",
              "OutputPath": "$.DbInstances[?(@.DbInstanceStatus == 'available')].DbInstanceIdentifier",
              "Resource": "arn:aws:states:::aws-sdk:rds:describeDBInstances",
              "Parameters": {}
            },
            "Iterete delete DB Instances": {
              "Type": "Map",
              "Comment": "Iterete Delete RDS Instances",
              "Next": "Describe DBClusters",
              "Iterator": {
                "StartAt": "Delete DB Instances",
                "States": {
                  "Delete DB Instances": {
                    "End": true,
                    "Type": "Task",
                    "Resource": "arn:aws:states:::aws-sdk:rds:deleteDBInstance",
                    "Parameters": {
                      "DbInstanceIdentifier.$": "$",
                      "SkipFinalSnapshot": true
                    }
                  }
                }
              },
              "MaxConcurrency": 1
            },
            "Describe DBClusters": {
              "Next": "Iterete delete DB Clusters",
              "Type": "Task",
              "OutputPath": "$.DbClusters[?(@.Status == 'available')].DbClusterIdentifier",
              "Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters",
              "Parameters": {}
            },
            "Iterete delete DB Clusters": {
              "Type": "Map",
              "Comment": "Iterete Delete DBClusters",
              "End": true,
              "Iterator": {
                "StartAt": "Delete DBClusters",
                "States": {
                  "Delete DBClusters": {
                    "End": true,
                    "Type": "Task",
                    "Resource": "arn:aws:states:::aws-sdk:rds:deleteDBCluster",
                    "Parameters": {
                      "DbClusterIdentifier.$": "$",
                      "SkipFinalSnapshot": true
                    }
                  }
                }
              },
              "MaxConcurrency": 1
            }
          }
        }
      ]
    }
  }
}

今後について

まだカイゼンをしてみたばかりであるため、今後続けてみて効果がわかってきたらどこかで報告してみたいと思います。

余談なのですが、今回立ち上げ放置してしまったAurora DBクラスターは以下のブログで自動停止を試している時に消し忘れたものです。

節約方法を紹介する記事を書くために、節約の重要性を知れたので必要なことだったのではないでしょうか?

そうに違いないですね。

この記事がどなたかの参考になれば幸いです。