たゆたふ。

定まる所なく揺れ動き、いろいろやってみたメモ。など

Alexaスキルをリリースしてみた

ここしばらく、仕事で Alexa スキルを作っていたのだけど、スキルアワードもあるし、自分でもなんか作って公開してみようと思っていた。

ただ、ネタがない。

アワードに応募するなら、なにか思いついてももう作っている時間がないなぁと思っていたところ 思いついて、構想 30 分、実装数時間で作って公開したのがこれ。

「アレクサ、発想の扉を開いて」で Echo に話しかけると オズボーンのチェックリストをベースにした、視点を変え発想を促す(と思う)問いかけしてくるだけのスキル。

「アレクサ、発想の扉を開いて」で始めると、連続問いかけモードで「ほかにもいりますか?」などと次を促してくるので、「はい」とか「次の頂戴」とか言うと連続して問いかけさせることができる。

「アレクサ、発想の扉で問いかけて」というと、単発問いかけモードとなり、1つだけ問いかけてすぐに終了する。

一応、連続問いかけモードモードの場合には、同じことを何度も言わないようにしている。 工夫といえばそれくらいで、対話しているようでしてなくて一方的にしゃべるだけのスキルなので作りもシンプル、簡単。

でも、アイコンが...

ただ、困ったのは、スキルのアイコン。
スキルの公開には必須だのだけど、絵心のない私にはコードを書くよりハードル高かった1

Pixabayで見つけた CC0の画像をちょっといじって使わせてもらった。

リジェクト喰らった

サクッと作って、サクッと申請して、サクッと公開!と思いきや、一度リジェクトされてしまった。

Amazon によるスキル審査でリジェクトされた場合、理由と再現手順が記載された丁寧なメールが届く。 どこをどう直せばよいのかがわかりやすかった。

Alexa の審査チームからの指摘は 3 点、うち 2 点は指摘の箇所が違うだけで同じ理由。

サンプル発話が少なすぎる

カスタムインテントの中で、サンプル発話を 2 つだけしか設定していないものがあった。 想定はしていたが、直接定義していなかった発話も拡張して拾ってくれていたので 実機テストをして問題なしと思っていた。でもダメらしい。

ユーザーによる発話 | Amazon Alexa Voice Design Guideには「1インテント当たりのサンプル発話数は30個以上を目安としてください」と書かれているが、18 個に増やしたら審査をパスできた2

セッションオープンなら必ず次の発話を促すこと

これは 2 箇所で指摘された。1 つはヘルプインテントを呼ばれた場合にスキルを終了せずに続けるようにしていたのだが、 次を促す台詞で終わっていなかった。これは単純にリファクタした際に落としてしまっており私のミス。

もう一箇所が、連続問いかけモードで問いかけの後で、次を促す台詞がないという指摘。
連続問いかけモードでは問いかけの後に「ほかにもいりますか?」と次を促す。 ただ、疑問文 2 連続となり不自然でうざいと思ったので 最初の 3 回は使い方を知ってもらうために「ほかにもいりますか?」と話すが、4 回目以降は言わないようにしていた。

申請時に記載するスキルのテスト方法にもその旨記載していたのだけど、セッションをクローズしないなら、次の発話を促す台詞を入れることというのは絶対らしい。

審査は速かった

修正自体はちょっとした変更で済んだので、修正して再申請。今度はサクッと通過して公開となった。 スキルアワードの締切間近だったので、混んでいるかと思ったが、最初の申請から 2 営業日目でリジェクト、 その日のうちに修正して再申請して翌日の公開となった。

Amazon の審査チームの皆さんの迅速で丁寧な審査に感謝。

最後に

スキルの呼び出しフレーズが「〜を開いて」であることから、思いつきで作ったたわいのないスキルなのだが、 ブレストなどで煮詰まったときの気分転換程度に使ってみてほしい。

その結果、このスキルの問いかけが役立っても立たなくても「発想の扉」が開いてアイデアが出れば幸いである。


  1. Amazon 側もそこがつまずきポイントになっていると思っているのか最近になって謹製のアイコン作成ツールAlexa Icon Builderを出してきた。

  2. Alexa ハンズオンセミナーでは「1 インテントに対して最低 6 つ、理想は 30」と教わった。2 個ではやっぱ少なすぎた。

GAS を Webpack + Babel でやってみた

久々に、Google Sheets の Add-on で欲しいものができたので作った。 良い機会なので、Webpack + Babel を使ってビルドする構成を試してみた。 また、Google Apps Script (GAS)周りでもいくつか変化があったので取り入れてみた。

その記録。

作ったもの

会社で開発に使った時間を資産管理の関係で申請する必要があるのだが、Redmine のチケットに時間を記録すれば OK となっている。 一方、私は Togglで日々の時間管理をしている。

チケット終了ごとに Redmine に登録したり、締め日に Toggl を見ながら手作業で Redmine に登録したりしてみたのだけど、どちらも面倒でやってられなかった。 やはり、普段の時間管理は Toggl だけにしたい。 ということで、Toggl から Redmine にデータ移行できるツールを作ることにした1

コマンドラインツールでも良かったのだけど、Redmine に登録する前にちょっと確認したり、調整したりできたほうが便利かと考えた。 となると Google Sheets に一旦書き出して、それを登録できるものがよかろう、それならば Add-on として実装するのが最も便利に使えそうだと考えた。

それで作ったのは toggl2rm 。 次の機能を備えている。

以下に、toggl2rmを作る過程でキーとなった部分や新たに調べたことを記述する。

Webpack + Babelの設定

ざっと、Toggl と RedmineAPI を叩く部分を GAS の Web エディタで検証がてら実装した後、gapps コマンド(node-google-apps-script)を使って、ローカルにソースを持ってきて、Webkit + Babel でビルドする設定した。

webpack.config.babel.jsは次の通り設定した。

import GasPlugin from 'gas-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import path from 'path';

export default {
  context: path.resolve(__dirname, 'src'),
  entry: {
    code: './code',
    props: './props',
    utils: './utils',
  },
  output: {
    path: path.resolve(__dirname, 'dest'),
    filename: '[name].js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /\.json$/,
        exclude: /node_modules/,
        use: {
          loader: 'file-loader',
          options: {
            name: './[name].[ext]',
          },
        },
      },
    ],
  },
  plugins: [
    new GasPlugin(),
    new HtmlWebpackPlugin({
      filename: 'sidebar.html',
      template: 'sidebar.html',
      inject: false,
    }),
    new HtmlWebpackPlugin({
      filename: 'setting_dialog.html',
      template: 'setting_dialog.html',
      inject: false,
    }),
  ],
};

.babelrcは次の通り。

{
  "presets": ["env", "gas"]
}

この設定では次のプラグインを利用している。

gas-webpack-pluginbabel-preset-gas で ES6 な JS をいい感じの GAS 向けコードにトランスパイルしてくれる。

html-webpack-plugin は結局 srcディレクトリからdestディレクトリに HTML ファイルをコピーするのにしか使っていない。 本当は HTML に含まれる CSS と JS を別ファイルで実装し、ビルドによって 1 つの HTML にまとめたかった。 html-webpack-pluginプラグインを更にインストールしていろいろ試したが、どうもうまくいかなかった。 最初からdestディレクトリに HTML だけ置いたり、Webpack 使わずコピーしても良かったのだけど、ソースとビルド成果物をはっきり分けたい+設定はwebpack.config.babel.jsに集約したいのでこうした。

また、JS を 1 つのファイルにビルドしてまとめても良かったのだけど、なんとなく大きな 1 ファイルを作りたくないなぁと思って、3 つに分けた。 しかし、1つにしたほうが、グローバルに晒したくない関数を隠せるので今後見直すかもしれない。

ESLint

ESLintも導入してみた。 ベースの設定は eslint-config-airbnb-baseを利用した。 これだけだと GAS 特有のオブジェクトの参照でエラーが出るので、それはeslint-plugin-googleappsscriptで解決した。

自分で実装したグローバルオブジェクトと console.error() 等のログ出力系(後述の Stackdriver Logging のため)を個別に許可設定した。

私は Atom エディタを使っているので、エディタの Linter が実装時にリアルタイムでチェックしてくれ、便利だった。

上記を設定したのものはこんな感じ。

extends: airbnb-base

plugins:
  - googleappsscript

env:
  googleappsscript/googleappsscript: true

globals:
  Props: false
  Utils: false

rules:
  no-console: ["error", { allow: ["info", "warn", "error"] }]

clasp

最初は gapps コマンド(node-google-apps-script)を使っていた。 しかし、複数の GAS のプロジェクトがある場合、個別に Google Drive API の認証が必要なのにその認証情報の管理は~/.gapps 1 ファイルで行われ、切り替えるのが不便だった。

汎用的な不満だと思うので認証情報ファイルをコマンド実行時に指定する方法があるのではと、リポジトリを久々に見るとディスコンしていた。 代わりに Google 謹製の google/claspを使えと書かれていた。

clasp を調べてみて gapps と比べ以下が優れていると思ったので乗り換えた。

  • GAS プロジェクト毎の認証設定が不要(1 回のログインですべての GAS プロジェクトの管理が可能)
  • GAS スクリプトの作成も可能(gapps は先に GAS スクリプトを作っておく必要がある)
  • ドキュメントに紐づくスクリプトの管理も可能(gapps では不可)
  • Google 謹製という安心感

使い方はこちらを参照 => GAS のGoogle謹製CLIツール clasp - Qiita

Stackdriver Logging

確か以前は console.log() は GAS では使えなかったと思うのだが、今は使える。 GAS はサーバサイドで実行されるスクリプトなので、ブラウザの Dev Tool には何も出力されない。 どこに出力されるかというと、Stackdriver という Google Cloud 上のログサービスに出力される。

どうやら、去年の夏頃こうなったようだ3。 以前から GAS にちゃんとしたロギングがほしいと思っていた。 Logger.log() では最後の実行分しか見られないので、どうしても必要な場合にはスプレッドシートに書いたりしたものだ。 しかし、これからは Stackdriver Logging が使える。 DEBUG、INFO、WARN、ERROR という一般的なログレベルを設定して出力できるのも嬉しい。

Stackdriver は無料のサービスではないが、毎月無料枠があり、個人ユースではそれで足りるのではないかと思う。 今回はエラーログの記録に利用している。

ざっと調べてみた結果はこちら => GAS でStackdriver Loggingを使う - Qiita

最後に

久々に GAS での開発をやってみて、自分なりのモダンな開発構成を確立できたと思う。 gas-webpack-plugin が Webpack4 に対応できていないのかどうもうまくトランスパイルできなかったので、実は今回は Webpack3 を使っている。 そのうち対応されればアップデートしようと思う。

しばらく見ないうちに GAS を取り巻く環境も色々変わっていた。 以前は自分にだけ自作 Add-on をインストールする方法があったのだが、それがなくっている。代わりにテスト設定を作成してそこから起動すれば動かせる様に変更されようだ。少々めんどくさくなった。 その一方で、謹製 CLI ツールが用意されたり、ログ周りが強化されていた。
更に、GAS 専用のマネジメントコンソールもできた。Google Drive では他のドキュメントとごちゃまぜの管理だけど、こちらだとドキュメントに埋め込まれた GAS も含め、すべての GAS のみを管理できる。 まだ、あまり調べていないが、バージョン管理と公開周りも開発版と公開版を別々に動かせるようにするためにちょっと変わったようだ。 このあたりの変更は Chrome アプリのプラットフォームとして GAS を使っていくためであろうか?

何れにせよ、以前より開発しやすくなったと思う。 便利なサーバレス環境なので今後も活用していこうと思う。


  1. 個人的には全員で Toggl に移行したほうが効率的なのではと思うものの、今、回っている仕組みを私の我儘トリガーで変更するほどでもないし、Toggl も有料プランが必要になりそうで費用もいくらか発生してしまうので個人的に解決する道を選択した。

  2. CSV でダウンロードする機能を Toggl は提供している。しかし有料プランの機能。同等のデータは API を使えば取得でき、それを利用している。無料で有料機能相当を提供していることに気を使ってライセンスは CC-BY-NC-4.0 としている。

  3. G Suite Developers Blog: Stackdriver Logging for Google Apps Script is now available

Google Homeでリモコン操作をやってみた。その2

hero.hatenablog.jp

上に示したポストで Google Home -> Node-RED までを書いたけど、これはその続き。 Node-RED で MQTT のメッセージを受けた後、赤外線を発するまでを書く。

irMagicianの準備

Node-RED のフローのフローを作成する前に、その要素として必要になる赤外線リモコン irMagician の準備をしておく。

irMagician は大宮技研製の赤外線リモコンモジュール。 Raspi との組み合わせでよく使われるものらしく、Amazonでも購入できる。 USB 接続なので、電子工作苦手な私にはありがたい。

最初はirmagicianを使ってリモコン信号を記録しようと MacBook に接続して、動かしてみた。 その時点では、少々不安定なものの動いており、それらしいデータも取れていた。

しかし、そのデータを書き込んで動作確認するも、ターゲットのシーリングライトは全く反応しない。 キャプチャしたリモコン信号データが、Mac ではうまく書き出せないのかなと思い、 Raspi 上で試すと今度は書き出したデータを書き込もうとするとエラーが発生する。 こりゃダメだと思い、仕方なく JS のライブラリの利用は諦めた。

ググると、Python を使っている人が多そうだった。 で、netbuffalo/irmcli: irMagician command line utilityを試してみた。

irmcliのインストール

インストールと言っても irmcli 自体は単なる Pythonスクリプトなので、GitHub から clone するだけ。

ただ、素の raspi だと、Git や pip がないのと、pyserial に依存しているのでそれらをインストールする必要がある。 手順は次の通り。

sudo apt-get update
sudo apt-get install git python-pip
sudo pip install pyserial
git clone https://github.com/netbuffalo/irmcli.git

リモコンデータの作成

リモコン信号のキャプチャするには次のコマンドを実行し、リモコン受光部に実際のリモコンの赤外線を浴びせる。

$ python irmcli/irmcli.py -c

キャプチャした信号は irMagician に保持されているので、次のコマンドで、発信できる。 irMagician を操作したい機器に向ければそれで操作できる。

$ python irmcli/irmcli.py -p

リモコン信号をファイルに書き出すには次のコマンド。

$ python irmcli/irmcli.py -s -f [書き出し先ファイルパス]

書き出したファイルを読ませてリモコンを発信するには次のコマンドを使う。

$ python irmcli/irmcli.py -p -f [読み込ませるファイルパス]

一通りのやりたい操作を Raspi 上で試して問題なく動作したので、 ターゲットのシーリングライトのリモコンの ON/OFF の信号をキャプチャしてファイルに保存しておく。 irMagician が保持できるリモコン信号は1つで、しかも揮発性で電源落とせば消えてしまう。 したがって、on と off のそれぞれの信号をデータとして保存しておいて、発信するたびに読み込ませる必要があるのだ。

事前準備はここまで。

Node-REDのフローの登録

いよいよ Node-RED 上のフローを登録する。 次のようなフローにした。

mqtt in ノードは その1 に書いたとおりの設定。 それ以降の設定を順に説明する。

JSON ノード

文字通り、受けたメッセージを JSON に変換する。 その1で書いたように、IFTTT からBeebotteには次のような JSON を送信している。

{ "data": {"room": "bedroom","device": "light","action":"{{TextField}}"} }

これが、次の様に mag.payloadstring で入れられて送られて来る。

"{"data":{"room":"bedroom","device":"light","action":"オン"},"ispublic":true,"ts":1515375632470}"

これを後の処理で使いやすいように JSON をオブジェクトに変換する。

{"data":{"room":"bedroom","device":"light","action":"オフ"},"ispublic":true,"ts":1515375632470}

Change ノード

Changeノードを制すものがNode-REDを制すほど使い勝手のあるノードらしい。

ここでは次の3つの設定で値を書き換えてある。

1つ目と2つ目は オンオフ をそれぞれ、onoff に書き換えている。 3つ目で、msg.payload.datamsg.payload にセットして必要な値のみ後続の処理に送るようにしている。

Switchノード

Switch ノードは条件分岐するノード。 ここでは、msg.payload.action の値 on|off により、点灯するのか消灯するのかを制御する。 次の様に設定した。

Exec ノード

前述の様に node 様の JS のライブラリはうまく動かなかったので、それを内部で利用する node-red-contrib-irmagician を使うことはできなさそうということで、PythonCLI ツール irmcli を Exec ノードで実行することにした。 まあ、Exec ノードがあればどんな言語で実装されたものでも、コマンドラインで実行できればなんとかなる。

次の様に設定した。 次のキャプチャは点灯用だけど、同様に消灯用も登録する。違うのは読み込ませる json ファイルのみ。

ついにシーリングライトのON/OFF

これですべての設定が完了した。

今回、利用した Ras Pi は昔に買って、使い道なく放置していた RASPBERRY PI 1 MODEL B+。 Raspi の中でスペックは最も低いが、代わりに消費電力は Pi3 より小さいし、Node-RED を動かすだけなら問題ない。 ただ、起動して動作するまでは少々時間がかかるが、ずっと稼働させておくものなので気にならない。

実際にやってみると、irMagician の赤外線が方向にシビアできちんと対象の方向に向けていなければ赤外線が届かない。 それで、irMagician を繋いでいる USB ケーブルに針金を巻き付け方向を保っている。

Google Home に「OK Google、寝室ライトオン」と話しかければシーリングライトが点灯するし、 「OK Google、寝室ライトオフ」と話しかければ消灯する。 これまで、夜明かりをつけるときには暗闇でリモコンを探していたので思った以上に便利だ。

本当は、寝室のエアコンも操作したかったところだが、irMagician の指向性が思った以上に強く、1つでいろんな家電を操作するには使いづらい。 LED 拡散キャップも試してみたが、今度が距離的に厳しくなりすぎる。 ラズベリー・パイ専用 学習リモコン基板 だと部屋に1個で事足りるようになるだろうか。

まあ、しばらくは寝室の明かりだけかな。

あ、部屋を片付けてたら、大昔に買った PC-OP-RS1 が出てきた。これまだ動くかなぁ。

Google Homeでリモコン操作をやってみた。その1

Amazon Echoの招待がなかなか届かないので*1Google Home を買ってみた。 使ってみて、ちょっとしたことを声だけで操作できるのは思った以上に便利だと感じる今日このごろ。

音楽を流したり、ちょっとした質問に答えてくれるのも便利だし、なぞなぞとか早口言葉をやってくれるのも面白いのだが、 やはり家電機器の操作もやってみたい。

とりあえず、最初は定番、電灯のON/OFF。我が家のシーリングライトは赤外線リモコン操作なので、リモコン信号を操作できれば実現できるだろう。

最初から連携するデバイスを購入してしまえばもっと簡単なのかもしれない。 しかしデバイスメーカーのサーバサイドサービスに依存するとサービスが終了した場合に機器ごと使えなくなってしまいかねないので、なるべく自作できるものの組み合わせで実現したい。

で、以下の組み合わせでやってみた。

うーん、どうもやりたいことのシンプルさの割に実現する仕掛けが複雑すぎるように思う。 しかし、2018年正月の時点ではインターネットに晒す自前サーバの運用なし構成ではこれでもシンプルな方かと思う。

以下にそれぞれの連携方法について書く。

Google Assistant -> IFTTT -> beebotte

IFTTTにGoogle Assistant のトリガーが用意されている。 そのトリガーとWebhookを利用して beebotte にメッセージを送る。 ここまでで、Google Assistantへの音声入力をプログラムから扱える形のメッセージに変換することになる。 そこまでを順に説明する。

Beebotte の設定

IFTTTのアプレット登録に、REST APIのエンドポイントが必要になるので先にBeebotteの設定を行う。

Beebotte はIoT向けのクラウドサービス。REST API や MQTTをサポートして、接続された機器にリアルタイムにメッセージを通知することができる。今回はこのサービスをMQTTブローカーとして利用する1。 50,000メッセージ/日までならフリープランで利用できる。個人ユースにはそれで十分であろう。

まずはアカウントを登録する。フリープランならユーザ名、メアドとパスワードだけで登録できる。 アカウントを作成したら、チャネルとリソースを登録するよう促される。 特に迷うところはないと思う。 登録したらこんな感じ。

f:id:HeRo:20180103182319p:plain

Channel名とresource名で REST API や MQTTのエンドポイントが決まる。 また Channel Token が発行されるが、これは後にIFTTTのアプレット登録やNode-REDから接続する場合に必要となる。

IFTTTでのアプレット登録

Google Assistant トリガーは4種類ある。用途に応じて使えば良いが、今回は Say a phrase with a text ingredient を利用する。これは音声入力したテキストをアウトプットに渡せる。今回は電灯の操作なので、オンオフを渡して制御する。

入力項目は次の通り。

設定項目 設定値
What do you want to say? 寝室ライト $
What's another way to say it? (オプションなのでよしなに)
And another way? (これもオプションなのでよしなに)
What do you want the Assistant to say in response? 寝室ライト $ しました。
Language Japanese を選択

What do you want to say? の設定が肝となる。 寝室ライト $と設定したが、「寝室ライト」の部分が 「OK Google,」の後に続くコマンドワードで何をしたいのかを示すキーワードとなる。 「$」はコマンドワードのパラメータとして設定できる音声入力のプレースホルダーで、ここでは「オン」あるいは「オフ」を想定している。 つまり、「OK Google, 寝室ライト オン」とGoogle Homeに話しかければ、このアプレットが起動して、$=オン がセットされるということとなる。

What do you want the Assistant to say in response? は音声コマンドを復唱させることにより、入力が正しいかどうか確認するために設定する。

続いて、アプレットの後半、アウトプットの設定。 このアプレットの目的は beebotte にメッセージをパブリッシュすること。パブリッシュの手段としてはbeebotteのREST APIを利用する。 そのため、アウトプットには Webhookを利用する。

設定は次の通り。

設定項目 設定値
URL http://api.beebotte.com/v1/data/publish/<Channel名>/<resource名>?token=
Method POST を選択
Content Type application/json を選択
Body {"data": [{"room": "bedroom","device": "light","action":"{{TextField}}"}]}

URL にはbeebotteのメッセージパブリッシュのRESR APIを指定する。先に登録した チャネルとリソースをパスにはめこんで、認証のためクエリパラメータtokenChennel token を設定する。 前述の スクリーンキャプチャに合わせると http://api.beebotte.com/v1/data/publish/MyIoT/light_at_bedroomというのがAPIのURLとなる。

Body には、APIに送信するデータを設定する。ここにはJSON形式で、後にNode-REDで処理する時に必要になりそうなパラメータを送信できるようにする。{{TextField}}には $ にセットされたテキスト、すなわち「オン」または「オフ」が入ることになる。

beebotte に Node−REDで接続

前節までで、Google Homeへの音声コマンドが、beebotte 上のメッセージに変換されるところまでができている。 あとは、Node-REDでそのメッセージを受け、処理するだけ。

Raspberry Pi やその上で動くNode-REDの準備は次に書いてあるとおり。

Node-REDではデフォルトで MQTTがサポートされている。 beebotte と連携するには mqtt in ノードを利用する。

mqtt in ノードの設定はこんな感じ。 トピックには [Channel名]/[resource名]を設定する

f:id:HeRo:20180107191427p:plain

mqtt in ノードのサーバ設定は別フォームが開くので次のように設定する。 セキュリティタブで、ユーザ名に token:[Channel Tokem] を設定するのが肝。

f:id:HeRo:20180107190740p:plain

設定項目 設定値
サーバ mqtt.beebotte.com
ポート 8883
SSL/TLS接続を使用 チェックする。TLSの設定は不要
ユーザ名 token:[Channel Tokem]

設定が正しく、beebotte に接続できる場合にはフローのデプロイ後、次のようにノードに 接続済 と表示される。

f:id:HeRo:20180107192117p:plain

動作確認

ここまでの作業を確認すために次のようなストリームを作って、Google Home => Node-REDに届くメッセージを確認する。

f:id:HeRo:20180108000451p:plain

OK Google, 寝室ライト オン」とGoogle Homeに話しかけて、 Debugノードのデバッグ出力に送ったメッセージが出力されれば成功。

さて、

この先は Node−REDでフローを作って、赤外線を発するまでだけど、ここまでで結構長くなったので、続きはまたあとで。

その2に続く。

hero.hatenablog.jp


  1. IFTTTはまあ大丈夫だろうけど、デバイスメーカーのディスコンを嫌う割にbeebotteなどというイマイチ聞いたことないサービスを使って大丈夫か?とも思ったが、いざとなればHerokuでCloudMQTTを動かすという代替策があるので採用した。ただし、CloudMQTTは無料では同時接続デバイスが10と少々少ない。

*1:今はやっと手元に届いたところ。

Severlessをやってみた

AWS でちょっとした処理を実行するのに Lambdaはとても便利。

これまで、ちょっとしたことを Lambda で実装したことはあったのだが、 ちょっとしたこと以上のコードを AWS コンソール上で書くのはなかなかつらい。

そこで、Serverless Frameworkを使ってみようと思った。

当初、Serverless を使うメリットとして考えていたのは次のとおり。

  • ローカルの使い慣れたエディタで Lambda を実装できる。
  • Webpack + Babel プラグインを使えば、モダンな JavaScript の機能を使える。
  • デプロイも楽になりそう

試しに簡単なアプリケーションを書いてみた。

サンプルアプリのシナリオ

試してみる課題シナリオは次のとおりとした。

  1. Github にプルリクを作ったり、プルリクにプッシュすると CodeBuild でビルドを実行する。
  2. プルリクには Github の Status API で状態を通知する
  3. ビルドの開始を Slack にも通知する
  4. CodeBuild の処理終了を CloudWatch Event で受け、その結果を Github の Status API で更新する
  5. 結果を Slack にも通知する。

作ってみたコードはこちら => HeRoMo/ServerlessSample: My First Serverless Application Sample

やってみて

やってみてどうだったのか?感想を以下に述べる。

関連リソースも一緒に定義可能

予備知識少なめで取り掛かったのでドキュメントを読みながら進めた。 最初はトリガーとなる Amazon SNS 等は別に登録して用意しないといけないのかな? と思っていたのだが、 SNS を始め、API Gateway など、Lambda をトリガーできるリソースは serverless.yml の関数定義で一緒に定義できる。 これはとても便利。 また、Lambda から他の AWS のリソース・サービスにアクセスするのに必要な権限も serverless.yml で Lambda の実行ロールに追加できる。 とにかく、Lambda の実行に必要なものが serverless.yml で一元管理できるのが管理しやすくてよかった。 最初にデプロイした時、裏で Cloud Formation のスタックが作成、実行されてちょっと驚いたが、こういうことなら納得。 Cloud Formation の定義記法で、Lambda とは直接やり取りしない設定も定義できそうなので Cloud Formation も覚えねばと思った。

やはり async/await は便利

Webpack プラグインと babel を追加して、ES2015 から Node6.10 相当にトランスパイルするように設定した。 Lambda を使う動機としては AWS のサービス間の連携を開発するためのグルーコードとして便利というのもあると思う。 実際、AWSSDK は組み込みで動作するので適切な権限が Lambda の実行ロールに付与されていれば、別サービスにアクセスするのは それほど難しくない。 しかし、AWS の実態は HTTP ベースの API なので、呼び出しはどうしても非同期となる。非同期を多用する必要がある場合には もう、async/await が使えないと辛い体になってしまっているので、これは助かった。 AWS SDK の関数は現状、コールバック式なので、早く Promise を返すようになればと思う。

全般的な感想

前述したメリットはすべて享受できたと思う。 私は JS を書くときには Atom エディタを使うことが多いが、やはり使い慣れたエディタだと実装しやすい。 webpack でビルドできると、babel でトラスアイルできるだけでなく、自由に node モジュールを使うこともできる。 そのため Github API や Slack API のライブラリを利用でき、その点でも効率的に実装できた。 デプロイも sls deploy ワン・コマンドで済むのはとても楽だった。

テンプレートも作った

今後別のアプリケーションを実装するのに便利なように、webpack+babel を設定したアプリケーションの雛形を作った。 Serverless には Serverless アプリを GitHub から取ってきてそれをひな形に別のアプリを作成する機能が備わっている。 そこもよく考えられているなぁと感心した。

テンプレートはこちら。=> HeRoMo/sls-template: Serverless Framework application template

サイトをチェックするツールを作ってみた

ボランティアで防災・減災関連の web サイトの情報を収集している。そのようなサイトは災害時にわっと作られるが、その後、閉鎖されたり、メンテナンスされなくなってしまうサイトも少なくない。同様な地方自治体のホームページも少なからずある。

時々そのようなサイトの状況をチェックするのに便利なツールがなかったので自分で作ることにした。

最初は HTTP でアクセスしてレスポンスをチェックするだけと思っていたので curl 等を使ってちょっとしたスクリプトで済ませようと思った。だた実際に確認してみると個人の作った Web サイトは思ったより続けられていないことが分かった。 同じドメインが全く違うサイトに変わっていたり、閉鎖されて Not Found ページが返ってくるのにレスポンスコードは 200 だったり。ドメインも管理者も変わってなさそうだけど、内容が全く変わっているものもあった。

また、文章ではなく画像に情報が書いてあったり、地図等の外部コンテンツを埋め込んでいるサイトも多く、レスポンスのテキストにキーワードが含まれるかどうかだけではどうも判別が難しい。ということで、チェックは目視ということになるが、いちいち URL にアクセスしていチェックしなくても良いようにスクリーンショットをとる機能を実装することにした。

その他、必要な機能を検討し、以下のようなツールを作ることとした。

  • URL が有効かどうか実際にアクセスして確認する
  • その際、リダイレクトにも追従できる
  • レスポンスのステータスコードが 200 ならスクリーンショットを取る
  • 単一の URL でなく、URL のリストを読ませて複数のサイトを一度にチェックできる
  • 結果を再利用しやすい形で出力する

最初は Phantom.js を使って作った

HTTP のリクエスト/レスポンスだけなら色々方法はあるが、スクリーンショットを取るとなると使えるツールが限られてくる。 実行中にブラウザがぱたぱた動くのも鬱陶しいし、できれば GUI を持たないサーバで動かせるようにしたい。 となると、Phantom.js を使うのが最も手軽かと思い、最初のプロトタイプを作った。 実際には、Phantom.js を使いやすくラップしてくれる CasperJS を利用した。

最低限、動くものを作って、さあ、使いやすくブラッシュアップしていこうかなと思った矢先、Chrome 59 にて ヘッドレスブラウジング がサポートされた。それに伴い、Phantom のメンテナがやる気を失ったらしい(参考:Phantom.jsのメンテナー、プロジェクトの将来に疑問を呈し、その座を降りる

ということで、PhantomJS をインストールしなくても使えるような実装のほうが良いし、いまやシェアトップとなった Chrome を使うほうが、スクリーンショットの結果も普段使っているブラウザとを乖離せず良かろうとこの実装は捨てることにした。

ヘッドレスChromeへの変更

Chrome をプログラムから操るには、DevTool のリモートプロトコルを使えば良いようで、そのためのライブラリchrome-remote-interface を利用して実装を変更した。 このライブラリ、Chrome DevTools Protocolの機能をほぼカバーしているけれど少々プリミティブ過ぎて、使いづらかった。

例えば、通信が1つづつハンドリングできる。ページのレスポンスコードを取るのにページからリンクされる画像や CSS その他へのリクエストの中からドキュメントへのリクエストを探して得る必要がある。それぞれリクエスト ID が取れるのでその ID で再度結果を取得する必要があるとか、とにかく煩雑でやりたいことに対して実装しないといけないことが多かった。おまけに、Chrome そのものを起動する機能は持っていないので、GoogleChrome/lighthouseを使って Chrome の起動を実装する必要があった。

このまま実装を続けても、この先の機能追加とか辛そうと思っていた矢先、GoogleChrome/puppeteer を知った。

puppeteer に切り替え

puppeteer だと、次のようにほんの数行書くだけで、Chrome をヘッドレスで起動して、URL にアクセスしてスクリーンショットを撮ってブラウザを終了してくれる。

// puppeteer でスクリーンショットのサンプル
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const response = await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});
  console.log(response.status)

  browser.close();
})();

同じことを chrome-remote-interface を利用すると次のようになる。スクリーンショットの保存も自前で、しかもこれには Chrome の起動/終了が含まれていない。

// chrome-remote-interfaceでのスクリーンショットのサンプル。
// Chromeの起動/終了は別途必要。
const CDP = require('chrome-remote-interface');
const fs = require('fs');

CDP(async (client) => {
    const {Page} = client;
    try {
        await Page.enable();
        await Page.navigate({url: 'https://example.com'});
        await Page.loadEventFired();
        const {data} = await Page.captureScreenshot();
        fs.writeFileSync('scrot.png', Buffer.from(data, 'base64'));
    } catch (err) {
        console.error(err);
    } finally {
        await client.close();
    }
}).on('error', (err) => {
    console.error(err);
});

また、puppeteer だと、レスポンスコードも簡単に取れるが、chrome-remote-interface だと更に Network クラスを使って Page.navigate のレスポンスコードを探して取り出す必要がある。

これだけ比べても、puppeteer に乗り換えるメリットを感じたし、Chrome チーム謹製というのも安心感があった。 それで、再度実装を変更した。

site-checker として公開

で、作ったツールをこの度、公開した。

我ながらセンスのないベタなネーミングに嫌気がさすが、これというのが思いつかなかったし、npm ライブラリとしても使われていない名前だったので、この名前にした。

次のコマンドでインストールできる。

$ npm install -g site-checker

インストールしたら、次のコマンドで、スクリーンショットが取れる。

$ site-checker -u http://hero.hatenablog.jp

できることは次の通り。

特に、フルページとエミュレーションは puppeteer に切り替えてとても簡単に実装できた。

詳しい使い方はこちらを参照 => 使い方 · HeRoMo/site-checker Wiki

災害に関わる「言い伝え」をマッピングしてみた

今日は大晦日。2016 年も残りあと数時間。

振り返ると熊本地震、北海道の台風に鳥取地震、そして先日の糸魚川の大規模火災に、茨城での震度 6 弱と大きな災害が多かった一年だったように思う。

起こってしまった災害をなかったことにはできないが、そこから教訓や今後発生する災害の被害を減らすための知恵を得ることはできるはずと思う。

そのようなことは私でなくても考えていて、公開から少し時間が経ってしまっているようだが、消防庁が収集・整理した防災に関わる「言い伝え」 なる資料が 全国災害伝承情報:総務省消防庁 に公開されている。

そこには資料に添えて次のように記されている。

有史以来、全国で発生した災害は各地に多大な被害をもたらし、それらの災害の教訓は各地域において記録としてあるもの、図画として残されているもの、あるいは物語、ことわざとして伝承されているものなどがあります。  そのような災害にまつわる資料や情報は、これまで国として整理されず今日にいたっており、その多くが各地域に埋もれたままとなっています。  全国災害伝承情報は、そうした各地域に残る貴重な資料を、国として整理集約し、インターネットを活用し広く一般に公開することを目的としたもので、平成16年度から平成18年度にかけて都道府県や市町村などの協力をいただき、調査を通じて収集した情報を整理集約しました。  この情報を通じて、身近な地域に残されている災害に対する教訓を個々人に認識していただき、防災意識高揚に役立てていただくとともに、防災教育用の教材としての活用が図られることを期待しています。

防災に関わる「言い伝え」

防災に関わる「言い伝え」はなかなか興味深い資料で、全国の災害・防災に関する 800 弱もの言い伝えが一覧でまとめられている。 各言い伝えごとにそれが伝わる自治体名が記されているので地図上にマッピングしてみた。なお、統廃合により自治体が消滅しているものは引き継がれた現在の自治体にマッピングして旧名を併記している。

地図上の色の付いたエリアをクリックすると、その土地に伝わる言い伝えを右ペインに表示する。

防災に関わる「言い伝え」MAP

地図化したデータを見てみると北海道、東北の日本海側と近畿地方で言い伝えが少ないのがわかる。これらの地域ではもともと言い伝えが少なかったのだろうかそれとも、伝承が途絶え収集できなかったのだろうか。近畿は長らく都があり、大勢暮らしていたはずなのに、どうして少ないのだろうか。 逆に伝承の多い地域は、古くから多くの災害に見舞われてきた結果、地域の知恵として言い伝えが受け継がれてきたということなのであろう。

個々の地域を見ていくと、言い伝えの内容からどのような災害に苦しんできたのかがわかる。水害に関する言い伝えが多い地域は水害に、地震に関する言い伝えが多ければ地震に苦しめられることが多かったのであろう。

栃木県には地名に関する言い伝えが多い。古い地名を見るとその土地でよく起こった災害がわかるようだ。災害の記憶をいまに伝える日本全国「あぶない地名」(週刊現代) | 現代ビジネス | 講談社(1/6)にも同じような記事がある。この記事にあるように安易に地名を変えてしまうのは良くないのではと思ってしまう。

言い伝えの類似性に目を向けると「地震が来たら竹やぶに逃げろ」に類する言い伝えは、全国的に多いのだなぁと気が付く。竹の根が地盤を強固にし地割れを防ぐことができると知られていたのであろう。

また、次のような言い伝えもある。

  • 地震のとき「マンダラッコ、マンダラッコ」と唱えるとよい。(神奈川県平塚市)
  • 地震のとき「マンザイロク、マンザイロク」といって、竹やぶに逃げる(新潟県新潟市
  • 地震のときは「まんぜえろく」と唱える(埼玉県毛呂山町

一説によると「まんぜいろく」とは「万歳禄」と書き、末永く神の恩恵を受けられますように、という祈願らしい。 同じような言葉が地域を超えて言い伝えられているのはなぜだろうか? 一見、つながりなさそうな地域で同じような言葉が言い伝えられているのも興味深い。昔は互いに交流があったのだろうか?

雑々とした印象・感想を綴ったが、眺めていると色々発見がありそうな資料だ。