LambdaとAmazon ConnectでRDS障害時のSlack通知と電話連絡を行う

# 概要
* RDSで障害が起きた時にSlackに通知すると同時に電話も掛けて欲しかったのでLambdaとConnectを使って実装しました。
* 単純に電話を掛けるだけではなく、複数人に電話をして１人目が対応可能だったら他の人には電話せずということも実現したかったのでそれも実装しました。
* 実際に実装してみて気になる細かい点（後述）はありますが、それは徐々に改善していこうと思っています。

# 処理の流れ
* RDSのイベントでトリガー
* SNSが受け取ってSlack通知用Lambda（Lambda1）と電話連絡用Lambda（Lambda2）を呼び出す
    * Slack通知用LambdaはRDSイベントの内容（どのRDSでどんな障害なのか）をSlackに通知
    * 電話連絡用LambdaはConnectを呼び出して電話を掛ける
        * 誰が対応可能なのか判定も行ってSlackへ通知

![rds-event-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386865/58d3de68-4093-9127-3531-e47458a9256f.png)

# 前提
* RDSイベントやSNSやConnectのセットアップなどの設定についてはここでは説明はしません。
* LambdaやConnectの実行権限は適宜付与してください。
* Lambdaのコードが拙い部分があるかと思いますがご容赦ください。

# 設定の流れ
1. Connectのセットアップ（省略）
2. Connectの問い合わせフロー設定
3. Lambda作成
4. SNS作成（省略）
5. RDSイベント設定（省略）

# Connectの問い合わせフロー
* 下記のように作りました。
    * `ログ記録動作の設定`はログを見たかったので有効化
    * `音声の設定`は適当に（裏ではPolly使ってるみたいですね）
    * `プロンプトの再生`でLambdaから呼び出される時に渡されるテキストを読むように設定
    * `顧客の入力を取得する`で番号を入力させて、対応するなら１、対応しないなら２で分岐するよう設定
    * `問い合わせ属性の設定`で対応するか否かを変数として持たせる
        * 後ほどLambdaでこの属性を取得する 
![connect-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386865/521d197b-ac0e-7dc7-9498-8d944c60fef9.png)


# Lambda
* 電話連絡用Lambdaだけ記載します。
* ロギングの設定は適宜行うことをおすすめします。
* Pythonで書きました。

```
import os
import json
import logging
import boto3
from datetime import datetime, timedelta
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Lambdaの環境変数からConnectで取得した発信元電話番号とインスタンスIDと問い合わせフローIDを取得
SOURCE_PHONE_NUMBER = os.getenv('SOURCE_PHONE_NUMBER')
INSTANCE_ID = os.getenv('INSTANCE_ID')
CONTACT_FLOW_ID = os.getenv('CONTACT_FLOW_ID')

# Lambdaの環境変数からSlack通知用のWebHookURL取得
Slack_Webhook_URL = os.environ['SLACK_WEBHOOK_URL']

# Lambdaの環境変数から電話を掛ける相手の番号と名前を取得
destination_phone_number = [os.getenv('DESTINATION_NUMBER_1'),os.getenv('DESTINATION_NUMBER_2'),os.getenv('DESTINATION_NUMBER_3')]
person_name = [os.getenv('NAME_1'),os.getenv('NAME_2'),os.getenv('NAME_3')]

def lambda_handler(event, context):
    # SNSから取得されるRDSイベントの内容
    rds_event_message = json.loads(event['Records'][0]['Sns']['Message'])

    # RDSイベントのメッセージをセット
    target = (rds_event_message["Source ID"])
    event_message = (rds_event_message["Event Message"])
    tel_message = "データベースで障害です。" + "対象のデータベースは " + target + "で、" + "障害内容は " + event_message + "です。"

    # 誰も対応しないということを初期設定    
    isCorrespond = "false"

    # ここからConnectの処理
    connect = boto3.client('connect' , region_name='ap-northeast-1')

    for number,name in zip(destination_phone_number,person_name):
        # connectで電話を掛ける
        contact = connect.start_outbound_voice_contact(
            DestinationPhoneNumber=number,
            ContactFlowId=CONTACT_FLOW_ID,
            InstanceId=INSTANCE_ID,
            SourcePhoneNumber=SOURCE_PHONE_NUMBER,
            Attributes={
                'message': tel_message ,
                'isCorrespond': 'false'  # ここはもしかしたら不要かもしれません
            }
        )

        # 電話を掛け始めて一旦待つ
        time.sleep(60)
    
        # Connectの結果を取得
        contact_id = contact['ContactId']
        attributes = connect.get_contact_attributes(
            InstanceId=INSTANCE_ID,
            InitialContactId=contact_id)
        
        # 対応者がいるかどうかの結果をセット
        isCorrespond = attributes['Attributes']['isCorrespond']
        
        if (isCorrespond == "true"):
            # 対応者あり
            slack_post_correspond("[対応者]" + name)
            break;
        else:
            # 対応者がいない
            continue;

    # 全員に電話を掛けても対応者不在
    if(isCorrespond == "false"):
        slack_post_correspond("対応者不在")

# Slackに対応者情報投稿
def slack_post_correspond(message):
    slack_message = {
        'text': "*" + message + "*",
    }

    req = Request(Slack_Webhook_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", Slack_Webhook_URL)
        return {
            'statusCode': 200
        }
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)
```


# 気になる細かい点
* 電話連絡先の番号や名前を設定する部分がイマイチな感じはありますが、頻繁に変動するものではないので良しとしています。理想はDBから取得したりしたいところです。
* Connectで電話を掛ける処理の呼び出しはsleepで強制的に待っているので、Connect側で何かいい感じに制御出来るものがあればなと思っています。
* RDSで複数イベントが発生した時に複数電話がかかってきてしまい（Lambdaが複数実行される）、ワチャワチャしてしまうので改善したいところです。

# 所感
* 色々と改善したいと感じるところはありましたが、Connectを使って障害時の電話連絡が実装出来て良かったです。
* これの応用が出来ればEC2障害時の一次対応が自動化出来たりしそうだなと感じています。
* Connectすごい！

# 最後に
* 初めてQiitaに記事を投稿したので色々とご容赦ください:bow: