Quantcast
Channel: クックパッド開発者ブログ
Viewing all 726 articles
Browse latest View live

Ruby 2.5 の改善を自慢したい

$
0
0

技術部でフルタイム Ruby コミッタをしている笹田です。最近ひさびさに Ruby のライブラリに pull request をしました(show valid break point lines #393)。

12/25 のクリスマスに、Ruby 2.5 が無事にリリースされました(Ruby 2.5.0 リリース)。関係各位の努力に感謝します。いろいろなバグ修正、いろいろな新機能、いろいろな性能改善があります(詳細は、上記リリースノート、もしくは Ruby のソースコードにある NEWS ファイルをご参照ください)ので、試して頂けると良いと思います。そういえば、私がクックパッドに入社して初めての Ruby リリースでした。

前回の techlife ブログ( Ruby の NODE を GC から卒業させた )で遠藤さんが

クックパッドからの主な貢献としては、「trace 命令の削除による高速化」「分岐・メソッドカバレッジの測定のサポート」などがあります。

と書いていました。

リリースノートより「trace 命令の削除による高速化」について引用します。

命令列中のすべての trace命令を削除することで、5~10% の高速化を実現しました。trace命令は TracePoint をサポートするために挿入されていましたが、ほとんどの場合、TracePoint は有効にされず、これらの命令は無用なオーバヘッドとなっていました。Ruby 2.5 では、trace 命令を用いる代わりに、動的書き換えを利用します。詳細は [Feature #14104]をご覧ください。

それから、同じくリリースノートに、ブロックパラメータに関する性能改善について書いてあります。

ブロックパラメータによるブロック渡し(例:def foo(&b); bar(&b); end)が、”Lazy Proc allocation” というテクニックを用いることで、Ruby 2.4 と比べて約3倍高速化しました。渡されたブロックを、さらに他のメソッドに渡したい場合、ブロックパラメータを利用する必要があります。しかし、ブロックパラメータは Proc オブジェクトの生成が必要であり、ブロック渡しのためにはこれが大きなオーバヘッドとなっていました。”Lazy Proc allocation” はこの問題を解決します。詳細は [Feature #14045]をご覧ください。

これらは私の仕事だったので、紹介文を書かせて頂きました。他と比べて長すぎますね。まぁ、迫力があっていいんじゃないでしょうか。具体的な数字があると、うれしいですしね。

本稿では、これらの機能をもう少し深掘りして、リリースノートやチケットでの議論では出てこない、普段、私がどんなことを考えながら開発しているのかをご紹介できればと思っています。また、これらの目立つ改善以外の、Ruby 2.5 のために私が行ってきた活動についてもご紹介します。

trace命令の除去と命令の動的書き換え

まずは、リリースノートに書いてある「trace命令の除去」についての話です。

何を実現したいのか

この新機能については、福岡Ruby会議02でのキーノート「Rubyにおけるトレース機構の刷新」でお話ししました。

www.slideshare.net

というか、このキーノートに間に合わせるために開発予定を調整しました(EDD: Event Driven Development)。

Ruby には TracePointという機能があります(リファレンス。古くは set_trace_func)。何かあると、例えば行を越えると何かフックを実行する、ということに使います。例えばこんな感じ。

trace = TracePoint.new(:line) do |tp|
  p tp
end

trace.enable do
  x = 1
  y = 2
  z = x + y
end

は、TracePoint#enableのブロック内で TracePoint:lineイベントごとに TracePoint#newに渡したブロックを実行します。そのため、出力は次のようなものになります。

#<TracePoint:line@t.rb:6>
#<TracePoint:line@t.rb:7>
#<TracePoint:line@t.rb:8>

この機能を実現するために、VM が実行する命令列(バイトコード)中に、trace命令というものを、フックを実行する可能性があるところに沢山挿入しています。一番多いのは :lineイベント用に、行が変わる度にこの trace命令が挿入されています。つまり、5 行のメソッドには 5 つの trace命令が入っています。

TracePointって知ってますか?」と聞くと、多くの Rubyist は「知らない」と答えると思います。つまり、あんまり使われない機能なのですが、使われないと、trace命令は単なるオーバヘッドにしかなりません。つまり、多くの場合、この命令は無駄なわけです。この無駄を排除するためのコンパイルオプション(Ruby のコンパイラはいくつかコンパイルオプションを受け取ります)を指定すれば、TracePointは動かなくなるけどちょっと速くなる、ということができたのですが、そもそもコンパイルオプションが指定できることを知っている人はごく少数ですよね。

なお、Ruby 2.4 以前を利用しなければならず、Ruby プログラムを 1% でも高速化したい、という方は、プログラムの最初に RubyVM::InstructionSequence.compile_option = {trace_instruction: false}と書いておけば、trace命令を利用しなくなります(が、TracePointが利用できなくなるため、例えば byebug といったデバッガが利用できなくなります)。

どうやって高速化するのか:trace命令を排除

そこで、TracePointのために trace命令を利用する、ということをやめました。代わりにどうするか。命令列の命令を書き換える、ということにしました。

実際の例を用いて説明します。

x=1
y=2
p x+y+3

このプログラムは、Ruby 2.4 では次のようにコンパイルされていました。

# Ruby 2.4
0000 trace            1     (   2)
0002 putobject        1
0004 setlocal         x, 0
0007 trace            1     (   3)
0009 putobject        2
0011 setlocal         y, 0
0014 trace            1     (   4)
0016 putself          
0017 getlocal         x, 0
0020 getlocal         y, 0
0023 send             :+
0027 putobject        3
0029 send             :+
0033 send             :p
0037 leave 

いくつか trace命令が入っていることがわかります。これを、Ruby 2.5 では、

# Ruby 2.5
0000 putobject      1       (   2)[Li]
0002 setlocal       x, 0
0005 putobject      2       (   3)[Li]
0007 setlocal       y, 0
0010 putself                (   4)[Li]
0011 getlocal       x, 0
0014 getlocal       y, 0
0017 send           :+
0021 putobject      3
0023 send           :+
0027 send           :p
0031 leave 

このように trace命令を排除した状態でコンパイルしておきます。trace命令がないので、なんとなく速そう、という気分が伝わるんじゃないかと思います。伝わるといいな。

さて、TracePointを利用した時です。有効にした瞬間、Ruby プロセス中に存在するすべての命令列を探し出して、必要な形に変換します。今回の場合、次のように変換されます。

# Ruby 2.5 / Trace on!
0000 trace_putobject 1    (   2)[Li]
0002 setlocal       x, 0
0005 trace_putobject 2    (   3)[Li]
0007 setlocal       y, 0
0010 trace_putself        (   4)[Li]
0011 getlocal       x, 0
0014 getlocal       y, 0
0017 send           :+
0021 putobject      3
0023 send           :+
0027 send           :p
0031 leave 

最初の putobjecttrace_putobjectに変わったのが見て取れると思います。普通の putobjectTracePointについて何もしませんが、trace_putobjectは、まず TracePointについての処理を行ってから、従来の putobjectの処理を行います。

この手法は、「TracePointをオンにするタイミングで命令書き換えが起こるので、それが大きなオーバヘッドになる」という問題がありますが、そもそも TracePointは使われないので、問題ないと判断しました。

検討した点、苦労したところ

この辺から、リリースノートに書いていない話になります。

なぜ今まで trace命令を使っていたのか?

見ての通り、難しい話はないので、もっと前からやっておけよ、と言われるかもしれませんが、YARV 開発から10年以上たって、やっと入りました。

以前は、命令の書き換えをしないほうが、別言語への変換(例えば、C 言語への変換)がやりやすいかな、と思っていたからなのですが、最近、「結局そういうことやってないしなぁ」と思ったり(すみません)、現在 JIT コンパイラの導入の話が進んでいますが、「書き換えるなら一度 JIT コンパイル済みコードをキャンセルすればいい」という踏ん切りがついたためです。なら、どうせなら派手に書き換えるようにしてしまえ、と思い、このようにしてみました。

書き換えるに当たり、trace_ prefix 命令ではなく、trace命令を動的に挿入する、という選択肢もありました(これが、最も互換性に優れた方法です)。ただ、命令を増やすと命令アドレスを変更しなければならず、若干面倒です。そのため、各命令の位置は変更しない、という選択をしました(そのため、プロトタイプは一晩で実現できました)。今後、もっとアグレッシブに命令書き換えを行うためには、命令位置変更にも対応しないといけないと思っています。

trace命令を沢山入れると、TracePointを有効にしない場合の速度劣化を気にしなければなりませんでしたが、これからはこのオーバヘッドが気にならなくなります。そのため、TracePoint向けのイベントを追加できると思っています。例えば、定数やインスタンス変数をトレースしたり、メソッドを呼び出す caller 側をフックしたりすることができると思っています。

trace_ prefix 命令をいつ戻すのか

TracePointを有効にしている間は trace_ prefix 命令が必要です。ですが、無効にしたタイミングで、TracePoint向けの処理が不要になります。そのため、初期実装では、TracePointがすべて不要になったタイミングで、同じようにすべての命令列を探し出して元に戻す処理を入れていました。これは、TracePointをパタパタと on/off 繰り返すようなプログラムはないだろうな、という予測に基づく設計でした。上記福岡Ruby会議02で紹介した時には、このような設計をしていました。off にするときのオーバヘッドを軽減するための工夫も盛り込んでいます。ただ、ある程度のオーバヘッドは、やはり必要になります(具体的には、ヒープ上からすべての命令列を探し出す部分)。

しかし、一部のライブラリ(具体的に問題としてあがってきたのは power-assert)では、TracePointの on/off が多く起こるため、問題になることがわかりました。そこで、結局一度 trace_ prefix 命令に変更すれば、その後 TracePointを無効にしても、そのままにしておくようにしました。TracePoint向けのチェックがついてしまい、trace命令があったときと同じように、若干遅くなるのですが、TracePointをちょっとだけ on にしてその後は利用しない、というシチュエーションは、あまりなさそうかな、と思い、最終的に「戻さない」とすることにしました。

非互換の対応

この変更にともない、バックトレースや TracePointなどで得られる行番号がずれる、という問題がありました。多少、変わっても、人間が見る分には問題ないだろう、と思っていたのですが、人間以外が見る、具体的にはデバッガ等で特定の行番号(例えば、endの位置の行番号)に依存した処理があったため、byebug という有名な Ruby 用デバッガで問題が起こる、ということがありました。

問題は修正できたのですが、この問題が発覚した(教えてもらった)のが 12/23 で、リリースの直前でした。久々にリリース直前にたくさんコーディングをして(例年は、リリース直前には怖くてコードをいじれません)、なんとか問題ないところまでもっていくことができました。本件でお世話になった関係各位に感謝いたします。

我々も、もっとちゃんと著名ライブラリはチェックしておかないとな、という反省をするとともに、RC1 リリースなどでちょっと試してもらえないかと読者の皆様にお願いするところです。

Lazy Proc allocation によるブロックパラメータを用いたブロック渡しの高速化

Lazy Proc allocation というテクニックを考えて、ブロックパラメータを用いたブロック渡しを約3倍高速化することができました。

何を実現したいのか

あるメソッドに渡されたブロックを、ほかのメソッドに渡したい、というシチュエーションが時々あると思います。

defblock_yieldyieldenddefblock_pass&b
  # do something
  block_yield(&b)
end

block_passのようなメソッドを書くと思いますが、このときブロックローカル変数 bでブロックを受け取り、その受け取ったブロックを block_yield(&b)のように渡すことで、このような受け渡しを実現することができます(なお、ブロックを渡す他の(素直な)方法はありません)。

とりあえず、これで一件落着なのですが、ブロックローカル変数を使うと、yieldするだけに比べて遅くなってしまう、という問題が生じます。というのも、ブロックローカル変数は Procオブジェクトを受け取るのですが、この Procオブジェクトの生成が重いためです。なぜ遅いかを大雑把に言うと、関連するローカル変数領域などをメソッドフレームをたどって芋ずる的にヒープに確保しなければならないためです(この理由をより深く理解するには、Rubyのしくみ Ruby Under a Microscopeなどをご参照ください)。

渡されたブロックという情報を他のメソッドに渡すために、ブロックパラメータを経由してしまうため、Procという、より冗長なデータを受け取ってしまい、遅い、という問題です。これを解決したい。

ポイントは、ブロックの情報だけだったら軽い、というところです。

どうやって高速化をするか:Lazy Proc creation

block_yield(&b)のようにブロックの情報を渡すだけなら、Procは必要ありません。なので、ブロックローカル変数が block_yield(&b)のように、他のメソッドにブロックを渡すだけであれば、Procを作らなくてもよいようにすれば速くなりそうです。本当に Procが必要になるまで Procオブジェクトの生成を遅延する、だから Lazy Proc creation と名付けています。まぁ、ある意味自明な機能なのですが、それでも名前を付けると、なんかカッコいいですよね。なお、並列分散処理の分野で "Lazy task creation"という技法があります。あまり関係ないですけど、カッコいい手法なので興味があれば調べてみてください。

さて、ここで問題になるのは、「Procが必要ないのか?」ということを知る必要があることです。

defsample1&b
  block_yield(&b)
end

このプログラムは、bProcにする必要はありません。ブロックの情報のまま、他のメソッドに渡してやればいいからです。

defsample2&b
  b
end

このプログラムは、bProcにする必要があります。呼び出し側が返値として Procオブジェクトを期待する(かもしれない)からです。

defsample3&b
  foo(b)
end

このプログラムも、bProcにする必要があります。fooを呼んだ先で Procオブジェクトを期待する(かもしれない)からです。

こう見ると、block_yield(&b)のようにしか使っていなければ、bはブロック情報のままで良さそうです。では、次の例はどうでしょうか。

defsample4&b
  get_b(binding)
end

一見すると、bは触っていないので、ブロック情報のままで良いような気がします。が、bindingオブジェクトを用いると、そのバインディングを生成した箇所のローカル変数にアクセスすることができるので、get_bの定義を、

defget_b bind
  bind.local_variable_get(:b)
end

のようにすると、bの中身をアクセスすることができます。この場合、bsample4の返値になるため、やはり Procオブジェクトにしなければなりません。bindingが出たら諦める、という方法もあるのですが、bindingはメソッドなので、任意の名前にエイリアスをつけることができます。つまり、どんなメソッド呼び出しも、bindingになる可能性があるのです。まぁ、ほぼそんなことは無いと思いますが。

どうやら、プログラムの字面を見て、「bProcオブジェクトにする必要は無い」と言い切るのは難しそうです(このようなことを調べることを、コンパイラの用語ではエスケープ解析ということがあります)。

そこで、実行時に bblock_yield(&b)のようなブロック渡し以外のアクセスがあったとき、初めて Procオブジェクトを生成するようにしました。

この高速化手法自体は長い間検討していたのですが、もう少し一般的なエスケープ解析が必要じゃないかと思って、それは Ruby では難しそうだな、どうしようかな、と考えていて実現できていませんでした。ただ、改めて考えてみると、ブロックパラメータへのアクセスを実行時に監視すればできることに、ふと自転車を乗っているときに気づいたので、実装することができました。

defiter_yieldyieldenddefiter_pass&b
  iter_yield(&b)
enddefiter_yield_bp&b
  yieldenddefiter_call&b
  b.call
endN = 10_000_000# 10Mrequire'benchmark'Benchmark.bmbm(10){|x|
  x.report("yield"){
    N.times{
      iter_yield{}
    }
  }
  x.report("yield_bp"){
    N.times{
      iter_yield_bp{}
    }
  }
  x.report("yield_pass"){
    N.times{
      iter_pass{}
    }
  }
  x.report("send_pass"){
    N.times{
      send(:iter_pass){}
    }
  }
  x.report("call"){
    N.times{
      iter_call{}
    }
  }
}

__END__ruby 2.5.0dev (2017-10-24 trunk 60392) [x86_64-linux]                 user     system      total        realyield        0.634891   0.000000   0.634891 (  0.634518)yield_bp     2.770929   0.000008   2.770937 (  2.769743)yield_pass   3.047114   0.000000   3.047114 (  3.046895)send_pass    3.322597   0.000002   3.322599 (  3.323657)call         3.144668   0.000000   3.144668 (  3.143812)modified                 user     system      total        realyield        0.582620   0.000000   0.582620 (  0.582526)yield_bp     0.731068   0.000000   0.731068 (  0.730315)yield_pass   0.926866   0.000000   0.926866 (  0.926902)send_pass    1.110110   0.000000   1.110110 (  1.109579)call         2.891364   0.000000   2.891364 (  2.890716)

ベンチマーク結果を見ると、ブロック渡しをしているケースでは、修正前と後で3倍程度性能向上していることがわかります。

なぜ block.callは速くならないのか?

defblock_call&b
  b.call
  # b.call と同じことをやるように見える yield なら速い。end

このようなプログラムがあったとき、bがブロック情報のままでも yield相当の処理に変換してしまえば、Procオブジェクトを生成せずに済みそうな気がします。が、Proc#callyieldには違いがあり、単純に yieldに変換することはできません。

さて、何が違うかというと、$SAFEの設定、待避を行う、という機能です。yieldでは $SAFEについて特に何もしませんが、Proc#callでは、$SAFEProcオブジェクト生成時のものに設定し、呼び出しから戻すときに、呼び出し時の $SAFEに戻します。つまり、Proc呼び出しの中で $SAFEを変更しても、呼び出しが終われば元通り、ということです。この違いがなければ、単純な yieldに変換することは容易なのですが...。

ところで、$SAFEってそもそもご存じですかね? 知らない方もいらっしゃるかと思いますが、これからも知らないでもあまり困らないのではないでしょうか。外部からの入力を用いて systemメソッドなどで外部コマンドを呼ぶ、といった危険な機能を検知するかどうかを決めるための機能ですが、現在ではあまり利用するということは聞きません(危険なことができないようにするには、もっと OS レベルのセキュリティ機能を使うことを検討してください)。

そういうわけで、「あまり使って無さそうだから、$SAFEなくしませんか? 性能向上も阻害するし」、といったことを Ruby 開発者会議という毎月行っている Ruby コミッタの集まりで聞いてみたところ、まつもとゆきひろさんから、「$SAFEをなくすのはどうかと思うが、Procオブジェクトで $SAFEの復帰・待避はしなくていいよ」という言質を取ったので([Feature #14250])、Ruby 2.6 では b.callのようにブロックパラメータを呼び出す速度が向上するのではないかと思います。だいたい、上記マイクロベンチマークの処理では、callyieldと同じくらいの速度になるんじゃないかと思います。実際、Ruby コミッタ(パッチモンスター)の中田さん実験ではそれくらいの速度が出ているようです。

その他の貢献の話

さて、実はここからが本番なのです。が、もう長々と書いてしまったので、短くまとめます。

上記二つの性能向上は、良い数字が出ているので目立つのですが、実はあんまり苦労していません。だいたい数日で実現できてしまっています(その後、安定させるために、もう少し時間がかかっているんですが)。では、それ以外は何をしていたのでしょうか。

クックパッドに入って、Ruby のテスト環境を新たに整備しました([ruby-core:81043] rapid CI service for MRI trunk)。いわゆる普通のテストを定期的に行う CI は rubyciというものがあるのですが、結果が出るまで時間がかかる、通知がないなど不満がありました。そこで、最短で2分で結果が出る環境を整備することにしました。計算機はクラウド環境では無く、実機を利用しています。私が主催した東京Ruby会議11の予備費を用いて購入したマシンと、ある企業様から Ruby Association へ寄贈頂いたマシン、それからクックパッドで確保できたマシンを利用しています。マシン調達・運用にお世話になった/なっている皆様に深く感謝いたします。

テストは、コミットフックを用いるのではなく、とにかく何度も何度もテストを繰り返す、という方針をとっており、時々しか出ないタイミング問題などをあぶり出すことも挑戦することができました(普通は、同じテストを2度以上実行しても、結果は変わらないと思いますが、Ruby のテストですと、そうでもないことがあります)。実際、いくつかの問題を発見しています(多くはテストの不備でした)。また、結果を Slack に流して(普通ですね)、問題のあるコミットがあれば、すぐに気づくことができるようにしました。複数環境で実行しているため、たとえばビルドエラーが起こると Slack に数十の通知が一斉に飛んでくるので、とても焦るので直さないと、という気分になります。

それから、Ruby でコルーチンを実現するための Fiber周りの整理・改善を行いました(リファレンスマニュアル)。結果は Fiberの切り替えが数% 速くなる、というなんとも地味な結果になりました。詳細は Ruby会議2017 での私の発表をご参照ください。

www.slideshare.net

実は、今年の多くの開発時間が、この改善につぎ込まれています。というのも、Fiber という機能を私が作ったのが約10年前なのですが、その頃テキトーにしていたところを、全部見直して書き直す、という作業になったからです。「実行コンテキストの管理」という、バグも出やすいムズカシイ部分ですし、そもそも覚えていない。そういう点でも気合いと開発時間が必要でした。うまくいかなくて、何度も最初からやり直しました。

この修正は、一義的には Fiber のための改善なのですが、実は狙いは将来導入を検討している新しい並行・並列のための機能を入れるための基盤作りでした。将来のデザインのために、今のうちに改善を行ったということになります。今年この点を頑張ったおかげで、来年は挑戦しやすくなったなという感触を持っています。

また、今年最大の Ruby への貢献といえば、遠藤さんにクックパッドにジョインして頂き、フルタイム Ruby コミッタとして活躍して頂いたことじゃないかと思います。遠藤さんによる目覚ましい成果は、だいたい私の成果と言っても過言ではないかと思います(過言です)。

おわりに

本稿では、Ruby 2.5.0 に導入した性能向上の仕組みについて、詳しくご紹介しました。その際、どのようなことを考え、気をつけながら開発をしているかも書いてみました。ちょっとだけのつもりが、長くなってしまいました。Ruby 2.5.0 と Ruby 開発の魅力を少しでもお伝えできていれば幸いです。来年も Ruby 2.6 そして Ruby 3 の実現のためにがんばります。

もし、Ruby インタプリタの開発が面白そうだな、と思ったら、できる限りサポートしますのでお声かけください。今年8月に好評だったRuby Hack Challenge( Cookpad Ruby Hack Challenge 開催報告 )の2回目である Ruby Hack Challenge #2は1月末に英語メインで開催予定ですが(申し込み締め切りは過ぎてしまいました)、2月にも日本語メインで開催しようと思いますので、よろしければ参加をご検討ください。また、RHC もくもく会を、だいたい毎月どこかで開催していますので、そちらもチェック頂ければ幸いです。来月は 1/29 (月) に RHCもくもく会#4を開催予定です。

寒いのでお体に気をつけて、良いお年をお迎えください。


サーバーレスなバックアップシステムを AWS SAM を用いてシュッと構築する

$
0
0

こんにちは。昨晩のお夕飯は鮭のカレー風味ムニエル定食だったインフラ部 SRE グループの @mozamimyです。

今回は、SRE グループでの取り組みのひとつであるマルチクラウドバックアップを題材にして AWS SAM、CodePipeline (CodeBuild および CodeDeploy を含む) を用いたサーバーレスアプリケーションの構築、ビルドおよびデプロイについて書いていきたいと思います。また、1月に Lambda で Golang が利用可能になったこともあり、CodePipeline の進捗を Slack に投稿する Lambda function を Golang で作ってみたので、そちらもあわせて解説したいと思います。

今回取り扱うトピック

  • マルチクラウドバックアップの重要性
  • サーバーレスアプリケーションについて
  • AWS SAM (Serverless Application Model)
  • s3-multicloud-backup: Lambda を中心に実装するイベント駆動の S3 -> GCS へのバックアップシステム
  • codepipeline-notify: Golang で実装された CodePipeline の進捗を Slack に投稿するアプリケーション

マルチクラウドバックアップ #とは

クックパッドでは、オンプレミスから AWS に移行してから長きにわたって AWS を中心に利用しています。その一部として、レシピ画像やつくれぽ画像の配信のための tofu というシステムが稼働しており1、tofu のバックエンドストレージとして Amazon S3 (以降 S3 と略記) を利用しています。S3 バケットに格納された画像データは、それぞれが大切なユーザーの皆様の画像であり、データの可用性が非常に重視されます。心のこもったひとつひとつの画像を大切に守って配信すべきときに漏れなく配信できるようにする、それは我々 SRE の使命です。

クックパッドにとって、ユーザーの皆様からお預かりしたデータはサービスの要です。S3 は非常に高い可用性を持っているため、たとえば S3 の障害などからデータを守るためには十分と言えるかもしれません。しかし、サービスの要であるからには、たとえば AWS アカウントへのアクセスができなくなるような自体にも備える必要があるとチームでは考えています。そこで我々は Google Cloud Storage (以下 GCS と略記) をバックアップ先として採用し、GCS にバックアップデータを預けることにしました。

今回は、AWS の S3 から Google の GCS のように、クラウドサービスをまたいだバックアップのことをマルチクラウドバックアップと呼ぶことにします。そして、それを実現するためのイベント駆動 Lambda function を中心としたサーバーレスアプリケーションを、AWS SAM のもとで構築するためのノウハウを解説していきます。

サーバーレスアプリケーション

サーバーレスアプリケーションとは、サーバのプロビジョニングなしに2利用できるマネージドサービスを駆使して、何らかのサービスを提供するアプリケーションのことを指します。たとえば AWS では以下のようなコンポーネントを利用することができます。

  • Lambda: コンピューティング
  • API Gateway: リバースプロキシ
  • SNS (Simple Notification Service): pub/sub によるメッセージング
  • SQS (Simple Queue Service): キューによるメッセージング
  • S3: オブジェクトストレージ
  • DynamoDB: NoSQL データベース
  • CloudWatch: ロギングおよびモニタリング

これらのコンポーネントは AWS によって維持管理されており、高い可用性とスケーラビリティを持つとされています。たとえば、コンピューティングという意味では Lambda は EC2 に代わるものであり、Lambda を利用することで EC2 のように OS そのものの面倒を見るといった運用作業から解放され、実現したい機能に集中することができます。また、Lambda function が実行されている時間だけ課金するというモデルなので、EC2 のようにインスタンスが起動している限り課金され続けるコンポーネントよりも経済的に運用しやすいというメリットもあります3

ただし、これらの個別のコンポーネントを組み合わせて動作させるという性質上、アプリケーション全体として俯瞰したときにピタゴラ装置になることは避けられない上、デプロイ作業が非常に面倒なものになります。そのため、これらのコンポーネントを定義し、スマートにデプロイできる「何か」が必要になります。そして、その「何か」の一つとして AWS SAM (Serverless Application Model) が公式に提唱されており、実際に利用できる状態になっています。

AWS SAM (Serverless Application Model)

AWS SAM (以降 SAM と略記) とは、ひとことで言うならば「CloudFormation をベースとしたサーバーレスアプリケーションのリソースを作成、管理できる仕組み (モデル)」といったところです。詳しくは以下の公式リポジトリに置かれているドキュメントに目を通してみてください。

awslabs/serverless-application-model: AWS Serverless Application Model (AWS SAM) prescribes rules for expressing Serverless applications on AWS.

SAM テンプレート自体は CloudFormation テンプレートの特殊化に過ぎず、最終的にベーシックな CloudFormation テンプレートに展開されます。つまり、SAM を使ってサーバーレスアプリケーションをデプロイする場合、最終的に CloudFormation stack が生えて、Lambda function を中心とした各リソースが作られることになります。

ちなみに Lambda のデプロイツールの有名所として Apexといったツールも検討しましたが、公式のエコシステムに賭けるほうが将来性があるということや、Apex は Lambda function のデプロイのみに特化しているといった点が気になり、SAM を中心にしていこうという流れになっています。

aws-sam-local でいい感じにサーバーレスアプリケーションを開発する

SAM を利用することのもう一つのメリットとして、ローカルで開発するときにとても便利な aws-sam-local というツールを利用することができる、ということがあげられます。まだベータ版の位置付けとなっていますが、十分使えるという印象です。API Gateway をシミュレーションすることもでき、SAM に乗っかるならば必携のツールといえます。以降の解説でも aws-sam-local を利用するので、チュートリアルとして試してみたい場合は README を読んでインストールしておいてください。

awslabs/aws-sam-local: AWS SAM Local 🐿 is a CLI tool for local development and testing of Serverless applications

2018-02-07 現在、npm を用いたインストールが Recommended となっていますが、個人的には https://github.com/awslabs/aws-sam-local#build-from-sourceにあるように、go get でインストールすることをおすすめします。まだベータ版ということもあってか、エラーメッセージがやや不親切であったり、不具合と思われる挙動に出くわしたときにちょっと直して動かしてみることが簡単だからです。今回説明するマルチクラウドバックアップシステムを構築する際にも、SAM テンプレートの CodeUriに相対パスが含まれていると期待通りに動作しないという問題を発見し、それを修正するための PR を出しました。

Fix a glitch that a tamplate CodeUri has like ../ relative path by mozamimy · Pull Request #279 · awslabs/aws-sam-local

余談ですが、SAM のリスのキャラクターは SRE グループの中ではとっとこ SAM 太郎と呼ばれてたいへん愛されて(?)おります。New – AWS SAM Local (Beta) – Build and Test Serverless Applications Locally | AWS News Blogのブログ記事でも、

At it's core, SAM is a powerful open source specification built on AWS CloudFormation that makes it easy to keep your serverless infrastructure as code –and they have the cutest mascot.

とあり、公式にも (?) SAM 太郎推しであることがうかがえます。

f:id:mozamimy:20180202132607p:plain

s3-multicloud-backup: Lambda を中心に実装するイベント駆動の S3 -> GCS へのバックアップシステム

アーキテクチャ

以下に、今回構成する s3-multicloud-backup のアーキテクチャを図示します。

ここでは、ソースとなる S3 バケットの名前を source-s3-bucket とし、バックアップ先の GCS バケットの名前を dest-bucket とします。source-s3-bucket にオブジェクトがアップロードされると、以下の順に処理が進み、最終的に dest-bucket にオブジェクトがバックアップされます。

  1. オブジェクトがアップロードされたという通知が source-s3-bucket-notification SNS topic に飛ぶ。
  2. source-s3-bucket-notification SNS topic を subscribe する s3-multicloud-backup function が起動する。
  3. s3-multicloud-backup function は、SNS 経由で届いた通知を開封し、対象のオブジェクトを source-s3-bucket から取り出して dest-bucket にアップロードする。

ただし、s3-multicloud-backup function によるオブジェクトのダウンロードとアップロードは、何らかの理由で失敗することがあり、堅牢なバックアップシステムを構築するためには失敗した事実を適切にハンドリングする必要があります。ここで起こりうる失敗の原因としては、以下の 3 点が上げられます。

  • S3 もしくは GCS に障害が起きている。
  • function にバグにより例外が起こって処理が正常に完了しなかった。
  • Lambda の実行環境でメモリ不足やタイムアウトが起きた。

このうち、上から 2 つについては障害が回復してから手動でリトライしたりバグを修正するなどの対応が必要となりますが、これらは避けることができません。とはいえ S3 や GCS の可用性を考えると滅多に起こることはないでしょう。

ここで特に考慮すべきは 3 つめのパターンで、オブジェクトのファイルサイズと Lambda の実行環境として設定するメモリ量によっては、それなりに頻繁に起こりえます。だからといって大きめのメモリサイズを確保するとコストに直接響くため、なるべく確保するメモリ量は小さくしたいところです。そこで、ここでは DLQ を用いた多段呼び出しというパターンを用いることにします。

Lambda では非同期呼び出しの場合、適当な時間を置いて 2 回まで自動的に再試行され、それでもダメなら DLQ として指定した SNS topic もしくは SQS queue にメッセージが送られます4。この機能を利用し、1 段目の s3-multicloud-backup function の実行が失敗した場合、s3-multicloud-backup-retry SNS topic に通知を送り、2 段目に控える s3-multicloud-backup-retry function を起動するようにします。もちろん、2 段目の function には、1 段目よりも大きめのメモリサイズを確保するように設定します。

このような構成にすることで、設計としてはやや複雑になりますが、より低コストでバックアップシステムを運用することができます。

最終的に s3-multicloud-backup-retry による実行も失敗した場合、最終的に DLQ として指定されている s3-multicloud-backup-dlq SQS queue に情報が格納されます。CloudWatch Alarm を用いてこの SQS queue の NumberOfMessageSentを監視しておき、DLQ にメッセージが届いたら PagerDuty を経由して SRE が状況判断をして対応する、という流れになっています。

ソースコードと SAM テンプレート

アーキテクチャを理解したところで、ここから function のソースコードと SAM テンプレートについて説明します。ソースコードは https://github.com/mozamimy/s3-multicloud-backupに push してあるので、必要に応じてご活用ください。

ディレクトリツリーは以下のようになっています。

.
├── README.md
├── deploy
│   ├── buildspec
│   │   ├── production.yml
│   │   └── staging.yml
│   └── template
│       ├── production.yml
│       └── staging.yml
├── sample-event-dlq.json
├── sample-event-s3-via-sns.json
└── src
    ├── index.js
    └── package.json

ここでは、ビルドやデプロイに関連するファイルを deploy ディレクトリ以下にまとめ、処理本体が記述された index.js およびパッケージ情報である package.json を src ディレクトリ以下にまとめています。また、aws-sam-local を用いて手元で function を動かすために、サンプルのイベントを含んだ 2 つの JSON ファイルが置かれています。

index.js

'use strict';

const fs = require('fs');
const uuid = require('uuid');
const aws = require('aws-sdk');
const kms = new aws.KMS();
const s3 = new aws.S3();

const GCS_BUCKET = process.env.GCS_BUCKET;
const PROJECT_ID = process.env.PROJECT_ID;
const ENCRYPTED_GC_CREDENTIAL = process.env.GC_CREDENTIAL;
const SKIP_KEY_REGEX = process.env.SKIP_KEY_REGEX;
const RETRY_SNS_TOPIC_ARN = process.env.RETRY_SNS_TOPIC_ARN;
const GC_CREDENTIAL_FILE_PATH = '/tmp/gcp_cred.json';
const TMP_FILE_PATH = `/tmp/${uuid.v4()}`;

function replicateObject(s3ObjectKey, s3Bucket) {const kmsParamas = {
    CiphertextBlob: new Buffer(ENCRYPTED_GC_CREDENTIAL, 'base64'),
  };

  const s3Params = {
    Bucket: s3Bucket,
    Key: s3ObjectKey,
  };

  return Promise.all([
    kms.decrypt(kmsParamas).promise().then((data) => {const decrypted = data.Plaintext.toString('ascii');
      fs.writeFileSync(GC_CREDENTIAL_FILE_PATH, decrypted);
    }),
    s3.getObject(s3Params).promise(),
  ]).then((results) => {const s3Object = results[1];
    fs.writeFileSync(TMP_FILE_PATH, s3Object.Body);

    const storage = require('@google-cloud/storage')({
      projectId: PROJECT_ID,
      keyFilename: GC_CREDENTIAL_FILE_PATH,
    });

    const gcsBucket = storage.bucket(GCS_BUCKET);
    const gcsParams = {
      destination: s3ObjectKey,
      validation: 'crc32c',
      resumable: false,
    };

    return gcsBucket.upload(TMP_FILE_PATH, gcsParams);
  });
}function deleteTempFile() {if (fs.existsSync(TMP_FILE_PATH)) {
    fs.unlinkSync(TMP_FILE_PATH);
  }}

exports.handler = (event, context) => {const message = (event.Records[0].Sns.TopicArn == RETRY_SNS_TOPIC_ARN)
    ? JSON.parse(JSON.parse(event.Records[0].Sns.Message).Records[0].Sns.Message)
    : JSON.parse(event.Records[0].Sns.Message);

  const s3Bucket = message.Records[0].s3.bucket.name;
  const s3ObjectKey = message.Records[0].s3.object.key;

  if (!(SKIP_KEY_REGEX == null) && s3ObjectKey.match(SKIP_KEY_REGEX)) {const logMessage = `skipped: Bucket: ${s3Bucket}, Key: ${s3ObjectKey}`;
    console.log(logMessage);
    context.succeed(logMessage);
  }else{
    replicateObject(s3ObjectKey, s3Bucket).then((success) => {
      deleteTempFile();
      const logMessage = `replicated: Bucket: ${s3Bucket}, Key: ${s3ObjectKey}`;
      console.log(logMessage);
      context.succeed(logMessage);
    }).catch((err) => {
      deleteTempFile();
      const logMessage = `ERROR!: Bucket: ${s3Bucket}, Key: ${s3ObjectKey}`;
      console.log(logMessage);
      console.log(err);
      context.fail(logMessage);
    });
  }};

Lambda が呼び出されたとき、exports.handlerに格納された関数が実行されます。この関数では、SNS topic から受け取ったイベントを開封し、バックアップ元のバケットとオブジェクトのキーを取り出し、GCS バケットにアップロードします。SKIP_KEY_REGEX環境変数に設定された正規表現にマッチするキーに対してはバックアップを行いません。また、184 行目で 1 段目の実行なのか 2 段目の実行なのかを判定して、適切にイベントの開封処理を変えています。

replicateObject()がバックアップ処理の本体で、GCS にアクセスするための暗号化されたクレデンシャル (JSON) を環境変数 ENCRYPTED_GC_CREDENTIALから読み出し、KMS API を呼ぶことで復号します。そして、S3 バケットから対象のオブジェクトをダウンロードし、復号したクレデンシャルを用いて GCS バケットにアップロードします。クレデンシャルの暗号化や、この function につけるべき IAM role については後述します。

package.json

package.json に関しては特筆すべきところはないでしょう。

{"name": "s3-multicloud-backup",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {"test": "echo \"Error: no test specified\"&& exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {"@google-cloud/storage": ">=0.52.0",
    "aws-sdk": ">=2.44.0",
    "uuid": ">=3.0.1",
    "fast-crc32c": ">=1.0.4"
  }}

ちなみに、fast-crc32c を依存に含めることにより、@google-cloud/storage による CRC チェックが高速になるという利点があります。また、後述しますが @google-cloud/storage の依存にネイティブライブラリが含まれるため、Lambda 環境で実行するためには Linux 上で npm installを実行する必要があります。

buildspec

package.json の項でも説明しましたが、依存ライブラリにネイティブバイナリのビルドが必要になるため、ローカル環境で npm installしたときに作成される node_modules をそのまま Lambda 環境に置いてもきちんと動作する保証はありません。ここではその問題を解決するために CodeBuild を利用し生成された生成物を CodeDeploy でデプロイする、という構成にし、一連の処理を CodePipeline にまとめることにします。

以下はそのための buildspec の例です。

version:0.2phases:install:commands:- 'cd src && npm install'build:commands:- 'aws cloudformation package --template-file ../deploy/template/production.yml --s3-bucket sam-artifact.ap-northeast-1 --output-template-file template.package.yml'artifacts:files:- 'src/template.package.yml'

AWS CLI の cloudformation packageサブコマンドを使うことで、--template-fileに指定したテンプレートを読み込んで src ディレクトリ以下を zip ファイルにまとめ、--s3-bucketに指定した S3 バケットに生成物を自動的にアップロードしてくれます。また、テンプレートに含まれる CodeUriを生成物用の S3 バケットに書き換えたテンプレートを --output-template-fileに指定したファイルに出力します。--s3-bucketに指定するバケットは、各自の環境の合わせて変更してください。

SAM テンプレート

以下の YAML ファイルがデプロイの要となる SAM テンプレート です。上述したアーキテクチャ図と見比べて、どこがどの項目にあたるかをチェックするとよいでしょう。SAM の詳細な記法は以下の公式ドキュメントを参照してください。

serverless-application-model/2016-10-31.md at master · awslabs/serverless-application-model

また、SAM のベースは CloudFormation なので、CloudFormation に慣れておくとより理解が深まるでしょう。CloudFormation については以下のドキュメントが参考になります。

What is AWS CloudFormation? - AWS CloudFormation

AWSTemplateFormatVersion:'2010-09-09'Transform:'AWS::Serverless-2016-10-31'Description:'A serverless application that replicate all objects in S3 bucket to Google Cloud Storage.'Resources: # リトライ通知用 topicRetrySNSTopic:Type:'AWS::SNS::Topic'Properties:DisplayName:'s3mc-stg'TopicName:'s3-multicloud-backup-retry' # 2 段目でも処理が失敗したとき用の queueDLQ:Type:'AWS::SQS::Queue'Properties:QueueName:'s3-multicloud-backup-dlq'MessageRetentionPeriod:1209600 # 14 days # 1 段目の functionS3MulticloudBackupFunction:Type:'AWS::Serverless::Function'Properties:Handler:'index.handler'Runtime:'nodejs6.10'CodeUri:'../../src'FunctionName:'s3-multicloud-backup' # 事前に作成しておいた IAM role をここに指定するRole:'arn:aws:iam::(Account ID):role/LambdaS3MulticloudBackup'MemorySize:256Timeout:30Events:ObjectUploaded:Type:'SNS'Properties: # S3 バケットからきた通知を受け渡すための # source-s3-bucket-notification topic の ARN を指定するTopic:'arn:aws:sns:ap-northeast-1:(Account ID):source-s3-bucket-notification'DeadLetterQueue:Type:'SNS' # `!Ref` を利用して論理名 `RetrySNSTopic` から ARN を取り出して設定するTargetArn:!Ref RetrySNSTopic
      # Lambda を起動した環境における環境変数を設定できるEnvironment:Variables: # マッチするキーのオブジェクトはバックアップしないSKIP_KEY_REGEX:'^(_sandbox|test)' # バックアップ先の GCS バケット名を指定 # 環境に合わせて書き換えるGCS_BUCKET:'s3-multicloud-backup' # バックアップ先のプロジェクトを指定 # 環境に合わせて書き換えるPROJECT_ID:'foo-project' # リトライ用の SNS topic の ARN を指定 # `!Ref` は環境変数を設定するときにも使えるRETRY_SNS_TOPIC_ARN:!Ref RetrySNSTopic
          GC_CREDENTIAL:'暗号化したクレデンシャルをここに(後述)' # 2 段目の functionS3MulticloudBackupRetryFunction:Type:'AWS::Serverless::Function'Properties:Handler:'index.handler'Runtime:'nodejs6.10'CodeUri:'../../src'FunctionName:'s3-multicloud-backup-retry' # 事前に作成しておいた IAM role をここに指定するRole:'arn:aws:iam::(Account ID):role/LambdaS3MulticloudBackup' # MemorySize および Timeout は 1 段目より大きめにするMemorySize:2048Timeout:90Events:BackupFailed:Type:'SNS'Properties: # 1 段目の DLQ に指定したリトライ用 SNS トピックの ARNTopic:!Ref RetrySNSTopic
      DeadLetterQueue:Type:'SQS' # 2 段目でも失敗した場合は SQS queue に格納するので、`!GetAtt` を用いて論理名から ARN を取り出して設定 # SNS の場合は `!Ref` で ARN が返るが、SQS の場合は `!GetAtt` であることに注意TargetArn:!GetAtt DLQ.Arn
      Environment:Variables:SKIP_KEY_REGEX:'^(_sandbox|test)'GCS_BUCKET:'s3-multicloud-backup'PROJECT_ID:'foo-project'RETRY_SNS_TOPIC_ARN:!Ref RetrySNSTopic
          GC_CREDENTIAL:'暗号化したクレデンシャルをここに(後述)'

SAM では、基本的に Resourcesに各種リソースを記述していくことになります。Resourcesのキーは各リソースの論理名で、別の場所で !Ref!GetAttでリソースの ARN や名前を取得することが可能です。

ここでは、リトライを通知する s3-multicloud-backup-retry SNS topic および DLQ である s3-multicloud-backup-dlq SQS queue のみを SAM テンプレートに定義します。Lambda function にアタッチする IAM role や、S3 バケットからの通知を受ける source-s3-bucket-notification SNS topic は SAM テンプレートの中に定義しません。

これは、source-s3-bucket-notification は s3-multicloud-backup 以外の用途でも利用されるかもしれないということと、クックパッドでは IAM を codenize ツールである codenize-tools/miam: Miam is a tool to manage IAM. It defines the state of IAM using DSL, and updates IAM according to DSL.を用いて GitHub flow に則ってレビューできるように中央集権的に管理しているからです。読者の皆さんの環境次第では、これらの設定も SAM テンプレートに含めてしまってもよいでしょう。

RetrySNSTopicおよび DLQは、基本的にデフォルトのパラメータを使うようにすれば十分でしょう。ただし、DLQでは MessageRetentionPeriod1209600に明示的に指定することにより、メッセージが最長の 14 日間キューに保持されるようにします。

S3MulticloudBackupFunctionおよび S3MulticloudBackupRetryFunctionは Lambda function で、それぞれ 1 段目、2 段目の Lambda function になります。MemorySizeTimeoutの値が違うことに注目してください。設定する値の詳細については、YAML 中のコメントを確認してください。

クレデンシャルを暗号化する

GCS バケットへのアクセスに利用するクレデンシャルをハードコードするわけにはいかないため、何らかの手段を用いて暗号化して環境変数にセットする必要があります。ここでは、AWS のキーマネジメントサービスである KMS (Key Management Service) を利用してクレデンシャルを暗号化および復号することにします。KMS の詳細については以下のドキュメントを参照してください。

AWS Key Management Service Documentation

まず、s3-multicloud-backup 専用の鍵を作成して、エイリアスおよび ARN をメモしておきます。そして、以下のような Ruby スクリプトを使ってクレデンシャル JSON ファイルの内容を暗号化します。暗号化の際には、作成した鍵を使える適当なアクセスキーを AWS_ACCESS_KEY_IDおよび AWS_SECRET_ACCESS_KEY環境変数にセットしてください。

https://github.com/mozamimy/toolbox/tree/master/ruby/easykms

#!/usr/bin/env rubyrequire'aws-sdk-kms'

key_id = ARGV[0]
kms_resp = Aws::KMS::Client.new.encrypt({
  key_id: key_id,
  plaintext: STDIN.read,
})

print Base64.strict_encode64(kms_resp.ciphertext_blob)
$ cat credential.json | ruby kms_encoder.rb [鍵の ARN]

以上の作業で暗号化され Base64 エンコードされた出力をテンプレート中の GC_CREDENTIALに書き込んでください。

Lambda function に付与する role

以下のような権限を持つ role を事前に作り、作った role の ARN を SAM テンプレートの Roleに指定するとよいでしょう。

{"Version": "2012-10-17",
  "Statement": [{"Effect": "Allow",
      "Action": ["kms:Decrypt"
      ],
      "Resource": ["暗号化用の KMS 鍵の ARN をここに"
      ]},
    {"Effect": "Allow",
      "Action": ["logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {"Effect": "Allow",
      "Action": ["sns:Publish"
      ],
      "Resource": "s3-multicloud-backup-retry SNS topic の ARN をここに"
    },
    {"Effect": "Allow",
      "Action": ["sqs:SendMessage"
      ],
      "Resource": "s3-multicloud-backup-dlq SQS queue の ARN をここに"
    },
    {"Effect": "Allow",
      "Action": ["s3:Get*"
      ],
      "Resource": "バックアップ元の S3 バケットの ARN をここに/*"
    }]}

Trust relationship には以下のように指定して Lambda function に付与する role であることを示します。

{"Version": "2012-10-17",
  "Statement": [{"Effect": "Allow",
      "Principal": {"Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }]}

aws-sam-local を使って開発する

SAM テンプレートを書いておけば、aws-sam-local を使ってローカルで Lambda function を実行することができます。その際、以下の点に注意してください。

  • 事前に Lambda 環境に近い状態で依存モジュールをビルドする
  • aws-sam-local コマンドを実行する際に、対象の S3 バケットを読み書きするためのクレデンシャルを AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYで与える

依存モジュールをビルドするには、aws-sam-local も内部で利用している lambci/lambda Docker imageを利用すると便利です。以下のようなコマンドで npm installしましょう。

$ docker run --rm -v "$PWD/src/":/var/task lambci/lambda:build-nodejs6.10 npm install

すると、Lambda 環境に近い状態でビルドされた node_modules が src/ ディレクトリに作られ、以下のようなコマンドで function を動かすことができます。

$ aws-sam-local local invoke S3MulticloudBackupFunction -e sample-event-s3-via-sns.json --template=deploy/template/staging.yml

このように、手元から動かしてみることも考慮して、テンプレートは staging (もしくは development) 用と production に分けておくとよいでしょう。

buildspec も同様で、開発用の AWS アカウントと本番用の AWS アカウントが分かれている場合など、CodeBuild によるビルドによる生成物をアップロードするための S3 バケットが異なる場合があるので、staging 用と production 用に設定が分かれているほうが何かと便利だと思います。

aws-sam-local の詳しい使い方を知りたい場合は GitHub ページの README を参照するとよいでしょう。

CodePipeline を使ってビルドとデプロイを行う

ここからは、CodePipeline を使って CodeCommit、CodeBuild および CodeDeploy を組み合わせて、SAM テンプレートで定義したアプリケーションのビルドおよびデプロイについて説明します。

それぞれのサービスの詳細については、以下のドキュメントを参照してください。

デプロイできるようになるまでの大まかな流れとしては、以下のようになります。

  • 各種サービスロールを用意する
    • 各サービスで利用する S3 バケットを 3 つ用意する
      • CodePipeline 用
      • CodeBuild 用
      • CloudFormation 用
  • CodeCommit にリポジトリを用意する
  • CodeBuild にプロジェクトを追加する
  • CodePipeline にパイプラインを作成する
    • ここで CodeCommit、CodeBuild プロジェクト、CodeDeploy を組み合わせる

各種サービスロールを用意する

まだ Code シリーズや CloudFormation を利用していない場合サービスロールがない状態なので、事前に作成する必要があります。CodePipeline などを利用する IAM ユーザが持ってない権限であっても、サービスロールに強い権限があると間接的に行使できることになるため、以下の例を参考になるべく権限を絞るほうがよいでしょう。

CodePipeline 用のサービスロール

以下に CodePipeline が利用するためのサービスロール CodePipelineServiceRoleの一例を示します。

{"Version": "2012-10-17",
  "Statement": [{"Effect": "Allow",
      "Action": ["cloudformation:CreateChangeSet",
        "cloudformation:CreateStack",
        "cloudformation:DeleteChangeSet",
        "cloudformation:DeleteStack",
        "cloudformation:DescribeChangeSet",
        "cloudformation:DescribeStacks",
        "cloudformation:ExecuteChangeSet",
        "cloudformation:SetStackPolicy",
        "cloudformation:UpdateStack",
        "cloudformation:ValidateTemplate"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["codebuild:BatchGetBuilds",
        "codebuild:StartBuild"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["codecommit:CancelUploadArchive",
        "codecommit:GetBranch",
        "codecommit:GetCommit",
        "codecommit:GetUploadArchiveStatus",
        "codecommit:UploadArchive"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["codedeploy:CreateDeployment",
        "codedeploy:GetApplicationRevision",
        "codedeploy:GetDeployment",
        "codedeploy:GetDeploymentConfig",
        "codedeploy:RegisterApplicationRevision"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["iam:PassRole"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["lambda:InvokeFunction",
        "lambda:ListFunctions"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["opsworks:CreateDeployment",
        "opsworks:DescribeApps",
        "opsworks:DescribeCommands",
        "opsworks:DescribeDeployments",
        "opsworks:DescribeInstances",
        "opsworks:DescribeStacks",
        "opsworks:UpdateApp",
        "opsworks:UpdateStack"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["s3:GetBucketVersioning",
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:PutObject"
      ],
      "Resource": ["[ここに CodePipeline が利用するための S3 バケットの ARN を入れる]",
        "[ここに CodePipeline が利用するための S3 バケットの ARN を入れる]/*",      ]
    }
  ]}
CodeBuild 用のサービスロール

以下に CodeBuild が利用するためのサービスロール CodeBuildServiceRoleの一例を示します。

{"Version": "2012-10-17",
  "Statement": [{"Effect": "Allow",
      "Action": ["codecommit:GitPull"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:GetAuthorizationToken",
        "ecr:GetDownloadUrlForLayer"
      ],
      "Resource": "*"
    },
    {"Effect": "Allow",
      "Action": ["logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:ap-northeast-1:(Account ID):log-group:/aws/codebuild/*"
    },
    {"Effect": "Allow",
      "Action": ["s3:GetObject",
        "s3:GetObjectVersion",
        "s3:PutObject"
      ],
      "Resource": ["[ここに CodePipeline が利用するための S3 バケットの ARN を入れる]/*",
        "[ここに CodeBuild が利用するための S3 バケットの ARN を入れる]/*",
        "[ここに CodeBuild によってビルドされた生成物を置くための S3 バケット (CloudFormation 用) の ARN を入れる]/*"
      ]}]}
CloudFormation 用のサービスロール

以下に CloudFormation が利用するためのサービスロール CloudFormationServiceRoleの一例を示します。

{"Version": "2012-10-17",
  "Statement": [{"Effect": "Allow",
      "Action": ["apigateway:*"
      ],
      "Resource": ["arn:aws:apigateway:ap-northeast-1::*"
      ]},
    {"Effect": "Allow",
      "Action": ["cloudformation:CreateChangeSet"
      ],
      "Resource": ["arn:aws:cloudformation:ap-northeast-1:aws:transform/Serverless-2016-10-31"
      ]},
    {"Effect": "Allow",
      "Action": ["events:*"
      ],
      "Resource": ["arn:aws:events:ap-northeast-1:(Account ID):rule/*"
      ]},
    {"Effect": "Allow",
      "Action": ["iam:PassRole"
      ],
      "Resource": ["*"
      ]},
    {"Effect": "Allow",
      "Action": ["lambda:*"
      ],
      "Resource": ["arn:aws:lambda:ap-northeast-1:(Account ID):function*"
      ]},
    {"Effect": "Allow",
      "Action": ["s3:GetBucketVersioning",
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:PutObject"
      ],
      "Resource": ["[ここに CodeBuild によってビルドされた生成物を置くための S3 バケット (CloudFormation 用) の ARN を入れる]/*"
      ]},
    {"Effect": "Allow",
      "Action": ["sns:ListSubscriptionsByTopic",
        "sns:Subscribe",
        "sns:Unsubscribe"
      ],
      "Resource": ["arn:aws:sns:ap-northeast-1:(Account ID):*"
      ]}]}

CodeCommit にリポジトリを用意する

CodeBuild では、コードのソースとして S3 バケットや CodeCommit、GitHub などをサポートしており、最近以下のブログポストにあるように、GitHub Enterprise 連携も登場しました。

Announcing AWS CodeBuild Support for GitHub Enterprise as a Source Type and Shallow Cloning | AWS DevOps Blog

ここでは、もっともお手軽な CodeCommit を利用することにし、CodeCommit に s3-multicloud-backup という名前でリポジトリを作成し、master ブランチをビルド・デプロイすることとします。

CodePipeline を構築する

SAM テンプレートで定義されたアプリケーションをデプロイするための最小のパイプラインは、以下のようなものになります。1 段目で CodeCommit からソースを取得し、2 段目で CodeBuild を用いて依存ライブラリのビルドおよび aws cloudformation packageを行い、3 段目で stack の change set を作成し、4 段目で stack の change set を適用します。

f:id:mozamimy:20180202132555p:plain

各ステップは以下のように設定します。

1 段目: CodeCommit

f:id:mozamimy:20180202132537p:plain

Source provider に AWS CodeCommit を指定し、Repository name と Branch name をいい感じに埋めてください。

2 段目: CodeBuild

まず、以下のような設定で CodeBuild のプロジェクト s3-multicloud-backupを作成しましょう。Node.js 用の image を利用し、Buildspec name に buildspec ファイルの位置を指定し、Service role に事前に用意しておいた CodeBuild 用のサービスロールを指定すれば OK です。

f:id:mozamimy:20180202132523p:plain

パイプラインには以下のように設定します。

f:id:mozamimy:20180202132532p:plain

3 段目: CodeDeploy と CloudFormation による stack の change set の作成

以下のスクリーンショットのように、Deployment provider に AWS CloudFormation を指定し、Action mode を Create or replace a change set を指定し、Role name には事前に用意しておいた CloudFormation 用のサービスロールを指定します。

f:id:mozamimy:20180202132548p:plain

4 段目: CodeDeploy と CloudFormation による stack の change set の適用

4 段目では、3 段目で作成した実行計画ともいえる change set を実行できるように、以下のスクリーンショットのように設定します。Action mode に Execute a change set を指定するのがミソです。

f:id:mozamimy:20180202132543p:plain

以上で CodePipeline の設定は完了です。Release change ボタンを押せばパイプラインの各ステップが実行され、最終的に SAM テンプレートに定義された各リソースが作成され、アプリケーションが利用可能な状態になるでしょう。

アラートの発行とモニタリング

クックパッドではアラート用の SNS topic が存在し、その topic にアラートの内容を送るとアラート用のメールアドレスにメールが飛び、最終的に PagerDuty で incident が発行されるという仕組みになっています。以下の例では DLQ 用の SQS の NumberOfMessageSentを CloudWatch Alarm で監視し、メッセージが発行されたことを検知するとアラート用の SNS topic に通知が飛ぶようになっています。アラートに関しては読者の皆さんの環境によって違うと思うので、それに合わせて設定してください。

f:id:mozamimy:20180207085432p:plain

CloudWatch では任意のメトリクスを組み合わせてダッシュボードを作ることができるので、以下のようなダッシュボードを作っておくと便利でしょう。

f:id:mozamimy:20180207085424p:plain

たまに数回エラーが発生していますが、ほとんどは Lambda の非同期呼び出しの自動リトライで成功していることがわかります。また、点になっていてやや見づらいですが、02/02 に一度だけ s3-multicloud-backup-retry function が実行されていることがわかります。

codepipeline-notify: Golang で実装された CodePipeline の進捗を Slack に投稿するアプリケーション

ここまで、SAM テンプレートで定義された s3-multicloud-backup というサーバーレスアプリケーションを CodePipeline を用いてデプロイする方法について述べてきました。

ここからは、1月に Lambda で Golang が利用可能になったということもあり、Golang で実装された Lambda function の一例として、CodePipeline の進捗を Slack に投稿するアプリケーションを SAM テンプレートで定義してデプロイする方法について書いていきます。従来 Lambda で公式にサポートされていた静的型付け言語は Java および C# のみであったことを考えると、手軽に書ける静的型付け言語ということで Golang は選択肢の一つとして有力なものとなるでしょう。

ソースコードと SAM テンプレート

ここからは Golang で実装された function のソースコードと SAM テンプレートについて説明します。ソースコードは https://github.com/mozamimy/codepipeline-notifyに push してあるので、必要に応じてご活用ください。

ディレクトリツリーは以下のようになります。

.
├── Makefile
├── README.md
├── deploy
│   ├── buildspec
│   │   ├── production.yml
│   │   └── staging.yml
│   └── template
│       ├── production.yml
│       └── staging.yml
├── handler
│   └── handler.go
├── main.go
└── sample-event.json

main.go

package main

import (
    "github.com/mozamimy/codepipeline-notify/handler""github.com/aws/aws-lambda-go/lambda"
)

func main() {
    lambda.Start(handler.HandleRequest)
}

ハンドラは handler.go に記述することにし、main では lambda.Start()にハンドラを渡して実行を待ち受ける形になります。

handler.go

package handler

import (
    "encoding/json""fmt""io/ioutil""net/http""net/url""os"
)

type CodePipelineEventDetail struct {
    Pipeline    string`json:"pipeline"`
    State       string`json:"state"`
    ExecutionID string`json:"execution-id"`
    Version     json.Number `json:"version"`
}

type CodePipelineEvent struct {
    Detail CodePipelineEventDetail `json:"detail"`
}

type SlackPayload struct {
    Text        string`json:"text"`
    Username    string`json:"username"`
    Icon_emoji  string`json:"icon_emoji"`
    Icon_url    string`json:"icon_url"`
    Channel     string`json:"channel"`
    Attachments []SlackAttachment `json:"attachments"`
}

type SlackAttachment struct {
    Color  string`json:"color"`
    Fields []SlackField `json:"fields"`
}

type SlackField struct {
    Title string`json:"title"`
    Value string`json:"value"`
    Short bool`json:"short"`
}

func HandleRequest(codePipelineEvent CodePipelineEvent) {
    field := SlackField{
        Value: fmt.Sprintf("The state of pipeline `%s` is changed to `%s` (execution_id: %s)", codePipelineEvent.Detail.Pipeline, codePipelineEvent.Detail.State, codePipelineEvent.Detail.ExecutionID),
        Short: false,
    }
    colorMap := map[string]string{
        "CANCELED":   "warning",
        "FAILED":     "danger",
        "RESUMED":    "warning",
        "STARTED":    "good",
        "SUCCEEDED":  "good",
        "SUPERSEDED": "warning",
    }
    attachment := SlackAttachment{
        Color:  colorMap[codePipelineEvent.Detail.State],
        Fields: []SlackField{field},
    }
    params, _ := json.Marshal(SlackPayload{
        Username:    "CodePipeline",
        Icon_emoji:  os.Getenv("SLACK_EMOJI_ICON"),
        Channel:     os.Getenv("SLACK_CHANNEL"),
        Attachments: []SlackAttachment{attachment},
    })

    resp, _ := http.PostForm(
        os.Getenv("SLACK_WEBHOOK_URL"),
        url.Values{
            "payload": {string(params)},
        },
    )

    body, _ := ioutil.ReadAll(resp.Body)
    defer resp.Body.Close()

    fmt.Printf(string(body))
}

CodePipelineEvent構造体がミソで、ハンドラとなる関数 HandleRequest()の引数に構造体を与えることで、自動的に JSON をパースして仮引数に渡してくれます。CloudWatch Event から受け取れるサンプルイベント (sample-event.json) を以下に示します。

{"version": "0",
  "id": "CWE-event-id",
  "detail-type": "CodePipeline Pipeline Execution State Change",
  "source": "aws.codepipeline",
  "account": "123456789012",
  "time": "2017-04-22T03:31:47Z",
  "region": "us-east-1",
  "resources": ["arn:aws:codepipeline:us-east-1:123456789012:pipeline:myPipeline"
  ],
  "detail": {"pipeline": "myPipeline",
    "version": 1,
    "state": "CANCELED",
    "execution-id": "01234567-0123-0123-0123-012345678901"
  }}

この仕組みは便利な半面、JSON と静的型付け言語の相性の悪さが浮き彫りになるところでもあり、デバッグがやや面倒になりがちな部分なので注意してください。ハンドラの仕様については以下のドキュメントや実装そのものが参考になります。ここでは、引数を取る場合でもっともシンプルなパターン func (TIn)で実装しています。

buildspec

buildspec は以下のように書くとよいでしょう。install フェーズでソースコードを GOPATH 以下に配置し、pre_build フェーズで go getし、build フェーズで go buildして得たバイナリを zip にまとめ、AWS CLI の cloudformation packageサブコマンドを利用してパッケージングします。このステップにより、--template-fileに指定したテンプレートを読み込んで src ディレクトリ以下を zip ファイルにまとめ、--s3-bucketに指定した S3 バケットに生成物を自動的にアップロードしてくれます。s3-multicloud-backup と同様、--s3-bucketに指定するバケットは、各自の環境の合わせて変更してください。

version:0.2env:variables:PACKAGE:'github.com/mozamimy/codepipeline-notify'phases:install:commands:- 'mkdir -p "/go/src/$(dirname ${PACKAGE})"'- 'ln -s "${CODEBUILD_SRC_DIR}" "/go/src/${PACKAGE}"'- 'env'pre_build:commands:- 'cd "/go/src/${PACKAGE}"'- 'go get ./...'build:commands:- 'go build -o main'- 'zip main.zip main'- 'aws cloudformation package --template-file deploy/template/production.yml --s3-bucket sam-artifact.ap-northeast-1 --output-template-file template.package.yml'artifacts:files:- 'template.package.yml'

SAM テンプレート

以下に SAM テンプレートの例を示します。今回はシンプルに Lambda function が 1 コしかないため、テンプレートの内容もシンプルです。SLACK_から始まる各環境変数は、お手持ちの環境に合わせて適宜書き換えてください。

AWSTemplateFormatVersion:'2010-09-09'Transform:'AWS::Serverless-2016-10-31'Description:'A serverless application to notify whether it succeeded or not.'Resources:CodePipelineNotify:Type:'AWS::Serverless::Function'Properties:CodeUri:'../../main.zip'Handler:'main'Runtime:'go1.x'FunctionName:'codepipeline-notify'MemorySize:128Timeout:8Events:CodeCommitStateChanged:Type:'CloudWatchEvent'Properties:Pattern:source:- 'aws.codepipeline'detail-type:- 'CodePipeline Pipeline Execution State Change'Environment:Variables:SLACK_WEBHOOK_URL:'Slack の incoming webhook URL をここに入れる'SLACK_CHANNEL:'#serverless'SLACK_EMOJI_ICON:':samtaro1:'

aws-sam-local を用いてローカルで function を動かす

今回は以下のような簡単な Makefile を用意したので、make コマンドを使ってビルドし aws-sam-local コマンドを使ってローカルで function を動かすことができます。s3-multicloud-backup の場合とは違い、Golang はクロスコンパイルが容易なため、手持ちの環境が Linux でない場合も Linux コンテナを使わずに GOOS=linuxとしてビルドすれば十分でしょう。

main: main.go handler/handler.go
    go build -o main
main.zip: main
    zip main.zip main
clean:
    rm -f main main.zip
$ GOOS=linux make main.zip
$ aws-sam-locallocal invoke CodePipelineNotify -e sample-event.json --template=deploy/template/production.yml
$ make clean

また、s3-multicloud-backup の場合と同様、CodePipeline を使ってビルドおよびデプロイのための仕組みを構築することができます。

まとめ

今回は、マルチクラウドバックアップを題材にして AWS SAM、CodePipeline を用いたサーバーレスアプリケーションの構築、ビルドおよびデプロイについて、チュートリアル的に解説しました。また、Golang で実装した CodePipeline の進捗を Slack に投稿するためのアプリケーションについても解説しました。

SAM を用いてサーバーレスアプリケーションの構成をテキストファイルに記述することで、いわゆる Infrastructure as Code の恩恵を受けることができ、加えて CodePipeline をはじめとする AWS の開発者ツールを利用することにより、ビルド及びデプロイの自動化が簡単になります。

今回は扱いませんでしたが、SAM と aws-sam-local の組み合わせによって API Gateway を用いた Web アプリケーションの開発やデプロイが容易になるといったメリットもあります。また、今回はテストコードを含めていませんが、buildspec にテストを起動するためのコマンドを含めれば、ビルドのパイプラインにテストを組み込むことも可能です。

他にも、codepipeline-notify の改良として、DynamoDB にパイプライン名と Slack チャンネルの対応を持たせておき、Slack channel の出し分けといったようなこともできるでしょう。また、AWS SDK や AWS CLI を用いて Slack などのチャットボットが CodePipeline を起動するようにすれば、サーバーレスアプリケーションに Chatops を持ち込むこともできます。

本記事がこれから Lambda を本格利用していこうとしている方や、すでに Lambda を利用しているが SAM などの管理手法を導入していない方の助けになれば幸いです。


  1. 料理を楽しくする画像配信システム

  2. 実際には DynamoDB のようにオートスケールを設定できるものの、プロビジョニングの概念が存在するコンポーネントはありますが…

  3. これはいかなる場合も当てはまるというわけではありません。たとえば、普段はほとんど利用されないが、定期的に実行がバーストするようなワークロードだと Lambda 型のほうが有利になります。

  4. Understanding Retry Behavior - AWS Lambda

Cookpad の新規事業と Firebase

$
0
0

国内事業開発部 iOS エンジニアの三浦です。私は17年新卒で入社したのですが、それ以来複数の新規事業の開発に携わってきました。 現在開発中のアプリでは、バックエンドに Firebase を用いた開発を進めています。 この記事ではなぜ Firebase を使っているのかと、そこで得られた知見についてまとめようと思います。

なぜ Firebase

みなさんご存知かと思いますが、Cookpad のレシピサービスでは主にバックエンドに AWS と Ruby on Rails が使われています。 なぜ新規事業ではその構成ではなく Firebase を使うのかということですが、以下のような理由があります。

基盤サービスが豊富

Firebase には RealtimeDatabase、FireStore といった Database を始めとして、CloudMessaging(Push通知基盤)、Authentication(認証基盤)といった開発のためのツールがあります。 これらの機能はサービス開発において大抵必要不可欠なものですが、サービスリリースまでの間ではメインの機能に時間を取られ、あまり時間を割くことができない部分になります。

これらの基盤部分が開発開始時から品質が担保された上で提供されていることで、本来のサービス開発に時間をかけることができ、開発スピード、アプリケーションの品質を高くすることができます。 また作っては壊しといったことを繰り返すリリース前の段階での開発においては、修正の範囲がクライアントのみで済むため非常にコストが低くアプリケーションの改修を行うことができます。

実際にどれくらい楽に実装できるか、簡単な iOS でのサンプルコードを載せます。

認証

ユーザーモデルを定義して Firebase に Facebook ログインするサンプルは以下のように書くことができます。 FireStore のモデルフレームワークとして Pring を利用し、モデルでは facebook のユーザーIDと名前をプロパティに持つとします。

// User modelimport Pring

classFirebase { }
extensionFirebase {
    classUser:Object {
        @objc dynamic varname:String?
        @objc dynamic varfacebookUserID:String?
    }
}

コントローラーから sign in するときにはこのような処理で実現できます。 実際は ViewModel や Helper を利用して処理を分割するのですが、サンプルなので1つのメソッドで処理を完結させています。

import UIKit
import Firebase
import FacebookLogin
import FacebookCore

classSignInViewController:UIViewController {
    privatefuncsignInWithFacebook() {
        letloginManager= LoginManager()
        // Facebook へログイン
        loginManager.logIn(readPermissions:[.email, .publicProfile, .userFriends], viewController:self) { result inswitch result {
            case .success(_, _, lettoken):// Facebook からユーザー情報を取得
                GraphRequest(graphPath:"me").start { (response, result) inswitch result {
                    case .success(letresponse):letuserID:String= response.dictionaryValue!["id"] as! String
                        letusername:String= response.dictionaryValue!["name"] as! String
                        letcredential= FacebookAuthProvider
                            .credential(withAccessToken:token.authenticationToken)
                        // Firebase への認証
                        Auth.auth().signIn(with:credential) { (user, error) inifleterror= error {
                                // error handlingreturn
                            }
                            letuser= Firebase.User()
                            user.name = username
                            user.facebookUserID = userID
                            // User モデルを FireStore に save
                            user.save { (_, error) inifleterror= error {
                                    // error handlingreturn
                                }
                                // 認証成功
                            }
                        }
                    case .failed:// error handlingbreak
                    }
                }
            case .cancelled:// Facebook へのログインがキャンセルされたbreakcase .failed:// Facebook へのログインが失敗したbreak
            }
        }
    }
}

電話番号や、Twitter、メールアドレスとパスワードによる認証に関しても同じような処理で実装をすることができます。

通知

次は Firebase Cloud Messaging を利用して Push 通知を受け取れるようにします。

User の Model に Token を管理できるようにようにプロパティを増やし、現在のログインユーザーを取得するメソッドを追加します。

// User modelimport Pring

classFirebase { }
extensionFirebase {
    classUser:Object {
        typealiasDeviceID= String
        typealiasToken= String

        @objc dynamic varname:String?
        @objc dynamic varfacebookUserID:String?
        // DeviceID をキー、FCMToken をバリューに持つ Dictionary@objc dynamic varfcmTokens:[DeviceID: Token]= [:]

        staticfunccurrent(_ completion:@escaping (Firebase.User?) ->Void) {
            guardletauthUser= Auth.auth().currentUser else {
                return completion(nil)
            }
            self.get(authUser.uid) { user, _ inguardletuser= user else {
                    completion(nil)
                    return
                }
                completion(user)
            }
        }
    }
}

あとは通知を登録したいところで Tsuchi(https://github.com/miup/Tsuchi) いう私の開発したライブラリを利用して Token を取得することができるので、 その Token をユーザーに DeviceID と共に保存します。

import Tsuchi

funcsaveFCMToken(_ toekn:String, completion: (()->Void)?) {
    Firebase.User.current { user inguardletuser= user else { return }
        user.fcmTokens[UIDevice.current.identifierForVendor!.uuidString] = token
        user.update { _ in completion?() }
    }
}

Tsuchi.shared.didRefreshRegistrationTokenActionBlock = { token in
    saveFCMToken(token)
}
Tsuchi.shared.register { granted inif!granted {
        // ユーザーが登録を拒否return
    }
}

通知を受け取ったときの処理に関しても Tsuchi を利用して以下のように書くことができます。

import Tsuchi

// payload の型
struct FCMNotificationPayload: PushNotificationPayload {
    var aps: APS?
    // custom payload data
}

// payload の型と通知受取時の処理を渡して push 通知を subscribe する
Tsuchi.shared.subscribe(FCMNotificationPayload.self) { result in
    switch result {
    case .success(let notification):
        let (payload, _) = notification
        print(payload)
    case .failure(let error):
        // error handling
        break
    }
}

// ログアウトなどで通知の受取を終了するとき
Tsuchi.unregister {
    Firebase.User.current { user in
        guard let user = user else { return }
        _ = user.fcmTokens.removeValueForKey(UIDevice.current.identifierForVendor!.uuidString)
        user.update()
    }
}

Growth もしっかりしている

リリース後に関しても Analytics や Crash Report が用意されていること、 ユーザーの行動を元に機械学習でユーザーのセグメント分けをしてくれる Prediction 機能、さらにそれを利用してABテストを行う事もできるなど、サービスのグロースに関する部分でもかなり強力なツールが揃っているため、将来的にも有用だろうということで技術選定をしました。

外部サービスとの連携

ここまで Firebase の利点についてお話してきましたが、当然 Firebase では用意されていないサービスも多くあります。

例えば現在開発しているサービスで必要なものだと、全文検索や決済機能などがあります。 それらの機能はすでに外部 SaaS が用意されているため、私たちは図のように Firebase の FaaS (Function as a Service) である CloudFunctions を利用してそれらのサービスとの連携を行っています。

f:id:MiuP:20180209102151p:plain

CloudFunctions では DB への変更をトリガーにして関数を発火することができるため、変更のあったオブジェクトから外部サービスへ渡すデータを構成し渡すだけの必要最低限の実装で外部サービスとの連携が行なえます。 実際にどのように連携を実装しているかという部分に関しては自分が Firebase.Yebisu #1での登壇でまとめてありますので、こちらの記事も一緒に読んでいただければ詳しい部分についても理解できると思います。

Firebase コミュニティへのコミット

Firebase をフルでバックエンドに置くサービスは世界的にもあまり多くはありません。そのため開発において、壁に当たることも多くあります。私達のチームではそれらの問題と解決法に関して外部にアウトプットしていくことを積極的に行っており、Firebase.Yebisuといったイベントや、サンプルコードでもいくつか登場しましたが、ライブラリを OSS として Github 上で公開するなど、コミュニティへの貢献も進めています。 実際に私達のチームのメンバーが開発した Firebase 関連のライブラリは以下です。

  • Salada (Realtime Database model framework)
  • Pring (Cloud Firestore model framework)
  • Tsuchi (Firebase Cloud Messaging helper)
  • Lobster (RemoteConfig helper)

もし Firebase を利用した開発をする場合は、よろしければ一度使用してみてください。

まとめ

AWS + Ruby on Rails の会社だと思われがちな Cookpad ですが、社内外向けを問わず新規アプリケーションではサービス毎に特徴を考慮し様々なフレームワーク、言語を用いた開発が行われています。 先程も述べましたが Firebase をフルで利用しているサービスは業界でもそれほど多くはないと思いますので、今後もいろいろな形で経過を報告していけたらと思います。

Cookpad Tech Kitchen #9 〜1行のログの向こう側〜 を開催しました!

$
0
0

こんにちは! @yoshioriです。

2017/07/26、技術系イベント「Cookpad Tech Kitchen #9 〜1行のログの向こう側〜Cookpad Tech Kitchen #9」を開催しました。 (はい。僕が記事公開するの忘れててだいぶ遅くなっちゃいました>< ごめんなさい)

クックパッドの技術的な知見を定期的にアウトプットすることを目的とする本イベント。第9回目となる今回は「ログの活用方法」をテーマに開催しました。(月に1回程度開催しています)

月間6000万ユーザが使っているクックパッドには大量のログが集まってきます。そのログを効果的に活用してサービスやユーザに還元するための取り組みについて、インフラ、広告事業、サービス開発それぞれの視点で知見の発表を行いました。

発表資料を交えてイベントのレポートをしたいと思います。

f:id:cookpadtech:20171003220443p:plainイベントページ:

Cookpad Tech Kitchen #9 〜1行のログの向こう側〜 - connpass

発表内容

「クックパッドのログをいい感じにしているアーキテクチャ」

一人目の発表者であるインフラストラクチャー部部長の星(@kani_b)は、SRE としてAWSやセキュリティ関連でのサービスインフラ改善に携わっています。

今回は AWS Summit Tokyo 2016 Developer Conference で発表した内容の続きとして発表を行いました。 具体的な数字として Fluentd に流れているログベースで言うとデータ総量は 400 〜 600GB / 日、レコード数は 8 億レコード以上 / 日(秒間 8,000 〜 25,000 レコードくらい)という規模のデータを扱っています。これをどのように集めて処理しているのかを紹介しました。

資料

「広告ログのリアルタイム集計とその活用」

二人目の発表者であるマーケティングプロダクト開発部の渡辺(@wata_dev)は、主に配信基盤の改善やマーケティングプロダクト開発部で開発しているサービスの基盤周りのサポートを行っています。

クックパッドでは広告の配信も自社で行っており、そのためにどのようにログを活用しているのかを、過去どういった問題点があってそれをどのように解決していったか、異常検知だけではなく配信制御や在庫予測などなど広告配信というドメインで実際に必要になるケースを出しながら紹介しました。

資料

「ログを活用したサービス開発」

3人目の発表者であるサービス開発部の外村(@hokaccha)は、バックエンドからWebフロントエンド、モバイルアプリの開発など幅広い分野でCookpadのサービス開発に携わっています。

発表ではモバイルアプリのロギングのやりかたから始まり、実際のログの活用方法としてサービス開発側でログをどのように扱っているか、どう活かしているかを紹介しました。 行動分析としてログの設計、ログの分析やその可視化によってサービス改善の意思決定に使われていること、ログを利用した機能開発として調理予測という実際にログがサービスとして使われている事例を紹介しました。

資料

「ログ」をテーマにしたご飯も登場!

クックパッドのイベントではご来場の感謝を込めて、会場で手作りしたご飯でおもてなしをします(食べながら飲みながら発表を聞いていただくスタイル)。今回はテーマである「ログ」にちなんだメニューを用意してもらいました。

f:id:cookpadtech:20171003220752j:plain

こちらはメッセージ入りのライスケーキ。クックパッドのインフラエンジニアが大切にしている言葉に「1行のログの向こうには1人のユーザがいる」というものがあります。画面で見るとたった1行のログだけど、その向こうには大切にすべき1人のユーザがいる、ということを思い出させてくれる合言葉です!

f:id:cookpadtech:20171003221913j:plain

川嶋シェフの粋な心意気で、素敵なメッセージもテーブルに並びました。

f:id:cookpadtech:20171003221721j:plain

こちらはログ=丸太をイメージした湯葉のデザートです。中に入っているのは甘すぎないところがおいしいあんこです。

まとめ

いかがでしたか。クックパッドでは毎月テーマを変えて技術イベントを開催しています。ご興味のある方は是非ご応募ください。

cookpad.connpass.com

新しい仲間を募集中

■ 日本最大の食のビッグデータを扱う「データ基盤」の開発に興味がある方 https://info.cookpad.com/careers/jobs/careers/data-infra-engineer

■ 広告事業の「Webアプリケーション開発」に興味がある方 https://info.cookpad.com/careers/jobs/careers/marketing-product-engineer

■ クックパッドアプリの「Webアプリケーション開発」に興味がある方 https://info.cookpad.com/careers/jobs/careers/software-engineer

"体系的"に開発サイクルを回して "効果的"に学びを得るには

$
0
0

会員事業部エンジニアの新井( @SpicyCoffee66 )です。 Splatoon2 で各ルール S+1 以上になるため日々奮闘中のところに MHW が発売されました。 加えて最近ぷよぷよを始めたので、どう考えてもいろいろ計算が合わなくなってきました。

本日おこなわれた Cookpad TechConf 2018 では「クックパッドの "体系的"サービス開発」と題し、社内でどのような点に気をつけて開発サイクルが回されているかをお話しさせていただきました。 動画・発表資料は後日アップロードされる予定ですので、よろしければ合わせてご覧ください。 今回は、TechConf 2018 での発表内容から、BML ループの運用について、多少の補足や要約を交えながら書きたいと思います。

サービス開発は難しい

まず前提として、サービス開発は難しいです。 その難しさの大部分は、以下の2つの要因からきています。

  • 到達するべきサービスのゴールが明確でない
  • サービスの今いる地点が明確でない

到達するべきサービスのゴールが明確でない

サービス開発においては、ユーザーさんの持つ欲求や、抱えている課題を解決することがゴールとなります。 しかし、この欲求や課題は、ユーザーさん本人を含めて、誰にもわからないことがほとんどです。 また、仮に一度この欲求を捉えたとしても、その後時間とともに変化してしまうことが一般的です。

私たちが何もしなくても、ユーザーさんの使うデバイスはガラケーからスマートフォン、タブレットへと移り変わっていくでしょうし、 個々人のライフステージに関しても、独身だったユーザーさんが結婚し、子どもを持つようになるなど、多くの変化が起こり得ます。 このような状況のもとでは、ユーザーさんが抱えている欲求も、常に変化し続けると考えるほうが理にかなっています。

サービスの今いる地点が明確でない

得てしてサービス開発者は、自分のサービスを正しく理解できていないことがほとんどです。 サービスの価値はこうだ!コアとなる機能はこれだ!と信じていても、 実際にユーザーさんに使ってもらっているところを見てみると、想像と全く違う使われ方をしていた、なんてことは日常茶飯事でしょう。

学びのサイクル

前述したような状況の中でサービス開発に取り組むためには、自分たちの仮説をユーザーさんにぶつけ、その結果からフィードバックを得ることで新たな仮説を立てるという作業を繰り返す必要があります。 そうるすことで、目指すべきゴールや、サービスの今いる地点を確認しながら前に進んでいくわけです。 この「自分たちの仮説をユーザーさんにぶつけ、その結果からフィードバックを得ることで新たな仮説を立てる」という行為を、開発サイクルや学びのサイクルと呼んでいます。

BML ループ

学びのサイクルを実現するフレームワークに、BML ループと呼ばれるものがあります。 これは、リーン・スタートアップの中で提唱されているフレームワークで、以下の 3 フェーズから成ります。

  1. 仮説からプロダクトを作成する Build
  2. プロダクトをリリースしユーザーの利用状況を計測する Measure
  3. 得られたデータから知見を抽出し、新たな仮説を構築する Learn

f:id:spicycoffee:20180210144116p:plain

それぞれのフェーズの頭文字を取って BML ループと呼ばれているわけです。 このループを数多く回しながら、その都度学びを得ていくのがサービス開発では重要になってきます。

よくある失敗とその対策

しかしながら、サービス開発ではサイクルを回しながら学びを得るのが重要であるということがわかったところで、実際に BML ループを回そうとすると大抵の場合どこかのフェーズで失敗します。 具体的には、各フェーズで

  • Build
    • プロダクトが不必要に大きくなって実装に時間がかかる
    • 検証したい仮説と完成したプロダクトの機能が噛み合っていない
    • そもそも仮説に考慮漏れがある
  • Measure
    • いざ計測しようとするとログが埋まっていない
    • 複数の A/B テストが衝突して計測結果に影響が出る
    • 集計 SQL に間違いがあり、最悪の場合それに気がつかない
  • Learn
    • 出てきた数字をどう解釈すればいいかイマイチわからない
    • 数字は動いたがその原因がわからない、再現性が取れない
    • 得られた知見が属人的になる、あるいは闇に消える

といったような失敗がよく起こります。

f:id:spicycoffee:20180210144443p:plainf:id:spicycoffee:20180210144447p:plainf:id:spicycoffee:20180210144451p:plain

※ 巷にあふれる失敗例

このような失敗をなるべく減らすために、社内では 「最初に BML ループ全体を設計する」ということが意識されています。 そうすることで「手戻りの防止」や「効率的な学び」を実現することが可能になります。

最初にサイクル全体を設計する

手戻りの防止

BML ループを、Build が終わってから Measure、Measure が終わってから Learn といったように、逐次的に実行した場合、大きな手戻りに繋がる可能性があります。

たとえば、Build が終わって Measure のフェーズに入ったタイミングで、ログが取れてないなかったことが発覚した場合、もう一度 Build のフェーズに立ち返ることになります。 Learn のフェーズに入って知見を抽出しようとしたタイミングで、数字の解釈がよくわからないといった状況に陥ってしまうと、もはや手戻りをすることすら難しく、何の学びも得られないままサイクルを回し終えてしまうこともありえます。

しかし、よく考えると、前のフェーズが終わらなくても、次のフェーズで何をやるかについては考えることが可能です。 むしろ、BML ループ自体を一つのプロジェクトと考えると、各フェーズを前から順番に実行していくようなやり方よりも、 最初に全体を設計することが自然に思えてきます。 BML ループにおいて、最初に全体を設計するというのは、仮説が立った段階で、各フェーズで必要になりそうなことを明確にしておくことになります。 実際にサイクルを回しだす前に、各フェーズでの要件を明確にした上で、それに沿って Build・Measure・Learn と施策を進めていくことで、手戻りの原因となりうる事故を事前に察知し、それを防ぐことが可能になるのです。

効率的な学び

最初にサイクル全体を設計することは、効率的に学びを得るためにも必要なことです。 "学び"は定義しにくい概念ではありますが、その一つの重要な要素として、 サービスに対する理解と現実とのギャップがあげられます。

これをもう少し具体化すると、サービスに対する理解は、今のサービスに対して施策を打ったときの、施策結果に対する予想と考えることができます。 それに対応する現実は、実際に施策を打った結果です。 この2つを比較することで、思ったより結果が良かった/悪かったといった事実が出てきます。 その事実について「それはなぜか?」という点を考えることで、自分たちがサービスに対して抱いている理解のズレ・勘違いが明確になり、それが大きな学びになるのです。

f:id:spicycoffee:20180210144500p:plain

したがって、サービスに対する理解を事前に固めておくことは非常に重要なことになります。 今のサービスに施策を打ったとき、ユーザーさんはどのような体験をして、その結果どういう指標がどのくらい動きそうかということを事前に予想しておくことが大事なのです。 これはすなわち、事前に Measure や Learn のフェーズで出てくる結果について、考えを巡らせておくことになります。 こういった観点からも、実際にサイクルを回しだす前に、先のフェーズの設計をおこなっておくことが重要になってきます。

各フェーズの設計

この節では、BML ループの各フェーズについて、何を設計すればよいのか、つまりは、具体的にどのようなことを事前に決定しておけばよいのかについて書いていきます。

Build の設計

Build の設計では、以下のようなことを決定・確認しておきます。 とはいっても、このフェーズは仮説立案から距離が近いため、特に意識せずとも施策立案の段階で合わせてやっていることも多いでしょう。

  • 絶対に検証したい仮説の明確化
  • 検証背景の整理
  • 検証内容・注意点などの整理

Measure の設計

Measure の設計では以下の様なことを決定・確認しておきます。

  • 計測手法
    • A/B テストでいいのか?
    • A/B テストでいいとして、全ユーザーを対象にしてもいいのか?
  • KPI
    • 施策をどういう数字で評価するのか
    • 他に影響を与える指標はないか?
  • ログの確認、SQL の実行
    • 取りたいデータに必要なログは、現在集計されているか?
    • SQL を叩いた結果が概ね正しそうか?

特に KPI 周りの設計については注意が必要です。 指標というのはそれ単体で存在することは珍しく、大抵は相反する指標、影響を与え合う指標が同時に存在します。 これらの指標を見落としてしまうと、たとえば

TOP ページにプロトタイプとして会員登録の導線を置いたら、ある程度の会員登録が認められた
→ この施策を採用してプロダクトにリリースした
→ しばらく経ってみると、別のページ(検索結果ページ等)からの会員登録数が減っていた

といったような事故が発生します。 このような事例を防ぐために、予め関連する指標としてどのようなものがありそうかリストアップしておくことが必要です。

Learn の設計

Learn の設計では以下の様なことを決定・確認しておきます。

  • 指標の解釈
    • この数値が高くてこの数値が低いときは、ユーザーさんはどのような体験をしているのだろうか?
  • 結果の想定
    • 測定指標がどのくらいの数字になったら施策を採用するか
    • そこまではいかなくても、どのくらいの数字になったら再度議論するか

この2つの項目について事前に考えておくことを、社内では「成功のイメージを共有する」といったような言葉で表現することが多いです。 施策が成功したときに、ユーザーさんがどのような体験をして、その結果どういった指標がどの程度まで上がっているだろうか といったことを事前に想定しておきます。 そうすることで、結果から意味のある知見を抽出しやすくなりますし、効果の薄い施策を採用してしまう可能性も減らすことができます。

まとめ

上述したように開発サイクルを回していくやり方は、既に当たり前の方法となっています。 しかし、当たり前の方法だからこそ、自分たちの中で注意するポイントをしっかりと定めて運用することで、より大きな効果を期待することができます。 組織の特性や置かれている状況によって、注意するポイントは変わってくると思いますが、一つのベースとして、この記事がみなさんの参考になれば幸いです。

施策の質と職務能力を高めたい!ディレクター会の取り組み

$
0
0

こんにちは。サービス開発部 ディレクターの五味です。 Android版クックパッドアプリのリリースマネージャーと、アプリ利用者に関わるいくつかのプロジェクトを担当しています。今回は私たちの部で実施している、ディレクターの定例会について紹介します。

f:id:natsuki53:20170829223631p:plain

サービス開発部

クックパッドの開発体制は、2年前に私が ディレクター知見共有会についてのエントリー*1を書いた頃から少し変遷を経て、2017年からはサービス開発部が、レシピ検索・投稿などの基幹機能と、サービス全体のユーザー体験を一手に管轄するようになっています。

部のメンバーは現在40人ほどおり、部の注力指標からブレイクダウンしたKPIをベースに9つのプロジェクトチームに分かれています。チームの編成や人数は様々で、状況に合わせて入れ替わりもOK、KPI達成に向かっていれば、各チーム主体的に動くことが推奨される柔軟な組織を試みています。

プロジェクトチームで働く中で

このような体制の利点は、自分のチームのミッションに対して裁量を持って施策を考え取り組めることです。やりがいがある反面、以下のような悩みを感じるようになりました。

  • 部の目標に対するチーム横断での進捗度や、自分のチームの遅れが見えづらい
    • チームで決めた施策を進めるだけで、施策数や速度は本当に十分なの?
  • チームが自律的に動く反面、チーム間の情報連携や相互補完が難しい
    • 他のチームは目標をどう考えてどんな施策をしているのか、知りたいけど聞きづらい…
  • ディレクターとしての自分の成長がわからない
    • この職種に必要なスキルは何なのか、自分のパフォーマンスは足りているんだろうか?

ディレクター会の発足

これらの悩みを持ち掛けた方々から助言を得て、部のディレクターがチームを越えて集うディレクター会を始めることにしました。部内のディレクター職の他、ディレクター不在のチームからは同等の役割を担っている他職種の方にも声をかけます。

初回の開催で、会の目的とアジェンダを以下のように決めました。

  • 会の目的
    • サービス開発部でディレクターの役割を持つ人の情報・知見をチーム横断で共有する
  • 成功のイメージ(会の参加者に対して)
    • 担当施策について目標に対する成果を把握し、責任を持って報告できるようになる
    • 部内の施策の内容・効果を横断的に把握し、自分の提案に活かせる
    • 定期的に悩み相談や意見交換をする機会を得て、施策の精度が部署全体で上がる
    • ディレクターとしてのスキルアップに積極的に取り組めるようになる
  • アジェンダ(60分)
    • ① 実施した施策の共有 30分
    • ② 施策やチーム運営の相談 20分
    • ③ その他アナウンス、連絡事項 10分

意識したことは「先週これをやりました、今週これをやります」という業務進捗報告に時間を割かないことです。他のチームの施策の進捗を聞いても必要な情報や問題を見出すのは難しいことと、ディレクターなのでチームの進捗管理は各自できている前提にしたかったためです。

会議の時間は1時間、開催頻度は週1回と仮決めしてスタートしましたが、これは毎週ちょっとだけ時間が足りないくらいアジェンダがある状態が続けられているので、そのまま継続しています。

「実施施策の共有」について

ディレクター会のメインコンテンツにしている施策の共有について少し紹介します。

この会では、部で実施する施策をできるだけすべて議題にあげたいので、施策共有用に手間のかかる資料は作らないことにし、GitHub Issue に報告事項の箇条書きだけ準備する方式にしました。

ただし、箇条書きの項目はテンプレートで決まっており、報告には、仮説・試算・実数・考察・次のアクションの5項目が必要です。PDCAを回せるような設計がきちんとできていない施策はこの5つに埋められない項目が出てくるため、施策を考える人の自浄装置のような働きをしています。

例えばこのディレクター会をテンプレートに沿って報告しようとすると、下記のようになります。

# 施策名:サービス開発部のディレクター週例
- 仮説
  - ディレクターが定期的に施策情報を共有し意見交換できる場ができると、部全体の施策の精度とスピードが上がる
- 試算
  - 部の施策数が週5本(各チーム2週に1本)になる
  - 部の目標達成の進捗度が10%上がる
- 実数
  - 施策報告数:2〜3本/週
  - 部の目標達成進捗度:変化なし
- 考察
  - 定性意見より、会があることで施策/プロジェクトの成功への責任者意識は強まった
  - 他チームの成功・失敗事例やお互いの助言を担当案件に活かせる機会はできた
  - ただ、実際の施策のスピードやKPIの進捗に変化が起こるほどの成果には至っていない
- 次のアクション
  - アジェンダの見直し:参加メンバーに課題提起し、次の会で改善策を話す時間を取る

また直接この会に起因することではありませんが、最近サービス開発部では、施策結果のレポートをPull Requestで作ってチームでレビューする手法が採られ始めています。何かをリリースして完了ではなく、検証内容を振り返り次にどう進めるのかの判断にチームで取り組めることと、メンバーがレビューに入ることで、施策に対するチームの理解が揃う利点があります。

ディレクター会ではこれらの箇条書きやPull Requestを見ながら、施策共有に使える30分を週ごとの施策数で割って時間配分を決め、どんどん報告していってもらいます。報告を聞いている側の人は、気になる点や使える知見があれば自由に発言してもらい、特筆すべき意見は後で議事録に残して使ってもらいます。

ディレクター会の効果と課題

現在、この会を始めて2ヶ月ほどが経ったところです。前段の報告テンプレートの事例で少し前述していますが、現時点で良かったと感じている点は以下です。

  • 他チームの成功・失敗事例や、他のメンバーの助言など、自分の施策に活かせる第三者からの情報を得やすくなった
  • 週ごとに報告できる施策の数から、各チームの進捗スピードが推し測れるようになった
  • ディレクター:プロジェクトを成功に進める責任者という意識を合わせ、施策に取り組めるようになった

反面、まだ成果は定性的なものに止まっており、施策のスピードや部の目標達成の進捗に効果が表れるには至っていません。またディレクターのスキルアップのような長期の取り組みには手を出せていない状況です。

ちょうど先週これらを課題として改善策を相談し、次から以下の2つを変更してみる予定です。

  • 施策報告を、終了した施策だけでなく、これから実施する施策も対象にする
    • 結果だけだとチームが何を考えてその施策をしたのかわからない、終了施策にツッコミをもらっても「次頑張ります」としか言えないという意見から。施策の改善の余地に事前に気づいて検証の精度を上げられるように。
  • 進行中施策に直接紐づかない大きめのトピックも持ち込むようにする
    • 仮説定義や分析手法のノウハウなど、具体的な解がすぐ出せないから話題にしづらいが、各自悩みの深い相談を持ち掛けられるように。

このような取り組みを継続させるコツ

前回ディレクター知見共有会のエントリーを読んだ方から「うちはこういう会を始めても3回で自然消滅します…」という感想をいただいたので、大変僭越ですが、複数のメンバーを巻き込んで定常的な取り組みを行う際に意識していることを紹介させていただきます。

1. 参加者のコストを必要最小限にする

時間と手間を取りすぎないことを念頭に置いています。 今回であれば、会議が1時間を過ぎないよう時間配分することと、準備はGitHubのIssueにテンプレートに沿った箇条書きで済むようにしています。

2. 参加者がすぐ活かせる粒度の情報を入れる

「ディレクターに必要なスキルとは?」といった少し高い次元の議論だけでなく、明日から自分の業務に使える実用的な情報を得られる議題を含めることで、参加の利点を感じやすくします。 そのためディレクター会では実施施策の話題に時間を厚めに充てています。

3. “他人事” になっているメンバーを放置しない

取り組みが軌道に乗ってから1番気を配る点です。会議中ぼんやり聞いているだけの人が出てくるようになったら要注意です。敢えてその人に指名で意見を求めてみたりして反応を見ながら、会議の内容自体に原因がないか見直しを考えます。

最後に

ディレクターはエンジニアやデザイナーに比べて職務定義が難しいということをよく聞きます。また1つのプロジェクトに複数名でアサインされることは少なく、1人で複数のプロジェクトを掛け持ちすることは多いため、各自が抱える情報や知見を共有するには意識的な働きかけが必要だと感じます。

ただ、どんなプロジェクトでどのような働きをしているにせよ、ゴールに向かってチームを進めていく大事な役割を担っていることは確実だと考えます。

開発者がすごい!と言われるクックパッドですが、「ディレクターもすごいんです!」と言えるよう、今後も頑張っていきたいと思います。

そして、そんなチームに一緒に加わって頑張ってくださるメンバーを募集しておりますので、よろしくお願いいたします! https://info.cookpad.com/careers

*1:注: 「ディレクター知見共有会」はそのあと対象を広げ、今は参加者の職種は問わず様々な部署の体制や取り組みについて聞ける場として継続されています。

クックパッド サマーインターンシップ2017 「17day 技術インターンシップ」を開催しました

$
0
0

いつもお世話になっております。エンジニア統括マネージャーの高井です。

クックパッドでは毎年恒例となりつつある、クックパッドのサマーインターンシップのうち「17day 技術インターンシップ」を開催しました。インターンに来てくれた学生のみなさんは本当に優秀で、毎日真剣に取り組んでくれました。本当に感謝しています!

インターンは、前半の「サービス開発講義・課題」パートと後半の「サービス開発実践」から構成されています。前半パートでの講義について資料を公開いたしますので、みなさまぜひご覧ください。

f:id:takai_naoto:20170831080358j:plain


【1日目】サービス開発

初日は、クックパッドで実践されているサービス開発の手法について学ぶワークショップです。グループでのユーザーインタビューを通じてサービスの設計をしました。

【2日目】Rails・TDD・Git

昨年に引き続き、講義初日はGit、TDD、Railsを1日で一巡りするという、忙しい構成でした。

【3日目】モバイルアプリケーション

3日目は、 iOS と Android のふたつに分かれて、 Google 社の Firebase をつかった Cookpatodon というマイクロブログ風のアプリケーションを題材に学習をしました。アプリケーションの基本部分を実装したあとは各自で自由に機能を実装してもらい、最後に成果発表会という形で発表してもらいました。皆ユニークな機能を実装して大変盛り上がりました。

【4日目】インフラストラクチャー

Web アプリケーションのインフラについてAWSをつかいながら、Railsアプリケーション動作させるところから、パフォーマンスチューニング、スケールアウト、キャッシュなどのトピックについて触れています。

(資料は公開準備中です)

【5日目】SQL

Redshiftで構築されたデータウェアハウスをつかって、分析用のSQLを書いていました。クックパッドの実際のデータをつかったので、参加者たちは億単位のレコードがあるテーブルと格闘していました。

(内部データを利用した講習のため資料の公開はありません。どのようなものだったかを知りたい方はこちらまで。こちらの書籍でも概要を知ることができます)

【6日目】機械学習

機械学習は講義と実習のセットになっており、講義では「機械学習とは何か」という概観とディープラーニング(特にCNN)を学んだうえで、実習ではクックパッドのデータを使ったレシピ分類に取り組みました。最後は各々が興味をトピックを取り組んでもらってその成果を提出しました。

【7日目】Ruby

最終日のRubyの講義では、RubyでRubyのコンパイラを実装したり、その最適化を行ないました。


番外編

前半の講義が終わった懇親会では、先輩社員による就職活動の体験談LTなどが行なわれ、参加者のみなさんが楽しんでいました。

f:id:takai_naoto:20170831080459j:plain

その後の二次会も盛り上ったようです。

クックパッドと分散トレーシング

$
0
0

こんにちは、技術部の Taiki (@taiki45) です。

近年の Web サービスの開発ではマイクロサービスに代表されるように分散アーキテクチャが採用されるようになってきました。大規模でも素早いプロダクト開発をするために、クックパッドでもマイクロサービスを採用し分散アーキテクチャへの移行を進めています*1。今回は、そのような分散アーキテクチャを利用したシステム構築において必須のコンポーネントになりつつある分散トレーシングについて、クックパッドでの事例を紹介したいと思います。

分散トレーシングとは

マイクロサービスのような分散アーキテクチャでは、個々のサービス同士の通信が複雑になるため、モノリシックアーキテクチャと比較して、システム全体としての振る舞いを把握することが難しくなります。これはプロダクト開発においては、障害発生時の原因究明が難しくなったり、あるいはシステム全体でのパフォーマンスの分析が難しくなるといった問題として顕在化します。 分散トレーシングはこのような問題に対処するためのツールです。開発者が、特定のクライアントリクエストを処理するのに関わったサービスを探したり、レイテンシに関するパフォーマンスをデバッグする時に利用されます。

分散トレーシングの実現のアプローチには大きくわけて2種類あり、一つは Black-box schemes *2、もう一つが Annotation-based schemes と呼ばれています。 前者の Black-box schemes はシステム内の各サービスに手を入れる必要がないことが利点ですが、それと引き換えに特定のリクエストに対する分析はできません。後者の Annotation-based schemes は各サービスに分散トレーシング用のメタデータを下流サービス*3へと伝播させる実装を加えることが必要になるという欠点がありますが、特定のリクエストを分析することができます。Annotation-based schemes は Google の Dapper*4や Twitter の Zipkin*5等に採用されており、Web サービス業界では主流なようです。

Annotation-based schemes に基づく実装

Annotation-based schemes に基づいた分散トレーシングシステムの実装の仕組みを大まかに説明すると、ユーザーからリクエストをうける最初のポイントで “トレースID” という分散トレーシングシステム内で一意となる文字列を発行し、トレースIDや “アノテーション” と呼ばれる処理結果等の追加情報を含んだログをストレージに保存し、さらに下流に存在するサービスへリクエストを発行する際にトレースIDとアノテーションを伝播していきます。このような「トレースIDに紐付く一連のログのまとまり」を “トレース” と呼びます。このトレースをトレースIDをキーにしてストレージから検索することにより、特定のリクエストに関わったサービスを特定したり、また複数のトレース情報を集計することで分散システム内のコミュニケーションパターンを分析することができます。また、各サービスがリクエストの処理を開始/終了した時刻もトレースログに一緒に保存すれば、レイテンシの算出もできます。Google の Dapper や Twitter の Zipkin といった実装では、トレースログのタイムスタンプから各ログの親子関係を算出するのではなく、 各トレース内で一意となる文字列である “スパンID” をログの識別子として利用し、ログの親子関係をスパンIDで表現するようになっています。

ほとんどの分散トレーシングシステムは次のようなコンポーネントに分解できます:

f:id:aladhi:20170905163926p:plain

  • Instrumented library: 各サービスのアプリケーションに組み込み、トレースIDの採番や伝播やトレースログの送信を担うライブラリ
  • Log Collector: 各サービスインスタンスから送信されるトレースログの集約を担う
  • Storage and Query:トレースデータの保存と検索を担う
  • UI: 人間がトレースデータを検索・分析する際に利用する

Instrumented library は各言語向けに整備する必要があるので、各分散トレーシングシステム普及のボトルネックになっています。この問題を緩和すべく OpenTracing*6のように Instrumented library API の標準化を進めているプロジェクトもあります。

クックパッドでの導入

分散トレーシングシステムの選定

クックパッドで分散トレーシングを導入するに当たり、いくつかの点を考慮して AWS が提供するマネージドサービスである AWS X-Ray*7を採用しました。クックパッド内では一般的なユースケースを想定しているので、既存の分散トレーシング実装を利用することを決めました。分散トレーシングシステム実装として採用例も多く開発も活発な Zipkin に焦点を当てましたが、大規模な環境で Zipkin を利用するには Cassandra/HBase/Manhattan いずれかの運用が必要であり、データストアを自分たちで運用するよりは、解決したい問題にフォーカスできるマネージドサービスの利用に比較優位がありました。クックパッドでは AWS を積極的に活用するインフラストラクチャを構築していることもあり AWS X-Ray の検証を始めました。

検証開始時点では AWS X-Ray が提供する Instrumented library は Java/Node.js/Python のみのサポート*8で、クックパッドではほとんどのサービスは Ruby を用いて実装されているため、そのままでは AWS X-Ray は利用できませんでした。サードパーティ製のものも特に存在しなかったのですが、Instrumented library の実装方法については目処が立っていたこと、及び Instumentation library を自分たちで管理できることで他の分散トレーシングシステムへ低いコストで移行できる余地を残せる利点があったので、自作することにしました。自作した Instrumented library である aws-xray gem は OSS として公開しています*9

現状の構成

AWS X-Ray を利用したクックパッドでの分散トレーシングは以下のような構成で実現されています:

f:id:aladhi:20170905163959p:plain

  • Instrumented library: aws-xray gem を利用
  • Log Collector: AWS X-Ray の提供する X-Ray daemon というソフトウェア*10を利用
    • ECS を利用しているアプリケーションではいわゆる Sidecar 構成を取っています
    • EC2 インスタンス上で動作しているアプリケーションについては EC2 インスタンスの上に X-Ray daemon プロセスを動作させています
    • Instrumented library から UDP で X-Ray daemon にトレースログを送信し、X-Ray daemon がバッファリングと AWS の管理する API へのトレースログの送信を担います
  • Storage and Query: Storage はこちら側からは見えません。Query として AWS X-Ray の提供する API*11を利用します
  • UI: AWS コンソールに組み込まれています*12

aws-xray gem の実装において、トレードオフを考慮しつつモンキーパッチを活用することにより、ほとんどの Rails アプリケーションでは gem の導入と X-Ray daemon への接続情報を設定するのみで、トレースログの収集を開始できるようになっています。

今後の展望

現状はデータを AWS X-Ray に集めるところまでで、まだ本格的なトレースデータの活用には至っていません。データの収集については、社内の主要なサービスをカバーしており、サービスマップのノード数は現在約70ほどです。

f:id:aladhi:20170905164033p:plain

エラートラッカーなどに記録されているトレースIDから該当リクエストに関する分析ができるようになっています:

f:id:aladhi:20170905164117p:plain

サンプリング方式については Head-based coherent sampling*13を採用しており、ユーザーからリクエストを受ける最初のサービスで sampled/not sampled を決めて下流サービスに伝播させています。サンプリングレートについては、特に rps の高いサービスのみ1%設定、他のサービスについては100%設定で運用しています。サンプリングについては課題があり、ミッションクリティカルなサービス*14の処理を含むトレースはトラブルシューティング用途に全件保存しておきたいですが、流量の高いサービスが上流にいるケースではサンプルされるトレースの割合が少なく、トラブルシューティングを行うユースケースで支障があります。その対策として、パス毎によるサンプリング設定等を実装・導入する予定です*15

クックパッドでは Barbeque*16という非同期ジョブシステムを利用して非同期ジョブを実行しています。多くのジョブは Web アプリケーションのリクエストによりトリガーされているので、リクエストとジョブ実行との紐付けを記録できるようにする予定です。

また、システム全体のレイテンシ変化を検知できるように、AWS X-Ray の API を利用して監視システムを構築する予定です。監視システムについては自前で実装する以外にも AWS X-Ray の機能追加にも期待しています。

おわりに

AWS X-Ray を利用した分散トレーシングの実現について、クックパッドでの事例を紹介しました。クックパッドでは比較的大規模な Web サービス開発が行われており、分散アーキテクチャ周辺に存在する興味深い問題が多々あります。このような課題解決を一緒に取り組む仲間を積極的に募集しています

*1:http://techlife.cookpad.com/entry/2016/03/16/100043

*2:M. K. Aguilera, J. C. Mogul, J. L. Wiener, P. Reynolds, and A. Muthitacharoen. Performance Debugging for Dis- tributed Systems of Black Boxes. In Proceedings of the 19th ACM Symposium on Operating Systems Principles, December 2003.

*3:ここではユーザーからリクエストを受けるフロント側を “上流"、その反対側を "下流” と呼びます

*4:https://research.google.com/pubs/pub36356.html

*5:http://zipkin.io/

*6:http://opentracing.io/

*7:https://aws.amazon.com/xray/

*8:今では Go 言語向けのライブラリもサポートされました https://aws.amazon.com/jp/about-aws/whats-new/2017/08/aws-x-ray-sdk-for-go-beta/

*9:https://github.com/taiki45/aws-xray

*10:http://docs.aws.amazon.com/xray/latest/devguide/xray-daemon.html

*11:http://docs.aws.amazon.com/xray/latest/api/Welcome.html

*12:http://docs.aws.amazon.com/xray/latest/devguide/xray-console.html

*13:http://www.pdl.cmu.edu/PDL-FTP/SelfStar/CMU-PDL-14-102_abs.shtml

*14:例えば課金系サービス

*15:http://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-configuration.html#xray-sdk-java-configuration-sampling

*16:https://github.com/cookpad/barbeque


データ分析からUI改善

$
0
0

こんにちは。サービス開発部デザイナーの平塚です。

クックパッドでは一部のデザイナーは日々の業務でSQLを書いて数値を見たり、リリースした施策の分析を行っています。 このエントリーでは機能をリリースしてデータ分析し、そこからUI改善を行った事例について紹介したいと思います。

なぜデザイナーがデータ分析?

サービスやプロダクトを改善するには現状について定性的・定量的の両方を理解しておく必要があります。
そのため、自分が進める施策やデザインするものを数値で把握しておくことで、より納得感を持って施策を進められます。
データ分析というと数学や分析の深い知識が必要そう…と構えてしまう印象ですが、日頃から自分の担当分野の基本的な数値を見ておくだけでもデザインで悩んだときの判断材料として使えるなど、デザイナーが数値をみる利点は多々あります。

分析の流れ

私は最近からデータ分析に取り組み始めたのですが、今はこのような流れで分析しています。

1.分析に必要な数値・グラフを決める

GitHubで分析用issueを立てて何を知りたいのか、そのためにはどんな数値をどんな形で見れると良いかを決めてから数値出しに進みます。

2.数値を出す

数値を出したらSQLが正しいかエンジニアにレビューしてもらいます。 レビューが通ったら数値から考えられることをチームで話しながら分析します。

3.分析レポートにまとめてGitHubのPull Request(以下PR)をだす

f:id:tsukasio:20170911145527p:plain

分析レポートを「仮説・試算・実数・考察・次のアクション」で整理してPRを出します。
レビューを受けることでさらに理解を深めるきっかけになったり、客観的な意見でアイディアをもらえたりします。
また、サービス開発部ではディレクター定例で施策の共有を行っていて、施策の分析結果はこのPRを共有しています。
ディレクター定例についてはこちらで詳しく説明されています。

つくれぽを簡単に送れる機能の分析

一部のユーザ向けに、よく見たレシピを利用したつくれぽを簡単に送れる機能を公開し、仮設通りの効果は得られたのか?どのように使われているのか?などを分析しました。

f:id:tsukasio:20170911145848p:plain

分析を進めていくうちに、アプリを起動してからつくれぽを送るまでの推移をファンネルグラフでみたところ、離脱ポイントが2つあることがわかりました。

  • 「投稿する」の分岐で離脱
  • レシピをフリックしてから離脱

f:id:tsukasio:20170911145750p:plain

まず「投稿する」の分岐での離脱はどんなユーザーが離脱しているのか調べました。結果はつくれぽを送ったことがないユーザーがほとんどで、興味本位で「投稿する」をタップしていた可能性がありそうです。
次に、つくれぽしようと思ってこの画面に来たのにフリックした後離脱してしまうのはなぜかを考えました。

  • つくれぽを送れるレシピがなかった
    • 実際には作っていなかった?
  • 料理画像がなかった
    • レシピをみて料理を作ったが料理画像を撮り忘れた?
  • つくれぽしようと思うレシピがわからなかった
    • 似たようなレシピが複数並んでいた?
    • レシピ名をきちんと覚えていなかった?

いくつか仮説を立てた中で、3つ目はUIで解決できそうだということになり改善を進めました。

UI改善

この機能のデザインをした時に考えたことは、新しい機能とはいえ、つくれぽを送るというアクションなのでユーザーが戸惑わないように既存のつくれぽ画面を参考にデザインしました。また、さくさく送れる感じを出したかったのでレシピをフリックして見れるようにしました。
ただ、既存のレシピ詳細画面からのつくれぽはすでにレシピを決めているので、つくれぽ画面ではレシピ名だけでもスムーズにつくれぽできていたという違いに気づきました。
この機能はつくれぽ送信画面に来てからつくれぽするレシピを探すので、どのレシピかがきちんと分かるUIが良いのではと思い、レシピ画像とレシピ作者名を入れたUIに変更しました。

beforeafter
f:id:tsukasio:20170911150004p:plainf:id:tsukasio:20170911150637p:plain

まとめ

現在は施策を企画する段階とリリース後の2つのタイミングで定量データを見ることを心がけています。
漠然とした「使いづらい」「分かりづらい」から改善を進めるのではなく、定量的なデータからその機能がどう使われているかを把握した上で仮説を立てると、より良い改善に繋がります。
ただ数値がすべてというわけではなく、定量データから見えない課題はユーザーテストで掘り下げるなど定量分析・定性調査をバランスよく見ていくと良いと思います。

料理きろくにおける料理/非料理判別モデルの詳細

$
0
0

研究開発部の菊田(@yohei_kikuta)です。機械学習を活用した新規サービスの研究開発(主として画像分析系)に取り組んでいます。 最近読んだ論文で面白かったものを3つ挙げろと言われたら以下を挙げます。

クックパッドのアプリには「料理きろく」という機能があります。 携帯端末から料理画像のみを抽出して表示することで自分が食べたものを振り返れるようになっており、ここからレシピ投稿やつくれぽを送ることもできるようになっています。

20170914153231

料理きろくはユーザ数が約12万8千人、累積の写真判別枚数が約7900万枚、そのうち料理と判別された画像が約960万枚(数字は20170912時点)と多くの方々に使っていただいている機能です。 本記事では、その料理きろくのコアの技術部分である、機械学習による料理/非料理判別の詳細に関してお伝えします。 料理きろく全体のアーキテクチャに関してはここでは述べませんが、ご興味のある方は AWS Summit Tokyo での発表資料をご覧ください。

料理きろくの背景と機械学習の役割

我々は、新しい技術を駆使してユーザ体験をより良いものに改善することで、クックパッドの使用頻度を増やしてもらったり、ユーザからもっとレシピやつくれぽを投稿してもらう、などの目標を持っています。 料理きろくはその目標に資する一つの施策であり、これは携帯端末に大量の食事情報が記録されていることから、それを活用してクックパッドのサービスに連携することを狙いとしています。 過去の自分の食事を振り返ることで食への楽しみや関心を高めてもらい、そこからレシピやつくれぽ投稿につなげたいというのが最初のターゲットとなっています。

ちなみに料理きろくではユーザのプライベートな画像を判別することになるため、全てサーバ上で処理がなされ、我々はユーザの画像を一切閲覧できないようになっています。

料理きろくにおける機械学習の役割は、料理/非料理を判別する高性能のモデルを提供することです。 機械学習によって正確に画像の料理/非料理が判別できるかどうかがサービス全体の質に直結するため、ここに Convolutional Neural Network (CNN) を用いたモデルを適用しました。 一口に CNN と言っても実に多様なので、データも拡充しながら、様々なモデルを試行錯誤をして改善を図りました。

以降では本番環境にデプロイしたモデルを中心に、どのような試行錯誤で改善をしていったのかをご紹介します。

最初にデプロイしたモデル

  • モデル:CaffeNet
  • データ:{料理,非料理}の二値ラベルの画像データ
  • フレームワーク:Chainer

最初のリリースでは CaffeNetというモデルを使っています。 これは ILSVRC2012で優勝した AlexNetを少し変更したもので、pooling が Local Response Normalization の前に来ています。

最初の段階では、素早くリリースまで持っていきたい、社内に画像分析の知見がまだ蓄積していなかった*1、などの理由で、事例も多いこのモデルを採用しました。 データはシンプルに料理画像と非料理画像をとりあえず手当たり次第に集めたものを使用しました。 テストデータも集めた画像の一部をテスト用に切り出して、料理判別の精度と再現率をチェックするというごくごく基本的な手法でした。

本番運用に際しては、精度が低いと料理ではないものが表出されてユーザ体験が悪くなるという考えのもと、再現率をある程度犠牲にしても精度を高めるという方向で閾値を調整しました。 単純な二値判別では softmax の出力が0.5を超えれば料理と判別されることになりますが、このモデルでは閾値を0.9で設定しました。 これは閾値を変えながらテストデータでの結果を目視でチェックしながら定めたものです。

最初のモデルは手探りの部分も多かったですが、素早くリリースまで到達できたことは非常に良かった点で、継続的改善を遂行していける土台が整えることができたので後の改善につながりました。

一回目のモデルアップデート

  • モデル:Inception-v3
  • データ:{料理,非料理,複数の間違えやすい非料理カテゴリ}の多値ラベルの画像データ
  • フレームワーク:TensorFlow

最初にデプロイしたモデルでも結構精度が高かったのですが、自分たちでも使っていくうちに間違いやすい画像があることが分かってきました。 具体的には植物や赤ちゃんの画像などが間違いやすい傾向があることが判明したため、これらの画像にもロバストなモデルを作りたいという要望が出てきました。 また、CNN のモデルも様々な発展があるため、それらを検証してより基本的な性能が高いモデルを採用したいという考えもありました。

そこで、まずは様々な CNN のモデルを比較検証をして良いモデルを探すという実験をしました。 この頃には画像分析ができる人員も増えていたため、手分けをして実験をして GHE の wiki に情報を集約し、最終的に我々のデータセットに対して最も良い結果を出した Inception-v3を採用することにしました。 モデルとして試したのは、{Inception-v3, GoogLeNet, ResNet, VGG, GAN を使った classification, NIN(軽量なモデルにも興味がある), …}、です。

フレームワークに関しても、TensorFlow を使う人が多くなったため、Chainer から切り替えました。 モデルの学習には Keras with TensorFlow backend を使っていますが、本番にデプロイする時は TensorFlow のみで動かすようにしています。 料理きろくのアーキテクチャはモデル部分を疎結合にしてあるので、この変更はそれほど大きなコストもなく実現できました。

次に単純な二値判別では問題として単純化しすぎているのではという考えのもと、多値判別に切り替えるという実験をしてみました。 思考回路としては、仮に世の中の料理画像の全集合が手に入ればその補集合が非料理画像だがそれは不可能→モデルは我々が集めた(非)料理画像の集合から(非)料理らしさを学習→これらは多様なので一つのカテゴリに集約し切るのは無理がありそう→特に間違いやすいものに関しては陽にカテゴリを作ってそちらに誘導したほうが我々が望むモデルができそう、という感じです。 料理/非料理ともに画像(二値ではなく多値のラベルを付与したもの)を追加収集して、それぞれが単一カテゴリ(この場合は多値の情報を潰して二値として扱う)の場合と複数カテゴリの場合とで性能を比較しました。 モデルの出力は一般に多値になりますが、判別結果としては多値の情報を潰して料理/非料理の二値判別として扱うようにしています。 全体的な性能を上げつつ特に精度を高めるものとして、料理は単一カテゴリとして非料理は多値カテゴリ(具体的には植物や人物を含む5カテゴリ)として扱うことに決定しました。

これらの改善によって、手元のデータで試験したところ、精度も再現率も向上し、特に間違えやすかった植物の画像に対しては間違いが約 1/3 に、赤ちゃんの画像に対しては間違いが約 1/20 ほどになりました。

これの取り組みは以前クックパッドで開催された Cookpad Tech Kitchenでも発表しています(発表資料)。

また、2017年度 人工知能学会全国大会やIJCAIのワークショップとして開催された 9th Workshop on Multimediafor Cooking and Eating Activitiesなどの学術的な場でも発表をしています。

二回目のモデルアップデート

  • モデル:Inception-v3 + patched classification
  • データ:{料理,非料理}の二値ラベルの画像データ、それらを14×14のパッチにしたもの
  • フレームワーク:TensorFlow

一回目のアップデートで大きく改善はしましたが、画像中に人と料理が同時に写っている場合はモデルが判断に迷う(人と判断すべきか、料理と判断すべきか)という問題が残っていました。 我々は「画像中の一部を切り取ってクックパッドのレシピとして掲載できそうなものは料理と判断する」という基準で料理/非料理を判断しているので、十分に料理が写っているのにモデルが非料理と判断されているものは改善の余地がありました。

この問題に取り組むために、まずはマルチラベルの判別をすることを考え、そのために Keras の ImageDataGenerator 辺りを改修したりもしましたが、データ準備のコストが高いため一旦保留としました。 次に、問題の根本は複数のものが写っているのにそれらをまとめて判別してしまっていることだと考え、画像をパッチに分割してパッチ毎に料理/非料理を判別するというモデルを構築しました。 具体的には、通常の Inception-v3 の出力付近で使う GlobalAveragePooling と Dense を、Conv2D や Dropout などを組み合わせて出力が 14×14×1 にするように置き換えて、sigmoid の出力で binary cross entropy を計算するようにしています。 パッチに分けることで料理と非料理を区別しやすくなることが分かったため、再び二値分類のモデル(ただしパッチ毎)になっています。 パッチサイズの 14×14 に関してはいくつかのパターンを実験した結果最も良い結果を返すものを選択しました。

このモデルの学習にはパッチ毎にラベルが付与されたデータが必要ですが、これは単純に元データをパッチに分割して、全パッチに元データと同じラベルを付与するという作り方でデータ準備を簡略化しました。 ただしこの作り方だと特に画像の端の部分が悪さをする可能性があるので、適切なラベルが得られるように一部の画像を crop したりもしています。

また、本番にデプロイした場合の性能を見積もるために、本番でのデータ分布に近くなるように社員から許諾を得て携帯端末のデータを提供してもらいました。 プライベートな画像のため、閲覧権限を絞って、特定の人が正解ラベルを付与してそれを使ってモデルの詳細な性能検証を実施しました。

このような改善を経て本番環境にデプロイされたモデルの結果の一例が以下の図となります。 色がついている領域が料理らしさが高い領域で、閾値以上のパッチを取り出してその領域が一定以上であれば料理と判別するというモデルになっています。

20170914153255

この改善によって、テストデータに対して精度を落とさずに再現率を5%以上改善することができました。 料理きろくは1000万というオーダーで画像を処理しているので、改善のインパクトは大きなものとなります。

モデル構築に使用したデータ

クックパッドには大量の料理画像があるので正例のデータには事欠きませんが、モデルの学習には負例のデータも重要になるため Creative Commons のデータや社員からデータを集めたりして様々な画像を収集しました。 プロジェクトの進行と共に画像を追加して試行錯誤してという作業を繰り返し、結果としてトータルで数十万枚程度の画像を扱いました。

どのような画像が重要になったかは上述のモデルアップデートでもご紹介しましたが、改めてモデルの性能向上に重要であった特徴的なデータをいくつか挙げたいと思います。

  • 料理 
    当然ながら正例としての料理画像は最重要のデータとなります。 クックパッドの豊富な料理画像データを用いて充実したデータセットを構築することができました。
  • 人(特に赤ちゃん) 
    人物画像は判別を間違えた場合のリスクも高い(ユーザ体験の観点から)ため、負例の中でも特に気をつけて画像を収集して学習に用いました。
  • 植物 
    想像に難くないですが、色合いや形状から誤判別される場合が多かったのが植物の画像でした。
  • 植木鉢 
    植物と似ていますが、植木鉢の場合は器もあるためにより一層誤判別されるものが多かったです。 そのため意識して負例としてデータを収集しました。
  • 料理が乗っていない空の皿
    料理には皿がつきものなので、皿があれば正例と勘違いしかねないため何も乗っていない皿も負例としてデータに追加しています。
  • 本番運用時にモデルが扱うデータ分布に近いテストデータ
    本番ではユーザの携帯端末中の画像が対象ですが、学習は収集したラベル付きの限定的なデータを用いているので、モデルの正しい性能が測りづらいという問題があります。 ユーザの画像は我々も閲覧できないため、この問題はオフライン/オンラインに限らず原理的な問題です。 そこで、許諾を得た社員のスマホのデータを使い、人力でラベルを付け、これを使ってモデルの評価を実施しました。 プライベートな画像であるため、閲覧権限を必要最低限の人間に絞って詳細な性能評価をして、他の人には統計情報のみを共有するという手法を採りました。

やはり学習データは最重要ですね。 料理きろくにおいては、多くの人の協力によって様々なデータを収集することができたので、非常に心強かったです。

今後の展望

料理きろくのプロジェクトを通じて、やはり継続的な試行錯誤に基づいた適切な改善策を講じることが重要であることが再認識できました。 それゆえに、これで完成ではなく、日進月歩の Deep Learning 技術を取り込んだり、データを拡充したりして、よりユーザにとって有用なモデルを構築し続けるのが大事だと考えています。

例えば最新の SENetや料理ドメインに特化した 横方向に広くスライスした畳み込みを試してみたり、自分たちで使いながら判別を間違った画像を収集してその傾向を探る、より高速に動作するモデルを構築するなど、改善の方向性は様々です。

「自分ならもっと良いモデルが作れる!」という人がいましたら是非一緒にやりましょう。

おまけ

以前 Twitter で話題になっていた画像として、犬と食べ物の区別が難しい画像というものがありました。 みなさんは下の画像のどれが犬でどれが食べ物かが判別できるでしょうか? 遠目で見ると判別はかなり難しい感じがします。

20170914153246

画像は以下から引用させていただきました。
左の画像:https://twitter.com/teenybiscuit/status/707727863571582978
右の画像:https://twitter.com/ohmycorgi/status/867745923719364609

これらの画像に我々の料理/非料理判別モデルを適用して、料理画像だけを抽出してもらうとしましょう。

20170914153237

見事に料理画像だけを選び出すことができました! Deep Learningのモデルが適切に料理とそれ以外を区別できていることを伺い知ることができます。

これ以外にも、近い内容の話として 弁護士の柿沼太一先生との対談などもあります。 ご興味があれば是非ご覧ください。

まとめ

クックパッドアプリの料理きろくという機能で用いている料理画像判別技術に関してお伝えしました。 CNN のモデル自体はもの凄く特殊なものを構築しているわけではありませんが、試行錯誤を経たモデルの変遷やその過程で遭遇したタスク特有の問題点などに興味を持っていただけたなら幸いです。 本記事でお伝えしたのは一例であり、クックパッドでは様々なサービスにおいて、発展著しい機械学習の技術をユーザに有益なものへと昇華させる取り組みに日々励んでいます。

いかがでしたでしょうか。 クックパッドでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。 クックパッド株式会社 研究開発部 採用情報

*1:このプロジェクトが始まった段階では私はまだ入社していませんでした

Cookpad Tech Kitchen #10を開催しました。

$
0
0

f:id:tsukasio:20170915160252p:plain

こんにちは、サービス開発部デザイナーの平塚です。

2017年9月13日(水)に、クックパッドオフィスにてデザイナー向けイベント「Cookpad Tech Kitchen #10」を行いました。

f:id:tsukasio:20170915160042j:plain

今回は「自社サービスで取り組むデザイン」をお題に、開発現場での実践事例を交えながら各社の文化やデザイナーの働き方についてご紹介しました。

お集まりいただいた皆さまの熱量も高く、イベントスタート時からQAセッション、懇親会に至るまで、とても充実した時間を過ごさせていただきました。 ご来場いただいた皆さま、本当にありがとうございました。

また今回は多数応募をいただいたため残念ながら抽選に漏れてしまった皆さま、たいへん申し訳ありません。 こういったイベントは今後も開催していきたいと思いますので、またのご参加を心よりお待ちしております!

一部ですが、イベントで行った各プレゼンテーションの概要をご紹介します。

「"料理の追体験"を実現するデザイン」

  • 若月 啓聡(Cookpad/デザイナー)

新機能タイムラインのデザインや、チームのユーザーとの関わり方についてお話を聞くことができました。

f:id:tsukasio:20170915160058j:plain

「はてなブログの世界観になじむ機能デザイン」

  • 松井 沙織(はてな/デザイナー)

はてなブログの新機能やUI変更の中で、書くことを邪魔しないデザインについてお話を聞くことができました。

f:id:tsukasio:20170915160114j:plain

「温度のあるサービスづくり」

  • 木坂 名央(GMOペパボ/シニアデザイナー)

minneの、作家に向き合った温かみのあるデザインについてお話を聞くことができました。

f:id:tsukasio:20170915160133j:plain

「自然さを追求した音楽体験のためのUX」

  • 冨樫 晃己(CyberAgent/Product Manager)

AWAの、現実世界にありえる自然な動きについてお話を聞くことができました。

f:id:tsukasio:20170915160153j:plain

QAセッション

パネルディスカッションでは、

「UIデザイナーとUXデザイナーは兼任していますか? 実際の所UIとUXの兼任は難しいと思うのですがどのように分業や兼業していますか?」

「ドッグフーディングをチーム内で浸透させるのにはどうしていますか?」

といった参加者からの質問について、各社の取り組みについてより深く触れていただきました。

f:id:tsukasio:20170915164720j:plain

これからも楽しいイベントを企画していきます。 今後ともよろしくお願いします!


【クックパッドではデザイナー/エンジニアを積極採用中です】
ユーザー体験に向き合ってサービス開発をしたいデザイナーやエンジニアの方は、下記をご覧ください。

クックパッド採用情報 | UX/UIデザイナー
https://info.cookpad.com/careers/jobs/careers/ux-ui-designer
クックパッド採用情報 | エンジニア
https://info.cookpad.com/careers/jobs/careers/type/engineer

Synthetic Monitoring を活用したグローバルサービスのネットワークレイテンシの測定と改善

$
0
0

インフラ部 SRE グループの渡辺(@takanabe)です。普段はクックパッドのグローバルサービス (https://cookpad.com/us) のインフラの開発や運用をしています。

クックパッドは、21 言語・67 カ国以上を対象にサービスを展開しています ( 2017 年 6 月末時点)。今後もその数を増やしていく予定です。 世界中で使われるサービスのインフラを開発していく上で、乗り越える必要がある課題は沢山ありますが、その中でも、ユーザが利用するクライアントとクックパッドのインフラをむすぶネットワークのレイテンシは特に大きい課題です。 本稿ではなぜグローバルに利用されるサービスにおいて、ネットワークレイテンシが問題になるのか、また、クックパッドではネットワークレイテンシをどう計測し改善しようとしているかについて解説します。

ネットワークレイテンシとは

ユーザがサービスにリクエストを送ってからレスポンスを受け取るまでにかかる時間 (レスポンスタイム) は、主にネットワークレイテンシとアプリケーションの処理時間の合計です。 アプリケーションの処理時間短縮もサービスのレスポンスタイム改善には有用ですが、グローバルに展開されたサービスにおいてはネットワークレイテンシも大きなオーバーヘッドになりえます。この両方を改善していくことがユーザ体験向上のために重要です。本稿ではネットワークレイテンシについてご紹介します。

ネットワークレイテンシは大きく分けると以下の 4 つから構成されています。

  • 伝播遅延: クライアントがパケットを送出してから私達の管理するサーバに到達するまでの時間(あるいはその逆方向の通信にかかる時間)
  • 伝送遅延: パケットがリンクに載るまでの時間
  • 処理遅延: ルータがパケットのヘッダをチェックして宛先を決定するまでの時間
  • キューイング遅延: ルータのパケット処理待ち状態の際にバッファキューで待機する時間

一方で私たちが普段ネットワークレイテンシという言葉を使う場合は、往復の伝搬遅延、つまり Round-trip-time を意味することが多いです。本稿でもネットワークレイテンシ(以下レイテンシ)を Round-trip-time (以下 RTT ) の意味で使います。

グローバルサービスとレイテンシ

クックパッドのグローバルサービスのサーバは現在 AWS の米国東部リージョンに集約されているため、サービスの通信は基本的にはユーザの居住地と米国との間を往復することになります。

サーバが米国東部リージョンに集約されていることで、米国や米国近辺に住んでいるユーザはレイテンシが小さくなります。一方で、アジアや中東など地理的に遠い国に住むユーザにとってはレイテンシを悪化させる要因の一つとなっています。例えばリクエストがネットワークを伝わる速度を光速( 300,000 km / sec)とし、日本から米国東部までの距離を 11,000 km としたとき、レイテンシは約 73.3 ms になります。現実には、サーバまでのネットワークの経路は一直線ではありません。加えて、伝送において 300,000 km / sec もの速度が出ることはないためレイテンシはさらに大きくなります。*1

f:id:takanabe_w:20170920163049p:plain

例として、東京の私の家から Amazon S3 の東京リージョンのエンドポイントと米国東部リージョン( us-east-1 )のエンドポイントに ping を打つと以下のように平均レイテンシは前者は約 22.2 ms、後者は約 186.2 ms でした。

# AWS の東京リージョンのエンドポイントに ping を打った場合
> ping -t 5 s3-ap-northeast-1.amazonaws.com
PING s3-ap-northeast-1.amazonaws.com (52.219.68.108): 56 data bytes
64 bytes from 52.219.68.108: icmp_seq=0 ttl=50 time=21.811 ms
64 bytes from 52.219.68.108: icmp_seq=1 ttl=50 time=20.666 ms
64 bytes from 52.219.68.108: icmp_seq=2 ttl=50 time=24.138 ms
64 bytes from 52.219.68.108: icmp_seq=3 ttl=50 time=22.797 ms
64 bytes from 52.219.68.108: icmp_seq=4 ttl=50 time=21.750 ms

--- s3-ap-northeast-1.amazonaws.com ping statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 20.666/22.232/24.138/1.167 ms

# AWS の米国東部リージョンのエンドポイントに ping を打った場合
> ping -t 5 s3.us-east-1.amazonaws.com
PING s3.us-east-1.amazonaws.com (54.231.120.114): 56 data bytes
64 bytes from 54.231.120.114: icmp_seq=0 ttl=43 time=179.987 ms
64 bytes from 54.231.120.114: icmp_seq=1 ttl=43 time=208.230 ms
64 bytes from 54.231.120.114: icmp_seq=2 ttl=43 time=176.016 ms
64 bytes from 54.231.120.114: icmp_seq=3 ttl=43 time=182.457 ms
64 bytes from 54.231.120.114: icmp_seq=4 ttl=43 time=184.399 ms

--- s3.us-east-1.amazonaws.com ping statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 176.016/186.218/208.230/11.357 ms

164 ms のレイテンシの差がユーザに与える影響

HTTP がクライアントとサーバ間の通信の往復から成っていることを考えると、この 164 ms のレイテンシの差がユーザが快適にサービスを使えるかに大きな影響を与えます。例えば、クックパッドは HTTPS を利用して暗号化された安全な通信をユーザに提供しています。 HTTPS に利用されている TLS 接続を確立するには下図のように TCP ハンドシェイクに 1.5 往復、 TLS ハンドシェイクに 2 往復の通信が必要です。

f:id:takanabe_w:20170920163203p:plain

(https://hpbn.co/transport-layer-security-tls/#tls-handshake Figure 4-2. TLS handshake protocol より引用)

TLS ハンドシェイクの Client Hello メッセージは TCP ハンドシェイクの ACK と同じタイミングで送出されることから、 TLS 接続には正味 3 往復必要になります。 つまり RTT の 3 倍の時間がかかります。

この TLS 接続が成立するまでの時間を先程例に上げた東京と東京リージョンの往復で換算すると、22.2 x 3 = 66.6 msの時間がかかることになります。一方で、東京と米国東部リージョンの往復で換算すると、186.2 x 3 = 558.6 msの時間がかかることになります。

その差は 492 ms です。 この数値は一見問題にならないようにも感じられますが、Jakob Nielsen の著書 Usability Engineering ではレスポンスタイムには3つの境界値が存在すると言われています。

0.1 second is about the limit for having the user feel that the system is reacting instantaneously, meaning that no special feedback is necessary except to display the result.

1.0 second is about the limit for the user’s flow of thought to stay uninterrupted, even though the user will notice the delay. Normally, no special feedback is necessary during delays of more than 0.1 but less than 1.0 second, but the user does lose the feeling of operating directly on the data.

10 seconds is about the limit for keeping the user’s attention focused on the dialogue. For longer delays, users will want to perform other tasks while waiting for the computer to finish, so they should be given feedback indicating when the computer expects to be done. Feedback during the delay is especially important if the response time is likely to be highly variable, since users will then not know what to expect.

(Jakob Nielsen, “Usability Engineering”, 1993, pp 135)

これによると人間はリクエストを送出して 100 ms 〜 1 sec 以内のレスポンスでも遅延を感じるとされています。つまり、 164 ms のレイテンシの違いが生む 492 ms の差はユーザの体験を悪化させる要因に成り得るのです。

今までのクックパッドのレイテンシ対策

クックパッドのグローバルサービスは、日本のサービスとは使われているリージョンやコードベースが異なりますが、cookpad.com ドメインを共有しています。 同じドメインにおいて異なるアプリケーションにリクエストを振り分けるため、ロードバランサ (ELB) 下のリバースプロキシでリクエストがグローバルサービスのもの (/uk, /id など) か日本のサービスのものかを判定してルーティングをしています。 ただ、アプリケーションがリージョンを跨いでいるのにリバースプロキシを 1 リージョンに置くだけではリバースプロキシが無いリージョンへのリクエストが遅くなってしまうため、 同様の設定がされたリバースプロキシを東京 (ap-northeast-1) と米国東部 (us-east-1) リージョンに配置し、Amazon Route 53 のレイテンシベースルーティング*2を利用して DNS ベースでユーザーからレイテンシの低いリージョンへ最初にリクエストを送信させるようにしています。 これにより、ユーザーから近いリージョンのロードバランサにアクセスできるというメリットがあります。

以上のようにレイテンシを増加させる要因はサーバと利用するユーザの所在や使っている技術により異なります。クックパッドの場合サービス利用者が多いインドネシアなどではこのレイテンシの問題は顕著に現れてきています。

レイテンシの計測方法

レイテンシを改善するにはレイテンシがどのような要素から成っており、何をした時にどのくらい改善されたのか、あるいは悪化したのかを定量的に評価しなければなりません。これを実現するにはレイテンシやレスポンスタイムなどのメトリクスを計測し続ける必要があります。

サービスのレイテンシを計測する方法を大別すると、以下の二つが挙げられます

  • Synthetic Monitoring
  • Real User Monitoring(RUM)

Synthetic Monitoring は専用の監視サーバからリクエストを送出して計測します。一方で RUM はユーザのクライアント上で実際にかかった時間そのものを収集します。この2つの方法はどちらが良いと言うわけではなく、集計の粒度も計測する対象も異なるため組み合わせて使うものです。今回は平均的な統計情報をまず取得し、その上でレイテンシを悪化させている要因を分析するという目的があり、Synthetic Monitoring を導入することにしました。

Synthetic Monitoring “Catchpoint”

クックパッドは Catchpoint Systems の Synthetic Monitoring サービス(以下 Catchpoint )を利用しています。 Catchpoint を選択した理由は他の Synthetic Monitoring ツールと同様の日別パフォーマンスの比較ができる点、 Waterfall Chart などの基本的な機能を有している点、 UI がシンプルである点で条件を満たしており、加えて監視サーバのノード数とロケーションの数が他社のものより多いためです*3。サービスの世界展開を目指しているクックパッドにとってこれは重要な機能のひとつです。

Catchpointによるパフォーマンスの分析

パフォーマンス解析機能による全体像の把握

Catchpoint では計測対象のエンドポイント、監視サーバ群、監視の頻度など監視に関する条件を定義するテストを作る必要があります。この記事では https://cookpad.com/usに対して複数の国の監視サーバからアクセスする “Global top page” というテストを用意しました。

パフォーマンス解析の機能を使うことでこの Global top page テストで定義した各国の監視ノードから https://cookpad.com/usにアクセスした際のレスポンスタイム(ms)を確認できます。例えば、2017年2月25 - 27日の3日間を対象に横軸を日時、縦軸をレスポンスタイムにして描画するとこのようなグラフが得られます。

f:id:takanabe_w:20170920163403p:plain

ご覧の通りテストで選択した監視サーバの国別のレスポンスタイムを俯瞰することができています。これをみるとインドネシア、アルゼンチン、エジプトなどの国のレスポンスタイムが相対的に悪いですね。グラフの上にポインタを乗せると他のメトリクスを確認できたり、この時間帯の Waterfall chart に飛ぶこともできます。 Waterfall chart については後述します。

また、 Geo chart という機能を使うと監視サーバそれぞれでパフォーマンスメトリクスの一つを地図上で可視化できます。以下ではサーバにリクエストを行って最初の1バイトが到着するまでの時間を示す Time To First Byte(TTFB)を表示しています。インドネシアは TTFB が他国と比較して長いことがわかります。物理的な距離が影響しているかもしれません。

f:id:takanabe_w:20170920163426p:plain

このようにおおまかなパフォーマンスメトリクスをパフォーマンス解析機能で確認し、当たりをつけ、その後より細かい分析を行うのが自然な流れかと思います。次は上記で確認したインドネシアのパフォーマンスを Waterfall chart で分析していきたいと思います。

Waterfall chartを使ったパフォーマンスボトルネックの分析

Waterfall chart の画面では特定のエンドポイントに特定の監視サーバからアクセスしたときのパフォーマンスメトリクスの詳細な分析ができます。例えば、2017年2月26日12時にインドネシアのジャカルタの監視サーバから https://cookpad.com/us ( Global top page テストを利用)にアクセスした時の Waterfall chart の画面はこのような感じです。ご覧の通り、名前解決や TLS 接続などを含む TTFB 、レンダリング開始、対象のページのレンダリングが完了したことを表す Document Complete などの時間が確認できます。 f:id:takanabe_w:20170920163450p:plain

また Waterfall chart を見るとどのリクエストがパフォーマンスに悪影響を与えているかが簡単にわかります。 Global top page テストの場合 cookpad.com/us にアクセスした際の最初のリクエストの TTFB で全体の約1/3の時間を要しています。さらに、TTFB の内訳を確認すると名前解決や TLS 接続に多くの時間を割いている事がわかります。本稿の最初に TLS ハンドシェイクについて言及しましたが、ここで計測された TLS 接続に必要な時間を短くする事は、すなわちレイテンシの改善につながります。

f:id:takanabe_w:20170920163504p:plain

またいわゆるクリティカルレンダリングパスが赤く塗りつぶされているのでどのリクエストをパフォーマンス改善のターゲットにすれば良いのかが分かりやすくなっています。以上が Cacthpoint の基本的な機能と使い方の紹介になります。他にもダッシュボードを作って public url で共有できたり、トランザクション処理のあるリクエストのボトルネックを解析したりと様々なことができます。

クックパッドでの Catchpoint の使用例

ここまでわかればこれらをどのように改善すべきかという手段の話ができるようになりますね。当初の目的の通りレイテンシを改善するのであれば相関関係が強い名前解決、TLS 接続、TTFB などを改善する方法を少し検討してみます。

サーバのマルチリージョン展開によるレイテンシ改善効果の調査

米国東部に集約しているサーバをインドネシア付近のデータセンタにも展開した場合 RTT はどのように変わるでしょうか。簡単な効果測定は Catchpoint の Instant Test 機能(定期的な計測ではなく任意の監視サーバから任意のエンドポイントに単発のリクエストを実行する機能)を使うことでできます。インドネシアの監視サーバから AWS の米国東部( us-east-1 )、東京( ap-northeast-1 )、シンガポール( ap-southeast-1 )の各リージョンに対して ping を打った結果を比較すると、インドネシアは米国東部や東京よりシンガポールの方が RTT は小さくなることがわかります。

f:id:takanabe_w:20170920163546p:plain

AWS のシンガポールリージョンを使うことでインドネシアのレイテンシは改善されそうですね。わざわざ現地に行かずとも世界中の監視サーバからのリクエストのパフォーマンスが計測できる Instant Test はとても便利です。

CDNの導入によるレスポンスタイム改善効果の調査

CDN の導入もレイテンシの改善に効果があります。CDN は主にキャッシュ用途で使われることが多いですが、今回は TCP と TLS の 終端のためだけに使っています。ユーザーとの接続を近いサーバで終端することで、レイテンシに大きく寄与する TCP および TLS ハンドシェイクの時間を短縮します。CDN のエッジサーバからオリジンとなるアプリケーションサーバへの TLS 接続は HTTP Keep-Alive により再利用することで、さらにレイテンシを短縮することができます。

クックパッドのグローバルサービスでも全てのリクエストを CDN を経由させる施策を進めています。しかし、全てのユーザに大きな影響がある上、それなりにコストをかける必要があるため、 CDN 導入後に改善効果がありペイするのか、 どの CDN を導入すれば良いかなどの検証を Catchpoint で行いました。 以下は CDN 利用前と Fastly と X 社の CDN を利用した場合のある API のレスポンスタイムの比較です。

f:id:takanabe_w:20170920163619p:plain

これを見るとインドネシアのユーザに対しては Fastly と X 社 共にレスポンスタイムの改善効果があるとわかります。現在はグローバルサービスには部分的に Fastly を導入しています。TLS 接続や全体のレスポンスタイムは以下のようになりました。期待していた通り、 TLS ハンドシェイクの短縮やレスポンスタイムの改善がされています。

f:id:takanabe_w:20170920163639p:plain

余談ですが、2017年7月中頃までは Fastly を導入するとアルゼンチンのユーザのレスポンスタイムは悪化するという計測結果が出ていました。その時点で Fastly もブラジルにデータセンタ、 いわゆる Point of Presence(POP) を持っていたのですが期待した結果が得られなかったため原因を調べました。 すると、当時アルゼンチンからのトラフィックではブラジルの POP が使えない状態であることがわかりました。その代わりに米国西部の POP が使われていたのです。計測せずに導入していた場合、大きなコストをかけてユーザ体験を悪化させていた可能性がありました。現在は Fastly でもブラジルの POP のキャパシティが拡張*4されて POP 数も増えました*5。南米のユーザのレスポンスタイムを改善する際の一つの手段となり得ると思います。

おわりに

この記事ではクックパッドのグローバルサービスにおいてなぜレイテンシが問題になっているのか、それを Catchpoint でどのように計測改善しようとしているのかについて書きました。

Catchpoint で日々レイテンシの計測をしているため問題の解決に集中できる環境が整いましたが、グローバルサービスのパフォーマンス改善はまだ始まったばかりです。今後は RUM の導入やパフォーマンス計測によって得た結果をレイテンシやレスポンスタイムの改善に活かし、世界中のユーザがサービスをより快適に使えるようにしていきたいと思っています。

参考文献

  • Jakob Nielsen, “Usability Engineering”, 1993
  • A・S・タネンバウム, “コンピュータネットワーク第4版”, 2003
  • 竹下隆史, 村山公保, 荒井透, 苅田幸雄, “マスタリングTCP/IP 入門編 第5版”, 2012
  • Ilya Grigorik, “High Performance Browser Networking”, https://hpbn.co/

たのしくなるコードレビュー

$
0
0

こんにちは!サービス開発部でAndroidアプリの開発をしているこまたつ(@k0matatsu)です。

みなさんコードレビューしていますか?
最近ではとりいれているチームも多いと思いますが、良い効果をもたらしてくれる一方で、負荷の高い作業でもあります。
また、コードレビュー自体に馴染みの薄かった人はなにをどうしたらいいのか難しいですよね。
同僚から得たアドバイスと自分なりのノウハウをあわせて、コードレビューの指針を考えていたので公開してみようと思います。

前提として、クックパッドではGitHub Enterpriseとプルリクエストを使った開発プロセスを採用しています。
また、コードレビューの前には自動テストと静的解析ツールによる単純なフォーマット、コードスタイル、頻出バグのチェックは行われているものとします。
静的解析による機械的なチェックはコードレビューよりも低コストで有効な方法ですので是非取り入れてみてください。

コードレビューの目的

なにをやるかの前に、なぜやるかをハッキリさせておくことはとても大事です。
目的を明確にしておくことで、判断が必要になった際に指標になります。

コードレビューの目的は会社やチーム、レビュアーとレビュイーの関係性などによって様々ですが、私は次の二軸に比重を置いています。

  • 品質向上
  • スキルアップ

品質向上

プログラムを書いているのは人間なので、ミスが発生します。
コンパイラや静的解析をすり抜けて来るものもありますし、全体の設計に沿っているかなど、人間でなくては確認が難しい要素もあります。
コードレビューを行うことで、複数人の違う視点が入るため、ミスを検出し「読みにくい」などの感覚的な部分のフィードバックも得ることができます。
ここで言う品質とは、バグの量ではなく、可読性やメンテナンスのしやすさも含めたソースコード全体の品質をさします。

スキルアップ

コードレビューの中で疑問を解決したりアドバイスを得ることで自分自身が知り得なかった情報を得ることができます。
自分のスキルに不安があっても、疑問を感じた部分を積極的に質問していくことで多くの学びが得られます。
一方的な査読ではなく、双方向コミュニケーションの場と捉えることでレビュアーとレビュイー双方のスキルアップが期待できます。

何に注意するべきか

チェックすべき項目は多岐に渡りますが、次のような部分を重点的に確認します。
ここであげる項目の他に、ドメイン知識など他の開発者よりも詳しい分野があれば、その知見を使ってフィードバックを行います。

  • アーキテクチャ・設計
  • 挙動
  • 改善

それぞれどのようなチェックを行うか、掘りさげて見ていきましょう。

アーキテクチャ・設計

目的に沿った設計がされているか、全体のアーキテクチャに沿った設計になっているかを確認します。
具体的には次のようなところを重点的に確認しています。

  • 単一責任原則: ひとつのメソッドに違う目的の処理を入れない
  • 命名: 一連の処理の中で統一されているか
  • 粒度: プルリクエストを分割した方がいい部分はあるか

また仕様に疑問を感じた場合も、コードレビューと一緒にその仕様で問題ないか確認してもらいます。

挙動

主に準正常系を見ていきます。
Androidの場合はとくに次の場合に予期せぬ挙動をすることが多いので注意しています。

  • バックキーが押されたとき
  • バックグラウンドから戻ったとき
  • 画面回転をしたとき

改善

より良い書き方があれば指摘します。
AndroidではSDKにTextUtilsやDateUtils、Uri.Builderなどの便利クラスが存在します。
この手のクラスは存在自体が知られてない場合もあるため、積極的にオススメしていきましょう。
標準ライブラリ以外にも、チーム内で運用している便利クラスやメソッドは新しいメンバーは知らないことが多いです。

手順

ここまで、コードレビューの内容部分をみてきました。
次はどのような手順でレビューを行っているかを記します。
人によってやりやすい方法は様々ですが、参考になれば幸いです。

  1. 内容を把握する

    • どのような目的で書かれたコードなのか、主な変更点などをdescriptionを読んで確認します
    • 必要な情報が足りなければ追加をお願いすることもあります
  2. 軽いチェックを行う

    • typoや粒度、明らかな間違いなど、ブラウザから差分を見ただけですぐに判断できる問題が無いか確認します
    • この時点でたくさん問題が見つかった場合は一旦修正を待ってから次のステップへ進みます
  3. 挙動を確認する

    • 実際の挙動を確認します
    • 前に上げたとおり、準正常系をメインに正常系と、切断などの簡単な異常系も確認します
    • 挙動が確認できないものや、確認する必要がないもの、微細な修正の場合はスキップする場合もあります
  4. 細かいチェックを行う

    • 挙動が問題なければソースを読んで細かいチェックを行います
    • 必要があればコードを手元に持ってきて、呼び出し箇所を調べることもあります
  5. 修正後の確認

    • 修正差分の確認を行います
    • 修正量によりますが、コミット内容だけをチェックする場合と、ステップ2からチェックしなおす場合があります

このように、コードレビューを軽いチェックと細かいチェックの2層に分けています。
作業の合間などに軽いチェックを行い、まとまった時間が取れるときに細かいチェックをすることで、レビュイーに素早くフィードバックが返せるように心がけています。
他にも、なるべくポジティブなコメントもつけるようにすることで心理的負担を減らす工夫をしています。
冒頭で軽く触れた、静的解析による機械的なチェックもレビュアーの負担を軽減するための取り組みのひとつです。

おわりに

コードレビューは業務の中でも集中力を要する大変な作業のひとつではないでしょうか?
技術によって人間が負担する部分を減らしていくのが理想ではありますが、コードレビューは多くの学びを得られるチャンスでもあります。
自分なりの手順を決めてこなせるようになってくると、今まで気が重かったタスクも楽しく進められると思います。
やっていきましょう。

Cookpad Ruby Hack Challenge 開催報告

$
0
0

f:id:koichi-sasada:20170929111101j:plain

技術部の笹田です。Ruby インタプリタの開発をしています。先日、RubyKaigi 2017 のために、広島に行ってきました(その話はまた別途)。

本記事では、2017/08/30, 31 に弊社で開催した Cookpad Ruby Hack Challenge (RHC) の模様についてご紹介します。

短いまとめ:

RHC の概要

Cookpad Ruby Hack Challenge は、Ruby インタプリタ(MRI: Matz Ruby Interpreter)に対して機能を追加したり、性能向上させたりする方法、つまり Ruby インタプリタを Hack する方法を、二日間かけてお伝えするイベントでした。細かい中身の話はせずに、最低限必要となる手順を一通り体験してもらう一日目と、自由にハックを行う二日目にわけて行いました。

イベント申し込みページにて6月末から7月末まで募集をしたところ、10人の定員に100名近くのご応募を頂きました。急遽、定員を5名追加し、15名定員としました。加えて、記事を執筆いただくために池澤あやかさんにご参加いただき、また弊社から4名の希望者が(サポート要員をかねて)参加しました。当日欠席は1名のみで、19名+私、という体制で行いました。

参加者とのコミュニケーションは Gitter を用いました。https://gitter.im/rubyhackchallenge/Lobbyという場所で連絡を行ったり、質問を受け付けたりしました。また、Ruby コミッタの集まる場所を https://gitter.im/ruby/rubyにも作り、Ruby の質問ができる状態にしましたが、遠慮したためか、参加者からの質問は、あまりありませんでした。

イベントの流れ

初日は基礎編、二日目は応用編という流れでした。

一日目は基本的に座学で、資料に沿って進めて頂きました。 解散が 17:00 と早いのは、私が保育園へお迎えに行かなければならないからでした。

二日目に、好きなテーマに挑戦してもらいました。また、その間にサブイベントとして、「まつもとゆきひろ氏 特別講演」「Ruby開発者との Q&A」を行いました。これらを開催するために、毎月行っているRuby開発者会議を、裏番組として同時開催してもらいました。

8/30 (水) 一日目

  • 10:00 オープニング
  • 10:30 ハックに必要となる事前知識の講義
  • 12:00 ランチ
  • 13:00 共通課題
  • 16:00 発展課題の紹介と割り振り
  • 17:00 解散

8/31 (木) 二日目

  • 10:00 発展課題の開始
  • 11:30 まつもとゆきひろ氏 特別講演
  • 12:00 Ruby開発者を交えてのランチ
  • 13:00 Ruby開発者との Q&A セッション
  • 14:00 発展課題の再開
  • 18:30 打ち上げパーティー

一日目 基礎編

f:id:koichi-sasada:20170929111031j:plain

https://github.com/ko1/rubyhackchallengeにある資料をもとに、説明を聞いてもらい、演習を行ってもらう、というように進めました。

資料をざっとご紹介します。

  • (1) MRI 開発文化の紹介
    • MRI の開発は、誰がどのように行っているのか、大雑把に説明しています。
    • バグ報告の仕方など、一般的な知識も含んでいます。
  • (2). MRI ソースコードの構造
    • MRI のソースコードの構造を紹介し、演習として、MRI をビルドしてもらいました。
    • 演習といっても、実際に行う手順は書いてあるため、その通りに手を動かしてもらう、というものになっています。ここで扱った演習一覧を抜き出します。
      • 演習: MRI のソースコードを clone
      • 演習: MRI のビルド、およびインストール
      • 演習:ビルドした Ruby でプログラムを実行してみよう
      • 演習:バージョン表記の修正(改造)
  • (3) 演習:メソッドの追加
    • 実際に、MRI に機能を追加していきます。
    • 次のようなメソッドを、演習として追加してもらいました(手順はすべて記述してあります)。
      • Array#second
      • String#palindrome?
      • Integer#add(n)
      • Time#day_before(n=1)
    • また、拡張ライブラリの作り方や、デバッグに関する Tips を補足しています。
  • (4) バグの修正
    • バグの修正方法と、バグの報告の方法について紹介しています。
    • 次の二つのケースについて、具体的な話を紹介しています。バグ発見の技術的な方法に加え、心構えみたいなことも書いているので、そこそこ実践的な内容だと思いますが、どうでしょうか。
      • 他の人のバグ報告を見る場合(Kernel#hello(name)という架空のメソッドを例に)
      • バグを自分で発見してしまった場合(Integer#add(n)という架空のメソッドを例に)
  • (5) 性能向上
    • 性能向上についての諸々の話を書いています。
    • このあたりは、最後の方に書いたので、だいぶ雑になっています。演習もありません。

読むだけで進められるように書いたつもりなので、興味がある方は、読んで実際に手を動かしてみてください(読んでもわからない、という場合は、どの辺がわかりづらいか、こっそり教えてください)。

二日目 応用編

発展課題として、いくつか課題の例をあげておきましたが、これに限らず好きなことに取り組んで頂きました。ただ、こちらにリストした内容を選んだ人が多かった印象です。取り組んで頂いたテーマは、GitHub の issue でまとめてもらいました https://github.com/ko1/rubyhackchallenge/issues

いくつかご紹介します。

Hash#find_as_hash の実装

Hash#findの返値が配列なので、Hash を返す版が欲しい、という新規メソッド開発の挑戦です。が、1要素の Hashを返しても使いづらい、ということに気づいたそうで、nice try! ということで、終わりました。

フレームスタックの可視化

VM の状態を可視化するために、各フレームの状態を JSON で出力する仕組みを作り、そしてそれを表示するビューア https://github.com/kenju/vm_stackexplorerを作ってくださいました。懇親会でデモまで行ってくださいました。一日でここまでできるとは。

help に --dump オプションを追加

ruby -hで出てくるメッセージに不足があったので、足しましょう、という提案です。この挑戦を行ったのは Ruby コミッタの sonots さんで、さすが手堅い、実際に困ったんだろうな、という提案です。

なお、参加者に Ruby コミッタの sonots さんも混ざっているのは、サポート役としてお願いしたためです。後で伺ったら、曖昧にしていたことが多く、得るものは多かったということです。

Procに関数合成を実装

Proc#composeという、二つの Procを合成する、いわゆる関数合成を行うためのメソッドを提案されました。一度試したことがあったそうで、C で書き直し、似たような提案のチケットにコメントとして追記してくださいました。コミッタを交えたパーティーでは、この仕様についていろいろと議論が盛り上がりました。

ビルドしたRubyでのGemのテスト

開発中の Ruby で、任意の gem のもつテストを行うことができる、という仕組みの提案です。私がほしーなー、と言っていたら、作ってくださいました。

最近の Ruby には bundled gem という仕組みで、いくつかの Gem をインストール時に同時にインストールする仕組みがあるのですが、それらの Gem のテストを簡単に行う方法がありませんでした。また、人気の Gem(例えば、Active Support とか)も、同様に試すには、一度インストールして、bundleして、... といくつかの手順を必要としていました。この提案では、これらのテストを、Ruby をインストールせずに makeコマンド一発でできるようになります。MRI 開発者が(人気の)Gem を動かせなくなるような変更を入れる前に気づくことができるように(多分)なります。

サブイベント

二日目の途中に、Ruby 開発者会議で来ている Ruby コミッタに頼んで、下記のイベントを行いました。

まつもとさんゆきひろさんによる特別講演

f:id:koichi-sasada:20170929111037j:plain

大雑把に「30分でいい話をしてください」と依頼したら、いい話をしてくださいました。話の詳細は、池澤さんのレポート記事( Rubyのなかを覗いてみよう!「Cookpad Ruby Hack Challenge」に参加してみた )をご参照ください。

昼時だったので、発表を行ってもらった場所に隣接するキッチンで、社員の昼食を作っていました。料理しているところで発表するのは、多数の発表経験のあるまつもとさんでもさすがに初めての経験だったとのこと。

Ruby開発者との Q&A

f:id:koichi-sasada:20170929111044j:plain

Ruby 開発者を並べて、参加者および弊社社員を含めた質疑応答大会を行いました。RubyKaigi での企画 Ruby Committers vs the World の前哨戦でした。

パーティー

f:id:koichi-sasada:20170929111051j:plain

最後に、開発を終えた参加者の皆さんと、開発者会議を終えた Ruby コミッタが合流し、パーティーを行いました。

パーティーでは、二日目に行った挑戦を発表してもらいました。その場で、まつもとさんをはじめ Ruby 開発者と本気のディスカッションが発生していました。

まとめ

本記事では、2017/08/30, 31 に弊社で開催した Cookpad Ruby Hack Challenge (RHC) の模様についてご紹介しました。

参加者の皆様へのアンケートからは、良かったという感想を多く頂きました。 ただし、いくつか反省する点があり、次回以降で改善していきたいと思っています。

今回は(多分)成功したので、今後も続けて行ければと思っています。 初回だったので、まつもとさんに特別講演をお願いするなど、だいぶ力を入れてしまいました。 次回以降は、もうちょっと力を抜いていこうと思います。

参加したかったけど、定員オーバーで参加出来なかった方、そして、今回知って、興味を持たれた方、次回以降にぜひご期待ください。 調整次第ですが、出張して行うことも可能かと思います。 また、Ruby 以外にも広げられるといいですね。夢(だけ)は広がります。

本イベント開催にあたり、Ruby コミッタや、多くの弊社社員に助けて頂きました。 この場を借りて、御礼申し上げます。

最後にご案内です。フォローアップイベントとして、RHC もくもく会を弊社にて開催します(2017/10/11 (水) 18:30-、申し込みは Ruby hack Challenge もくもく会)。Ruby コミッタとして遠藤と笹田がサポートします。 RHC 参加者に限らず、Ruby のハックに興味のある方がいらっしゃいましたら、ぜひご参加ください。

料理の追体験を実現する「タイムライン」のデザイン

$
0
0

こんにちは、サービス開発部のデザイナー若月(id:puzzeljp)です。

すでにご利用していただいている方もいらっしゃると思いますが、iOS / Android アプリにタイムラインという機能が登場しました。

f:id:puzzeljp:20170929172254p:plain

先日そのタイムラインのデザインについての登壇しました。 (イベントレポートはこちら)
今回はその時話しきれなかったこと、タイムラインの開発時のデザインの工夫や苦労についてご紹介します。

タイムラインとは

フォローしているユーザーさんやすべてのユーザーさんの新しいレシピ投稿やつくれぽが見られるようになりました。 レシピ検索では出会えなかった料理に出会うことができ、実際にレシピが見られるので料理をすることができます。

どんな使い方があるかと言うと例えば「Aさんがパエリアを作っている!私作ったことないけど、Aさんが作っているなら私でも作れそう。作ってみよ!」のような料理の追体験ができるようになります。

f:id:puzzeljp:20170929172351p:plain

新しい見え方

タイムラインを開くと、「レシピのカード」と「つくれぽのカード」があります。検索結果と比べて料理の写真を大きく見やすく表示しています。ユーザーさんのアイコンや名前がカードに表示されているので、誰がどんな料理をしているかわかるようになりました。

f:id:puzzeljp:20170929172408p:plain

登壇資料

当日の登壇資料については、以下で見ることができます。

デザインアプローチ〜工夫と苦労〜

つくれぽカードのデザイン

タイムラインでは、「つくれぽのカード」で料理の追体験ができるような様々な工夫をしています。

工夫した点

つくれぽとレシピ投稿を比較すると、つくれぽは気軽に投稿できます。そのため、タイムラインを見ると「つくれぽのカード」の方が多く存在します。もう1つに、つくれぽは作った直後に送るため、今日何の料理を作ったこともわかるようになります。 そのため、レシピ投稿と違い「誰のレシピを作ったのか」「何のレシピを作ったのか」が必要となります。

「誰のレシピを作ったのか」「何のレシピを作ったのか」がタイムライン上でより伝わる物は何かをWebプロトタイピングで検証をしました。Webプロトタイピングのメリットして、以下があげられます。

  1. 実データを利用できること
  2. アプリよりもより高速に検証できること
  3. デザインプロトタイピングよりも正確に検証できること

実際に検討したレイアウトについてご説明します。

A案
作者名やレシピ名を同じ文章として扱いました。文字の大きさは同じですが、色はそれぞれ分けています。1文として見えるので文章としては見やすくしました。
B案
A案と似ていますが、レシピ名を目立たせるために、作者名を小さくすることでバランスを取りました。1文というのは同じですが、文章内で優先度がつきました。

f:id:puzzeljp:20170929172428p:plain

他にもレイアウトを考えましたが、最終的には以下のレイアウトになりました。
理由としては、タイムラインでカードが並んでいる場所に、文章があっても読まないのでは?という仮説がありました。それを解決するために、写真の上に「誰のレシピなのか」、写真の中に「何のレシピなのか」というレイアウトにしました。このレイアウトにしたことにより、適度な文章量と写真も目立つように、カードの高さも少なくなりました。

f:id:puzzeljp:20170929172450p:plain

苦労した点

今回のWebプロトタイピングは、メリットを活かした検証ができました。 問題点として、細かいデザインはアプリと異なるため、実際のアプリに実装してみたらイメージと異なったことやWebプロトタイピングも実装の時間がかかり、アプリの実装にも時間がかかってしまうことでしたが、デザインのプロトタイピングよりは検証は正確にできる点はやはりメリットだと感じました。

誰が作ったがわかる機能

フォローユーザーさんが既につくれぽを送ったレシピに対して、自分自身がつくれぽを送ると、送ったつくれぽが、フォローユーザーさんの「つくれぽのカード」に表示されるようになります。他のフォローユーザーさん同士でも同じレシピにつくれぽを送っていても「つくれぽのカード」に表示されます。

工夫した点

表示される内容は、「誰が作ったかわかる見出し」、それぞれのアイコン・名前とつくれぽの写真です。 例えば見出しには、「○○さんと△△さん他n人が作りました」や「○○さんがn回リピートしています」等と表示され、「誰が作っているか」「誰が何回作っているのか」「自分自身が何回作ってるか」がわかるような文言を20パターンほど用意しています。パターンが多いことで、より正確に「誰が作っているか」「誰が何回作っているのか」「自分自身が何回作ってるか」がわかるようになりました。

f:id:puzzeljp:20170929172510p:plain

苦労した点

見出しの部分が一番苦労しました。まず起こりうる組み合わせを考え、その後実際に表示される文言を考えましたが、「カード投稿者が他人の場合は他人をテキスト内に含めない」や「テキストがカード投稿者が自分の場合と異なる」などのカードの見た目は共通しているものの表示される文言が違うと複雑になってしまいました。 複雑になってしまったことで、テストケースを回した時に、その起こりうる組み合わせを出すことが難しく、専用のアカウントを作成し、ログインし確認を行う作業を20回以上しました。
もし次回こういった文言を考える時には、ユーザーさんに最低限伝わる文章を考えようと思いました。ユーザーさんにわかりやすいものをと思いましたが、開発が遅くなってしまいユーザーさんに届くのが遅くなってしまうよりも早くリリースを行い、検証をしたほうが良いためです。

まとめ

タイムラインは、時間をかけて開発を行ってきました。新しい機能のため、実装に時間がかかるのはもちろんですが、デザイン的にも様々な工夫を行ったためです。 そんなにデザインを工夫する必要があるの?…といった部分があるかもしれませんが、「タイムラインを見て料理がしたくなる」「タイムラインを見ていて、料理をしたくなったらレシピを見たら料理ができる。」そんな体験が自然とできるようにと開発を行いました。
タイムラインがリリースされたことによって、みんながどんな料理をしていることがわかるようになりました。 気になった作者さんをフォローをすると、よりタイムラインが楽しく、料理がしたくなるようになると思います。ぜひタイムラインを使ってみてください!


Ruby の脆弱性を見つけた話

$
0
0

こんにちは、技術部の遠藤(@mametter)です。フルタイム Ruby コミッタとして、クックパッドにあたらしく入社しました。よろしくお願いします。

最近、Ruby や RubyGems の脆弱性を発見して、その結果セキュリティリリースにつながるということを経験しました。どういう動機でどのように脆弱性を発見したか、どのように通報したか、などについてまとめてみます。Ruby の脆弱性を見つけたけどどうしよう、という人の参考になれば幸いです。

HackerOne について

HackerOneという脆弱性情報の通報と公開のためのプラットフォームをご存知でしょうか。

OSS にとって脆弱性情報の管理は面倒なものです。脆弱性の通報を秘密裏に受け付け、関係者だけで議論しなければなりません。そのため、通常のバグトラッカとは別のコミュニケーションチャンネルを用意する必要があります。

そこで HackerOne が使えます。HackerOne は簡単に言えば、脆弱性情報の管理に特化した非公開のバグトラッカサービスです。登録されたOSSプロジェクトに対して誰でも脆弱性情報を通報できます。また、プロジェクトメンバ間や報告者の間で非公開の議論もできます。問題が解決された際には議論の内容が公開されます。

さらに、Internet Bug Bounty (IBB) programがインターネットを維持するために特に重要なソフトウェアと指定している一部のプロジェクトについては、通報されたバグが開発者によって脆弱性と認定された場合、IBB から報告者に報奨金が支払われます。

ただ、報奨金が出るのは良し悪しです。良い通報をしてくれた人が報われるのは当然良いことなのですが、報奨金目当ての雑な指摘がたくさん来るという副作用があります。完全に見当違いな例を上げると、「SVN が公開状態だぞ!」とか、「バグトラッカの issue 一覧が丸見えだぞ!」とか 1。もちろん有益な通報も来るのですが、通報を受ける側としては、もうちょっとノイズが減るといいなあ、と思っています。

そこで、 Ruby ユーザの方々に HackerOne を紹介したいと思い、そのために一回、私自身が通報者としてのプロセスを経験してみました。

ターゲットの選定

自分が一番慣れている OSS プロジェクトは Ruby なので、Ruby のソースコードから脆弱性を探すことにしました 2。Ruby に標準添付されたライブラリの中で、「脆弱性といえば WEBrick」。という直観にもとづき、そのへんをターゲットにしました。

探す脆弱性の選定

「Ruby の脆弱性」に明確な定義はありません。ある Rails アプリに任意コード実行(外部から攻撃コードを送り込んで実行させられる)があれば、どこかに脆弱性があることは確かですが、Ruby の脆弱性かもしれないし、Rails(またはサードパーティ)の脆弱性かもしれないし、はたまたユーザの書いたプログラムの脆弱性かもしれません。極端な例では、system("ls " + user_input)みたいなプログラムがあると OS コマンドインジェクションができますが、これを Ruby の systemのせいだと言われても困ります。Ruby 本体かユーザプログラムかの切り分けは、わりと揉めやすいところです。

今回はここで揉めないよう、言い逃れしにくい脆弱性を探すことにしました。それは、そのプロジェクト自身が過去に脆弱性と認めたバグに近いバグを見つけることです。

WEBrick の過去の脆弱性を探したら、『WEBrick にエスケープシーケンス挿入の脆弱性』が見つかりました。要するに、ログにエスケープシーケンスを紛れ込ませることができたら脆弱性のようです。個人的には「このくらいで脆弱性なんだ」という驚きがありますが、一部のターミナルエミュレータはエスケープシーケンスで危うい挙動を起こせることがあるそうです。詳しくはリンク先を読んで下さい。

脆弱性の発見

実際に脆弱性を探します。過去の脆弱性の修正コミットを手がかりに WEBrick のログ出力まわりを読解すると、WEBrick::AccessLog.escapeというメソッドでエスケープシーケンスを除去(サニタイズ)し、WEBrick::BasicLog#error#warnなどのメソッドで実際にログを書き出すらしいことがわかります。ここで、AccessLog.escapeWEBrick::HTTPStatus::Status#initializeの中でしか呼ばれていないことに気づきました。つまり、この例外経由でしかサニタイズがされないらしいということです。

そこで、#error#warnを直接呼び出すところを探したところ、WEBrick::HTTPAuth::BasicAuth#initializeに見つかりました。不正なユーザ ID で BASIC 認証すると、そのユーザ ID がサニタイズなしでログに流れ出るようです。

(あっさり見つけたように書いてますが、実際にはいろいろ探したり試行錯誤したりしながらだったので 2 晩くらいはかかったと思います)

脆弱性の確認

この脆弱性が実際に exploit 可能であることを確かめます。WEBrick の BASIC 認証のコードを Web 検索しながら書きます。

require "webrick"
require "webrick/httpauth"

srv = WEBrick::HTTPServer.new({ Port: 34567 })
db = WEBrick::HTTPAuth::Htpasswd.new("dot.htpasswd")
authenticator = WEBrick::HTTPAuth::BasicAuth.new(UserDB: db, Realm: "realm")
srv.mount_proc("/") do |req, res|
  authenticator.authenticate(req, res)
    res.body = "foobar"
  end
srv.start

↓サーバを起動した様子 f:id:ku-ma-me:20171004180721p:plain

このサーバに対して、エスケープシーケンスを混入した不正なユーザ ID でログインを試みます。ここでは、"\e]2;BOOM!\a"というエスケープシーケンスで実験しました。これは、端末のタイトルを BOOM!という文字列に変える命令です。

require "open-uri"

open("http://localhost:34567/login",
  http_basic_authentication: [
  "ESCAPE SEQUENCE HERE->\e]2;BOOM!\a<-SEE WINDOW TITLE",
  "passwd"
]).read

↓クライアントを起動する様子 f:id:ku-ma-me:20171004180728p:plain

この結果、WEBrick サーバを動かしている端末のタイトルが、BOOM!に変わることが確認できました。

↓攻撃成功した様子(タイトルバーが "BOOM!"になっているところがポイント) f:id:ku-ma-me:20171004180738p:plain

脆弱性の報告

めでたく(?)脆弱性を確認できたので、HackerOne に投稿します。Weakness や Severity は該当すると思うものを選ぶだけですが、よくわからなかったら空欄でもよさそうです。重要なのは Proof of Concept です。といっても、普通のバグ報告と同じです。どういう問題であるかと、再現手順をきっちり書けば十分でしょう。問題の重大さを書くとさらに親切です。今回の脆弱性は過去の脆弱性の修正漏れなので重大さに議論の余地はないと考え、ほとんど再現手順だけを簡単に書きました

あとは普通のバグ報告と同じ対応です。よほど致命的な問題でない限り(あるいは致命的な問題であっても)、開発者はなかなか返事をしてくれないものです。パッチを書いて送ったり、ときどき催促したりしながら、気長に待ちます。今回は、4 月に報告して、セキュリティリリースは 9 月でした。

セキュリティリリース

普通の報告者ならここで終わりですが、今回は私が Ruby コミッタでもあるということで、セキュリティリリースに少しだけ参加しました。といっても私がやったのは、ブランチマネージャや公式サイト管理人たち(@unakさん、@nagachikaさん、@hsbtさん)の指示の下、私が書いたパッチをコミットしただけです。あとは彼らが一生懸命 tar ball を作ってリリースするのを応援していました。

コミットしてからリリースアナウンスを出すまでの時間を最小化するため、リアルタイムのコミュニケーションを取りながら進める必要があります。Ruby のブランチマネージャたちは、セキュリティリリースのたびに命を燃やして頑張っています。敬礼。

報奨金の獲得

無事セキュリティリリースがなされたということで、IBB から報奨金として $500 をいただきました。このプロセスも簡単に説明しておきます。

まず、初めて報奨金をもらう場合、税務上の書類 W-8BENを作成して提出します 3。すべてオンラインの手続きなので難しいことはありませんでした。

それから支払いの受取方法を登録します。PayPal 、Bitcoin via Coinbase 、銀行間振替がサポートされていました。私は銀行間振替を選んだので、口座情報を入力するだけでした。4

RubyGems の脆弱性

同じようなプロセスで、RubyGems にも通報をしました。

詳細は割愛しますが、CVE-2015-3900という過去の脆弱性が適切に修正されていないというものでした。ただ、こちらはすでに他の人が通報済みだったので、Duplicate でクローズされました。

ただ、コードを読んでいるうちに次の 3 つの問題を新規発見しました。こちらの方の通報は認められたようです。

これらの通報に対する修正は、RubyGems 2.6.13としてリリースされています。特に 3 つめの問題は、WEBrick の問題よりもう少し重大だと思うので、バージョンアップすることをおすすめします。なお、Ruby 2.4.2 は RubyGems 2.6.13 の修正を含んでいるので、Ruby 2.4.2 にするのでも大丈夫です。

まとめと所感

Ruby の脆弱性を探して HackerOne に通報した事例を紹介しました。

セキュリティ報告をすると、多くの場合、公式サイトでクレジットに載せてもらえるので、承認欲求が満たされますし、HackerOne ならちょっとした報奨金までもらえます 5

最初に触れたとおり、今のところ HackerOne 経由で Ruby にくる通報は、雑な通報が多くて Ruby 開発者的にはノイズが多いと感じられています。この記事を見た人が、(Ruby に限らず)有意義な通報を増やしてくれるといいなと思います。

最後になりましたが、クックパッドでは脆弱性のない Rails アプリを作れる Web アプリケーションエンジニアを募集しています。詳しくは募集要項ページをご覧ください。


  1. Ruby はオープンソースプロジェクトなので、もちろん意図的に公開しています。

  2. IBB の FAQによると、プロジェクトの開発者自身でも、(1) そのプロジェクトで収入を得ていないこと、(2) 問題のコミットに関わった人間でないこと、の条件を満たせば報奨金がもらえます。私はフルタイムコミッタになったので、もう無資格のようですが、今回の通報は入社前にやりました。

  3. 米国非居住者が米国の人から支払いを受け取るときに、源泉徴収の金額を低減してもらうための書類。

  4. 正確には、海外からの送金を受け取るために、銀行にマイナンバーの登録をする手続きもありました。

  5. エスケープシーケンスインジェクションでは大した金額にはなりませんでしたが、もっと重大な脆弱性ならそれなりに高額になるはずです。たとえば有名な Shellshock だと $20,000も支払われたそうです。

Cookpad Tech Kitchen #9 〜1行のログの向こう側〜 を開催しました!

$
0
0

こんにちは! @yoshioriです。

2017/07/26、技術系イベント「Cookpad Tech Kitchen #9 〜1行のログの向こう側〜Cookpad Tech Kitchen #9」を開催しました。 (はい。僕が記事公開するの忘れててだいぶ遅くなっちゃいました>< ごめんなさい)

クックパッドの技術的な知見を定期的にアウトプットすることを目的とする本イベント。第9回目となる今回は「ログの活用方法」をテーマに開催しました。(月に1回程度開催しています)

月間6000万ユーザが使っているクックパッドには大量のログが集まってきます。そのログを効果的に活用してサービスやユーザに還元するための取り組みについて、インフラ、広告事業、サービス開発それぞれの視点で知見の発表を行いました。

発表資料を交えてイベントのレポートをしたいと思います。

f:id:cookpadtech:20171003220443p:plainイベントページ:

Cookpad Tech Kitchen #9 〜1行のログの向こう側〜 - connpass

発表内容

「クックパッドのログをいい感じにしているアーキテクチャ」

一人目の発表者であるインフラストラクチャー部部長の星(@kani_b)は、SRE としてAWSやセキュリティ関連でのサービスインフラ改善に携わっています。

今回は AWS Summit Tokyo 2016 Developer Conference で発表した内容の続きとして発表を行いました。 具体的な数字として Fluentd に流れているログベースで言うとデータ総量は 400 〜 600GB / 日、レコード数は 8 億レコード以上 / 日(秒間 8,000 〜 25,000 レコードくらい)という規模のデータを扱っています。これをどのように集めて処理しているのかを紹介しました。

資料

「広告ログのリアルタイム集計とその活用」

二人目の発表者であるマーケティングプロダクト開発部の渡辺(@wata_dev)は、主に配信基盤の改善やマーケティングプロダクト開発部で開発しているサービスの基盤周りのサポートを行っています。

クックパッドでは広告の配信も自社で行っており、そのためにどのようにログを活用しているのかを、過去どういった問題点があってそれをどのように解決していったか、異常検知だけではなく配信制御や在庫予測などなど広告配信というドメインで実際に必要になるケースを出しながら紹介しました。

資料

「ログを活用したサービス開発」

3人目の発表者であるサービス開発部の外村(@hokaccha)は、バックエンドからWebフロントエンド、モバイルアプリの開発など幅広い分野でCookpadのサービス開発に携わっています。

発表ではモバイルアプリのロギングのやりかたから始まり、実際のログの活用方法としてサービス開発側でログをどのように扱っているか、どう活かしているかを紹介しました。 行動分析としてログの設計、ログの分析やその可視化によってサービス改善の意思決定に使われていること、ログを利用した機能開発として調理予測という実際にログがサービスとして使われている事例を紹介しました。

資料

「ログ」をテーマにしたご飯も登場!

クックパッドのイベントではご来場の感謝を込めて、会場で手作りしたご飯でおもてなしをします(食べながら飲みながら発表を聞いていただくスタイル)。今回はテーマである「ログ」にちなんだメニューを用意してもらいました。

f:id:cookpadtech:20171003220752j:plain

こちらはメッセージ入りのライスケーキ。クックパッドのインフラエンジニアが大切にしている言葉に「1行のログの向こうには1人のユーザがいる」というものがあります。画面で見るとたった1行のログだけど、その向こうには大切にすべき1人のユーザがいる、ということを思い出させてくれる合言葉です!

f:id:cookpadtech:20171003221913j:plain

川嶋シェフの粋な心意気で、素敵なメッセージもテーブルに並びました。

f:id:cookpadtech:20171003221721j:plain

こちらはログ=丸太をイメージした湯葉のデザートです。中に入っているのは甘すぎないところがおいしいあんこです。

まとめ

いかがでしたか。クックパッドでは毎月テーマを変えて技術イベントを開催しています。ご興味のある方は是非ご応募ください。

cookpad.connpass.com

新しい仲間を募集中

■ 日本最大の食のビッグデータを扱う「データ基盤」の開発に興味がある方 https://info.cookpad.com/careers/jobs/careers/data-infra-engineer

■ 広告事業の「Webアプリケーション開発」に興味がある方 https://info.cookpad.com/careers/jobs/careers/marketing-product-engineer

■ クックパッドアプリの「Webアプリケーション開発」に興味がある方 https://info.cookpad.com/careers/jobs/careers/software-engineer

クックパッドのデータ活用基盤

$
0
0

インフラ部 & 技術部の青木峰郎です。 クックパッドでは全社的にAmazon Redshiftを中心としたデータ活用基盤を構築しています。 今日はその全体像についてお話ししたいと思います。

データ活用基盤の全体像

まず、以下にクックパッドのデータ活用基盤の全体像を示します。

f:id:mineroaoki:20171005230822p:plain

大きく分けると入力が2系統、内部処理が1系統、出力が3系統あります。 入力はMySQLからのインポートとログのロードがあり、どちらも独自に構築したシステムで行われています。 DB内部のデータ処理はSQLバッチのみです。 そして出力は管理画面やBIツールからのアクセスとバッチ処理によるエクスポートに大別できます。

以下1つずつ説明していきましょう。

入力その1: MySQLインポートシステム

MySQLからRedshiftへのマスターテーブル取り込みにも独自のインポートシステムを使っています。 このインポート処理には、つい最近まではごく普通のバッチジョブを使っていたのですが、 現在は独自開発の専用システム(pipelined-migrator)に乗り換えつつあります。

専用システムを作った理由は、インポートするテーブルの追加を誰でも簡単にできるようにするためです。 pipelined-migratorにはウェブベースの管理画面が付いており、 この画面からボタン1つでインポートするテーブルの追加や削除が行えます。 またインポート状況などを確認することもできます。

バッチとpipelined-migratorのいずれにしても、 MySQLからテーブルを取り込む方法としてはごく単純な全行ダンプ・全行ロードのみを実装しています。 分析システムの構築当初はbinlogを使った差分更新も検討したのですが、運用が面倒すぎることと、 「全行ロードでも間に合うから」という消極的な理由によってこの実装になりました。 将来的にパフォーマンスが間に合わないなどの理由があれば差分更新にするかもしれません。

入力その2: ログをロードするStreaming Loadシステム

ログのロードには自前で開発した bricolage-streaming-loaderbricolage-streaming-preprocessorを使っています。 loaderはRuby製でpreprocessorはJava製です。

このシステムは、一言で言うと、fluentdからS3に書き込んだJSONファイルを前処理しながらロードするシステムです。 またRedshiftはコミットの遅延が比較的大きいため、そこを軽減するためにバッファリングも行っています。 このシステムの設計方針については本ブログの過去の記事 「Amazon Redshiftへ継続的にデータをロードする際に気をつけること」で詳しく説明しているので、そちらをごらんください。

このStreaming Loadシステムには専用の管理画面が用意されており、 ログが処理されていく様子を1オブジェクトずつ丁寧に見守ることができます。

入力その3: Redshift Spectrum向けロードシステム(未リリース)

さきほどの図には存在しませんでしたが、 RedshiftからS3のデータをアクセスできる「Redshift Spectrum」への対応も計画しています。 Spectrumはまだ東京リージョンに来ていないので計画に留めていますが、来た瞬間に稼働させるつもりです。

Spectrumを使う目的は、第1にログのロードのレイテンシを短縮すること、第2にディスク節約です。 特に、巨大なわりにあまりアクセスのない過去ログなどは、Spectrum(S3)に逃してやると、 Redshiftの高速性とS3の安価なストレージをいいとこ取りできるので大変いい選択肢だと思います。

データの加工: SQLバッチ

いったんRedshiftにデータを取り込んだら、あとは原則としてすべての処理をSQLで記述します。 このSQLによるバッチ処理はBricolageというSQLバッチフレームワークと、 ジョブ管理システムKuroko2の組み合わせで構築しました。

この2つについては過去にだいぶいろいろ書きましたので、 Bricolageについては 「巨大なバッチを分割して構成する 〜SQLバッチフレームワークBricolage〜」を、 Kuroko2については 「クックパッドのジョブ管理システム kuroko2 の紹介」を、それぞれごらんください。

Redshift内のデータアーキテクチャ

SQLバッチは全体で一番地味ですが、最も重要な部分でもあります。 データ分析基盤と言うとデータを取り込むところばかりが注目されがちですが、 データ取り込みは本番前の準備にすぎません。 その後ろに連なるデータの統合と分析こそがデータ「活用」の本丸です。

クックパッドではRedshift内を論理的に3層に区切り、 1つめを入力層、2つめを論理DWH層、3つめを論理データマート層としています。

入力層は名前の通り、他のデータソースからデータを入力する層です。 基本的に送られてきたデータが元のままの形でそのまま入っています。

論理DWH層は、いわゆるデータウェアハウス(Data WareHouse)です。 入力層のデータをクレンジングし、複数システムのデータを統合し、場合によっては集計もして、 全社の分析基盤となるデータを作ります。

……と書くとかっこいいですが、まだこの層はあまり成長させられていません。 まだここまで手が回っていないというのが正直なところで、今年末から来年にかけての最大の課題だと考えています。

最後の論理データマート層は特定のデータ分野ごとの特化領域です。 例えばクックパッドの場合だと、レシピ検索、広告、有料会員、レシピサービス、などの分野が存在します。

またこの層は対応する部がはっきり決まるので、その部に全権を委任しています。 逆に言うと、入力層とDWH層はインフラ部が管理しており、 他の部のメンバーが何か変更したい場合はpull requestが必須ということです。

これらの主要領域以外に、それぞれのメンバーは自分専用のスキーマを持つことができ、 その中では自分の自由にデータを加工・保存することができます。いわゆるサンドボックスです。 エンジニアはもちろん、ディレクターやプランナーも場合によっては自分のスキーマを持ち、 自分でSQLを書いて分析することがあります。 最近の社内におけるSQL熱の高まりを受けて、先日はインフラ部メンバーによる社内向けSQL講座も行われました。

出力その1, 2: BIツールと管理アプリからの参照

データベースへの入力と加工について話したので、残るは出力系です。 まずBIツールと管理アプリ(社内用ウェブアプリ)について話しましょう。

BIツールと管理アプリのアクセスは傾向が似ており、 ごく少量のメタデータ読み書きと、大量の統計データ読み込みが発生します。 前者はO/Rマッパーによる構造化されたアクセス、 後者は直SQLとカーソルを使ったアクセスが主になるでしょう。

Redshiftにおけるカーソルの特徴と使いかたについては過去の記事 「ActiveRecordを使ってRedshiftから大量のデータを効率的に読み出す」を参照してください。

なお、BIツールとしては現在のところ、社内で動かしているRedashをメインに使っています。 しかし正直なところRedashはキューまわりのできが悪すぎて、 アドホックな社内ローカルパッチを大量に当ててなんとか回しているような状況です。 いま真剣に移行を検討しています。

移行先の第一候補はなんだかんだでTableauでしょうか。 Tableauは以前から細々と使ってはいたのですが、 ついにTableau ServerのLinux版が出そうなのでいよいよ本格導入の時かもしれません。

ところで、RedashやTableauは共有ダッシュボードを作るために使われることが多いですが、 それ以外に個人単位のアドホックな分析も多く行われます。 そのような目的には、Posticoのような普通のPostgreSQLクライアントや Jupyter、 それに弊社社員の外村が開発したbdashなどを使っています。 bdashは手軽にグラフが書けることと、過去のクエリーを記録しておける点が非常に便利で、 個人的にも気に入っています。

出力その3: バッチからの参照

3つめの出力系統は、主に他システムへのエクスポートを目的とした、バッチからの参照です。 以前はこの処理のためには単純にRedshiftへ接続してselectさせていたのですが、 最近はQueueryというHTTP APIシステムを挟むようにしています。

Queueryは、APIでselect文を受け付けて結果をS3にUNLOADし、そのURLを返すだけの単純なシステムです。 このシステムを作った一番の理由は、バッチからの読み込み方法をRedshiftのUNLOADだけに限定したかったという点です。

Redshiftのカーソルはleader nodeにデータをマテリアライズするうえに、カーソルがクローズされるまでコネクションを占有しつづけます。 いずれの特徴もleader nodeにかなり負荷をかけることになります。 そこを考えると、長時間に渡って大量のデータを転送する可能性のあるバッチアクセスは、ぜひともUNLOADにしておきたいのです。 UNLOADはcompute nodeからS3へ直接に、並列でデータを転送するので安心です。

また特に、Redshiftで作ったサマリーをMySQLへ単純転送する用途のためには、 redshift-connectorというRubyのライブラリ(gem)を用意して対応しました。 むろん、このredshift-connectorも抜かりなくQueueryに対応しています。

データベースのドキュメント: dmemo

さて、ここまでで、データを入れて、きれいにして、サマリーも作り、 他のシステムから参照・利用できるようになりました。ではそれで終わりでしょうか?

当然、違います。データを作ったら、それを使えるように説明する必要があります。 ようするにドキュメントがいるのです。

データのドキュメントのためには、これまた弊社社員の小室が開発したdmemoを使っています。 これにも過去記事があるので、詳しくは下記の記事をごらんください。

まとめ

今回はクックパッドのデータ活用基盤について、その全体像をお話ししました。 これまでそれぞれの部分について書いたり話したことはたくさんあったのですが、 よくよく考えてみると全体を説明したことがなかったので、この機会にまとめてご紹介しました。 過去に書きためてきたブログ資産も生かすことができて一石二鳥ですね! ネタが思い付かないときはまたこの手で行こうと思います。

[宣伝] 『ふつうのLinuxプログラミング』の第2版が出ました

www.amazon.co.jp

わたしが12年前(!)に書いた書籍『ふつうのLinuxプログラミング』の第2版が出版されました。 第2版には、各方面からリクエストされまくっていたKindle版もついに出ています。 よっしゃ、いっちょ久しぶりにLinuxでCでも書いたるか! などという(わりと珍しい)機会がありましたらぜひご活用ください。

しかし、なぜわたしがブログ当番のときに限ってこう毎年毎年新しい本が出るのか…… まったく狙っていないのに、本当に不思議です。

Cookpad TechConf 2018 開催報告

$
0
0

こんにちは、技術広報を担当している外村です。

f:id:hokaccha:20180210115823j:plain

2018年2月10日にエンジニア向けのカンファレンス、Cookpad TechConf 2018を開催しました。当日はたくさんの方に参加いただき、活気あるカンファレンスになりました。ご来場の皆様本当にありがとうざいました。

新しい試みとして、当日の司会をAmazon Pollyの音声合成で行なったのですが、こちらもみなさんにお楽しみいただけたようでした。

講演資料・動画

当日の講演資料および動画を公開いたしましたので是非ご覧になってください。

基調講演: 毎日の料理を楽しみにする挑戦をし続けた20年 by 橋本 健太

コーポレート戦略部本部長の橋本による基調講演でイベントはスタートしました。クックパッドはテックカンパニーとしてどのように成長してきたか、グローバル展開をどのように行ってきたか、現在取り組んでいる新プロジェクトについての話などがありました。

講演資料・動画

クックパッドの "体系的"サービス開発 by 新井 康平

会員事業部の新井の講演は、クックパッドではサービス開発の難しさにどのように立ち向かっているか、という内容です。こちらの講演の捕捉記事を公開していますのでこちらもご覧になってください。

"体系的"に開発サイクルを回して "効果的"に学びを得るには - クックパッド開発者ブログ

講演資料・動画

クックパッドクリエイティブワークフロー by 辻 朝也

会員事業部デザイナの辻の講演は、クックパッドにおけるサービス開発のフローについての話です。やるべき施策を決めてリリースし、分析して評価するという一連のサイクルの中で具体的にどういったことをおこなっているのか、というのがよくまとまっていました。

講演資料・動画

What/How to design test automation for mobile by 松尾 和昭

海外事業部にてサービスの品質向上やテストを担当している松尾の講演は、モバイルテストの自動化についての話です。モバイルのテストで重要なトピックをSPLIT(Scope, Phase, Level, sIze, Type)というキーワードにまとめて解説しました。

講演資料・動画

Rubyの会社でRustを書くということ by 小林 秀和

インフラストラクチャー部の小林の講演は、Rustを使ったプロダクト開発についての話です。CookpadはRubyを使ってサービス開発をすることが多いですが、そういった環境でRustを採用した経緯や、実際にRustを導入したプロダクトで得られた知見を紹介しました。

講演資料・動画

cookpad storeTV 〜クックパッド初のハードウェア開発〜 by 今井 晨介

メディアプロダクト開発部の今井の講演は、cookpad storeTVについての話です。cookpad storeTVはスーパーに設置する料理動画を配信するサイネージで、クックパッドがハードウェアの開発から手がけました。今井はその開発を担当しており、実際に発生した問題や具体的な開発フローについて紹介しました。

講演資料・動画

Challenges for Global Service from a Perspective of SRE by 渡辺 喬之

インフラストラクチャー部SRCグループの渡邉の講演は、クックパッドのグローバルサービスのSREとしてどのような取り組みをしてきたか、という内容です。グローバルサービスならではの課題というのはどういったものがあり、それをどう解決したのかという、あまり他では聞くことが少ない興味深い話でした。

講演資料・動画

動き出したクックパッドのCtoCビジネス by 村本 章憲

Komerco事業部の村本からの講演は、クックパッドの新規事業であるKomercoについての話です。Komercoとはどのようなサービスなのか、どのようなチーム・技術スタック・フローで開発しているかということについて紹介しました。

講演資料・動画

Solve "unsolved" image recognition problems in service applications by 菊田 遥平

研究開発部の菊田の講演は、機械学習による画像分析の取り組みについての話です。機械学習をサービスに活かすうえで難しい問題はどういったところにあるのか、それを実際の業務でどのように解決したか、という内容でした。

講演資料・動画

基調講演: Beyond the Boundaries by 成田 一生

取りをつとめたのはCTOの成田による基調講演でした。クックパッドの技術スタックはどのようなものか、エンジニアの行動指針である「Beyond the Boundaries」とは何なのか、エンジニアが成長できるために具体的にどういった取り組みを行っているか、といった話でイベントを締めました。

講演資料・動画

Lifestyle Product Award授賞式

講演の途中に、昨年開催した2017 Lifestyle Product Award by Cookpadの表彰式をおこないました。今回は優秀賞としてGOKURIが選出されました。

GOKURIは嚥下機能、飲み込みの能力を計測するためのデバイスです。基礎的な研究は数年前から行なわれていたものの、精度が課題となっていました。昨年、深層学習による精度向上によってプロダクトとしてリリースできる水準となり、リハビリ学会などでその成果が発表されました。

感想エントリ

当日参加いただいた方の感想エントリを以下にまとめました。素敵な記事を書いていただき、ありがとうございます。

他にもありましたら@hokacchaまでお知らせください!

まとめ

クックパッドにおけるサービス開発の手法やプロダクト開発の事例、その背景にある技術的なトピックなど、幅広い領域の講演をお届けしました。当日参加いただいた沢山の方に楽しんでいただいたようです。

クックパッドでは引き続き、このようなイベントを開催していきます。ぜひ、楽しみにしていてください!

高速に仮説を検証するために ~A/Bテスト実践~

$
0
0

会員事業部エンジニアの佐藤です。クックパッドでは日々データと向き合い、データを基にした施策作りに関わっています。

Cookpad TechConf 2018で新井が発表した「クックパッドの "体系的"サービス開発」の中で、社内で仮説検証を行う際に使われているツールについて触れている箇所がありました。 本記事ではそのツールと実際の取組み方について、実際の流れを踏まえながらもう少し詳しく説明していきます。

仮説検証

仮説検証は以下のフローで進んでいきます。

  1. 前提条件を確認する
  2. 検証の設計をする
  3. 各パターンの機能を実装する
  4. 各パターンにログを仕込む
  5. デプロイ後の監視
  6. 検証結果の振り返りとネクストアクション

小さく・手戻りなく・高速な検証を行うためには手を動かす前の段階、上記フローにおける1・2のステップが重要となります。

具体例として「朝と夜はプレミアム献立の需要が高まる」という仮説の検証フローを見ていきます。 これは献立プレミアム献立のアクセス分布をみると朝と夜にもアクセスが増加していたことから得た仮説です。

前提条件を確認する

下記の2つの点について合意が取れている必要があります。

  • 確かめたい価値(仮説)が明確化されている
  • その検証にA/Bテストを用いる

今回は話をわかりやすくするため「朝と夜はプレミアム献立の需要が高まる」という仮説が既にたっており、それをA/Bテストで確かめるという流れになります。ですが実際にはそもそも仮説が検証可能な状態にまで明確化されていないといった状況が考えられます。 手段を具体化する前にチームで方針決定・合意形成がなければ検証は始まりません。 ごく当たり前のように感じますが、いつでも振り替えられるよう土台を固めておくことが大事です。

検証の設計をする

仮説を確かめるためにA/Bテストの設計を行います。 まず、仮説を確かめるために何と何を比較するか考えます。 この記事で例題として扱う仮説は「朝と夜はプレミアム献立の需要が高まる」という仮説でした。 前提知識として人気順検索とプレミアム献立では人気順検索の方が需要があり、単純に人気順検索の枠をプレミアム献立に差し替えて比較すると前者が有効であることがわかっているとします。 よって、今回は「普段は人気順検索での訴求に使っていた枠を朝と夜の時間にだけプレミアム献立に切り替える」施策に取り組みます。

  • パターンA: 人気順検索(通常)
  • パターンB: 朝と夜だけプレミアム献立、それ以外の時間帯は人気順検索
パターンAパターンB
f:id:ragi256:20180221172307p:plainf:id:ragi256:20180221172324p:plain

この時、対象も出来る限り明確にしておきます。 今回はサイト内の該当部分を訪れたプレミアム会員以外の全てのユーザーを対象とすることにします。 また、検証の結果がどうなったかによって次にとるアクションまで決めます。

次にA/Bテストで監視・比較をするKPIも設定します。今回はプレミアムサービス会員の転換率(CVR)をKPIとします。 KPIが決定したことで同時に具体的なログの測定箇所と測定内容も決定します。 今回はそれぞれのパターンにおける訪問ユーザー数とプレミアムサービスへの転換数が必要となります。

ここで検証期間の見積もりを行うため、必要となるサンプルサイズを算出しておきます。 サンプルサイズの算出には「A/B両パターンの平均値」と「求める確度」を事前に決めておく必要があります。 言い方を変えると「どれだけの改善を確認したいのか」と「どれだけ偶然を排除したいか」という点です。 統計学では前者を効果量、後者を有意水準と検出力と呼びます。 詳しくは「仮説検証とサンプルサイズの基礎」を御覧ください。 これらを基にしてサンプルサイズを計算し、サンプルサイズと現状のUUから今回の仮説検証に必要とする日数を求めます。

そして検証設計の最後に、検証期間が経過した時点でどういう結果だったらどうするということを決めておきます。 実際に手を動かす前に、最終的な結果を大雑把に場合分けして次の行動を決めておくことが手戻りの防止につながります。

両パターンを実装する

パターンAには従来通りの挙動を、パターンBには時間帯によって枠内表示が変わるように実装をします。 この際、プロトタイプ開発用プラグインである「Chanko」とChankoのA/Bテスト用拡張である「EasyAb」を使うことで下記のように書くことができます。

パターンの制御を行うChanko内部のコントローラー

moduleTimeSlotPsKondateincludeChanko::UnitincludeEasyAb

  split_test.add('default', partial: 'default_view')
  split_test.add('time_slot_ps_kondate', partial: 'time_slot_ps_kondate_view`')

  split_test.log_template('show', 'ab_test.time_slot_ps_kondate.[name].show')
  split_test.log_template('click', 'ab_test.time_slot_ps_kondate.[name].click')

  split_test.define(:card) donext run_default if premium_service_user? # プレミアムサービスユーザーは対象としない
      ab_process.log('show') # 訪問ユーザー数カウント用ログ
      render ab_process.fetch(:partial), time_slot: target_time?
  endend

パターンの差し替えを行うChanko外部のviewファイル(haml)

-# 対象となるviewの書かれているファイル= invoke(:time_slot_ps_kondate, card) do  -# 差し替え部分 
end

これらに加え、パーシャルとして必要となる default_card.hamltime_slot_ps_kondate_card.hamlとCSSを追加すれば実装は完了です。 既存コードとの接点はinvokeメソッドの部分のみであるため、A/Bテストのon/offはごくわずかな変更で制御することができます。 Chankoは既存のコードと切り離された場所に置かれるため、検証の後始末もスムーズに行えます。

このように、ChankoとEasyAbを使うことで必要最低限のコードのみで検証を行えます。

両パターンにログを仕込む

今回はCVRをKPIとして追いかけていく必要があります。 不要なログを大量にとっても仕方がないのでログは必要最低限に留めるべきですが、後になって「あのログをとっておけばよかった」と後悔しても遅いため必要なログに抜け漏れがないよう列挙しておきます。 今回は該当部分のページに訪れた人(show)とプレミアムサービス枠をクリックした人(click)のログを取ります。 実際にCVRを取るには前者だけで十分なのですが、後者のログもとっておくことでCTRを算出できるようになり、クリエイティブに問題があったかどうか振り返るのに役立ちます。

A/Bテストに限らず一時的なログをササッと仕込みたい場合、社内ではKPI管理ツール「Hakari2」を使っています。 Ruby・JavaScript・HTMLそれぞれで利用することができます。

Ruby

Hakari2Logger.post("ab_test.hakari_log.ruby.A", user: user, request: request)

JavaScript

hakari2.post(['ab_test.hakari_log.javascript.A'])

HTML(hamlで書いた場合)

= link_to xx_path, class: 'track_hakari2', data: { hakari2_keywords: 'ab_test.hakari_log.html.A' }

このようにしてクライアント側でセットされたログは共通ログ基盤Figlogを経由し、最終的にDWHチームの管理するRedshift内へ格納されます。 A/Bテストを開始した後、ログの監視や分析を行う時にはこのログを他のデータと組み合わせて利用します。 今回の検証で必要となるCVRはshowのログとプレミアム会員登録のログを結合することで求まります。

デプロイ後の監視

A/Bテスト用の実装をデプロイし、公開した後に実装やログ取得にミスがないか確認をする必要があります。 日次の集計結果などはcookpadの管理用アプリケーション「papa」上のダッシュボードで確認できます。 検証期間後の最終的な検証結果もこちらで確認します。

f:id:ragi256:20180221172442p:plain

検証結果の振り返りとネクストアクション

「仮説検証の設計をする」の段階であらかじめ決めておいた目標サンプルサイズに到達したところで検証を終えます。 その時点で再度ダッシュボードを確認し、今回の施策の結果がどうであったかを結論づけます。

ダッシュボードでは集計値だけでなく、数値をもとにして描かれた確率分布や計測期間中の推移を見ることができます。 これらをみることで有意差がありそうかどうか、特定日時のイベントによる影響がないかどうかを確認します。

確率分布時系列変化
f:id:ragi256:20180221172500p:plainf:id:ragi256:20180221172509p:plain

このステップでは知見を得るための考察や議論を行いますが、よほど想定外の結果にならない限り「検証の設計」で決めた方針に従い次の行動を決定します。 この施策に関しては当初想定していた量の改善が得られなかったため、仮説の正しさを証明する結果が得られませんでした。 この仮説は献立とプレミアム献立のアクセス分布から得た仮説でしたが、その仮説を得る過程をアプローチ方法から見直す必要があります。

まとめ

クックパッドで高速に仮説を検証するために普段行っている作業についてお話しました。 6ステップに分けて説明をしてきましたが、「前提条件の確認」と「検証の設計」までがキチンとこなせていればその後は特に考えることなく実行することができます。 このサイクルを回す作業に慣れていくことで、実際に手を動かす作業よりもサービス改善のためにどうするべきか頭を使う作業へ労力を割くことができるようになります。

また、今回はwebでのA/Bテストの説明をしましたが、iOS/Androidでも同様にA/Bテスト用のツールを利用することで手軽に仮説検証を行うことができます。

クックパッドでは日々このように各種ツールを利用してサービス改善のサイクルを高速にまわしています。

Viewing all 726 articles
Browse latest View live