TwilioAPIを使用してZabbix + AWS CloudWatchのシステム監視で異常検知した時に電話で通知する

はじめに
---

Webサイトやサーバの監視において、Webサイト停止やサーバ停止等の異常を検知した場合、メールで異常を通知する事が多いと思います。

「Twilio」というクラウドAPIサービスを使用すると、RubyやPHPといった様々なプログラム言語から、指定した電話番号に対して、簡単に電話をかけたりSMS通知を行なえます。
http://twilio.kddi-web.com/

この間、ZabbixやAWS CloudWatchによるシステム監視で、Webサイト停止やサーバ停止といった異常を検知した時、TwilioAPI(Ruby)を使用して、電話で障害を通知する仕組みを作ってみましたので、まとめていこうと思います。

システム構成
---

以下のような構成で、ZabbixやAWS CloudWatchからTwilioAPIをコールして、電話で障害を通知する仕組みを作ってみました。

ZabbixサーバやTwilioAPIサーバ(詳細は後述)については、Amazon EC2のAmazon Linux上に構築しました。

![構成図.png](https://qiita-image-store.s3.amazonaws.com/0/63647/6cacc92e-c5fc-cee0-f0d1-442d0fcd728f.png)

Twilioの利用を開始するまでの流れ
---

まず、Twilio公式サイトにサインアップして、Twilioアカウントを作成します。
http://twilio.kddi-web.com/

![スクリーンショット 2016-04-19 21.46.40.png](https://qiita-image-store.s3.amazonaws.com/0/63647/0be8989d-3b68-d989-411f-a8adc1f68f1e.png)

<img width="1022" alt="スクリーンショット 2016-04-19 21.47.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/467f7d0f-7f89-8fc9-a67c-51c3119e14a8.png">

Twilioアカウントを作成したら「プラグラマブル Voice」->「はじめよう」をクリックすると、「Twilio音声通話 電話番号」が発行されます。

<img width="685" alt="スクリーンショット 2016-04-19 21.49.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/3b677bc0-a6a9-93d7-0de7-cc4a2b79ed36.png">

TwilioAPIを使って電話をかけると、発行された「Twilio音声通話 電話番号」の電話番号から電話がかかってきます。
https://jp.twilio.com/user/account/voice/phone-numbers

次に、APIクレデンシャル画面で、TwilioAPIを利用する為の「ACCOUNT SID」「AUTH TOKEN」と確認します。
https://jp.twilio.com/user/account/voice/getting-started

TwilioAPIを使用したTwilioクラウドサービスへの電話発信について
---

ZabbixやAWS CloudWatchから簡単に電話をかけられるよう、Ruby + SinatraでTwilioAPIサーバを作成してみました。

ZabbixやAWS CloudWatchからTwilioAPIサーバをコールする事で、Webサイトやサーバ監視で異常を検知した時、電話をかけれるようにしました。

TwilioAPIを使用して電話をかけるサンプルコード、またTwilioAPIを使う時に気をつけた方が良さそうな点について記載致します。

・TwilioAPIで電話をかけるには、TwilioクラウドサービスからTwilioAPIへのHTTPコールバック通信を通す必要があります。AWSのNetworkACLsやセキュリティグループで、TwilioクラウドサービスからTwilioAPIサーバへのHTTP通信を許可しておく必要があります。

https://twilioforkwc.zendesk.com/entries/82773249-TwilioがアクセスしてくるIPアドレスレンジは何ですか-

・サンプルコードでは、GETで電話をかける電話番号をパラメータ指定出来るようにしていますが、電話番号は秘匿した方が良い情報なので、実際の運用時にはGETでパラメータ渡さない方が良いと思います。また、発信先の電話番号は特定番号だけに絞る、API認証を設ける等の対応も適宜行なうのが良いかと思います。

・Twilioで電話をかける際には、電話番号をE.164形式に変換する必要があります。
　　Twilioから電話をかける電話番号　090-XXXX-1234　→　+8190XXXX1234
　　Twilio音声通話 電話番号　050-XXXX-5678　→　+8150XXXX5678

・サンプルコード内では「phony」を利用させて頂く事で、GETパラメータで「090-XXXX-1234」のような発信先の電話番号を指定する際、内部的に「+8190XXXX1234」のようなE.164形式の電話番号に変換し、Twilioクラウドサービスへ電話発信のリクエストを投げて、電話をかけるようにしています。
https://github.com/floere/phony/blob/ebcfcb42f3c50a8d1c9e23d9ae05b338c8cc6a95/qed/country-specific.md

・電話番号のE.164形式については、以下をご参照下さい。
https://www.nic.ad.jp/ja/basics/terms/E164.html

TwilioAPIで電話をかけるサンプルコード
---

以下がTwilioAPIのサンプルコードになります。

雑多なコードで済みませんが、以下のようにTwilioクラウドサービスが提供するAPI(client.account.calls.create)を利用する事で、指定した電話番号に電話をかける事が出来ます。

```rb:twilio_api_server.rb
[root@twilio-example-server ~]# cat /root/twilio/twilio_api_server.rb 
require 'rubygems'
require 'twilio-ruby'
require 'sinatra'
require 'sinatra/reloader'
require 'json'
require 'phony'

get '/call/test' do
  # put your own credentials here 
  @account_sid = 'Twilioアカウント APIクレデンシャルのACCOUNT SID'
  @auth_token = 'Twilioアカウント APIクレデンシャルのAUTH TOKEN'

  @to_tel_number = '+8190XXXX1234'
  @from_tel_number = '+8150XXXX5678'

  # set up a client to talk to the Twilio REST API 
  @client = Twilio::REST::Client.new @account_sid, @auth_token 

  call = @client.account.calls.create(
      :url => "http://demo.twilio.com/docs/voice.xml",
      :to => @to_tel_number,
      :from => @from_tel_number
  )
  puts call.to

  "Twilio API Test Target Tel Number = [#@to_tel_number]]"
end

get '/call' do

  if params['api_key'] == nil
    halt 400, 'API Key Parameter Not Found!'
  end

  if params['api_key'] != 'twilio-api-sample-key'
    halt 400, 'API Key Wrong!'
  end

  if params['to_tel_number'] == nil
    halt 400, 'To E.164 Telephones Number Parameter Not Found!'
  end

  if params['to_tel_number'].count("-") != 2 then
    halt 400, 'To Telephones Number Format Error [XXX-XXXX-XXXX]!'
  end

  if params['tel_messages_type'] == nil
    halt 400, 'Telephones Messages Type Parameter Not Found!'
  end

  # put your own credentials here
  @account_sid = 'Twilioアカウント APIクレデンシャルのACCOUNT SID'
  @auth_token = 'Twilioアカウント APIクレデンシャルのAUTH TOKEN'

  @to_tel_number = params['to_tel_number']
  @tel_messages_type = params['tel_messages_type']
  @from_tel_number = '+8150XXXX5678'

  ## https://github.com/floere/phony/blob/ebcfcb42f3c50a8d1c9e23d9ae05b338c8cc6a95/qed/country-specific.md
  @japan = Phony["81"]
  @to_tel_number = @japan.normalize( @to_tel_number )
  @to_tel_number = '+81' + @to_tel_number
  ##

  if @tel_messages_type == "zabbix_site_alert" then
    @twiml_url = 'http://TwiMLサーバのIPアドレスやFQDN/twilio_test_zabbix_site_alert.xml'
  elsif @tel_messages_type == "cloudwatch_disk_alert" then
    @twiml_url = 'http://TwiMLサーバのIPアドレスやFQDN/twilio_test_cloudwatch_disk_alert.xml'
  elsif @tel_messages_type == "cloudwatch_instance_stop_alert" then
    @twiml_url = 'http://TwiMLサーバのIPアドレスやFQDN/twilio_test_cloudwatch_instance_alert.xml'
  else
    @twiml_url = 'http://TwiMLサーバのIPアドレスやFQDN/twilio_test.xml'
  end

  # set up a client to talk to the Twilio REST API
  @client = Twilio::REST::Client.new @account_sid, @auth_token

  call = @client.account.calls.create(
        :url => @twiml_url,
        :to => @to_tel_number,
        :from => @from_tel_number
  )
  puts call.to

  "Twilio API Test Target Tel Number = [#@to_tel_number]]"
end

get '/' do
  erb :index
end

get '/about' do
  erb :about
end

post '/confirm' do

  if params[:to_tel_number] == '' || params[:to_tel_number] == nil then
    halt 400, 'To E.164 Telephones Number Parameter Not Found!'
  end

  if params[:to_tel_number].count("-") != 2 then
    halt 400, 'To Telephones Number Format Error [-] Not Found! Example Number Format [XXX-XXXX-XXXX]!'
  end

  @to_tel_number = params[:to_tel_number]

  @japan = Phony["81"]
  @to_tel_number = @japan.normalize( @to_tel_number )
  @to_tel_number = '+81' + @to_tel_number
  ##

  # put your own credentials here
  @account_sid = 'Twilioアカウント APIクレデンシャルのACCOUNT SID'
  @auth_token = 'Twilioアカウント APIクレデンシャルのAUTH TOKEN'

  @from_tel_number = '+8150XXXX5678'

  @twiml_url = 'http://TwiMLサーバのIPアドレスやFQDN/twilio_test.xml'

  # set up a client to talk to the Twilio REST API
  @client = Twilio::REST::Client.new @account_sid, @auth_token

  call = @client.account.calls.create(
      :url => @twiml_url,
      :to => @to_tel_number,
      :from => @from_tel_number
  )
  puts call.to

  erb :confirm

end
[root@twilio-example-server ~]#
```

TwilioAPIから電話をかけれるようにするには、TwilioAPIサーバ上でサンプルコードを実行します。

```
[root@twilio-example-server ~]# ruby /root/twilio/twilio_api_server.rb -o 0.0.0.0 -p 80 &
[1] 3337
[root@twilio-example-server ~]#

[root@twilio-example-server ~]# ps awux | grep -v grep | grep ruby
root      3337  0.8  6.1 220624 30932 pts/0    Sl   19:42   0:00 ruby /root/twilio/twilio_api_server.rb -o 0.0.0.0 -p 80
[root@twilio-example-server ~]#  
```

・Twilioから電話かけた時に流す音声メッセージは「TwiML」というファイルで定義します。

TwiMLファイルはTwilioAPIサーバ自身に置いても、別のHTTPサーバに置いても良いようです。今回は別のサーバにTwiMLファイル置けるか試したかったので、TwilioAPIサーバ以外にTwiMLファイルを作成してみました。

```xml:TwiMLファイルのサンプル
[root@TwiML-example-server ~]# cat /var/www/html/twilio_test_zabbix_site_alert.xml
<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say language="ja-jp">TwilioとZabbix</Say>
    <Say language="ja-jp">の監視テストです。</Say>
    <Say language="ja-jp">監視対象サイト</Say>
    <Say language="ja-jp">が停止しました。</Say>
</Response>
[root@TwiML-example-server ~]# 
```

TwilioAPIで電話をかける例
---

Webブラウザを起動し、以下のようにTwilioAPIサーバへアクセスすると、090-XXXX-1234に電話をかける事が出来ます。

http://TwilioAPIサーバのIPアドレスやFQDN/call?api_key=twilio-api-sample-key&to_tel_number="090-XXXX-1234"&tel_messages_type=zabbix

ZabbixやAWS CloudWatchのシステム監視で異常を検知した時、上記のようにTwilioAPIをコールすると、電話をかける事が出来ます。
電話を取ると、TwiMLファイルに記述したメッセージが流れます。

ZabbixからTilioAPIサーバ経由で電話をかける
---

Zabbixのアクションで以下のようなアクションを設定し、監視トリガーでアクションを設定すると、監視で異常を検知した時に電話をかける事が出来ます。

```bash:Zabbixで設定するアクション
/usr/bin/curl -s "http://TwilioAPIサーバのIPアドレスやFQDN/call?api_key=twilio-api-sample-key&to_tel_number=%22090-XXXX-1234%22&tel_messages_type=zabbix_site_alert"
```

<img width="1016" alt="zabbix1.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/4a8d83a8-ab2f-78d2-6ac2-08bfc6468087.png">

<img width="912" alt="zabbix2.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/d1866f23-f902-c0e3-855c-eefbdfffd2c9.png">

<img width="874" alt="zabbix3.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/8562edc4-35e4-a2a6-a299-9febbf68f028.png">



AWS CloudWatchからTwilioAPIサーバ経由で電話をかける
---

AWS CloudWatchからは直接APIをコール出来ないので、AWS Lambdaに以下のようなファンクションを作成します。

AWS CloudWatchの監視で異常を検知した時、AWS CloudWatch -> AWS SNS -> AWS Lambdaファンクションを実行するように設定します。これによりAWS CloudWatchによる監視で、EC2インスタンスの停止やディスク使用率超過等を検知した時、電話をかける事が出来ます。

```js:TwilioAPIで電話をかけるLambdaファンクションのサンプルコード
console.log('Loading function');

exports.handler = function(event, context) {
    var exec = require('child_process').exec;

    var cmd = "curl -s 'http://TwilioAPIサーバのIPアドレスやFQDN/call?api_key=twilio-api-sample-key&to_tel_number=%22090-XXXX-1234%22&tel_messages_type=cloudwatch_disk_alert'"
    var child = exec(cmd, function(error, stdout, stderr) {
        if (!error) {
            console.log('standard out: ' + stdout);
            console.log('standard error: ' + stderr);
            context.done();
        } else {
            console.log("error code: " + error.code + ", err: " + error);
            context.done(error,'lambda');
        }
    });
};
```

<img width="708" alt="スクリーンショット 2016-04-19 21.02.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/5a9841c8-66dd-2e9b-2864-229a2aca6607.png">

<img width="655" alt="スクリーンショット 2016-04-19 21.03.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/6fb6a017-8bdf-3899-0c06-67a218dd3b01.png">


<img width="1186" alt="スクリーンショット 2016-04-19 21.05.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/d50518af-b4be-523b-5002-45d22f902b86.png">

<img width="1157" alt="スクリーンショット 2016-04-19 21.08.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/63647/515f5d8f-c88f-4535-073b-d8b00517e8c8.png">



***

ざっとまとめてみましたが、障害の緊急度や影響度を踏まえて、軽微な障害では、電話による通知でなく、SMSによる通知でも良いかもしれませんね。

メール・電話といった障害通知手段を上手く組み合わせて、システム障害を検知しやすくしたいですね。

細かい設定や情報はあらためて記載していきたいと思います。