2012年12月11日火曜日

CDPってなんじゃ?(通知に関するオレオレパターンでCloudwatchからサイレンをならす)


CDP Advent Calendar 2012へ参加しました。

CDPのパターンリストを見ていると純粋に勉強になったり、知らずに実践していたパターンなどがあったり、見ていて楽しいです。

何を書くか迷いましたが、オレオレパターンを定義してみました。

普段運用などでCloudWatchからのアラートがよく飛び交っていますが、うっかり気づかなかったりすると困ることがあります。
SNSを使用した通知はインフラ内で使用されることが多いような気がしますが、HTTP Notificationを使用してアプリケーションサーバーからクライアントまで含めたプッシュ通知の可能性を探ってみたいと思います。

名前をつけるとしたらDeep Notificationでしょうか。
構成は以下の様なイメージです。



今回は例として、CloudWatchでアラートが発生したら、管理画面にプッシュしてWebAudioでサイレンを鳴らしてみたいと思います。

 まず、SNSに"sound"というトピックを立てます。




次にEC2インスタンス内で、HTTP通知先になるWEBアプリをつくります。
今回はFuelPHPで、/monitor/alert のアクションを通知先とします。

SNSのメール通知では購読確認はメールのリンクを踏めばOKですが、HTTP通知では登録後通知先のURLにSNSがアクセスし、その際のJSONパラメータに含まれる情報を元に、SNSの認証APIをリクエストするという方式のようです。

/monitor/alert のアクションで、以下のようにJSONリクエストからTopicArnとTokenを抽出し、SNSに購読確認を行うようにしておきます。

fuel/app/class/controller/monitor.php
     public function action_alert()
     {

          $sns = new AmazonSNS(array('key'=>'xxxxxxxxxxxxxxxxxx',                                               
                                      'secret'=>'xxxxxxxxxxxxxxxxxxxx'));
          $sns->set_region(AmazonSNS::REGION_APAC_NE1);
          $input = file_get_contents('php://input');
          $input =json_decode($input, true);

          if ($input["Type"] === 'SubscriptionConfirmation') {
               $response = $sns->confirm_subscription(
                                $input["TopicArn"], 
                                $input["Token"]);
          }
     }


次に、SNSの"sound"トピックで、購読者の登録を行います。
プロトコルはHTTP、エンドポイントは/monitor/alert アクションのURLとします。



すると、すぐにSNSがエンドポイントにアクセスし、認証が完了すると、以下のようにSubscriptin IDにsnsのarn値がセットされます。
これで認証が完了しました。

すでに認証が完了しているので、このコードは不要なのでコメントアウトし、実際に通知があったときの処理を書きます。
SNSから送信されたJSONデータをredisにPublishするようにします。

fuel/app/class/controller/monitor.php
     public function action_alert()
     {

/*
          $sns = new AmazonSNS(array('key'=>'xxxxxxxxxxxxxxxxxx',
                                     'secret'=>'xxxxxxxxxxxxxxxxxxxx'));
          $sns->set_region(AmazonSNS::REGION_APAC_NE1);
          $input = file_get_contents('php://input');
          $input =json_decode($input, true);

          if ($input["Type"] === 'SubscriptionConfirmation') {
               $response = $sns->confirm_subscription(
                              $input["TopicArn"], 
                              $input["Token"]);
          }
*/

          $input = file_get_contents('php://input');

          $redis = Redis::instance('default');
          $redis->publish('alert', $input);

          $this->template->title = 'Monitor » Alert';
          $this->template->content = View::forge('monitor/alert');
     }


次に、node.jsで、redis経由でSubscribeしたJSONデータをユーザーにプッシュするようにします。

node/server.js
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3000);

var io = require('socket.io').listen(server);
var opts = {host:'127.0.0.1', port:6379};

var redis = require('redis');
var sub = redis.createClient(opts.port, opts.host);
sub.subscribe('alert');
sub.on("message", function(channel, message){
  io.sockets.emit('alert', JSON.parse(message));
});


また、アラート通知画面を別途用意し、alertイベントを受信したら情報を表示してWebAudioを鳴らすようにします。

/fuel/app/views/monitor/index.php
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
<script src="/assets/js/client.js" type="text/javascript"></script>

<div class="container">
<div class="hero-unit">
<span id="timestamp" style="color: red;"></span>
<h3 id="subject" style="color: red;"></h3>
<dl class="dl-horizontal" id="message"></dl>
<button class="btn" id="stop" type="button">Stop</button>
  </div>
</div>


/public/assets/js/client.js
$(function(){

  var context = new webkitAudioContext();
  var band = 24000;
  var buf = context.createBuffer(1, band, band);
  var data = buf.getChannelData(0);
  for (var i = 0;i < data.length;i++) {
        data[i] = i > band*0.5 ? 0 : ((i % 100) < 50 ? 1 : 0);
  }
  var src = context.createBufferSource();
  src.buffer = buf;

  $.getScript("http://"+location.hostname+":3000/socket.io/socket.io.js", function(){
    var socket = io.connect('http://'+location.hostname+':3000/');
    socket.on('connect', function() {
    });

    socket.on('alert', function(data){
      $("#timestamp").text(data.Timestamp);
      $("#subject").text(data.Subject);
      $("#message").empty();
      var msg = JSON.parse(data.Message);
     for(var p in msg){
        $("#message").append("<dt>"+p+"<dt><dd>"+msg[p]+"</dd>");
     }
      src.connect(context.destination);
      src.loop = true;
      src.noteOn(0);
    });

  });

  $("#stop").click(function(){
    src.disconnect();
  });
});



そして、試しにCloudWatchで監視対象のインスタンスの10%のCPUでアラートがでるようにしておきます。



それでは監視対象のインスタンスで無限ループを動かして、様子を見てみます。


10%を超えたラインでしばらくするとプッシュ通知され、サイレンが鳴りました!!

運用だけでなくバッチの完了通知やGrowlへの通知など、HTTP通知を使ってSNSをアプリケーション・サーバーのほうに一歩踏み込ませることで、通知に関する自由度が一気に高まり、運用効率などに効果があるのではないかと思います。

以上です。