値に変更があった時だけ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を作った

Awesome 某のスター付き一覧

Awesome Hoge のページをよく見ることがあり、特に iOS をよく見ていたのだが、スターの数をまとめて見たいと思い、主に自分用にスター付きの一覧を生成してみました。Awesome repositories の contributors と全てのレポジトリの developers の方々に感謝です。

URLはこれです。
Awesome repos with 🌟

GitHub API で取得した stargazers_count を使っています。 GitHub API、レポジトリの取得なら1時間に5,000回のLimitなので結構叩ける印象。

サーバは要らなそうだったので、AWSのサービスを組み合わせてサーバレスでページを生成しています。

  • Lambda でレポジトリ一覧を取得
  • まとめて GitHub API を叩くと Lambda がタイムアウトするので、一旦 SQS に JSON にしていれる。
  • CloudWatch Events で Lambda を定期的に起動して、 SQS のキューを受信しながら、 GitHub API を叩いて SimpleDB に結果を入れる
  • 定期的に、 SimpleDB のデータを Lambda で JSON にしてs3 に PUT する。

という風にしています。APIにも優しいように、1日かけて1ループするくらいの周期で処理をしています。

DynamoDB ではなく SimpleDB なのは、最後の処理で一括でデータを取得する必要があるのですが、DynamoDB はスループットの設定的に一括取得には向いていないので、課金的な意味で SimpleDB にしてます。無料枠を考えないと、ミニマムコストが SimpleDB はが$0で DynamoDB は$0.67なので。ただ、DynamoDBで読み書き1ずつにして、s3にPUTするときだけ scan しても、もしかしたら DynamoDB の方が安い、ということはあるかもしれません。この部分の比較は見たことがないというか、SimpleDBに関する情報をあまり見かけません。 Lambda からもデフォルトで使えるし、ちょっとした時に便利だと思うのですが、管理画面にも無いし、そのうちサービスが終わってしまうのではないかというのが心配です。

Lambda 開発環境は、最近知った node-lambda がシンプルで使いやすいです。手元での実行とデプロイが出来ます。

HTML側は、S3 + CloudFront です。最近 CloudFront が HTTP/2 に対応したので、個人的には静的ページの配信に HTTP/2 対応のサーバを立てる意味がほぼなくなりました。あと、React で描画してるので、対応してないブラウザでは見えないです。

JSON の URL はパブリックなので、直接叩いても良いです。見ればフォーマット何となくわかると思いますので、 jq とか使って絞り込めば、例えば 「Awesome iOSで、Buttonのカテゴリで、500スター以上付いているレポジトリを開く」とかもすぐ出来ます。

curl https://s3-ap-northeast-1.amazonaws.com/awesome-repositories/ios.json | gunzip | jq -r 'map(select(.category1 == "Button")) | map(select(.stargazers_count >= 500)) | "http://github.com/"+.[].path' | xargs open

※ 一気にブラウザ上でページが立ち上がるので、内容に注意して下さい!

最初 iOS だけ作ったら、他のものもほぼ同じコードで生成できたので、自分がよく使う言語だけですが、6個作りました。 Elixir 力を上げたい。

Awesome 某のスター付き一覧

AWS API Gateway を使ってサーバレスな ifconfig API を作ってみる

サーバが自分自身のグローバルIPアドレスを調べるとき面倒なことがよくあるので、API Gateway を使って ifconfig のようなことをする。単純に、リクエストしてきたIPアドレスを返すだけ。(こういうサービスは他にもあるけど、レスポンスが遅めだったり、信用できるか分からなかったりするので、自分で作るのが安全そう)

curl https://ip.ecp.plus

としたら自分のIPアドレスが返るというのがゴール。ブラウザで見ても同じ。ちなみに↑はもう動いているのでご自由にお使いください。

最初、API Gateway -> Lambda として、Lambda に IPアドレスを渡して、Lambda はそれを返して、API Gateway はレスポンスをそのままスルーする。というのをやったが、実は Lambda なしで出来たようだ。

はじめ Lambda を使ったバージョンをやったので、両方書く。

API Gateway の設定

Create Method

URLは短くしたいので、ドメイン直下をGETしたときに返したい。 / に対して Create Method で GET を指定する。

Method Request は認証などしないのでそのまま。

/ – GET – Method Execution

API Gateway にアクセスしてきた IP アドレスは、下記のようにして取れる。

{
"ipaddress" : "$context.identity.sourceIp"
}

のだが、API Gateway のデフォルトで割り振られる URL は非常に長く、自分のドメインで使いたい。Custom Domain Names を使うためには、TLS の証明書が必要なのだが、お手軽さが無いのでやめた。既に動いている Caddy でリクエストを転送することにした。

ただ、そうするとリクエスト元のIPアドレスが Caddy が動いているサーバの IPアドレスになってしまうため、ヘッダーの X-Forwarded-For を見ることにする。ここの先頭に、オリジナルのIPアドレスが入ってる。

{
"ipaddress" : "$input.params().header.get('X-Forwarded-For')"
}

Lambda

ipaddress として渡ってきたデータの中から、一番最初のIPアドレスを返すだけの処理を書く。

exports.handler = function(event, context) {
context.succeed([event.ipaddress.split(/\s*,\s*/)[0], "\n"].join(''));
};

/ – GET – Integration Response

再び API Gateway に戻ってきて、レスポンスの加工をする。一見、Output passthrough で良さそうだが、これだとダブルクオーテーションで囲まれたものが返ってしまう。

Content-Type: application/json で Mapping Template として下記のようにする。

$input.path('$')

これでAWS側の実装は完了。

Caddy の設定

ip.ecp.plus に来たものを API Gateway に飛ばす。API Gateway 側で GET / を叩くために、転送先の末尾にスラッシュを忘れず付ける必要がある。

ip.ecp.plus {
proxy / https://EXAMPLE.execute-api.ap-northeast-1.amazonaws.com/prod/
}

とりあえずここまでで完成。

だが、Lambda がどう見てもあまり意味がなさそうだったので、API Gateway だけで出来ないか見ていたところ、開発中に使う用途と思っていた Mock Integration を使えば出来た。

API Gateway の設定(再)

/ – GET – Method Execution

Integration type として Mock Integration を選択。Mapping template として

{
"statusCode" : 200
}

のようなものが定義されてるが、これがないと勝手にエラーにされてしまうのでそのままにしておく。

/ – GET – Integration Response

どうやら、Response 側でも、リクエストで渡ってきた値がそのまま取れるようなので、こちらにIPアドレスを返す処理を書けば良い。

#set($ipAddresses = $input.params().header.get('X-Forwarded-For').split(','))
$ipAddresses[0]
#set($dummy = "dummy")

最後の行でよくわからないことをしているが、レスポンスの末尾に改行を入れたいためこうしている。”\n” をどうにか入れたかったが、うまく行かずバッドノウハウ的な解決方法になっている…。本当は良い方法があるはずだ。

ここまでで完成。

尤も、こんな単純なものなら Caddy 動かしてるサーバで処理しろよという感じだけれど、最近 API Gateway にハマっているので作ってみた。API Gateway 単体で動かないと、Caddy のサーバが落ちたときに使えなくて激しく意味がないが、Custom Domain Names の設定までやるのは面倒であった。Route53 使ってたらシームレスに連携出来たりすれば良いのに、と思った。

(追記)CloudFront を使ってサーバレスに出来た

その後、Caddy の代わりに CloudFront を使えば、TLS証明書も無料で、好きなドメインで出来ることが分かった。

CloudFront の画面から、Create Distribution -> Web の Get Started で、作成画面へ入る。

Origin Domain Name は、API Gateway の Invoke URL を指定する。https:// を付けても勝手に外してくれるので、URLコピペで良い。stage名は消す。つまりドメイン部分だけを記述する。
Origin Path は、stage名を入れる。 prod なら /prod とする。

あとは項目を読みながら設定すれば良い。CloudFront は独自ドメインのTLS証明書が無料なので、とてもありがたい。注意点としては、 CloudFront にキャッシュされると前に実行した人のIPアドレスが返ってしまうので、キャッシュさせないようにしなければいけない。
Behaviors のタブで、初期から存在しているものを編集して、Object Caching : Customizeとして、Minimum TTL : 0, Maximum TTL : 0, Default TTL : 0 とすれば良い。

これで、自己管理するインフラ無しで、当初のやりたかったことが出来た。

API Gateway から Export した Swagger 用の YAML

https://gist.github.com/ecpplus/1373c676f602326955f8

AWS API Gateway を使ってサーバレスな ifconfig API を作ってみる

Amazon API Gateway で Endpoint URL のドメイン名によるエラーが起きることがある

やりたかったこと

https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/sample/api/product/{id}

というURLにアクセスすると、HTTP Proxy で設定した別のサーバの

https://example.com/api/product/{id}

へリクエストが飛ぶこと。

設定したこと

/api/product/{id} という Resource を作って、それを HTTP Proxy に /api/product/{id} としたかったら、[Integration Request] で、Endpoint URL として http://example.com/api/users/{id} を設定して、 [URL Path Parameters] で Name: id, Mapped from: method.request.path.id とする。

普通のドメインならこれで設定完了。

今回起きたこと

で、HTTP Proxy で設定するドメインに新しめのTLDを設定したら、うまく動かない(保存できない)という現象があった。紛らわしかったのが、

/api/products

という Resource は設定できるのに、{id} というパラメータをつけて

/api/products/{id}

とするとだめだった。

“Invalid HTTP endpoint specified for URI” と言われたら、ドメインを example.com にして試してみると良いかもしれない。

Amazon API Gateway で Endpoint URL のドメイン名によるエラーが起きることがある