【AWS】Aurora(MySQL)のCluster/Instanceを自動的に削除／リストアする【Aurora】

# はじめに
Auroraがリリースされてから後、AWS上で稼働するサービスでAuroraを利用している諸兄諸姉が多いのではないかと思います。

RDS全般にまつわる悩み事ですが、開発環境の費用削減のため、RDSを利用しない間は停止したいと思っても、
RDSは容易に停止できない、削除することしかできないという制約があります。

そこで、Aurora Cluster/Instanceのsnapshotを作成して削除するスクリプトと、
削除時と同じ構成のAurora Cluster/Instanceをリストアするスクリプトを作ってみましたので、参考までに公開します。

このスクリプトを実際に利用したことにより発生した損害等には責任を負いません。
また、利用に関するサポートは致しません。
飽くまで参考程度にしてください。


# 基本的な考え方
Auroraはその構成上、単一インスタンス構成であろうとも、必ずClusterとして構成されます。

Auroraインスタンスの作成／削除時の流れは概ね下記のようになります。
簡単に言うと、Clusterというガワがまず存在して、ここにInstanceを追加／削除していく、というイメージになります。

- インスタンス削除
    - メンバーInstanceを削除する
    - Instanceの削除完了を待つ（対象Instanceが存在しなくなる、ClusterのDBClusterMembersが存在しなくなる）
    - Clusterを削除する。併せて、snapshotを作成する。
    - Clusterの削除完了を待つ
- インスタンス作成
    - Aurora Clusterを作成する
    - Clusterの作成完了を待つ（Statusがavailableになる）
    - 作成したClusterのメンバーとなるInstanceを作成する
    - Instanceの作成完了を待つ（Statusがavailableになる）

※2018/5/15追記
Clusterを削除せずInstanceのみを削除／リストア対象とすることで、費用を抑えつつ、削除／リストアの時間を短縮することが可能です。
DBインスタンスの費用はInstanceだけにかかり、Clusterはストレージ費用程度となるためです。
この改修は別途実施予定。

# スクリプトの解説
## 前提条件

- 既に存在しているAurora Cluster/Instanceを削除し、これを任意のタイミングでリストアする、という利用方法を想定しています。
- このスクリプトはAWS CLIがインストールされたLinuxで動作します。
- AWS CLIに環境ごとのprofileが設定されていることとします。
- 対象はAurora(MySQL)エンジンのみとし、1Cluster・1Instance構成のみに対応しています。
- Cluster名はInstance名に「-cluster」サフィックスが付与された名称とします。

2インスタンス以上のClusterは対象としていません。
こまめな削除・リストアを必要とするのは検証や開発で利用する環境のみであること、
それらの環境は通常はシングルインスタンスの最小構成であることから、このような前提となっています。

## 検証した環境

OS: CentOS Linux release 7.2.1511
Bash: GNU bash, version 4.2.46(1)-release (x86_64-redhat-linux-gnu)
AWS CLI: aws-cli/1.11.63 Python/2.7.5 Linux/3.10.0-327.22.2.el7.x86_64 botocore/1.5.26

## Cluster/Instance削除

第1引数にAWS CLIのプロファイルを、第2引数には削除対象のAurora Instanceを改行（LFのみ）で羅列したファイルを指定します。
また、空行をスキップする処理は入れていないので、空行は含めないようにしてください。

具体的には、下記のようなファイルとなります。

```
rds-aurora-instance1
rds-aurora-instance2
```

Aurora Clusterは、Aurora Instance名に「-cluster」サフィックスが付与されたものと想定します。

これを実行すると、カレントディレクトリ配下に「result-delete-yyyymmdd」というディレクトリが作成され、ログ等が出力されます。
削除したCluster/Instanceに関する情報がJSONとして出力されますが、これはリストア時に必要となります。
\# 上記JSONは、rds-describe-db-clusters及びrds-describe-db-instancesの実行結果となります。

RDS snapshotとして、「Instance名-yyyymmdd」と「Instance名-final」の2つが作成されます。
「Instance名-final」は次回削除時に手動で削除する必要があるため、誤操作に対する備えとして、「Instance名-yyyymmdd」を作成するようにしています。

```bash
#!/bin/sh


if [ $# -lt 2 ] ;then
    echo "usage: $0 <AWS CLI profile> <Listfile of RDS instances to be deleted>"
    exit 1;
fi


AWS_CLI_PROFILE=$1
RDS_LIST=$2

CURRENT_DATE=`TZ=Asia/Tokyo date +%Y%m%d`
CURRENT_DATETIME=`TZ=Asia/Tokyo date +%Y%m%d-%H%M%S`

OUTPUT_DIR="./result-delete-${CURRENT_DATE}"
LOG_STDOUT_FILE=${OUTPUT_DIR}/rds_delete_instance_${CURRENT_DATETIME}_stdout.log
LOG_STDERR_FILE=${OUTPUT_DIR}/rds_delete_instance_${CURRENT_DATETIME}_stderr.log
TMP_FILE=`mktemp`

alias aws="aws --profile ${AWS_CLI_PROFILE}"

mkdir -p ${OUTPUT_DIR}

#
# Utility Functions
#

function log_both () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDOUT_FILE}
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDERR_FILE}
    return
}

function log_std () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDOUT_FILE}
    return
}

function log_err () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDERR_FILE}
    return
}

function _describe_current_information (){
    local _rds_instance=$1
    local _rds_cluster=$2
    local _cmd

    # fetch current information of the RDS instance
    log_both "creating snapshots of \"${_rds_instance}\""
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-instances --filters Name=db-instance-id,Values=${_rds_instance}" >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd > ${TMP_FILE} 2>> ${LOG_STDERR_FILE}
    cat ${TMP_FILE} >> ${LOG_STDOUT_FILE}
    cp ${TMP_FILE} ${OUTPUT_DIR}/${_rds_instance}_instance_information_${CURRENT_DATETIME}.json

    # fetch current information of the RDS cluster
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd > ${TMP_FILE} 2>> ${LOG_STDERR_FILE}
    cat ${TMP_FILE} >> ${LOG_STDOUT_FILE}
    cp ${TMP_FILE} ${OUTPUT_DIR}/${_rds_instance}_cluster_information_${CURRENT_DATETIME}.json

}

function _create_snapshot (){
    local _rds_cluster=$1
    local _rds_snapshot=$2
    local _cmd

    # create an explicit snapshot
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds create-db-cluster-snapshot --db-cluster-identifier ${_rds_cluster} --db-cluster-snapshot-identifier ${_rds_snapshot}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

}

function _delete_instance (){
    local _rds_instance=$1
    local _cmd
    local _instance_num

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds delete-db-instance --db-instance-identifier ${_rds_instance}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-instances --filters Name=db-instance-id,Values=${_rds_instance}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}


    # wait to finish deliting the RDS instance
    log_both "waiting to finish deleting the RDS instance \"${_rds_instance}\"."
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-instances --filters Name=db-instance-id,Values=${_rds_instance}"
    log_both "checking the RDS instance sutatus with the following AWS API: \"${_cmd}\"."

    while true
    do
        sleep 10

        _instance_num=`${_cmd} | jq '.DBInstances | length'`
        if [ ${_instance_num} -eq 1 ];then
            continue
        fi

        break
    done

    log_both "deleting the RDS instance \"${_rds_instance}\" has been finished."
}

function _delete_cluster (){
    local _rds_cluster=$1
    local _rds_snapshot=$2

    # wait until the members of its cluster is 0.
    log_both "waiting to change the members of the cluster \"${_rds_cluster}\" 0."
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --db-cluster-identifier ${_rds_cluster}"
    log_both "checking whether the members of the cluster is 0 with the following AWS API: \"${_cmd}\"."

    while true
    do
        sleep 5

        _instance_num=`${_cmd} | jq '.DBClusters[0].DBClusterMembers | length'`
        if [ ${_instance_num} -ne 0 ];then
            continue
        fi

        break
    done

    log_both "current cluster status: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    # start deleting the cluster
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds delete-db-cluster --db-cluster-identifier ${_rds_cluster} --final-db-snapshot-identifier ${_rds_snapshot}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

}


#
# Main Function
#

    #
    # Description:
    #   This program deletes an Aurora RDS cluster and its instance and creates its snapshots automatically.
    #   It enables you to treat an RDS as if it can stop/start instances.
    #   It deal with each RDS instances one by one.
    #   It assumes that the cluster name is a combination with its instance name and a "-cluster" suffix.
    #
    # How it works in detail:
    #   1. Fetch current information. It will be saved into the ${OUTPUT_DIR} directory.
    #   2. Create its snapshot. it will be named what consists its instance name and a datetime suffix.
    #   3. Delete the RDS instance with creating a snapshot named what is the same as its instance name.
    #
    # License:
    #   Copyright (c) 2017 Taichi Miki
    #   Released under the MIT license
    #


    log_both "a temporary file is ${TMP_FILE}"

    for _rds_instance in `grep -v "^#.*$" ${RDS_LIST}`
    do

        _rds_cluster="${_rds_instance}-cluster"

        log_both "Start deleting the instance \"${_rds_instance}\", cluster \"${_rds_cluster}\" with creating snapshots."

        # fetch current information
        _describe_current_information ${_rds_instance} ${_rds_cluster}

        # create an explicit snapshot
        _create_snapshot ${_rds_cluster} "${_rds_instance}-${CURRENT_DATE}"

        # delete the RDS instance
        log_both "deleting the instance \"${_rds_instance}\""
        _delete_instance ${_rds_instance}


        # delete the RDS cluster with creating a snapshot
        log_both "deleting the cluster \"${_rds_cluster}\""
        _delete_cluster ${_rds_cluster} "${_rds_instance}-final"

        log_both "the deleting process for the instance \"${_rds_instance}\", cluster \"${_rds_cluster}\" has been finished."
    done

    log_both "delete the temporary file \"${TMP_FILE}\"."
    rm ${TMP_FILE}

    log_both "the deleting process for all instances has been finished."

```

## Cluster/Instanceリストア

第1引数にAWS CLIのプロファイルを指定します。
第2引数には下記項目を「,」で区切って並べたファイルを指定します。項目の間には空白を含めないようにしてください。
空白を含めると確実に動かなくなります。

- Aurora Instance名
- リストアさせたいSnapshot名
- 対象Aurora Clusterに対して、削除前に取得したrds-describe-db-clustersの実行結果（JSON形式）
- 対象Aurora Instanceに対して、削除前に取得したrds-describe-db-instancesの実行結果（JSON形式）

Aurora Clusterは、Aurora Instance名に「-cluster」サフィックスが付与されたものと想定します。

上記項目のうち、3つ目と4つ目のファイルは、Cluster/Instance削除時に出力されたファイルを指定する想定です。
対象を複数指定する場合は、改行（LFのみ）で羅列したファイルを指定します。
また、空行をスキップする処理は入れていないので、空行は含めないようにしてください。

具体的には、下記のようなファイルとなります。

```
rds-aurora-instance1,rds-aurora-instance1-final,rds-aurora-instance1_cluster_information.json,rds-aurora-instance1_instance_information.json
rds-aurora-instance2,rds-aurora-instance2-final,rds-aurora-instance2_cluster_information.json,rds-aurora-instance2_instance_information.json
```

これを実行すると、カレントディレクトリ配下に「result-restore-yyyymmdd」というディレクトリが作成され、ログ等が出力されます。
VPCやDB Subnet Group, Security Groupなど、Aurora Cluster/Instance構築に必要な情報は、
削除前に実行したrds-describe-db-clusters/rds-describe-db-instancesの実行結果から取得します。


```bash
#!/bin/sh


if [ $# -lt 2 ] ;then
    echo "usage: $0 <AWS CLI profile> <List file consists an RDS restoration definition.>"
    echo "       The list file is like a CSV file consists 4 parameters those are"
    echo "       \"instance name\", \"snapshot name\", \"Information of the cluster\", \"Information of the instance\"."
    echo '       The 3rd and 4th parameters in the argument file are the result of AWS APIs "describe-db-cluster" and "describe-db-instances"'
    echo '       when the cluster and the instance were still alive.'
    exit 1;
fi


AWS_CLI_PROFILE=$1
RDS_LIST=$2

CURRENT_DATE=`TZ=Asia/Tokyo date +%Y%m%d`
CURRENT_DATETIME=`TZ=Asia/Tokyo date +%Y%m%d-%H%M%S`

OUTPUT_DIR="./result-restore-${CURRENT_DATE}"
LOG_STDOUT_FILE=${OUTPUT_DIR}/rds_delete_instance_${CURRENT_DATETIME}_stdout.log
LOG_STDERR_FILE=${OUTPUT_DIR}/rds_delete_instance_${CURRENT_DATETIME}_stderr.log
TMP_FILE=`mktemp`

alias aws="aws --profile ${AWS_CLI_PROFILE}"

mkdir -p ${OUTPUT_DIR}

#
# Utility Functions
#

function log_both () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDOUT_FILE}
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDERR_FILE}
    return
}

function log_std () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDOUT_FILE}
    return
}

function log_err () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDERR_FILE}
    return
}

function _describe_current_information (){
    local _rds_instance=$1
    local _rds_cluster=$2
    local _cmd

    # fetch current information of the RDS instance
    log_both "creating snapshots of \"${_rds_instance}\""
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-instances --filters Name=db-instance-id,Values=${_rds_instance}" >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd > ${TMP_FILE} 2>> ${LOG_STDERR_FILE}
    cat ${TMP_FILE} >> ${LOG_STDOUT_FILE}
    cp ${TMP_FILE} ${OUTPUT_DIR}/${_rds_instance}_instance_information_${CURRENT_DATETIME}.json

    # fetch current information of the RDS cluster
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd > ${TMP_FILE} 2>> ${LOG_STDERR_FILE}
    cat ${TMP_FILE} >> ${LOG_STDOUT_FILE}
    cp ${TMP_FILE} ${OUTPUT_DIR}/${_rds_instance}_cluster_information_${CURRENT_DATETIME}.json

}

function _create_cluster (){
    local _rds_cluster=$1
    local _snapshot=$2
    local _cluster_json_file=$3

    local _db_subnet_group_name=`cat ${_cluster_json_file} | jq -r '.DBClusters[0].DBSubnetGroup'`
    local _vpc_security_group_ids=`cat ${_cluster_json_file} | jq -r '.DBClusters[0].VpcSecurityGroups[0].VpcSecurityGroupId'`

    local _cmd


    _cmd="aws --profile ${AWS_CLI_PROFILE} rds restore-db-cluster-from-snapshot --engine aurora --db-cluster-identifier ${_rds_cluster} --snapshot-identifier ${_snapshot} --db-subnet-group-name ${_db_subnet_group_name} --vpc-security-group-ids ${_vpc_security_group_ids}"
    log_both "invoking an AWS API: \"${_cmd}\"."

    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}


    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    # wait until the cluster's status changed into available.
    log_both "waiting to finish creating the RDS cluster \"${_rds_cluster}\"."

    while true
    do
        sleep 15

        _rds_status=`${_cmd} | jq -r '.DBClusters[0].Status'`
        if [ ${_rds_status} == "available" ];then
            break
        fi

    done
}

function _modify_cluster (){
    local _rds_cluster=$1
    local _cluster_json_file=$2

    local _db_cluster_parameter_group_name=`cat ${_cluster_json_file} | jq -r '.DBClusters[0].DBClusterParameterGroup'`

    local _cmd

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds modify-db-cluster --db-cluster-identifier ${_rds_cluster} --db-cluster-parameter-group-name ${_db_cluster_parameter_group_name}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

}


function _create_instance (){
    local _rds_instance=$1
    local _rds_cluster=$2
    local _instance_json_file=$3

    local _db_instance_class=`cat ${_instance_json_file} | jq -r '.DBInstances[0].DBInstanceClass'`
    local _db_parameter_group_name=`cat ${_instance_json_file} | jq -r '.DBInstances[0].DBParameterGroups[0].DBParameterGroupName'`

    local _cmd

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds create-db-instance --engine aurora --db-cluster-identifier ${_rds_cluster} --db-instance-identifier ${_rds_instance} --db-instance-class ${_db_instance_class} --db-parameter-group-name ${_db_parameter_group_name} --no-multi-az --no-publicly-accessible"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

}


#
# Main Function
#

    #
    # Description:
    #   This program restores an Aurora(MySQL) cluster with one instance from a snapshot.
    #   It assumes that the cluster name is a combination with its instance name and a "-cluster" suffix.
    #
    #   The 3rd and 4th parameter in the argument file
    #   are the result of AWS APIs "describe-db-cluster" and "describe-db-instances"
    #   when the cluster and the instance are still alive.
    #
    # How it works in detail:
    #   1. read argument file and separate each line by one comma ",".
    #   2. get information of the cluster and instance to check they don't exist yet.
    #   3. create an Aurora(MySQL) cluster with parameters indicated by the information json file..
    #   4. modify the cluster's parameter group, because it cannot be set at creating.
    #   5. create an RDS instance as a member of the cluster.
    #
    # License:
    #   Copyright (c) 2017 Taichi Miki
    #   Released under the MIT license
    #

    log_both "a temporary file is ${TMP_FILE}"

    for _restore_definition in `grep -v "^#.*$" ${RDS_LIST}`
    do

        _rds_instance=`echo ${_restore_definition} | awk -F, '{print $1};'`
        _rds_cluster=${_rds_instance}-cluster
        _snapshot=`echo ${_restore_definition} | awk -F, '{print $2};'`
        _cluster_info_json=`echo ${_restore_definition} | awk -F, '{print $3};'`
        _instance_info_json=`echo ${_restore_definition} | awk -F, '{print $4};'`

        log_both "Start restoring the instance \"${_rds_instance}\", cluster \"${_rds_cluster}\" from the snapshot \"${_snapshot}\"."

        # fetch current information
        _describe_current_information ${_rds_instance} ${_rds_cluster}

        # create the RDS cluster
        log_both "creating a cluster \"${_rds_cluster}\""
        _create_cluster ${_rds_cluster} ${_snapshot} ${_cluster_info_json}

        # modify the cluster's parameter group
        log_both "modifying the cluster \"${_rds_cluster}\" to set its parameter group appropriately."
        _modify_cluster ${_rds_cluster} ${_cluster_info_json}

        # create an RDS instance
        log_both "creating an instance \"${_rds_instance}\" into the cluster \"${_rds_cluster}\"."
        _create_instance ${_rds_instance} ${_rds_cluster} ${_instance_info_json}


        log_both "the restoring process for the instance \"${_rds_instance}\", cluster \"${_rds_cluster}\" has been finished."
    done

    log_both "delete the temporary file \"${TMP_FILE}\"."
    rm ${TMP_FILE}

    log_both "the restoring process for all instances has been finished."

```

# おわりに
Githubは準備中...