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

# 災害時に気づかれない安否確認を何とかしたかった
先日の[台風19号](https://ja.wikipedia.org/wiki/%E4%BB%A4%E5%92%8C%E5%85%83%E5%B9%B4%E5%8F%B0%E9%A2%A8%E7%AC%AC19%E5%8F%B7)の上陸時、自社の安否確認システムが発動し、全社員に一斉連絡が行われました。
この連絡は、安否確認システムに登録された電話番号とメールアドレス宛に行われることとなっています。
これは、各社でよくある安否確認システムの仕組みだと思われます。
しかし、今回の安否確認連絡に対する回答率は、普段実施している訓練の時よりも悪くなってしまったようです。
個人的な考察ですが、訓練では事前にメールを送信されると知らされているから回答率が良いのであり、いざ本番となると気づかない人が多いのだと考えました。
電話はEメールよりも効果的だと思うのですが、いたずら電話と勘違いして、取られないのかもしれません。

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

# 安否確認をSlackに流すサーバレスアーキテクチャ
以下の図のように、安否確認システムが発送したメールを、Amazon SESのメール受信機能で受け、メール受信イベントでAWS Lambdaをトリガーして、LambdaからWebhookでSlackに通知します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/2374eeac-a408-fbde-4d89-270c1fb7c748.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/202e9de6-4803-a94e-924b-fd876f7ceda8.png)

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

```python
# -*- 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/78ba6ff7-c7b2-0314-26a6-0315c51bc632.png)
※Lambda Functionの名前は各自によります。画像では、Cloud9で開発したため「cloud9-」という名前になっています。

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

## 4. 安否確認システムのメール送信先に、SES受信用メールアドレスを追加
**これを忘れると意味がありませんので、ちゃんと設定しておきます。**

# Slackに通知される様子
安否確認システムからメールが送信されると、以下のようにSlackに通知されるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211986/1a1c69c4-e3bd-defd-0179-dccf477cda9f.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の電話番号を登録するというアーキテクチャも考えられましたが、冷静に考えた結果、見送っています。**