git push したらブログが更新されるようにした話。

概要

こんなややこしいことしなくてもいいなとも思うけど、こういうのが趣味なのでやっているという話。

詳細

更新の作業が面倒だと記事を全然書かなくなるであろうことは最初から分かりきっていたので後でなんとかしようと思ってたのだけど、遂になんとかした。

このサイトのリポジトリは bitbucket の無料プライベート枠で管理しており、 Webhook からのアクセスによって自分自身を更新できるような機能を実装すれば git push するだけで更新出来るようになるのでその様にした。

sinatra で bitbucket からの webhook を受ける。

特に言うことはない。任意に設定した URL に POST で JSON が飛んでくるので、

post '/webhook' do
  payload = JSON.parse(request.body.read)
  # payload から必要な値を使ったり適当な処理をして、適当な status code と body を返す。
end

とすれば良い。仕様は Manage webhooksEvent Payloads を見れば良い。

ただこの sinatra アプリケーション自体は Docker 内に閉じ込めているので、ホスト側に良い感じに通知して、通知を受けたホスト側が良い感じになんとかするような実装をする必要があった。

通知パターンの設計の勘所だけど、登場人物がお互いの事を完全に知っている必要があると容易に破綻してしまうので、そこだけは注意する必要がある。

それに bitbucket から飛んできた Webhook のリクエストにはさっさとレスポンスを返しておきたい。更新の成否を bitbucket 側に返す必要も今回の用途だと無い。

シェルスクリプトで何とかする。

悩んだ結果、Webhook からのアクセスが来たら Docker Volume でマウントしたホスト側にファイルを作るだけ作ったらさっさとレスポンスを返しつつ、別に tail -F で受けておいてその中でゆっくり処理するみたいな感じにした。

tail -F /path/to/file | while read -r LINE; do
    # $LINE を使ってなんか処理
    echo $LINE
done

こんな感じで tail -F にパイプで while read に食わせてやればコマンドに流せるようだ。シェルスクリプトの世界は奥が深い…。

実際に使うには、各所のエラーハンドリングとか、重複で起動しないようにしたりとか、大量の Webhook が来ても大丈夫なようにしたりとか、シェルスクリプト自身が変更されないように、あるいは変更されても大丈夫なようにしたりとか、成否は Slack に通知したいよねとか、まぁ考えることが結構あるので実装はやや複雑にはなる。

シェル芸をやりだすとキリがないし未来の自分が読めなくなるのであまりトリッキーな書き方はしないようにしているが、基礎的かつ嵌るポイントみたいなのはちゃんと押さえておきたい。

上記の while read -r を付けた話で言うと、例えば以下のワンライナーを実行した場合、

sleep 10 | echo 'print'

これは 10 秒経った後に print と表示される訳ではない。即座に print と表示され、10 秒後に操作が返ってくるという挙動を示す。

複数のコマンドをパイプで繋げたとき、プロセスはそれぞれ同時に実行されており、ただ単に入出力が繋がっているだけ、という理解を個人的にはしている。

tail -F からの出力が来る度にそれを利用する処理を実行したい、といった場合は間に while read を挟んでやるといったテクニックが定石なようだ。

しかしこの手法では行毎にプロセスが立ち上がるはずなので、大量のデータが流れてくる状況で出来るだけ短時間で処理する必要がある用途には向かないと思われる。まぁ把握だけしてれば良いだろう。

締め

今回は docker image の更新の必要がないように予め作っていた部分を利用して作ったため、docker image の更新が伴う更新は実現出来てない。

ただ docker image の build には Google Cloud Container Builder を使っているのでなんとでもなるというか、その内ちゃんと作ります。

(追記:2018/6/1)作った。