Amazon Auroraの先進性を誰も解説してくれないから解説する

TL;DR;

  • Amazon AuroraはIn-Memory DBでもなくDisk-Oriented DBでもなく、In-KVS DBとでも呼ぶべき新地平に立っている。
  • その斬新さたるやマスターのメインメモリはキャッシュでありながらWrite-BackでもなくWrite-Throughでもないという驚天動地。
  • ついでに従来のチェックポイント処理も不要になったのでスループットも向上した。
  • 詳細が気になる人はこの記事をチェキ!

Amazon Aurora

Amazon AuroraはAWSの中で利用可能なマネージド(=運用をAWSが面倒見てくれる)なデータベースサービス。
ユーザーからはただのMySQL、もしくはPostgreSQLとして扱う事ができるのでそれらに依存する既存のアプリケーション資産をそのまま利用する事ができて、落ちたら再起動したりセキュリティパッチをダウンタイムなしで(!?)適用したりなどなどセールストークを挙げだすとキリがないけど、僕はAWSからお金を貰っているわけではないのでそこは控えめにしてAuroraでのトランザクションの永続性について論文から分かる範囲と想像で補った内容を説明していく。

Auroraのアーキテクチャ

AWSの公式資料を取ってこればいくらでもそれっぽい図はあるが、説明に合わせて必要な部分だけ切りだした。

aurora1.png

AZとはAvailability Zoneの事で、AWSのデータセンターで障害が発生した場合に別の故障単位になるよう設計されているユニットの事である。物理的には部屋が分かれているのか建物が分かれているのかわからないが、電源やスイッチは確実に系統が分かれておりミドルウェアのバージョンアップなども分かれているという。それをまたがる形でMasterが一つとSlaveが複数(最大15台)立ち上がる。MasterはDBに対する読み書き両方ができるが、Slaveは読み出ししかできない。典型的なWebサービスは読み出しが負荷の多くを占めるので読み出し可能な複製が複数容易できるのは理にかなっている。

このそれぞれのコンポーネントを繋ぐ矢印はRedo-Logを表している。Redo-Logとは「特定のページを書き換える操作とその内容」が記述されたDBログの最小単位である。一般にDBを複製すると言うと読み書きされるあらゆるデータが複製されるものであるがAuroraではこのRedo-Logしか複製しない点が面白い。論文中にTHE Log IS THE DATABASEとでっかく書いてあるのは恐らくこの辺に由来する。

Masterは普通のMySQL(もしくはPostgreSQL)サーバのように見えてユーザから読み書きがリクエストできる。
InnoDBの代わりのバックエンドのデータストアとして分散KVSが稼働しており、その分散KVSはAZを跨った6多重に冗長化されている。論文中ではKVSだなんて一言も書いていないがストレージバックエンドの説明として理解しやすいのであえてKVSに例える。
6多重のうち4つにまで保存できた段階で永続化完了と見なしユーザに返答する事でレイテンシの短縮を図っている。システムはいろんなノイズで遅れるが、全体の足を引っ張って律速するは決まってstrugglerであり、90パーセンタイルぐらいであれば圧倒的に機敏に返事を返してくるのは巨大システムの常である。
全部の複製が全く同じ情報を持っていないといけないので、仮にログをもらえず取りこぼした複製がいたとしてもMasterに聞き直さず複製同士でGossip通信を行って全部のログを全員が受け取るように取り計らう。
この辺の話はAWSの人の公式スライドにも腐るほど出てくるので僕は詳しく説明しない。

トランザクションの挙動の違い

どれかのDBにとって極端に都合が良いワークロードで比較しても単なるセールストークにしかならない。
複数の方式のDBが明白に異なる挙動をする典型例のワークロードとして「巨大テーブルの全部の行の特定のフラグを立てる」という一つのトランザクションを例に挙げて伝統的なDisk-Oriented DB・In-Memory DB・Auroraの動作を順を追って説明する。

update.png.png
SQL文としてはこんな感じである。

UPDATE table1 SET flag = true;

なおこのtable1はものすごく行数が多い(=縦に長い)とする。

Disk-Oriented DBの挙動

まず巨大テーブル全体を一気にメモリに置くアーキテクチャにはなっておらず、メモリ上に用意したデータベースページ領域にDisk上のDBの一部を複製してくる所から始まる。ここまではMySQLでもPostgreSQLでも同じはず。この文脈でのページとはDBの中身の一部が8KBぐらいの大きさ毎に詰め込まれた連続したメモリ領域であり、OSが提供するメモリぺージとは少し違う。

diskoriented1.png

Disk上のデータを直接一瞬で書き換えることは当然できないので、狭いメモリ空間でLRU等を用いて取り回しながら書き込みの終わった未コミットなダーティページをディスクに書き戻しながら進行する他ない。
だがそんなことをすると、その瞬間にDBのプロセスが強制終了してリスタートした時に未コミットなダーティページがディスク上から読み出し可能な状態で観測される恐れがある。そこで各DBは僕の知る限り以下の挙動をとる。

ARIES

進行しながらRedo-Undo logをディスクに永続化し、もし途中でシステムがリスタートした時はリカバリとしてUndo処理を行う。
aries.png
この図で言うとページ1は未コミットなトランザクションによって既に書き換えられているが、Undo-Redo Logの形で既にWALを永続化しているのでリカバリ可能でありダーティなページはそのままディスクに永続化して構わない。なので空いたスペースに次に更新したいページ5をフェッチしてくる事ができる。

PostgreSQL

 上書きは常に新たなバージョンでの追記操作であり、clogというデータで保存されているトランザクションステータスが commited でない限り読み出しできない。したがって痕跡は物理的にページに残るがデータベースのユーザからは不可視であり問題にならず、いずれバキュームされて物理的にも消失する。postgres.png
この図でいうと、ページ1は物理的にはダーティだが追記がされているだけでありclogのお陰で論理的に他のトランザクションから見えないのであればそのままディスクに永続化されても問題が発生しない。なのでバッファプールからページ1を追い出して、空いた領域にページ5を持ってくる事ができる。

MySQL

ibdataの中に更新前の値が保存されており、ディスクに書き戻される際にはそちらも永続化されるので、リスタート時のリカバリ処理でibdataとテーブルデータを突き合わせて可視なデータがユーザから見えるように整合性を保つ(詳しくないが多分)。

いずれにせよ、トランザクションが走りながらログを記述していく事は変わらない。

In-Memory DBの挙動

全部のデータがメモリに収まる前提を置いて良いのでこちらはだいぶシンプルに収まる。
進行途中でログを書き出す必要は無いし、バッファの中でLRU等を用いてどのページをディスクに書き戻すかなども心配しなくてよい。
トランザクションログを書き出すタイミングは典型的な実装としてはコミット時に一気に書き出す事が多いようだ。

inmemory.png

リスタート時はログデータをスキャンしてデータベースを再構築するので、ユーザから commit が命じられていないトランザクションはログにすら残っておらず、ダーティページはそもそも概念が存在しない。

Auroraの挙動

メモリにもローカルのディスクにもテーブル全体が入りきらない前提で設計されている。
トランザクションの都合上必要なページがMasterのメモリで運良くキャッシュできていない場合、KVSに問い合わせを行いページを持ってくる。
なお、KVSは物理的には6多重で保存しているが論理的には一つのデータが6重に保存されているだけなので論理的には1つのストレージ領域と考えて良い(RAID1を論理的には単一のHDD扱いするのと同様)のでそう書く。

走りながら当然ログも永続化していく。6多重で保存されるのはログも同じだ。驚くべき事にRedo-logしか保存していかない。

当然Masterのメモリには全データ乗らないので、どうにかして処理用にメモリを取り回す必要がある。
そこでMasterは一番使わないと判断したページをKVSに書き戻…さずに捨てる。  もう一度言う、捨てるのだ、キャッシュなのに。

aurora2.png

そんなことをしたらKVSに載ったページは古いままじゃないかと心配になるが、Auroraの分散KVSは単なるストレージではなくてAurora用の専用のロジックが駆動するインテリジェントな分散KVSである。
こいつらはMasterから受け取ったRedo-Logを必要に応じて手元のページに適用(Apply)していく事ができる1

aurora3.png

なんでせっかく作った更新済みPage1を捨ててまで新たにKVS側でログを適用し直すかというと、基本的にAWSにおいてMasterのCPUやネットワーク資源は限られたリソースである一方、KVS側のCPUは相対的に持て余したリソースであり安いこと。さらには後に述べるチェックポイントの簡潔さのために完全にこちら側に倒した設計を行っていると考えている。

Masterがページを問い合わせる場合、バージョン番号もセットで問い合わせるのでそこまでに投げつけたRedo-logをKVS側で適用した最新ホカホカのページが返ってくるのでMasterは手元のメモリに乗っているダーティなページを気兼ねなく任意のタイミングで捨てて構わない。問い合わせの際はトランザクションの識別子を入れて引いてくるので、読んではいけないDirtyなページを獲得することはない。Slaveがページを問い合わせる場合は必ず永続化されたバージョンのものだけを読むようにしている。
ついでに言うとSlaveのページはMasterが6多重な分散KVSの他にSlaveにもRedo-logを投げつける。それを受け取るたびに(恐らくKVSと同じようなロジックで)ログ適用を行い、最新のコミット済みデータが読めるようになっている。ここで気づいた人もいると思うが、MasterはSlaveにログを共有するがその完了を待つとは一言も書いていない。4/6のKVS永続化が完了した時点でユーザにコミットを報告してしまう。なのでMaster側で更新を確認したデータがSlave側で読めるようになるには若干のタイムラグが発生する可能性がある。いわゆるSequential Consistencyである。ミリ秒オーダーなのでHTTPなWebサービスの文脈で大問題になるケースは稀だが覚えておいた方がいいかも知れない。

チェックポイントの挙動の違い

Auroraはシステム全体で見ると、Masterがせっかく更新したページをそのまま複製せずにKVSがログリプレイして再構築する分CPUクロックは無駄になっている。しかし、Masterはページを書き戻す必要が無くなり、更に言うとMasterがチェックポイント処理をする必要もなくなった。なぜならチェックポイント処理は分散KVS側で継続的にページ単位で実施されているからだ。なんだこれは。In-MemoryDBでもDisk-Oriented DBとも違うチェックポイントアーキテクチャだ。それぞれのチェックポイント戦略をここに列挙する。

  • ARIES: Checkpoint-Begin をWALに書いてからその瞬間のDirty Page TableとTransaction Tableを保存して、リスタート時のRedo-Log適用開始ポイントを算出可能にする。
  • MySQL: ダーティなページをディスクに書き出す。ページの境界とブロックストレージのページ境界が一致しない事のほうが普通なのでチェックポイント中に電源が落ちたらページの一部が中途半端に永続化されてしまう。そこで二度書く事によってアトミック性を達成する(Double Write と呼ぶ)
  • PostgreSQL: ダーティなページをディスクに書き出す。ページの境界とブロックストレージのページ境界が一致しない事のほうが普通なのでチェックポイント中に電源が落ちたらページの一部が中途半端に永続化されてしまう。そこでそのチェックポイント後に最初にそのページに触るWALの中にページ(デフォルトで8KB)を丸っと埋め込んで完全性を保障する。
  • In-Memory DB: どこかのタイミングでメモリの内容をモリッとディスクに書き出してリスタート時に整合性を直すSiloRとか、ログを並列スキャンして完全なイメージを生成するFOEDUSとか戦略はまだ多岐に渡っている。
  • Aurora: バックエンドのAuroraストレージが自動でログを適用していく。ページごとにログバッファが付いてて、バッファの長さがしきい値を超えるたびにページへのログ適用が実施される。ログは未コミットのトランザクションの進行中のログも含むがMasterがリスタートしている時点でそのトランザクションはそれ以上進むはずがないのでログを切り詰める(Truncate)。その際には最新の永続化済みのコミット完了のLSNまで復旧する。なおこの復旧処理はMasterが元気に進行している最中であってもバックグラウンドで良しなに実行される。ここのバックグラウンド処理とチェックポイントに差がないのがAuroraの学術的新規性の一つだと思う。

ベンチマーク結果

論文から抜粋すると
bench.png
大きめのインスタンスの場合に性能向上の伸びしろが大きいようだ。

その他

なんか他に工夫ないの

ログ処理周りは大胆に手が加えられており、中でも感心したのはFlush Pipeliningが実装されている。
通常、ログが永続化されるのを待つにはロガーにログ内容を渡して、完了が報告されるまでセマフォなどで寝るのが典型的な実装パターンである。しかしAuroraではロガーにログ内容を渡した後に、クライアントに完了を報告せよというキューに依頼を投げ込むだけで、そのスレッドは即座に次のリクエストを捌く処理に移行する。ログを4/6多重で保存した後で、キューの中身を確認する専用のスレッドが居て、今回永続化されたログのLSNとキューに登録された依頼を見比べて、永続化されたコミットの完了をクライアントに報告する。
PostgreSQLでもpgbenchでベンチマークを取ってイジメてみるとすぐにセマフォ処理近辺がボトルネックになるのでこの辺弄っても良さそうな気がするが大改造になるのでコミュニティには歓迎されない気がする。

Aurora Multi-Masterってどうなの

この論文で解説されてる仕組みだとLSNの発行からして複数台のマシンからやってダメなのでログのフォーマットのレベルで改造が加えられてそうな気がする。詳しくは動画で
https://www.youtube.com/watch?time_continue=2620&v=rPmKo2g9znA
どうやらパーティション単位で「テーブルのこの範囲はサーバAがリーダーね!」的に分割統治してMasterを複数用意するようだ。そして自分がMasterじゃないテーブルには一応書き込めるが最終的には調停者が決定するとの事である。更新が競合している場合はWrite性能は上がらないが競合していない場合は性能はよく伸びるらしい。

どんな更新がこれから来るかな

分散KVS側のCPUが安くて空いていて、そいつが保存しているページ内容に対してredo-logを適用できる程度に中身を解釈して動いているので、そいつらに集計系クエリを実行させるのはコストメリットが良さそう。貧者のOLAPとしてJOINが苦手なDB実装がクローズドな世界に君臨する可能性はあると思っている。もしくはredo-logをRedshiftにそのまま投げつけていってSlaveの一つとして稼働するようになるとか。

まとめ

  • Auroraは投げつけられたRedoログをストレージ側でバックグラウンドで適用できるからMasterの負担が減った。なので性能が伸びるようになった。
  • インテリジェントな分散ストレージすげーな!

  1. この処理を6多重全部でやると重いので実は一部のマシンでしかこのApply操作はしないらしい。 

続きを読む

Raspberry Pi 3 でAlexaと対話する

re:Invent 2017AlexaをRaspberryPi経由で利用する方法のワークショップに参加した。

ワークショップでは、構成済みのMicroSDを使って、単純にコマンドを流したり、支持されたようにファイルを書き換えたりしただけだったので、まっさらなMicroSDの状態から同様に動くようになるまでをやってみたいと思う。

用意するもの

Raspberry Pi 3 Model B
USBマイク < 多分これ
イヤホン
USBキーボード
USBマウス
HDMIケーブル
ディスプレイ
MicroSD Class10 16G

事前準備(Macでの作業)

インストーラーのダウンロード

こちらからNOOBSをダウンロードする
今回ダウンロードしたバージョンは v2.4.5

MicroSDにコピーする

ダウンロードしたNOOBS_v2_4_5.zipを解凍しFAT32でフォーマット済みのMicroSDにファイルをコピーする。

コマンド例
cp -a Downloads/NOOBS_v2_4_5/* /Volumes/UNTITLED

Raspbianのインストール

先ほどのMicroSDをRaspberry Piに刺して電源を入れるとインストーラーが立ち上がるので、Raspbian [RECOMMENDED]にチェックを入れて、左上のInstallをクリックしてインストールすればOK



事前準備(Raspberry Piでの準備)

一応OSのバージョンチェック

こんな記載があるので、Raspbian Stretchかどうか一応チェック

This guide provides step-by-step instructions to set up the Alexa Voice Service (AVS) Device SDK on a Raspberry Pi running Raspbian Stretch with Desktop

$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 9 (stretch)"
NAME="Raspbian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

ネットワークに接続

有線LANもあるので有線でつなげる場合には、何もしなくて大丈夫
Wi-Fiを利用する場合には右上にマークがあるのでそこから接続設定する

SSH/VNCの有効化

有効化しなくてもOKだけど、リモートから行える作業はできるだけリモートで行うと楽なので有効化しておく


ビルド環境の構築+ビルド

基本的にはこちらにある通りに行う
https://github.com/alexa/avs-device-sdk

依存関係のあるライブラリ、AVS Device SDK, Sensory wake word engine のインストール

Sensory wake word engineはオープンソースは非商用限定のライセンスなので注意すること

cd /home/pi/
mkdir sdk-folder
cd sdk-folder
mkdir sdk-build sdk-source third-party application-necessities
cd application-necessities
mkdir sound-files

sudo apt-get update

sudo apt-get -y install 
  git gcc cmake build-essential libsqlite3-dev libcurl4-openssl-dev 
  libfaad-dev libsoup2.4-dev libgcrypt20-dev libgstreamer-plugins-bad1.0-dev 
  gstreamer1.0-plugins-good libasound2-dev doxygen

cd /home/pi/sdk-folder/third-party
wget -c http://www.portaudio.com/archives/pa_stable_v190600_20161030.tgz
tar zxf pa_stable_v190600_20161030.tgz
cd portaudio
./configure --without-jack
make

pip install commentjson

cd /home/pi/sdk-folder/sdk-source
git clone git://github.com/alexa/avs-device-sdk.git

cd /home/pi/sdk-folder/third-party
git clone git://github.com/Sensory/alexa-rpi.git
cd ./alexa-rpi/bin/
./license.sh

ビルド

make のオプションは -j4までいけるけど、オーバーヒートに注意しましょう(と書いてある)

cd /home/pi/sdk-folder/sdk-build
cmake /home/pi/sdk-folder/sdk-source/avs-device-sdk 
  -DSENSORY_KEY_WORD_DETECTOR=ON 
  -DSENSORY_KEY_WORD_DETECTOR_LIB_PATH=/home/pi/sdk-folder/third-party/alexa-rpi/lib/libsnsr.a 
  -DSENSORY_KEY_WORD_DETECTOR_INCLUDE_DIR=/home/pi/sdk-folder/third-party/alexa-rpi/include 
  -DGSTREAMER_MEDIA_PLAYER=ON 
  -DPORTAUDIO=ON 
  -DPORTAUDIO_LIB_PATH=/home/pi/sdk-folder/third-party/portaudio/lib/.libs/libportaudio.a 
  -DPORTAUDIO_INCLUDE_DIR=/home/pi/sdk-folder/third-party/portaudio/include
make SampleApp -j2

Alexa Voice Serviceへの登録

先ほどビルドしたSampleAppからAlexa Voice Serviceへ接続するためには設定を作る必要があるので作成する。

Amazon Developerに登録

こちらからログインする
日本でもEchoが発売されたのでamazon.co.jpアカウントでもログインできるらしい

Alexa Voice Service に製品を登録する

!!! 一度作成した設定は削除できないようなので注意 !!!

Alexa Voice Service を選ぶ

CREATE PRODUCTを選ぶ

プロダクト情報を入力する

Product Name: 任意
Product ID: 任意(あとで使うのでメモっておくこと)
Is your product an app or device?: Device
Will your device use —–: 任意(Deviceを選ぶと出てくる)
Product category: 任意
Brief product description: 任意
How will end users —–: Hands-free
Upload an image: 任意
Do you intend to —–: No
Is this a children’s —–: No

セキュリティプロファイルの設定を作る

CREATE NEW PROFILEを選択すると、下にプロファイル名と説明を入れる欄が出るので、入力する。

URLの登録

次で使うので、Client ID, Client Secretをメモに取る
http://localhost:3000を入力してADDボタンを押す
同様にAllowed return URLsにはhttp://localhost:3000/authresponseを入力してADDボタンを押す

アプリケーション側に設定を反映

その前にvimのインストール

nano, edとかは入ってるけど辛いのでvimをインストール

sudo apt-get install -y vim

設定ファイルの書き換え

YOUR_CLIENT_SECRET, YOUR_CLIENT_ID, YOUR_PRODUCT_IDを先ほどメモったものに書き換えて以下を実行する

cat <<EOF >/home/pi/sdk-folder/sdk-build/Integration/AlexaClientSDKConfig.json
{
    "authDelegate":{
        "clientSecret":"YOUR_CLIENT_SECRET",
        "deviceSerialNumber":"123456",
        "refreshToken":"",
        "clientId":"YOUR_CLIENT_ID",
        "productId":"YOUR_PRODUCT_ID"
   },
   "alertsCapabilityAgent":{
        "databaseFilePath":"/home/pi/sdk-folder/application-necessities/alertsCapabilityAgent.db"
   },
   "settings":{
        "databaseFilePath":"/home/pi/sdk-folder/application-necessities/settings.db",
        "defaultAVSClientSettings":{
            "locale":"en-US"
        }
   },
   "certifiedSender":{
        "databaseFilePath":"/home/pi/sdk-folder/application-necessities/certifiedSender.db"
   },
   "sampleApp":{
       "displayCardsSupported":false
   }
}
EOF

音の設定

cat <<EOF >~/.asoundrc
pcm.!default {
  type asym
   playback.pcm {
     type plug
     slave.pcm "hw:0,0"
   }
   capture.pcm {
     type plug
     slave.pcm "hw:1,0"
   }
}
EOF

リフレッシュトークンの取得

先ほど書き換えず空のままだったrefreshTokenを取得する

まずは、下記コマンドを実行して認証用Webサーバーを立ち上げる

cd /home/pi/sdk-folder/sdk-build && python AuthServer/AuthServer.py

直接もしくはVNCを利用して、Raspberry Pi内のブラウザを立ち上げて http://localhost:3000/ にアクセスするとSign in to [セキュリティプロファイル名] using your Amazon accountのページにリダイレクトされるので、ログインする

ログインが完了すると立ち上げてWebサーバーにリダイレクトされて戻ってきて、The file is written successfully.
Server is shutting down, so you can close this window.
という表示がされている

サンプルアプリケーションの実行

$ cd /home/pi/sdk-folder/sdk-build/SampleApp/src
$ TZ=UTC ./SampleApp 
  /home/pi/sdk-folder/sdk-build/Integration/AlexaClientSDKConfig.json 
  /home/pi/sdk-folder/third-party/alexa-rpi/models
#############################
#       Connecting...       #
#############################

########################################
#       Alexa is currently idle!       #
########################################

///ここに大量のエラーが出ているけど、一旦動作には関係なさそうなので放置///

                  #    #     #  #####      #####  ######  #    #
                 # #   #     # #     #    #     # #     # #   #
                #   #  #     # #          #       #     # #  #
               #     # #     #  #####      #####  #     # ###
               #######  #   #        #          # #     # #  #
               #     #   # #   #     #    #     # #     # #   #
               #     #    #     #####      #####  ######  #    #

       #####                                           #
      #     #   ##   #    # #####  #      ######      # #   #####  #####
      #        #  #  ##  ## #    # #      #          #   #  #    # #    #
       #####  #    # # ## # #    # #      #####     #     # #    # #    #
            # ###### #    # #####  #      #         ####### #####  #####
      #     # #    # #    # #      #      #         #     # #      #
       #####  #    # #    # #      ###### ######    #     # #      #

+----------------------------------------------------------------------------+
|                                  Options:                                  |
| Wake word:                                                                 |
|       Simply say Alexa and begin your query.                               |
| Tap to talk:                                                               |
|       Press 't' and Enter followed by your query (no need for the 'Alexa').|
| Hold to talk:                                                              |
|       Press 'h' followed by Enter to simulate holding a button.            |
|       Then say your query (no need for the 'Alexa').                       |
|       Press 'h' followed by Enter to simulate releasing a button.          |
| Stop an interaction:                                                       |
|       Press 's' and Enter to stop an ongoing interaction.                  |
| Privacy mode (microphone off):                                             |
|       Press 'm' and Enter to turn on and off the microphone.               |
| Playback Controls:                                                         |
|       Press '1' for a 'PLAY' button press.                                 |
|       Press '2' for a 'PAUSE' button press.                                |
|       Press '3' for a 'NEXT' button press.                                 |
|       Press '4' for a 'PREVIOUS' button press.                             |
| Settings:                                                                  |
|       Press 'c' followed by Enter at any time to see the settings screen.  |
| Speaker Control:                                                           |
|       Press 'p' followed by Enter at any time to adjust speaker settings.  |
| Info:                                                                      |
|       Press 'i' followed by Enter at any time to see the help screen.      |
| Quit:                                                                      |
|       Press 'q' followed by Enter at any time to quit the application.     |
+----------------------------------------------------------------------------+

こんな感じで動いてとりあえずひと段落、Alexaと呼びかけるか、t + EnterでLinteningモードに入る
そうすると、以下のように、ステータスが遷移して、Speakingで話してくれるはず、、、はず、、、

############################
#       Listening...       #
############################

###########################
#       Thinking...       #
###########################

###########################
#       Speaking...       #
###########################

########################################
#       Alexa is currently idle!       #
########################################

あれれ?

音が出ない

音が出ない何故???と思ったら、ディスプレイ側のピンジャックにイヤホンを繋げたら声が聞こえる
ということはHDMI側に信号が流れてしまっているみ???

インターフェイスの優先度的な問題かと思い、HDMIを抜いたり、刺したり、設定変えたりしたけれども解決の糸口が見えないのでとっても困ってます

例えば、以下を実行してもやっぱりHDMI経由で音が出力される・・・

$ amixer cset numid=3 1
numid=3,iface=MIXER,name='PCM Playback Route'
  ; type=INTEGER,access=rw------,values=1,min=0,max=2,step=0
  : values=0

上の状況でYouTubeを確認したところ、ピンジャックからの出力になっていたので、ここの設定はAlexaには関係なさそう
また、ワークショップで使ったMicroSDを刺して起動した場合には、しっかりとピンジャックから音が出力されるので何らかの設定ミスの線が濃厚

RaspberryPiの知見も、Linuxで音を出す的な知見も全くないので、お手上げ状態です
どなたかわかる方いらっしゃったら教えてもらえたら嬉しいです

音が切れる

もう一つの問題として、Alexaの話が途中で切れることWhat's your nameときくとMy name is Aleという感じで途中で切れてしまう
これも設定で何とかできるんじゃないかと思っているけれども、今の所、どう調べたらいいものやらという感じで止まっている

音がピンジャックから出ない問題と同様に教えてもらえたら嬉しいです

助けてもらえるときのために、関連しそうな情報を末尾に載せておきます

今後のやりたいこと

単純にAlexaと声をかけて、声で操作するだけならEchoを使えばいいだけなので、すぐに思いつくようなことだけではあるけれども、以下のようなことを考えている

  • Wake word engineが独立しているので、Alexa以外の言葉に反応させられたら、専用で作る意味が出てくるので、試したい
  • Wake wordだけでなくコマンドから入力受付状態にできるので、受付システムとかできそうなので試したい

音の設定関連の情報

$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
  Subdevices: 8/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
card 0: ALSA [bcm2835 ALSA], device 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
$ aplay -L
null
    Discard all samples (playback) or generate zero samples (capture)
default
sysdefault:CARD=ALSA
    bcm2835 ALSA, bcm2835 ALSA
    Default Audio Device
dmix:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct sample mixing device
dmix:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct sample mixing device
dsnoop:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct sample snooping device
dsnoop:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct sample snooping device
hw:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct hardware device without any conversions
hw:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct hardware device without any conversions
plughw:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Hardware device with all software conversions
plughw:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Hardware device with all software conversions
$ amixer cset numid=3
numid=3,iface=MIXER,name='PCM Playback Route'
  ; type=INTEGER,access=rw------,values=1,min=0,max=2,step=0
  : values=0

続きを読む

愛テーマ 白浜・アドベンチャーワールド、来月8日まで /和歌山

… ホイールやメリーゴーラウンドなど4種類の乗り物が乗り放題となる。 ウィンターナイト(午後5時~7時半)の入場料は3000円(ナイトマリンライブ指定S席)と2500円(A~C席)。12、13、19、20日は休園。チケット購入はホームページ(http://www.aws-s.com)、問い合わせはナビダイヤル(0570・06・4481)へ。【藤田宰司】 … 続きを読む

カテゴリー 未分類 | タグ

Node.js × AWS Lambda アプリケーション開発Tips

これは AWS Lambda Advent Calendar 2017 の12日目の記事です。

先日の 東京Node学園祭2017 にて Lambda についてライトニングトークしてきました。

スクリーンショット 2017-12-12 12.47.14.png
Node.js × AWS Lambda アプリケーション開発Tips (SlideShare)

興味がある方は上記のリンクから内容を見て頂ければ。

今回はスライド中で一番時間をかけて書いた下記の図について、手前味噌ですがなかなかよく書けたんじゃないかなと思うので、紹介させていただきます^^

スクリーンショット 2017-12-12 12.55.54.png

Lambda は図の左上のように AWS のイベント経由で起動する他に、左下のように HTTP 経由で起動することができます。Lambda は AWS 内の各種リソースにアクセスできるのはもちろんなのですが、Lambda からHTTPで外に出られるので、インターネット上の任意のサービスにアクセスできます。また HTTP 経由でなくても定期実行(CloudWatch イベント)ができるので、定期的に何かをする、といった用途にも使えます。

インフラにがっつり AWS を使ってなければ、Lambda は馴染みがないだろうと思われがち?なのですが、HTTP 経由で Lambda を利用して、必要あらばLambda から HTTP 経由で他のサービスを利用する、といったことは、がっつりAWSを利用するといった用途でなくてもサーバの管理が必要ないアプリケーションの実行環境として利用できるのかなと思います。スライドの中でも触れてるように、(Amazon Linux 上で動かせる)任意の実行ファイルも動かせます。

少しでも AWS Lambda に可能性を感じ、AWS Lambda を触ってみるキッカケになれば幸いです。それでは!

続きを読む

「私、いくつに見える?」への返答機能をRoBoHoNに実装する(AWS Rekognition連携)

モバイル型ロボット電話 RoBoHoN に、AWS Rekognition APIを連携させ、ユーザーからの「私、いくつに見える?」という問いかけに返事をできるように実装しました。

キャプチャ.PNG

環境

Windows 7 SP1 64bit
Android Studio 2.3.1
RoBoHoN_SDK 1.2.0
RoBoHon端末ビルド番号 02.01.00
AWS SDK for Android 2.6.9

AWS Mobile SDK for Android 導入

  • 導入要件

 Android 2.3.3 (API Level 10) or higher

RoBoHoN は Android 5.0.2 (API Level 21 (Lollipop))

  • 参考

[AWS] Set Up the AWS Mobile SDK for Android
http://docs.aws.amazon.com/mobile/sdkforandroid/developerguide/setup.html

  1. Get the AWS Mobile SDK for Android.
  2. Set permissions in your AndroidManifest.xml file.
  3. Obtain AWS credentials using Amazon Cognito.

[qiita] Amazon Rekognitionで犬と唐揚げを見分けるアプリを作ってみた
https://qiita.com/unoemon/items/2bdf933127b6e225d036

  • 追加ライブラリ
compile 'com.amazonaws:aws-android-sdk-core:2.6.9'
compile 'com.amazonaws:aws-android-sdk-rekognition:2.6.9'
compile 'com.amazonaws:aws-android-sdk-cognito:2.6.9'

RoBoHoN実装

RoBoHoN独自の発語用 VoiceUI 、撮影用ライブラリ、そして Rekognition 通信を行き来をする実装を書いていきます。

  • Rekognition 通信部分参考

[AWS] Documentation » Amazon Rekognition » 開発者ガイド » Amazon Rekognition の開始方法 » ステップ 4: API の使用開始 » 演習 2: 顔の検出 (API)
http://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/get-started-exercise-detect-faces.html

  • インターネット疎通とストレージパーミッション
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • RoBoHonシナリオ関連定義類
ScenarioDefinitions.java
/**
*  Guess my age? シーン、accost、通知設定
*/
public static final String SCN_CALL = PACKAGE + ".guess.call";
public static final String SCN_RES = PACKAGE + ".guess.res";

public static final String ACC_CALL = ScenarioDefinitions.PACKAGE + ".guess.call";
public static final String ACC_RES = ScenarioDefinitions.PACKAGE + ".guess.res";


public static final String FUNC_CALL = "guess_call";
public static final String FUNC_RES = "guess_res";


/**
* memory_pを指定するタグ
*/
public static final String MEM_P_RES = ScenarioDefinitions.TAG_MEMORY_PERMANENT + ScenarioDefinitions.PACKAGE + ".res"; 
  • HVML(ユーザーからの呼びかけで起動、年齢判定のための写真撮影の声かけ、年齢判定を通知する)
home.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.rekog</producer>
        <description>いくつにみえる?のホーム起動シナリオ</description>
        <scene value="home" />
        <version value="1.0" />
        <situation priority="78" topic_id="start" trigger="user-word">${Local:WORD_APPLICATION} eq
            いくつにみえる
        </situation>
        <situation priority="78" topic_id="start" trigger="user-word">
            ${Local:WORD_APPLICATION_FREEWORD} eq いくつにみえる
        </situation>
    </head>
    <body>
        <topic id="start" listen="false">
            <action index="1">
                <speech>${resolver:speech_ok(${resolver:ok_id})}</speech>
                <behavior id="${resolver:motion_ok(${resolver:ok_id})}" type="normal" />
                <control function="start_activity" target="home">
                    <data key="package_name" value="com.dev.zdev.rekog" />
                    <data key="class_name" value="com.dev.zdev.rekog.MainActivity" />
                </control>
            </action>
        </topic>
    </body>
</hvml>
rekog_call.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.rekog</producer>
        <description>Guess my age? 撮影呼びかけ</description>
        <scene value="com.dev.zdev.rekog.guess.call" />
        <version value="1.0" />
        <accost priority="75" topic_id="call" word="com.dev.zdev.rekog.guess.call" />
    </head>
    <body>
        <topic id="call" listen="false">
            <action index="1">
                <speech>お顔をよーく見せて……。写真を撮るよ…。<wait ms="300"/></speech>
                <behavior id="assign" type="normal" />
            </action>
            <action index="2">
                <control function="guess_call" target="com.dev.zdev.rekog"/>
            </action>
        </topic>
    </body>
</hvml>
rekog_res.hvml
<?xml version="1.0" ?>
<hvml version="2.0">
    <head>
        <producer>com.dev.zdev.rekog</producer>
        <description>Guess my age? 結果通知</description>
        <scene value="com.dev.zdev.rekog.guess.res" />
        <version value="1.0" />
        <accost priority="75" topic_id="call" word="com.dev.zdev.rekog.guess.res" />
    </head>
    <body>
        <topic id="call" listen="false">
            <action index="1">
                <speech>${memory_p:com.dev.zdev.rekog.res}にみえるみたいだよ</speech>
                <behavior id="assign" type="normal" />
            </action>
            <action index="2">
                <control function="guess_res" target="com.dev.zdev.rekog"/>
            </action>
        </topic>
    </body>
</hvml>
  • MainActivity(各所抜粋)
MainActivity.java

private boolean hascall;

//onCreate ファンクション にカメラ連携起動結果取得用レシーバー登録
        mCameraResultReceiver = new CameraResultReceiver();
        IntentFilter filterCamera = new IntentFilter(ACTION_RESULT_TAKE_PICTURE);
        registerReceiver(mCameraResultReceiver, filterCamera);

//onResume ファンクション にScenn有効化追記
        VoiceUIManagerUtil.enableScene(mVoiceUIManager, ScenarioDefinitions.SCN_CALL);
        VoiceUIManagerUtil.enableScene(mVoiceUIManager, ScenarioDefinitions.SCN_RES);

//onResume ファンクション にScenn有効化後、即時発話(初回のみ)

        if (mVoiceUIManager != null && !hascall) {
            VoiceUIVariableListHelper helper = new VoiceUIVariableListHelper().addAccost(ScenarioDefinitions.ACC_CALL);
            VoiceUIManagerUtil.updateAppInfo(mVoiceUIManager, helper.getVariableList(), true);
        }

//onPause ファンクション にScene無効化
        VoiceUIManagerUtil.disableScene(mVoiceUIManager, ScenarioDefinitions.SCN_CALL);
        VoiceUIManagerUtil.disableScene(mVoiceUIManager, ScenarioDefinitions.SCN_RES);        

//onDestroy ファンクション にカメラ連携起動結果取得用レシーバー破棄
        this.unregisterReceiver(mCameraResultReceiver);

    /**
     * VoiceUIListenerクラスからのコールバックを実装する.
     */
    @Override
    public void onExecCommand(String command, List<VoiceUIVariable> variables) {
        Log.v(TAG, "onExecCommand() : " + command);
        switch (command) {
            case ScenarioDefinitions.FUNC_CALL:
                // 写真呼びかけ状況設定
                hascall =true;
                //写真を撮る
                sendBroadcast(getIntentForPhoto(false));
                break;
            case ScenarioDefinitions.FUNC_RES:
                finish();
                break;
            case ScenarioDefinitions.FUNC_END_APP:
                finish();
                break;
            default:
                break;
        }
    }


    /**
     * カメラ連携の結果を受け取るためのBroadcastレシーバー クラス
     * それぞれの結果毎に処理を行う.
     */
    private class CameraResultReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.v(TAG, "CameraResultReceiver#onReceive() : " + action);
            switch (action) {
                case ACTION_RESULT_FACE_DETECTION:
                    int result = intent.getIntExtra(FaceDetectionUtil.EXTRA_RESULT_CODE, FaceDetectionUtil.RESULT_CANCELED);
                    break;
                case ACTION_RESULT_TAKE_PICTURE:
                    result = intent.getIntExtra(ShootMediaUtil.EXTRA_RESULT_CODE, ShootMediaUtil.RESULT_CANCELED);
                    if(result == ShootMediaUtil.RESULT_OK) {
                        // 1. 撮影画像ファイルパス取得 
                        final String path = intent.getStringExtra(ShootMediaUtil.EXTRA_PHOTO_TAKEN_PATH);
                        Log.v(TAG, "PICTURE_path : " + path);
                        Thread thread = new Thread(new Runnable() {public void run() {
                            try {
                                // 2. APIリクエスト、レスポンス取得
                                String res = (new GetAge()).inquireAge(path, getApplicationContext());
                                Log.v(TAG, "onExecCommand: RoBoHoN:" + res);
                                int ret = VoiceUIVariableUtil.setVariableData(mVoiceUIManager, ScenarioDefinitions.MEM_P_RES, res);
                                VoiceUIManagerUtil.stopSpeech();
                                // 3. RoBoHon 結果発話
                                if (mVoiceUIManager != null) {
                                    VoiceUIVariableListHelper helper = new VoiceUIVariableListHelper().addAccost(ScenarioDefinitions.ACC_RES);
                                    VoiceUIManagerUtil.updateAppInfo(mVoiceUIManager, helper.getVariableList(), true);
                                }
                            } catch (Exception e) {
                                Log.v(TAG, "onExecCommand: Exception" +  e.getMessage());
                            };
                        }});
                        thread.start();}
                    }
                    break;
                case ACTION_RESULT_REC_MOVIE:
                    result = intent.getIntExtra(ShootMediaUtil.EXTRA_RESULT_CODE, ShootMediaUtil.RESULT_CANCELED);
                    break;
                default:
                    break;
            }
        }
    }
  • AWS Rekognition通信
MainActivity.java
package com.dev.zdev.rekog;

/**
 * Created by zdev on 2017/12/10.
 */

import android.util.Log;
import android.content.Context;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import com.amazonaws.auth.CognitoCachingCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.rekognition.AmazonRekognitionClient;

import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.List;

import com.amazonaws.services.rekognition.model.DetectFacesRequest;
import com.amazonaws.services.rekognition.model.DetectFacesResult;
import com.amazonaws.services.rekognition.model.FaceDetail;
import com.amazonaws.services.rekognition.model.AgeRange;
import com.amazonaws.services.rekognition.model.Image;

public class GetAge {
    private static final String TAG = GetAge.class.getSimpleName();
    private static int newWidth = 326;
    private static int newHeight = 244;

    private String resMsg = "ゼロさいからひゃくさいの間 ";
    AmazonRekognitionClient amazonRekognitionClient = null;

    private Bitmap resizeImag(String path){
        return Bitmap.createScaledBitmap(BitmapFactory.decodeFile(path), newWidth, newHeight, true);
    };

    private void setAmazonRekognitionClient(Context appcontext){
        // Amazon Cognito 認証情報プロバイダーを初期化します
        CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(
            appcontext,
            "us-east-1:XXXXXXXXXXXX", // ID プールの ID
            Regions.US_EAST_1 // リージョン
        );
        this.amazonRekognitionClient =  new AmazonRekognitionClient(credentialsProvider);
    };

    public synchronized String inquireAge(String path, Context appcontext) throws Exception {

        try {
            Bitmap img = resizeImag(path);

            if (this.amazonRekognitionClient == null) {
                setAmazonRekognitionClient(appcontext);
            }

            ByteBuffer imageBytes = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            img.compress(Bitmap.CompressFormat.JPEG, 100, baos);
            imageBytes = ByteBuffer.wrap(baos.toByteArray());

            DetectFacesRequest request = new DetectFacesRequest()
                    .withImage(new Image()
                            .withBytes(imageBytes))
                    .withAttributes("ALL");

            DetectFacesResult result = amazonRekognitionClient.detectFaces(request);
            List<FaceDetail> faceDetails = result.getFaceDetails();

            Log.v(TAG, "inquireAge: " + faceDetails.toString());

            for (FaceDetail face: faceDetails) {
                if (request.getAttributes().contains("ALL")) {
                    AgeRange ageRange = face.getAgeRange();
                    Log.v(TAG, "inquireAge: " + ageRange.getLow().toString() + " and " + ageRange.getHigh().toString() + " years old.");
                    this.resMsg = ageRange.getLow().toString() + " さいから " + ageRange.getHigh().toString() + " さいの間 ";
                } else { // non-default attributes have null values.
                    Log.v(TAG, "inquireAge: " + "Here's the default set of attributes:");
                }
            }

            return this.resMsg;

        } catch (Exception e) {
            Log.v(TAG, "inquireAge: exception: " + e.toString());
            return this.resMsg;
        }
    }
}

実行の様子と感想

年末も近いし、色々とひとが集まる機会に使えたらいいな、と、パーティアプリにしたく、実装しました。
(Microsoft「How-Old.net」が流行ったときのように、1~2回/人 試して場が沸いたら上出来!という)

家族で試したところ、下記のようになりました。

  • 30代後半男性: 45 ~ 63 才 (!)の間
  • 30代後半女性: 26 ~ 38 才の間
  • 9才 … 6 ~ 13 才の間
  • 4才 … 4 ~ 7 才の間、4 ~ 9 才の間

私(30代後半)は 「14 ~ 23 才の間」というスコアも叩き出せたので、家族が沸き「ロボホンがひいきしてる!(子供たちの声)」「なんか匙加減して実装してない?」など、”結果をロボホンが発声する”1 という文脈も楽しめました。



  1. ロボホンSDK内規定(0201_SR01MW_Personality_and_Speech_Regulations_V01_00_01)には “ロボホンはユーザに対して誠実です。ユーザを裏切ることはありません(中略) 不確実な情報を話すときは、それとわかる言い回しをする(例:「雨になるよ」→「予報によると、雨になるみたいだよ」” があります。そのため、今回も結果通知の語尾に「~才にみえるよ」でなく「~才にみえるみたいだよ」とつけています。(また、同規定書には “主観を持たない”というくだり(”ロボホンはロボットです 。ロボットは 、基本的に プログラムされたとおりに動くものです 。ロボホン自身の主観(好き嫌いや感想など ロボホン自身の主観(好き嫌いや感想など 、人によって感じ方が変わるもの 、人によって感じ方が変わるもの )は 持たないのが基本的考え方です 。”)ともあります) 

続きを読む

HoneypotのDionaeaでマルウェアを収集しちゃって、APIでスキャンして、結果をビジュアライズしちゃうぞ

はじめに

こんばんはー
Advent Calendar11日目ですね!
そして、間違えて同じ日に二つもエントリーしてしまいましたd(゚∀゚d)ォゥィェ!!

ちなみに、もう一つのQiita記事は、以下なので興味ある人は見て頂ければと思います。

絶対的に使った方がいいLogstashのMultiple Pipelinesについて書いてみた

今年は、ハニーポッターになるべく、いろいろ試してたのですが、Qiita記事を一切書いてませんでした。。
書こう書こうと思ってたら、今年ももう終わり。。
でも、最後の追い上げ!ということで、しっかりと足跡を残したいと思いますー

てことで、今回は、Dionaeaというハニーポットツールを使ってマルウェアを収集して、スキャンとか可視化までをゆるーく書いていきます。
流れはこんな感じです!

  1. 環境について
  2. 全体構成について
  3. Dionaeaのインストール
  4. VirusTotalのAPIを利用するよ!
  5. Logstashで取り込むよ
  6. Kibanaで見てみる

環境について

すべてAWSで構築してます。

  • Dionaea Server

    • Ubuntu Server 14.04 LTS
    • Dionaea
    • Python 2.7.6
  • ElastciStack Server

    • Logstash 6.0
    • logstash-input-s3
    • Elasticsearch 6.0
    • Kibana 6.0

全体構成について

ざっくりと構成について説明しますー

test.png

Dionaeaを配置し、VirusTotalにAPIコールし、マルウェアかどうかをスキャンします。
スキャン結果のjsonファイルをS3に格納し、Logstashがデータを取得します。
Logstashが取得したデータをElasticsearchにストアし、Kibanaがビジュアライズします。
といった感じの構成です!

Dionaeaのインストール

Dionaeaのインストールは、以下の手順でインストール完了です。
公式の手順に則るため、Ubuntuのバージョンは14.04にしてます。

参考:Install Dionaea

$ sudo apt-get update
$ sudo apt-get dist-upgrade
$ sudo add-apt-repository ppa:honeynet/nightly
$ sudo apt-get update
$ sudo apt-get install dionaea
$ sysv-rc-conf dionaea on
$ sysv-rc-conf --list | grep dionaea
$ service dionaea start

Dionaeaについて

Dionaeaは、低対話型のハニーポットのため、危険度は低いです。
ただ、一般的なサーバのセキュリティ対応は実施していることを前提にしてます。
細心の注意を払って対応してくださいね!

Dionaeaが、アタックされるように対象ポートをセキュリティグループでフルオープンにします。

Port Service Source
21/TCP ftp 0.0.0.0/0
42/TCP nameserver 0.0.0.0/0
80/TCP http 0.0.0.0/0
135/TCP msrpc 0.0.0.0/0
443/TCP https 0.0.0.0/0
445/TCP microsoft-ds 0.0.0.0/0
1433/TCP mssql 0.0.0.0/0
3306/TCP mysql 0.0.0.0/0
5060/TCP sip 0.0.0.0/0
5061/TCP sip-tls 0.0.0.0/0

Dionaeaのディレクトリについて

  • /opt/dionaea/var/dionaea/binaries/: マルウェアが配置されるディレクトリ
  • /opt/dionaea/log/: dionaea自身のログを保存
  • /opt/dionaea/wwwroot/: Webのドキュメントルート

VirusTotalのAPIを利用するよ!

VirusTotalは、マルウェアなんじゃないか?!とか、怪しいURLなのでは?!といった時に使用するマルウェアチェックサービスです。
VirusTotalは、マルウェア検出するために50種類以上のエンジンで検査ができます。
しかも、APIも公開されており、スキャン結果を容易に取得できます。
そして、無料!

今回は、VirusTotalのAPIを利用して、Dionaeaに仕込まれたマルウェアをスキャンします。
APIを利用するには、API Keyの取得が必要なため、以下のサイトからアカウント登録を実施してAPI Keyを取得します。

アカウント登録:VirusTotal

注意点ですが、APIは、1分間に4回までしかリクエストできません。
以下のドキュメントを参考にしてください。

参考:Document VirusTotal

あと、森久さんのサイトも非常に参考になります!
VirusTotalだけの話ではなく、ハニーポットに興味ある方は、読んだ方がいいです!

参考:www.morihi-soc.net

APIコール用のスクリプト

Dionaeaのbinariesに仕込まれたマルウェアに対してスキャンを実行するスクリプトです。

#!/usr/bin/env python
import sys
import json
import urllib
import urllib2
import os
import time

files = os.listdir('/opt/dionaea/var/dionaea/binaries/')

for hash in files:
  print hash

  url = "https://www.virustotal.com/vtapi/v2/file/report"
  # API Keyにはアカウント登録した際に取得したKeyを入力する
  params = {"resource": hash, "apikey": "API Key"}

  data = urllib.urlencode(params)
  request = urllib2.Request(url, data)
  response = urllib2.urlopen(request)
  json = response.read()

  with open("{}.json".format(hash), "w") as result:
    result.write(json)

  print "..processing"
  # 1分間に4回までのAPIコール制限があるためスリープを設ける
  time.sleep(20)

結果をjsonファイルで出力します。
出力したファイルをS3マウントした箇所に配置することで、S3にアップロードされます。

Logstashで取り込むよ

ElasticStackはすでにインストール済みを前提にしてます。
また、S3にアクセスするため、Logstashのプラグインの”logstash-input-s3″もインストールしていることとします。
もしインストールされていない場合は、対応お願いします。

Logstash.conf

Logstashのconfですが、データソースがjsonのため、フィルタに書くのはjsonだけです。
めっちゃ楽ですね!
これでElasticsearchにストアされます。

logstash.conf
input {
  s3 {
    tags => "dionaea"
    bucket => "hoge_backet"
    region => "ap-northeast-1"
    prefix => "hoge/"
    interval => "30"
    sincedb_path => "/var/lib/logstash/sincedb_dionaea"
    codec => json
  }
}
filter {
  json {
    source => "message"
  }
}
output {
  elasticsearch {
    hosts => [ "localhost:9200" ]
    index => "vt-logs-%{+YYYYMMdd}"
  }
}

Kibanaで見てみる

それでは、ElasticsearchにストアしたデータをKibanaで確認してみましょう!

FireShot Capture 50 - Kibana_ - http___54.211.11.251_5601_app_kibana#_discover.png

ちゃんとビジュアライズできてますね!

最後に

こんな感じでマルウェアを収集し、スキャン結果をビジュアライズできちゃうのです!
それでは、楽しいお時間をお過ごしくださいーヽ(*゚д゚)ノ

ではでは、明日は、”GoogleHome”についてですね!
めっちゃ楽しみだー

続きを読む

LaravelとElastic Beanstalkでサクッとスタブサーバを作る

概要

とにかく大至急スタブAPIサーバが必要でした。

Laravelのルーティング設定だったらすぐできるな

サーバどうしよう。PHP7のサーバが手元にない

Elastic Beastalkでできるかな

Laravelの準備

Laravelのプロジェクトを新規に作成。

Laravelプロジェクトを作成
composer create-project --prefer-dist laravel/laravel stub-server --no-secure-http

モックなのでルーティングだけ書いて済ませます。

routes/api.php
Route::get('test', function () {
    $res = <<< END
{
  "token": "token"
}
END;
    return response($res)
        ->withHeaders([
            'Content-Type' => 'application/json',
        ]);
});

アプリ側はこれだけです。
リクエストパラメータのバリデーションや、ステータス200以外も返したい場合はこれだと対応できないので、コントローラに書く必要があります。

zipでまとめる

Beanstalkにデプロイするにはいくつか方法がありますが、今回はzipファイルにして管理コンソールからアップします。

zipファイル作成
$ cd stub-server
$ zip ../laravel.zip -r * .[^.]*

Elastic Beanstalkの設定

管理コンソールのBeanstalkの画面で 新しいアプリケーションの作成 をクリックします。
アプリケーション名を入力して 作成 ボタンをクリック

1.jpg

以下のような画面に遷移するので 今すぐ作成しましょう をクリック

2.jpg

開いたウィンドウの ウェブサーバー環境 を選択して 選択 をクリック

3.jpg

URLを決めて 使用可能かチェック をクリックします。使用可能なら下記のようになります。

4.jpg

プラットフォームに PHP 、アプリケーションコードに コードのアップロード を選択し アップロード ボタンをクリックします。

5.jpg

先ほど作成したZIPファイルを選択し アップロード します。

6.jpg

作成が始まります。

7.jpg

5分ほどで作成完了します。 ※現在アプリは削除済みです

8.jpg

ただ、このままではアクセスしても403エラーになるので、最後にドキュメントルートの設定を行います。

9.jpg

10.jpg

設定後、自動的に環境に反映されるのを待って ドメイン/api/XXXXXXXXX にアクセスすると、、、

11.jpg

表示されました😃

今回はここまでですが、HTTPS化したいですね。

続きを読む

動画を探して自動ツイートしてくれるPython製botをAWSに載せてみた(前編)

TL;DR

  • YouTubeから動画を拾ってTweetするbotをPythonで開発し、AWS Lambdaに載せてみました
  • 全2記事です。前編のこちらでは、主にPythonでの開発周りのトピックにフォーカスします
    • TwitterAPIを使ってプログラムからツイートしてみます
    • YouTubeのページを構文解析し、文字列操作を使って動画URLを抽出してみます

動機

新しい職場にて初めてAWSを触ることになったので、これを機にと個人アカウントを取ってみました。チュートリアルだけというのももったいないので、何か自分のためのサービスを作って載せると面白そうです。

で、Twitterのbot開発にはもともと興味があったので、これも前から興味を持ちつつ触ってなかったPythonでbotを作り、lambdaを使って運用してみようと思い立ちました。AWS lambdaは2017年4月からPython3系を扱えるようになったので、心置き無く最新バージョンで書けそうだなー、というのも狙いです。

ユーザーストーリー

毎日の退勤をもう少し楽しみにするために、定時になると自分が興味ありそうなYouTube動画をbotが勝手に検索して、自分のTwitterアカウントに届けてくれるようにしたい。
スクリーンショット 2017-12-06 23.30.33.png

前提

  • 開発にはMacを使用します
  • Pythonは3.6系を使用します
  • pyenvもvirtualenvも使用しません。議論はあろうかと思いますが、個人開発なので。。
  • で、開発環境構築はこちらの記事等を参照しました
  • bot化したいTwitterアカウントはあらかじめ用意してあるものとします

TwitterAPIを使ってプログラムに呟かせる

アクセスキーの取得

bot化したいアカウントでTwitter Application Managementにログインすると、アプリケーションの作成とConsumer Key、及びAccess Tokenの取得ができます。

なお、Appの作成にはTwitterアカウントが電話番号認証済みである必要があります。認証済みでないと怒られるので、エラーメッセージ中のリンクからさらっと済ませておきましょう。

  • Consumer Key
  • Consumer Key Secret
  • Access Token
  • Access Token Secret

以上の4パラメータがあればプログラムからのツイートができます。コピーしてこんな感じのファイルを作っておきましょう。

config.py
CONSUMER_KEY        = "xxxxxxxxxxxxxxxxx"
CONSUMER_SECRET     = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ACCESS_TOKEN        = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
ACCESS_TOKEN_SECRET = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

複数の外部ユーザーからアクセスがあるようなアプリケーションの場合(=「このアプリケーションとの連携を許可しますか?」など出るやつ)はそれぞれの役割についてもう少し説明が必要ですが、今回はある程度一緒くたに考えてしまっても実装に支障ありません。

PythonでOAuth認証

ライブラリの導入と管理

Pythonのライブラリは、パッケージ管理ツールであるpipでインストールできます。仮想環境がない場合、オプション無しで勝手にglobalに入るのがうーん、という感じですがまあそれは置いておいて。

PythonでHttp通信を行うライブラリとしては、requestsがポピュラーなようです。また、今回はTwitterAPIを使うための認証が必要なので、OAuth認証を扱えるライブラリも必須です。ここはrequestsと同じところが公開しているrequests_oauthlibを使用しました。

pip3 install requests requests_oauthlib

さて、インストールはできましたが、今度は開発するプロジェクトがこれらのライブラリに依存していることを表明しておくのがマナーです。js界隈で言うところのpackage.jsonですね。

Pythonでは依存関係を記したrequirements.txtなどを作っておくケースが多いようです。

requirements.txt
requests==2.18.4
requests-oauthlib==0.8.0

ちなみに、pip3 freeze > requirements.txtでインストールされた依存関係をrequirements.txtに吐き出せます。

逆に.txtファイルを元に一括インストールする場合は、-rオプションを用いてpip3 install -r requirements.txtなどと書けます。結構便利です。

つぶやいてみる

first_tweet.py
from requests_oauthlib import OAuth1Session
import config, json

twAuth = OAuth1Session(
  config.CONSUMER_KEY,
  config.CONSUMER_SECRET,
  config.ACCESS_TOKEN,
  config.ACCESS_TOKEN_SECRET)
apiURL = "https://api.twitter.com/1.1/statuses/update.json"
params = { "status": "プログラムにツイートさせてみるテスト" }

res = twAuth.post(apiURL, params = params)
print(json.loads(res.text))

先ほど作ったconfig.pyimportして、これだけ。思ったよりだいぶ手軽です。Twitterにアクセスして実際にツイートされたことを確認しましょう!

また、せっかくなのでレスポンスをjsonライブラリでロードして吐き出してみます。

{'created_at': 'Wed Dec 06 14:00:00 +0000 2017', 'id': 9384076800000000, 'id_str': '9384076800000000', 'text': 'プログラム
にツイートさせてみるテスト', 'truncated': False, 

...(中略)...

'retweeted': False, 'lang': 'ja'}

思ったよりいろんな属性があることがわかりますね。深掘りは公式のリファレンスにて。

YouTubeから動画のURLを拾ってくる

続いて、YouTubeから動画を探してくるパートです。

Webクローリング

この分野では、「クローリング」や「スクレイピング」と言った言葉が有名です。

クローリングとスクレイピング

クローリングはウェブサイトからHTMLや任意の情報を取得する技術・行為で、 スクレイピングは取得したHTMLから任意の情報を抽出する技術・行為のことです。

たとえば、あるブログの特徴を分析したい場合を考えてみましょう。
この場合、作業の流れは

  1. そのブログサイトをクローリングする。
  2. クローリングしたHTMLからタイトルや記事の本文をスクレイピングする。
  3. スクレイピングしたタイトルや記事の本文をテキスト解析する。

というようになります。

今回は、YouTubeをクローリングし、その中から動画のURLをスクレイピングすることになりますね。

Webページのクローリングとスクレイピングを行う際は、それがどんな目的のものであれ、HTMLを構文解析することが必須となります。Pythonでは、これを強力に支援するBeautifulSoupと言うライブラリがあります。執筆時点で最新のbeautifulsoup4を導入してみます。

pip3 install beautifulsoup4

早速使ってみましょう。Qiitaのトップページから<a>タグを探し、その中に含まれるhref属性の値を取得してみます。

crawling.py
import requests
from bs4 import BeautifulSoup

URL = "https://qiita.com/"
resp = requests.get(URL)

soup = BeautifulSoup(resp.text)

# aタグの取得
a_tags = soup.find_all("a", href=True)
for a in a_tags:
    print(a["href"])

結果

/about
https://qiita.com/sessions/forgot_password
https://oauth.qiita.com/auth/github?callback_action=login_or_signup
https://oauth.qiita.com/auth/twitter?callback_action=login_or_signup

・・・(中略)

https://qiita.com/api/v2/docs
https://teams.qiita.com/
http://kobito.qiita.com

いい感じです!

HTMLパーサーについて

さて、先のコードを実際に試すと、HTMLパーサーが明示されていないために警告が出ます。これは実際の解析時に使われるパーサーが実行時の環境に依存するためです。異なる環境下で同じ振る舞いを期待するには、使用するHTMLパーサーを明示してあげる必要があります。

デフォルトではhtml.parserが使われますが、lxmlかhtml5libを導入してこちらを明示してあげるのが無難なようです。このあたりの情報は下記の記事をだいぶ参考にさせていただきました。パーサーの選択だけでなくスクレイピング全般の情報が非常によくまとまっているエントリなので、オススメです。

PythonでWebスクレイピングする時の知見をまとめておく – Stimulator

パーサの良し悪しを考えるとlxmlでチャレンジしてダメならhtml5libを試すのが良さそう。

今回はこの1文に愚直に従ってみます。事前にpip3 install lxml html5libも忘れずに。


import requests
from bs4 import BeautifulSoup

URL = "https://qiita.com/"
resp = requests.get(URL)

+try:
+  soup = BeautifulSoup(resp.text, "lxml")
+except:
+  soup = BeautifulSoup(resp.text, "html5lib")
-soup = BeautifulSoup(resp.text)

# ...以下は先ほどと同様

Crawlerクラスを作ってみる

すでにPythonでオブジェクト指向な書き方を経験している方はこの辺りを飛ばしていただいて構いません。せっかくHTMLを解析してくれるコードができたので、クラスとして書き換えてみます。

crawler.py
import requests
from bs4 import BeautifulSoup

class Crawler:
    def hrefs_from(self, URL):
        a_tags = self.soup_from(URL).find_all("a", href=True)
        return set(map(lambda a:a["href"], a_tags))

    def soup_from(self, URL):
        res_text = requests.get(URL).text
        try:
            return BeautifulSoup(res_text, "lxml")
        except:
            return BeautifulSoup(res_text, "html5lib")

個人的にはインスタンスメソッドの第1引数が常にselfでなければならないのは書く量が増えるので少しもどかしいですね。ハマりポイントにもなりかねない…。

ちなみに、ここではラムダ式を使用し、hrefs_fromメソッドの戻り値の型をsetにしてみました。これは、今回のユースケースを鑑みてリンク先URLの重複を排除した方が便利と判断したためです。出現頻度など解析したい場合はまた改めて設計を考える必要があるでしょう。

継承と、YouTubeへのアクセス

YouTubeをクローリングするにあたって、「検索文字列を与えたら検索結果のページをクローリングし、動画を探してくる」などの機能があると便利そうです。先ほどのクラスを継承して、実装してみます。

tube_crawler.py
import random
import re
from crawler import Crawler

class TubeCrawler(Crawler):

    URLBase = "https://www.youtube.com"

    def hrefs_from_query(self, key_phrase):
        """
        検索文字列を与えると検索結果ページに含まれるhref属性の値を全て返す
        """
        return super().hrefs_from(self.URLBase + 
            "/results?search_query=" + key_phrase.replace(" ", "+"))



    def movies_from_query(self, key_phrase, max_count = 10):
        """
        検索文字列を与えると検索結果ページに含まれる動画のビデオIDを返す
        """
        return self.__select_movies(self.hrefs_from_query(key_phrase), max_count)



    def __select_movies(self, hrefs, max_count):
        """
        privateメソッド。href属性の値のsetからビデオIDのみを返す
        """
        filtered = [ re.sub( "^.*/watch?v=", "", re.sub( "&(list|index)=.*$", "", href )) 
            for href in hrefs if "/watch?v=" in href ]
        return filtered[:min(max_count, len(filtered))]



    def choose(self, movie_ids, prefix = "https://youtu.be/"):
        """
        渡した文字列のリスト(ビデオIDのリストを想定)から1つを選び、prefixをつけて返す
        """
        return prefix + random.choice(movie_ids)

文法的には継承とprivateメソッドの書き方あたりが新しい話題となります。この記事の主題ではないので特段の説明は省きます。

実際に試すとわかるのですが、検索結果のページにノイズとなるリンクが多いばかりか、再生リストへのリンクなど紛らわしいものも多く、その辺を適切に弾いていくのに手こずりました。おかげでfilter関数や正規表現に少し強くなれた気がします。

正規表現についてはこちらの記事をだいぶ参考にしました。

Pythonの正規表現の基本的な使い方

繋げてみる

準備が整ったので検索->ツイートの流れを試してみます。

main.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tube_crawler import TubeCrawler
from tweeter import Tweeter
import config

def main():
    t = TubeCrawler()
    movies = t.movies_from_query("Hybrid Rudiments")
    chosen = t.choose(movies)

    # ツイートする部分をクラス化したもの
    tw = Tweeter()
    tw.reply(config.REPLY_TO, chosen)

if __name__ == '__main__':
    main()

エントリーポイントとなる関数が必要かなー、と思ったので何気なく(そう、本当に何気なく。これで良いと思っていたんですLambdaを使うまでは…)main関数を作成。

直接./main.pyでも呼べるようにこの辺からShebangを記述し始めました。また、末尾はファイル名で直接実行した場合にmain()を呼ぶためのおまじない。Rubyにも似たやつがありますね。あとはターミナルから呼んで動作確認するだけです。

$ ./main.py

実行したところ問題なく動きそうだったので、次回はAWS Lambdaに載せていきます。それなりの尺となったのでこのページはここまでです。お読みいただきありがとうございました。

リンク

続きを読む

phpでlocalstackのs3にファイルをアップロード

AWS周りのものをローカルで開発する場合 localstack を使うと便利だというのを聞いたので試してみました。 github.com localstackは、GUIはあまり提供されていない用で、主にawscliコマンドを使います。 s3にファイルをアップロードしたい場合は、 –endpoint-url=http:… 続きを読む

はじめてのServerless ✕ Webpack ✕ TypeScript


このエントリーはaratana Advent Calendar 201712日目のエントリーです。

こんばんは!最近Google Home MiniAmazon echo dotを購入したはいいが置き場所に困っている蔭山です。
みなさんはどのような場所に置かれているのでしょうか。。。

前日は新卒エンジニアには決して見えない安定感をお持ちの猿渡くんの「NoSQLについて何か。」という記事でした!
NoSQL?あぁ、聞いたことはある。
みたいな僕でもわかりやすい記事でした!
最近AWSに興味が出始めたところでしたので、ぜひDynamoDBを使って軽い画像投稿サービスでも作ってみます!

さて今回はServerless ✕ Webpack ✕ TypeScriptの組み合わせで使えるように手順をまとめてみたいと思います!

動作環境

今回の動作環境は以下になります!

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1114

$ node -v
v8.2.1

$ serverless -v
1.24.0

$ webpack -v
3.5.6

環境準備

nodeやserverlessのインストールについては下記記事を参考に。。。

とりあえずやってみる

まずはServerlessのプロジェクトを作ってみましょう

$ serverless create -t aws-nodejs -p hogehoge
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/fugafuga/hogehoge"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.24.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"
$ cd ./hogehoge

JSer御用達npmを使って必要なパッケージをダウンロードしましょう。

$ npm init
$ npm install --save-dev serverless-webpack serverless-offline ts-loader typescript webpack

インストールが終わり次第、各種設定を行います。
今回はTypescript -> ES2015へのコンパイルを目的に設定させていただきます。
細かい設定内容に関しては割愛します。

./serverless.yml
service: hogehoge

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

plugins:
- serverless-webpack
- serverless-offline

functions:
  hello:
    handler: handler.hello
    events:
     - http:
         path: hello
         method: get
./webpack.config.js
module.exports = {
  entry: './handler.ts',
  target: 'node',
  module: {
    loaders: [{
      test: /\.ts$/,
      loader: 'ts-loader'
    }]
  },
  resolve: {
    extensions: ['.ts']
  },
  output: {
    libraryTarget: 'commonjs',
    path: __dirname + '/.webpack',
    filename: 'handler.js'
  }
};
./tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs"
  },
  "exclude": [
    "node_modules"
  ]
}

準備はできたので、次はTypeScriptでコーディングしてみましょう!

./handler.ts
export * from './src/ts/functions/hello';
./src/ts/functions/hello.ts
export function hello(event, context, callback): void {
  const response = {
    statusCode: 200,

    headers: {
    },

    body: JSON.stringify({
      "message": "Hello Serverless x Webpack x TypeScript!!"
    })
  };

  callback(null, response);
};

コードが書けたらローカル環境で動作確認

$ sls offline
・・・・・・・・・・・・・・・・
途中は割愛。m(__)m
・・・・・・・・・・・・・・・・
Serverless: Routes for hello:
Serverless: GET /hello

Serverless: Offline listening on http://localhost:3000

きちんと動作するか確認。

$ curl -X GET http://localhost:3000/hello
{"message":"Hello Serverless x Webpack x TypeScript!!"}

動作が問題なければ、早速デプロイしてみましょう!

$ sls deploy
・・・・・・・・・・・・・・・・
途中は割愛。m(__)m
・・・・・・・・・・・・・・・・
api keys:
  None
endpoints:
  GET - https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
functions:
  hello: hogehoge-dev-hello

デプロイが完了したようです。
では早速動作確認。

$ curl -X GET https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"Hello Serverless x Webpack x TypeScript!!"}

ちゃんと動きましたね!

最後に

無理やりTypeScriptを使った感が凄まじいですね。。。申し訳ありません><
僕個人がTypeScriptを使ったことがなかったため使ってみたかったんです

明日は新卒田村くんの「Ctagsで自由な翼を得たVimについて」です!
お楽しみに!

参考

主にこちらの記事を参考にさせて頂きました!ありがとうございますm(__)m

続きを読む