boto3でRDSのwaiterを使用するとき注意したいこと

2016.02.09

サーモン大好き、横山です。

waiterって、RDSのインスタンス作成とか実行の長い処理のあとにすることを簡単にかけて出来て便利ですよね。
ただ、思っていた処理と違ってハマってしまったので、メモとして残しておきたいと思います。

目的

今回はRDS(mysql)のスナップショットから復元を行ったあと、マスタパスワードを変更してから、RDS(mysql)にアクセスしたいということで以下のコードを準備しました。

コード

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import boto3
import sys
import datetime

from my_func import connect_rds
from traceback import print_exc

dt_now = datetime.datetime.now()
def now_string(dt_format="%Y/%m/%d %H:%M:%S"):
    return datetime.datetime.now().strftime(dt_format)

def output(s, fp=sys.stdout):
    fp.write('[{}] '.format(now_string()))
    fp.write(s)
    fp.write('\n')
    fp.flush()

output_fp = sys.stdout

DB_INSTANCE_IDENTIFIER = 'rds_name'
DB_SUBNET_GROUPNAME = 'db_subnet_groupname'
DB_USER = 'user'
DB_NAME = 'dbname'
DB_MASTER_PASSWORD = 'password' + dt_now.strftime('%Y%m%d%H%M%S')

rds = boto3.client('rds', region_name='ap-northeast-1')

output('modify', output_fp)
db_modify = rds.modify_db_instance(
    DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER,
    ApplyImmediately=True,
    MasterUserPassword=DB_MASTER_PASSWORD,
)['DBInstance']

output('available_waiter', output_fp)
available_waiter = rds.get_waiter('db_instance_available')
available_waiter.wait(
    DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER
)

output('describe instances', output_fp)
db_infos = rds.describe_db_instances(
    DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER,
)['DBInstances']

if db_infos:
    db_info = db_infos[0]
    db_address = db_info['Endpoint']['Address']
    db_port = db_info['Endpoint']['Port']

    db_url = 'mysql://{}:{}@{}:{}/{}'.format(DB_USER,
                                             DB_MASTER_PASSWORD,
                                             db_address,
                                             db_port,
                                             DB_NAME)
    output('connection mysql: {}'.format(db_url), output_fp)

    try:
        connect_rds(db_url) #rdsに接続
    except:
        print_exc()

output('Done', output_fp)

結果

こちらを実行すると、以下のエラーメッセージが出ました。

$ python main.py
[2016/02/09 04:46:47] modify
[2016/02/09 04:46:48] available_waiter 2
[2016/02/09 04:46:48] describe instances
...
OperationalError: (_mysql_exceptions.OperationalError) (1045, "Access denied for user 'root'@'10.0.xxx.xxx' (using password: YES)")
[2016/02/09 04:46:48] Done

検証

結論から言いますと、RDSのマスターパスワードの変更前、もしくは変更中にアクセスしようとして失敗しています。

modify_db_instanceApplyImmediately=True にしていますが、 db_instance_available のwaiterを実行しても即座に DBInstanceStatus が変わらず、マスターパスワード変更前にwaiterを抜ける前に抜けてしまっています。 少しコードを変えて、症状を見てみようと思います。

ソースコード

49行目の if db_infos: のステートメントを以下の様に書き換えました。

if db_infos:
    while True:
        import time
        db_infos = rds.describe_db_instances(
            DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER,
        )['DBInstances']
        output('DBInstanceStatus: ' + str(db_infos[0]['DBInstanceStatus']))
        output('PendingModifiedValues: ' + str(db_infos[0]['PendingModifiedValues']))
        time.sleep(5)

実行結果

python chk.py
[2016/02/09 04:59:48] modify
[2016/02/09 04:59:49] available_waiter 2
[2016/02/09 04:59:49] describe instances
[2016/02/09 04:59:49] DBInstanceStatus: available
[2016/02/09 04:59:49] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 04:59:55] DBInstanceStatus: available
[2016/02/09 04:59:55] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:00] DBInstanceStatus: available
[2016/02/09 05:00:00] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:05] DBInstanceStatus: available
[2016/02/09 05:00:05] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:10] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:10] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:15] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:15] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:21] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:21] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:26] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:26] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:31] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:31] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:36] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:36] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:41] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:41] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:47] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:47] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:52] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:52] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:00:58] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:00:58] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:01:04] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:01:04] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:01:09] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:01:09] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:01:15] DBInstanceStatus: resetting-master-credentials
[2016/02/09 05:01:15] PendingModifiedValues: {u'MasterUserPassword': '****'}
[2016/02/09 05:01:21] DBInstanceStatus: available
[2016/02/09 05:01:21] PendingModifiedValues: {}
[2016/02/09 05:01:26] DBInstanceStatus: available
[2016/02/09 05:01:26] PendingModifiedValues: {}

modify_db_instance を実行して約30秒後に DBInstanceStatusavailable -> resetting-master-credentials に変化しています。
この変化の前にwaiterを実行してしまうとすぐに抜けてしまい、結果的にすぐマスターパスワード変更前にアクセスを試みて失敗してします。

改善案

modify_db_instance 後に、 PendingModifiedValues の値が入っていますので、5秒おきにdescribe_db_instancesを実行し、PendingModifiedValues が入っており、DBInstanceStatus の値が available でなくなったらwaiterを実行するようにします。

ソースコード

if db_infos:
    db_info = db_infos[0]
    db_address = db_info['Endpoint']['Address']
    db_port = db_info['Endpoint']['Port']

    db_url = 'mysql://{}:{}@{}:{}/{}'.format(DB_USER,
                                             DB_MASTER_PASSWORD,
                                             db_address,
                                             db_port,
                                             DB_NAME)

    import time
    while db_info['PendingModifiedValues']:
        if db_info['DBInstanceStatus'] != 'available':
            break

        output('wait 5s ...')
        time.sleep(5)

        db_infos = rds.describe_db_instances(
            DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER,
        )['DBInstances']
        if db_infos:
            db_info = db_infos[0]


    output('available_waiter', output_fp)
    available_waiter = rds.get_waiter('db_instance_available')
    available_waiter.wait(
        DBInstanceIdentifier=DB_INSTANCE_IDENTIFIER
    )

    output('connection mysql: {}'.format(db_url), output_fp)

    try:
        connect_rds(db_url) #rdsに接続
    except:
        print_exc()

output('Done', output_fp)

実行結果

[2016/02/09 05:36:41] modify
[2016/02/09 05:36:41] available_waiter 2
[2016/02/09 05:36:41] describe instances
[2016/02/09 05:36:41] wait 5s ...
[2016/02/09 05:36:46] available_waiter
[2016/02/09 05:38:16] connection mysql: mysql://user:password20160209053641@rds_name.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com:3306/db_name
[2016/02/09 05:38:16] Done

まとめ

RDSの modify_db_instance を実行後に、waiterを実行する場合はご注意ください。
今回の改善案は一例ですので、もっといい方法が有りましたら、是非教えてください!

waiterは用法・容量を守って、正しく使いましょう。