10
5

More than 3 years have passed since last update.

AWSサーバレスアーキテクチャで、よくある安否確認システムをSlackに対応させる

Last updated at Posted at 2019-10-26

災害時に気づかれない安否確認を何とかしたかった

先日の台風19号の上陸時、自社の安否確認システムが発動し、全社員に一斉連絡が行われました。
この連絡は、安否確認システムに登録された電話番号とメールアドレス宛に行われることとなっています。
これは、各社でよくある安否確認システムの仕組みだと思われます。
しかし、今回の安否確認連絡に対する回答率は、普段実施している訓練の時よりも悪くなってしまったようです。
個人的な考察ですが、訓練では事前にメールを送信されると知らされているから回答率が良いのであり、いざ本番となると気づかない人が多いのだと考えました。
電話はEメールよりも効果的だと思うのですが、いたずら電話と勘違いして、取られないのかもしれません。

部員全員が使っているSlackに通知したら回答率が上がるのではないか

「Eメールは埋もれる」「電話は取られない」のだとすると、皆がもっと良く見るところに通知すれば、お互いに声を掛け合って安否確認できるのではないか、と考えました。
そこで、部員が全員使っているSlackに、緊急事態を煽るように通知すれば、回答率を高められるのではないかと思い、AWSのサーバレスアーキテクチャを駆使して、安否確認連絡をSlackに通知するようにしてみました。

安否確認をSlackに流すサーバレスアーキテクチャ

以下の図のように、安否確認システムが発送したメールを、Amazon SESのメール受信機能で受け、メール受信イベントでAWS Lambdaをトリガーして、LambdaからWebhookでSlackに通知します。
image.png

作り方

注: AWSのリージョンはus-east-1となります。

1. SlackのIncoming Webhookが使えるようにする

以下の記事に最高に詳しくまとめられています。

slackのIncoming webhookが新しくなっていたのでまとめてみた
https://qiita.com/kshibata101/items/0e13c420080a993c5d16

この記事の解説を読みながら、SlackにIncoming Webhookで通知するためのアプリを作成します。
その後、#general(または全員が参加しているチャンネル)に通知するためのWebhook URLを取得します。

こんな感じでWebhook URLが取れますのでコピーしておきます。
image.png

2. Lambda Functionを作る

Lambda Functionの環境変数SOURCE_MAIL_ADDRESSに送信元メールアドレス(安否確認システムのメールアドレス)、WEBHOOK_URL_OF_SLACKにSlackのWebhook URLを指定します。
urllib.requestを使ってWebhook URLに通知を流し込みます。
悪用防止の為、Amazon SESのメール受信イベントで送信元メールアドレスを判定し、安否確認システムからのメールのみ処理するようにしています。
また、通知メッセージはコードに埋め込みましたが、お好みで外部化したり、カスタマイズできます。

# -*- coding: utf-8 -*-

import datetime
import traceback
import urllib.request
import json
import os

# ロギング用関数
def logging(errorLv, lambdaName, errorMsg):
    loggingDateStr=(datetime.datetime.now()).strftime('%Y/%m/%d %H:%M:%S')
    print(loggingDateStr + " [" + errorLv + "] " + lambdaName + " " + errorMsg)
    return 0

# Lambdaハンドラ
def lambda_handler(event, context):
    logging("info", context.function_name, "実行開始")
    try:
        # メールの情報からソースメールアドレスを抽出
        source = event["Records"][0]["ses"]["mail"]["source"]

        # ソースメールアドレスをチェック
        if source == os.environ['SOURCE_MAIL_ADDRESS']:
            # Slack投稿用Data (textをお好みで変更可能)
            slackData = {
                "text": "安否確認メールが発報されました。至急、メールボックスを確認し、回答してください。",
                "blocks": [
                    {
                        "type": "section",
                        "text": {
                            "type": "mrkdwn",
                            "text": '*【プロパー宛】*\n*安否確認メールが発報されました。*\n*至急、メールボックスを確認し、回答してください。*\n※協力会社の方も、各社毎の安否確認の対応をお願いします。'
                        }
                    },
                    {
                        "type": "section",
                        "text": {
                            "type": "mrkdwn",
                            "text": '*<https://xxxxx.co.jp|エマージェンシーコール>*\n安否確認システムからの緊急連絡です。'
                        }
                    },
                    {
                        "type": "divider"
                    },
                    {
                        "type": "context",
                        "elements": [
                            {
                                "type": "image",
                                "image_url": "https://a.slack-edge.com/80588/img/blocks/bkb_template_images/notificationsWarningIcon.png",
                                "alt_text": "notifications warning icon"
                            },
                            {
                                "type": "mrkdwn",
                                "text": '*緊急アラートです。至急の回答が必要です。*'
                            }
                        ]
                    }
                ]
            }

            # Slackに投稿
            headers = {
                'Content-Type': 'application/json'
            }

            postRequest = urllib.request.Request(os.environ['WEBHOOK_URL_OF_SLACK'], data=json.dumps(slackData).encode("utf-8"), method="POST", headers=headers)

            tempResponse = bytes()
            with urllib.request.urlopen(postRequest) as postResponse:
                tempResponse += postResponse.read()

            if (str(tempResponse, encoding='utf-8') != 'ok'):
                raise Exception("Invalid Response from " + os.environ['WEBHOOK_URL_OF_SLACK'])


            response = {}
            response.update([('response', 'OK'), ('request', slackData)])

            logging("info", context.function_name, "Slackチャンネルへの投稿完了:" + str(response))
            logging("info", context.function_name, "実行終了")    
            return response

        else:
            raise ValueError("Receiving mail from an invalid address")

    except Exception as OtherException:
        # error時. lambda_handlerは、Pythonの標準エラーをエラーログに出力する
        logging("error", context.function_name, str(OtherException).replace("\n",""))
        logging("info", context.function_name, "実行終了")
        return {"errorMessage": str(OtherException)}

3. SESでドメインを設定し、受信ルールを設定

公式ドキュメントの解説に従って、簡単に設定できます。
AWSマネジメントコンソールのSESの画面で設定します。

まず、Route 53 を使用してドメインを購入および登録します。
2019/10時点では「.de」が年間$9.00で購入でき、最安値のようです。
https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/domain-register.html

次に、SESでドメインを検証します。
こちらも、解説に従って実施するだけなので簡単です。
https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/receiving-email-getting-started-verify.html

最後に受信ルールの設定ですが、以下の解説に従います。
https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/receiving-email-receipt-rules.html
https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/receiving-email-action-lambda.html
Recipientには、安否確認メールを受信するメールアドレスを書きます。(受信用のメールアドレスはここで決めます)
次にActionはLambdaを選び、1で開発したLambda Functionを選択し、Invocation typeはEventにします。
image.png
※Lambda Functionの名前は各自によります。画像では、Cloud9で開発したため「cloud9-」という名前になっています。

ここまでの設定が完了すれば、AWSサーバレスアーキテクチャの実装は完成です。

4. 安否確認システムのメール送信先に、SES受信用メールアドレスを追加

これを忘れると意味がありませんので、ちゃんと設定しておきます。

Slackに通知される様子

安否確認システムからメールが送信されると、以下のようにSlackに通知されるようになりました。
image.png
もちろん、スマホにも通知が飛んできます。

自社の安否確認システムなので、システム的には自社プロパー宛の点呼になるのですが、Slackに連携させるにあたり、協力会社の方にも呼びかけをするようにしています。

補足

1. Slackのメール受信機能ではダメなのか?

以下のSlack公式サイト上では、Slackでメールを受信する方法が書かれていますが、blocksのmrkdwn等で自由に通知フォーマットをカスタマイズしたいとなると、今回のような方法を取る必要があります。

「Slack でメールを受信する」
https://slack.com/intl/ja-jp/help/articles/206819278-slack-%E3%81%A7%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92%E5%8F%97%E4%BF%A1%E3%81%99%E3%82%8B

フォーマットをカスタマイズするには、前述のコードのJSONの部分を変えます。
カスタマイズできる方が、何かと便利ですね。

2. 安否確認はそもそも個人宛ではないのか?

その通りです。
つまり、チャンネル毎に誰か1人が、この仕組みで作成したSESの受信用メールアドレスを安否確認の送信先に設定すれば、Slackに通知されるようになります。
同じチャンネルで全員が設定すると複数件通知されますので、チャンネル毎に誰か1人でOKです。

最後に

余談ですが、この仕組みを作りながら、会社の安否確認にどんな意義があるのだろうと、立ち返り、調べてみました。

従業員の安全を守ることは、経営者の務めだといえる
(略)公益財団法人 ひょうご震災記念21世紀研究機構 人と防災未来センターが行った調査で『BCPを決めているかどうかは別にして、災害発生を想定した対策をとっているか』という質問への回答です。実に95.5%もの企業が『社員の安否確認の手法・手段』を定めています。その背景には、事業を続ける施設が残っても、そこで働く人がいなくなっては、事業を続けることができないという現実があります。そして、経営者は従業員を守る責務があり、事業を継続することで働く場を提供する義務もあるのです。
https://www.kddi.com/business/column/20180620/security-verification/

様々に意見はあるのでしょうが、安否確認の回答率を上げることで、組織のメンバーや大事な人の命を守ることができるならば、その回答率を上げることに意義があると、私は考えました。

自社内のSlackは、単なるコミュニケーションツールだけでなく、RSS等による情報収集ツールとしても機能し、アクティブユーザが多い傾向にあります。
その性質を逆手に取り、安否確認をSlackに飛ばすことで、回答率を増やそうと試みました。
最近だと、社内のコミュニケーションでメールよりもSlackが使われる事例が増えてきていると思いますので、このような仕組みにより、万が一の災害時の安否確認がより確かに行われればと思いました。

なお、安否確認システムにAmazon Connectの電話番号を登録するというアーキテクチャも考えられましたが、冷静に考えた結果、見送っています。

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5