AWS Lambda関数でRDSインスタンスのMulti-AZ有効無効を切り替える

isdはじめに

前回は、AWS EC2インスタンスのバックアップのため、スナップショットを作成するLambda関数を紹介しました。

今回はRDSインスタンスのMulti-AZの有効、無効(Muti-AZとSingle-AZ)を切り替えるLambda関数を紹介します。

今回のLambda関数は、以下の記事で公開されていた関数を参考にさせていただきました。
(ありがとうございます!)

・boto3でRDSのwaiterを使用するとき注意したいこと – Developers.IO
https://dev.classmethod.jp/cloud/note_when_use_waiter_of_rds_in_boto3/

ユースケース

Lambda関数を紹介する前に、どういった場面でこの関数が必要となるかを考えてみます。

RDSは、Multi-AZを有効にすることで、可用性が高くなります。
万一RDSインスタンスに何らかの障害が発生した場合に、異なるアベイラビリティゾーンで起動しているスタンバイインスタンスにフェイルオーバーすることで、1分~3分程度のダウンタイムでRDBサーバーを自動復旧させることができます。
また、AWSによるメンテナンスやアップグレード、インスタンスタイプの変更の際も、ダウンタイムはフェイルオーバーにかかる時間のみの短時間となります。

とても便利な機能ですが、常に2つのインスタンスを起動していますから、RDSの料金がMulti-AZ無効(Single-AZ)のときの2倍となります。
このため、Multi-AZを有効にする時間帯を限定することで、ある程度料金を削減することができます。

たとえば「休日や夜間は障害発生時の復旧に時間がかかってもよい」ということであれば、

  • 平日営業時間帯はMulti-AZを有効
  • 夜間や休日はMulti-AZを無効(Single-AZ)

とするような運用方法が考えられます。

逆に「平日の障害発生時はサポート要員がいて人手で復旧できる(このとき、復旧に多少時間がかかってもよい、という前提)。夜間や休日はサポート要員がいないので、自動復旧してほしい」ということであれば、

  • 平日営業時間帯はMulti-AZを無効(Single-AZ)
  • 夜間や休日はMulti-AZを有効

という運用方法が考えられます。

このような場合に今回のLambda関数を使うと、Multi-AZの有効無効を、曜日や時間を指定してスケジューリングできます。

isdLambda関数の仕様・設定

仕様

  • RDS DBインスタンスのMulti-AZを無効(または有効)にする。
  • RDS DBインスタンスのステータスが「available」以外のときや、すでにMulti-AZが無効(または有効)となっているときは何もしない(ステータスが「available」以外のときは、DBインスタンス情報の変更ができない)。
  • Multi-AZを無効、有効にする関数はそれぞれ別に作成する(変数で工夫してひとつにまとめてもよいでしょう)。
  • 対象となるDBインスタンス名は、Lambdaの環境変数で指定する。
  • Lambda関数のランタイムはPython 3.6

Lambda関数

Multi-AZを有効・無効にする対象となるRDS DBインスタンスと同じリージョンでLambda関数を作成します。
ランタイムは、Python 3.6を指定します。

Multi-AZを無効にするLambda関数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 指定したRDSインスタンスのMulti-AZを無効にする
#	Python 3.6
#
# Lambda関数の環境変数で以下を設定する。
#
# 	DB_INSTANCE_IDENTIFIER: 対象とするRDSインスタンス名
#
import boto3
import collections
import time
from botocore.client import ClientError
import os

DB_INSTANCE_IDENTIFIER = os.environ['DB_INSTANCE_IDENTIFIER']

rds = boto3.client('rds', os.environ['AWS_REGION'])

def lambda_handler(event, context):
    disable_multiaz()

def disable_multiaz():
	description = 'db_instance: ' + DB_INSTANCE_IDENTIFIER
	db_infos = rds.describe_db_instances(
						 DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER
				)['DBInstances']

	if db_infos:
		db_info = db_infos[0]
		db_multiaz = db_info['MultiAZ']
		db_status = db_info['DBInstanceStatus']

		# すでにMulti-AZが無効のときは、何もしない
		if not db_multiaz:
			print('Multi-AZ is already False, %s' % (description))
			return

		# ステータスが available 以外のときは、何もしない
		if db_status != 'available':
			print('DB Instance is not available, %s' % (description))
			return

		db_instance = _disable_multiaz(DB_INSTANCE_IDENTIFIER, description)
		print('disable Multi-AZ, %s' % (description))

	return

# RDSインスタンスのMulti-AZを無効にする
# 実行エラーした場合、合計3回までリトライする
def _disable_multiaz(db_instance_identifier, description):
    for i in range(1, 3):
        try:
            return rds.modify_db_instance(
                DBInstanceIdentifier=db_instance_identifier,
                MultiAZ=False,
                ApplyImmediately=True)
        except ClientError as e:
            print(str(e))
        time.sleep(1)
    raise Exception('Cannot disable Multi-AZ, ' + description)

# EOF

 

Multi-AZを有効にするLambda関数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 指定したRDSインスタンスのMulti-AZを有効にする。
#	Python 3.6
#
# Lambda関数の環境変数で以下を設定する。
#
# 	DB_INSTANCE_IDENTIFIER: 対象とするRDSインスタンス名
#
import boto3
import collections
import time
from botocore.client import ClientError
import os

DB_INSTANCE_IDENTIFIER = os.environ['DB_INSTANCE_IDENTIFIER']

rds = boto3.client('rds', os.environ['AWS_REGION'])

def lambda_handler(event, context):
    enable_multiaz()

def enable_multiaz():
	description = 'db_instance: ' + DB_INSTANCE_IDENTIFIER
	db_infos = rds.describe_db_instances(
						 DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER
				)['DBInstances']

	if db_infos:
		db_info = db_infos[0]
		db_multiaz = db_info['MultiAZ']
		db_status = db_info['DBInstanceStatus']

		# すでにMulti-AZが有効のときは、何もしない
		if db_multiaz:
			print('Multi-AZ is already True, %s' % (description))
			return

		# ステータスが available 以外のときは、何もしない
		if db_status != 'available':
			print('DB Instance is not available, %s' % (description))
			return

		db_instance = _enable_multiaz(DB_INSTANCE_IDENTIFIER, description)
		print('Enable Multi-AZ, %s' % (description))

	return

# RDSインスタンスのMulti-AZを有効にする
# 実行エラーした場合、合計3回までリトライする
def _enable_multiaz(db_instance_identifier, description):
    for i in range(1, 3):
        try:
            return rds.modify_db_instance(
                DBInstanceIdentifier=db_instance_identifier,
                MultiAZ=True,
                ApplyImmediately=True)
        except ClientError as e:
            print(str(e))
        time.sleep(1)
    raise Exception('Cannot Enable Multi-AZ, ' + description)

# EOF

 

実行ロール

Lambda関数に付与するIAMロールのカスタムポリシーは以下のようにします。
CloudWatch Logsのログ書き出しと、RDSインスタンスの情報取得、変更アクションを付与しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:GetLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "rds:DescribeDBInstances",
                "rds:ModifyDBInstance"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

 

環境変数

対象とするRDSインスタンスは、Lambda関数の環境変数で指定します。

  • キー: DB_INSTANCE_IDENTIFIER
  • 値: <DBインスタンス名>

 


 

基本設定

メモリはデフォルトの128MBのままでOKです。
タイムアウトは、リトライをする場合はデフォルトの3秒では短かすぎてエラーすることがあるので、10秒や30秒などに変更します。

定期実行設定

定期的にMulti-AZの無効、有効を切り替えるためにこのLambda関数を実行するには、トリガーとしてCloudWatch Eventを指定して、スケジュールのルールを設定します。

以下は「日本時間の月-金の22時」に関数を実行する設定例です。
「スケジュール式」の時刻はUTCで指定することに注意します。
 


 

isdMulti-AZの有効チェック

設定ミスや、Lambda関数の実行エラーなど、何らかの原因で、必要なときにMulti-AZが有効になっていない可能性もゼロではありません。
念のため、Multi-AZが有効になっていることをチェックし、無効であればAmazon SNSで通知するLambda関数も用意してみました。
ステータスが「available」以外のときも通知します。

Multi-AZの有効無効を切り替えるLambda関数と同様に、対象となるDBインスタンス名は、Lambdaの環境変数で指定します。

SNSトピックは、あらかじめRDSインスタンスと同じリージョンで作成し、通知先(SNSやWebhookなど)のサブスクリプションを設定しておきます。
このときのSNSトピックのARN(arn:aws:sns:ap-northeast-1:~)はのちに必要となるので、メモしておきます。

Lambda関数

Multi-AZを有効・無効にする対象となるRDS DBインスタンスと同じリージョンでLambda関数を作成します。
ランタイムは、Python 3.6を指定します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 指定したRDSインスタンスのMulti-AZが有効となっているかチェックする。
# 有効になっていなければ、SNSでアラート通知する。
#	Python 3.6
#
# 対象とするDBインスタンス名を DB_INSTANCE_IDENTIFIER で指定すること。
# SNSによる通知先トピックのARNを SNS_TARAGET_ARN で指定すること。
#
# Lambda関数の環境変数で以下を設定する。
#
# 	DB_INSTANCE_IDENTIFIER: 対象とするRDSインスタンス名
# 	       SNS_TARAGET_ARN: SNSによる通知先トピックのARN
#
import boto3
import collections
import time
from botocore.client import ClientError
import os

DB_INSTANCE_IDENTIFIER = os.environ['DB_INSTANCE_IDENTIFIER']
SNS_TARAGET_ARN = os.environ['SNS_TARAGET_ARN']

rds = boto3.client('rds', os.environ['AWS_REGION'])
sns = boto3.resource('sns', os.environ['AWS_REGION'])

def lambda_handler(event, context):
    check_multiaz()

def check_multiaz():
	topic = sns.Topic(SNS_TARAGET_ARN)

	if not topic:
		print('SNS Topic is not exist. ARN: %s' % (SNS_TARAGET_ARN))
		return

	subject = 'DB Instance Status Error'
	description = 'db_instance: ' + DB_INSTANCE_IDENTIFIER
	db_infos = rds.describe_db_instances(
						 DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER
				)['DBInstances']

	if db_infos:
		db_info = db_infos[0]
		db_multiaz = db_info['MultiAZ']
		db_status = db_info['DBInstanceStatus']

		# ステータスが available 以外のとき
		if db_status != 'available':
			message = 'DB Instance is not available, ' + description + ', db_status: ' + db_status
			print(message)
			response = topic.publish(
						Subject = subject,
						Message = message
						)
			print('response={}'.format(response))
			return

		# Multi-AZが無効のとき
		if not db_multiaz:
			message = 'DB Instance is not Multi-AZ, ' + description
			print(message)
			response = topic.publish(
						Subject = subject,
						Message = message
						)
			print('response={}'.format(response))
			return

		print('DB Instance is available and Multi-AZ. %s' % (description))

	return

# EOF

 

実行ロール

Lambda関数に付与するIAMロールのカスタムポリシーは以下のようにします。
CloudWatch Logsのログ書き出しと、RDSインスタンスの情報取得、SNSのPublish(トピックの発行)権限を付与しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:GetLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "rds:DescribeDBInstances"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "sns:Publish"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

 

環境変数

対象とするRDSインスタンスとSNS通知先を、Lambda関数の環境変数で指定します。

  • キー: DB_INSTANCE_IDENTIFIER、値: <DBインスタンス名>
  • キー: SNS_TARAGET_ARN、値: <SNSによる通知先トピックのARN>

 


 

基本設定

メモリはデフォルトの128MBのままでOKです。
タイムアウトは、余裕をもって10秒や30秒などに変更します。

定期実行設定

定期的にMulti-AZをチェックするためにこのLambda関数を実行するには、トリガーとしてCloudWatch Eventを指定して、スケジュールのルールを設定します。

たとえば、平日月-金の8時から22時はMulti-AZを有効、それ以外は無効とする場合、平日月-金の9時にチェックするようにします。

以下は「日本時間の月-金の9時」に関数を実行する設定例です。
「スケジュール式」の時刻はUTCで指定することに注意します。
 


 

実行結果

このLambda関数を実行し、RDSインスタンスのMulti-AZが無効、もしくはステータスが「available」以外の場合は、指定したSNSサブスクリプションに通知されます。
サブスクリプションがEメールの場合は、以下のようなメールが届きます。

  • Subject
    DB Instance Status Error

  • Message
    DB Instance is not Multi-AZ, db_instance: <DBインスタンス名>(Multi-AZが無効の場合)
    DB Instance is not available, db_instance: <DBインスタンス名>(ステータスが available 以外の場合)

通知が届いた場合は、とりいそぎ、Multi-AZを手動で有効として、なぜ有効になっていないのか、CloudWatchのLambda関数実行ログなどで調査するとよいでしょう。

isdおわりに

AWS RDSインスタンスのMulti-AZの有効、無効(Muti-AZとSingle-AZ)を切り替えるLambda関数を紹介しました。

本番運用であれば、Multi-AZを有効にしたままにするのがよいと思いますが、コスト削減の必要があり、一時的に可用性が低くなることが許容できるのであれば、定期的に無効にするような運用もアリかと思います。

(関連記事)
・AWS Lambda関数でEC2のスナップショットを作成する~対象はEBSボリュームのタグで指定
https://inaba-serverdesign.jp/blog/20180330/aws_ec2_create_snapshot_lambda.html
 

Follow me!