今まではre:invent等に参加しないと手に入れられなかったAWS IoT Buttonが、5/17にAmazon.comでトライアル販売を開始したの並行輸入して試してみました。
今回は、AWS IoT Button(以下Buttonとする)が元々Amazonから1ボタンで商品を購入できるAmazon Dash Buttonをベースにしているということで、それを模してDash Buttonが対応していない日本のAmazonから自動で商品を買えるような仕組みを作っています。
AWS IoT Button概要
AWS IoT ボタンは、Amazon Dash Button ハードウェアをベースにしたプログラミング可能なボタンです。このシンプルな Wi-Fi デバイスは設定が簡単で、特定のデバイス向けのコードを書くことなく、AWS IoT、AWS Lambda、Amazon DynamoDB、Amazon SNS、およびその他多くのアマゾン ウェブ サービスの使用を開始する開発者向けに設計されています。
クラウド内のボタンのロジックをコーディングして、アイテムのカウントまたはトラッキング、呼び出しまたは警告、何かの開始または停止、サービスのオーダー、またはフィードバックを提供するようにボタンのクリックを設定できます。
AWS公式より引用
https://aws.amazon.com/jp/iot/button/
なおこのボタンはAmazon.com(下記リンク参照)から購入できますが、2016年5/25(水)時点で次回発送が8/16となっており、届くのはだいぶ先になりそうです。夏休みの自由研究に良いかもしれません。
http://www.amazon.com/AWS-IoT-Button-Limited-Programmable/dp/B01C7WE5WM
構成
以下の様な振る舞いをします。
- AWS IoTに設定されたボタンを押下
- Lambda関数(node.js)がキックされる
- Lambda関数が子プロセスとしてPhantomJSを起動
- PhantomJS内で自動購入スクリプトが動作、最後にキャプチャをエビデンスとして残す
- キャプチャをS3に保存して終了
この後の段落ではLambda関数がどのようになっているのか、それを動かすためのIoT Buttonの設定について述べていきます。
Lambda関数の実装
GitHubにソースを置いたのでその解説です。
https://github.com/jsoizo/amazon-jp-auto-orderer
前提
node-lambdaを利用してプロジェクトを設定しています。これがないと動きません。
https://github.com/motdotla/node-lambda
これを利用することで、lambda関数のイベント実行をローカルでテストしたり、環境変数により環境ごとの値を設定させられたり、AWSに対してlambda関数をデプロイするのが容易になります。本来はlambda関数のデプロイ時に環境変数をセットすることは出来ませんが、node-lambdaがデプロイ用のビルド時に、.env
から読み取った値をソースの先頭に process.env['hoge']='fuga'
のようにセットする処理を追加したうえでデプロイしてくれることで、擬似的に実現しています。
参照 : https://github.com/motdotla/node-lambda/blob/master/lib/main.js#L263
よって、実装(index.js)もprocess.envから値を取得していますが、これを一般的なデプロイ方法と同じようにディレクトリをzipで固めてS3にアップしても、実行時に環境変数から値を読み取れず失敗してしまいます。
PhantomJSの準備
PhantomJSは実行環境毎に異なる実行ファイルがビルドされているので、
開発時(Mac)とLambdaからPhantomJSを起動するとき(Amazon Linux)では実行ファイルを分ける必要があります。
開発時に npm install phantomjs-prebuilt
すると node_modules/phantomjs-prebuilt/bin/phantomjs
がダウンロードされますが、これはLamdaから呼ぶときは動かせないので、明示的に置きなおすことにします。
wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
tar -xjvf ./phantomjs-2.1.1-linux-x86_64.tar.bz2
mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs ./
実装上も以下のように環境によって利用する実行ファイルを分けています。
var phantomJsPath;
If (mode === 'production') {
phantomJsPath = path.join(__dirname, 'phantomjs');
} else {
path.join(__dirname, 'node_modules', 'phantomjs-prebuilt', 'bin', 'phantomjs');
}
Lambda上のPhantomJSが日本語を扱えるようにするための対応
空のnpmプロジェクトを作成し、以下リンクを参考にfontconfigをビルドしてプロジェクト直下に配置します。今回は出来る限り動作環境と揃えるということで、ビルドをLambdaの実行環境と同じAMI(ami-383c1956)でEC2インスタンスを建てて行いました。
AWS LambdaでPhantomJS日本語フォント対応 | RCO アドテクLabブログ
プロジェクト最終形
このような形になります
% tree -L 1 -a
.
├── .env # 環境変数ファイル(ローカル用)
├── .git
├── .gitignore
├── deploy.env # 環境変数ファイル(本番用)
├── event.json # ローカル開発時のLambdaイベントのダミーデータ
├── fontconfig # ビルドしたfontconfig
├── index.js # Lambda関数本体
├── node_modules
├── package.json
├── phantomjs # 子プロセスとして起動するPhantomJSバイナリ
└── phantomjs-script.js # PhantomJSの実行処理
Amazonから自動購入する処理の実装
以下のようにPhantomJSを子プロセスとして起動します。
このとき, phantomjs-script.js
が必要とする値(ログインID/Pass, 商品URL, キャプチャ画像保存先)を引数として渡してあげます。
var childArgs = [
path.join(__dirname, 'phantomjs-script.js'),
amazonEmail,
amazonPass,
amazonItemUrl,
captureImagePath
];
var phantomExecution = childProcess.execFile(phantomJsPath, childArgs);
PhantomJSの中で実行している処理を steps
として定義し、100msec毎にページの読み込みが完了している場合は次のStepを実行、ということを繰り返しています。
var stepIndex=0;
var steps = [
function() {
// 処理したいこと その1
},
function() {
// 処理したいこと その2
}
// 以降何件でも
];
interval = setInterval(executeRequestsStepByStep, 100);
function executeRequestsStepByStep(){
if (loadInProgress == false && typeof steps[stepIndex] == "function") {
console.log("step " + (stepIndex + 1));
steps[stepIndex]();
stepIndex++;
}
if (typeof steps[stepIndex] != "function") {
console.log("test complete!");
phantom.exit();
}
}
今回は以下の様な順序でstepを組み立てました。
ログインや決済などのフォームの送信の実装として、フォームのIDを取ってきて submit()
するのではなく明示的に送信ボタンをクリックしています。これはどうもAmazon側のページの処理が送信ボタンのクリックイベントにフックしてアレコレしているからです。細くは追っていませんがそのようになっている模様です。
var steps = [
function() {
console.log("### Open Top Page ###");
page.open("https://amazon.co.jp", function(status){})
},
function() {
console.log("### Click Signin Button ###");
page.evaluate(function(){
document.getElementById("nav-link-yourAccount").click();
});
},
function() {
console.log("### Submit Login Form ###");
page.evaluate(function(email, password){
document.getElementById("ap_email").value = email;
document.getElementById("ap_password").value = password;
document.getElementsByName('signIn')[0].submit();
}, email, password);
},
function() {
console.log("### Open Item Page ###");
page.open(itemUrl, function(status){})
},
function() {
console.log("### Add To Cart ###");
page.evaluate(function(){
document.getElementById("add-to-cart-button").click();
})
},
function() {
console.log("### Proceed To Payment ###");
page.evaluate(function(){
document.getElementById("hlb-ptc-btn-native").click();
})
},
function() {
console.log("### Confirm Payment ###");
page.evaluate(function(){
document.getElementsByName("placeYourOrder1")[0].click();
})
},
function() {
page.render(captureImagePath);
}
];
デプロイ&イベント設定
AWSデプロイ
プロジェクトルートで node-lambda deploy
と実行することでデプロイされますが、 .env
ファイルの中身をそのままにAWSにデプロイされてしまします。本番環境(=AWS上)においてLambdaを実行する際、AWS_ACCESS_KEY_ID
や AWS_SECRET_ACCESS_KEY
、 AWS_ROLE_ARN
などのAWSの認証系の値は自動的に環境変数にセットされます。これらの値は手元のPCで実行する際には必要になりますが、本番環境時は不要なので排除するため、別のenvファイルを作ることとします。
MODE=production
AMAZON_JP_EMAIL={{Amazonアカウントのメールアドレス}}
AMAZON_JP_PASSWORD={{Amazonアカウントのパスワード}}
AMAZON_JP_ITEM_URL={{購入したい商品のURL}}
# 例: AMAZON_JP_ITEM_URL=http://www.amazon.co.jp/dp/B007V6MQJY
CAPTURE_IMAGE_PATH=/tmp/one_click_order.png
CAPTURE_BUCKET={{実行後のキャプチャを保存するバケット名}}
これを設定ファイルとして明示的に渡して以下のコマンドでデプロイすると、AWSに新規Lambda関数が作成されているはずです。
node-lambda deploy -f deploy.env
Lambda関数をIoT Buttonから実行する設定
長々と書いてきましたがココが本題です…
AWSコンソール上でデプロイしたLambda関数のページに行き、
Event Sources
→ Add event source
をクリックしてイベントソースを追加します。
Event source type
を AWS IoT
、 IoT type
を IoT Button
を選択し、 Device Serial Number
にButtonの下部に書かれたシリアルナンバーを入力し Generate certificate and keysを
クリックすると下図のような画面になります。 ここで、certificate PEMとprivate Keyをダウンロードします(ピンクの枠の箇所)
上図に示されたとおりの流れでButtonをセットアップ(=WiFiに接続&AWSに登録)していきます。
https://aws.amazon.com/jp/iot/button/ に記載されているのと同様の手順になります。
- Buttonを5秒間押してAP モードにする。このとき青色LEDが点灯
- PCのWiFi接続先として
Button ConfigureMed - xxx
というSSIDが表示されるので、コンソール上に表示されたパスワード(Buttonのシリアルナンバーの末尾8文字)で接続 - http://192.168.0.1/index.htmlを開く
以下の様な画面が表示されるので、WiFiネットワーク接続設定、AWS IoT設定を行う。
- WiFi系
- SSID : 接続したいWiFiネットワークのSSID
- Password : WiFiネットワークに接続するためのパスワード
- AWS IoT系
- Certificate : AWSコンソール上でダウンロードしてきたCertificate
- Private Key : AWSコンソール上でダウンロードしてきたPrivate Key
Configure
をクリックすると設定を適用し、緑色のLEDが点灯したら成功です。
※ CertificateとPrivate Keyについての解説は以下AWSのドキュメントを参照
http://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/identity-in-iot.html
Buttonのセットアップが完了したら、AWSコンソール上に戻り、Submitしたらイベントソースの追加が完了します。
あとはButtonを押すとLamda関数が実行され60秒くらいで完了し、いつもの注文完了メールがAmazonから届くはずです。もしもメールが届かない場合はS3に保存したキャプチャ、CloudWatchのログを参照しましょう。概ね以下の様なことが起こりえます。
- Botと判定されてログイン成功後にCAPTCHA画面が表示される
- 時間を置けば大丈夫なはずですが、必ずしも解決するとは限りません…
- 何度も連続で購入しすぎてAmazonから心配される画面が表示される
- こちらも時間を置けば大丈夫なはずです
デモ
Buttonを押すとLEDが青く光り、時間が経つと完了下記しとして緑色に光ります。
この時点ではLambdaの関数がキックされただけで、正常終了したかどうかはLambda側に依存します。
まとめ
IoT Buttonを利用したLamda関数の実行は、
- 任意のLamda関数を作る
- イベントソースとしてIoT Buttonを設定する
のように、やり方さえ覚えてしまえば非常に簡単に出来てしまいます。
AWS IoT Buttonを使うことで、ハードウェアを殆ど意識することなく物理的なボタンをシステムに組み込むことが出来るというのは、ソフトウェア開発者にとって非常に有用なオプションであります。
一方で、結局はButtonはトリガでしか無く、大事なのはLambdaにあるなと感じます。
今回PhantomJSを動かしたように、Lambda内で子プロセスを起動できるため実行時間の許す限りなんでも出来る(例えばgolangでビルドしたバイナリなど)ので、思っている以上にLambdaとして可能なことの範囲は広く、様々なことに応用できるはずです。
以上です。