2016年01月26日

AWS「DynamoDB」「API Gateway」「Lambda」を使って、センサーで取得した温度を保存してみた。


◆今回のゴール

1、RaspberryPiに繋いだ温度センサー【BMP180】で現在の温度を取得
2、「AWS API Gateway」で作成したAPIで温度の値をPOST
3、「AWS Lambda」を経由して「AWS Dynamo DB」(データベース)に値を保存


ondo.png

それではやってみましょう。

必要な道具

・温度センサー【BMP180】
・AmazonAWSのアカウント
 ※ 利用にあたり、AWSの利用料金がかかる場合があります。

スポンサードリンク
◆手順

1、AmazonAWS【Dynamo】
   1-1、テーブルの作成
2、AmazonAWS【IAM】
   2-1、ポリシーの作成
   2-2、ロールの作成
3、AmazonAWS【Lambda】
   3-1、Functionの作成
4、AmazonAWS【API Gateway】
   4-1、APIの作成
   4-2、POST Methodの作成
   4-3、GET Methodの作成
   4-4、APIのデプロイ(Deployment)
   4-5、API Keyの作成
   4-5、endpointの確認
5、【RaspberryPi】温度の取得とAPIでのPOST
6、【RaspberryPi】クーロン化
7、【次回予告】GET APIも使ってみる

1、AmazonAWS【Dynamo】
1-1、テーブルの作成

AWS管理コンソールにログインしたらサービス一覧から「DynamoDB」をクリックして開きます。
今回はリージョンは「東京(ap-northeast-1)」を選択します。
「テーブル作成」をクリックして下記の通り入力します。

テーブル名「raspi」
プライマリキー「type」(文字列) ※ センサーの識別子を格納します。
ソートキー「date」(文字列) ※ センサー値をインサートした日時を格納します。

2015db01.png

「作成」をクリックしたらテーブルが作成されます。
続いては、LambdaからDynamoにアクセスするための権限ロールを作ります。

2、AmazonAWS【IAM】
2-1、ポリシーの作成

サービス一覧からIAM(Identity and Access Management)に移動します。
IAMにリージョンはありませんのでリージョンは気にしなくて大丈夫です。

IAMのポリシー画面で、「ポリシーの作成」ボタンを押し、「独自のポリシーを作成」を選択します。
下記の内容でポリシーを作成します。

ポリシー名「raspiDynamodb」
ポリシードキュメント
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Action": [
                "dynamodb:DeleteItem",
                "dynamodb:GetItem",
                "dynamodb:PutItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Effect": "Allow",
            "Resource": "*"
        },
        {
            "Sid": "",
            "Resource": "*",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Effect": "Allow"
        }
    ]
}

作成したら、続いて、このポリシーをアタッチしたロールを作成します。

2-2、ロールの作成

ロールの作成をクリックします。

ロール名は「raspi_lambda_dynamo」とします

「手順2:ロールタイプの選択」では、下記の通り【AWS Lambda】を選択します。
2015iam06.png

「手順4:ポリシーのアタッチ」で先ほど作成した「raspiDynamodb」を選択します。

2015iam07.png

下記の通りの内容で保存したら、IAMでの作業は完了です。
2015iam08.png
3、AmazonAWS【Lambda】
3-1、Functionの作成

続いて、サース一覧から「Lambda」に移動します。
今回はリージョンは「東京(ap-northeast-1)」を選択します。

「Create a Lambda Function」をクリックします。


下記、Blueprintの選択では何も選ばず「Skip」します。
2015lamd01.png

下記の通り入力・選択します。
Name「raspiFunction」
Runtime「Node.js」(デフォルトのまま)

Handler「index.handler」(デフォルトのまま)
Role「raspi_Lambda_Dynamo」(手順「2-2、ロールの作成」で作ったロールを設定します)

Lambda function code 下記の通り。(※ POSTとGETの処理を記述しています。)
var aws = require('aws-sdk');

var dynamo = new aws.DynamoDB();
var tableName = "raspi";

exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));
 
    var method = event.httpmethod;

    var mylimit = 50;    
    if(event.limit){
        if(Number(event.limit) < mylimit) {
            mylimit = Number(event.limit);
        }
    }
    
    var dynamoRequest = {
        "TableName" : tableName
    };
    
    switch (method) {
        case 'POST':
            var nowTheTime = new Date();
            var nowTheTimeStr = getTimeString(nowTheTime);

            dynamoRequest.Item = {
                "type" : {"S":event.type},
                "date" : {"S":nowTheTimeStr},
                "value" : {"N" :event.value}
            };

            console.log('putItem dynamoRequest:', JSON.stringify(dynamoRequest, null, 2));
            dynamo.putItem(dynamoRequest, function (err, data) {
                if (err) {
                    console.log('err:', JSON.stringify(err, null, 2));
                    context.fail(new Error('putItem query error occured'+ JSON.stringify(dynamoRequest, null, 2)));
                } else {
                    context.succeed(data);
                }
            });
            break;
            
        case 'GET':
            dynamoRequest.Limit = mylimit;
            dynamoRequest.ScanIndexForward = false;

            if(event.date1 && event.date2){
                dynamoRequest.KeyConditions = {
                    "type" : {
                        AttributeValueList:[{"S" : event.type }],
                        ComparisonOperator:'EQ'
                    },
                    "date" :  {
                        AttributeValueList:[
                            {"S" : event.date1},
                            {"S" : event.date2}
                        ],
                        ComparisonOperator:'BETWEEN'
                    }
                };
            } else if(event.date1) {
                dynamoRequest.KeyConditions = {
                    "type" : {
                        AttributeValueList:[{"S" : event.type }],
                        ComparisonOperator:'EQ'
                    },
                    "date" :  {
                        AttributeValueList:[
                            {"S" : event.date1},
                        ],
                        ComparisonOperator:'GE'
                    }
                };
            }
            else {
                dynamoRequest.KeyConditions = {
                    "type" : {
                        AttributeValueList:[{"S" : event.type }],
                        ComparisonOperator:'EQ'
                    }
                };
            }
            
            console.log('query dynamoRequest:', JSON.stringify(dynamoRequest, null, 2));
            dynamo.query(dynamoRequest, function (err, data) {
                if (err) {
                    context.fail(new Error('putItem query error occured'));
                } else {
                    context.succeed(data);
                }
            });
            break;
        default:
            context.fail(new Error('Unrecognized operation "' + method + '"'));
    }

};

// UTC  ISO 8601 format
function getTimeString(now) {
    var year = now.getYear(); 
    var month = ("0" + (now.getMonth() + 1)).slice(-2); 
    var day = ("0" +  now.getDate()).slice(-2); 
    var hour =  ("0" + now.getHours()).slice(-2); 
    var min =  ("0" + now.getMinutes()).slice(-2); 
    var sec =  ("0" + now.getSeconds()).slice(-2); 
    if(year < 2000) { year += 1900; }
    var dateStr = year + "-" + month + "-" +  day  + "T" + hour + ":" + min  + ":" + sec + "Z";
    return dateStr;
}
----------
【追記】2016年2月1日 46行目 「dynamoRequest.ScanIndexForward = false;」
GETする際、直近のデータを取得するよう、ソート順を変更する処理を入れました。
----------


2015lamd02.png

2015lamd03.png
「Next」を押し次の確認画面で「Create Function」を押し保存したら完了です。
続いてはAPIの設定です。

4、AmazonAWS【API Gateway】
4-1、APIの作成

サービス一覧から「API Gateway」に移動し、新規APIの作成を行います。
下記の通り、入力し「Create API」をクリックします。

API name「IoT」
Clone from API「Do not clone from existing API」
2015api02.png

APIが作成された時点で「/」というResourceがあります。この下にさらに子のResourceを作っていきます。 Resources画面で「/」を選択した状態で「Create Resource」ボタンを押します。
2015api03.png
下記の通り入力します。
Resource Name 「raspi」
Resource Path /「raspi」
2015api04.png

「raspi」というResourceが「/」の下に作成されました。 2015api05.png
「raspi」を選択した状態で「Create Resource」をクリックして更に子Resourceを作ります。

Resource Name 「type」
Resource Path /raspi/「{type}」

2015api06.png

上記の内容で保存します。{type}というResourceが作られます。
2015api07.png

続いて、{type}の下にPOSTとGETメソッドを作成します。

4-2、POST Methodの作成

Resources画面で「{type}」を選択した状態で、「Create Method」ボタンをクリックします。
「POST」を選択して、横の「チェックマーク」をクリックします。

2015api08.png
「/raspi/{type}-POST-Setup」の画面が表示されますので、下記の通り前の手順で作ったLambdaでの関数を選択します。

Integration type 「Lambda Function」
Lambda Region「ap-northeast-1」(Lambdaのリージョン。今回は「東京」です。)
Lambda Function「raspiFunction」(最初の数文字を入れると、候補が出てきます)

2015api09.png
「Save」を押すとパーミッション追加の確認ダイアログが出ますので「OK」を押すと、POST Methodの作成が完了します。

2015api11.png
上図の「/raspi/{type} - POST - Method Execution」画面で「Method Request」をクリックします。

Authorization type 「NONE」
API Key Required「true」
に設定します。

2015api12.png
「/raspi/{type} - POST - Method Execution」画面に戻ります。
2015api13.png
「/raspi/{type} - POST - Method Execution」画面から「Integration Request」をクリックします。

Mapping Templatesを展開して、Content-Typeを下記の通り入力します。
Content-Type「application/json」

この「application/json」のTemplateを下記の通り入力して保存します。
{
    "httpmethod" : "$context.httpMethod",
    "type" : "$input.params('type')",
    "value" : $input.json('$.value')
}

2015api14.png

以上で、POSTの設定は完了です。つづいて、GETのMethodを作成します。

4-3、GET Methodの作成

左側のツリーで「{type}」をクリックします。
この状態で、「Create Method」ボタンをクリックします。

2015api15.png
「GET」を選択して、横の「チェックマーク」をクリックします。 「/raspi/{type}-GET-Setup」の画面が表示されますので、下記の通り前の手順で作ったLambdaでの関数を選択します。

Integration type 「Lambda Function」
Lambda Region「ap-northeast-1」(Lambdaのリージョン。今回は「東京」です。)
Lambda Function「raspiFunction」(最初の数文字を入れると、候補が出てきます)

2015api16.png
「Save」をクリックすると下記の通り、GET Methodが作成されます。

2015api17.png

「Method Request」の設定を行います。

Authorization type「NONE」
API Key Required「true」

「URL Query String Parameters」を開き、以下の3つのquery stringを追加します。
「limit」「date1」「date2」

2015api18.png

設定を保存したら一度、「Method Execution」に戻り、続いて「Integration Request」をクリックします。 Mapping Templatesを展開して、Content-Typeを下記の通り入力します。
Content-Type「application/json」

この「application/json」のTemplateを下記の通り入力して保存します。
{
    "httpmethod" : "$context.httpMethod",
    "type" : "$input.params('type')",
    "date1" : "$util.urlDecode($input.params('date1'))",
    "date2" : "$util.urlDecode($input.params('date2'))",
    "limit" : "$util.urlDecode($input.params('limit'))"
}

2015api20.png

設定を保存したらデプロイに進みます。
デプロイをすることで、APIが使える状態になります。

4-4、APIのデプロイ(Deployment)

APIをデプロイします。
「Deploy API」ボタンをクリックしてください。

下記の通り入力します。
Deployment stage「New Stage」
Stage Name「prod」

2015api22.png

StageとはAPIが動作する環境のことです。
「テスト環境」「開発環境」「本番環境」など自由にStageを作成できます。

4-5、API Keyの作成

続いて、このAPIを利用するためのAPI Keyを発行します。
上部メニューバーの「Amazon API Gateway」の隣のメニューリストから「API Keys」をクリックします。
「Create API Key」をクリックし、下記の通り入力します。

Name 「raspi」
Enabled「チェック有り」

上記で保存後、下記の通り選択して「Add」ボタンをクリックします。

Select API「IoT」
Select stage「prod」

※ ここで発行したAPI Keyはこの後の手順で使用します。
2015api24.png

4-5、endpointの確認

最後にendpointのURLを確認しておきます。

上部メニューバーの「Amazon API Gateway」の隣のメニューリストから「APIs」を選択。

「IoT」APIの「Stage」をクリックします。
下記の通り、「prod」を展開して、「POST」または「GET」Methodを選択すると、上部に「Invoke URL」が表示されます。

このURLがendpointです。このURLはこの後の手順で使用します。

2015api25.png

以上でAWSでの設定は完了です。
いよいよ次は、RaspberryPi側での作業です。

5、【RaspberryPi】温度の取得とAPIでのPOST

まずは、下記記事を元に、温度が取得できるようにしてください。
Raspberry Piで気温を測ってみた【BMP180を使用】

上記記事で、実行している「Adafruit_BMP085_example.py」を少し改造します。
「Adafruit_BMP085_example.py」が置いてある場所に移動し、コピーを取って編集します。
$ cd {gitでコード取得した場所}/Adafruit-Raspberry-Pi-Python-Code/Adafruit_BMP085
$ sudo cp -ip Adafruit_BMP085_example.py Raspi_Adafruit_BMP085.py
$ sudo vi Raspi_Adafruit_BMP085.py

一番下の行(33〜35行目)を編集します。
print "Temperature: %.2f C" % temp
print "Pressure:    %.2f hPa" % (pressure / 100.0)
print "Altitude:    %.2f" % altitude
print "%.2f" % temp
ちなみに、変更後にこのファイルを実行すると、下記のように、気温が数値のみで出力されます。
$ ./Raspi_Adafruit_BMP085.py 
28.20

続いては、APIをコールするプログラムを任意の場所に作ります。
※ 今回はPython3でやります。

httplib2というモジュールを使うので入っていない場合は事前にインストールします。
sudo apt-get install python3-httplib2


$ cd {任意のディレクトリ}
$ sudo vi raspi_post.py

「raspi_post.py」というファイルを作り、下記の通り編集します。
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
import httplib2
import json

param = sys.argv

myheaders = {
  "x-api-key" : "●●●●●●●●●●●●●●●●",
  "Content-type" : "application/json",
  "Accept" : "application/json"
}

mybody = {
  "value" : param[2]
}

mybody = json.dumps(mybody)

url = "https://●●●●●●.●●●●●●.ap-northeast-1.amazonaws.com/prod/raspi/%s" % param[1]

h = httplib2.Http()

resp, content = h.request(url, "POST", mybody, headers = myheaders)

print (resp.status)
print (resp.reason)

print (content)

【解説】
10行目は、「4-5、API Keyの作成」で発行したAPI Key に置き換えます。
21行目は、「4-5、endpointの確認」で確認したURLに置き換えます。

1つ目のパラメータ(param[1])はセンサーの種類の識別子。
2つ目のパラメータ(param[2])はセンサーの値。

保存したら、実行権限を付けます。
$ sudo chmod +x raspi_post.py

最後に、「Raspi_Adafruit_BMP085.py」で取得した温度を「raspi_post.py」でPOSTするためのシェルを作ります。
$ sudo vi /usr/local/sbin/post_temperature.sh
#!/bin/sh

VALUE=`/{任意のディレクトリ}/Adafruit-Raspberry-Pi-Python-Code/Adafruit_BMP085/Raspi_Adafruit_BMP085.py`

RESULT=`/{任意のディレクトリ}/raspi_post.py temperature $VALUE`

echo $RESULT

保存したら、実行権限を付けて、試しに実行してみましょう。
$ sudo chmod +x /usr/local/sbin/post_temperature.sh
$ post_temperature.sh
200 OK b'{}'
AWSのコンソールを開き「DynamoDB」で「raspi」テーブルから「項目」タブを開きます。
下記の通りデータが入っていればOKです。
(例)「type」temperature、「date」2016-01-26T01:22:33Z、「value」25.7

* ちなみに、時間は、協定世界時(UTC) で保存しています。日本時間とは9時間の差があります。

6、【RaspberryPi】クーロン化

cronで定期的(1時間毎)に気温をPOSTするようにします。
$ sudo vi /etc/cron.d/post_temperature_cron
下記の通り、毎時0分に実行するように編集し保存します。
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

0 * * * * pi /usr/local/sbin/post_temperature.sh


以上で、RaspberryPiを起動しておけば、1時間に1回、気温を測って、APIでPOSTしてDynamoに値がたまるようになりました!

7、【次回予告】GET APIも使ってみる

あまり、解説しませんでしたが、Dynamoにたまったデータを取得するためのGET APIも作っています。
次回は、GET Methodを使って、API経由でDynamoの保存しているデータを取得します。



posted by Raspberry Pi at 14:52 | Comment(0) | Raspberry Pi | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

※ブログオーナーが承認したコメントのみ表示されます。