AWS Lambdaを定期実行して、Amazon Auroraクラスターを最小限の稼働に抑えるシステムを組んでみた

AWS Lambdaを定期実行させて、Amazon Auroraクラスターを7日以上停止するシステムを構築してみました。Amazon Auroraは自動メンテナンスなどのマネージドな性質上、長期停止ができません。これを他のAWSサービスを組み合わせて擬似的に長期停止させます。
2024.03.26

Amazon Auroraのクラスターの稼働を抑えたい

おのやんです。

みなさん、Amazon Aurora(以下、Aurora)のDBクラスター(以下、クラスター)の稼働を抑えたい状況になったことはありませんか?私はあります。

一般的に、Auroraをはじめとするデータベース系のマネージドサービスは、必要なメンテナンスを実施できる状態を維持するために、完全な停止ができない仕様となっています。例えばAmazon RDS(以下、RDS)の場合、データベースを停止できるのは7日までです。こちらの日数が経過すると、停止状態のRDSは自動で起動します。

公式ドキュメントでも、一時的に Amazon RDS DB インスタンスを停止する断続的に DB インスタンスを停止できますなど、一時的な停止であることが強調されています。

一時的なテストや毎日の開発作業のために、断続的に DB インスタンスを停止できます。最も一般的なユースケースはコスト最適化です

そういった背景を認識した上で、AWS環境の運用開始まで不必要な課金を抑えたり、検証環境で一時的に使用したりするなどの目的で、Auroraクラスターを長期間停止したいケースはあります。Auroraの長期停止は自前で実装する工数や運用のコスト、手動でのメンテナンスなど多くの考慮点があるので十分検討いただきたいですが、こういった場合は他のマネージドサービスを組み合わせることで実現可能です。

ちなみに、停止中であってもAuroraは課金が発生します参考ブログ)。クラスターの停止によってAuroraの課金をゼロにできるわけではないので、ここはご注意ください。

ということで今回は、AWS Lambda(以下、Lambda)をAmazon EventBridge(以下、EventBridge)で定期実行させて、Auroraクラスターを断続的に停止し、稼働を極力抑えてみたいと思います。

参考資料

今回紹介する方法は、AWS re:Postで紹介されているアーキテクチャがもとになっています。

2024年3月時点で、Lambdaで実行するPythonのバージョンやコードが少し異なりましたので、これらを修正しながらまとめたいと思います。

アーキテクチャ

実際に構築するアーキテクチャは以下の通りです。稼働しているAuroraクラスターに対して、停止と起動を行うLambda関数をそれぞれ作成します。このLambda関数を適切な時間に実行するように、EventBridgeでスケジュールします。

Auroraクラスター詳細

今回は、検証用に作成したAuroraクラスターに対して起動・停止を行います。構築したAuroraクラスターは以下のようになります。ap-northeast-1aとap-northeast-1cにそれぞれマスターインスタンスとリードレプリカを配置しています。なおDBエンジンはAurora MySQL 5.7であり、DBインスタンスタイプはdb.t2.smallです。

参考までに、これらのスペックを以下の表にまとめておきます。

項目 設定値 備考
DBインスタンスタイプ db.t2.small 検証用途につき、Auroraで使用できる最小スペックのインスタンスタイプのため
DBエンジン Aurora MySQL バージョン 2(MySQL 5.7) db.t2.smallがAurora MySQL 2系を要求するため
メンテナンスウィンドウ UTC: 毎週金曜18:00~19:00
(JST:毎週土曜03:00~04:00)
Lambda実行スケジュールの基準(後述)

今回使用するアーキテクチャでは、毎週実行されるメンテナンスウィンドウの前後1時間で自動起動・自動停止を行います。そのため、厳密に全期間停止するわけではなく、週に2時間ほどAuroraクラスターが稼働することになります。ここはご注意ください。

実行時間帯 【実行サービス】タスク
UTC 17:30 【Lambda】Auroraクラスターを自動で起動
UTC 18:00~19:00 【Aurora】メンテナンスウィンドウ
UTC 19:30 【Lambda】Auroraクラスターを自動で停止

IAMポリシーの作成

それでは、Auroraクラスターを長期停止する作業手順をまとめていきます。

最初にIAMポリシーを作成します。IAMポリシー画面にて「ポリシーの作成」ボタンを押下します。

具体的なIAMポリシーの内容は以下になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "rds:StartDBCluster",
                "rds:StopDBCluster",
                "rds:ListTagsForResource",
                "rds:DescribeDBInstances",
                "rds:StopDBInstance",
                "rds:DescribeDBClusters",
                "rds:StartDBInstance"
            ],
            "Resource": "*"
        }
    ]
}

こちらを入力した後、「次へ」のボタンを押下します。

IAMポリシーの設定を行います。今回はIAMポリシーにRDSStartAndStopIAMPolicyという名前をつけました。これらが設定できたら、「ポリシーの作成」ボタンを押下してIAMポリシーを作成します。

検索フォームからIAMポリシーを検索できれば、RDSStartAndStopIAMPolicyのIAMポリシーが正常に作成されています。

IAMロールの作成

上記で作成したIAMポリシーをもとに、IAMロールを作成します。IAMロールの画面から「IAMロールを作成」を押下します。

信頼されたエンティティタイプは「AWSのサービス」、サービスは「Lambda」を選択します。これらが設定できたら、「次へ」を押します。

許可を追加する際は、先ほど作成したRDSStartAndStopIAMPolicyを選択します。こちらが設定できましたら、「次へ」を押下します。

今回は、IAMロール名としてRDSStartAndStopIAMRoleを設定しておきましょう。エンティティ・許可は先ほどの設定値の確認ですので、これらが大丈夫であったら「ロールを作成」ボタンを押下します。

検索フォームからIAMロールを検索できれば、RDSStartAndStopIAMRoleが正常に作成されていることがわかります。

Auroraクラスターに自動起動・自動停止用タグを付与

次に、Auroraクラスターに自動起動・自動停止のためのタグを付与します。Lambda関数からAuroraクラスターを停止する場合、タグを手がかりにして自動起動・自動停止するクラスターを判別します。ということで、Auroraクラスターのタグを編集していきます。

Auroraクラスターの詳細画面のタブに「タグ」の項目がありますので 、ここから「追加」ボタンを押下し、以下のタグを追加します。

キー
autostart yes
autostop yes

Auroraクラスターのタグにこれらを追加できたら、Aurora側の設定は完了です。

自動起動Lambda関数の作成

上記でタグを付与したAuroraクラスターを実際に自動で起動・停止するLambda関数を作成しましょう。

Lambda関数の画面から「関数の作成」を押下します。

関数の作成においては「1から作成」を選択します。今回は自動で起動するLambda関数にstartRDSInstanceという名前をつけます。

ここで、Pythonのバージョンは3.11にしておきます。re:Postの回答ではPythonのバージョンが3.7になっていますが、本記事の執筆時点でPythonの3.7が選択できなくなっていますので、最新のPython3.11を選択しておきます。

Lambdaに付与するIAMロールは、先ほどのRDSStartAndStopIAMRoleを選択します。これらの設定を行なって、「関数の作成」ボタンを付与します。

作成した関数のタイムアウトも変更します。「設定タブ」からサイドメニューの「一般設定」を選択し、「編集」ボタンを押下します。

タイムアウトは、デフォルトの3秒から10秒に変更しておきましょう。

自動起動Lambda関数であるstartRDSInstanceの処理として、以下のPythonコードを設定します。re:Postのコードとは少し異なっていますが、具体的にはPythonの記法をPython3.11に対応させていたり、起動対象をAuroraクラスターに変更したりしています。

import boto3
rds = boto3.client('rds')

def lambda_handler(event, context):

    #Start Aurora MySQL clusters
    clusters = rds.describe_db_clusters()
    for cluster in clusters['DBClusters']:
        #Check if Aurora MySQL cluster is stopped. Start it if eligible.
        if cluster['Status'] == 'stopped':
            try:
                GetTags = rds.list_tags_for_resource(ResourceName=cluster['DBClusterArn'])['TagList']
                for tag in GetTags:
                    #if tag "autostart=yes" is set for cluster, start it
                    if tag['Key'] == 'autostart' and tag['Value'] == 'yes':
                        result = rds.start_db_cluster(DBClusterIdentifier=cluster['DBClusterIdentifier'])
                        print(f"Starting Aurora MySQL cluster: {cluster['DBClusterIdentifier']}.")
            except Exception as e:
                print(f"Cannot start Aurora MySQL cluster {cluster['DBClusterIdentifier']}.")
                print(e)

if __name__ == "__main__":
    lambda_handler(None, None)

こちらのコードをテストしてみたいと思います。一旦、Auroraクラスターは手動で停止しておきます。Auroraクラスターが停止すると、ステータスが「一時的に停止済み」に変わります。

この状態で、まずは簡単なテストイベントを作成します。

この状態でLambda関数の「Test」を押下します。実行してから数秒でこのようなresultが表示されたら、正常に実行できています。

Auroraクラスターのステータスも「起動中」に変わっており、正常に起動処理が実行されていることが確認できます。

自動停止Lambda関数の作成

Auroraクラスターを停止するLambda関数には、stopRDSInstanceという名前をつけます。他の設定はstartRDSInstanceと同様です。

停止処理に関しては、以下のようなPythonコードになります。こちらもre:Postの内容とは少し異なってますが、Python3.11対応などの変更を加えています。

import boto3
rds = boto3.client('rds')

def lambda_handler(event, context):

    #Stop Aurora MySQL instances
    clusters = rds.describe_db_clusters()
    for cluster in clusters['DBClusters']:
        #Check if Aurora MySQL cluster is not already stopped
        if cluster['Status'] == 'available':
            try:
                GetTags = rds.list_tags_for_resource(ResourceName=cluster['DBClusterArn'])['TagList']
                for tag in GetTags:
                    #if tag "autostop=yes" is set for cluster, stop it
                    if tag['Key'] == 'autostop' and tag['Value'] == 'yes':
                        result = rds.stop_db_cluster(DBClusterIdentifier=cluster['DBClusterIdentifier'])
                        print(f"Stopping Aurora MySQL cluster: {cluster['DBClusterIdentifier']}.")
            except Exception as e:
                print(f"Cannot stop Aurora MySQL cluster {cluster['DBClusterIdentifier']}.")
                print(e)

if __name__ == "__main__":
    lambda_handler(None, None)

こちらの関数をテストしてみると、先ほど起動したAuroraクラスターに停止処理が適用され、停止が始まっていることが確認できます。

EventBridgeによるスケジューリング

最後に、startRDSInstancestopRDSInstanceの両方にEventBridgeを設定して、Lambda関数を定期実行できる状態にします。

Lambda関数の詳細画面に「トリガーを追加」というボタンがあるので、こちらを押下します。

EventBridgeの詳細画面では、以下のように設定していきます。なお、起動スケジュールと停止スケジュールはそれぞれRunStartFunctionRunStopFunctionという名前をつけています。また、それぞれ以下のcron式で実行スケジュールを設定しています。

スケジュール cron式 備考
UTC 毎週金曜 17:30 cron(30 17 ? * FRI *) cron式はUTC表現です
日本時間: 毎週土曜 02:30
UTC 毎週金曜 19:30 cron(30 19 ? * FRI *) 日本時間: 毎週土曜 04:30

今回は即日での検証を実施するため、EventBridgeのスケジュールをいったん設定したあとに、以下のような変更を手動で行いました。

RunStartFunctionRunStopFunctionのそれぞれで、指令した時間帯に自動で処理が走るのを確認しました。

Lambdaの定期実行によって、Auroraの稼働を抑えられる

以上の作業を実施すれば、Auroraの稼働を抑えることができます。不要な時間帯にAuroraクラスターを停止するなど、コストを抑える運用が可能です。

繰り返しにはなりますが、こちらの対応を行う際には、LambdaやEventBridgeなどの実装工数やメンテナンス、Auroraのアップデートの対応などを十分に検討いただくようお願いします。では!