RDSメンテナンス情報をSlackへ通知する

RDS（Aurora）のメンテナンス情報を、AWS CLIの[describe-pending-maintenance-actions](https://docs.aws.amazon.com/cli/latest/reference/rds/describe-pending-maintenance-actions.html)で情報を取得して、
[slack api](https://api.slack.com/methods/files.upload)にメッセージを送るシェルスクリプトです。

[PHD](https://docs.aws.amazon.com/ja_jp/health/latest/ug/getting-started-phd.html)や[AWS Health API](https://docs.aws.amazon.com/ja_jp/health/latest/ug/getting-started-api.html)でもRDSのメンテナンス情報の取得は可能です。
他のリソースについても通知が可能なので、メンテナンス情報の通知を受け取るだけであれば、そちらを利用した方が良いと思います。
ただ、実行時点のメンテナンス適用状況の情報が必要な場合は[describe-pending-maintenance-actions](https://docs.aws.amazon.com/cli/latest/reference/rds/describe-pending-maintenance-actions.html)を使用します。
※PHD や Health API で取得する情報は、その時点のメンテナンス適用状況が取得されるのではなく、通知した時点での適用対象から変化しない為
# 環境情報
Slackワークスペースプラン：スタンダード（有料）

```shell-session:OS,カーネル,AWSCLIバージョン確認
$ cat /etc/system-release
Amazon Linux release 2 (Karoo)
$ uname -a
Linux hogehuga.ap-northeast-1.compute.internal 4.14.77-81.59.amzn2.x86_64 #1 SMP Mon Nov 12 21:32:48 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ aws --v
aws-cli/1.16.102 Python/2.7.14 Linux/4.14.77-81.59.amzn2.x86_64 botocore/1.12.92
```
# SlackAPIToken準備
別稿としておりますので[こちら](https://qiita.com/hiren/items/c8ffa3f9de58b80ba5da)をご参照ください。
今回Botを使用するので、AppのPermission Scopeは「bot（Add a bot user with the username@hogehuga）」があれば大丈夫です。
# シェルスクリプト本体
元々Slack通知には[Incoming Webhooks](https://api.slack.com/incoming-webhooks)を使用し、メンテナンス内容の緊急度によってicon_emojiを変更するつもりだったのですが、jq等で解析せずに見やすく通知出来なかったので[files.upload](https://api.slack.com/methods/files.upload)を使用しています。
<details><summary>見やすく出来なかった実装（クリックで展開）</summary><div>

```bash:RDS_maintenance_notification.sh
#!/bin/bash

# ================================
# 設定
# ================================
SHELL_FILE_NAME=`basename $0`
SHELL_DIR=$(cd "$(dirname "$0")" && pwd)
WORK_DIR=${SHELL_DIR}/work
LOG_DIR=${SHELL_DIR}/log

OUTPUT_AWSCLI_JSON=${WORK_DIR}/output_awscli.json
BEFORE_OUTPUT_AWSCLI_JSON=${WORK_DIR}/before_output_awscli.json
RDS_MENTENANCE_NOTICE_LOG=${LOG_DIR}/rds_maintenance_notice.log

# 前回と差分が無い場合（true:通知しない,false:通知する）
NO_DIFF_NOT_NOTIFICATION=true
# 結果をSlackへ（true:送信する,false:送信しない）
SEND_SLACK=true

# Slack設定
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/ABCDEFGHI/GKLMNOPQR/stuvwxyz1234567890"
SLACK_INFO_USER_NAME="RDS maintenance info"
SLACK_NOTIFIER_USER_NAME="RDS maintenance notifier"
SLACK_EMERGENCY_USER_NAME="RDS maintenance Alert"
SLACK_INFO_ICON=":information_source:"
SLACK_NOTIFIER_ICON=":warning:"
SLACK_EMERGENCY_ICON=":sos:"
SLACK_CHANNEL_NAME="#AWS通知"
# ================================
# 関数
# ================================
# 使用方法
function usage {
    cat <<EOF
Usage:
    $(basename ${0}) [options]
Description:
    $(basename ${0}) is a tool for Get pending RDS maintenance actions and send Slack notification
Options:
    -h                  show this help, then exit
EOF
    return 0
}

# オプション解析
while getopts h OPT ; do
    case $OPT in
      h)
        usage
        exit 0
        ;;
    esac
done

# ログ出力
function log() {
    time=`date "+%Y/%m/%d %H:%M:%S.%3N"`
    shellName=`basename ${0%.*}`
    level=$1; shift
    color=$1; shift
    echo -e "${time} ${level:0:1} [${shellName}] \e[${color}m$*\e[m"
}

# ログ出力：各レベル
function log_error() { log "ERROR" "1;31" "$*";}
function log_warn() { log "WARN" "1;33" "$*";}
function log_info() { log "INFO" "1;34" "$*";}
function log_debug() { log "DEBUG" "1;37" "$*";}

# Slack：メッセージ送信
function slack_post() {

    # パラメータ取得
    hookUrl="$1"; shift
    text="$1"; shift

    # 必須パラメータチェック
    if [ "${hookUrl}" = "" ]; then
        log_error "hookUrl is empty."
        return 1
    fi
    if [ "${text}" = "" ]; then
        log_error "text is empty."
        return 1
    fi

    # text内に引用符が存在するとinvalid payloadとなるので除去
    text=$(echo ${text} | sed 's/"//g')

    # オプション指定を反映
    local OPTIND
    while getopts u:i:c: OPT
    do
        case $OPT in
            "u" ) username="$OPTARG";;
            "i" ) iconEmoji="$OPTARG";;
            "c" ) channel="$OPTARG";;
        esac
    done
    shift $((OPTIND - 1))

    payload=`cat << EOF
payload={
    "text": "${text}",
    "username": "${username}",
    "icon_emoji": "${iconEmoji}",
    "channel": "${channel}"
}
EOF
`

    # Slackに送信
    curl -X POST -d "${payload}" "${hookUrl}"
    return $!
}
# ================================
# 処理
# ================================
log_info "処理開始"

# 前回の出力をbeforeに変更
if [ -f "${OUTPUT_AWSCLI_JSON}" ]; then
    mv -f ${OUTPUT_AWSCLI_JSON} ${BEFORE_OUTPUT_AWSCLI_JSON}
fi

# RDSメンテナンス情報取得
output=`aws rds describe-pending-maintenance-actions | tee ${OUTPUT_AWSCLI_JSON}`

# メンテナンス情報が無ければ通知しない
if [ -f "${OUTPUT_AWSCLI_JSON}" ]; then
    maintenanceInfo=`cat ${output} | jq '.PendingMaintenanceActions[]'`
    if [ -z "${maintenanceInfo}" ]; then
        log_info "RDS maintenance info is not."
        log_info "処理終了"
        exit 0
    fi
fi

# 前回の出力と内容が変わっていなければ通知しない
if [ -f "${BEFORE_OUTPUT_AWSCLI_JSON}" ]; then
    diff ${BEFORE_OUTPUT_AWSCLI_JSON} ${OUTPUT_AWSCLI_JSON}
    if [ $? -eq 0 ] && "${NO_DIFF_NOT_NOTIFICATION}"; then
        log_info "RDS maintenance info is not changed."
        log_info "処理終了"
        exit 0
    fi
fi

# Slackに送信
if "${SEND_SLACK}"; then
#    text="<!channel> 保留中のメンテナンスアクションがあります。 ${output}"
    username="${SLACK_INFO_USER_NAME}"
    icon="${SLACK_INFO_ICON}"

    # 適用日が決まっていれば通知内容を変更
    if grep "ForcedApplyDate" "${OUTPUT_AWSCLI_JSON}"; then
#        text="<!channel> 強制的に適用予定のメンテナンスアクションがあります。 ${output}"
        username="${SLACK_EMERGENCY_USER_NAME}"
        icon="${SLACK_EMERGENCY_ICON}"
    elif grep -e "AutoAppliedAfterDate" -e "CurrentApplyDate" "${OUTPUT_AWSCLI_JSON}"; then
#        text="<!channel> 次回メンテナンスウィンドウ時に自動で適用予定のメンテナンスアクションがあります。 ${output}"
        username="${SLACK_NOTIFIER_USER_NAME}"
        icon="${SLACK_NOTIFIER_ICON}"
    fi

    # Slackに送信
    slack_post "${SLACK_WEBHOOK_URL}" "${output}" -u "${username}" -i "${icon}" -c "${SLACK_CHANNEL_NAME}"
fi

log_info "処理終了"
exit 0
```

どうやっても本文の改行が除去されてしまい、以下画像のような大変見辛い通知に...
![info_failed.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/4123b4e8-e94f-af65-a0a8-e73675a989ad.png)
</details></div>

[slack api](https://api.slack.com/methods/files.upload)では投稿するBotかユーザーを用意する必要がありますが、API内で[Incoming Webhooks](https://api.slack.com/incoming-webhooks)のようにアイコンが変更出来なかったので異なるアイコンのBotを用意し、それぞれのTokenを設定しています。
通常はBot1体分のTokenを設定すれば良いと思います。
もしより良い実現方法あればコメントいただけると嬉しいです！

```bash:RDS_maintenance_notification.sh
#!/bin/bash

# ================================
# 設定
# ================================
SHELL_FILE_NAME=`basename $0`
SHELL_DIR=$(cd "$(dirname "$0")" && pwd)
WORK_DIR=${SHELL_DIR}/work
LOG_DIR=${SHELL_DIR}/log

OUTPUT_AWSCLI_JSON=${WORK_DIR}/output_awscli.json
BEFORE_OUTPUT_AWSCLI_JSON=${WORK_DIR}/before_output_awscli.json
RDS_MENTENANCE_NOTICE_LOG=${LOG_DIR}/rds_maintenance_notice.log

# 前回と差分が無い場合（true:通知しない,false:通知する）
NO_DIFF_NOT_NOTIFICATION=true
# 結果をSlackへ（true:送信する,false:送信しない）
SEND_SLACK=true

# Slack設定
SLACK_FILEUPLOAD_URL="https://slack.com/api/files.upload"
SLACK_INFO_TOKEN="xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
SLACK_NOTIFIER_TOKEN="xoxb-yyyyyyyyyyyy-yyyyyyyyyyyy-yyyyyyyyyyyyyyyyyyyyyyyy"
SLACK_EMERGENCY_TOKEN="xoxb-zzzzzzzzzzzz-zzzzzzzzzzzz-zzzzzzzzzzzzzzzzzzzzzzzz"
SLACK_CHANNEL_NAME="#AWS通知"
# ================================
# 関数
# ================================
# 使用方法
function usage {
    cat <<EOF
Usage:
    $(basename ${0}) [options]
Description:
    $(basename ${0}) is a tool for Get pending RDS maintenance actions and send Slack notification
Options:
    -h                  show this help, then exit
EOF
    return 0
}

# オプション解析
while getopts h OPT ; do
    case $OPT in
        h)
            usage
            exit 0
        ;;
    esac
done

# ログ出力
function log() {
    time=`date "+%Y/%m/%d %H:%M:%S.%3N"`
    shellName=`basename ${0%.*}`
    level=$1; shift
    color=$1; shift
    echo -e "${time} ${level:0:1} [${shellName}] \e[${color}m$*\e[m"
}

# ログ出力：各レベル
function log_error() { log "ERROR" "1;31" "$*";}
function log_warn() { log "WARN" "1;33" "$*";}
function log_info() { log "INFO" "1;34" "$*";}
function log_debug() { log "DEBUG" "1;37" "$*";}

# Slack：ファイルアップロード
function slack_upload() {

    # パラメータ取得
    token="$1"; shift
    file="$1"; shift

    # 必須パラメータチェック
    if [ "${token}" = "" ]; then
        log_error "channels is empty."
        return 1
    fi
    if [ "${file}" = "" -a ! -f "${file}" ]; then
        log_error "file is empty or not exists: ${file}"
        return 1
    fi

    # パラメータをマップに格納
    declare -A parameters
    parameters=(
        ["token"]="${token}"
    )

    # オプションを格納
    # filename:ダウンロード時のファイル名、title未設定の場合はメッセージ上で見えるファイルタイトルにもなる
    # title:メッセージ上で見えるファイルタイトル、filename未設定の場合はダウンロード時のファイル名にもなる
    local OPTIND
    while getopts c:f:t:i:h:n: OPT
    do
        case "$OPT" in
            "c" ) parameters["channels"]="$OPTARG";;
            "f" ) parameters["filename"]="$OPTARG";;
            "t" ) parameters["filetype"]="$OPTARG";;
            "i" ) parameters["initial_comment"]="$OPTARG";;
            "h" ) parameters["thread_ts"]="$OPTARG";;
            "n" ) parameters["title"]="$OPTARG";;
        esac
    done
    shift $((OPTIND - 1))

    # cURLの引数にパラメータを格納
    curlArgs=()
    for paramName in ${!parameters[@]}; do
        paramValue="${parameters[$paramName]}"
        curlArgs=("${curlArgs[@]}" "--form-string" "${paramName}=${paramValue}")
    done
    # ファイルをパラメータに追加
    curlArgs=("${curlArgs[@]}" "-F" "file=@${file}")

    # Slackに送信
    log_debug "curl args: ${curlArgs[@]}"
    curl "${curlArgs[@]}" "${SLACK_FILEUPLOAD_URL}"
    return $!
}
# ================================
# 処理
# ================================
log_info "処理開始"

# 前回の出力をbeforeに変更
if [ -f "${OUTPUT_AWSCLI_JSON}" ]; then
    mv -f ${OUTPUT_AWSCLI_JSON} ${BEFORE_OUTPUT_AWSCLI_JSON}
fi

# RDSメンテナンス情報取得
aws rds describe-pending-maintenance-actions > ${OUTPUT_AWSCLI_JSON}

# メンテナンス情報が無ければ通知しない
if [ -f "${OUTPUT_AWSCLI_JSON}" ]; then
    maintenanceInfo=`cat ${OUTPUT_AWSCLI_JSON} | jq '.PendingMaintenanceActions[]'`
    if [ -z "${maintenanceInfo}" ]; then
        log_info "RDS maintenance info is not."
        log_info "処理終了"
        exit 0
    fi
fi

# 前回の出力と内容が変わっていなければ通知しない
if [ -f "${BEFORE_OUTPUT_AWSCLI_JSON}" ]; then
    diff ${BEFORE_OUTPUT_AWSCLI_JSON} ${OUTPUT_AWSCLI_JSON}
    if [ $? -eq 0 ] && "${NO_DIFF_NOT_NOTIFICATION}"; then
        log_info "RDS maintenance info is not changed."
        log_info "処理終了"
        exit 0
    fi
fi

# Slackに送信
if "${SEND_SLACK}"; then
    token="${SLACK_INFO_TOKEN}"
    now=`date "+%Y%m%d%H%M"`
    filename="rds_maintenance_info-${now}.log"
    filetype="text"
    message="<!channel> 保留中のメンテナンスアクションがあります。"
    title=`basename ${OUTPUT_AWSCLI_JSON}`

    # 適用日が決まっていれば通知内容を変更
    if grep "ForcedApplyDate" "${OUTPUT_AWSCLI_JSON}"; then
        token="${SLACK_EMERGENCY_TOKEN}"
        message="<!channel> 強制的に適用予定のメンテナンスアクションがあります。"
    elif grep -e "AutoAppliedAfterDate" -e "CurrentApplyDate" "${OUTPUT_AWSCLI_JSON}"; then
        token="${SLACK_NOTIFIER_TOKEN}"
        message="<!channel> 次回メンテナンスウィンドウ時に自動で適用予定のメンテナンスアクションがあります。"
    fi

    slack_upload "${token}" "${OUTPUT_AWSCLI_JSON}" -c "${SLACK_CHANNEL_NAME}" -f "${filename}" -t "${filetype}" -i "${message}" -n "${title}"
fi

log_info "処理終了"
exit 0
```
# cron設定
「AutoAppliedAfterDate」や「ForcedApplyDate」が設定されてから実際に適用されるまでの猶予についてAWSサポートへ問い合わせたところ、以下のような回答をいただきました。
> 「AutoAppliedAfterDate」や「ForcedApplyDate」は設定されてから実際に適用されるまでの期間については、特に一定猶予期間は定められておりません。メンテナンスをスケジュールする際に、メンテナンスの種類によって都度判断して設定する形にさせていただいております。
> なお、RDS のメンテナンスは、パッチの影響範囲、深刻度に応じて、フォーラムまたはメールにて通知させて頂いております。大規模なメンテナンスが計画される場合の連絡時期につきましては、通常は二週間前を目安に実施しておりますが、パッチの緊急度によっては直前のご連絡になることもございます。

1日1回以上確認してもあまり意味が無さそうなので、毎朝9時に情報を取得するようにcronにシェルを仕込みました。

```shell:/var/spool/cron/root
# RDS maintenance notification
0 9 * * * /usr/local/maintenance_notification/RDS_maintenance_notification.sh >> /usr/local/maintenance_notification/log/rds_maintenance_notice.log 2>&1
```
# 結果
丁度良いタイミングでメンテナンスが計画されたので、テストしてみたところ、以下画像のように通知できました！
<details><summary>※画像内のAlertとnotifierは通知テスト用にjsonを直接編集していますが、実際にはこのようなjsonが返却されます。</summary><div>

```json:PendingMaintenanceActionDetailsブロック内サンプル
{
    "PendingMaintenanceActions": [
        {
            "PendingMaintenanceActionDetails": [
                {
                    "Action": "system-update", 
                    "ForcedApplyDate": "2019-07-02T16:00:00Z", 
                    "CurrentApplyDate": "2019-06-18T14:00:00Z", 
                    "AutoAppliedAfterDate": "2019-06-18T14:00:00Z", 
                    "Description": "Upgrade to Aurora PostgreSQL 1.3.2"
                }
            ], 
            "ResourceIdentifier": "arn:aws:rds:ap-northeast-1:123456789123:cluster:hoge-cluster"
        }
    ]
}
```

</details></div>

![Alert.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/5a04477b-e14b-9c96-7002-607eb01d8a18.jpeg)
![notice.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/0d3811ea-5134-36c2-257f-f9c2e263a88a.jpeg)
![info.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/d09570d3-2214-cf6a-fc54-493ad8a8774f.jpeg)
# 参考
- AWSCLI

[describe-pending-maintenance-actions — AWS CLI 1.16.171 Command Reference](https://docs.aws.amazon.com/cli/latest/reference/rds/describe-pending-maintenance-actions.html)
[Amazon RDSのメンテナンスについて調べてみた - 本日も乙](https://blog.jicoman.info/2017/01/rds_maintenance/)
[【速報】Amazon RDS：メンテナンスへの柔軟な対応が可能に（pending-maintenance） ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/rds-pending-maintenance-actions/)

- Slack file upload

[files.upload method | Slack](https://api.slack.com/methods/files.upload)
[Slack APIでファイルをアップロードする方法 - UTALI](https://www.utali.io/entry/2016/10/07/172806)