Amazon RDS のリザーブドインスタンスは、RDS を一定のサイジングで長期的に使うケースではコスト削減に効果的で、条件が合えばガバメントクラウド環境でも積極的に活用したいサービスです。
しかし、現時点でガバメントクラウドでのリザーブドインスタンスの購入には、以下の制限事項があります。
- 選択できる購入期間は 1 年間のみ
- 会計年度を跨ぐ購入はできないため、購入できる期間は 4/1 から 3/31 までの 1 年間のみ
- 購入オペレーションは、4/1 日本時間午前 9:00 から 9:59 の間の 1 時間しか認められない
- 前払いは認められない
ここで問題となるのが、RDS リザーブドインスタンスは事前に日時を指定して予約購入できないことです。
先日発表された Database Savings Plans の方は事前に日時を指定して購入することが可能です。しかし、Database Savings Plans の方は、インスタンス変更の柔軟性が極めて高い代わりに、割引率がリザーブドインスタンスに比べると劣ります。そのため、引き続きリザーブドインスタンスを選択する場面はあると思われます。
そこで、ガバメントクラウドで実施することを想定し、4 月 1 日の指定した時間に RDS リザーブドインスタンスを購入する方法を、Lambda と EventBridge スケジューラを使って実現できないか検証してみました。
本記事は ガバメントクラウド Advent Calendar 2025 の 7 日目の投稿となります。
Lambda と EventBridge スケジューラによる指定日時の RDS リザーブドインスタンスの購入について
検証構成の概要
今回の検証の構成は次のとおりです。
- Lambda にて、指定の条件で RDS のリザーブドインスタンスを購入する関数を作成
- EventBridge スケジューラで、作成した Lambda 関数を指定した日時に実行
リザーブドインスタンス購入の注意点
リザーブドインスタンスの最大の注意点は、一度購入したらキャンセルができない ことです。
そのため今回の検証で留意するポイントを整理します。
- Lambda 関数が何らかの原因で多重に実行されてリザーブドインスタンスを多重購入してしまわないようにする
- 事前に購入条件を確認して Lambda を実行できるようにする
これを実現するため、Lambda 関数の実行の前に、AWS CLI などでリザーブドインスタンスの購入条件にマッチする Purchase ID という注文 ID を調べておき、これと中でマッチしない条件が関数にセットされていたら処理を中止するようにしました。また、購入条件に合致する既存の RDS リザーブドインスタンスの台数を調べておき、Lambda 関数の中でこの台数が想定と異なる場合は購入を中止するようにしました。
それでは具体的な検証について見ていきます。
RDS リザーブドインスタンスの Purchase ID の事前確認
今回、RDS リザーブドインスタンスは次の条件で購入することにしました。
| 購入条件 | 設定値 |
|---|---|
| リージョン | ap-northeaset-1(東京) |
| インスタンスクラスタイプ | t4g.micro |
| DB エンジン | PostgreSQL |
| マルチ AZ | 無効(シングル AZ) |
| 対象期間 | 1 年間 |
| 支払方法 | 前払いなし |
なお、リザーブドインスタンスはインスタンスサイズやエディションの変更の条件が厳しいため、実際に購入する場合は慎重に検討してください。(変更が予想される場合は Database Savings Plans も選択肢に入れましょう。)
この条件で RDS リザーブドインスタンスを購入した時の料金を AWS の Web サイト で確認すると、1 時間当たり 0.020 USD であることが分かります。
次に AWS CLI で次のコマンドを実行してみます。
$ aws rds describe-reserved-db-instances-offerings \
--db-instance-class "db.t4g.micro" \
--duration "1" \
--product-description "postgresql" \
--offering-type "No Upfront" \
--no-multi-az \
--profile 適切な権限を持つプロファイル名
JSON が出力され、内容を確認するとこの購入条件の Purchase ID が確認できます。また、1 時間当たりの料金が事前に AWS の Web サイトで確認していた 0.02 USD であることも分かります。
出力された JSON の「ReservedDBInstancesOfferingId」キーの値「8e51c316-003d-48b0-ac7f-1be68bcdbc90」を RDS リザーブドインスタンスの購入の際に指定することで、この購入条件で注文ができる仕組みになっているので、後で Lambda コードにハードコーディングするため値を控えておきます。
RDS リザーブドインスタンスを購入する Lambda 関数の作成
RDS リザーブドインスタンスを購入できる権限の IAM ロールの作成
Lambda 関数から RDS リザーブドインスタンスを購入できるようにするため、以下の内容で IAM ロールを作成して Lambda 関数へアタッチするようにします。
許可ポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowWriteCloudWatchLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:ap-northeast-1:*"
},
{
"Sid": "AllowPurchaseReservedDBInstance",
"Effect": "Allow",
"Action": [
"rds:PurchaseReservedDBInstancesOffering",
"rds:DescribeReservedDBInstancesOfferings",
"rds:DescribeReservedDBInstances",
"rds:AddTagsToResource"
],
"Resource": "*"
}
]
}
信頼されたエンティティ
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Python boto3 で RDS リザーブドインスタンスを購入する処理を作成
今回ランタイムは Python で、AWS SDK は boto3 を使ってコードを作成しました。
import logging
import boto3
import botocore
from botocore.config import Config
logger = logging.getLogger()
logger.setLevel("INFO")
# 不慮のリトライで多重購入を防ぐためリトライ数を 0 回に設定
config = Config(retries={"total_max_attempts": 1, "mode": "standard"})
# 購入対象の RDS インスタンスクラスタイプとリザーブドインスタンスの Purchase ID を指定
# Purchase ID は事前に AWS CLI などで調べておき、ダブルチェック用に使う
EXPECTED_PURCHASE_ID = "8e51c316-003d-48b0-ac7f-1be68bcdbc90"
# 購入しようとしている条件の現時点のリザーブドインスタンスの台数を指定
# もし想定している台数と違ったら購入を中止するために使う
EXPECTED_INSTANCE_NUMBER = 0
# 購入する対象のリージョンを指定
REGION = "ap-northeast-1"
# 購入するインスタンスクラスタイプを指定
INSTANCE_CLASS_TYPE = "db.t4g.micro"
# DB エンジンを指定
PRODUCT_DESCRIPTION = "postgresql"
# マルチ AZ を有効にする場合は True を、そうでない場合は False を指定
IS_MULTI_AZ = False
def lambda_handler(event, context):
client = boto3.client("rds", config=config, region_name=REGION)
try:
#
# 購入しようとしている Purchase ID が正しいか確認する処理
#
offering_description = client.describe_reserved_db_instances_offerings(
DBInstanceClass=INSTANCE_CLASS_TYPE,
Duration="1",
ProductDescription=PRODUCT_DESCRIPTION,
OfferingType="No Upfront",
MultiAZ=IS_MULTI_AZ,
)
purchase_id = offering_description["ReservedDBInstancesOfferings"][0]["ReservedDBInstancesOfferingId"]
# 想定している Purchase ID と違ったら処理を中止
if not purchase_id == EXPECTED_PURCHASE_ID:
logger.warn("想定しているインスタンスクラスタイプではないので購入を中止しました。")
return 0
#
# 現在のリザーブドインスタンスの台数が想定どおりか確認する処理
#
current_reserved_instance = client.describe_reserved_db_instances(
ReservedDBInstancesOfferingId=EXPECTED_PURCHASE_ID,
)
current_reserved_instance_number = len(current_reserved_instance["ReservedDBInstances"])
# 現在のリザーブドインスタンスの数が想定している数と違ったら処理を中止
if not current_reserved_instance_number == EXPECTED_INSTANCE_NUMBER:
logger.warn("現在のリザーブドインスタンスの台数が想定と異なるので購入を中止しました。")
return 0
#
# リザーブドインスタンスを注文する処理
#
# 今回は 1 台のみ購入したいため、DBInstanceCount は指定しないことで、
# デフォルト値の 1 台の注文となるようにしている。
# 複数台まとめて注文する場合はこのパラメーターを指定する。
response = client.purchase_reserved_db_instances_offering(
ReservedDBInstancesOfferingId=EXPECTED_PURCHASE_ID,
ReservedDBInstanceId="test01-dev-reserved-rds-01",
Tags=[
{"Key": "CostTag", "Value": "testtag01"},
],
)
logger.info(response)
return 0
except botocore.exceptions.ClientError as error:
logger.error(error.response["Error"]["Message"])
return 1
このコードのポイントを簡単に解説します。
Purchase ID が事前に調べたものと一致しているか照合(ダブルチェック目的)
購入条件から Purchase ID を取得する処理を再掲します。
#
# 購入しようとしている Purchase ID が正しいか確認する処理
#
offering_description = client.describe_reserved_db_instances_offerings(
DBInstanceClass=INSTANCE_CLASS_TYPE,
Duration="1",
ProductDescription=PRODUCT_DESCRIPTION,
OfferingType="No Upfront",
MultiAZ=IS_MULTI_AZ,
)
パラメーターの意味を確認します。
| キー | 引数 | 説明 |
|---|---|---|
| DBInstanceClass | t4g.micro | インスタンスクラスタイプ |
| Duration | 1 | 期間のため 1 年間で固定する |
| ProductDescription | postgresql | DB エンジンを指定 |
| OfferingType | No Upfront | 支払い方法のため前払いなしで固定する |
| MultiAZ | false | シングル AZ の場合は false を指定 |
この処理は、上記の条件を示す Purchase ID を含む JSON を取得できます。取得した Purchase ID が、事前に AWS CLI で確認しておいたものと一致していない場合、処理を中止します。
当該 Purchase ID の条件にマッチする現在のリザーブドインスタンスの台数の照合
同様に、boto3 の RDS クライアントが Purchase ID の条件にマッチする、現在購入済みのリザーブドインスタンスの台数 を取得します。これが事前に確認しておいた台数と一致していない場合、処理を中止します。
今回はリザーブドインスタンスの購入が初めてだったため、台数は 0 台の想定です。そのため、Lambda 関数実行後は台数が 1 台になり、もし誤って Lambda 関数を再実行してしまったとしても、この時には台数が 0 台ではなくなっているので、購入処理は再実行せず中止できるようになっています。
RDS リザーブドインスタンスの購入処理
具体的に RDS リザーブドインスタンスを購入している箇所の処理を再掲します。
#
# リザーブドインスタンスを注文する処理
#
# 今回は 1 台のみ購入したいため、DBInstanceCount は指定しないことで、
# デフォルト値の 1 台の注文となるようにしている。
# 複数台まとめて注文する場合はこのパラメーターを指定する。
response = client.purchase_reserved_db_instances_offering(
ReservedDBInstancesOfferingId=EXPECTED_PURCHASE_ID,
ReservedDBInstanceId="test01-dev-reserved-rds-01",
Tags=[
{"Key": "CostTag", "Value": "testtag01"},
],
)
logger.info(response)
パラメーターの意味を確認します。
| キー | 設定値 | 説明 |
|---|---|---|
| ReservedDBInstancesOfferingId | 8e51c316-003d-48b0-ac7f-1be68bcdbc90 | Purchase ID |
| ReservedDBInstanceId | 任意の名前 | 予約の名前を任意で指定 |
| DBInstanceCount | セットしない | 購入数を指定。指定しない場合はデフォルト値が 1 となる。 |
| Tags | {"Key": "タグ名", "Value": "タグの値"} | 任意のタグを JSON で設定 |
purchase_reserved_db_instances_offering API に、先に確認した Purchase ID を指定して購入しています。省略された DBInstanceCount パラメーターはデフォルトで 1 となるため、この処理で 1 台分のリザーブドインスタンスが購入されます。
購入処理の戻り値は JSON になっており、購入処理が成功すればこれをログへ出力するようにしています。
なお、この Lambda 関数をテスト実行してしまうとリザーブドインスタンスが購入されてしまうため、十分に注意してください。
次にこの Lambda 関数を指定した日時で実行できるようにしてみます。そのためにここでは Lambda 関数の ARN を確認しておきましょう。
EventBridge スケジューラで Lambda 関数を実行する
EventBridge から Lambda 関数を実行するための IAM ロールの作成
最初は、EventBridge から Lambda 関数を実行できる権限を持った IAM ロールを作成します。設定は以下のとおりです。
許可ポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowInvokeFunction",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "上記で作成した Lambda 関数の ARN"
}
]
}
信頼されたエンティティ
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowFromEventBridge",
"Effect": "Allow",
"Principal": {
"Service": "scheduler.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
スケジュールの作成
EventBridge のマネジメントコンソールから、Lambda 関数を実行する EventBridge スケジューラを作成します。
RDS リザーブドインスタンスは、購入と同時に期間がスタートしますので、日付と時刻に、スタートしたい日時を指定します。
今回は 12/6 午後 7:00 を指定しました。ガバメントクラウドの場合はここが 日本時間で 4/1 の午前 9:00 から 9:59 の間で設定することになります。
次は EventBridge スケジューラから実行する Lambda 関数を指定します。今回作成した Lambda 関数はイベントから引数を取得する処理がないため、ペイロードは空のままで問題ありません。
最後はスケジュールのオプションを設定します。意図せず二重に実行されないよう、再実行とデッドレターキューは今回使わないことにしました。また、ここでは先ほど作成した Lambda 関数の実行権限がある IAM ロールを指定します。
スケジュールが設定できました。あとは指定した日時を待つだけです。
EventBridge スケジューラから実行された Lambda 関数の実行ログを確認
今回作成した Lambda 関数は、CloudWatch Logs のロググループにログを配信するようにしており、またコードの中で Python から purchase_reserved_db_instances_offering API の戻り値の JSON をログへ出力しているので、これを確認してみます。
指定した日時で RDS リザーブドインスタンスの購入が成功していました!
RDS のマネジメントコンソールからも購入したリザーブドインスタンスが確認できました。
購入が正常に完了したら、再度の誤実行を防ぐため Lambda 関数にアタッチしている IAM ロールから rds:PurchaseReservedDBInstancesOffering の権限を削除した方が良いでしょう。
EventBridge スケジューラと Lambda 関数を使った購入の問題点
ここまでの検証で、RDS リザーブドインスタンスを EventBridge スケジューラと Lambda 関数を使って日時を指定して購入できることは分かりました。
事前に購入条件をコードにしておくことで、事前にレビューが可能であること、指定した日時に実行されることが大きな利点です。
ただしこの方法は、いくつか問題点があります。
Lambda 関数のエラーハンドリングが十分ではないため人間がフォローする必要あり
今回、方針として多重課金のリスクを取らないよう、エラー時に RDS リザーブドインスタンスの購入処理を再実行する仕組みは一切入れませんでした。
というのは、エラーが本当に期待したエラーなのかの検証が難しく、例えば購入オーダーは通っているけどエラーを返してくる場合があるのであれば、それを見分ける処理を入れなければならないですが、そういったものが起きうるのか、起きたとして全パターン網羅できるのかと考えたら、購入処理には敢えてエラー時のリトライ処理を入れず、人間がフォローする方針でよいのではないかと思ったからです。
Lambda 関数の事前テストが難しい
今回使っている API は、処理に成功すると実際に RDS リザーブドインスタンスが購入されてしまうものです。API に Dry Run オプションがないため、事前テストが非常に困難です。
私は実際に処理を流す直前まで、該当箇所のコードを非破壊的操作の API に書き換えてテストしましたが、正直実際に処理を流す時はかなりビビりました。
例えば AWS にレビューしてもらえるような契約を結んでいるのであれば、ぜひ事前にレビューを受けておいた方がいいと思います。
AWS CLI で人間が実行する方が安心感がある?
AWS CLI でも同じようにパラメーターを指定して RDS リザーブドインスタンスを購入することができます。結局人間がフォローするのであれば、AWS CLI でスクリプトを書いておいて、失敗したら再実行などする方が安心かもしれません。
まとめ
最後にまとめです。
- RDS リザーブドインスタンスは日時を指定して購入できないが、EventBridge スケジューラと Lambda を組み合わせるなどすれば一応は日時指定した購入が実現できる
- ただし、エラー時の処理が十分ではないため、人間のサポートも併せて実施した方が安全
- 事前テストが難しいので、実際にやるとしたら綿密な事前レビューが必要
来年度を迎える前に、RDS の長期継続割引を使うのか、使うとしたら Database Savings Plans かリザーブドインスタンスか、リザーブドインスタンスならどのように日本時間 4/1 午前 9:00 から 9:59 の間に購入オペレーションを実施するか、今から頭を悩ませる必要がありそうです。
参考 URL








