背景
あるシステムで Aurora PostgreSQL(Aurora クラスター + Writer/Reader インスタンス)を利用しています。
アプリケーション側の挙動の問題で、まれに CPU 使用率が 100% 近くまで張り付いた状態が数分続く ことがありました。
- その間はアプリのレスポンスが悪化する
- ただしフェイルオーバーで Writer を切り替えるほどではない
- アプリ側の根本対応がすぐには難しいため、インスタンスだけ再起動してクリーンな状態に戻したい
そこで、
「CPU 使用率が一定時間以上高止まりしたら、Writer/Reader インスタンスを自動で再起動する」
という仕組みを CloudWatch + Lambda + DynamoDB で実装しました。
この記事では、ステージング環境での検証を経て、本番環境に適用するまでの手順をまとめます。
全体構成
やっていることはシンプルで、構成はこんな感じです。
- CloudWatch アラーム Aurora インスタンスの CPUUtilization を監視し、 「5/5 データポイントで 90%以上」などの条件で ALARM に遷移
- CloudWatch → Lambda アクション アラーム状態に変わったタイミングで Lambda 関数を起動
- Lambda 関数
- イベントから「どのインスタンスのアラームか」を判定
- DynamoDB で「最近再起動していないか」を確認(ループ防止)
- 問題なければ RebootDBInstance API を叩いて、そのインスタンスのみ再起動
- DynamoDB テーブル インスタンスごとの最終再起動時刻を管理し、短時間の連続再起動を防止
ポイントは、フェイルオーバーではなくインスタンス再起動だけ を自動化しているところです。
前提環境(例)
以下のような前提で説明します(実際の環境に合わせて読み替えてください)。
- リージョン:ap-northeast-1
- アカウント ID:123456789012
- Aurora クラスター:myapp-prod-rds
- Writer インスタンス:myapp-prod-rdsinst
- Reader インスタンス:myapp-prod-rdsinst2
Step 1. DynamoDB テーブルで「再起動履歴」を管理する
まず、インスタンスごとの最終再起動時刻を記録する DynamoDB テーブルを作成します。
- AWS マネジメントコンソールで DynamoDB を開く
- 左メニュー「テーブル」→「テーブルを作成」
- 以下のように設定
- テーブル名:rds-auto-reboot-history
- パーティションキー
- 名前:DbInstanceIdentifier
- 型:文字列 (String)
- その他のオプションはデフォルト(オンデマンド課金)で OK
これで DbInstanceIdentifier をキーにして再起動時刻を保存できるようになります。
Step 2. Lambda 実行ロールを作成する
Lambda からは次の権限が必要です。
- 対象 RDS インスタンスに対する DescribeDBInstances と RebootDBInstance
- DynamoDB テーブルに対する GetItem と PutItem
- CloudWatch Logs へのログ出力
2-1. ロールの作成
- IAM コンソール → 左メニュー「ロール」→「ロールを作成」
- 信頼されたエンティティタイプ:AWS のサービス
- ユースケース:Lambda を選択 → 次へ
- 「許可ポリシーをアタッチ」で マネージドポリシー AWSLambdaBasicExecutionRole を追加
- ロール名を lambda-rds-auto-reboot-role などにして作成
2-2. RDS & DynamoDB 用インラインポリシーの追加
- 作成したロール lambda-rds-auto-reboot-role を開く
- 「許可」タブ → 「インラインポリシーを追加」
- JSON タブに切り替えて、以下を貼り付け (アカウント ID とインスタンス名は環境に合わせて書き換え)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RdsReboot",
"Effect": "Allow",
"Action": [
"rds:DescribeDBInstances",
"rds:RebootDBInstance"
],
"Resource": [
"arn:aws:rds:ap-northeast-1:123456789012:db:myapp-prod-rdsinst",
"arn:aws:rds:ap-northeast-1:123456789012:db:myapp-prod-rdsinst2"
]
},
{
"Sid": "DynamoRebootHistory",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/rds-auto-reboot-history"
}
]
}4.「ポリシーの確認」→ 名前(例:lambda-rds-auto-reboot-policy)を付けて保存
Step 3. Lambda 関数を作成する
3-1. 関数の作成
- Lambda コンソール →「関数の作成」
- 「一から作成」を選択し、以下のように設定
- 関数名:rds-auto-reboot-on-high-cpu
- ランタイム:Python 3.12
- アーキテクチャ:x86_64(デフォルト)
- 実行ロール:lambda-rds-auto-reboot-role を選択
- 「関数の作成」をクリック
3-2. Lambda コード
lambda_function.py に以下のコードを貼り付けて「デプロイ」します。
import os
import json
from datetime import datetime, timezone, timedelta
import boto3
rds = boto3.client("rds")
dynamo = boto3.client("dynamodb")
DDB_TABLE_NAME = os.environ["DDB_TABLE_NAME"]
MIN_REBOOT_INTERVAL_MINUTES = int(os.environ["MIN_REBOOT_INTERVAL_MINUTES"])
def lambda_handler(event, context):
"""
CloudWatch アラームから呼び出されて RDS インスタンスを再起動する Lambda。
対応イベント形式:
1) 新しい CloudWatch アラーム → Lambda 形式
{
"source": "aws.cloudwatch",
"alarmArn": "...",
"alarmData": { "state": {...}, "configuration": {...} }
}
2) EventBridge (CloudWatch Alarm State Change) 経由
{ "detail-type": "...", "detail": { "state": {...}, "configuration": {...} } }
3) 古い CloudWatch → Lambda 形式
{ "AlarmName": "...", "NewStateValue": "ALARM", "Trigger": { "Dimensions": [...] } }
"""
print("Event:", json.dumps(event))
db_instance_id = None
# --- パターン1: 新しい CloudWatch アラーム → Lambda 形式 ---
if event.get("source") == "aws.cloudwatch" and "alarmData" in event:
alarm_data = event["alarmData"]
state_value = alarm_data.get("state", {}).get("value")
if state_value != "ALARM":
print(f"[CloudWatch new] State is {state_value}. Do nothing.")
return
db_instance_id = extract_db_instance_id_from_metric_container(alarm_data)
# --- パターン2: EventBridge (CloudWatch Alarm State Change) 経由 ---
elif "detail-type" in event and "detail" in event:
detail = event["detail"]
state_value = detail.get("state", {}).get("value")
if state_value != "ALARM":
print(f"[EventBridge] State is {state_value}. Do nothing.")
return
db_instance_id = extract_db_instance_id_from_metric_container(detail)
# --- パターン3: 古い CloudWatch → Lambda 形式 ---
elif "AlarmName" in event:
new_state = event.get("NewStateValue")
if new_state != "ALARM":
print(f"[LegacyAlarm] NewStateValue is {new_state}. Do nothing.")
return
db_instance_id = extract_db_instance_id_from_alarm_trigger(event)
else:
print(f"Unknown event format. keys={list(event.keys())}")
return
if not db_instance_id:
print("DBInstanceIdentifier not found. Abort.")
return
print(f"Target DB instance: {db_instance_id}")
# --- ループ防止: 直近で再起動していないか確認 ---
if is_recently_rebooted(db_instance_id):
print("Recently rebooted. Skip reboot.")
return
# --- RDS インスタンス状態確認 ---
inst = rds.describe_db_instances(DBInstanceIdentifier=db_instance_id)["DBInstances"][0]
status = inst["DBInstanceStatus"]
print(f"Current status: {status}")
if status != "available":
print("Status is not 'available'. Skip reboot.")
return
# --- 再起動実行 (ForceFailover=False でフェイルオーバーはしない) ---
resp = rds.reboot_db_instance(
DBInstanceIdentifier=db_instance_id,
ForceFailover=False
)
print("Reboot started:", resp["DBInstance"]["DBInstanceIdentifier"])
# --- DynamoDB に再起動時刻を記録 ---
now = datetime.now(timezone.utc).isoformat()
dynamo.put_item(
TableName=DDB_TABLE_NAME,
Item={
"DbInstanceIdentifier": {"S": db_instance_id},
"LastRebootTime": {"S": now}
}
)
return {"status": "reboot_started", "instance": db_instance_id}
def extract_db_instance_id_from_metric_container(container: dict) -> str | None:
"""
alarmData / detail など、CloudWatch アラームの configuration.metrics[]
を含むコンテナから DBInstanceIdentifier を取り出す。
dimensions の形が
- {"DBInstanceIdentifier": "..."} という dict
- [{"name": "...", "value": "..."}] という list
の両方をサポートする。
"""
cfg = container.get("configuration", container)
metrics = cfg.get("metrics", [])
for m in metrics:
metric_stat = m.get("metricStat", {})
metric = metric_stat.get("metric", {})
dims = metric.get("dimensions")
if not dims:
continue
# パターンA: dict 形式 {"DBInstanceIdentifier": "..."}
if isinstance(dims, dict):
for k, v in dims.items():
if k == "DBInstanceIdentifier":
return v
# パターンB: list 形式 [{"name": "...", "value": "..."}]
elif isinstance(dims, list):
for d in dims:
if not isinstance(d, dict):
continue
name = d.get("name") or d.get("Name")
value = d.get("value") or d.get("Value")
if name == "DBInstanceIdentifier":
return value
return None
def extract_db_instance_id_from_alarm_trigger(event: dict) -> str | None:
"""
古い CloudWatch メトリクスアラームの Trigger.Dimensions[] から
DBInstanceIdentifier を取得。
"""
trigger = event.get("Trigger", {})
dims = trigger.get("Dimensions", [])
for d in dims:
name = d.get("name") or d.get("Name")
value = d.get("value") or d.get("Value")
if name == "DBInstanceIdentifier":
return value
return None
def is_recently_rebooted(db_instance_id: str) -> bool:
"""
DynamoDB に記録された最終再起動時刻と現在時刻を比較し、
MIN_REBOOT_INTERVAL_MINUTES 以内なら True を返す。
"""
res = dynamo.get_item(
TableName=DDB_TABLE_NAME,
Key={"DbInstanceIdentifier": {"S": db_instance_id}},
ConsistentRead=True
)
item = res.get("Item")
if not item:
return False
last_str = item.get("LastRebootTime", {}).get("S")
if not last_str:
return False
last_dt = datetime.fromisoformat(last_str)
now = datetime.now(timezone.utc)
diff = now - last_dt
print(f"Last reboot diff minutes: {diff.total_seconds() / 60:.1f}")
return diff < timedelta(minutes=MIN_REBOOT_INTERVAL_MINUTES)3-3. 環境変数
Lambda の「設定」タブ →「環境変数」で以下を設定します。
| キー | 値 |
|---|---|
| DDB_TABLE_NAME | rds-auto-reboot-history |
| MIN_REBOOT_INTERVAL_MINUTES | 30(本番は 30〜60 を推奨) |
Step 4. CloudWatch アラームを作成する(Writer/Reader)
4-1. Writer 用アラーム
- CloudWatch →「アラーム」→「アラームを作成」
- メトリクスの選択
- 名前空間:AWS/RDS
- メトリクス:CPUUtilization
- DBInstanceIdentifier = myapp-prod-rdsinst を選択
- しきい値の設定
- アラーム名:rds-myapp-prod-rdsinst-high-cpu-auto-reboot
- 統計:平均
- 期間:60 秒
- 条件:CPUUtilization が > 90
- 評価データポイント:5/5
- アクションの設定
- 「その他のアクション」→「Lambda 関数」を追加
- 関数:rds-auto-reboot-on-high-cpu
4-2. Reader 用アラーム
同様に Reader インスタンス向けにアラームを作成します。
アクションは同じ Lambda 関数
DBInstanceIdentifier = myapp-prod-rdsinst2
アラーム名:rds-myapp-prod-rdsinst2-high-cpu-auto-reboot
Step 5. Lambda のリソースベースポリシーで CloudWatch からの起動を許可
ここが一番ハマったポイントです。
CloudWatch アラーム → Lambda 連携の principal は lambda.alarms.cloudwatch.amazonaws.com です。
cloudwatch.amazonaws.com では動きません。
5-1. アラームの ARN を確認
CloudWatch アラーム詳細画面に ARN が表示されます。例:
arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:rds-myapp-prod-rdsinst-high-cpu-auto-reboot
arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:rds-myapp-prod-rdsinst2-high-cpu-auto-reboot
5-2. CLI で Lambda に許可を追加
# writer アラームからの Invoke を許可
aws lambda add-permission \
--region ap-northeast-1 \
--function-name rds-auto-reboot-on-high-cpu \
--statement-id AllowCloudWatchInvokeWriterRdsCpuAlarm \
--action lambda:InvokeFunction \
--principal lambda.alarms.cloudwatch.amazonaws.com \
--source-account 123456789012 \
--source-arn arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:rds-myapp-prod-rdsinst-high-cpu-auto-reboot
# reader アラームからの Invoke を許可
aws lambda add-permission \
--region ap-northeast-1 \
--function-name rds-auto-reboot-on-high-cpu \
--statement-id AllowCloudWatchInvokeReaderRdsCpuAlarm \
--action lambda:InvokeFunction \
--principal lambda.alarms.cloudwatch.amazonaws.com \
--source-account 123456789012 \
--source-arn arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:rds-myapp-prod-rdsinst2-high-cpu-auto-reboot
statement-id は既存のものと重複しない任意の文字列で OK です。
Step 6. 本番での動作確認
6-1. まずは「Lambda が呼ばれる」だけ確認する
- アラームのしきい値を一時的に
- 閾値:> 1
- 評価データポイント:1/1 に変更
- 数分待ってアラーム状態になるのを確認
- CloudWatch の「アクション」タブに 「正常に実行されたアクション arn:aws:lambda:…」 と出ること
- Lambda の「モニタリング」タブで呼び出し回数が増えていること
- CloudWatch Logs に
Event: {"source": "aws.cloudwatch", "alarmArn": "..."}
Target DB instance: myapp-prod-rdsinst
...が出ていれば、イベント解析までは成功です。
※ 本番でいきなり再起動させたくない場合は、事前に DynamoDB に「最近再起動した」時刻を書き込んでおき、is_recently_rebooted で True を返すようにしておけば実際の Reboot は走りません。
6-2. メンテナンス時間帯に、実際の再起動テストを 1 回だけ実施
- DynamoDB テーブル rds-auto-reboot-history から対象インスタンスのレコードを削除
- アラームのしきい値を > 1(1/1)に下げ、ALARM 状態に遷移させる
- Lambda ログで
Reboot started: myapp-prod-rdsinstと出ることを確認
- RDS コンソールで、対象インスタンスが「再起動中」→「利用可能」と遷移することを確認
- テスト後はしきい値を本番値(> 90、5/5 など)に戻す
Reader 側も同様に 1 回だけテストしておくと安心です。
運用してみての所感・注意点
- 本記事の仕組みはあくまで「延命・自動復旧」のための仕掛けであり、 根本的にはアプリケーション側の CPU 高負荷の原因を解消するべき です。
- 再起動時には短時間の接続断が発生するため、アプリ側の再接続ロジックなどは事前に確認が必要です。
- MIN_REBOOT_INTERVAL_MINUTES を短くしすぎると、負荷が下がらないまま何度も再起動を繰り返す可能性があります。 本番環境では 30〜60 分程度を目安に設定しました。
- インスタンス名を変更したり、新しいインスタンスを追加した場合は、
- IAM ロールのリソース ARN
- CloudWatch アラームの対象メトリクス
- Lambda の DynamoDB レコード などのメンテナンスも忘れずに行う必要があります。
以上、Aurora PostgreSQL の CPU 高負荷時に自動でインスタンス再起動を行う仕組みを、CloudWatch アラーム + Lambda + DynamoDB で実装した手順の紹介でした。