repl.info

IIJmioのクーポン残量をAWS LambdaとMackerelでプロットしてみよう

この記事は pepabo Advent Calendar 2016 の3日目です。

先日IIJmioにNMPしたんです。

みおふぉんにMNPした

で、MNPしていろいろ調べていると、IIJmioには IIJmioクーポンスイッチ というアプリがあって、クーポン(高速通信可能なデータ量)の利用を制御できるんですね。そして、クーポンスイッチAPI というものが存在する。何ができるのかというと、クーポンの残量照会やON/OFFの状態、データ利用量の照会なんかができるらしい。そこでふと思ったんですが、データ利用量をAPIで取得できるということはグラフにプロットできるわけです。これは触ってみるしかない!ということで11月末頃にいろいろ触っていました。

プロットした結果どうなったか

まずは結果を見てみましょう。11/26(土) 0:00〜11/27(日) 24:00の様子です。

repl_coupon___%c2%b7_mackerel

グラフと記憶から、どういう時にクーポンを使っていたのか書き出してみました。

  •  11/26
    • 0:00〜11:30:変化なし。これは寝ていたのと、午前中家にいてWiFiにつながっていたから。
    • 11:30〜13:00:移動中に使っていたからだと思われる
    • 13:00〜16:00:変化なし。ワークショップに参加していて触っていなかった。
    • 16:00〜17:00:移動。
    • 17:30〜18:30:ほとんど触っていなかった記憶がある。
    • 18:30〜20:00:移動+スマホを見ながら飲んでいたはず。
    • 20:00〜21:30:知人と合流して飲んでいた
    • 21:30〜22:30:帰宅途中。
    • 22:30〜24:00:自宅でWiFi使用なのでクーポン利用なし。
  • 27日
    • 0:00〜12:30:寝ていたのと、家でコードを書いていたので利用なしもしくはWiFi利用。
    • 12:30〜13:30:お昼を食べに外に出た。近所の中華で、異常に量が多い店です。
    • 13:30〜20:30:家にこもっていたのでクーポン使用はなし。
    • 20:30〜21:30:夕食のために外に出たので使用したと思われる。
    • 21:30〜24:00:自宅なので利用なしもしくはWiFi利用。

こうやって見ると、休日は大体移動中にデータ通信をしている、というのが可視化されておもしろいですね。というか、移動中にスマートフォンを使いすぎでひどい…クーポンが減っていなくてもWiFi経由でかなり使っているはずなのでもっとひどい。このグラフからだと実際の利用時間などはわからないのが恐ろしいですね。

続いて、12月1日になってクーポンが支給された時の様子。

repl_coupon___%c2%b7_mackerel

0:00時点で増えていることが可視化されました。便利!

実装の話

タイトルで大体わかると思いますが、一応実装のことも書いておきます。コードはnodejsで書いて、AWS lambda上にデプロイ。CloudWatch Eventsで15分に1回動かすようにしています。

クーポンスイッチAPIの認証はOAuth2なんですが、手元でaccessTokenを取得し、DynamoDBに放り込んでそれを参照する、という方法をとりました。記事を書いてる時に知ったのですが、11/18に環境変数機能が追加されていました(

AWS Lambda の新機能 – 環境変数とサーバーレスアプリケーションモデル (SAM))。KMSを使うとのことで、暗号化もされるのでこちらを使うのがよさそうです。また、3ヶ月ほどでトークンは使えなくなるので、継続してプロットしたい場合はちゃんとトークンを再取得するようにするか、DynamoDB上のトークンを手で書き換える必要があります。refresh tokenがあればそれを用いて再取得できると思いますが、今のところできないようです。

実際のプロットに使ったコードは以下の通りです。

var AWS = require('aws-sdk');

var dynamoDB = new AWS.DynamoDB.DocumentClient({region: 'ap-northeast-1'})

var rp = require('request-promise');

var Mackerel = require("mackerel");



var mackerel = function() {

    return dynamoDB.get({TableName: 'repl_keys', Key: {"key": "mackerel"}}).promise();

};



var iijmio = function() {

    return dynamoDB.get({TableName: 'repl_keys', Key: {"key": "iijmio"}}).promise();

};



var getCouponInfo = function(clientID, accessToken) {

    var options = {

        method: "GET",

        url: "https://api.iijmio.jp/mobile/d/v1/coupon/",

        headers: {

            "X-IIJmio-Developer": clientID,

            "X-IIJmio-Authorization": accessToken

        }

    };

    console.log(options);



    return rp(options);

};



var couponTotal = function(data) {

    var total = 0;

    data["couponInfo"].forEach(function(couponInfo) {

        couponInfo["coupon"].forEach(function (coupon) {

            total += coupon["volume"]

        });

        couponInfo["hdoInfo"].forEach(function (hdoInfo) {

            hdoInfo["coupon"].forEach(function (coupon) {

                total += coupon["volume"]

            });

        });

    });

    return total;

};



var serviceMetrics = function() {

    return Promise.all([iijmio()]).then((results) => {

        var clientID = results[0]['Item']['clientID'];

        var accessToken = results[0]['Item']['accessToken'];

        return getCouponInfo(clientID, accessToken);

    }).then((couponInfo) => {

        console.log(couponInfo);

        metrics = [

            {

                "name": "coupon.stock",

                "time": Math.round((new Date).getTime() / 1000),

                "value": couponTotal(JSON.parse(couponInfo))

            }

        ];

        console.log(metrics);

        return Promise.resolve(metrics);

    });

};



exports.handle = function(event, context, callback) {

    return Promise.all([serviceMetrics(), mackerel()]).then((results) => {

        var metrics = results[0];

        var mackerelToken = results[1]['Item']['token'];

        mackerel = new Mackerel(mackerelToken);

        return mackerel.postServiceMetric("repl", metrics);

    }).then(() => {

        console.log("finish!");

        callback(null, "finish!");

    }).catch((error) => {

        console.error(error);

        callback(error);

    });

};

細かく見ていきましょう。使用ライブラリはaws-sdk、request-promise、muddydixon/node-mackerel です。

var AWS = require('aws-sdk');

var dynamoDB = new AWS.DynamoDB.DocumentClient({region: 'ap-northeast-1'})

var rp = require('request-promise');

var Mackerel = require("mackerel");

この2つの関数は、DynamoDBからMackerelのAPI Keyを取得したり、IIJmioのトークンを取得するためのものです。Promiseを全体的に使用しているので、dynamoDB.get()の結果をPromise化して返しています。

var mackerel = function() {

    return dynamoDB.get({TableName: 'repl_keys', Key: {"key": "mackerel"}}).promise();

};



var iijmio = function() {

    return dynamoDB.get({TableName: 'repl_keys', Key: {"key": "iijmio"}}).promise();

};

getCouponInfoはIIJmioのクーポン情報を取得する関数です。APIについては クーポンスイッチAP を参照してください。OAuthのキーはDynamoDBに置いてある物を使うので、素朴にGETします。

var getCouponInfo = function(clientID, accessToken) {

    var options = {

        method: "GET",

        url: "https://api.iijmio.jp/mobile/d/v1/coupon/",

        headers: {

            "X-IIJmio-Developer": clientID,

            "X-IIJmio-Authorization": accessToken

        }

    };

    console.log(options);



    return rp(options);

};

次のcouponTotalは、取得したクーポン情報から残量の合計を求める関数です。

var couponTotal = function(data) {

    var total = 0;

    data["couponInfo"].forEach(function(couponInfo) {

        couponInfo["coupon"].forEach(function (coupon) {

            total += coupon["volume"]

        });

        couponInfo["hdoInfo"].forEach(function (hdoInfo) {

            hdoInfo["coupon"].forEach(function (coupon) {

                total += coupon["volume"]

            });

        });

    });

    return total;

};

次はserviceMetrics。Mackerelに投稿するためのメトリクスを生成します。サービスAPIの仕様は ここ を参照してみてください。

var serviceMetrics = function() {

    return Promise.all([iijmio()]).then((results) => {

        var clientID = results[0]['Item']['clientID'];

        var accessToken = results[0]['Item']['accessToken'];

        return getCouponInfo(clientID, accessToken);

    }).then((couponInfo) => {

        console.log(couponInfo);

        metrics = [

            {

                "name": "coupon.stock",

                "time": Math.round((new Date).getTime() / 1000),

                "value": couponTotal(JSON.parse(couponInfo))

            }

        ];

        console.log(metrics);

        return Promise.resolve(metrics);

    });

};

最後に、Lambdaのmain関数であるexport.handleです。サービスメトリクスとmackerelのキーを取得した後、それを使ってMackerelに投稿しています。

exports.handle = function(event, context, callback) {

    return Promise.all([serviceMetrics(), mackerel()]).then((results) => {

        var metrics = results[0];

        var mackerelToken = results[1]['Item']['token'];

        mackerel = new Mackerel(mackerelToken);

        return mackerel.postServiceMetric("repl", metrics);

    }).then(() => {

        console.log("finish!");

        callback(null, "finish!");

    }).catch((error) => {

        console.error(error);

        callback(error);

    });

};

 

まとめ

IIJmioのクーポンスイッチAPIとAWS Lambda、Mackerelを使ってクーポン残量をプロットしました。

やってみてよかった点としては、

  • どういうタイミングでデータ通信しているのか可視化できた。
    • 移動中によく使っているので、控えようという意識付けにつなげたい。

というものが挙げられます。課題としては、以下があるでしょうか。

  • OAuthなAPIで、accessTokenをどう更新するのがベターなのかがわからなかった
  •  nodejsは時々触る…という感じの使用頻度なので、Poromise周りで躓いた
    • 毎回躓いている気がする
  • クーポン利用量だけだとスマートフォンの使用状況はわからない
    • WiFiの通信も見る必要があった
  • 一定時間に一定以上通信するとアラートを出す、というのをやってみるとおもしろいかも、と書いている時に思った
  • KMSを用いた環境変数が最近追加されていたので、そちらを使うのがベター

かなり手軽に使えて、楽しいので是非やってみてください。