SORACOM Funnel で CloudWatch に温度グラフを作る

この記事は [SORACOM Advent Calendar 2016](http://qiita.com/advent-calendar/2016/soracom) の15日目です。

# tl;dr

1. RasPi に USB 温度計と [SORACOM Air](https://soracom.jp/services/air/) のドングルを差して
2. [SORACOM Funnel](https://soracom.jp/services/funnel/) に送って
3. Kinesis Streams からの
4. Lambda からの
5. CloudWatch Custom Metric でグラフ化するよ

![cloudcraft - Oreno Architecture.png](https://qiita-image-store.s3.amazonaws.com/0/78376/96b54071-5c40-f910-40a9-79ee9947b37f.png)

# RasPi の準備

実は全く触ったことがなかったので、お手軽そうな [Raspberry Pi3 Model B ボード＆ケースセット (Element14版, Clear)](https://www.amazon.co.jp/Raspberry-%E3%83%9C%E3%83%BC%E3%83%89%EF%BC%86%E3%82%B1%E3%83%BC%E3%82%B9%E3%82%BB%E3%83%83%E3%83%88-Element14%E7%89%88-Computing-Lab/dp/B01CHJRAJA/) を購入しました。色は SORACOM ネタということで青。kawaii。

Raspbian を `dd` で MicroSD に焼いて差し、 HDMI でテレビにつないで起動、 SSH の設定あたりまでをさくっと完了しました。 Web を検索すると山ほど先達がいて、ありがたいことこの上なし。

- [Raspberry Pi 3を買ってMacを使ってWiFi接続とSSHの接続するまで - Qiita](http://qiita.com/toshihirock/items/8e7f0887b565defe7989)

# 温度計を使えるようにする

家の温度計ってみようか、と思って1年半くらい前に買った（そしてちょっとだけ触って放置してた） [USB温度計! USB thermometer-528018](https://www.amazon.co.jp/OEM-USB%E6%B8%A9%E5%BA%A6%E8%A8%88-USB-thermometer-528018/dp/B004FI1570) の存在を思い出したので発掘。

[初老丸](http://qiita.com/advent-calendar/2016/syoroumaru) a.k.a. かっぱ先輩の記事を参考に `temper` をインストールしました。

- [Raspberry Pi で室内の温度を可視化するよー | cloudpack.media](https://cloudpack.media/8541)

```shell-session
pi@raspberrypi:~ $ sudo temper
12-Dec-2016 20:11,27.310661
```

いちおう気温が取れてるようです。

# SORACOM Air のセットアップ

SORACOM から発売されている [3G USBドングル AK-020 SORACOMスターターキット](https://www.amazon.co.jp/3G-USB%E3%83%89%E3%83%B3%E3%82%B0%E3%83%AB-AK-020-SORACOM%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BF%E3%83%BC%E3%82%AD%E3%83%83%E3%83%88/dp/B01G1GSYHW/)を購入しました。

まずは届いた SIM を [SORACOM Console](https://console.soracom.io/) で登録して、のちほど Funnel を使うときに必要なのでグループを作って紐付けておきます。

![Capture.PNG](https://qiita-image-store.s3.amazonaws.com/0/78376/b0e70a99-d34a-b25d-b29e-16f5f6364252.png)

登録したらデバイス側の設定を

- [各種デバイスで SORACOM Air 使用する | Getting Started | SORACOM Developers](https://dev.soracom.io/jp/start/device_setting/)
- [AK-020 を Raspbian で使う(とりあえず版) - Qiita](http://qiita.com/j3tm0t0/items/08c199fb8863b69e1139)
- [(Raspbian) AK-020で3Gモデム自動接続(毎回のejectとmodprobeなし) - Qiita](http://qiita.com/aquahika/items/dc7d8ad8f481414d3528)

あたりを参考にやっておきます。有線 LAN も Wi-Fi も切って、 SORACOM Air でインターネットに通信できることを確認します。

# Kinesis Stream を作る

<img width="1131" alt="Amazon_Kinesis_Streams_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/78376/b23a91be-f386-bb93-0984-0929008dde58.png">

設定項目もほとんどないのでさくっと作ります。もちろん Shard 数は `1` で充分です。

# Funnel 用 IAM User を作る

<img width="621" alt="IAM_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/78376/20d7b1bd-1197-a41e-1f94-379d877575d7.png">

たぶん `kinesis:Put*` だけを許可すれば問題ない気がしますが、今回は大雑把に `AmazonKinesisFullAccess` ポリシーを付加しました。

# SORACOM に credentials を登録

<img width="930" alt="SORACOM_ユーザーコンソール.png" src="https://qiita-image-store.s3.amazonaws.com/0/78376/68392274-7026-667f-8ed2-a21e6b073121.png">

`セキュリティ` > `認証情報ストア` から認証情報を登録します。上で作った IAM User の credentials をコピペしましょう。

# SORACOM Funnel のセットアップ

<img width="995" alt="SORACOM_ユーザーコンソール.png" src="https://qiita-image-store.s3.amazonaws.com/0/78376/e2f6507e-2418-4a88-0b39-e3828fc732e2.png">

`グループ` > `（SIM を登録した時に作ったグループ）` から Funnel を有効化します。

リソースタイプは `Amazon Kinesis Streams` 、リソース URL は `https://kinesis.<Stream を作ったリージョン>.amazonaws.com/<Stream の名前>` （これは AWS コンソールで Stream を見ても表示されておらず、ちょっとわかりづらい）、認証情報は上で登録した認証情報、送信するデータの形式は `JSON` にします。

[SORACOM Funnel の Kinesis Firehose アダプターを使用してクラウドにデータを収集する(コンソール版) | Getting Started | SORACOM Developers](https://dev.soracom.io/jp/start/funnel_console/) を参考に、 SORACOM Air 接続済みの RasPi から動作確認をします。

```shell-session
$ curl -vX POST http://funnel.soracom.io -d '{"message":"Hello SORACOM Funnel via HTTP!"}' -H 'Content-Type: application/json'
```

`Content-Type` はちゃんと指定しないと怒られます。 `HTTP 20x` が返ってくればおそらく成功です。

# 温度計のデータを Funnel に投げ続けるようにする

言語はなんでもよいですが、ここでは Ruby を選択（好みの問題）。

```rb:temp2funnel.rb
require "time"
require "json"
require "net/http"

result      = %x(sudo temper).split(",")
time        = Time.parse(result.first)
temperature = result.last.to_f

payload = JSON.generate(time: time, temperature: temperature)

Net::HTTP.start("funnel.soracom.io", 80) do |http|
  http.post "/", payload, "Content-Type" => "application/json"
end
```

これを RasPi に入れて、 cron で1分おきに叩くようにしておきます。

```
* * * * * /usr/bin/ruby /home/pi/temp2funnel.rb
```

# Lambda Function をデプロイする

[Serverless Framework](https://serverless.com/) を使いました。簡単なコマンドで Function のみならず、追加の IAM 権限や Kinesis からの導火線もまとめて設定でき、非常～～～に楽です。

こちらも言語はお好みで。

```yaml:serverless.yml
service: orenotempmeter
provider:
  name: aws
  runtime: nodejs4.3
  region: ap-northeast-1
  iamRoleStatements:
  - Effect: Allow
    Action:
    - "cloudwatch:PutMetricData"
    Resource:
    - "*"
functions:
  kinesis2cw:
    handler: handler.default
    events:
    - stream: 
        arn: arn:aws:kinesis:ap-northeast-1:123412341234:stream/soratemp # さっき作った Stream の ARN
        batchSize: 5
        startingPosition: LATEST
        enabled: true
    memorySize: 128
```

```js:handler.js
'use strict';

const AWS        = require('aws-sdk');
const cloudwatch = new AWS.CloudWatch();

module.exports.default = (event, context, callback) => {
  const decodedRecords = event.Records.map(record => {
    const buffer   = new Buffer(record.kinesis.data, 'base64');
    const dataJSON = buffer.toString();

    return JSON.parse(dataJSON);
  });

  cloudwatch.putMetricData({
    Namespace: 'OrenoTempMeter',

    MetricData: decodedRecords.map(record => {
      return {
        MetricName: 'Temperature',
        Value:      record.payloads.temperature,
        Timestamp:  new Date(record.timestamp),

        Dimensions: [
          {
            Name:  'IMSI',
            Value: record.imsi,
          },

          {
            Name:  'OperatorId',
            Value: record.operatorId,
          },
        ],
      }
    }),
  }).promise().then(putMetricDataResult => {
    console.log(JSON.stringify(putMetricDataResult));
    callback(null, {putMetricDataResult});
    return null;
  }).catch(err => {
    console.log(JSON.stringify(err));
    callback({err}, null);
    return null;
  });
};
```

ポイントとしては

- Kinesis Streams で流れてくるデータ本体は `base64` でエンコードされているので、これを復号する必要がある
- データには、ユーザーが送信した元データ以外に Funnel が付加したメタデータが含まれている
- Funnel が付加したメタデータの `timestamp` はミリ秒なので、数値のまま `PutMetricData` の `Timestamp` に指定するとエラーになる（一方で、 JavaScript の `new Date()` には1000を掛けなくてもそのまま入力できる）

今回は CloudWatch の Dimension として、 Funnel が付加したメタデータの `IMSI` (SIM のユニークな ID) と `OperatorId` (SORACOM のユーザー ID ？)を追加しました。今回は1台しか RasPi がありませんが、もし複数のデバイスで同様にデータを収集したとしても、複数の EC2 インスタンスから特定インスタンスのメトリックを見るのと全く同じ感覚で使えることでしょう。

# CloudWatch の確認

```shell-session
$ metrin --region ap-northeast-1 --namespace OrenoTempMeter --metric-name Temperature --statistic Average --dimension IMSI:123451234512345 --dimension OperatorId:OP1234567890 debug
Params: {
  Dimensions: [{
      Name: "IMSI",
      Value: "123451234512345"
    },{
      Name: "OperatorId",
      Value: "OP1234567890"
    }],
  EndTime: 2016-12-14 04:15:15 +0900 JST,
  MetricName: "Temperature",
  Namespace: "OrenoTempMeter",
  Period: 60,
  StartTime: 2016-12-14 04:10:15 +0900 JST,
  Statistics: ["Average"]
}
Response: {
  Datapoints: [
    {
      Average: 33.482864,
      Timestamp: 2016-12-13 19:13:00 +0000 UTC,
      Unit: "None"
    },
    {
      Average: 33.482864,
      Timestamp: 2016-12-13 19:12:00 +0000 UTC,
      Unit: "None"
    },
    {
      Average: 33.482864,
      Timestamp: 2016-12-13 19:11:00 +0000 UTC,
      Unit: "None"
    },
    {
      Average: 33.482864,
      Timestamp: 2016-12-13 19:10:00 +0000 UTC,
      Unit: "None"
    },
    {
      Average: 33.354275,
      Timestamp: 2016-12-13 19:14:00 +0000 UTC,
      Unit: "None"
    }
  ],
  Label: "Temperature"
}
```

取れているようですね。 [y13i/metrin](https://github.com/y13i/metrin) は最近作った CloudWatch のミニマルな CLI です。そのうち紹介記事を書きます。

<img width="1423" alt="Screen_Shot_2016-12-14_at_4_36_55.png" src="https://qiita-image-store.s3.amazonaws.com/0/78376/c18bee7a-3271-29a1-246e-813201a5113e.png">

コンソールでも問題なさそう。

# ここでそうびしていくかい？

室内だけではあまりにデータが地味なので、一式を持って外を小一時間お散歩してみます。

![Slack for iOS Upload.jpg](https://qiita-image-store.s3.amazonaws.com/0/78376/aa9b2985-60a7-54e1-fce0-b94d3b5109c2.jpeg)

温度計は直差しだと RasPi 本体の発熱が影響するので、延長ケーブル経由で。モバイルバッテリーは Ingress エージェント御用達のリアルパワーキューブこと[コレ](https://www.amazon.co.jp/%E3%82%A4%E3%83%B3%E3%82%B0%E3%83%AC%E3%82%B9%E5%85%AC%E5%BC%8F-cheero-Ingress-Cube-12000mAh-%E3%83%A2%E3%83%90%E3%82%A4%E3%83%AB%E3%83%90%E3%83%83%E3%83%86%E3%83%AA%E3%83%BC/dp/B00RF9WUEI/)。

結果がこちらです。

![CloudWatch_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/78376/bdc8d843-f6e6-fd5b-7e26-3c73fc3a1b54.png)

温度計の品質の問題か、あまり反応性はよくなくて、なだらかに変化してますね。

# まとめ

- SORACOM Funnel を使うことで、 RasPi の中には AWS の認証情報を入れておく必要がありません。機器の数が多くなればなるほど管理の容易さ・セキュリティともに恩恵が大きくなっていくでしょう
- 今回は CloudWatch にデータを送りましたが、一度 Kinesis に入れてしまえば後の取り回しは自由度が高いです