Zabbix × Twilio でアラートの電話通知を自動化

ディップ Advent Calendar の4日目です。

既にQiitaにTwilio関連の記事が多くなり、あまり目新しくない話になってしまいますが、
ZabbixとTwilioを連携してアラートの電話通知を実装したので、Zabbixとの連携部分をメインにお話しさせていただきます。


# はじめに
## 環境
 - Zabbixと電話通知アプリは同一サーバー上に置いています。
 - Twilioとの通信はAWSのIP帯域フルでアクセス許可をあける必要があるので、外部と通信の取れる環境にサーバーを置いています。
 - サーバー情報
  - OS: CentOS 6.5
  - Zabbix: Zabbix 2.4
  - 電話通知アプリ（PHP）: PHP 5.3.3


## 電話通知フロー
アラート検知～電話通知までの流れはざっくり以下のような感じです。

![image](https://qiita-image-store.s3.amazonaws.com/0/139464/95831e2f-9c3b-be42-2f70-6babec596edc.png)

①　Zabbixにてサイトダウン（Timeout）を検知した場合に、アクションにて電話通知アプリをキック
②　キックされた電話通知アプリはTwilioに対して発信対象電話番号に対して発信をキック
③　Twilioにて、電話発信を行う。電話に応答した場合、障害対応可否のキー操作を促す
④　応答者のキー操作の値をTwilioが受取り、結果を電話通知アプリに返す
⑤　③の結果が「対応可能」であった場合、対応者の名前をシステム運用担当にメール通知


# 導入
## Zabbix設定
### 1. 電話通知アプリのメディアを作成する
メディアに作成したZabbix - 電話通知アプリ連携用のスクリプトを設定します。

Ver.2.4
![WS000018.JPG](https://qiita-image-store.s3.amazonaws.com/0/139464/7dea8088-5a47-8b9b-78cf-cb0073170bc5.jpeg)

 - 名前：電話通知アプリ　※任意の名前をつけてください
 - タイプ：スクリプト


※余談ですが、Zabbix3.0以降ではスクリプトに引数を渡す場合、[スクリプトパラメータ]の指定が必要になります。
別件ではまったのでメモ。

Ver.3.0
![メディア_Ver3.0.JPG](https://qiita-image-store.s3.amazonaws.com/0/139464/b53b259d-e54c-cac1-0cd5-ed1501795435.jpeg)

 - スクリプトパラメーター　※3.0以降
  - {ALERT.SENDTO}
  - {ALERT.SUBJECT}
  - {ALERT.MESSAGE}


### 2. 電話通知用ユーザー作成
電話通知用のユーザーを新規作成します。
ユーザー設定の[メディア]にて、1.で作成したメディアを追加します。
※すでにあるユーザーにメディアを追加するでもOK

![ユーザー.JPG](https://qiita-image-store.s3.amazonaws.com/0/139464/5b64deb7-bad4-18bd-ddbc-30a8a41585f4.jpeg)

 - タイプ：電話通知アプリ
 - 送信先：電話通知ユーザー　※スクリプト実行時、第一引数に入ります


### 3. 電話通知ユーザーのアクションを作成
電話通知をキックするためのアクションを作成します。
[アクション]にて、アラートメールのフォーマットを指定します。
連携スクリプトにてTRIGGER.STATUS、EVENT.ID、TRIGGER.NAMEを抽出したいので、件名にそれらの値をいれています。

![アクション_メール.JPG](https://qiita-image-store.s3.amazonaws.com/0/139464/6e1510a8-85d5-90b4-527c-1860ca519aad.jpeg)

 - デフォルトの件名：【{TRIGGER.STATUS}】 :{EVENT.ID}:[{TRIGGER.SEVERITY}]:{HOSTNAME}: {TRIGGER.NAME}

※使用できる変数はZabbix公式ドキュメント参照。3.0のが見やすい。。。
https://www.zabbix.com/documentation/2.4/manual/appendix/macros/supported_by_location


[アクションの実行内容]にて、2.で作成（またはメディア追加）した電話通知アプリ用ユーザーを指定します。

![アクション_メディア.JPG](https://qiita-image-store.s3.amazonaws.com/0/139464/d162327c-89eb-331f-7228-995f3c50a4a0.jpeg)

 - ユーザーに送信：電話通知アプリ用ユーザー
 - 次のメディアのみ使用：すべて または 電話通知アプリ


## Zabbix - 電話通知アプリ連携用スクリプト
こんなかんじに作りました。
スクリプトはzabbix_server.confの[AlertScriptsPath]で指定したディレクトリに置いてください。

```
#!bin/bash

## Values received by this script:
# To = $1 (宛先)
# Subject = $2 (件名)
# Message = $3 (メールメッセージ部)

PHP=/usr/bin/php

USER="$1"
SUBJECT="$2"

# セッションIDセット
SES_ID=`echo ${SUBJECT} | awk -F":" '{print $2}'`
# トリガー名チェック（Timeoutのみ実行）
TRIGER_CHECK=`echo ${SUBJECT} | grep 'Timeout' | wc -l`
# トリガーステータスチェック
STATUS_CHECK=`echo ${SUBJECT} | grep 'OK' | wc -l`

# アラート件名ファイル
SUBJECT_FILE=/[電話通知アプリのHOME]/ALERT_SUB_dev_${SES_ID}.txt

# メイン処理
# トリガーがTimeoutの場合実行
if [ ${TRIGER_CHECK} -gt 0 ];
then
# トリガーステータスが正常でない場合、実行
    if [ ${STATUS_CHECK} -le 0 ];
    then
        echo ${SUBJECT} > ${SUBJECT_FILE}
        chmod 777 ${SUBJECT_FILE}
        ${PHP} [電話通知アプリ] ${USER} ${SES_ID}
    else
        exit 2
    fi
else
    exit 5
fi
```

 - 第一引数：ユーザーの送信先に指定した値
 - 第二引数：アラートメールの件名　※アクションで指定したフォーマット
 - 第三引数：アラートメールのメッセージ　※アクションで指定したフォーマット

復旧通知は電話通知させたくないので、件名に含まれるトリガーのステータスが「OK」でないかを判断します。
更に今回はサイト停止の場合のみ電話通知を実施したいので、件名に含まれるトリガー名に「Timeout」があるかを判断しています。

おまけ機能ですが、
電話通知アプリにて担当者をメール通知する際（電話通知フローの⑤）に、メール本文にアラート件名を載せたいので、
Zabbixから受け取った件名をSUBJECT_FILEにはいて、メール送信時に電話通知アプリにて読み込ませています。


## 電話通知アプリ
アプリの開発言語は以下をサポート

 - C#
 - Java
 - Node.js
 - PHP
 - Python
 - Ruby
 - セールスフォース

今回はPHPで作りました。
Twilioから提供されているPHPのライブラリをサーバーに格納。
Twilio公式HPにはマニュアルしっかりあるので、そちら通りに導入すればできます。
https://jp.twilio.com/docs/guides/voice/how-to-make-outbound-phone-calls-in-php

### 電話通知アプリの機能
 - 障害検知をキックに担当者（zabbixのアクションにて設定されたユーザー）に電話通知を行う
 - 初回は第一担当から通知を実施。その後対応者が決まるまで順々に担当者に電話を行う
 - 電話番号リストを3回ループした場合、電話通知を終了する
 - 障害対応者が決まった場合はシステム運用担当に障害対応者名をメール通知する

電話通知アプリのソース全部は載せませんが、処理フローとしては以下の通り。

 - 1. zabbixよりクラウド電話APIを呼ぶRESTAPIをキック
 - 2. twilioの通話状態が格納される戻り値（CallStatus）を確認し、通話終了状態（completed）である場合は処理を終了させる
 - 3. 電話通知ループカウンターファイルを確認し、ファイルがない場合はファイルを作成して初期値（1）をいれる。
 - 4. ループカウンターの値を確認し、カウントが3以上である場合はループ終了をメール通知して処理を終了させる
 - 5. 担当者カウンターファイルの有無を確認し、ファイルがない場合はファイルを作成して初期値（1）をいれる。ファイルがある場合はカウントに+1する
 - 6. 担当者リストを確認し、カウントが担当者リストの数より多い場合は担当者カウンターファイルを削除し、ループカウンターファイルの値を+1してフロー2にうつる
 - 7. 担当者リストより担当者カウンターのカウント数の電話番号に発信。応答がない場合はフロー2にうつる
 - 8. 応答をキックに障害対応する場合はダイヤルキー[1]を押すようアナウンスする。1以外が押される、または対応がない場合はフロー2にうつる
 - 9. 対応者が決まった場合は対応者の名前をシステム運用担当にメール通知する

### TwiML
Twilioにしゃべらせる部分（上記フロー8）はTwiMLという、Twilio独自のタグが使えるXML形式で記載のもので記述します。
TwiMLはこんな感じです。

```
<?php
// 環境セット
    // $user、$sesidの値取得
    if(isset($_GET['user'])) {
        $user = $_GET['user'];     // 変数1 電話番号リストユーザー
        $sesid = $_GET['sesid'];   // 変数2 セッションID
    }

    // 共通環境変数・定数セット
    header("content-type: text/xml");
    require("[環境変数ファイル]"); //共通の環境変数ファイル
?>
<Response>
    <?PHP if (empty($_POST["Digits"])): ?>
        <Gather numDigits="1" timeout="<?PHP echo $key_timeout ?>">
            <Say language="ja-jp" voice="alice">障害が発生しました。対応する場合は1を押してください。</Say>
        </Gather>
        <Redirect method="POST" ><?PHP echo $next_url.'user='."$user".'&amp;sesid='."$sesid" ?></Redirect>
    <?PHP elseif ($_POST["Digits"] == "1"): ?>
            <Say language="ja-jp" voice="alice">対応よろしくお願いします。</Say>
                <?php
                    // 対応者メール送信
                    exec("/usr/bin/php $home/[メール送信PHP] $user $sesid");
                ?>
            <Hangup/>
    <?PHP elseif ($_POST["Digits"] != "1"): ?>
            <Say language="ja-jp" voice="alice">次の担当者に連絡します。</Say>
                <?PHP
                // 次の担当者に発信
                exec("/usr/bin/php $home/[TwilioにコールかけるPHP] $user $sesid");
                ?>
            <Hangup/>
    <?PHP endif ?>
</Response>
```

 - Gatherタグ　・・・電話応答者のキー操作を受取ります。
  - numDigits：ダイヤルキーの入力を受け付ける桁数指定
  - timeout：キー操作のタイムアウト値。この時間を超えるとRedirectタグで指定したURLにリダイレクトされる
 - Sayタグ　・・・Twilioが話す内容をしていします。
  - language：言語指定。日本語ならja-jp
  - voice：音声の指定。日本語は今のところ「alice」しか対応していないらしい　※女性の声です
 - Redirectタグ　・・・Gatherタグで指定したtimeoutを越えた場合にリダイレクトするURLの指定
 - Hungupタグ　・・・通話を終了する

上記の他にもいろいろ処理を指定できます。（マニュアル参照）
https://jp.twilio.com/docs/api/twiml

しかし、TwiMLとTwilioにコールかける処理（RestAPI）の使い分けには注意が必要です。
TwiMLとRestAPIの違い（使い分け）は基本的に以下の通り。

 - TwiMLはすでに発生している呼に対して、Twilioがその次の処理を指定するためのもの
 - RestAPIはTwilioを操作することで、呼を新規に発生させたり、ログを取得するもの

呼（発信）と実現したい処理の関係性に注意して、TwiMLとRestAPIを使い分けてください。
※結構はまりました。


## 最後に（今後の課題）
Twilioは一回のコール（相手が出た場合）毎に課金となるので、アラートが発生しなければお金かかりません。
しかし、NW障害等で大量にアラートがあがり、電話通知が大量発生するとあっという間に課金がつもってゆきます。
Zabbix側での制御は難しいので、アプリ側でロックをかける処理が必要です。
※現在実装に向けて改修中


以上、長文を御拝読いただきありがとうござました。