miniconda環境にlocalstack専用のenvironmentを作成する

localstack の実行環境を作成するにあたり、既存のPythonの環境とは分離しておきたいので、miniconda で localstack 専用の environment を作成して、ここにインストールした記録。

environment の作成

$ conda create -n localstack
Fetching package metadata .........
Solving package specifications: 
Package plan for installation in environment /Users/<username>/miniconda3/envs/localstack:

Proceed ([y]/n)? y

environment を activate

$ . activate localstack
(localstack) $

localstack をインストール。

(localstack) $ pip install localstack

localstack を起動

(localstack) $ localstack start

筆者の環境では以下のようなエラーが出て起動に失敗する状態だったため、つぎ示すようなパッチを当てた。

(localstack) $ localstack start
Starting local dev environment. CTRL-C to quit.
Error starting infrastructure: 'zipimport.zipimporter' object has no attribute 'path'
Traceback (most recent call last):
  File "/Users/<username>/miniconda3/envs/localstack/bin/localstack", line 79, in <module>
    infra.start_infra()
  File "/Users/<username>/miniconda3/envs/localstack/lib/python3.6/site-packages/localstack/services/infra.py", line 362, in start_infra
    raise e
  File "/Users/<username>/miniconda3/envs/localstack/lib/python3.6/site-packages/localstack/services/infra.py", line 308, in start_infra
    load_plugins()
  File "/Users/<username>/miniconda3/envs/localstack/lib/python3.6/site-packages/localstack/services/infra.py", line 102, in load_plugins
    file_path = '%s/%s/plugins.py' % (module[0].path, module[1])
AttributeError: 'zipimport.zipimporter' object has no attribute 'path'

object has no attribute 'path' の対策:

--- envs/localstack/lib/python3.6/site-packages/localstack/services/infra.py    2017-07-20 19:57:18.000000000 +0900
+++ envs/localstack/lib/python3.6/site-packages/localstack/services/infra.py    2017-07-20 20:22:30.000000000 +0900
@@ -96,7 +96,8 @@
         if six.PY3 and not isinstance(module, tuple):
             file_path = '%s/%s/plugins.py' % (module.module_finder.path, module.name)
         elif six.PY3 or isinstance(module[0], pkgutil.ImpImporter):
-            file_path = '%s/%s/plugins.py' % (module[0].path, module[1])
+            if hasattr(module[0], 'path'):
+                file_path = '%s/%s/plugins.py' % (module[0].path, module[1])
         if file_path and file_path not in loaded_files:
             load_plugin_from_path(file_path)
             loaded_files.append(file_path)

最初は jar ファイルのダウンロードが行われるため、立ち上がるまでかなり時間がかかるがじっと待つ。

(localstack) $ localstack start
Starting local dev environment. CTRL-C to quit.
Starting mock ES service (http port 4578)...
Starting local Elasticsearch (http port 4571)...
Starting mock S3 (http port 4572)...
Starting mock SNS (http port 4575)...
Starting mock SQS (http port 4576)...
Starting mock SES (http port 4579)...
Starting mock API Gateway (http port 4567)...
Starting mock DynamoDB (http port 4569)...
Starting mock DynamoDB Streams service (http port 4570)...
Starting mock Firehose service (http port 4573)...
Starting mock Lambda service (http port 4574)...
Starting mock Kinesis (http port 4568)...
Starting mock Redshift (http port 4577)...
Starting mock Route53 (http port 4580)...
Starting mock CloudFormation (http port 4581)...
Starting mock CloudWatch (http port 4582)...
WARNING:infra.py:Service "elasticsearch" not yet available, retrying...
WARNING:infra.py:Service "elasticsearch" not yet available, retrying...
WARNING:infra.py:Service "elasticsearch" not yet available, retrying...
Ready.

初期状態では、DynamoDB は -inMemory オプションがついた状態で起動され、localstack を停止するとデータも消滅する。
データを保存するためには、環境変数 DATA_DIR をセットしておく。
この環境変数をセットすると Kinesis と Elasticsearch のデータ保存場所もこのディレクトリの中に変更される。

conda 環境では、activate および deactivate を実行したときに、environment の etc/conda/activate.d/*.sh あるいは etc/conda/deactivate.d/*.sh が自動的に source されるので、自動的にこの環境変数をセットするようにフックを作成する。

ここでは、environment の data を DATA_DIR に指定している。

(localstack) $ cd $CONDA_PREFIX
(localstack) $ mkdir data
(localstack) $ mkdir -p etc/conda/activate.d
(localstack) $ mkdir -p etc/conda/deactivate.d
(localstack) $ echo 'export DATA_DIR=$CONDA_PREFIX/data' >etc/conda/activate.d/env_vars.sh
(localstack) $ echo 'unset DATA_DIR' >etc/conda/deactivate.d/env_vars.sh

いったん deactivate して、再度 activate すれば、環境変数 DATA_DIR がセットされているはずである。

(localstack) $ . deactivate
$ . activate localstack
(localstack) $ echo $DATA_DIR
/Users/<username>/miniconda3/envs/localstack/data

この状態で localstack を起動すれば、DynamoDB のデータも localstack の停止後も消えずに残るようになる。

続きを読む

awsでgolangのサーバーを立てる & basic認証

ec2のインスタンスを立てるところとgolangのプログラムは割愛させていただきます。
主に、apacheの設定周りの説明をします

apacheの設定

sudo yum install httpd
sudo chkconfig httpd on
sudo /etc/init.d/httpd start

basic認証を設定する
– hogeの部分がbasic認証のユーザ名になります。
– パスワードを聞かれるので、任意のパスワードを設定します
– 複数のユーザを設定したい場合は、-cを抜いてhoge2みたいな感じでユーザを追加します
余談ですが、chromeでbasic認証のテストをする場合、cmd + shift + n でシークレットウィンドウを開いてテストすると楽です

sudo htpasswd -c /etc/httpd/.htpasswd hoge
  • httpd.confを編集します。hoge.com部分はサーバ名が決まったらそれを
  • ServerNameに指定してください。
  • golangはport8888で設定するものとします

※最低限の設定なので、最低限の設定としてご参照ください

sudo vim /etc/httpd/conf/httpd.conf 

////httpd.confの中身

//ServerNameを変更する。
ServerName hoge.com:80

//VirtualHostを追加する
<VirtualHost *:80>
  ProxyPreserveHost On
  ProxyRequests Off
  ServerName hoge.com:80
  ServerAlias hoge.com
  ProxyPass / http://localhost:8888/
  ProxyPassReverse / http://localhost:8888/
  <Proxy *>
   AllowOverride all
   Order deny,allow
   Allow from all
   AuthType Basic
   AuthUserFile /etc/httpd/.htpasswd
   AuthName "Please enter your ID and password"
   Require valid-user
  </Proxy>
</VirtualHost>

あとは、簡易なgolangのサーバを立てて、サーバをポート8888で設置して、

go run app.go

を実行すればOKです。#app.goは任意です

続きを読む

AWS Lambda と Fastly instant purge を利用して S3 コンテンツの更新を即時反映する

Fastly CDN の特徴のひとつに、Instant purge(コンテンツの即時削除)があります。通常、CDN では更新の多いコンテンツはキャッシュすることができませんが、Fastly では Purge を即座に行うことが可能であるため、更新の多いコンテンツでもキャッシュすることが可能です。
ただし、コンテンツが変更されるたびに Instant purge を手動で行うのは理想的な運用とはいえません。ここでは AWS Lambda を利用して、S3 でのファイル更新を検知して、自動的に Instant purge を実行する方法を紹介します。

Lambda の設定

  1. Services から Lambda を選択し、Create a Lambda function をクリックします
  2. blueprint が表示されますが、ここでは Blank Function を選択します
  3. トリガーの設定画面が表示されるので、S3 を選択し、Bucket にコンテンツが配置される S3 バケットを選択し、Event type を PUT とします
    Screen Shot 0029-07-18 at 21.06.54.png
  4. Name / Description を入力し、Runtime はデフォルトの Node.js を選択します。
    Screen Shot 0029-07-18 at 21.11.26.png
  5. Code は下記のように設定します。hostname を Instant purge を行うドメインに変更してください。
    下記のコードでは、アップデートされたパスを key に格納し、options にドメイン、ポート番号、method(purge のため、PURGE) と共に設定し、http.request で Instant purge のリクエストを送信しています。
console.log('Loading event');
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var http = require('http');

exports.handler = function(event, context) {
   console.log('Received event:');
   console.log(JSON.stringify(event, null, '  '));
   // Get the object from the event and show its content type
   const bucket = event.Records[0].s3.bucket.name;
   const key = event.Records[0].s3.object.key;
   s3.getObject({Bucket:bucket, Key:key},
      function(err,data) {
        if (err) {
           console.log('error getting object ' + key + ' from bucket ' + bucket + 
               '. Make sure they exist and your bucket is in the same region as this function.');
           context.done('error','error getting file'+err);
        }
        else {
           console.log('CONTENT TYPE:' + data.ContentType + ' bucket: ' + bucket + ' path: ' + key);
           context.done(null,'');
        }
      }
   );

  var options = {
    hostname: 'www.example.com',
    port: 80,
    path: '/' + key,
    method: 'PURGE',
  };

  callback = function(response) {
  var str = '';

    //another chunk of data has been recieved, so append it to `str`
    response.on('data', function (chunk) {
      str += chunk;
    });

    //the whole response has been recieved, so we just print it out here
    response.on('end', function () {
      console.log(options.hostname + ':' + options.port + options.path + ' method:' + options.method);
      console.log(str);
    });
  }

  http.request(options, callback).end();
};

6 . Role は Choose an existing role を選択し、Existing role を選択します。
Screen Shot 0029-07-18 at 21.14.43.png

以上で設定は完了です。

動作確認

ここでは index.html ファイルを更新し、パージによりキャッシュ MISS となることを確認します。
1. curl コマンドでキャッシュヒットになることを確認します。

$ curl -svo /dev/null http://www.example.com/lambda
...
< Age: 3
...
< X-Cache: MISS, HIT
< X-Cache-Hits: 0, 1

2 . s3 上の lambda ファイルを更新します
3. curl コマンドでキャッシュミスになることを確認します。

$ curl -svo /dev/null http://www.example.com/lambda
...
< Age: 0
...
< X-Cache: MISS, MISS
< X-Cache-Hits: 0, 0

4 . CloudWatch のログでも下記のように PURGE リクエストが送信されていることが確認できます。
Screen_Shot_0029-07-18_at_22_40_08.png

続きを読む

TerraformでAWSの構成を読み取るぞ(Terraform import)

前書き

Terraform import については、Qiitaでお2人も記載してくれていますので、参照ください。

tyasuさん:terraform importの使い方メモ
kt_higaさん:terraform importを試してみた

同じスタンスで記載してもしょうがないので、勉強している最中に違う観点でいくつかTerraform import について記載します。

環境

Windows にTerraform(2017/7/18時点:v0.9.11)をインストール
AWSには、EC2,ELB,RDS,S3 など以前手作業で作った環境があります。

準備

まずは、最低限Terraformの利用には、access_key, secret_keyが必要です。
こちらを参照してキーを確認しましょう。
Windowsの作業フォルダにファイルを作成して、確認したキーを記載します。
※ ファイル名は、<任意の名前>.tf としてください。
ここでは、c:work フォルダを作業フォルダとします。

provider "aws" {                                            ・・・・決め打ちです。
    region      = "ap-northeast-1"                          ・・・・リージョンを記載(ap-notheast-1:日本リージョン)
    access_key  = "XXXXXXXXXXXXXXXXX"                       ・・・・access_keyを登録
    secret_key  = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" ・・・・secret_keyを登録
}

※ ほとんどが決め打ちです。
   今回は、AWS環境のため"aws" となります。

インポート!

叩くコマンドは、簡単!

AWSのマネジメントコンソールから、インスタンスID(赤枠部分)を確認して、コマンドのオプションに指定します。
C:> cd work
C:work> terraform import aws_instance.<任意の名前> <インスタンスID>

題.png

ここでは、こんな感じでコマンドを叩いてみました。
もともとの環境では、タグ名を「batchserver01」という名前を付けていましたが、ここではわざと「batch01」に変更しています。
タグ名と合わせても勿論OKです。

import02.png

無事、terraform.tfstate ファイルが作成されました。

※ <> 部分は、修正しています。

{
    "version": 3,
    "terraform_version": "0.9.11",
    "serial": 0,
    "lineage": "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_instance.batch01": {
                    "type": "aws_instance",
                    "depends_on": [],
                    "primary": {
                        "id": "<INSTANCE_ID>",
                        "attributes": {
                            "ami": "<AMI_ID>",
                            "associate_public_ip_address": "false",
                            "availability_zone": "<AvailabilityZone_ID>",
                            "disable_api_termination": "false",
                            "ebs_block_device.#": "0",
                            "ebs_optimized": "false",
                            "ephemeral_block_device.#": "0",
                            "iam_instance_profile": "",
                            "id": "<INSTANCE ID>",
                            "instance_state": "running",
                            "instance_type": "<INSTANCE_TYPE>",
                            "ipv6_addresses.#": "0",
                            "key_name": "<KEY_NAME>",
                            "monitoring": "false",
                            "network_interface.#": "0",
                            "network_interface_id": "<NW_INTERFACE_ID>",
                            "primary_network_interface_id": "<NW_INTERFACE_ID>",
                            "private_dns": "<DNS_NAME>",
                            "private_ip": "<PRIVATE_IP>",
                            "public_dns": "",
                            "public_ip": "",
                            "root_block_device.#": "1",
                            "root_block_device.0.delete_on_termination": "false",
                            "root_block_device.0.iops": "100",
                            "root_block_device.0.volume_size": "10",
                            "root_block_device.0.volume_type": "gp2",
                            "security_groups.#": "0",
                            "source_dest_check": "true",
                            "subnet_id": "<SUBNET_ID>",
                            "tags.%": "1",
                            "tags.Name": "batchserver01",
                            "tenancy": "default",
                            "volume_tags.%": "0",
                            "vpc_security_group_ids.#": "1",
                            "vpc_security_group_ids.2148762998": "<SECURITY_GROUP_ID>"
                        },
                        "meta": {
                            "schema_version": "1"
                        },
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": "aws"
                }
            },
            "depends_on": []
        }
    ]
}

ここから本題

★ EC2を1台しかimportしていないので、もう1台importしたらどうなるんだ?

import01.png

別に問題なくコマンド終了。
先ほど作成されたterraform.tfstate は、terraform.tfstate.backup にCOPYされ、terraform.tfstate に2台目が追記されました。
(省略)

★ S3をimport しても大丈夫だよね?

C:work> terraform import aws_s3_bucket.<任意の名前> <バケット名>

わざとバケット名を間違えて見ました。

import03.png

★ ALBをimport !!

C:work> terraform import aws_alb.<任意の名前> <ARN>

import04.png

結果、terraform.tfstate ファイルにはどんどんimport すればしただけ、追記されていきました。
どんどん下に追記されていくわけではなく、DataSource(aws_s3_bucket, aws_instance, aws_albなど)の名前で順に並んでいくようです。

参考サイト

対応しているDataSource をはじめ、オプション確認にはやっぱりここは外せません。

本家HashiCorp:AWS Provider

続きを読む

API Gatewayを通じてKinesisにPOSTでデータを入れる方法

API GatewayにPOSTで送ったリクエストを直接Kinesisに入れる方法について解説します。

「あるURLにPOSTメソッドでデータを送ると、Kinesisにそのデータが入れることが可能な仕組み」

を作ることができます。

Kinesisにデータを入れる以外にも、参照/削除もできます。

以下の作業はすべて、AWSのコンソール画面から行うことを想定しています。

目次

  • IAMでロールの作成
  • Kinesis Streamsの作成
  • API Gatewayの設定
  • 確認
  • エラー集
  • 参考

目次としては、以上となります。AWSの3つのサービスを使用します。

IAMでロールの作成

本システムでは、「API Gateway」で設定するロールを一つ用意する必要があります。

IAM上で「AmazonKinesisFullAccess」ポリシーをもったロールを作成すれば大丈夫です。
適当に、KinesisProxyRoleと呼ぶことにします。

Kinesis Streamsの作成

続いて、Kinesisストリームを用意します。
AWSコンソール上で、「Kinesisストリームの作成」をクリックすると、以下の画面が表示されます。

スクリーンショット 2017-07-18 17.28.03.png

ストリームの名前をつけましょう。test_streamと呼ぶことにします。
この名前は慎重につけましょう。理由としては、このストリーム名がAPI GatewayのURLパターンで参照されるからです。

API Gatewayの設定

API Gatewayの管理画面を開きます。

APIの作成

[APIの作成]より、「新しいAPI」を選択し、API名を入力します。

スクリーンショット 2017-07-18 17.33.31.png

作るものを確認

さて、これからどういうAPIを作っていくかですが、次のようなURLに対してPOSTで送れるものを作ります。

http://<api gateway url>/<DeployされるStage名>/streams/<Stream名>/record

/streamsの作成

これは、コンソール画面の[アクション] -> [リソースの作成]より、入力します。

入力内容
リソース名 Streams
リソースパス streams

ここでは特にメソッドを作成しなくて大丈夫です。

/streams/{stream-name}の作成

/streams配下に、以下のように入力します。

入力内容
リソース名 stream-name
リソースパス {stream-name}

この波括弧ですが、stream_nameというなめのパスパラメータを表しており、この部分に、先ほど作ったKinesisの名前を入れます。

このリソース配下でも、特にメソッドを作成しなくて大丈夫です。

/streams/{stream-name}/recordの作成

/streams/{stream-name}配下に、以下のように入力します。

入力内容
リソース名 Record
リソースパス record

このリソースでは[メソッドの作成]より、POSTメソッドを作成します。
次に、作成された[POST]という部分をクリックします。
表示された、図中の「統合リクエスト」をクリックします。

入力内容
統合タイプ サービス
AWS リージョン Kinesisと同じregion
AWS サービス Kinesis
AWS サブドメイン (空白)
HTTP メソッド POST
アクション PutRecord
実行ロール arn:aws:iam:123456xxxx:role/KinesisProxyRole

と設定します。

続いて同じ画面において、
[HTTP ヘッダー]で

名前 マッピング元
Content-Type ‘x-amz-json-1.1’

[本文マッピングテンプレート]で、「テンプレートが定義されていない場合」を選び、[マッピングテンプレートの追加]を押します。
Content-Typeとして、application/json と入力します。

そして、テンプレートの部分に以下をPasteしてください。

{
    "StreamName": "$input.params('stream-name')",
    "Data": "$util.base64Encode($input.json('$.Data'))",
    "PartitionKey": "$input.path('$.PartitionKey')"
}

以上で、設定は完了です。

ここまでの設定を確認するために、[リソース] -> [/recordのPOST]をクリックします。
表示された図中の[テスト]をクリックすると、

指定された入力で メソッドに対してテストコールを行います

という画面に遷移します。

{stream-name}に、test_streamを入力し、リクエスト本文に、

{
   "Data": "some data",
   "PartitionKey": "some key"
}

を貼ります。 [テスト]をクリックしましょう。

すると、

リクエスト: /streams/test_stream/record
ステータス: 200
レイテンシー: 121 ms

レスポンス本文

{
  "SequenceNumber": "1234.......................",
  "ShardId": "shardId-000000000000"
}

という形で、成功が確認できます。

CORSの設定が必要であれば、[アクション]より「CORSの有効化」を押せば設定されます。

確認するために、このAPIをデプロイしましょう。
[アクション] -> [APIのデプロイ]より、ステージを作成しましょう。今回は prodとしましょう。

確認

今回deployにより取得したURLは以下のようになります。

https://hogehoge.fugafuga.amazonaws.com/prod/streams/test_stream/record

こちらをcurlで叩いて見る場合は、以下のコマンドで叩けます。

$ curl -X POST https://hogehoge.fugafuga.amazonaws.com/prod/streams/test_stream/record -H 'Content-type: application/json'  -d '{"Data": "some data","PartitionKey": "some key"}'

JqueryのAjaxで送信する場合は以下のようにします。

js
var data = {"Data": "some data", "PartitionKey": "some key"};
$.ajax({
    type:"POST",
    url:"https://hogehoge.fugafuga.amazonaws.com/prod/streams/test_stream/record",
    data: JSON.stringify(data), // stringifyはお忘れなく
    scriptCharset: 'utf-8',
    contentType: 'application/json',
    success: function(data) {
    }
});

エラー集

やってるときにエラーに遭遇しそうなエラーに関する記述です。

[API Deploy時に] No integration defined for method

これは、

「HTTPメソッドを定義してるけど、integrationに関する情報がない」

というエラーです。つまり、GETやPOSTなどのメソッドを追加しているものの、「Integration Request」の情報の入力がされてないという状態です。

解決方法としては、「追加したもののIntegrationに関する情報を登録してないメソッドは削除する」です。

参考

https://forums.aws.amazon.com/thread.jspa?threadID=216710

[KinesisにRequest送信時に] 1 validation error detected: Value ” at ‘partitionKey’ failed to satisfy constraint: Member must have length greater than or equal to 1

これは、

「partitionKeyは1文字以上必要だ」

というエラーです。起こっているのは、おそらく、Kinesisに対する送信パラメータに「PartitionKey」が足りてない、もしくは、認識されてないということでしょう。

よくあるのは、JavaScriptでKinesisにパラメータを送る場合、PartitionKeyを入れ忘れることがあるので注意しましょう。

JavaScriptから送信する場合は、jsonオブジェクトを JSON.stringifyで文字列化しない場合、こういうエラーが出ます

参考

http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/integrating-api-with-aws-services-kinesis.html

続きを読む

SONY MESHの温度・湿度タグのデータをAWS Amazon ElasticSearchに送信してグラフ化する

1.概要

SONY MESHの温度・湿度タグで取得した値をクラウドに送信してグラフ化できるようにします。
クラウドはAWSのAmazon Elasticsearchを使用して、kibanaでデータを可視化します。
前提としてAWS環境はなるべく無料枠で収まる設定になるようにし、送信データ量も個人で試してみるレベルという想定で書いています。

全体のシステム構成としては以下のイメージになります。
system.png

2.環境

実施時の環境は以下の通りです。

2.1.AWS環境

Elasticsearchのバージョン 5.3
Kibana Version: 5.3.2

2.2.MESHアプリ利用環境

Ipad iOS 10.3.2
MESHアプリ ver. 1.8.0

3.手順

3.1.AWS環境

※AWSアカウントは開設済みとします

3.1.1. Amazon Elasticsearch環境の準備

Amazon Elasticsearch serviceを開き「新しいドメインの作成」を選択。

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-24-23_No-00.png

任意のドメイン名を入力する。ここでは「mydomain」と入力することにする。またElasticsearchのバージョンはデフォルトの5.3のままとする。

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-25-18_No-00.png

ノードの設定ではAWS無料利用枠に収まるようにインスタンスタイプを「t2.small.elasticsearch」にする。インスタンス数は1、ストレージタイプはEBSでデフォルトのまま。

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-25-38_No-00.png

EBSボリュームサイズは最小の「10」(デフォルト)とする。

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-25-43_No-00.png

アクセスポリシーの設定ではテンプレートの中から「特定のIPからのドメインのアクセスを許可」を選択。自身が使用しているグローバルIPアドレスを入力する。今回はあくまで個人で試すのが目的のため、利用想定としては自分の家からのみアクセスできればよしとする。なお複数のIPアドレスをリスト上で記載することも可能。

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-26-4_No-00.png

最後に設定内容に間違いがないか確認したら「確認」ボタンを押して完了。

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-26-30_No-00.png

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-26-34_No-00.png

3.1.2. Amazon Elasticsearchを利用する

Amazon Elasticsearch Serviceを開くと以下のように作成済みのドメインが表示される。

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-59-9_No-00.png

さきほど作成した「mydomain」をクリックすると、以下の画面に遷移し「エンドポイント」にElasticsearchのURLが「Kibana」にデータを可視化してくれるKibanaのURLが書いてある。「エンドポイント」のURLはMESHから温湿度データを送信する際にアクセスするURL。Kibanaはブラウザから温湿度のグラフを見たい時にアクセスするURL。

SnapCrab_Amazon Elasticsearch Service Management Console と 15 ページ ‎- Microsoft Edge_2017-7-14_14-59-14_No-00.png

3.1.3. Amazon ElasticSearchにデータを送信する

上記のエンドポイントに対してhttpでPOSTメッセージを送るとAmazonES(Elasticsearch)にデータを蓄えることができる。試してみたい場合、例えば以下のようにJSON形式でデータを送ればよい。

curl -XPOST https://search-mydomain-xxx.ap-northeast-1.es.amazonaws.com/indexname/typename/ ¥
-d '{"temp":26.5, "humid":63, "@timestamp":"2017-07-08T10:27:00+0900"}'

※温度をtemp、湿度をhumid、送信日時を@timestampとして格納する場合

3.1.4. AmazonESに送信したデータをKibanaで確認する

上記「Kibana」のURLにブラウザでアクセスすると、初回以下のような画面が表示される。
※表示されない場合はメニューの「Management」を選択

スクリーンショット 2017-07-18 13.44.26.png

ここでさきほどcurlコマンドで送信した時に指定したindex(今回は”indexname”)を指定すると「Time-field name」で「@timestamp」が以下のように選択できる。

indexname.png

そして「Create」する。
その後はメニューの「Discover」を選択することで送信されたデータ1つ1つを確認することができる。

以上でAWS側の準備は完了です。

3.2.MESHアプリ

次はMESHアプリ側での作業です。
MESHやMESHアプリの基本については以下を参照ください。
MESH
https://meshprj.com/jp/

MESH SDK マニュアル ※要SONY MESH SDKユーザーアカウント
https://meshprj.com/sdk/doc/ja/

MESHの温度・湿度タグを使い、MESH SDKを活用してカスタムタグを作成してAmazonESに温湿度データを送信します。
手順としては以下のようになります。

3.2.1. MESHアカウントで以下MESH開発サイトにサインインする

https://meshprj.com/signin/

3.2.2. サインインしたら「Create New Tag」を選択

スクリーンショット 2017-07-18 16.44.06.png

3.2.3. 湿度データ送信用のカスタムタグを作成

任意のカスタムタグの名前をつけます。ここではHumidMonitorとしました。
スクリーンショット 2017-07-18 18.03.06.png

「New Function」の左側にある三角を押して詳細設定を開いて、「Execute (0)」をクリック ※New Functionという記載は任意の名前に変更する。ここでは「sendToES」とします。
スクリーンショット 2017-07-18 16.45.54.png

ここにカスタムタグ実行時のプログラムを書きます
温度・湿度タグから受け取った湿度データをAmazonESに送信するコードは以下になります。

//endpoint
var endPointURL = "https://search-mydomain-xxxx.ap-northeast-1.es.amazonaws.com/indexname/typename/";

var nowdate=new Date();

//年・月・日・曜日を取得する
var year = nowdate.getFullYear();
var month = nowdate.getMonth()+1;
var day = nowdate.getDate();
var hour = nowdate.getHours();
var minute = nowdate.getMinutes();
var second = nowdate.getSeconds();

if ( month < 10 ) {
    month = "0" + month;
}
if ( day < 10 ) {
    day = "0" + day;
}
if ( hour < 10 ) {
    hour = "0" + hour;
}
if ( minute < 10 ) {
    minute = "0" + minute;
}
if ( second < 10 ) {
    second = "0" + second;
}

var nowtime = year + "-" + month + "-" + day + "T" + hour + ":" +
minute + ":" + second + "+0900";

log(nowtime + "  " + messageValues.humidity);

//APIを叩く際のパラメータ
//プロパティの値を渡す
var jsonData = {
    "humid" : messageValues.humidity,
    "@timestamp":nowtime
};

var sendData = JSON.stringify(jsonData);
log(sendData);

ajax ({
    url : endPointURL,
    type : "post",
    data : sendData,
    contentType : "application/json",
    dataType : "json",
    timeout : 5000,
    success : function ( contents ) {
        log("POST success");
        log(JSON.stringify(contents));
    callbackSuccess( {
        resultType : "continue"
        } );
    },
    error : function ( request, errorMessage ) {
        log("POST error");
        log("detail: " +  JSON.stringify(request));
        callbackSuccess( {
            resultType : "continue"
        } );
    }
});

return {
    resultType : "pause"
};

3.2.4. 温度データ送信用のカスタムタグを作成

同様に再度「Create New Tag」から実施し、温度データを送信するプログラムを書きます。以下がコードです。

//endpoint
var endPointURL = "https://search-mydomain-xxxx.ap-northeast-1.es.amazonaws.com/dataindex/thtype/";

var nowdate=new Date(); 

//年・月・日・曜日を取得する
var year = nowdate.getFullYear();
var month = nowdate.getMonth()+1;
var day = nowdate.getDate();
var hour = nowdate.getHours();
var minute = nowdate.getMinutes();
var second = nowdate.getSeconds();

if ( month < 10 ) {
    month = "0" + month;
}
if ( day < 10 ) {
    day = "0" + day;
}
if ( hour < 10 ) {
    hour = "0" + hour;
}
if ( minute < 10 ) {
    minute = "0" + minute;
}
if ( second < 10 ) {
    second = "0" + second;
}

var nowtime = year + "-" + month + "-" + day +  "T" + hour + ":" + minute + ":" + second + "+0900";

log(nowtime + "  " + messageValues.temperature);

//APIを叩く際のパラメータ
//プロパティの値を渡す
var jsonData = {
    "temp" : messageValues.temperature,
    "@timestamp":nowtime
};

var sendData = JSON.stringify(jsonData);
log(sendData);

ajax ({
    url : endPointURL,
    type : "post",
    data : sendData,
    contentType : "application/json",
    dataType : "json",
    timeout : 5000,
    success : function ( contents ) {
        log("POST success");
        log(JSON.stringify(contents));
        callbackSuccess( {
            resultType : "continue"
        } );
    },
    error : function ( request, errorMessage ) {
        log("POST error");
        log("detail: " +  JSON.stringify(request));
        callbackSuccess( {
            resultType : "continue"
        } );
    }
});

return {
    resultType : "pause"
};

3.2.5. iOSのMESHアプリ上で設定する

以下のようにタイマータグと組み合わせて、以下のようにMESHタグを構成する。

スクリーンショット 2017-07-18 15.59.37.png

タイマーは1分間隔としています。データ量と精度のバランスを見て間隔は調整してください。
作成したカスタムタグはMESHアプリ上でメニューからダウンロードできます。
※詳細はMESH SDKマニュアルを参照

以上でMESH側の作業は完了です。

3.3. Kibanaでグラフ化された温湿度データを確認する

上記AmazonESの「Kibana」のURLにブラウザでアクセスする。
「Visualize」を選択。
スクリーンショット 2017-07-18 17.25.50.png

今回は「Area chart」を選択。
スクリーンショット 2017-07-18 17.26.38.png

作成したindex(indexname)を選択し、グラフの詳細設定画面を開きます。
まずY軸(Y-Axis)を設定します。「Aggregation」は「Average」を選択肢、「Field」は「humid」とする。続いてX軸(X-Axis)を設定します。

スクリーンショット 2017-07-18 17.33.42.png

「Aggregation」は「Date Historgram」を選択肢、「Field」は「@timestamp」とする。

スクリーンショット 2017-07-18 17.34.12.png

そして上部にある再生ボタン?(右向き三角形のボタン)を押すと以下のようにグラフが表示されます。
スクリーンショット 2017-07-18 17.39.57.png

同様に温度の方もVisualizeの設定をします。

2つのグラフが作成できたら、最後にDashboardに作成したグラフを並べて一覧できるようにします。
Kibanaのメニューから「Dashboard」を選択し、「Add」から上記で作成したVisualizeを選択して、Dashboardに表示していきます。
これでKibanaのDashboardで温度と湿度のデータを一覧できるようになりました。
このDashboardのURLにアクセスすればいつでもグラフを確認できます。

4.まとめ

本稿ではMESHの温度・湿度タグのデータをAWSのAmazon Elasticsearchに送信し、Kibanaでグラフ化したデータをブラウザから閲覧できるようにする手順を書きました。

5.参考

Interface 2017年 7月号「IOTセンシング初体験」

続きを読む

GAになったLambda@Edgeを使ってSPAをSSR無しでOGPとかに対応させてみる

ということで、前回前々回の続きでServer Side Rendering無しでOGPとかに対応する試みその3です。

今回は先ほどGAになったLambda@Edgeを使ってみます。前回から時間が空いたのはLambda@Edgeが中々Previewを抜けず、またPreview中の情報公開は規約違反になってしまうためです。

Lambda@Edgeとは

Lambda@Edgeは簡単に言うとCloudFrontのいくつかのイベントをトリガーにLambda関数を実行する機能です。UAによって返すコンテンツを変えたりできるようになるので、ABテストとかに利用できます。今回はこれを使って、一般的なブラウザなら本来のアプリを表示し、それ以外の場合は静的なコンテンツを返すことでOGP対応してみます。

手順

今回のソースはここにあります。

Reactアプリを作る

前回同様create-react-appで作ります。

yarn create react-app edge-demo

必要なパッケージを追加

今回はreact-router-domとreact-helmetのみ使います。helmetはOGPとかに対応するだけなら必要ありませんが普通にアプリを表示させた場合にもヘッダを書き換えたいので入れてます。

yarn add react-router-dom react-helmet

ルートを作ってビルドする

前回とほぼ同じ内容なので省略しますが、HomeとAboutの二つのルートを作ります。

そしてyarn buildするとbuildディレクトリ以下に静的ファイルが出来上がります。前回のprerenderと違ってabout.htmlは作られずindex.htmlしか作られないのでこれをS3に配置しただけだとOGPの情報は拾えませんが、一旦これをS3に配置します。

Lambda関数を作る

AWS Lambdaのコンソールからviewer-requestをトリガーとした以下のような関数を作ります。

'use strict';

const whitelist = [
  'chrome',
  'crios', // chrome for ios
  'firefox',
  'fxios', // firefox for ios
  'googlebot', // since google bot can render javascript. TODO: update to react-router v4 on front
];

const isSupportedBrowser = (uas) => {
  if (uas && Array.isArray(uas) && uas.length > 0) {
    return uas.some(ua => whitelist.some(w => ua.value.toLowerCase().indexOf(w) !== -1));
  }
  return false;
};

exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;
  const ua = 'user-agent';

  if (!isSupportedBrowser(headers[ua])) {
    if (!request.uri || request.uri === '/' || request.uri.indexOf('index.html') !== -1) {
      request.uri = '/index.static.html';
    } else {
      request.uri += '.static.html';
    }
  }
  callback(null, request);
};

かなりいい加減ですが、Chromeの場合とgooglebotの場合はそのままアプリを表示、それ以外のUAの場合はパス+.static.htmlというファイルを表示するようにしています。
ちなみに静的ファイルのパスに書き換えるだけじゃなく、レスポンスの内容を動的に作って返すこともできますが、自動的にマッピングした静的ファイルを作成するような仕組みを考えているのでこのようにしました。
というか、この辺はどうするのがベストか模索中なのでオススメの方法があれば教えて頂けると嬉しいです。(というか今後出てくると思ってます)

.static.htmlを配置

上記でuriの書き換え先にしたindex.static.htmlとabout.static.htmlを配置します。

index.static.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Home</title>
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="Home">
  <meta name="twitter:description" content="description of Home">
  <meta name="twitter:image" content="http://path/to/image">
  <meta property="og:title" content="Home">
  <meta property="og:type" content="website">
  <meta property="og:url" content="http://path/to/this/url">
  <meta property="og:image" content="http://path/to/image">
  <meta property="og:description" content="description of Home">
</head>
<body>
  Home
</body>
</html>

about.static.htmlは省略します。

ブラウザから確認

まずはChromeで確認すると通常のアプリの画面が表示されます。画面自体は前回のこの画面と全く同じなので省略します。

Slackで確認

前回同様Slackに貼るときちんとOGPの情報を解釈してルートごとに表示してくれています。
でも前回と同じ画像にしてしまったので、前回のこのスクショと見た目が全く同じになってしまいました。別のテキストや画像にすれば良かったですね。

googlebot

ソースにある通りですが、googlebot向けには普通のアプリを返してます。これは前回まででgooglebotはネットワークリクエスト等のあるSPAでも問題なくindexできることが分かっているためと、クローキングと見なされる可能性があるためです。(確認はしていません)

所感

このやり方の利点は、既存AWSユーザはそのままCloudFront使い続けられることと、既存のアプリには手を加えずに対応できる(マッピングさせる処理などは除く)ことだと思います。一部のURLしか対応しなくて良い場合は楽な選択肢なんじゃないでしょうか。

ただCloudFrontでしか使えないので、AWSユーザ限定になってしまいますね。また、実際のLambda関数は特定のリージョンで動くので結局CDNの利点が損なわれてる気もします。この辺はその内改善されることを期待したいと思います。あと真面目にマッピングさせたり変更時にInvalidateさせたりするとそれなりに手間なので、素直にSSRするなり、Prerendering使うなりした方が良いって結論になるかもしれません。

最後に

ということで簡単ですがLambda@Edgeを使ってSSR無しでOGPに対応させる方法の紹介でした。これで一旦SSR無しのOGP対応のネタは終わりの予定です。

個人的には今回の用途ならNetlifyみたいなサービスが一番だと思いますが、Lambda@Edge自体は今回以外の用途にも色々応用できるので非常に強力なサービスだと思います。

ということで、参考になれば幸いです。
実運用等して気付いた点があれば追記したいと思います。

続きを読む