Scalaで書いたWebアプリケーションのレスポンスにデプロイされているGitコミットのSHA1を含める

WebアプリケーションのレスポンスヘッダにGitのsha1を含めておくと、デプロイに失敗していて古いバージョンだった (そのせいでおかしかった) ということに気がつきやすくなって多少便利。

Perl (PSGI) だと、 `git rev-parse HEAD > VERSION` などしておいて Plack::Middleware でファイルの内容を読んで `Plack::Util::header_set` と `Plack::Util::response_cb` で用が済むけれど、Scalaコンパイル時にいろいろできそうな気がしたので調べた。

sbt-buildinfoでビルド情報をcase objectに出力する

sbt-buildinfoというsbtプラグインを使うとプロジェクト名やバージョンなどの情報を含むcase objectのコードを生成してくれる。

Usageより:

lazy val root = (project in file(".")).
  enablePlugins(BuildInfoPlugin).
  settings(
    buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),
    buildInfoPackage := "hello"
  )
GitHub - sbt/sbt-buildinfo: I know this because build.sbt knows this.

`build.sbt` にこう書いておくと `hello.BuildInfo` に `name` や `version` が埋められたcase objectを定義するコードが出力される。

sbt-gitでGitのコミット情報をプロジェクトのバージョン文字列にする

sbt-gitというsbtプラグインを使うとプロジェクトのバージョン文字列をGitのコミット情報から生成することができる。

`git describe` を使うようにするとか、自分でsha1を加工する (e.g. 先頭8文字だけを使う) ことができるので使い勝手がよい。

タグは使っていないのでsha1の先頭8文字を埋めるようにしている。

lazy val root = (project in file(".")).
  enablePlugins(GitVersioning).
  settings(
    git.formattedShaVersion := git.gitHeadCommit.value map { sha =>
      sha.substring(0, 8)
    }
  )

Playのレスポンスを加工してバージョンを含める

Action.async { implicit request =>
  val result = ???
  result.withHeaders("x-version" -> app.BuildInfo.version)
}

こういうかんじ。

筋トレティータイム

僕はコーヒーが苦手で、飲むと必ずお腹を壊すので避けるようにしている。

その代わりではないけれど、気分転換したいときには紅茶を淹れて飲むことが多い。

コーヒーと違って蒸らし時間があるので待ち時間がどうしても発生する。ティーバッグを淹れてそれを処分する関係上、自席にも戻りづらい。

そこで腹筋ローラーの出番となる。

もともと学生時代と比べて筋肉の衰えが気になりはじめたので買ってオフィスに送ったのだけれども、持って帰るのが面倒で置きっぱなしにしていた。

あるとき、ふと思い立って紅茶を蒸らしているあいだに腹筋ローラーをしてみたらちょうどいい時間だったので、これ幸いとしばらく続けている。

紅茶の蒸らし時間はだいたい2分くらいで、慣れていないうちは10回腹筋ローラーするだけでもそれくらいの時間がかかる。

数回続けると慣れて素早くできるようになってくるので、成長と共に蒸らし時間あたりの回数が増えていって、ティータイムと共に成長の実感が得られて思わぬ喜びがあった。

腹筋ローラー、1オフィスに1台オススメです。

デフォルトでHTTPレスポンスがキャッシュされるのはどんな場合か

HTTP APIのキャッシュ戦略を考えながら、ふとcache-controlなどを指定しなかったとき、デフォルトのキャッシュ戦略はどのように定義されているのか気になったので調べた。

Storing Responses in Caches

RFC7234のStoring Responses in Cachesにレスポンスをキャッシュしてもよい条件が述べられている。

A cache MUST NOT store a response to any request, unless:

  • The request method is understood by the cache and defined as being cacheable, and the response status code is understood by the cache, and
  • the "no-store" cache directive (see Section 5.2) does not appear in request or response header fields, and
  • the "private" response directive (see Section 5.2.2.6) does not appear in the response, if the cache is shared, and
  • the Authorization header field (see Section 4.2 of RFC7235) does not appear in the request, if the cache is shared, unless the response explicitly allows it (see Section 3.2), and
  • the response either:
    • contains an Expires header field (see Section 5.3), or
    • contains a max-age response directive (see Section 5.2.2.8), or
    • contains a s-maxage response directive (see Section 5.2.2.9) and the cache is shared, or
    • contains a Cache Control Extension (see Section 5.2.3) that allows it to be cached, or
    • has a status code that is defined as cacheable by default (see Section 4.2.2), or
    • contains a public response directive (see Section 5.2.2.5).
RFC 7234 - Hypertext Transfer Protocol (HTTP/1.1): Caching

(マークアップは筆者による)

Cacheable Methods

まず1つ目でHTTPメソッドがcacheableであるかとある。特に言及はないけどRFC7231で定義されている。

this specification defines GET, HEAD, and POST as cacheable, although the overwhelming majority of cache implementations only support GET and HEAD.

RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

注意したいのはPOSTもcacheableである点。「ほとんどの実装は対応していないけどね」と但し書きされているが、明示しておくに越したことはなさそう。

a status code that is defined as cacheable by default

巡り巡ってRFC7231のSection 6.1で定義されている。

Responses with status codes that are defined as cacheable by default (e.g., 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 in this specification

RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

501 Not Implementedはキャッシュ可能と定義されているので、5xxがキャッシュされないという認識でいると注意が必要かもしれない。

その他

range付きリクエストのレスポンスのキャッシュ戦略については別途述べられているが、必要としなかったので読んでいない。

結論

  • HTTPメソッドがGET, HEADもしくはPOSTの場合で、かつ特定のステータスコードの場合はキャッシュ可能と定義されている

まずどんなcache-controlが付けられそうか考えるだけでも、HTTP APIの特徴を掴めそうなのでこれからもやっていきたい。

nginx: 同じ configuration level で auth_basic off は指定できない

nginx の ngx_http_auth_basic_module を使って BASIC 認証をかけることができる。

ドキュメントを読むと明示的に `auth_basic off` を指定することで BASIC 認証を切ることができそうだったので試したけど、うまくいかないケースがある。

結論からいうと `auth_basic off` は上位のコンテキストから継承した設定を打ち消すためのもので、同じコンテキストで指定された `auth_basic` は打ち消せない。

実際のコード例:

# app.nginx.conf
server {
    server_name app.example.com;

    include "/path/to/config/auth.nginx.conf"; # 同じコンテキストで auth_basic が指定されている
    auth_basic off; # 祖先コンテキストではなく同じコンテキストなのでダメ
}

auth.nginx.conf:

auth_basic "Authentication Required";
auth_basic_user_file "/etc/nginx/conf/auth.htpasswd";

実際に config test を行うとエラーになる:

nginx: [emerg] "auth_basic" directive duplicate in app.nginx.conf:5
nginx: configuration file /etc/nginx/nginx.conf test failed

これはドキュメント中の以下の文章で示されている:

The special value off allows cancelling the effect of the auth_basic directive inherited from the previous configuration level.

http://nginx.org/en/docs/http/ngx_http_auth_basic_module.html#auth_basic

生活における夢を集めるTrelloのボードを作った

f:id:aereal:20170414214141p:plain

プライベートで締切のあるタスクや、忘れてはいけないタスクは To Do 管理ツールでリマインドしている。

それらのようなタスクは、誰かに依頼されるだとかして具体的なタスクとして直接インプットされる一方、日々の生活における夢はたとえば「石油を掘り当てたい」といった曖昧でふわっとしているし、なんなら実現可能性なんてものを考えていない。

ふとした瞬間に省みると、自分が細々としたルーチンを繰り返す生活の奴隷に思えることがあってげんなりする。

しかし自分はまだまだ生活に夢や希望を抱いていて、壮大とはいえぬものの少なくないそれらを支えにして生きているわけだから、目に見えるようにしておくと楽しいのではないかと思って夢を集める場所を作った。

それがタイトルにあるとおり Trello のボードでその名も「生活の夢」。

Trello にした深い理由はなく、たまたま楽しい気分で Trello を見ていたら思いついただけ。

ルールや運用なんてものは存在せず、ただ思いついたときに思いついたことを書いていくだけ。達成したら Done に移す。

達成したタスクを捨てたりせずただ Done と名付けられたリストへ移動するやり方は、カンバンに由来するもので、叶った・叶えた夢が集まってくるというところがとても楽しい。

最近は「テレビを買う」という夢を叶えた。「テレビを買う」ことがなぜ夢たりえたかというと、あると楽しいだろうけれど必要というわけではなく、そのわりに高いのでどうかと思っていたから。贅沢ともいう。

世の中においては理性的な振る舞いが求められるので、この Trello の前では自分の欲望に素直になろうと思います。

YAPC::Kansai 2017 OSAKAに参加した

YAPC::Kansai 2017 OSAKA

オーディエンスとして参加したので聞いたトークのメモなど。

Webアプリケーションのキャッシュ戦略とそのパターン (40min)

speakerdeck.com

主に Web アプリケーションの世界におけるキャッシュの話。目新しい話題ではないけれど、キャッシュは本質的にむずかしい話題だし、よくまとまった内容なのでこれからキャッシュの戦略を考える際の議論に役立ちそう。

「パターンに名前を与えると、名前を使って認知し議論できます」という話がよかった。確かにどれもよくあるパターンだけど、普段会話するときは「get_or_set 的なやつで……」みたいな表現をしがちだったので、名前をつけることだけでもとても意義のある内容だと思う。

後日 id:moznion さんと個人的に会話して Varnish の話をするなど、発表をもとに会話が生まれる技術カンファレンスの原体験みたいなことが起きて、そういう意味でもいいトークだったなーと思う。

いい話聞けたし、最近自分も Varnish を触っているので YAPC::Fukuoka 2017トークに応募しようかな、と思った。

Perl でわかる!サーバレス (20min)

www.slideshare.net

別のところ (社内だったかも?) でも「ServerlessってつまりCGIじゃん」みたいな話を聞いたので、静かに普及しつつある見解っぽい。

Microsoft Azure Functions のランタイムが公開されているところは後発ならではでよいなと思った。

利用しているBaaSが終了するときにすべきこと または Parse.com の終了と私たちの取り組み (20min)

speakerdeck.com

parse.com 終了は対岸の火事だったけど、このトークを聞きながら何度か自分のことのように肝が冷えることがあって、臨場感のあるトークだった。

撤退の作戦を考える際に DNS のレイヤからアプリケーションのレイヤまで検討できるだけのエンジニアがいるチームだからこそ切り抜けられたのかもなあ、と思った。小さいチームっぽいけど、いいエンジニアが集まっているチームという印象を受けた。

あとこういう内容だとともすればヘイトばかり撒き散らしがちだけど、ケーススタディとして具体性があって役立ちそうだし、parse.com へのリスペクトを欠かしていなくて建設的で聞いていて安心感があった。

カヤックのゲーム開発・運用の「今」 力技と効率化の先に我々が目にしたものとは (40min)

speakerdeck.com

Google スプレッドシートプログラマブルであるという意味でエンジニアにも受け入れられていて、良くも悪くも「エクセル」はもはや表計算ソフトではなくて「エクセルなんだなあ……とか思った。

イベントでものすごいスパイクがやってくるとか、カラムにサイズの大きなデータを入れていて転送量が逼迫するとか、心当たりのあるできごとが多くて他山の石感があった。

マスターデータとコードでは、コンフリクト解決の方法や頻度が違うので別リポジトリにしているのはなるほどってかんじ。

はてなシステムの考古学 (40min)

speakerdeck.com

15年の歴史をご紹介という内容。

こうして俯瞰してみるとコンウェイの法則のお手本みたいだなーと思った。人が増えてきて効率化のために Ridge のようなフレームワークが生まれ、さらに増えて人の多様性が増すと実装としてのフレームワークが薄まる揺り戻しが起きる、というような。

感想

気付いたときには懇親会のチケットが売り切れていて、それだけが個人的に残念であったものの、イベントとしてかなり濃い Perl トークが多くて楽しかった。

次回は福岡で、温故知新にかけて未来をテーマとのこと。福岡観光したいしトーク応募考えます。

Git であるディレクトリ以下に最近追加されたファイルを見る

git log \
  --grep 'Revert' --invert-grep \ # revert コミットを除く
  --diff-filter=A \ # 新規追加のみ
  --format='' --name-only \
  -- lib/Plack/Middleware

lib/Plack/Middleware/ 以下に最近ファイルを新規追加したコミットの一覧:

lib/Plack/Middleware/IIS7KeepAliveFix.pm
lib/Plack/Middleware/IIS6ScriptNameFix.pm
lib/Plack/Middleware/LighttpdScriptNameFix.pm
lib/Plack/Middleware/Head.pm
lib/Plack/Middleware/Recursive.pm
lib/Plack/Middleware/HTTPExceptions.pm
lib/Plack/Middleware/BufferedStreaming.pm
lib/Plack/Middleware/Dechunk.pm
lib/Plack/Middleware/Log4perl.pm
lib/Plack/Middleware/LogDispatch.pm

Vue.js で分割払いの手数料をわかりやすく表示するページを作った

Card Payment

リポジトリ: playground/card-payment at gh-pages · aereal/playground · GitHub

実質年率って意味わからないし計算がめんどうなので、金額と支払い回数を入れると総支払い額と月々の支払い額を表示するページを作った。

普段自分が使うブラウザで動けばいいので arrow function を生で使ってままで、トランスパイラを使って ES5 に変換していない。

Vue.js はライブラリを読み込んでさっと使いはじめられてよい。

UIKit という CSS ライブラリを試してみた。Bootstrap と同じくらいの範囲をカバーしつつも、その名の通りフォームとか操作するためのモジュールがたくさんある。

クラス属性を使ったり `uk-grid` のようなカスタム属性を使ったりと節操がない印象。

分割払いしまくりたいわけではなく、むしろカジュアルに手数料の高さなどを確認して戒めようというもの。

……X-T2 ほしい。

Go の text/scanner で改行をトークナイズしたいときは

Go の text/scanner はレキサを書く面倒な仕事の大半をいいかんじにしてくれて本質的なコードに取り組みやすくとても便利。

しかしはてな記法Markdown のように、行全体が単なるテキストではなくいくつかのパターンからなる文法のレキサを書くときは、パターン中に明示的に改行を含めたい。

どういうことかというと:

line: inlines CR

みたいに書きたい。

しかし scanner.Scan が改行を含む空白文字をスキップしてしまうので改行をトークナイズする、ということができない。

結論からいうと Scanner.ScanScanner.Whitespace というフラグを見て空白文字かどうか判断するので、このフラグを変えて改行は空白文字ではないと教えてあげるとよい。

(よく読めば Scanner.Whitespace にコメントで説明が書いてあった)

実際のコード:

Support inline HTTP annotations by aereal · Pull Request #2 · aereal/go-text-hatena · GitHubn

WANDRD Prvke Pack を買った - カメラバッグ考2017

PRVKE Pack というバックパックを買ったのでそれの紹介と、バックパックを選ぶにあたっていろいろ比較検討したメモについて書いておく。

新たなバッグに求めること

  • 素早くカメラにアクセスできること (サイドアクセス)
  • フルサイズ DSLR とレンズが入ること
  • 見た目

昨年、フルサイズの PENTAX K-1 に買い替えたうえ、さらに DFA☆ 70-200mm F2.8 という望遠ズームレンズも買い足した。

望遠ズームは大きいし重いので、これに加えていくつかのレンズを入れて持ち歩けるだけの容量のあるバッグを欲しくなったというのが、そもそものきっかけ。

機材が増えて多少重くなっても体に負担がかからないようバックパックを求める一方、レンズ交換が手間にならないようカメラやレンズの気室が分かれていることも条件とした。

他に検討したバックパック

Google Spreadsheet に諸元などをまとめて比較し、PRVKE Pack に加えて以下を最後まで検討した:

  • Chrome Niko Pack
  • ZKIN Raw Yeti
  • Thule Covert DSLR Rolltop Backpack

Chrome Niko Pack は要件をほとんど満たしていたものの容量が少なかった。 24L くらいで、実物を見るとかなり小さい印象だった。

ZKIN Raw Yeti は多少重いこと以外ほとんどよかったけれどいかんせん高すぎる。 ヨドバシで7万くらいして、レンズ買えるじゃん……と思って候補から落ちた。

Thule のバッグはだいたいよかったけれど見た目がやぼったくて実物を見るとあまり欲しくなくなった。

WANDRD PRVKE Pack

カメラと交換レンズを入れて撮りに出かける時のためのバックパックとして PRVKE Pack を選んで買った。

1ヶ月半くらい比較検討してみて求める要素をすべて持っていたので購入に至った。

Kickstarter で資金を集めながらバッグを作っているアメリカの小さなメーカーで個人輸入するしかなく、実物を手に取る機会がなかったのは若干不安だった。 その分、レビュー記事や YouTube の動画を通してサイズ感を念入りにチェックした。

ただほとんどのレビューは自分と比べると大柄な男性であることが多くて背負った印象はあまり当てにならなかった。 その点では SLRLounge のレビューに載っている写真は、レビュアーが女性だったので身長は近しく、レビュー本文も充実していてだいぶ有益だった。

価格は270米ドルで、PayPal 経由で決済した。当時のレートでは33,996円になった。 他に検討していた候補より少し高かったけれど、他はどれも求めるところにあと一歩足りない印象だったのでほとんど選択肢はなくなっていた。

気に入っているところ

  • サイドからカメラ用気室にアクセスできる
  • 手持ちのフルサイズと交換レンズを入れられるだけの容量がある
  • 加えて着替えなども入れられる容量がある
  • 荷室に背中側からアクセスでき荷造りしやすい
  • 見た目

公式では目安としてフルサイズの DSLR とズームレンズが2〜3本入るよと言っており、実際にそれくらいのサイズ感だった。 今の手持ちレンズで大きいものは DFA☆ 70-200mm F2.8 と DFA 15-30mm F2.8 で問題なく収まる。

一般にバックパックは中身を取り出すのに手間がかかるけれどもレンズ交換に手間はかけたくないのでサイドアクセスができると便利。

さらに飛行機に乗るときはなるべく受託手荷物を無くしたいのでバッグひとつで搭乗したい。

そうするとカメラやレンズに加えて着替えなども収められるだけの容量がほしい。 とはいえ数泊程度で、かつ下着や肌着くらいで済ませるので大した量ではないけれど。

clamshell-opening

あと荷室に背中側からアクセスできるためカメラやレンズを荷造りするときに楽。

いまいちなところ

だいたい満足しているけれど、強いて挙げると大きさと重さはちょっと気になる。

とはいえ普段使い用にはこれまで使っていたバッグも使うし、もっぱら旅行やレンズを持ち出したい時用のつもりなので、現実的には許容できる。

あと価格がちょっと高かった。2万円代だったらなあ、と思うけれどもう買ってしまったのでどうでもよくなった。

セットアップスクリプトでは必ず rbenv exec/rbenv which したほうがいい理由

簡単に言うと、rbenv の shims はシェルの `PATH` 探索とは独立して実行ファイルを探索しているため。

以下、単に rbenv としているけれどクローンである plenv なども同様と考えてよい。

shims とは

rbenv の README が詳しい。

Shims are lightweight executables that simply pass your command along to rbenv. So with rbenv installed, when you run, say, rake, your operating system will do the following:

  • Search your PATH for an executable file named rake
  • Find the rbenv shim named rake at the beginning of your PATH
  • Run the shim named rake, which in turn passes the command along to rbenv
GitHub - rbenv/rbenv: Groom your app’s Ruby environment

シェルは shims だけを探索する。

shims を実行すると rbenv によって Ruby のバージョンごとの実行ファイルが探索され、見つかった実行ファイルを `exec` する。

セットアップスクリプトの例

アプリケーションのセットアップを自動化するために以下のようなスクリプトが添えられていることが多いと思う:

#!/bin/bash

set -e

which bundle || gem install bundler
bundle install

Bundler がなかったら入れて、 `bundle install` で依存ライブラリを入れて終わり。

`which(1)` が成功するけど `rbenv which` が失敗する例

そんなことがあるのかと思うけれど、shims のライフサイクルを考えるとありうる。

rbenv で管理している Ruby のいずれかで Bundler をインストールした時点で `bundle(1)` の shim が作られる。

一方で、各バージョンごとの実行ファイルの実体は、各バージョンにおいてインストール (gem install) しないと存在しない。

つまり以下のようになる:

rbenv shell 2.0.0
rbenv exec gem install bundler
which bundle # => ~/.rbenv/shims/bundle: この時点で shim が作られる
rbenv which bundle # => ~/.rbenv/versions/2.0.0/bin/bundle: 実体も作られる

rbenv shell 2.4.0
which bundle # => ~/.rbenv/shims/bundle: 既に 2.0.0 でインストールし shim は作られている
rbenv which bundle # => (fail): 2.4.0 ではまだインストールしていないので存在しない!

rbenv exec gem install bundler
which bundle # => ~/.rbenv/shims/bundle: 既に 2.0.0 でインストールし shim は作られている
rbenv which bundle # => ~/.rbenv/versions/2.4.0/bin/bundle: めでたく 2.4.0 に対応する実体もインストールされた

先に紹介したセットアップスクリプトは `which bundle` で shim が探索されて見つかってしまうので `gem install bundler` が実行されないが、そのアプリケーションで使うバージョンに対応する実行ファイルの実体は存在しないので `bundle install` が失敗する、ということが起きてしまう。

そこで rbenv exec/rbenv which

どうすればいいかというと、タイトルにあるとおり `rbenv exec` / `rbenv which` を使うとよい。

これらは基本的に `exec(1)` `which(1)` の shims を考慮するバージョンと考えてよいので単純にコマンドに前置するだけで対応はほぼ完了する。

まとめ

  • rbenv ないし rbenv のクローンを使っているときは `rbenv exec` `rbenv which` を使う

Perl で memcached を使うときは Cache::Memcached::Fast::Safe がよさそう

memcached はコマンドインジェクションの機会が主に2つあって、1つはプロトコルで定められたキー長250文字を越えた場合と、キーに改行文字を含む場合。

詳しくは: 第2回 memcachedのセキュリティと脆弱性:memcachedの活用と運用 実践編|gihyo.jp … 技術評論社

今やっているプロジェクトでは Cache::Memcached::Fast を使っているけど、安全なキーの構築は Cache::Memcached::Fast を呼び出すレイヤでそれぞれ行うようになっている。

キー長の制約を知らなくて250文字以上のキーを使おうとしてうまく動かなくてハマったし、そもそもよく知られたセキュリティリスクがあるならライブラリレベルで対策されているとよさそう。

ということで調べたら Cache::Memcached::Fast::Safe というのがあったのでこれがよさそう。

  • Cache::Memcached::Fast のサブクラスなので同じように使える
  • キーはデフォルトでサニタイズされる
  • ついでに get_or_set という便利なやつが生えている

markdown-toc: Go で Markdown の見出しをパースして目次を作る

github.com

Go でテキスト処理をする練習のために書いてみた。

mdtoc - The markdown ToC generator - ウェブログ - Hail2u.net が元ネタで再実装というかんじ。

小難しいことをやるとどうしても泥臭くなるけど、普段書いている高水準なコードはこれくらい泥臭くてともすれば効率が悪いんだろうな〜、ということを Go を書くたびに思っている気がする。

数字で振り返った2016年

社内 LT 大会で「数字で振り返る2016年」と題していろいろ個人にまつわる数字を集めたので整えたものを遅ればせながら旧年の振り返りとします。

Amazon で購入した金額

金額 前年比
2016 389,277 円 (124.54%)
2015 312,547 円 (136.27%)
2014 229,352 円 -

カメラやレンズなどは含んでいない。主に Kindle 本や生活用品など。

年間でサラリーマンの月収として現実味のある額 *1 ほどしか使っていないと考えるかどうか。

この調子でいくと来年は45万くらい年間で使う目測だけど、そんなに成長するかな、どうかな。生活用品に使う額は減りはしないだろうけど大きく増えもしないだろう。

額の増加が鈍化すると踏んで特に考えないことにする。増加率が130%に戻ったら考える。

写真ブログに投稿した記事数

記事数 前年比
2016 73 486.7%
2015 15 13.0%
2014 115 -
  • 2015年がやばかった
    • 怪我したの
    • 自転車に乗った

2015年はロードバイクにはまったこと、小指を骨折したことが重なって激減したという背景がある。

そう考えると2016年はロードバイクにあまり乗らず PENTAX K-1 など大きな買い物をした割には2014年の6割程度で奮わない。

写真ブログは自分が発表するに耐えると考えたものだけを載せる方針なので、目が肥えてハードルが高まったかもしれない。

また、最近は組写真っぽいアプローチについて考えはじめて、1記事で複数の写真を載せることが増えたように思うので、1記事1写真が多かった気がする2014年と比べて写真数では見劣りしないかもしれない。

とにかくアウトプットの機会を増やさないことには上達しないし、気が向いたときに載せるくらいのペースだと選りすぐりを発表する場としても片手落ちなので、今年は数を重視して2014年の1.5倍まで戻したい。

写真 (カメラごと)

機種 枚数 割合 採用率
all 4,925 - 17.6%
K-1 3,642 73.9% 19.7%
K-3 1,202 24.4% 11.6%
K-7 33 0.7% 36.4%
dp0 48 1.0% -

Lightroom より。これを算出したのは12月2日だったので、実際はこれより増えている。

採用率というのは Lightroom でフラグを立てて現像し Google Photos に上げた写真の数の割合。20%弱という微妙な数字が素人っぽい。

意外とK-3が多かったなという印象だけれど3月から4月の梅・桜シーズンにまだいたのでさもありなん。

写真 (レンズごと)

機種 枚数 割合 採用率
all 4,925 - 17.6%
70-200mm 231 4.7% 12.6%
35mm 89 1.8% 23.6%
55mm 785 15.9% 13.9%
31mm 2,557 51.9% 17.9%
77mm 886 18.0% 20.1%

こちらも Lightroom より。

31mm が大好きなことが使用率からわかる。一方で採用率は目立って高くないので、いいレンズだし気に入っているけれど、あとから自分の眼鏡に敵う写真が量産できているわけではない。

77mm は少し採用率が高いようにも見えるけど有意差はない。

Strava (自転車)

走行距離

実績 前年比
2016 226.6km 19.93%
2015 1,136.8km -

時間

実績 前年比
2016 15時間47分 -
2015 70時間23分 -

獲得標高

実績 前年比
2016 6,501m 29.72%
2015 21,868m -

まあ2015年が元気すぎた。時間は有限なので、写真を撮りにいけばロードバイクに乗る時間は減るし……という当たり前のことではある。

とはいえ運動不足を解消するためにも2015年ほどと言わずとももう少し乗りたい気はする。

日記

記事数 前年比
2016 161.45 94.41%
2016 148 86.54%
2015 171 54.11%
2014 316 -

減少傾向にある。家にいる時間が減ったからだろうか。そもそも2014年はプライベートでいろいろあってずっとなにか書いていた気がするし、2015年くらいがいつもの調子だったかも。

むすび

これらに加えてアフィリエイト収入について書いた (雀の涙ほど) けれど Amazon の規約的に公開するのはよくなかった気がするので省略。

趣味のことでもいろいろ数字を出してみると発見があっておもしろい。

ここでは書いていないけれど、入社して以来、月収を額面、控除額などつぶさにしてスプレッドシートに記録していて、毎月給料日に記録するたび控除額を書き入れて暗い気持ちになるし、ここ3年の控除額の推移を見るとさらに暗くなる。

今年は光熱費などの推移も記録していきたい。今年もよろしくおねがいします。

*1:月100万までが現実味を感じられる

はてなブログのデプロイを約6倍高速化したはなし

  • 今年、稼働中のサービスであるはてなブログのデプロイ方法を新しい方式へ無事故で移行し、従来と比べて約6倍速くデプロイできるようになりました。

この記事では、安全にデプロイ方式を変えたプロセスを順を追って紹介します。

はてなブログと継続的デリバリー

はてなブログは1日あたり平均して1.02回デプロイを行っています。これは土日を除いた週5日の営業日に対する平均です。ざっくりとした算出で、祝日は考慮していません。5月と9月の祝日を含めるともう少し多くなるかもしれません。

また、原則として休日の前日にはデプロイしないことになっています。もしもデプロイした変更にバグがあった場合、休日が明けてから対応するか、さもなくば休日中に対応する必要があります。 安定したサービスの提供のためと、開発メンバーの精神衛生のためにチームで取り決めました。

デプロイが遅い

はてなブログのデプロイにかかる時間を Mackerel のサービスメトリクスとしてプロットしています。

f:id:aereal:20161213183243p:plain

従来、はてなブログのデプロイには1回あたりおよそ300秒ほどかかっていました。 なお、ここでいうデプロイとは bundle exec cap deploy をシェルで実行して各ホスト上に変更が行き渡るまでの一連の流れを指します。

1回あたり300秒ということは、デプロイした変更に致命的な不具合があって変更を巻き戻したり修正をデプロイするためにさらに300秒かかるということです。

念入りに QA を行ったり、テストを充実させたり、事前に不具合を混入させない努力はもちろん行うべきですしはてなブログでも取り組んでいますが、根絶することは現実的ではありませんし悪い事態に陥ったときのことも欠かさず考えなければいけません。

リカバーに時間がかかる状況ではリスクのある変更をためらってしまいますし、事故が起きたときの影響・損失はできるだけ小さく留めたいものです。

複雑なデプロイ設定

遅いという現状がわかったところで実際にデプロイの仕組みへ手を入れて高速化を図りたいところですが、 Capistrano 2 と古い社内ライブラリを組み合わせた上、Thread を直接使い並列実行するためにほとんど魔改造といって良いほどの状態で、手を出しかねていました。

Capistrano 2 と古い社内ライブラリ、そしてはてなブログ独自の設定と、3つの実装のかけ算により複雑さは極まっていました。

このような状態で変更を加えようとしても、良くて袋小路に入って時間を浪費してしまう程度、悪ければ致命的な不具合により障害に繋がります。

そこでまずデプロイで起きる変更の仕様をテストコードへまとめ検証可能にしました。 テストが書けた・書こうという気持ちが挫けなかったのは「何が起きているかはわかるが、どうして起きているかわからない」類のコードだったからとも思います。

デプロイのテストを書く

デプロイの仕様とはなんでしょうか? end-to-end 的に考えると「追加・変更された機能を使えるようになる」ことでしょうが、あまりに漠としてテストへ落とし込みづらいです。

これから書こうとしているテストは開発者向けなのでよりホワイトボックス的に「任意のバージョンのアプリケーションがホスト上に配置され、アプリケーションプロセスが再起動される」とします。

より細かくは:

  • 任意のバージョンのアプリケーションが配置される
  • 任意のディレクトリにアプリケーションコードが配置されている (e.g. /apps/Hatena-Blog/releases/$VERSION)
  • ワーキングディレクトリがデプロイしたいバージョンのディレクトリの symlink になっている (e.g. /apps/Hatena-Blog/current -> /apps/Hatena-Blog/releases/$VERSION)
  • アプリケーションが再起動される
  • Plack::Middleware::ServerStatus::Lite のアクセスカウンタが0になっている

……くらいに分けられそうです。

アプリケーションが再起動したか確かめる方法は悩みましたが、後述するようにテストを実行する際はホストをロードバランサから外すため、リクエストを受け付けることはないという前提を立てて、アクセスカウンタがリセットされたことを指標に据えます。

リモートのホスト上でファイルシステムなどを見てアサーションを行うくらいであれば SSH 越しにシェルスクリプトを実行するくらいでもよいのですが、自動実行し、テスト結果を収集するところまで考えるとより高水準なフレームワークを使いたいところです。

まさしくこのような用途のためのフレームワークとして Serverspec があります。

たとえば「ワーキングディレクトリ (= /apps/Hatena-Blog/current) がデプロイしたいバージョンのディレクトリの symlink になっている」アサーションは以下のように書けます:

describe file('/apps/Hatena-Blog/current') do
  it 'is a symlink' do
    expect(subject).to be_symlink
  end

  it 'is linked to releases' do
    expect(subject.link_target).to be_start_with('/apps/Hatena-Blog/releases/')
  end
end

Plack::Middlware::ServerStatus::Lite のアクセスカウンタがリセットされている」アサーションは、素朴にレスポンスボディから正規表現でアクセス数を抽出します:

describe 'total access' do
  it 'looks like restarted' do
    total_accesses = `curl -s http://#{host}:#{port}/server-status`.
      each_line.
      grep(/\ATotal Accesses:/) {|l| l[/\ATotal Accesses: (\d+)/, 1] }.
      first.to_i

    expect(total_accesses).to be < 10 # デプロイ直後は10回もアクセスがきていないだろう
  end
end

テストフレームワークの実装そのものは RSpec なので RSpec のフォーマッタがそのまま使えます。JUnit 形式の XML を出力するフォーマッタを導入すれば Jenkins でテスト結果を収集するのも簡単です。

これでデプロイを行って事後条件が満たされているか自動テスト可能な状態になりました。

次はより高速なデプロイの仕組みについて検討します。

ボトルネックの発見、そして pull 型から push 型のデプロイへ

はてなブログのデプロイのボトルネックを明らかにするため、デプロイの所要時間をより細かに見てみます。

Capistrano によるデプロイはアプリケーションコードの配布 (deploy:update) やプロセスの再起動 (deploy:restart) などいくつかのタスクに分割され、それらを逐次 (あるいは並列に) 実行していき、すべて完了したらデプロイが完了、というモデルです。

そこで各タスクごとそれぞれにかかった時間を計測し、各タスクを Mackerel のグラフ定義の1メトリックとしさらに積み上げグラフにすることで、デプロイ全体にかかった時間を詳細に可視化してみます。

参考: Mackerel のグラフ定義

文章で表現すると煩雑で伝わりにくそうですが、実際のグラフを見るとわかりやすいでしょう:

f:id:aereal:20161213183243p:plain

update_elapsed が支配的であることがわかります。 update_elapseddeploy:update の実行にかかった時間です。 実際、デプロイ中も deploy:update で待たされる印象が強かったので主観とも一致しています。ここを改善できるとよさそうです。

なぜこんなにも時間がかかるかというと、中央の Git リポジトリからアプリケーションコードの変更を取ってくるアーキテクチャになっているのですが、この Git リポジトリがスケールしないためです。

詳しくは YAPC::Asia Tokyo 2015 で発表した「世界展開する大規模ウェブサービスのデプロイを支える技術」を参照してください。

さて、Git リポジトリボトルネックとわかったのですが、他にアプリケーションコードの配布の方法を考えてみると上記発表でも触れたソースコードをひとつのファイルにまとめて HTTP でダウンロードする方法が挙げられます。

むしろアプリケーションの配置を VCS からソースコードを取得するだけで済ませるというのは、事前のコンパイルを必要としないインタプリタ言語かつ Web アプリケーションという限定的なシチュエーションでのみ実現できることで、むしろ1ファイルにまとめてダウンロードさせるという方法はより古典的でありふれているでしょう。

ソースコードをまとめたアーカイブファイルを各ホストにダウンロードさせるよう指示するデプロイを仮に push 型デプロイとしますが、この push 型デプロイで高速化できるか・スケールさせられるかは、既に上記発表にあるように検証済みなので、新デプロイ方式は push 型デプロイにすることとなりました。

次は実際に新デプロイ設定を書きます。

新デプロイへの移行

既存の設定をベースに書き換えていくこともできますが、今回はスクラッチから書いていくことにし、リポジトリもアプリケーションコードとは別のリポジトリを作りました。

そもそも既存の設定が秘伝のタレ化して行き詰まっていたという背景がありますし、古い社内ライブラリは Git リポジトリからアプリケーションコードを配布するアーキテクチャを前提とした便利ライブラリという体裁が強いので、もはや有用ではなくなります。

新たなデプロイ設定は、まずステージング環境のデプロイに導入しました。アプリケーションの開発と同じように、まずスコープを狭めて安全な砂場で試します。

次にロードバランサから外した一部のホストのみにデプロイし、前述したテストを実行し、期待する事後条件を満たすか確認します。

Web アプリケーションを実行するホストだけではなくバッチジョブを実行するホストなどでも同様に検証し、問題なくデプロイされたことを確かめた後に、サービスのホスト全体のデプロイをついに移行しました。

念入りにテストを書いたりコードレビューをしてもらってはいるものの、やはり稼働中のサービスのデプロイ方法を変える瞬間ほど緊張した瞬間はなかなかありません。

幸いなことに事前の準備が功を奏し無事故で移行でき、現在も push 型方式でデプロイしています。

結果

f:id:aereal:20161213183331p:plain

従来の方式ではおよそ300秒ほどかかっていたデプロイが、新方式では平均でおよそ 50秒 に短縮されました。実に約6倍の高速化です。

従来は年間でおよそ20時間近く待ち時間が発生していましたが、新しい方式では年間3時間ほどに短縮されました。

不具合などによりリカバーを行う場合や、はてなブログチームでは導入していませんが Site Reliability Engineering における error budget に対するインパクトも大きく変わりえます。

『継続的デリバリー』の p.68 より:

開発・テスト・リリースプロセスを自動化することで、ソフトウェアをリリースする際のスピードや品質、コストがかなりの影響を受ける。 ......それによってビジネス上の利益をより素早く提供できるようになる。リリースプロセス自体の敷居が下がっているからだ。

今回のデプロイ改善は直接利益をもたらすものではありませんが、リリースマスターの拘束時間が減ることにより心理的な負担が減るなど、はてなブログというサービスを改善し安定した提供を続けるためにポジティブな影響を与えています。

まとめ

はてなブログのデプロイを高速化した背景と導入のステップについて紹介しました。

この記事では紹介しませんでしたが、実際にデプロイを高速化したことで障害に繋がることを防げたこともあります。 エンジニア個人としても普段行うデプロイが速く済むのは数字以上に快適で達成感もありました。


この記事は、はてなエンジニアアドベントカレンダー2016の16日目の記事です。

昨日は id:amagitakayosi でした。明日は id:ikesyo です。