Amazon Connectのちょっと便利な使い方!対応確認ができる自動電話通知

こんにちは。SCSKの和田です。

私は、SCSKのプライベートクラウド「USiZEシェアードモデル」(ユーサイズシェアードモデル)のサービス企画・開発を担当しています。「USiZEって何だろう?」と思われた方、または「ちょっと気になる!」という好奇心旺盛なあなた!USiZEの世界を覗いてみたくなったら、まずはこちらをクリック↓

SCSKのプライベートクラウド「USiZEシェアードモデル」とは?
SCSKのプライベートクラウド「USiZEシェアードモデル」(ユーサイズシェアードモデル)についてご紹介します。

さて今回は、ちょっとした工夫を施した自動電話通知の仕組みを作成したので、その紹介をさせていただきます。

そもそも なぜ作ったの?

正直なところ、システムのトラブルとは無縁でいたいのが本音ですが、いざという時には迅速な対応が求められます。
特にシステム監視からの重要なアラームが鳴った際には、ただちに通知を受け取りたいもの。ですが、夜中に急に鳴る電話にぼんやりと応答するのは、あまり賢明な対応とは言えませんよね。。。

そこで、電話に出た際に「きちんとアラーム発生の電話だと認識して、自分が対応し始めるぞ!」というアクションが伴う自動通知の仕組みが必要だと考えました。まずは、大まかなアーキテクチャからご紹介します。

 

アーキテクチャ

USiZE自動通知アーキテクチャ_サイズ変更

処理の流れを説明します。

  1. アラーム発生時に左上の「電話通知用Lambda」を起動します。連絡先電話番号テーブルから連絡先を取得し、Amazon Connectを利用して電話をかけます。電話をかけたタイミングで、電話結果格納用テーブルに通話が開始されたことを登録します。
  2. 相手が電話に出て「ダイヤル1を押す」という対応の意思表示をした場合、右上の「電話結果確認用Lambda」を呼び出し、1で登録した電話結果格納用テーブルに結果を更新して終了します。
  3. 相手が電話に出たけれどダイヤル1を押さなかった、または電話に出なかった場合、「電話通知用Lambda」を非同期で呼び出すことで、次の連絡先へ電話をかける処理を開始します。

つまり、電話に出た際にダイヤル1を押すという明確なアクションを誰かが実施するまでは、アラーム担当者たちに順番に電話がかかり続けるというものです。次は、実際にどうやって作ったのかをご説明します。

 

どうやって作ったの?

Step1.監視設定・DynamoDBテーブル作成

まず、Amazon CloudWatch等を使用して監視システムを整えます。詳細な設定方法は他の記事に譲るとして、要点はアラームが作動した際にLambda関数を自動で起動することです。これにより、アラームの発生をシステムが即座に検知し対応に移れるようにします。
そして、「連絡先電話番号テーブル」と「電話結果格納テーブル」の2つのDynamoDBのテーブルを作成します。

  • 連絡先電話番号テーブル
    パーティションキーはcall_group(String)、ソートキーはpriority(Number)を指定し、下記のようなデータを登録します。
    処理を動かすには最低限これだけあれば十分です。
    ※call_group:電話を掛ける対象のグループ、priority:通知順序の数字、phone_number:電話を掛けたい相手の電話番号

USiZE連絡先電話番号テーブル

  • 電話結果格納テーブル
    パーティションキーはcontact_id(String)を指定しておきます。こちらは事前にデータ登録は不要です。

Step2.Lambdaの作成

続いて、「電話通知用」と「電話結果確認用」の2つのLambdaを作成します。どちらもランタイムはPython3.9です。
IAMロールについては今回の記事のメインではないので、説明を割愛します。適宜、必要となる権限を付けたロールを作成してください。

電話通知用Lambda

まず連絡先電話番号テーブルに登録されている1人目に電話をかけ、ダイヤル1を押すという意思表示がされた場合には処理を終了します。意思表示の対応が確認できなかった場合には、自分自身を非同期で呼び出し、次の人へ電話がかかるようにして処理を終了します。
電話をかける相手が多数いる場合や何周も電話をかけたい場合に、Lambdaのタイムアウト上限に達しないように、1回の電話ごとにLambdaを終了させるようにしています。

説明用に様々な設定情報を環境変数に設定していますが、実際の運用では設定ファイルをS3バケットに配置し、そこから読み込むなど、状況に応じて適宜変更してください。

import json
import boto3
import os
import time

from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource("dynamodb")
clientLambda = boto3.client("lambda")
connect = boto3.client("connect")

def lambda_handler(event, context):

    print(json.dumps(event))
    # *****処理1*****
    # 環境変数に適宜必要となる情報を設定しておき、取得する
    connectInstanceId = os.environ["CONNECT_INSTANCE_ID"]  # Amazon ConnectのインスタンスID
    connectContactFlowId = os.environ["CONNECT_CONTACTFLOW_ID"]  # Amazon ConnectのコンタクトフローID
    callerPhoneNumber = os.environ["CALLER_PHONE_NUMBER"]  # 発信元の電話番号
    callStartMessage = os.environ["callStartMessage"]  # 電話開始時の音声 例)<speak>アラームが発生しました。対応する場合は、ダイヤル1を押してください<speak>
    callConnectMessage = os.environ["callConnectMessage"]  # ダイヤル1が押されたときの時の音声 例)<speak>対応を確認しました<speak>
    callNotConnectMessage = os.environ["callNotConnectMessage"]  # 電話が繋がらなかったときの音声 例)<speak>次の人へお繋します<speak>
    callGroup = os.environ["call_group"]  # 電話を掛ける対象のグループ
    nextLambda = os.environ["lambda_name"]  # 次に実行するLambdaの名前。このLambdaを再帰的に呼び出すために使用
    # DynamoDBのテーブル名を取得
    phoneTable_name = os.environ["phoneTable_name"] # 連絡先電話番号テーブル
    contactTable_name = os.environ["contactTable_name"] # 電話結果格納テーブル
    phoneTable = dynamodb.Table(phoneTable_name)
    contactTable = dynamodb.Table(contactTable_name)

    # *****処理2*****
    # 何周目の何番目に登録の人に電話通知したいのかを判定。初回実行の場合は初期値を設定し、2回目以降は前回の情報を引き継ぐ
    # 2回目以降の実行の場合は、前回の情報を引き継ぐ
    if "processId" in event.keys():
        processId = event["processId"]
        callLoop = int(event["call_loop"]) # 何周目か
        lastPhoneOrder = int(event["phone_order"])  # 前回は何番目の人に電話をかけたのか

    # 初回実行の場合(processIdが存在しない場合)は初期値を設定
    else:
        processId = context.aws_request_id # lambdaのRequestIdをプロセスIDに設定
        callLoop = 1  # 初回なので1周目とする
        lastPhoneOrder = 0  # 初回なので0番目(前回はいない)とする

    # *****処理3*****
    # 電話を掛ける対象のグループ(1周)が何人なのか、何周目かを判定し、周回上限を超えるまでは次にかける電話番号を取得する
    try:
        phoneTableQueryResponse = phoneTable.query(
            KeyConditionExpression=Key("call_group").eq(callGroup)
        )
        # call_group内の電話番号の数
        maxPhoneOrder = phoneTableQueryResponse["Count"]

    except Exception as error:
        print("[ERROR]連絡先電話番号テーブルから通知先電話番号数の取得に失敗")
        print(error)
        return {'statusCode': 400 }

    nextPhoneOrder = lastPhoneOrder + 1 # 次にかける電話番号の順番
    # 次にかける電話番号の順番がcall_group内の通知先電話番号の数を超えていた場合は1番目に戻し、周回数を+1する
    if nextPhoneOrder > maxPhoneOrder:
        nextPhoneOrder = 1
        callLoop += 1

    # 周回数が3以上になったら終了させる(2周まで電話をかけるとします)
    if callLoop >= 3:
        print("[INFO]上限周回数まで電話をかけ続けましたので、処理を終了します")
        return {'statusCode': 200 }
    # 連絡先電話番号テーブルから次かける電話番号を取得
    try:
        phoneTableGetResponse = phoneTable.get_item(
            Key={"call_group": callGroup, "priority": nextPhoneOrder}
        )
        phoneNumber = phoneTableGetResponse["Item"]["phone_number"] # 次にかける電話番号

    except Exception as error:
        print("[ERROR]電話番号テーブルから次の電話番号の取得に失敗")
        print(error)
        return {'statusCode': 400 }

    # *****処理4*****
    # Step3で作成するコンタクトフローを指定して電話を掛ける
    try:
        connectResponse = connect.start_outbound_voice_contact(
            DestinationPhoneNumber=phoneNumber,  # 送信先の電話番号
            ContactFlowId=connectContactFlowId,
            InstanceId=connectInstanceId,
            SourcePhoneNumber=callerPhoneNumber,
            Attributes={
                "callStartMessage": callStartMessage,
                "callConnectMessage": callConnectMessage,
                "callNotConnectMessage": callNotConnectMessage,
            },
        )

    except Exception as error:
        print("[ERROR]Amazon Connectへの連係に失敗")
        print(error)
        return {'statusCode': 400 }

    contactId = connectResponse["ContactId"]  # 発信のContactID

    # *****処理5****
    # 電話結果格納テーブルに通話開始を登録し、結果が格納されるのを待つ
    try:
        contactTabelputResponse = contactTable.put_item(
            Item={
                "contact_id": contactId,  # 発信のContactID
                "call_flag": 0,  # 電話繋がったかフラグ 繋がれば1になる
            }
        )

    except Exception as error:
        print("[ERROR]電話結果格納テーブルへのコールフラグの準備に失敗")
        print(error)
        return {'statusCode': 400 }

    # 応答があり電話結果格納テーブルが更新されるまで待機
    time.sleep(60)

    # 電話結果格納テーブルからcall_flagの情報を取得
    try:
        contactTableGetResponse = contactTable.get_item(
            Key={
                "contact_id": contactId,
            }
        )
        callFlag = contactTableGetResponse["Item"]["call_flag"]  # 電話繋がったかフラグ

    except Exception as error:
        print("[ERROR]電話結果格納テーブルからコール結果の取得に失敗")
        print(error)
        return {'statusCode': 400 }

    # *****処理6*****
    # 電話応答の結果から、つながっていれば処理を終了し、つながっていなければ次の人に電話をかける
    if callFlag == 0:
        try:
            passEvent = {
                "processId": processId,
                "phone_order": nextPhoneOrder,
                "call_loop": callLoop,
            }

            lambda_responce = clientLambda.invoke(
                FunctionName=nextLambda,
                InvocationType="Event",  # 非同期的に呼び出す
                Payload=json.dumps(passEvent),
            )

            # 呼び出しタイプがEventの場合はステータスコードが202が正常
            lambdaStatusCode = lambda_responce["StatusCode"]
            if lambdaStatusCode == 202:
                print(f"[INFO]{nextLambda}にステータスコード「{str(lambdaStatusCode)}」で正常に連携されました")
            else:
                print(f"[ERROR]{nextLambda}にステータスコード「{str(lambdaStatusCode)}」で正常に連携されませんでした。")

        except Exception as error:
            print("[ERROR]次のLambda呼び出しに失敗")
            print(error)
            return {'statusCode': 400 }

    # 電話が繋がっていれば終了
    elif callFlag == 1:
        print("[INFO]受電を確認したため架電を終了")

    print(f"[INFO]{context.function_name}を正常に終了")
    return {"statusCode": 200}

 

電話結果確認用Lambda

ダイヤル1が押された場合にのみ、Amazon Connectコンタクトフローから呼び出されるLambdaです。
説明用に割愛していますが、通知先や通知時刻等もテーブルに書き込むと、後からログとしても確認がしやすい状態になります。

import json
import boto3
import os

def lambda_handler(event, context):
    print(json.dumps(event))

    # 環境変数に電話結果格納用DynamoDBのテーブル名を設定しておき、取得する
    table_name = os.environ["contactTable_name"]
    dynamodb = boto3.resource("dynamodb")
    table = dynamodb.Table(table_name)

    try:
        # contactidを取得して、電話結果格納用DynamoDBのcall_flagを更新
        contactId = event["Details"]["ContactData"]["ContactId"]
        response = table.update_item(
            Key={
                "contact_id": contactId,
            },
            UpdateExpression="set call_flag=:a",
            ExpressionAttributeValues={":a": 1},
        )

    except Exception as error:
        print("[ERROR]コールフラグの変更に失敗")
        print(error)
        return {"statusCode": 400}

    return {"statusCode": 200}

Step3.Amazon Connectコンタクトフロー作成

以下、Amazon Connectのインスタンス作成、電話番号の取得は終わっていることを前提とします。
Amazon Connectの初期設定はこちらの記事で解説していますので是非ご参照ください。

それでは適宜、Amazon Connect側の設定が完了している前提で、今回の肝となるAmazon Connectコンタクトフローについて説明します。ここでは以下の流れに沿って、電話をかけるプロセスを作成します。

  1. 電話接続(発信)・ログや音声の設定
  2. 自動音声にて「アラームが発生しました。対応する場合は、ダイヤル1を押してください」と案内し、相手がダイヤル1を押したかどうかを15秒間チェック ※下記コンタクトフローの赤枠の「顧客の入力を保存する」の部分で定義
  3. ダイヤル1が押された場合は、Step2にて作成した電話結果確認用Lambda関数を呼び出して、「対応を確認しました。」と案内し、通話を終了
  4. ダイヤル1が時間内に押されなかった場合は、「次の人にお繋ぎします。」と案内し、通話を終了

USiZE自動通知コンタクトフロー

このシンプルな流れによって、確実に対応意思表示のアクションが取られるまでのプロセス自動化が可能となります。

 

最後に

この自動通知の仕組みよって、ちょっとした改善が日々の業務にもたらされました。大幅な変化というわけではありませんが、これまでの手作業による連絡リストの確認や、数回の電話をかける手間が省けたのは大きな進歩です。

USiZE(ユーサイズ)が提供するサービスも、このような小さな工夫を積み重ねて、お客様にとってより使いやすく、信頼性の高いものになるよう日々努力しています。私たちが提供するサービスを通じて、皆様のビジネスもさらにスムーズに、そして快適に進んでいくことを願っています。もし、USiZE(ユーサイズ)についてもっと知りたいと思われたら、ぜひ以下サイトを訪れてみてください。

運用付きの国産クラウドサービス USiZEシェアードモデル

SCSKのプライベートクラウド「USiZEシェアードモデル」とは?

最後までお読みいただきありがとうございました。
今回の小さな工夫が、皆様の仕事に少しでも役立つことを願っています。それでは、快適なITライフをお過ごしください。

タイトルとURLをコピーしました