MyCSS

2011/07/20

AWS SDK for Ruby はてなブックマークに追加

「七夕記念企画/雲(AWS)に願いを!」という事で、お願いしたらあっという間に願いがかなったわけでして。


【AWS発表】 AWS SDK for Rubyを提供開始 - Amazon Web Services ブログ

大変喜ばしい!というわけで、ちょっとだけ試してみたメモです。

AWS::SNSを試してみる
一年以上前に、Amazon SNSのRubyライブラリをRightAwsを元に作ったんですが、もう不要になりました。万歳!

SDKの導入方法はこの辺を見ていただくとして、例えばTopicを全て総ざらいして、その中のSubscriptionを取得するにはこんな感じになります。
# -*- coding: utf-8 -*-
require 'rubygems'
require 'aws-sdk'

AWS.config(:access_key_id => 'アクセスキー', 
  :secret_access_key => 'シークレットキー')
  
sns = AWS::SNS.new
sns.topics.each do |topic|
  puts topic.name
  topic.subscriptions.each do |sub|
    puts sub.arn
  end
end
実にいい感じですね。

Messageをpublishするには、Topicオブジェクトにpublishしてあげるだけです。
topic.publish('デフォルトメッセージ',
  :subject => 'Test',
  :email => 'ほげほげ')

送信先がメールの場合、:subject に日本語をそのまま与えるとエラーになりますんで、例のごとくJISエンコードしてあげます。
subject = '=?ISO-2022-JP?B?' + Kconv.tojis('日本語サブジェクト')
  .split(//,1).pack('m').chomp + '?='
topic.publish('デフォルトメッセージ',
  :subject => subject,
  :email => 'ほげほげ')

~を含むとpublishに失敗するぞ!
まだちゃんと検証していないのですが、問題を発見しましたので報告しておきますw
先のpublishですが、メッセージの本文に~(0x007E)があると、AWS::SNS::Errors::SignatureDoesNotMatchになってしまいます。
※ちなみに \(~0~)/ を試して気がつきましたw
topic.publish('デフォルトメッセージ',
  :subject => 'Test',
  :email => '~') 

以下、エラーメッセージ
E, [2011-07-20T20:45:38.321580 #3504] ERROR -- : [AWS SNS 403 0.870669] publish {:message_structure=>"json", :topic_arn=>"arn:aws:sns:us-east-1:XXXXXXXXXXXX:XXXXXXXX", :message=>"{\"default\":\"デフォルトメッセージ\",\"email\":\"~\"}", :subject=>"Test"} AWS::SNS::Errors::SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.

これってAWS SDKの問題って言うよりはRubyの問題なのかな?それともJSONエンコード?ちなみに試した環境は以下です。
$ ruby --version
ruby 1.8.7 (2009-06-12 patchlevel 174) [universal-darwin10.0]

2011/07/15

第9回 AWS User Group - Japan 東京勉強会で、ひさびさにLTしてきました はてなブックマークに追加



↑自分は12:00位からです

こんにちは。フォロアー泥棒の@dateofrockです。

思い起こせば、第一回でSQSネタを、第二回でSNS自爆ネタを発表してからはや一年が経ちました。その間に、QCon Tokyo 2010とか、女子会とか、札幌支部の勉強会でしゃべったりと、結構いろんなところでお話しさせていただく機会をもらい、大変ありがたく思います。(沖縄行きてー)

AndroidでTwitter follower数をCustom Metricsに突っ込む
お約束の「続きはWebで」の件ですが、まずJMXを使ったCustom Metricsはこちらをご覧ください。
で、これをAndroidからやるためには、cron的な動作をさせる事が出来るAlarmManagerを使うのが便利です。

Main.java

package com.dateofrock.android.test;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class Main extends Activity {

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // 監視開始ボタン
  Button startWatchButton = (Button) findViewById(R.id.startWatchButton);
  startWatchButton.setOnClickListener(new View.OnClickListener() {
   public void onClick(View v) {
    long interval = 60 * 1000;// 1分間隔

    // Intent作成
    PendingIntent pendingIntent = createPendingIntent();

    // AlarmManagerに登録
    AlarmManager am = (AlarmManager) Main.this
      .getSystemService(ALARM_SERVICE);
    am.setRepeating(AlarmManager.ELAPSED_REALTIME, 1000, interval,
      pendingIntent);
   }
  });

  // 監視終了ボタン
  Button stopWatchButton = (Button) findViewById(R.id.stopWatchButton);
  stopWatchButton.setOnClickListener(new View.OnClickListener() {
   public void onClick(View v) {
    PendingIntent pendingIntent = createPendingIntent();
    AlarmManager am = (AlarmManager) Main.this
      .getSystemService(ALARM_SERVICE);
    am.cancel(pendingIntent);
    Toast.makeText(getBaseContext(), "Twitterの監視をやめますた", 10).show();
   }

  });
 }

 private PendingIntent createPendingIntent() {
  Intent intent = new Intent(getBaseContext(),
    TwitterWatchBroadcastReceiver.class);
  PendingIntent pendingIntent = PendingIntent.getBroadcast(
    getBaseContext(), 0, intent, 0);
  return pendingIntent;
 }
}

res/layout/Main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent" android:layout_height="fill_parent"
 android:orientation="horizontal">
 <Button android:id="@+id/startWatchButton"
  android:layout_height="wrap_content" android:layout_width="wrap_content"
  android:text="@string/start_watch_string"></Button>
 <Button android:layout_height="wrap_content"
  android:layout_width="wrap_content" android:id="@+id/stopWatchButton"
  android:text="@string/stop_watch_string"></Button>
</LinearLayout>

極限まで研ぎすまされた無駄のないユーザーインターフェース


Main.javaは、アプリケーションを起動した時に最初に表示されるActivityです。スタートボタンが押されるとAlarmManager#setRepeating()するのですが、ここで渡されるPendingIntentはBroadcastReceiverを継承したTwitterWatchBroadcastReceiverを起動させるものになります。この中で、Twitter APIに現在のフォロワー数を聞きに行って、それをCloudWatchにputMetricDataするだけです。
上記のコードでは1分間隔で監視していますが、これは結構電池を消耗するのでおススメしません。今回はどれだけ短時間でフォロワーが増えるかみたかったので、あえて1分にしています。

TwitterWatchBroadcastReceiver.java

package com.dateofrock.android.test;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.text.format.DateUtils;

import com.dateofrock.android.test.TwitterClient.APIResult;

public class TwitterWatchBroadcastReceiver extends BroadcastReceiver {

 @Override
 public void onReceive(Context context, Intent intent) {
  // Twitterにお伺い
  TwitterClient twitter = new TwitterClient();
  APIResult result = twitter.getResult("dateofrock");

  // 結果をCloudWatchに送信
  CloudWatchClient cloudWatch = new CloudWatchClient();
  cloudWatch.sendMetrics("dateofrock", result);

  // Notify
  doNotification(context);
 }

 private void doNotification(Context context) {
  NotificationManager notificationManager = (NotificationManager) context
    .getSystemService(Context.NOTIFICATION_SERVICE);

  String text = "CloudWatchにTwitter情報を登録しました("
    + DateUtils.formatDateTime(context, System.currentTimeMillis(),
      DateUtils.FORMAT_SHOW_TIME) + ")";
  Notification notification = new Notification(
    android.R.drawable.btn_star, text, System.currentTimeMillis());
  notification.flags = Notification.FLAG_AUTO_CANCEL;

  Intent intent = new Intent(context, Main.class);
  PendingIntent contentIntent = PendingIntent.getActivity(
    context.getApplicationContext(), 0, intent, 0);
  notification.setLatestEventInfo(context, "TwitterWatch", text,
    contentIntent);
  notificationManager.notify(R.string.app_name, notification);
 }

}


CloudWatchClient.java

package com.dateofrock.android.test;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.cloudwatch.AmazonCloudWatchClient;
import com.amazonaws.services.cloudwatch.model.Dimension;
import com.amazonaws.services.cloudwatch.model.MetricDatum;
import com.amazonaws.services.cloudwatch.model.PutMetricDataRequest;
import com.amazonaws.services.cloudwatch.model.StandardUnit;
import com.dateofrock.android.test.TwitterClient.APIResult;

public class CloudWatchClient {

 public void sendMetrics(String twitterUser, APIResult result) {
  if (result.isError()) {
   return;
  }

  Dimension dimension = new Dimension().withValue(twitterUser).withName(
    "User");

  // リクエストの組み立て
  PutMetricDataRequest request = new PutMetricDataRequest()
    .withNamespace("Twitter").withMetricData(

      new MetricDatum().withMetricName("Follower")
        .withDimensions(dimension)
        .withUnit(StandardUnit.Count.toString())
        .withValue((double) result.followersCount),

      new MetricDatum().withDimensions(dimension)
        .withMetricName("Friends")
        .withUnit(StandardUnit.Count.toString())
        .withValue((double) result.friendsCount),

      new MetricDatum().withDimensions(dimension)
        .withMetricName("Status")
        .withUnit(StandardUnit.Count.toString())
        .withValue((double) result.statusCount)

    );

  // CloudWatchにデータ送信
  AWSCredentials cred = new BasicAWSCredentials("アクセスキー", "シークレットキー");
  AmazonCloudWatchClient client = new AmazonCloudWatchClient(cred);
  client.setEndpoint(CloudWatchEndPoint.US_EAST);
  client.putMetricData(request);
 }

}

CloudWatchEndPoint.java

package com.dateofrock.android.test;

public class CloudWatchEndPoint {

 public static final String US_EAST = "monitoring.us-east-1.amazonaws.com";
 public static final String US_WEST = "monitoring.us-west-1.amazonaws.com";
 public static final String EU_WEST = "monitoring.eu-west-1.amazonaws.com";
 public static final String AP_SOUTHEAST = "monitoring.ap-southeast-1.amazonaws.com";
 public static final String AP_NORTHEAST = "monitoring.ap-northeast-1.amazonaws.com";

}

TwitterClient.java

package com.dateofrock.android.test;

import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.User;

public class TwitterClient {

 public APIResult getResult(String twitterUser) {
  APIResult result = new APIResult();
  Twitter twitter = new TwitterFactory().getInstance();
  User user;
  try {
   user = twitter.showUser(twitterUser);
   result.followersCount = user.getFollowersCount();
   result.friendsCount = user.getFriendsCount();
   result.statusCount = user.getStatusesCount();
  } catch (TwitterException e) {
   result.error = true;
  } finally {
   return result;
  }

 }

 public class APIResult {
  int followersCount, friendsCount, statusCount;
  private boolean error = false;

  public boolean isError() {
   return this.error;
  }
 }
}

TwitterWatchBroadcastReceiver#onReceive()で起動されるTwitterClientですが、Twitter APIのGET users/showが幸い認証が不要なので、非常に簡素に書く事ができます。ただし、Twitterの状況によっては取得に失敗する事もありますし、ましてやケータイは圏外になる事もよくありますので、例外はキャッチして取得に失敗したという状況が分かるようにして(APIResult.error=true)値を返すようにしています。というわけで、エラー処理は適当ですw

結果発表

で、どのくらいTwitterのフォロワーが増えたかを貼っておきますとこんな感じでした。わずか3分で純増70弱です。協力していただいた皆様、本当にありがとうございます!
あれから半日以上経過しましたが、若干の増減はあるものの、今のところキープですww

ちなみに、所々グラフが切れている部分がありますが、これはTwitterAPIやCloudWatchAPIの呼び出しに失敗したからだと思います。原因はほぼ間違いなくAndroid端末の通信状況です。移動中でトンネルの中に入っていたりすると圏外になってしまい、失敗します。

ちなみに、CloudWatchではあるしきい値を超えるとAmazon SNS経由でメールを飛ばす事が出来るのですが、それもしゃべっている間に来ておりましたw
実際に来たメール(クリックで拡大)
Alarmの設定画面(クリックで拡大)


CloudWatch Custom Metricsの注意点
5分という制限時間ではお伝えしきれなかった事ををここにメモしておきます。※2番目、3番目はCustom Metricsに限った話ではありません。
  1. 消す手段が無いw

  2. 時刻表記がUTCのみ
    これは、Management Consoleで見た場合に非常に分かりにくいのでなんとかして欲しいところです < AWS様
  3. グラフデータを画像として取得できない
    ただし、APIのGetMetricStatisticsを叩けば、DataPointがドバドバっと取得できますので、自力でグラフを描く事は可能です。ただ、どうせなら絵にして欲しいところです < AWS様


まとめ
今回はネタとしてわざとAndroidとかTwitterなどを出してみましたが、これはCloudWatchのCustom Metricsって使い勝手があるよ、という事を強調したかったからです。(というか、フォロワーを増やしてみたかっただけ?)
通常OSやアプリケーションレベルのメトリクスをとりたい場合、Nagios、Zabbix、Muininあたりが出てくるのでしょうけれども、クラウド環境でそれらを動かすってのもある意味芸が無いというか、そもそも監視サーバのお守りもなんだかなぁ、と思うので、CloudWatchに全て統合できれば嬉しいな、という個人的見解です。

もう、ここまで来るとIaaSとかPaaSっていうレイヤー分けが意味が無い気がする今日この頃でした・・