【AWS】高額請求にはもううんざり!RDSの自動起動で知らないうちに課金される状況の回避策を考えてみる


はじめに

こんにちは。

突然ですがAWSアカウントを検証用途などで自己保有していて、リソースの消し忘れで高額請求された経験のある方はいらっしゃいますか?

わたしはあります。しかも過去に2度も。。。

どちらもAuroraを終了ではなく停止にしていたことで、停止してから7日後に自動起動してしまい、請求が来た時に初めて課金額が高額になっていたことがわかった、という経緯でした。

私の場合はAWSの勉強をし始めたときに「検証で利用したリソースは検証が終了したらすぐに削除すること」と口酸っぱく言われていたのですが、「また使うかも?作り直すのも面倒だし、終了じゃなくて停止にしておこう」というもったいない精神が働いてしまって、2度も痛い目を見てしまいました。。

検証用リソースはすぐに削除するというのは大前提として、2度あることは3度あるとも言いますし、同じ失敗はしたくないのでリソースの削除を行わなかった場合の高額請求にならないための回避策を考えてみました。

回避策

構成図

回避策として、Auroraが起動したのをトリガーに、LambdaでAuroraを自動的に停止しにいくような構成を作成したいです。

以下のような流れを考えてみました。

①RDSイベントサブスクリプションを設定し、Auroraクラスターが起動したイベントをSNSにプッシュする。
②SNSトピックをトリガーにLambdaを起動する。
③①でプッシュされたイベントをSNSを経由してユーザーにEメールで通知する。(今回の回避策という観点で言うとなくてもいい)
④①でプッシュされたイベントがAuroraクラスターが起動したという内容のイベントの場合、Auroraを停止する。

実現できそうですね。実際の構築に入っていきたいと思います。

構築

構築していくにあたっての前提として、Auroraが「testdb」という名前で構築済みであることとします。

RDSイベントサブスクリプションの設定

コンソール上でRDSの画面に遷移し、イベントサブスクリプションをクリックし、「イベントサブスクリプションを作成」をクリックします。

以下でサブスクリプションの設定を行い、「作成」をクリックします。

  • 名前:subscription01
  • ターゲット:新しいEメールトピック
  • トピック名:任意の名前(ここでは「rds-subscription-topic」)
  • これらの受取人を含みます:通知させるEメールアドレス
  • ソース:インスタンス
  • インスタンスを含みます:特定のインスタンス
  • 特定のインスタンス:イベント通知させるインスタンス(ここでは「testdb-instance-1」)
  • イベントカテゴリを含みます:特定のイベントカテゴリ
  • 特定のイベントカテゴリ:norification イベントサブスクリプションが作成されたことを確認します。

上記で設定したEメールアドレスあてに確認メールが届くので、メール本文に記載のリンクから確認しておきます。

Lambda作成

コンソール上でLambdaの画面に遷移し、「関数の作成」をクリックします。

以下で関数の設定を行い、「関数の作成」をクリックします。

  • 関数の作成:一から作成
  • 関数名:任意の関数名(ここでは「aurora-stop-test」)
  • ランタイム:Python(ここでは「Python3.6」)
  • アクセス権限:基本的な Lambda アクセス権限で新しいロールを作成(後程IAMポリシーは追加します)


作成されたIAMロールには以下のIAMポリシーを追加しておきます。

  • AmazonRDSFullAccess
  • AmazonSNSFullAccess

タイムアウト値はデフォルトの3秒だと短いので15秒くらいに設定しておきます。

作成された関数の概要から「トリガーを追加」をクリックし、RDSイベントサブスクリプションの設定時に作成したSNSトピックをトリガーに設定します。

  • トリガーを選択:SNS
  • SNSトピック:RDSイベントサブスクリプション設定時に作成したSNSトピック(ここでは「rds-subscription-topic」)

コードは以下を設定します。

lambda_function.py
import json
import boto3
import re
client = boto3.client('rds')

def lambda_handler(event, context):

### Formatting SNS topic messages

    print('===Loading function===')

    message_json = json.loads (event ['Records'][0]['Sns']['Message'])

    notification_message = message_json['Event ID']
    formatting_message = re.split('#', notification_message)
    event_id = formatting_message[1]

    notification_cluster = message_json['Source ID']
    formatting_cluster = re.split('-', notification_cluster)
    cluster = formatting_cluster[0]

    print('===RDS Cluster:' + cluster + '===')
    print('===RDS Event:' + event_id + '===')

### Stop the Aurora Cluster

    print('===Evaluate the event id===')

    if event_id == 'RDS-EVENT-0088' or event_id == 'RDS-EVENT-0154':
        print('===Stop the cluster because the event id is "' + event_id + '".===')
        response = client.stop_db_cluster(
            DBClusterIdentifier=cluster
        )
        print('===stopped cluster: ' + cluster + '===')
    else:
        print('===Do not stop the cluster because the event id is "' + event_id + '".===')

※RDSのイベントIDは88と154にしています。88は起動時で154は自動起動したときのイベントです。今回は手動でAuroraの起動停止をした際の動確もしたいのでID88も入れていますが、動確ができたら削除してもいいかもしれません。
自動起動時のイベントIDを確認したら88でした。ですのでLambdaのコードは上記のままで問題ないかと思います。(2021/6/28追記)
参考:Amazon RDS イベント通知の使用

構築は以上です。続いて動作確認をしていきましょう。

動作確認

コンソールでRDSの画面に戻り、インスタンスを起動させてみます。
起動が完了するとメールが届くので、メールが届いたことを確認してからCloudWatchLogsを確認してみます。

Auroraの停止コマンドが実行され、Lambdaが正常に終了していることが確認できました。

Auroraもちゃんと停止されています。

以上で動作確認は終了です。

おわりに

今回は、停止したAuroraが7日後に自動起動することによって高額請求されてしまうのを防ぐための構成を作成してみました。
この記事をご覧いただけている方が私と同じ轍を踏まないように、ぜひこちらの構成を活用いただけたらと思います!

とはいえ、、

検証用リソースは検証が終わったらすぐに消す!
これに越したことはありません。
皆さんはリソースの消し忘れや、もったいない精神からリソースを保持しておくのはやめましょう。。

[おまけ]ちょっとした課題点

Auroraが起動されたことを検知するとすぐにLambdaが起動してしまうため、Auroraが起動しきれておらず停止コマンドが正常に完了できずエラーとなってしまいます。

SNSがトリガーとなってLambdaを実行するときは非同期呼び出しとなり、エラーを返すと2回再試行してくれるようです。
参考:非同期呼び出し

回避策として作成している構成のためそこまでの作りこみはしなくてもよいと考えていることと、現状再試行2回目で正常にコマンド実行できているためこれで良しとしていますが、何か他によい設定の仕方やいけてるコードの書き方などあればアドバイスいただければと思います!