Amazon Connect による障害検知連絡を構築した話

こんにちは。
テクノロジー本部で内製開発をしているKenny Songです。

私の所属するデータ共通BITAグループでは、パーソルキャリアが提供している doda をはじめとした様々な情報システムから取得するデータを分析基盤として提供・運用を行なっています。

その中で内製開発部隊は現在、基盤側のシステム管理・構築を主に担当しています。
(データを活用したAIやレコメンド開発は別に部隊が存在しています)

今回は Amazon Connect を使用し Zabbix で検知した障害内容を自動でアウトバウンドコールさせる仕組みを構築、トライアル運用を開始したので本記事にて我々が行っている業務の参考としてご紹介したいと思います。  

Content: 

 

1. 構築に当たっての背景

分析基盤では様々なデータを取り扱っており、社内でのデータ活用推進がされていったことでデータレイクとしての機能も担うようになりシステム間連携の本数も増えていきました。
これまでは基本的に分析環境内での閉じた運用が多かったため障害検知用の仕組みは必要最低限しか導入されていませんでした。

よりSLAを求められる基盤と成長してきたため、自動復旧に失敗した際の検知方法としてテキストベースな通知配信の他に人を介さず自動発信を行う音声ベースの通知を導入することとなり企画されました。

2. システム要件

まず今回実現したかった要件は以下の通りでした。

  • システム毎に複数人の連絡先を指定できること
  • 架電先に優先順位を割り振り、先頭から順番に架電できること
  • 架電先が応答しなかった場合は次のメンバーに再度連絡し、誰も出ずに一巡したら再度先頭に戻って連絡を継続すること

ポイントとして、連絡先に指定するメンバーは一人ではなく複数人設定し優先順位を指定したいケースが大半だと思います。

よって電話に出られなければ連続して次のメンバーに電話を繰り返すようにする必要があります。
ただし、基本的に Amazon Connect はインバウンドコールをベースにしたコンタクトセンターのサービスです。

  • 電話に応答したかどうか
  • 途中で顧客が通話を切ったかどうか
  • 顧客とAgent、どちらから通話を切ったのか

このような架電結果を確認出来るものは全てアウトバウンドでは対応していません。
Lambda からAmazon Connect を呼び出して架電させることは出来るのですがその結果は取得する術がなく、電話自体は非同期で実行されます。

よって、電話に出なかったら次のメンバーに架電を回すという要件は Amazon Connect 単体で実現することができないので、Dynamo DB に応答ステータス持たせ Lambda から参照しその結果によって引き続き電話するかどうかの処理を分岐させることにしました。

3. Architecture

システム全体の概要は次のようになっています。

f:id:KennySong:20201113174026p:plain

使用したリソースは S3, Amazon Connect の他に3種類の Lambda と SQS が2つ、ステータス管理用のDynamoDB という構成です。

Dynamo DB を選択した理由としてはサーバレスなマネージドサービスを活用した運用負担の低減を狙った点と、今後対象ホストが増えた際の拡張性、Lambda との相性の良さ[^ 現在は Amazon RDS Proxy が発表されたためRDSを選択するのもアリです。] がポイントでした。

それぞれの機能を小さくシンプルにすることで全体のコントロールが Dynamo DB に集約し、保守メンテナンス性に優れるよう設計してみました。

 

4. 処理の流れ

一連の処理は Zabbix で障害を検知した際、S3 に障害情報が記載されたファイルを投げ込まれることから始まります。

f:id:KennySong:20201113174937p:plain

このファイルは Json 形式を採用しており以下のフォーマットとしました。

{
  "host_name": "hogehoge-webap1",
  "trigger_name": "CPU使用率が90%を越えました。",
  "event_date": "2020-11-01",
  "event_time": "02:11:46"
}

対象のホスト名の他、発生した障害のサマリと日時が最低限記載されています。
尚、この形式であれば連携元が Zabbix である必要性は全くないです。

これをPut Eventsで架電シークエンス開始用の SQS にキューさせています。
架電シークエンス開始用 SQS が担っている役割は、Amazon Connect からコンタクトフローを呼び出す Lambda をキックします。

キックされた Lambda は event 引数から S3 のファイルパスを取得し、トリガーとなった Json ファイルをパースします。 ここで取得されるホスト名を使用し Dynamo DB に障害情報の登録を行い、架電する連絡先を取得します。

def get_dynamodb_items(query_key=None):
    if query_key is None:
        query_key = {}
    dynamoDB = boto3.resource("dynamodb")
    table = dynamoDB.Table(os.environ["DYNAMODB_TABLE"])
    try:
        response = table.get_item(Key=query_key)
    except ClientError as e:
        logger.error(e.response["Error"]["Message"])
    else:
        return response["Item"]
def update_dynamodb(contacts, info):
    [contact.setdefault("Answered", "-1") for contact in contacts]

    dynamoDB = boto3.resource("dynamodb")
    table = dynamoDB.Table(os.environ["DYNAMODB_TABLE"])
    table.update_item(
        Key={"HostName": info["host_name"]},
        UpdateExpression="set Attrs.ErrorNotice=:E",
        ExpressionAttributeValues={
            ":E": {
                "TriggerName": info["host_name"],
                "EventDate": info["event_date"],
                "EventTime": info["event_time"],
                "RecursionStatuses": contacts,
            },
        },
        ReturnValues="UPDATED_NEW",
    )

実際に喋らせるメッセージはテキスト形式でもある程度喋ってくれるのですが、サーバ名やシステム名といった綺麗な英単語にならない横文字をうまく発音することができないので音声合成マークアップ言語(SSML)で渡しています。

コンタクトフローを呼び出す部分は次のようになっています。

connect = boto3.client("connect")
connect.start_outbound_voice_contact(
    DestinationPhoneNumber=priority_sorted[0]["PhoneNumber"],
        ContactFlowId=os.environ["CONNECT_CONTACT_FLOW_ID"],
    InstanceId=os.environ["CONNECT_INSTANCE_ID"],
    SourcePhoneNumber=os.environ["CONNECT_SOURCE_PHONE_NUMBER"],
    Attributes={
        "host": props["trigger"]["host_name"],
        "message": props["message"],
        "prompt": prompt,
        "priority": props["priority"],
    },
)

Attributes={}‌ で渡す属性は Amazon Connect 側から参照できるので条件分岐などに使用するパラメータはここに載せておくと吉です。
この仕組みを利用することで作り込む必要はありますが Amazon Connect がアウトバウンドで提供していない機能をカバーすることができます。

呼び出し先のコンタクトフローはこんな感じです。

f:id:KennySong:20201116111640p:plain

「顧客の入力を保存する」という部分でユーザに入力を求めることができます。
求める際に開幕喋らせる音声は Attributes で引き渡す属性を指定することが可能で、今回は "message" の中に入れた文言を喋らせました。

コンタクロフロー内からLambdaを呼び出す際にも Attributes の属性を引数に渡すことができます。
呼び出し元 Lambda → コンタクトフロー → 入力結果保存Lambda とリレーさせることでどのメンバーに応答結果を記録するか判定させています。

dynamoDB = boto3.resource('dynamodb')
table = dynamoDB.Table(os.environ['DYNAMODB_TABLE'])
phone_number = event['Details']['ContactData']['CustomerEndpoint']['Address']
input_data = event['Details']['Parameters']['inputData']
flag = '-2' if input_data == 'Timeout' else '1'
host_name = event['Details']['Parameters']['hostName']
try:
    response = table.get_item(Key={
        'HostName': host_name,
    })
except ClientError as e:
    logger.info(e.response['Error']['Message'])

statuses = response['Item']['Attrs']['ErrorNotice']['RecursionStatuses']
for status in statuses:
    if phone_number in status.values():
        target = str(statuses.index(status))

Amazon Connect から引数で渡した値は event['Details']['Parameters'] の中に格納されています。
尚、コンタクトフローで指定したタイムアウト値内に電話を受けたメンバーが何かしらの値を入力しなかった場合は Timeout という文字列がAWSによって埋め込まれています。
この関数では電話に出て何かしの値を入力していれば '1' を、Timeoutであれば '-2' をDynamoDBに書き込みます。

以上の処理を纏めたのが下記のシークエンスです。

f:id:KennySong:20201116111715p:plain

応答結果を確認する Lambda は SQS 側で80秒間の実行遅延を経て呼び出されます。
これは Amazon Connect の最大呼び出し時間が60秒であるためで、59秒目で応答したケースを想定し余裕を持たせた値にしています。

Lambda で行っている内容は Dynamo DB にクエリを発行し結果が応答であれば処理終了、応答していなければ再度コンタクトフローを呼び出し次のメンバーへ架電させ SQS にキューしています。

最後にDynamo DB に保持しているマスタのサンプルです。

{
  "Attrs": {
    "ErrorNotice": {},
    "Maintainers": [
      {
        "Name": "保守電話1",
        "PhoneNumber": "+81XXXXXXXXXX",
        "Priority": "1"
      },
      {
        "Name": "保守電話2",
        "PhoneNumber": "+81XXXXXXXXXX",
        "Priority": "2"
      },
      {
        "Name": "保守電話3",
        "PhoneNumber": "+81XXXXXXXXXX",
        "Priority": "3"
      }
    ]
  },
  "CreatedAt": "2020-08-22 15:00:00",
  "HostId": 1,
  "HostName": "hogehoge-webap1",
  "UpdatedAt": ""
}

Dynamo DB にはこのようにホスト名の他、架電先の情報を持たせ、電話を掛ける順番は Maintainers 属性の中に置いている "Priority" で指定しています。
処理開始の Lambda がトリガーファイルから障害情報を読み取ると "ErrorNotice" 属性の中に詳細と "Maintainers" を "Priority" を追加した状態で転写します。
これによって今誰に架電していて応答結果はどうだったか、次は誰に架電するのか を判断させています。

5. コストについて

Amazon Connect の課金形式は基本的に通話時間と通話回数で決まります。
今回のケースでは相手が電話に出た後の通話時間は、ガイダンスが流れた後そのまま通話終了となるためせいぜい多くて20秒程度です。

となると、あとはどれくらいの頻度で架電するか = 障害が発生するか で見積れます。
電話番号1つに通話時間1分、月に100回の架電として試算してみると驚異の1,000以下!
弊社の実績値でも500円以下という結果になっています。  

24時間体制で人間を張るとどうしてもNode単位で月に100$近くのコストがかかったりしますが、これならNode数が幾つあっても純粋に障害発生件数で課金されるので上振れも少ないですね。

Zabbixなどのミドルウェア側で障害の規模を判定し軽めのものはSlack等に通知のみ、重度なものは通知+電話という風にコントロールすれば架電回数はより抑えられそうです。

まとめ

マネージドサービスを組み合わせることで最低限の実装で済んだため企画から設計、実装まで2人日で完成させることができました。
Amazon Connect は CloudFormation にネイティブ対応してる部分が少なかったりと痒いところに手が届かない感は多少なりともありますが、全体的に触っていてとても面白いサービスでした。
ただし一度作成したコンタクトフローは物理削除不可というキツい制約があるので要注意です。

そのうえ、デフォルトでは単一アカウントに作成できる Amazon Connect インスタンスは2つまで。
これは申請することで緩和できるようですが、複数の部で共有しているAWSアカウントなどの場合は留意が必要です。

alt

Kenny Songさんのプロフィール

インフラ基盤統括部 データ共通BITA部 データ基盤グループ リードコンサルタント

※2020年11月現在の情報です。

▶パーソルキャリアの求人ページはこちらから