メール以外の方法でもアラートの通知を行う(Twilio)
アラート通知の続きです。
サービスダウン検知
MSPに運用を手伝ってもらっていればサービスダウンが発生した場合でも電話で障害連絡してくれたり一次対応をやってくれたりするのですが、今回はスモールスタートでの運用ということもありMSPを使うことを勘定にいれなかったので、なんとか自力で対応できるようにします。
監視検知としてはMuninを使ってWEBサイトのヘルスチェックを行うのですが、サイトダウン検出時にアラートメール送ってもすぐ誰かに見てもらえると限らず、社内で警告音鳴らしたとしても社内に誰もいなければ気づいてもらえません。
いざという時はやはり電話で担当者に連絡したいと思っていたのですが、Twilio参考になる記事という電話周りのAPIが提供されているようなので、試しに使ってみてます。 なお、携帯に電話をかけると1分20円ほど課金が発生します。(電話に出なければ発生しないのかな?)
どんなものかというと、Twilioが監視サーバと電話の中間に入ってやりとりを代行してくれます。Twilioと監視サーバの間でhttpでのやりとりが発生するので、監視サーバ上にWEBサーバ等が必要です。今回は監視サーバ上にSinatraで簡単なサイトを作って、架電開始のリクエストや、Twilioからのコールバックの対応を行います。
サンプルを見ながら作ってみたのですが、ざっくり流れは以下の感じです。
アラート発生時にTwilioのAPIを使って担当者に電話する。
- アラート検知のスクリプトなどから、架電スタートのリクエスト/startを叩く。
wget http://watch.example.com:4567/start?secret=xxx&message=アラートです
- /startからstart_callが呼び出され、Twilioが担当の携帯に架電する。
担当者が電話にでない、出ても自動留守電メッセージだったという場合は、次の担当者に架電する。
- Twilioの架電結果が/callbackにコールバックリクエストとして来るのでパラメータの中のCallStatus:no-answerで電話に出てないと判断して 再度start_callを呼んで次の電話番号に架電する。次の番号がない場合は架電を停止する。
担当者が電話に出たら、文章を読み上げ始める。
- 担当が電話を取ると、/answerで設定した文面をTwilioがゆっくりさんみたいな音声で読み上げる。
担当者が内容を聞いたら確認のボタンを押す。
- 読み上げの他にキー入力の制御も設定しているので、担当のキー入力待ち状態となる。
確認のボタンを押したのが検知できたらそこで架電をやめる。
- 担当が1桁のキーを押すか、10秒何もしていない時点でTwilioは/completeにアクセスくる。
- キーが押されていれば架電停止のフラグをたて、「確認しました」の応答を返す。
- 押されていなければ「確認できませんでした」を返す。
- この後もTwilioから/callbackにコールバックが返ってくるので、架電停止フラグをみて停止でなければ次の電話番号に電話をかける。
sinatraのコード
rubyはこれから勉強します。。
require 'sinatra/base' require 'twilio-ruby' require 'uri' require 'redis-objects' require 'json' FROM = '+81xxx' #電話する際の電話番号 ACCOUNT_SID = 'xxx' #twilioにサインアップしたときにもらうID AUTH_TOKEN = 'xxx' #twilioにサインアップしたときにもらうToken NUMBERS = ["+81xxx","+81xxx","+81xxx"] #電話番号リスト SECRET = "xxx" #だれでも実行できないようにするための簡単な合言葉 HOST = "http://watch.example.com:4567" #このアプリが動くURL CALLBACK_URL = "#{HOST}/callback?secret=#{SECRET}" #架電後に結果をtwilioが送ってくる際の受取先 ANSWER_URL = "#{HOST}/answer" #電話がつながった後にtwilioにやってもらいたいことをxmlで教える COMPLETE_URL = "#{HOST}/complete" #電話中の担当者が番号入力を行ったあとにtwilioにやってもらいたいことをxmlで教える #アラートメッセージの文面、今何番目に電話してるとか各種状態をredisに保存する。 class State include Redis::Objects value :processing list :numbers counter :number_index value :message def id 1 end def number_shuffle numbers.clear NUMBERS.shuffle.each{|n| numbers << n } end def processing? processing == "0" ? false : true end def new_number if number_index.to_i < NUMBERS.size number = numbers.at(number_index) number_index.increment number else nil end end def stop processing.value = "0" end def start(text) processing.value = "1" number_shuffle message.value = text number_index.reset end def dump "processing: #{processing.value},numbers: #{numbers.each{|n| n} },number_index: #{number_index},message: #{message.value}" end end State.redis = Redis.new(:host => '127.0.0.1') def log text `echo \`date\` >> /var/tmp/twilio` `echo #{State.new.dump} >> /var/tmp/twilio` `echo #{text} >> /var/tmp/twilio` end #電話をかける def start_call callback = "#{HOST}/callback?secret=#{SECRET}" url = "#{HOST}/answer" @state = State.new if number = @state.new_number @client = Twilio::REST::Client.new ACCOUNT_SID, AUTH_TOKEN @client.account.calls.create({ :to => number, :from => FROM, :url => URI.encode(url), :method => 'GET', :status_callback => URI.encode(callback), :status_callback_method => 'GET', :timeout => '30', :record => 'false', 'IfMachine' => 'hangup' #電話に出た相手が留守電メッセージのようなマシンだった場合電話を切る(US向けの機能らしいです) }) else @state.stop end end class Server < Sinatra::Base get '/'do "" end get '/stop' do State.new.stop log "stop" "stop" end # get '/start' do if params[:secret] == SECRET State.new.start(params[:message]) log "start #{params.to_json}" start_call end "start" end get '/callback' do if params[:secret] log "callback #{params.to_json}" if State.new.processing? start_call else content_type "text/xml" "<Response><Say language='ja-jp'>completed</Say></Response>" end end end get '/answer' do log "answer #{params.to_json}" content_type "text/xml" "<Response><Gather timeout='10' numDigits='1' action=\"#{COMPLETE_URL}\" method='GET'><Say language='ja-jp'>#{State.new.message.value}。確認のため、何か数字を押して下さい。</Say></Gather><Say>確認できませんでした。次に電話します。</Say></Response>" end get '/complete' do log "complete #{params.to_json}" if params[:Digits] State.new.stop content_type "text/xml" "<Response><Say language='ja-jp'>確認しました。アラートを停止します</Say></Response>" else content_type "text/xml" "<Response><Say language='ja-jp'>確認できませんでした。次に電話します。</Say></Response>" end end end