常時稼働が不要なRDSインスタンスを停止してAWS料金を節約する

SREチームの長田です。 今回は開発・検証用Amazon RDS(以下RDS)の運用のはなしです。

はじめに

「常時使用するわけではないけど、一定の頻度で必要になるデータベース」というものがあります。 AWSリソースの動作確認を行う環境(カヤックではこれを「ステージング環境」と呼ぶことが多いです)や、 リリース後の負荷試験環境など、本番環境とは異なる環境にあるデータベースがこれにあたります。

AWSのようなクラウドサービスを利用している場合、起動時間に対して課金が発生することが多いでしょう。 負荷試験用に用意したRDSインスタンスは、試験が実施されていない期間はただ課金が発生するだけのリソースになってしまいます。

たまにしか使われないデータベースを放置しておくのはもったいない

負荷試験で使用するものは、大抵の場合本番環境と同じスペックのものを用意することになるでしょう。 すると本番環境と同様の課金が発生するわけですが、試験機関以外に使用されないのに本番環境と同等の課金が発生するのは 少々、というかかなりもったいないです。

RDSインスタンスは停止していれば課金は発生しません *1。 つまり、使用しない期間はRDSインスタンスを停止してしまえば無駄な課金を抑えることができるわけです。

RDSは停止し続けられない

しかし、RDSインスタンスは恒久的な停止ができません。 停止後、7日間が経過すると自動で起動するという仕様になっています。 ドキュメントにも「一時的」な停止であることが明記されており、重要事項として以下の記述があります。

DB インスタンスは最大 7 日間停止できます。 7 日後に DB インスタンスを手動で起動しなかった場合、DB インスタンスは自動的に起動されます。

docs.aws.amazon.com

止め続けたい場合は?

7日以上停止し続ける手段として、AWS re:Postの 情報センターには AWS Lambda を使った方法と、AWS Step Functions を使った方法が紹介されています。

repost.aws repost.aws

どちらもRDSインスタンスに設定されたメンテナンスウィンドウ前に対象クラスタを起動し、 メンテナンスウィンドウ後に停止するという仕組みです。

これをそのまま使用しているAWSアカウントにデプロイしてもよいのですが、 同じような停止し続ける要件が出るたびにソースコードをコピペしてデプロイするのは面倒だと感じたので、 同様の動作をするツールを作ることにしました。

let-rds-sleep

作ったツールがこちらです。

github.com

Lambdaのカスタムランタイムで動作させ、EventBridge Ruleで定期実行することを想定しています。

let-rds-sleep には以下の2つの動作モードがあります。

モード 動作 実行タイミング
START RDSインスタンスを起動する メンテナンスウィンドウ前
STOP RDSインスタンスを停止する メンテナンスウィンドウ後

環境変数で動作モードを指定した START STOP それぞれのLambda functionを作成し、 EventBridgeのルールでメンテナンスウィンドウ前後にそれぞれのLambda functionを実行するように設定します。 具体的な設定例はリポジトリの terraform/example ディレクトリにありますのでそちらを参照いただくのがわかりやすいかもしれません。

基本的には re:Post にあるPythonによる実装例に倣って実装していますが、いくつか改良ポイントがあります。

dryrunできる

オプション -dryrun を付けて実行すれば、実際には起動・停止の処理を行わず、 処理されるインスタンスの一覧を表示します。 いきなり停止処理を実行する勇気はなかったので実装しました。

同一AWSアカウント内に本番環境で使用中のインスタンスがある場合、 うっかり停止対象インスタンスを間違えたなんてことになると大事故です。

AWSのAPIをリクエストするのに必要な権限さえあればローカル環境でも動作しますので、 実際にLambda functionで実行する前に、まずはdryrunで対象インスタンスを確認することを強くおすすめします

以下はdryrunの実行例です。 処理対象となるインスタンス/クラスタをログとして出力しています。

$ ./let-rds-sleep -mode STOP -target Sleep=true -dryrun
started as oneshot app
2023/08/30 16:08:57 [INFO] running as STOP mode
2023/08/30 16:08:58 [INFO] processing cluster/main
2023/08/30 16:08:58 [INFO] cluster/main will be STOP [dryrun]
2023/08/30 16:08:58 [INFO] processing cluster/loadtest
2023/08/30 16:08:58 [INFO] cluster/loadtest will be STOP [dryrun]
2023/08/30 16:08:58 [INFO] processing instance/sandbox
2023/08/30 16:08:58 [INFO] instance/sandbox will be STOP [dryrun]
bye

柔軟なタグ指定

参考とした re:Post のPythonによる実装例と同じく、 インスタンスに付与されているタグを見て処理対象とするかどうかを判断していますが、 let-rds-sleep ではもう少し柔軟性を持たせています。

  • -target にタグ指定がなければ、すべてのインスタンスを対象にする
  • -target にタグ指定(複数可)があれば、全てのタグが付与されたインスタンスを対象にする
  • -exclude にタグ指定(複数可)があれば、全てのタグが付与されたインスタンスを対象外とする

たとえば -exclude IgnoreSleep=true を付けて let-rds-sleep をデプロイしておき、 負荷試験期間中はRDSインスタンスに IgnoreSleep=true というタグを付与しておけば、 一時的に停止対象から外すことができるというわけです。

カスタムランタイムで動作する

let-rds-sleepはgoで書かれています。 ビルド済みのバイナリをリリースしているので、 これをLambdaのカスタムランタイムで動作させることができます。

カスタムランタイムなので、言語ごとのランタイムのようにEoLに付き合う必要がありません。 一度Lambda functionとしてデプロイすれば後はメンテナンスフリーというわけです。

おわり

managedな停止ソリューションとして、クラスメソッドさんが提供している opswitch というサービスがあるようです。

dev.classmethod.jp

セットアップとしてはこちらの方が断然お手軽なので、 利用規約を把握した上で利用の判断をするとよいでしょう。

ソース部分のコピペを許容するのであれば、先に挙げたre:PostにあるStep Functionsを使った例を借用するのが手っ取り早いでしょう。

RDSインスタンスを起動しっぱなしにするのはもったいないので適宜掃除をしましょう。 停止ではなくスナップショットをとって削除してしまったほうがいい場合もあるでしょう。 条件に合う場合は let-rds-sleep を使ってみてください。

カヤックではあの手この手でコスト削減図るエンジニアを募集しています

*1:プロビジョニングされたストレージにはインスタンスの起動状況に関係なく課金が発生します。