AWSのRDSを自動停止するLambdaを作る記録

RDSは停止しても7日経つと自動で起動してしまいます。

DB インスタンスは最大 7 日間停止できます。DB インスタンスを手動で起動しないで 7 日間が経過すると、DB インスタンスは自動的に起動します。
一時的に Amazon RDS DB インスタンスを停止する - Amazon Relational Database Service

なので、RDSを監視して自動で停止してほしいです。

案件が動いていない時は停止していてほしいです。ちょいちょい確認して停止するのは面倒くさいです。
そこで、世の中の知識を利用して自動停止できるようにします。

このサイトのやり方でRDSを自動停止するLambdaを作る

qiita.com

実行権限を作成する

Lambdaの実行権限を作成する - ponsuke_tarou’s blog
上記を参考にIAMのポリシーを作成し、ロールを作成します。
ポリシーに設定する内容は以下になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "rds:StopDBInstance",
                "rds:ListTagsForResource"
            ],
            "Resource": "arn:aws:rds:*:*:db:*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "rds:DescribeDBInstances",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

自動停止の対象と停止時間を設定できるようにするためにRDSにAutoStopタグを追加します。

Amazon RDS リソースのタグ付け

  1. AWSマネジメントコンソールにある[RDS] > サイドメニューの[データベース] > 対象となるRDSを選択して詳細画面を開きます。
  2. [タグ]タブ > [追加]ボタンで[タグの追加] ウィンドウを表示します。
  3. [タグキー]に「AutoStop」と[値]に「自動停止したい時間」を入力します。
    • f:id:ponsuke_tarou:20191126100110p:plain
      タグの追加ウィンドウ
  4. [追加]を選択します。
    • f:id:ponsuke_tarou:20191126100356p:plain
      追加後

Lambda関数を作成します。

  1. AWSのコンソールにある[Lambda] > [関数の作成]ボタンで画面を開きます。
  2. 必要な項目を入力後に[関数の作成]ボタンで関数を作成します。
    • オプション : [一から作成]
    • 関数名 : 任意の関数名
    • ランタイム : Python3.8
    • 実行ロール : 既存のロールを使用する
      • 作成したロールを選択します。

関数を実装します。

[関数コード] > [lambda_function.py]に以下のコードを張り付けて[保存]ボタンで保存します。

# -*- coding: utf-8 -*-

from __future__ import print_function

import sys
import json

import boto3
import datetime

REGION_NAME = "ap-northeast-1"

print("Loading function")
rds = boto3.client('rds')

def get_auto_stop_tag(instance_arn):
    instance_tags = rds.list_tags_for_resource(ResourceName=instance_arn)
    tag_list = instance_tags['TagList']
    tag = next(iter(filter(lambda tag: tag['Key'] == 'AutoStop' and (tag['Value'] is not None and tag['Value'] != ''), tag_list)), None)
    return tag

def get_auto_stop_time(auto_stop_tag):
    today = datetime.datetime.now()
    param_day = today.day
    auto_stop_val = auto_stop_tag["Value"].split(":")
    # 設定時刻が8:59以前である場合、GMT変換時に前日にならないよう日付を1日進めておく
    if int(auto_stop_val[0]) < 9:
        param_day = param_day + 1
    # タグに指定されたGMTでの時間
    tag_time = datetime.datetime(today.year, today.month, param_day, int(auto_stop_val[0]), int(auto_stop_val[1]))
    gmt_time = tag_time + datetime.timedelta(hours=-9)
    return gmt_time

def lambda_handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))
    # インスタンスを取得
    instances = rds.describe_db_instances()
    if len(instances['DBInstances']) > 0:
        # インスタンスを順番に処理していく
        for instance in instances['DBInstances']:
            if instance['DBInstanceStatus'] == 'available':
                instance_arn = instance['DBInstanceArn']
                print(instance_arn + 'は、稼働中です。')

                tag = get_auto_stop_tag(instance_arn)
                print('取得したAutoStopタグ:' + str(tag))

                if tag:
                    # AutoStopタグが指定されているインスタンスを処理する
                    reference_time = get_auto_stop_time(tag)
                    print('AutoStopタグに指定された時間(GMT)は、' + reference_time.strftime('%Y-%m-%dT%H:%M:%SZ') + 'です。')

                    event_time = datetime.datetime.strptime(event["time"], '%Y-%m-%dT%H:%M:%SZ')
                    reference_time_from = reference_time + datetime.timedelta(minutes=-5)
                    reference_time_to = reference_time + datetime.timedelta(minutes=5)
                    print('処理時間(' + event_time.strftime('%Y-%m-%dT%H:%M:%SZ') + ')が、' + \
                          reference_time_from.strftime('%Y-%m-%dT%H:%M:%SZ') + 'から' + reference_time_to.strftime('%Y-%m-%dT%H:%M:%SZ') + 'だったら停止します。')

                    # AutoStopタグに指定された時刻の前後5分以内であればインスタンス停止する
                    if reference_time_from <= event_time and event_time <= reference_time_to:
                        rds.stop_db_instance(DBInstanceIdentifier=instance['DBInstanceIdentifier'])
                        print(instance_arn + 'を停止しました。')
関数を動かしてみます。
  1. [テスト]ボタンで[テストイベントの設定]ダイアログを表示します。
  2. [新しいテストイベントの作成]を選択します。
  3. [イベントテンプレート]で「Hello World」を選択します。
    • f:id:ponsuke_tarou:20191129101513p:plain
      テストイベントの設定
  4. 引数の欄に「{"time": "2019-12-04T10:02:00Z"}」を入力します(時間はAutoStopタグに指定した時間の近い時間)。
  5. [イベント名]に任意の名前を設定して[作成]ボタンで保存します。
    • f:id:ponsuke_tarou:20191129101840p:plain
      保存後の状態
  6. [テスト]ボタンで関数を実行します。
  7. 実行結果とログを確認して、「成功」になるまでソースや設定の見直しをします。
    • f:id:ponsuke_tarou:20191129102116p:plain
      実行結果

Lambda関数を実行するトリガーを作成します。

CloudWatch EventsをトリガーとしてLambda関数を実行できるようにするためにイベントを登録します。

CloudWatch Eventsにイベントを登録します。

  1. AWSのコンソールにある[Lambda] > 作成したLambda関数を選択して詳細画面を開きます。
  2. [Designer]にある[トリガーを追加]ボタンでトリガーの設定画面を開きます。
    • f:id:ponsuke_tarou:20191127101110p:plain
      トリガーを追加ボタン
  3. プルダウンからCloudWatch Eventsを選択します。
  4. [ルール]で「新規ルールの作成」を選択し、[ルール名(必須)][ルールの説明(任意)]を入力します。
    • f:id:ponsuke_tarou:20191127101515p:plain
      トリガーの設定画面
  5. [ルールタイプ]で「スケジュール式」を選択し、[スケジュール式]に以下のサイトを参考にスケジュールを設定します。

失敗したこと

CloudWatch Eventsの[スケジュール式]の書き方を間違えた

Cron式で時間に「10-24」と指定したところエラーになりました。24時はだめなんですね。

Parameter ScheduleExpression is not valid. (Service: AmazonCloudWatchEvents; Status Code: 400; Error Code: ValidationException; Request ID: c.....)

Lambda関数でCloudWatch Logsに書き込むための権限がなかった。

Lambda関数を実行するCloudWatch Eventsを登録してからLambda関数の[モニタリング]タブを見てみるとメッセージが表示されていました。
メッセージにある[AWSLambdaBasicExecutionRole]の権限を追加します。

アクセス権限が見つかりません
お使いの関数には、Amazon CloudWatch Logs に書き込むためのアクセス許可がありません。ログを閲覧するには、その実行ロールに AWSLambdaBasicExecutionRole の管理ポリシーを追加します。IAM コンソールを開きます。
  1. AWSマネジメントコンソールにある[IAM] > サイドメニューの[ポリシー] > [AWSLambdaBasicExecutionRole]を検索して詳細画面を表示します。
  2. [アクセス権] > [JSON]で権限の詳細を表示します。
  3. [Statement]に記載されている内容以下をコピーします。
    • f:id:ponsuke_tarou:20191128100745p:plain
  4. サイドメニューの[ポリシー] > Lambda関数に設定したポリシーを検索して詳細画面を表示します。
  5. [アクセス権] > [JSON] > [ポリシーの編集]ボタンで編集画面を表示します。
  6. コピーした内容を[Action]に追記します。
  7. [ポリシーの確認]ボタン > [変更の保存]ボタンで保存します。

describe_db_instancesを実行する権限がなかった。

Lambda関数を実行したらエラーになりました。ポリシーの[Action]に「rds:DescribeDBInstances」を追加しました。

{
  "errorMessage": "An error occurred (AccessDenied) when calling the DescribeDBInstances operation: User: arn:aws:sts::8xxxxxxxxxxx:assumed-role/{ロール名}/{Lambda関数名} is not authorized to perform: rds:DescribeDBInstances",
  "errorType": "ClientError",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 27, in lambda_handler\n    instances = rds.describe_db_instances(Filters=[{\"Name\": \"DBInstanceStatus\", \"Values\": [\"available\"]}])\n",
    "  File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n",
    "  File \"/var/runtime/botocore/client.py\", line 661, in _make_api_call\n    raise error_class(parsed_response, operation_name)\n"
  ]
}
「rds:DescribeDBInstances」はリソースを限定できなかった。

以下のように単純に「rds:DescribeDBInstances」を追加したところ同じエラーになりました。
一部のRDSの情報の閲覧だけできるポリシーは作れなかった話 - Qiitaを読んでリソースを限定できないことを知りました。

...省略...
            "Action": [
                "rds:StopDBInstance",
                "rds:DescribeDBInstances"
            ],
            "Resource": "arn:aws:rds:*:*:db:*"
...省略...

f:id:ponsuke_tarou:20191129104647p:plain
ポリシーの[ビジュアルエディタ]タブ