CloudFormation であら便利に静的サイトを作る

cloudpack あら便利カレンダー 2017 の18日目です。

AWS で静的ウェブサイトといえば S3 + CloudFront ですが、その構成を CloudFormation で作る際にあら便利な Tips を入れていこうという記事です。

とりあえず起動してみる

AWS コンソールにログインした状態で、このリンクから Stack を起動できます。

Stack が起動したら、 OutputsURL にアクセスすれば CloudFront/S3 に置かれた静的ページが閲覧できるはずです。

このテンプレートが置いてあるバケットは誰でも List/Get できるので、

$ aws s3 sync s3://cfn-staticweb-example/ .

すれば CloudFormation テンプレートとカスタムリソース用の Lambda ファンクションのソースが入った zip が落ちてきます。そのあとに aws cloudformation create-stack コマンドで起動するもよし、内容を見るも改造するもよしです。

さらに、このファイル一式を生成しているコードは y13i/cfn-staticweb-example にあります。 Serverless Framework を使いつつ、 serverless deploy はせず、 serverless package して出力されたファイルを一部改変して S3 バケットに上げるということをしています。

CFn でどんなあら便利があるの?

CloudFront の利用する・しないの切り替え

CloudFront Distribution は作成にかなり時間が掛かります。このテンプレートでは CloudFront を使わないことも選べるようにしました。

CloudFormation の Conditions を使い、 UseCloudFront という Parametertrue の時のみ Distribution を作成します。さらに S3 Bucket Policy で読み取りを許可する参照元は

  • UseCloudFronttrue

    • CloudFront からのみ可(Origin Access Identity を使用)
  • UseCloudFrontfalse

    • どこからでも可

切り替わるようにしています。

CloudFront の各種パラメーターの切り替え

ACM (AWS Certificate Manager) で発行した証明書を AcmCertificateArn パラメーター指定した場合は Distribution で ViewerCertificate として使用するようにしています。

指定しない場合(値が空文字)の場合は AWS::NoValue 擬似要素を使って、そもそも ViewerCertificate が指定されていないように振る舞うようになっています。

同様に、 Distribution Alias も Parameter で指定された場合のみ設定するテンプレートになっています。

Origin Access Identity の作成

Origin Access Identity は普通には CloudFormation で作成できません!

なんで対応していないのか疑問に思うところですが、文句を言ってもしかたないので Custom Resource使って Lambda Function に作成させています。

インデックスドキュメントの生成

CloudFormation でサポートされている S3 のリソースは BucketBucketPolicy のみです。バケットの内容物は CloudFormation では管理できません。

しかし、前項と同様にカスタムリソースを使えばどうにでもなります。

ここでは、 Parameter で受け付けた IndexDocumentTitleIndexDocumentBodyejs テンプレートに流し込むという処理をして HTML を生成し、バケットに置くという処理をさせてみました

まとめ

Lambda とその周辺環境がそこそこ枯れてきたことで、 Lambda-backed Custom Resource がだいぶ使いやすくなったと感じます。つまり、 CloudFormation の使い勝手も増しているということです。

地味ながら進化を続ける CFn をあら便利に使っていきましょう。

続きを読む

LoRaWANとSORACOMFunnelのAWSIoTアダプタを使ってDynamoDBにデータを書き込む

はじめに

つい先日、SORACOMFunnelがAWSIoTに対応したというニュースを耳にしました
ちょうど仕事の関係でSORACOMのシールドが届いたし、会社にLoRaWANのPublicGWもあることだし・・・
ということでちょいと触ってみた
SORACOM公式ブログにも手順が書いてありましたが、ちょっと躓いたところがあったりしたので、まとめてみました

やりたいこと

  1. LoRaデバイスからLoRaゲートウェイを通ってAWSIoTにセンサーデータを投げる
  2. AWSIoTが受け取ったデータを加工するためのLambdaファンクションをキックする
  3. Lambdaがデータを加工してDynamoDBに格納する

SORACOM Funnelって?

SORACOM Funnel(以下、Funnel) は、デバイスからのデータを特定のクラウドサービスに直接転送するクラウドリソースアダプターです。
Funnel でサポートされるクラウドサービスと、そのサービスの接続先のリソースを指定するだけで、データを指定のリソースにインプット
することができます。

http://soracom.jp/services/funnel/より抜粋
要するに、デバイスからAWSなどのクラウド上に閉域網でデータを送信することができるサービス(合ってるかな・・・)

AWSIoTって?

AWS IoT によって、さまざまなデバイスを AWS の各種 Services や他のデバイスに接続し、データと通信を保護し、
デバイスデータに対する処理やアクションを実行することが可能になります。
アプリケーションからは、デバイスがオフラインの状態でもデバイスとのやり取りが可能です。

https://aws.amazon.com/jp/iot-platform/how-it-works/より抜粋
うーん、なるほどわからん。とりあえず使ってみよう

デバイス側の設定

同じ部署の電気系強いお方が気づいたらセッティングしていただいていましたので割愛
この時点でSORACOM Harvestにてデータが送信されているのを確認できている状態

AWSIoTの設定

Funnelでデータを送信する先のAWSIoTを作成します

エンドポイントを控える

Funnelを設定する際に必要なAWSIoTのエンドポイントを控えておきます

AWSIoT_TOP.PNG

Ruleを作成する

左のサイドメニューから「Rule」を選択し、「Create a rule」をクリック

AWSIoT_Rule.PNG

「Name」と「Description」を入力する(Descriptionは任意)

AWSIoT_Rule_name.PNG

「Attribute」に「*」、「Topic filter」に「IoTDemo/#」を入力
「Using SQL version」は「2016-03-23」で問題なければそのままでOK

AWSIoT_Rule_massage.PNG

「Set one or more actions」の「add action」をクリック

AWSIoT_Rule_set_action.PNG

今回はLambdaでデコードする必要があるため「Invoke a Lambda function passing the message data」を選択

AWSIoT_Rule_select_lambda.PNG

「Configure action」を選択

AWSIoT_Rule_select_lambda_button.PNG

キックするLambda Functionを選択
今回は初めて作成するので、Lambdaが呼ばれたときのeventの中身をログに吐き出すLambdaを作成して、それをキックするようにします
※DynamoDBに格納する処理は後ほど実装

「Create a new resouce」をクリック。Lambdaのページに遷移します

AWSIoT_Rule_lambda_create.PNG

「Blank Function」を選択

Lambda_create.PNG

Lambdaのトリガーを設定
「IoTタイプ」は「カスタムIoTルール」を選択
「ルール名は」現在作成中のルール名
「SQLステートメント」は作成中の「Rule query statement」の中身をコピー
「次へ」をクリック

Lambda_trigger.PNG

「名前」はお好きなFunction名をつけてください
「ランタイム」は筆者の好みによりNode.jsです
コードには

exports.handler = (event, context, callback) => {
    console.log(event);
};

と書いておいてください。

Lambda_setting.PNG

あとは、DynamoDBの権限を持ったロールを選択(作成)して、ページ下部の「次へ」をクリックしてLambdaFunctionを作成してください

AWSIoTのページに戻って、先ほど作成したLambdaFunctionを選択し、「Add action」をクリック

AWSIoT_Rule_add_lambda.PNG

その後「create Rule」をクリックするとRuleが作成されます
これでAWSIoTのRule作成が完了です

SORACOM Funnelの設定

まず、SORACOMコンソールにログインし、再度メニューから「LoRaグループ」⇒「追加」をクリックします
ポップアップが出てきてグループ名を入力するように言ってくるので、任意のグループ名を入力しグループを作成します

作成したグループを選択し、設定画面に移動します

転送先サービス:AWS IoT
転送先URL:https:///rule内で作成したSQLTopicFilter/#{deviceId}
認証情報:AWSIoTの権限を持ったIAMアカウント情報で作成したもの
送信データ形式:無難にJSON

funnel_setting.PNG

※転送先URLにはプレースホルダーを作成することができます
  - SIMを利用する場合:{imsi}
  - LoRaデバイスを利用する場合:{deviceId}

これでFunnelの設定は完了です

Lambdaの実装

デバイスの電源を入れ、データが送信されるようになると、Lambdaが起動してeventの中身をログに吐き出していると思います
↓こんな感じ

2017-06-23T04:13:59.850Z 62014535-57ca-11e7-b4e4-9fbd147f2037 { 
  operatorId: '0123456789',
  timestamp: 1498191237793,
  destination: { 
    resourceUrl: 'https://xxxxxxxxx.iot.ap-northeast-1.amazonaws.com/xxxxxxx/#{deviceId}',
    service: 'aws-iot',
    provider: 'aws' 
  },
  credentialsId: 'iot-sys',
  payloads: { 
    date: '2017-06-23T04:13:54.276320',
    gatewayData: [ [Object] ],
    data: '7b2268223a36312e367d',
    deveui: '1234567890' 
  },
  sourceProtocol: 'lora',
  deviceId: '1234567890' 
}

センサーから送られてくるデータはevent[“payloads”][“data”]にHEX形式で格納されているので、取り出してデコードする必要があります。


const data = event["payloads"]["data"];
const decodeData = new Buffer(data, "hex").toString("utf8");

デコードすると「7b2268223a36312e367d」⇒「{“h”: 61.6}」のようなString型になります(これは一例)

Object型のほうが使い勝手がよいので、parseしてしまいましょう


const parseData = JSON.parse(decodeData); // {h : 61.6}

あとはDynamoDBにputで投げつけます

index.js
"use strict";

const AWS = require("aws-sdk");
const co = require("co");
const moment = require("moment-timezone");

const dynamodb = new AWS.DynamoDB.DocumentClient({
  region: "ap-northeast-1"
});

const dynamoPutData = require("./lib/dynamo_put_data");

exports.handler = (event, context, callback) => {
  // UTCなのでJSTに変換
  const date = event["payloads"]["date"];
  const time = moment(date).tz("Asia/Tokyo").format();
  // HEX形式をデコード
  const data = event["payloads"]["data"];
  const decodeData = new Buffer(data, "hex").toString("utf8");
  // Object型に変換
  const parseData = JSON.parse(decodeData);
  // deviceIdを取得
  const deviceId = event["deviceId"];

  // DynamoDBにPUTするItem
  const item = [{
    deviceId: deviceId,
    time: time,
    value: parseData
  }];

  co(function *() {
    yield dynamoPutData.putDynamoDB(dynamodb, item[0]);
  }).then(() => {
    console.log("success!")
  }).catch((err) => {
    console.log(err);
  });
};

dynamo_put_data.js
"use strict";

class dynamoPutData {
  /**
   * DynamoDBへのPUT処理
   * @param {DocumentClient} dynamoDB
   * @param item
   * @returns {Promise}
   */
  static putDynamoDB(dynamoDB, item) {
    const params = {
      TableName: "TABLE_NAME",
      Item: item
    };
    return dynamoDB.put(params).promise();
  }
}

module.exports = dynamoPutData;

dynamo_put_data.js中の”TABLE_NAME”にはデータを投げつけるテーブル名を書いてください
関数を外だしして複数ファイルがあるので、Lambdaにはソースコード一式をZIPに固めてアップする方法でデプロイを行います
データが送られてきてLambdaがキックされると、DynamoDBにデータが格納されていると思います

まとめ

日ごろからAWSのサービスを使っていましたが、AWSIoTを利用する機会がなくとてもいい経験になりました。
今回はデバイスからクラウドといった方向でしたが、AWSIoTを利用すればその逆方向も実現することができるらしいので、近々そういった実装もしてみたと思います

では!

続きを読む