値に変更があった時だけSlackに通知するAPIを作った

ユーザ数などを cron などで定期的に Slack に POST していたが、変化が無い時に通知されてもあまり意味がないので、変化したときだけ Slack に通知する、というのを Serverless で作ってみた。

出来ること

監視する任意の値に変化があった時に Slack に通知してくれる。

もう少し詳細に

監視する値を API に POST し続け、前回の値から変化があったら Slack に通知するというシンプルなもの。例えば、

  • ファイルの md5 を1時間おきに取って API に投げれば、ファイル内容に変更があったら分かる。
  • サービスの登録人数の count を1時間おきに取れば、変化があったときに分かる。
  • クローリングしているデータの最終更新日時の文字列を15分おきに投げれば、変化があった時に分かる。

など、 trivial なデータを気軽に監視できるようにする。というのがこのシステムの主な目的。

ちゃんと作ったシステムはデータ解析のための仕組みがちゃんとあるだろうけど、数時間で作った自分用アプリとかだとそういうのまで作らないので、こういうのがあると便利だなと思って作った。

Getting started

https://monit.ecp.plus/

Web インターフェイスを作ったので、これを使えば監視が始められる。値を入力して作成したら、 curl のサンプルコードが出てくるので、値のとり方だけ自分が取りたいものに合わせて、 crontab とかに登録しておけばOKだと思います。

API Gateway + Lambda + DynamoDB で作ったので、サービスが落ちる心配もないと思います。

GitHub

Value changing monitor

(View on GitHub)

API

監視するリソースはなんでも良いので、 Resource というあまりに抽象的な名前しか思いつかなかったがとりあえずこれで。

  • POST /resources
  • PUT /resources/:uuid

POST で監視対象のリソースを生成し、以後はそこで発行された uuid に対して PUT をしていくようにする。 POST するときに Slack の Webhook の情報 (URL, Channel, Template) を付けておいて、もし変更があったらまた POST して作ればいいやという方針にした。

これは serverless.yml に書くだけで済む。

POST の方を、createResource という function名にして、PUT の方を updateResource という function名にして、 URL から uuid という名前でパラーメータとして受け取るようにした。

Serverless で、 integration を指定しない時のデフォルトが、以前は lambda だったのだが、最近デフォルトが lambda-proxy になってた。 API Gateway のマッピングが変わるので、 Lambda 側で値の取り方が変わる。 lambda-proxy にすると、 path が、 pathParameters という名前になってた。

functions:
  create:
    handler: handler.createResource
    memorySize: 128
    integration: lambda
    events:
      - http:
          path: resources
          method: post

  update:
    handler: handler.updateResource
    memorySize: 128
    integration: lambda
    events:
      - http:
          path: resources/{uuid}
          method: put
          request:
            parameters:
              paths:
                uuid: true

DB

値は DynamoDB に貯めておく。 POST 時に初期値を設定出来て、以後は value のみを PUT していく。変更があった時は Slack に通知して、 DynamoDB の値を書き換える、というシンプルなもの。これなら書き込み&読み込みのキャパシティー両方1で良いので、1秒間に1回!以内のペースであれば月額$0.67で済む(大事)

Key-Value Store としてしか使わないので、AWS Console からテーブルを作った。 uuid を文字列でキーとしてみた。

IAMの設定は serverless.yml に書くだけでOK。今回は、 PutItem, GetItem, UpdateItem のみを許可したいので、下記のような感じにした。

iamRoleStatements:
  - Effect: "Allow"
    Action:
      - "dynamodb:PutItem"
      - "dynamodb:GetItem"
      - "dynamodb:UpdateItem"
    Resource:
      - "arn:aws:dynamodb:ap-northeast-1:1234567890:table/your-awesome-table-name"

Domain

API Gateway のカスタムドメインが未だによく分かっておらず、内部的には CloudFront の裏にいるのに、 CloudFront のようにシームレスに証明書をつけられなくてめんどくさい。Let’s Encypt とかでTLS証明書を自分で作って上げてみてもよいけれど、楽をするために前に CloudFront を立てることにする。なので、おそらく AWS 全体を見ると、 自分で立てたCloudFront -> APIGatewayの内部で使われているCloudFront -> APIGatewayの某かの経路 -> AWSLambda となっており、電気が無駄になっていてエコではなさそう。

API Gateway の前に CloudFront を置く時の注意点としては、動的なデータを扱いたい場合、

  • キャッシュの TTL を 0 にする
  • 必要なヘッダーがあったら転送する( Content-Type を転送しようと思ったが、JSONしかないので application/json でオーバーライドした)
  • 必要に応じて、GET, OPTION 以外のメソッドも許可(詳細に選べないので、今回は全部になった)
  • API Gateway 側に stage名がドメイン直下につくので、 CloudFront の Origin Path をそこに設定する。例えば /production とか。

CloudFront が出来たら、Route53 で Alias として設定すれば完了する。

Serverless

設定をしたら後は特に変わったことはしていない。
GitHub に設定諸々のコードを上げた。 value-changing-monitor

最近ローカルでの開発は、 node-lambda を使ってやっていたが、シンプルなものなら Serverless で作ってデプロイして確認という方法も楽で良いかもしれない。

Web interface

https://monit.ecp.plus/

curl で POST するのは面倒だったので、 mithril を使って Web interface を作ってみた。これも特に変わったことはしておらず特に面白くはない。

値に変更があった時だけSlackに通知するAPIを作った

PhoenixFramework の WebSocket に iOS からつなぐ

PhoenixFramework の WebSocket に iOS から繋ぐのはとても簡単だった。

Awesome iOS with 🌟 で、 Swift で書かれている中から Starscream を選んで使ってみた。README にでかでかと表示されたロボットがカッコよかったので。

PhoenixFramework は ES6 で書かれた WebSocket 通信のためのクラスが最初から用意されてて、その仕組みに乗るとすごく楽なので、Swift 側で同じ仕組みに乗るような通信をすればよい。PhoenixFramework で実装された WebSocket の Chat を使ってみる。

PhenixFramework では、特定の topic に join するというのがあるが、これは JSON に event: "phx_join" というのを付けて送ってる模様。また、まだ良く理解できていないが毎通信ごとに ref というカウンターをインクリメントしてるので倣ってみる。 topic には、Phoenix 側で使ってる topic をそのまま入れれば、あとは好きなように payload にデータを入れてやり取りすればOKなようだった。とても簡単。

下記のようなコードで、Phoenix 側と通信して、 JavaScript で繋がってるブラウザとやり取りが出来た。

PhoenixFramework の WebSocket に iOS からつなぐ

WKWebView で NetworkActivityIndicator を良い感じに表示(Swift3)

以前 iOS9 が出たときに書いたものが古くて使えなくなってたので、書き直した。

iOS9とSwift2の頃のやり方

iOS9 から追加された WKWebView で、networkActivityIndicator (iPhoneの上部のステータスバーで読込中にぐるぐるさせるやつ) を表示する方法。 UIWebView と WebView はもう使うなって API Reference に書いてある。

Swift3 になって書き方も変わっているのと、iOS9 から constrains の設定もしやすくなった。WebView の一種でコンテンツを読み込むとき、isNetworkActivityIndicatorVisible を処理しないことが無かったので、 pod でインストール出来るようにした。

EcpWebView (on GitHub)

続きを読む “WKWebView で NetworkActivityIndicator を良い感じに表示(Swift3)”

WKWebView で NetworkActivityIndicator を良い感じに表示(Swift3)

Mithril (JS Framework) を ES6 で書く

Mithril という軽量な JavaScript Framework を友人に勧められたので使ってみた。フレームワークとしてとてもシンプルなので、公式サイトの説明を読めばすぐ使い始められる。

ES6 で書く場合、 Mithril ES6 Components(gist) をベースにすればOK。と公式に書いてあった。

ES6 で書いてトランスパイルした後のファイルが、 React を使っていた場合だと 120KB くらいになってたが(もしかしたら手元の設定がダメなだけでもっと小さく出来るのかもしれない) Mithril を使ったら、24KB くらいになった。

公式サイトの Performance を見ると、ローディングもレンダリングもかなり速いようだが。 (公式URL: Mithril)

React のように仮想DOMを内部で持っていて、データバインディングして使えるので、React で書く感覚で書ける。試しにReact 使って書いたものを置き換えてみたらすぐ置き換えられた。

実装はシンプルで、JSの機能さえ要求を満たしていれば古いブラウザでも動くらしいので、追加でライブラリをロードすればIE7ですらいけるらしい。

あまり使い込んでいないけど、AJAXでファイルを読み込むのがフレームワークに組み込まれてて、 Promise のインターフェイスで実装されてるので個人的にはかなり使いやすい。尤も、AJAXでファイルロードするのは標準API使って書いても短いので、その他の部分でどうなってるかは分からないけれど。

Awesome Elixir with 🌟 の JS はこんな感じ。 JSX 的なライブラリも出てるようで、そっち使った方が見やすいかも。

Mithril (JS Framework) を ES6 で書く

Phoenix Framework で WebSocket のチャット

WebSocketでチャットという、遠い昔に流行った題材で、 Phoenix Framework を使ってデプロイまでやってみた。

URL

とりあえず Nginx の背後において動かしてみた。 EC2 の t2.nano インスタンスで動かしてみてる。割とサクサクしてる感じがします。
https://chat.ecpplus.net 2つブラウザ開くか、PCとスマホとかで見ると通信の速度が確認できます。

環境構築

elixir

elixir のインストールは Homebrew とかで出来ます。

$ brew install elixir
$ brew install rebar3 # 手元だと、prod の compile で必要だった
$ elixir -v
Erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.3.3
$ node -v
v6.6.0
$ npm -v
3.10.3

vim

vim-elixir, vim-phoenix を入れてみた。2個目はhaml対応してなかったので、fork して使った。 vim-phoenix は、 vim-projectionist で作られてるので、使い勝手は rails.vim と似たような感じになります。 rails.vim ほどは作り込まれていないけど、十分な感じ。 Rails の開発は、 rails.vim のおかげで3倍くらい速くなってる気がするので、 Phoenix Framework のファイル構成が Rails に似てるのもあって、慣れてきたらはやく書けそう。

というのを、dein.toml に追加しました。

# Elixir
[[plugins]]
repo = 'elixir-lang/vim-elixir'

[[plugins]]
repo = 'ecpplus/vim-phoenix'

開発前の準備

  • Static Assets をビルドするデフォルトのツールが brunch なのだが、慣れてるのと情報が多そうなので webpack に変更
  • Templates を記述するのが EEx (like erb)がデフォルトなのだが、閉じタグ書きたくないので haml に変更

という2点をやった。

brunch -> webpack

package.json

Delete brunch-config.js, package.json and initialize package.json.

$ rm brunch-config.js package.json
$ npm init

Install npm packages. I want to use sass, Bootstrap4, es2015, React but anything is OK.

Then I got a package.json!

webpack.config.js

When I compile for production, run

$ NODE_ENV=production node_modules/webpack/bin/webpack.js -p

Don’t forget to put .babelrc.

config/dev.exs

Change watchers from brunch to webpack. npm start is defined in package.json. And add .haml to live reloading hook.

--- a/config/dev.exs
+++ b/config/dev.exs
@@ -11,9 +11,9 @@ config :chat, Chat.Endpoint,
   debug_errors: true,
   code_reloader: true,
   check_origin: false,
-  watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
-                    cd: Path.expand("../", __DIR__)]]
-
+  watchers: [
+    npm: ["start", cd: Path.expand("../", __DIR__)]
+  ]

 # Watch static and templates for browser reloading.
 config :chat, Chat.Endpoint,
@@ -22,7 +22,7 @@ config :chat, Chat.Endpoint,
       ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
       ~r{priv/gettext/.*(po)$},
       ~r{web/views/.*(ex)$},
-      ~r{web/templates/.*(eex)$}
+      ~r{web/templates/.*(eex|haml)$}
     ]
   ]

web/static/css/app.sass

Delete app.css, phoenix.css(Bootstrap3). Then add app.sass like following.

I copied default _variables.scss for a template.

$ cp node_modules/bootstrap/scss/_variables.scss web/static/css/bootstrap_variables.sass

That’s it!

実装

コード全然整理されていないけれども、GitHub にコードを上げてあります。
(ecpplus/phoenix-framework-websocket-chat-example)[https://github.com/ecpplus/phoenix-framework-websocket-chat-example]

デプロイ

phoenix.server

Phoenix Framework の公式サイトにあったコマンドをほぼ持ってきただけだが、 Ubuntu で webpack のコンパイルが通らなかったので、 assets を手元で作ってからデプロイした。出来なかった理由は追ってない。

$ MIX_ENV=prod mix phoenix.digest

というので、 assets に hash 値をつけてくれる模様。 priv/static/ 以下に出力されているので、ここを webroot として扱えばOKなようだ。

アプリケーションサーバは、 MIX_ENV=prod mix phoenix.server で十分高速ということだったので、そのまま使ってる。

$ MIX_ENV=prod mix phoenix.server

とするとフォアグラウンドで立ち上がります。

$ MIX_ENV=prod PORT=4001 elixir --detached -S mix do compile, phoenix.server

とするとバックグラウンドで立ち上がります。

多分 elixir より堅牢な supervisor となる(ややこしい) ソフトウェアもない気もするので、これでとりあえずこのままにしてみようと思います。

nginx

TLS は Let’s Encrypt とかでサクッと用意して、それ以外の設定です。 WebSocket は /socket 以下につなぐようにしています。 proxy_http_version は 1.1 にしないと Nginx のエラーになりました。Nginxで動かす場合で調べると、書いてる人みんな同じ設定なので多分それで良いのだと思われるが、あまり調べてないです。 Static assets は Nginx で配信して、それ以外を phoenix.server という感じで動かしました。

負荷テスト

t2.nano インスタンスで、DBアクセスがないページに keepalive 付きで、100並列で20000回アクセスしてみた結果です。
アプリケーションの処理自体はほとんどないページなので、フレームワークが必ず行う処理の速度がこれくらいということだと思います。
この非力なサーバで1514.92リクエスト/秒も処理できてるので、しょぼいVPSでも十分実用的な速度が出そう。
この処理の間で、 beam のプロセスが CPU60% くらい使ってました、メモリはあまり変わらず3%くらいでした。

$ ab -k -c 100 -n 20000 "https://chat.ecpplus.net/"
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking chat.ecpplus.net (be patient)
Completed 2000 requests
Completed 4000 requests
Completed 6000 requests
Completed 8000 requests
Completed 10000 requests
Completed 12000 requests
Completed 14000 requests
Completed 16000 requests
Completed 18000 requests
Completed 20000 requests
Finished 20000 requests


Server Software:        nginx
Server Hostname:        chat.ecpplus.net
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128

Document Path:          /
Document Length:        905 bytes

Concurrency Level:      100
Time taken for tests:   13.202 seconds
Complete requests:      20000
Failed requests:        0
Keep-Alive requests:    19829
Total transferred:      30119145 bytes
HTML transferred:       18100000 bytes
Requests per second:    1514.92 [#/sec] (mean)
Time per request:       66.010 [ms] (mean)
Time per request:       0.660 [ms] (mean, across all concurrent requests)
Transfer rate:          2227.94 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5  53.5      0     915
Processing:    42   60  21.9     55     808
Waiting:       42   59  21.7     55     808
Total:         42   65  63.4     55     976

Percentage of the requests served within a certain time (ms)
  50%     55
  66%     57
  75%     59
  80%     60
  90%     66
  95%     89
  98%    162
  99%    303
 100%    976 (longest request)

keepalive なしだと、回線かクライアント側の問題で、サーバに全然負荷をかけられませんでした。
Nginx の CPU 使用が20%くらいで、beam はほとんど CPU 使ってない状態でした…。

~ ᐅ ab -c 100 -n 20000 "https://chat.ecpplus.net/"
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking chat.ecpplus.net (be patient)
Completed 2000 requests
Completed 4000 requests
Completed 6000 requests
Completed 8000 requests
Completed 10000 requests
Completed 12000 requests
Completed 14000 requests
Completed 16000 requests
Completed 18000 requests
Completed 20000 requests
Finished 20000 requests


Server Software:        nginx
Server Hostname:        chat.ecpplus.net
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128

Document Path:          /
Document Length:        905 bytes

Concurrency Level:      100
Time taken for tests:   263.151 seconds
Complete requests:      20000
Failed requests:        0
Total transferred:      30020000 bytes
HTML transferred:       18100000 bytes
Requests per second:    76.00 [#/sec] (mean)
Time per request:       1315.753 [ms] (mean)
Time per request:       13.158 [ms] (mean, across all concurrent requests)
Transfer rate:          111.41 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      148 1017 820.6    715   12429
Processing:    42  295 391.6    212   14791
Waiting:       42  235 373.8    160   14791
Total:        233 1313 918.9    991   16536

Percentage of the requests served within a certain time (ms)
  50%    991
  66%   1230
  75%   1468
  80%   1656
  90%   2233
  95%   3289
  98%   4166
  99%   4885
 100%  16536 (longest request)

実は ecpplus.net は t2.nano に置いてるのですが、動的なコンテンツを吐くの恐ろしすぎて WordPress や Sinatra で吐いてる HTML も全ページ Nginx のキャッシュにしてて、静的な以外配信してません。が、 Elixir なら割と行けそうな感じでした。メモリ的にも、フレームワークを起動しただけなら 10MBくらいしか消費しません。 Unicorn で動かしてるほぼ最小構成な Sinatra は6MBくらいなので、Phoenix が特に少ないわけではないかもしれないですが。

次にやりたいこと

  • AWS のサービスとの連携をやってみる
  • NoSQL との連携をやってみる
  • テストを書く
  • 良いデプロイ方法を調べる
  • iOSから叩く何かのAPIを作ってみる
Phoenix Framework で WebSocket のチャット