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

サービス開発でぶつかってきた壁と、そのとき助けてくれた本

$
0
0

こんにちは、開発ディレクターの五味です。クックパッドにレシピを投稿してくれるユーザーのための機能やサービスを開発する「投稿開発部」に在籍しております。

投稿開発部は、2018年1月に前身となる部からメンバーを一新して発足した部署です。自分たちで1から戦略を作るため、強い実感を持ってユーザーを理解することを信条に、資料を読んだり前任者に聞いたりするだけではなく、実際にユーザーとたくさん話し、たくさんレシピを投稿し、ユーザーのことをたくさん考えてきました。

この記事では、その中でぶつかった課題を解決するために取り入れた書籍や、それをうまく業務に取り入れるために行っている工夫を紹介します。

サービス開発にはさまざまな壁が現れる

ユーザーと事業目標に真摯に向き合うほど、サービス開発にはたくさんの壁が現れます。私たちも例外ではなく、部の発足以降、以下のような壁に激突してきました。

  1. 「ユーザー課題の見極め難しい!」の壁
  2. 「戦略づくり難しい!」の壁
  3. 「良いソリューションアイデアが出ない!」の壁

f:id:natsuki53:20190711170248j:plain
開発の歩みとぶつかった壁の所在

壁にぶつかったら学習チャンス

投稿開発部には、日常的に書籍を読んで、仕事に取り入れようとする文化があります。特に壁にぶつかった時は、ブレイクスルーを図るため、チームで意識的に本を読んだりします。

この1年半で、激突した壁ごとにお世話になった本をご紹介します。

1. 「ユーザー課題の見極め難しい!」の壁

部の発足後、早々に苦悩したのが「レシピ投稿ユーザーの本質的な課題は何か」という問いでした。

クックパッドには、レシピを検索するユーザーと投稿するユーザーがいますが、それぞれ数や志向が大きく異なります。投稿ユーザーのための開発をするなら、彼らのことを誰よりもわかっているべきです。「投稿ユーザーはなぜレシピを投稿してくれるのか?」という命題に、自分たちが心底信じられる答えを得たい、でもどうすれば良いのだろう...というのが最初の壁でした。

そしてその時は、以下の本からとっかかりを得ました。

「ジョブ理論」

https://www.amazon.co.jp/dp/B0746JCN8B/

人が何かプロダクトを使う時、必ずその人は何らかの解決したい「ジョブ」を持っており、その解決のためにプロダクトを「雇用」している、という見地に立って、顧客の「ジョブの解決」に寄り添うプロダクト開発を論じた本。「ジョブ」はいわゆる「課題」や「インサイト」と似た意味だが、顧客の置かれている「状況」により注目し、特定状況下で発生する実用的な欲求に目を向けている。

どう活用したか?

  • ユーザーにレシピ投稿を雇用させている「ジョブ」は何なのか、読み解くことにしました
  • 具体的には、さまざまなレシピ投稿者を呼んで根掘り葉掘り話を聞き出し、あとから彼らの「ジョブ」を推察するインタビューを実施しました

やってみてどうだったか?

ユーザーの抱える「ジョブ」の解決をサービス発想の起点にすることは本質的だと感じます。これだ!という「ジョブ」を発見できると強いコンセプトが作れる、という実感が、取り組むうちに芽生えてきました。

ただし「ジョブ」は、ユーザーの発言内容だけでなく、発話時の表情やその人の価値観、普段の生活の様子など、さまざまな情報を複合的に組み合わせて考えないと推測できないので、慣れるまでは考えるのが難しいです。私たちは、インタビュー後に観察した情報を参加者全員でぶちまけ、それを見ながら「このユーザーのジョブは何だったか」を議論して定義する手法で乗り切りました。インタビューの内容は、本に出てくる、ユーザーの発話からキーワードを捉えて深堀りしていく様子を参考にし構成しています。

また、「ジョブ」の定義を何度か重ねていくと、「ジョブ」の粒度をどのくらいに設定するかが難しいことに気づくと思います。その際は本で紹介される事例を引き合いに出しながら、”定義した「ジョブ」を解決している競合サービスを思いつくか”という基準で調整するのがおすすめです。

なお今では施策設計の際、誰のどんな「ジョブ」をターゲットにするのか定義することが必須になるほど、「ジョブ理論」は深く活用されています。最近はインタビューの結果を人物ごとに「ジョブ」起点で記事のようにまとめた「ユーザー白書」を作成・蓄積するなど、実践手法も進化させながら続けています。

f:id:natsuki53:20190711174549p:plain
インタビュー後、「ジョブ」を見つけようとしている議論の様子

2. 「戦略づくり難しい!」の壁

向き合うユーザーのことがわかってきたところで、次は、事業目標をどう達成するか、戦略立案の壁にぶつかりました。投稿開発部の事業戦略は、部長が部の発足前に作った草案を、適時メンバーを巻き込みながら見直し、アップデートしています。目標がかなり動かし難い数字であることに加え、自分たちの学びも日々進化していく状況であるためです。

ただ当時は、事業戦略を考えたことのないメンバーが大半だったので、議論に入っても、意見すらうまく出ない状態でした。そこで、部長主導で以下の書籍を取り入れました。

「ストーリーとしての競争戦略」

https://www.amazon.co.jp/dp/4492532706/

競争戦略(事業戦略)は、静止画でなく動画、ストーリーであり、良い戦略は人に思わず話したくなる面白いストーリーになるはずだ、という見地に立って、他社が追従しようと思えないほど優れた戦略を作るための考え方を説いた本。講義を受けているような文調で、文量も多いが、事業戦略立案の本質を捉えて論じている良著。実在企業の事例も多く紹介されて参考しやすく、チームでの議論の際の引き合いに出しやすい(ただしITの事例は少ない)。

どう活用したか?

  • レシピ投稿サービスのコンセプト=「本当のところ我々は誰に何を売っているのか?」を定義し直すことから始め、自分たちの戦略ストーリー図を作りました
  • その上で、本で紹介されている他社の戦略図と見比べながら、自分たちの戦略図を磨いていきました

やってみてどうだったか?

自分たちのサービスが「本当のところ誰に何を売っているのか?」を定義するのは、想像以上に難しいです!これまでのインタビューで得たユーザーのエピソードをかき集めたり、競合サービスのコンセプトを推測して自社と比較したり、腹落ちする定義に至るまでに少し時間がかかりました。

しかし、そこで定義した提供価値を軸に、ゴール達成を引き起こすまでの中長期のストーリーを描いて作った戦略図は、シンプルで筋が通っていて、戦術や計画を考えるのに使いやすいです。現場でも、単発のアイデアをやみくもに実行することがなくなり、複数の要素を因果関係を持たせながら実現していく計画を考えられるようになりました。

ただし、実際に戦略図を書き起こすのは至難です。筋の良い戦略ストーリー図は、そらで描けるほどシンプルで、且つ、目標達成のために必要な変化を含む...とのことですが、ここは本に紹介されている実在企業の戦略図を横目に見ながら頭を捻りまくるしかありません。戦略図をチームで議論して描くのは難しすぎるので、今はコンセプトと解決策をチームで議論し、ある程度骨子が見えたところで部長が戦略図の草案を書くことが多いです。チームでは、その草案を元に、ストーリーを確実に実現する方法や、より強い「非合理」を入れてストーリーを面白くする方法を考えることにしています。

f:id:natsuki53:20190711171051p:plain
何度もお手本にしている、スターバックスの戦略ストーリー図

3. 「良いソリューションアイデアが出ない!」の壁

事業戦略ができ、具体的な施策づくりに入って行けるようになると、今度は自分たちの出すソリューションアイデアが今ひとつに思えて悩むようになってきました。戦略と仮説には自信があるのに、思いつく解決策に新しさや捻りがない、そもそも出てくるアイデアの数が少ない...。この壁は二重構造になっていて、以下2つの問題で成り立っていました。

  • ①チームでのアイデア出しの進め方がわからない
  • ②出すアイデアの量・質に自信が持てない

それぞれを助けてくれた本は以下です。

① 「SPRINT 最速仕事術―あらゆる仕事がうまくいく最も合理的な方法」

https://www.amazon.co.jp/dp/B06Y5NW5PQ/

問題①に効いた本。Googleで開発されたという、5日間で新しいアイデアを形にして検証・評価まで完了させる「スプリント」というフレームワークを紹介し、その実践手順を詳しく解説している。

どう活用したか?

  • 施策検証の段階にある施策で何度かそのまま取り入れて、スプリントを実施しました
  • その後、一部の手法を部分的に切り出して、普段の会議に取り入れるようにしました

やってみてどうだった?

スプリント自体は一長一短あると感じます。良いところは、テーマに対して参加者全員で大量のインプットを得て、めちゃくちゃ集中して考えることです。メンバーの脳内同期やステークホルダーの巻き込みにも効いたりします。また、とにかく時間が制限されるので、煮え切らないアイデアを捨てる判断ができる点も良いです。デメリットは、参加者全員の5日間の時間拘束が辛いことと、得られる成果の質が参加者の能力に左右されること。また、「レシピ投稿」はもともと施策の効きに時間がかかる傾向があるのですが、それを1日のインタビューで評価して良いのか?という疑念が拭いきれないことも、私たちにとっては大きな気がかりです。

ただ、「有能な人の仕事の流れをフレームワーク化した」と言うだけあって、スプリントのフレームワークには、目的に対する情報インプット、アイデア出し、検証、評価を効率的に行う工夫が詰まっていると感じます。それらを切り出して普段の業務に取り入れても、アイデアを考えたり意思決定するのがラクになり、チームのアイデア出しのパフォーマンスが高まります。専門家インタビュー、光速デモ、クレイジー8、ストーリーボード、ヒートマップ、サイレント投票などの手法は、普段の会議でも単発で取り入れやすいのでおすすめです。

f:id:natsuki53:20190711171137p:plain
スプリントの手法を活用したアイデア出しの様子

② 「直感と論理をつなぐ思考法 VISION DRIVEN」

https://www.amazon.co.jp/dp/B07NMN1B5Z/

問題②に効いた本。世の中を動かしてきたのは、ロジカルに組み上げられたアイデアではなく、「自分駆動の妄想」を起点にしたビジョンだ、という前提の元、まず根拠のない妄想(ビジョン)があって、それを実現する筋道を作るために論理を組み立てる「ビジョン思考」を提唱している。「ビジョン思考」の強化方法や、それに則ったアイデア作りの方法も詳しく解説されている。

どう活用したか?

  • チームで、既存路線上にない新しいアイデアを出さなければいけない時に、本で紹介されている「組替」の手法を取り入れました
    • 変化を起こしたい事象の「当たり前」を洗い出し、違和感のある「当たり前」をひっくり返してから、それを元に新しいアイデアを生み出していく手法です
  • 併せて、チームメンバーと「プロトタイピング志向」を徹底する同盟を組んでいます
    • 生煮えの考えもissue化したり、粗いプロトタイプを作って早期に人に見せることでアイデアに客観的な視点を加え、そこから練り上げていくところに時間をかけます

やってみてどうだったか?

「組替」の手法は取り入れ始めて日が浅いですが、以前より新しさを含んだアイデアがたくさん出るようになったと感じます。フレームワークに則って頭と手を動かせば、「ひらめき」=”既存要素の組み替え”を意図的に起こせるよう考えられており、チームで一緒に実践しやすいのも良い点です。

「プロトタイピング志向」は、普段のディレクター業務で実践するのにはまだ慣れないですが、うまくできた時は、1人で考え込むことに時間を使ってしまった時よりも成果物の質は高まると感じます。ちなみにこのブログ記事も、構想段階から色んな人に相談し、レビューしてもらって仕上げました!

また個人的にはこの本によって、「仕事のアイデアは論理で組み上げなければならない」という思い込みを打破できたことは大きかったです。まず直感に目を向け、それを人に説明できるよう後から論理づけして磨いていくという思考を意識すると、出せるアイデアの質が変わってくるのを感じます。

f:id:natsuki53:20190711171221p:plain
「組替」の手法を使ったアイデア出しの様子

本の知識をチームに取り入れるために

せっかく読んだ本から知識を”モノにする”には、読了後すみやかに得た知識を業務で実践することが1番重要だと感じます。また、チームで仕事を進める中では、”共通言語を作る”という意味でも、書籍で得た知識の共有は有効です。特にチームが悩んでいる時や、抽象度の高い議論を進めなければならない時、うまい共通言語を得ると、停滞していた話が進み始めることが多々あります。

新しい知見を業務で実践するところまで漕ぎつけたり、自分の読んでいる本の知識をうまくチームに共有できるかは、読んだ人の裁量によりがちです。そのため、本の知識をチームや業務に取り入れやすくするために、以下のような工夫をしています。

1.読んでいる本を共有しやすくする工夫

読書感想文共有スレ

GHEの部署のリポジトリ配下に「サービス開発系の読んだ本の感想を書くスレ」というissueを常設。簡素な1行コメントから超長文レポートまで、メンバーが読書感想を自由にpostしています。熱量の高い感想文には自然と注目が集まるし、共有したい知識はそこにまとめておけば参照してもらえるので、その後の議論でも話題に出しやすくなるのが利点です。最近このissueはコンテンツ力が増してきて、他部署からもファンや投稿者を創出する人気スレ(?)になりつつあります。

定例や1on1で読んでいる本の話をする

定例ミーティングや部長との1on1で、いま読んでいる本をよく共有しあいます。同じ本でも人によって読み取り方が異なるので、話すことで新しい観点が得られたり、チームの誰かと「それいいね!」「あの施策で試せるじゃん!」という会話をしておけると、そのあとの情報発信や業務での実践提案がしやすくなるので、意図的に活用しています。

その他、読んで良かった本は、物理本を買ってデスクに置いておくのも一手です。興味を持ってくれた人にサッと貸し出して、味方を増やします。

f:id:natsuki53:20190711171325j:plain

2.本の知識を業務で実践しやすくする工夫

わかりやすい事例や概念を切り出しておく

目的の書籍を読んでいない人を巻き込みたい時に使います。何も知らない相手に、自分が本から得た知識を口頭で説明してわかってもらう(その上で同じレベルで議論に参加してもらう)ことは至難の技ですが、本の中からわかりやすい事例や、概念を端的に表した図解などを切り出しておいて「うちもこんな風にやってみませんか」と提案すると、やりたいことをわかってもらいやすくなります。

読書後、自分の業務で実践するtodoを出す

「本を読んでも自分の仕事にどう活かせるか、パッとわからない...」という人(=私)におすすめの手法です。本を1冊読んだら、そこから自分の仕事で試してみたいtodoを1〜3個だけ考え出します。それだけで実践に運べる確率が上がります。「前のtodoが終わるまで次の本を読めない」というルールをつけると、より強制力が働くのでおすすめです。あとは実際にやってみて、継続するか・やめるかを振り返る機会を作れば完璧です。

f:id:natsuki53:20190711171436p:plain

おわりに

今回は特に大きかった壁と、ヘビロテしている選りすぐりの本を紹介しましたが、チームでお世話になってる本は他にもまだまだたくさんあります。仕事に行き詰まった時は、視野を広げて新しい知識を取り入れるチャンスと捉え、これからも積極的に本を読んでいきたいところです。

なお現在は、よりインプットの幅を広げたく、書籍に加えて、似た課題に直面している方々との情報交換も積極的に行っていきたい所存です!もしご興味を持ってくださる方がいらっしゃいましたら、お気軽にご連絡ください。
fb: https://www.facebook.com/natsuki.gomi.7

そしてそして、こんな私たちと一緒に壁に激突して、一緒に成長してくれる仲間も募集中です!募集中の職種は採用サイトからご確認ください!
https://info.cookpad.com/careers/


冪等なデータ処理ジョブを書く

$
0
0

こんにちは、マーケティングポート事業部データインテリジェンスグループの井上寛之(@inohiro)です。普段はマーケティングに使われるプライベートDMP(データマネジメントプラットフォーム)の開発を行っています。本稿では、その過程で得られた冪等なデータ処理ジョブの書き方に関する工夫を紹介したいと思います。今回は、RDBMS上で SQL によるデータ処理を前提に紹介しますが、この考え方は他の言語や環境におけるデータ処理についても応用できるはずです。

まずクックパッドのDMPと、冪等なジョブについて簡単に説明し、ジョブを冪等にするポイントを挙げます。また、SQL バッチジョブフレームワークである bricolage を使った、冪等なジョブの実装例を示します。

クックパッドのDMPと冪等なジョブ

クックパッドのプライベートDMPは、データウェアハウス(社内の巨大な分析用データベースで、クックパッドでは Amazon Redshift を使っている。以下 DWH) 上で構築されており、主に cookpad.com 上のターゲット広告や、社内のデータ分析に活用されています。材料となるデータは、広告のインプレッションログや、クックパッド上での検索・レシピ閲覧ログです。また他社から得たデータを DWH に取り込んで、活用したりしています。

これらのデータを活用したバッチジョブ群は、社内でも比較的大きめのサイズになっており、途中でジョブが止まってしまうことも考慮して、基本的にそれぞれのジョブが冪等な結果を生成するように開発されています。

冪等についての詳しい説明は省略しますが、簡単に言うと「あるジョブを何度実行しても、同じ結果が得られる」ということです。特にデータ処理の文脈においては、「途中で集計ジョブが失敗してしまったがために、ある日のデータが重複・欠損して生成されていた」ということはあってはなりません。ジョブが冪等になるように開発されていれば、失敗した場合のリトライも比較的簡単になります。また、ジョブが失敗しなかったとしても、(オペミス等で)たまたま複数回実行されるかもしれませんし、毎回同じ結果が生成されるべきです。

さらに、ジョブを冪等になるように開発すると、開発時に手元で試しに実行してみるときも検証が簡単なため、おすすめです。

冪等なジョブにするポイント

プライベート DMP を開発して得られた、ジョブを冪等にするためのポイントはズバリ「トランザクションを使え」です。

トランザクションを使ってロールバック

大量のデータを、長時間(N時間)かけて書き込むようなバッチジョブを考えるとき、途中で止まってしまったり、そこから復旧(リトライ)するという状況は予め考慮されているべきです。このとき、書き込む先がトランザクションをサポートするようなデータベース(一般的なRDBMSなど)ならば、トランザクションを利用しましょう。一つのトランザクションとしてまとめた一連の処理は、「すべて成功した状態」か、「すべて失敗した状態(ロールバック)」のどちらかになることが保証され、中途半端な状態にはなりません。途中で失敗しても、最初からぜんぶ書き直すことになりますが、冪等性は保たれています。

クックパッドの DMP は並列分散 RDB である Amazon Redshift 上に構築されているので、トランザクションをフルに活用しています。

自前でロールバック

一度実行された集計ジョブを再度実行した場面を考えてみます。再度実行される理由はいろいろ考えられますが、「意図せず間違って実行されてしまった」というのも同じような状況と考えられます。前回実行したときと同じ結果が得られれば問題ありませんが、集計した結果が重複してしまうと、後続のジョブが失敗するか、最悪の場合正しくない分析結果を用いて、何らかの意思決定が行われてしまうかもしれません。

つまり、現在実行中のジョブが書き込むテーブルに、今から書き込もうとしている条件で、既にデータが書き込まれているかもしれないのです。そこで、新たな結果をを書き込む前に、既存の行を削除(自前でロールバック)することで重複の発生を避けます。さらに、「削除」と「新しい結果の書き込み」を一つのトランザクションにまとめることで、このジョブは冪等になります。

冪等なデータ構造を利用する

一方で、トランザクションをサポートしないような NoSQL データベースを使っているとき、ジョブを冪等にするのは比較的簡単ではありません。このような状況で考えられる一つの解決策として、何度書き込まれても結果が変わらないデータ構造の利用が挙げられます。集合(Set)やハッシュテーブルです。これらのデータ構造は、データの順序は保証されないものの、既に存在する値(もしくはキー)を書き込んでも、要素が重複しません。

クックパッドの DMP で作成したターゲット広告用のデータは、最終的に Amazon DynamoDB *1に書き込まれ、広告配信サーバーがそのデータを使っています。ターゲット広告用のデータは、一度に数千万要素をバッチジョブが並列で書き込みますが、このジョブが稀に失敗することがあったり、過去に書き込まれている要素が時を経て再度書き込まれることがあるため、SS(文字列のセット)型を使っています。過去には Redis のセット型を使っていることもありました。

bricolage による冪等なジョブの実装例

クックパッドの DMP だけでなく、社内で SQL バッチジョブを書くときのデファクトスタンダードになっている bricolageには、頻出パターンのジョブを書く際に便利な「ジョブ・クラス」がいくつかあり、これを使うことで冪等なジョブを簡単に実装することができます。この節では bricolage を使った「トランザクションでロールバック」パターンと、「自前でロールバック」パターンの実装例を示します。

bricolage については、ここでは詳しく説明しませんが、詳細については過去の記事「巨大なバッチを分割して構成する 〜SQLバッチフレームワークBricolage〜」や、RubyKaigi 2019 でのLT「Write ETL or ELT data processing jobs with bricolage.」をご参照ください。また inohiro/rubykaigi2019_bricolage_demoにデモプロジェクトを置いてあります。

「トランザクションでロールバック」パターン

rebuild-dropもしくは rebuild-renameジョブ・クラスを使うと、「現行のテーブルを削除し、新規のテーブルに集計結果を書き込む」または「新規にテーブルを作り、集計結果を書き込み、現行のテーブルとすり替える」という操作を、一つのトランザクションで行うジョブを簡単に実装することができます。rebuild-dropは対象のテーブルを作り直す前に drop tableし、rebuild-renameはすり替えられた古いテーブルを、別名で残しておきます。

以下は、毎日作り変えられるようなサマリーテーブルを rebuild-dropジョブ・クラスで実装した例です。

/*class: rebuild-drop -- ジョブ・クラスの指定dest-table: $public_schema.articles_summarytable-def: articles_summary.ctsrc-tables:  pv_log: $public_schema.pv_loganalyze: false*/insertinto $dest_table
select
    date_trunc('day', logtime)::dateas day
    , id_param::integeras article_id
    , count(*) as pv
from
    $pv_log
where
    controller = 'articles'and action = 'show'and logtime < '$today'::dategroupby1, 2
;

このジョブは、以下の SQL に変換されて実行されます。

\timing onbegin transaction; -- トランザクション開始droptableifexistspublic.articles_summary cascade; -- 既存テーブルの削除/* /Users/hiroyuki-inoue/devel/github/rubykaigi2019_bricolage_demo/demo/articles_summary.ct */createtablepublic.articles_summary
( day date
, article_id integer
, pv bigint
)
;

/* demo/articles_summary-rebuild.sql.job */insertintopublic.articles_summary
select
    date_trunc('day', logtime)::dateas day
    , id_param::integeras article_id
    , count(*) as pv
frompublic.pv_log
where
    controller = 'articles'and action = 'show'and logtime < '2019-07-13'::dategroupby1, 2
;

commit; -- トランザクション終了

ジョブ全体が begin transaction;commit;で囲われているので、仮に集計クエリに問題があり失敗した場合は、元のテーブルは削除されずに残ります。

「自前でロールバック」パターン

insert-deltaジョブ・クラスは既存のテーブルに差分を書き込むために利用され、差分を書き込む直前に指定した条件でdeleteを実行します。また、一連の SQL は一つのトランザクションの中で行われるので、delete直後の差分を集計するクエリが失敗しても安心です。

以下は、日毎に広告インプレッションを蓄積しているテーブルimpressions_summaryに、前日($data_date*2の集計結果を書き込むジョブの例です。delete-cond:に削除条件を指定します。今回の例では、集約条件の一つである日付を指定しています。

/*class: insert-delta -- ジョブ・クラスの指定dest-table: $public_schema.impressions_summarytable-def: impressions_summary.ctsrc-tables:    impressions: $ad_schema.impressionsdelete-cond: "data_date = '$data_date'::date" -- 削除条件の指定analyze: false*/insertinto $dest_table
select'$data_date'::dateas data_date
    , platform_id
    , device_type
    , count(*) as impressions
from
    $impressions
groupby1, 2, 3
;

このジョブは以下のような SQL に変換され、実行されます。

\timing onbegin transaction; -- トランザクション開始deletefrom impressions_summary where data_date = '2019-07-12'::date; -- 既存行を指定した条件で削除/* demo/impressions_summary-add.sql.job */insertinto impressions_summary
select'2019-07-12'::dateas data_date
    , platform_id
    , device_type
    , count(*) as impressions
from
    ad.impressions
groupby1, 2, 3
;

commit; -- トランザクション終了

テーブルに書き込む前に指定した条件(delete-cond: "data_date = '$data_date'::date")で deleteクエリが実行され、"掃除"してから書き込むクエリが実行されるのが確認できると思います。対象の行がなければ何も削除されませんし、対象の行が存在すれば、新たな結果を書き込む前に削除されます。

まとめ

本稿では、クックパッドの DMP 開発において「冪等なデータ処理ジョブ」を書くために行われているいくつかの工夫について紹介しました。また、bricolage を使ってこれらのジョブを実装する例を示しました。

このように、トランザクションのあるデータベースを利用する場合は、なるべくその恩恵に乗っかるのがお手軽です。また、一つのジョブに色々なことを詰め込まず、ジョブを小さく保つことで、ロールバックの対象も小さくなり、失敗した場合のリトライなどもシンプルに行えると思います。bricolage のジョブ・クラスを上手に使うことで、トランザクションを利用した冪等なデータ処理ジョブを簡単に実装することができます。ぜひお試しください。

*1:この記事を書いていて思い出しましたが、Amazon DynamoDB はトランザクションをサポートしたのでした https://aws.amazon.com/jp/blogs/news/new-amazon-dynamodb-transactions/

*2:変数には前日の日付が入るように仮定しているが、ジョブのオプションで上書きが可能

ISMM 2019 で発表してきました

$
0
0

技術部の笹田です。遠藤さんと同じく Ruby のフルタイムコミッタとして、Ruby インタプリタの開発だけをしています。

先日、アメリカのフェニックスで開催された ISMM 2019 という会議で発表してきたのと、同時開催の PLDI 2019 という会議についでに参加してきたので、簡単にご報告します。

f:id:koichi-sasada:20190717032426j:plain
カンファレンス会場

ISMM 2019

ISMM は、International Symposium on Memory Management の略で、メモリ管理を専門にした、世界最高の学術会議です。というと凄いカッコイイんですが、メモリ管理専門って凄くニッチすぎて、他にないってだけですね。多分。ACM(アメリカのコンピュータ関係の学会。すごい大きい)SIGPLAN(プログラミングに関する分科会。Special Interest Group)のシンポジウムになります。

発表するためには、他の多くの学術会議と同じく、論文投稿をして、査読をうけ、発表に値すると判断される必要があります。基本的に、ガーベージコレクション(GC)のテクニックの提案や、新しい malloc ライブラリの提案とか、NVMどう使うかとか、そういう話を共有する場です。

ISMM 2019 は、6/23 (日) にアメリカのアリゾナ州フェニックスで1日で開催されました。外はムッチャ暑い(40度近い)ですが、室内は空調でムッチャ寒い、というつらい環境でした。外は暑すぎて歩けなかった。

会議は、キーノート2件に通常発表が11件でした。投稿数が24件だったそうで、採択率は50%弱だったようです。日本国内の会議より難しい(私の知っている範囲では、50%はあまり切らない)けど、トップカンファレンスに比べると通りやすい、というレベルだと思います。

今回、ISMM 2019 に投稿した論文が採択されたので、はじめて行ってきました。GC に関する仕事をしているので、ISMM は一度行ってみたい会議だったので、今回参加できてとても嬉しい。Ruby の GC に関する論文の発表だったので、出張としていってきました。感謝。おかげで、最新研究の雰囲気を感じることができました。

正直、内容と英語が難しくて、あんまり聞き取れなかったんですが、分かる範囲でいくつか発表などをご紹介します。

基調講演

2件の発表がありました。

Relaxed memory ordering needs a better specification

1件目はGoogleのHans-J. Boehmさんよる「Relaxed memory ordering needs a better specification」という発表でした。Boehmさんといえば、私にとってはBoehm GCというよく知られた実装の開発者の方ということで、お会いできて大変光栄でした。最近はC++言語仕様の策定などでお名前をよく聞きますが、今回はその話でした。なお、ここ最近は GC の実装にはほとんど関わってないと伺いました。

f:id:koichi-sasada:20190717032523j:plain
Boehmさんのキーノート

マルチスレッドプログラミングにおいて、メモリを読み書きする順序(メモリオーダリングといいます)が問題になることがあります。書いたと思った変数の値を読み込んでみたら、書き込む前の値だった、ってことがあったら驚きますよね。実は、マルチスレッドだとそういうことが起こってしまうんです。性能を良くするために、いくつかのCPUでは、共有メモリに対する他のスレッドからの書き込みが、逐次実行で見える書き込みの順序と違う可能性があるのです。

何を言っているかよくわからないと思うんですが、正直私もよくわかりません。例えば、0初期化された共有メモリ上にある変数 a, bがあったとき、a = 1; b = 2;というプログラムがあったら、(a, b) の組は (0, 0)、(1, 0)、(1, 2) の3通りしかないように思うんですが(逐次(シングルスレッド)プログラムだと、実際そうです)、他のスレッドから観測すると、(0, 2) という組が見えたりします(他の最適化が絡むと、もっと変なことが起る可能性があるらしいです)。わけわからないですよね? わからないんですよ。人間にはこんなの管理するのは無理だと思う。共有メモリなんて使うもんじゃない(個人の感想です)。

さて、どんなふうにメモリーオーダリングをするか、という指定をするための言語機能が C++ などにあります(std::memory_order - cppreference.com)。例えば memory_order_seq_cstというのが一番厳しい指定で、他のスレッドからも同じように見える(つまり、上記例だと (0, 2) という組は見えない)ようになり、プログラミングするにはこれが多分一番便利です。ただ、性能のために都合の良いように CPU が順序を変えている(可能性がある)のに、その順序を厳しく制御する、ということになるので、オーバヘッドがかかります。で、どの程度厳しくするか、みたいなので、いくつか種類があるわけです。CPU によって、どの程度デフォルトが厳しいか決まるんですが、幸い、x86(x86_64)は比較的強いメモリオーダリングを行うので、あんまり難しくない、らしいのです。ARM とかだと弱いらしいとか、さっきググったらありました。やばいっすね。

今回の基調講演では memory_order_relaxedという、多分一番ゆるい(何が起こるかわからない)メモリオーダリング指定を、どうやって仕様化すればいいか難しい、という話を、実際にすごく不思議な挙動があるんだよねぇ、という豊富な実例をあげて紹介されていました。従来の仕様では、例ベースでしか仕様に書けなかったんだけど、なんとか書きたいなぁ、でも難しいなあ、というお話でした。結論がよくわかってなかったんだけど、結局うまいこと書けたんだろうか。

なんでメモリ管理の会議 ISMM でメモリオーダリングの話が問題になるかというと、並行GCっていう研究分野があって、GC するスレッドとプログラムを実行するスレッドを並行・並列に実行していくってのがあるんですね。で、それを実現するためにはメモリオーダリングをすごく気にしないといけないわけです。これもきっと人間には無理だと思うんですが、実際にいくつかの処理系でやってるのが凄いですよねえ。いやぁ凄い。

Why do big data and cloud systems stop (slow down)?

2件目のキーノートは、シカゴ大学のShan Lu氏による「Why do big data and cloud systems stop (slow down)?」という発表でした。

実際のウェブアプリケーションや分散処理基盤(Azure。共同研究されてるんでしょうなあ)でどんな問題があるか、主に性能の観点から分析してみたよ、という話でした。ウェブサイト(Shan Lu, CS@U-Chicago)を拝見すると、輝かんばかりの業績ですね(研究者は良い学会に論文を通すことが良い業績と言われています。で、見てみると本当に凄い学会に沢山論文が採択されていて凄い)。

面白かったのが、ウェブアプリケーションの性能分析で Rails が題材になっていたことです。「あ、見たことあるコードだ」みたいな。

ウェブアプリケーションに関する分析の話は、View-Centric Performance Optimization for Database-Backed Web Applications (ICSE'19) のものだったように思います。主に ORM でのアンチパターンをいろいろ分析して(講演では、そのパターンを色々紹介されていました)、それを静的解析してアプリからそのアンチパターンを見つけて良い方法を提案するツールを作ったよ、と。Panoramaというツールを作っていて公開されています。なんと IDE (Rubymine)との統合までやっているようです。凄い。論文中に、いくつかリファクタリング例があるので、気になる方は参考にしてみてください。しかし、Rails アプリの静的解析って、えらく難しそうだけど、どれくらい決め打ちでやってるんですかねぇ。

Azure のほうは、設定間違いがほらこんなに、とか、そんなご紹介をされてたような気がします。具体的には What bugs cause production cloud incidents? (HotOS'19) の話かなぁ。論文中 Table 1 がわかりやすいので引用します。

    What are the causes of incidents?
↓ Few hardware problems
↓ Few memory bugs
↓ Few generic semantic bugs
↑ Many fault-detection/handling bugs
↑ Many data-format bugs
↑ More persistent-data races

    How are incidents resolved?
↑ More than half through mitigation w/o patches

Table 1: How are cloud incidents different from failures in single-machine systems?
(↑ and ↓ indicate cloud incidents follow certain pattern more or less than single-machine systems.)

いやぁ、こういう網羅的な調査を行うって凄いですよね。

一般発表

一般発表は、次の4つのセッションに分かれていました(Program - ISMM 2019)。

  • Scaling Up
  • Exotica
  • Mechanics
  • Mechanics / Message Passing

かなり大ざっぱな区切りですよね。Exotica とか凄い名前。

そういえば、"Scaling Up"セッションは、東工大とIBM東京基礎研の方々による3件の発表となっており「東京セッション」と座長に紹介されてました。また、私が発表しているので、東京の組織の発表が11件中4件あったことになるんですね。日本人はメモリ管理好きなんでしょうか。まぁ、私は好きですけど。

いくつか紹介します。

malloc の改良

  • Timescale functions for parallel memory allocation by Pengcheng Li (Google) et.al.
  • A Lock-Free Coalescing-Capable Mechanism for Memory Management by Ricardo Leite (University of Porto) et.al.
  • snmalloc: A Message Passing Allocator by Paul Lietar (Drexel University) et.al.

これら3件の発表は、malloc の実装を改良、もしくは新規に作りました、という話でした。なんというか、malloc()は、まだまだ進化するんだなぁ、やることあるんだなぁ、という感想。どれも、並列計算機(マルチスレッド環境)での弱点をどう克服するか、という研究でした。

とくに最後の snmalloc は面白くて、確保 malloc()、解放 free()のペアって、たいていは同じスレッドで行われると仮定してライブラリを作るので、別スレッドで free()しちゃうと余計なオーバヘッドがかかっちゃう、ことが多いようです(実際、私も作るならそう作りそう)。ただ、いくつかの種類のプログラム、例えば複数スレッドで仕事をパイプライン的に流していくとき、確保と解放は必然的に別スレッドになって、そこがボトルネックになるので、メッセージパッシング機構をうまいこと作ることで、free()の時にしか同期が不用で速いアロケータを作ったよ、というものでした。

Google の中川さんが論文の説記事を書いていたので、ご参照ください(論文「snmalloc: A Message Passing Allocator」(ISMM 2019))。

GC の改良

  • Scaling Up Parallel GC Work-Stealing in Many-Core Environments by Michihiro Horie (IBM Research, Japan) et.al.
  • Learning When to Garbage Collect with Random Forests by Nicholas Jacek (UMass Amherst) et.al.
  • Concurrent Marking of Shape-Changing Objects by Ulan Degenbaev (Google) et.al.
  • Design and Analysis of Field-Logging Write Barriers by Steve Blackburn (Australian National University)

GCの改善の話も結構ありました。

最初の話は、IBM東京基礎研の堀江さんらによる、並列GCの work-stealing を効率化した、という話でした。GCスレッドを複数立てて、GC処理を速く終わらせるには、仕事を分散させるためのテクニックである work-stealing が必要になります。それに関するテクニックの話でした。対象が PPC なのが IBM っぽくて凄いですね。

二つ目は、GCのいろいろなチューニングをランダムフォレストでやってみよう、という話でした。GC の制御も AI 導入、みたいな文脈なんでしょうか?

三つ目は、Google V8 での並行マーキングにおいて、メモリの形(というのは、メモリレイアウトとかサイズとか)を変更しちゃう最適が、並行GCと食い合わせが悪いので、それをうまいこと性能ペナルティなくやるって話で、実際に Chrome に成果が入っているそうです。みんなが使うソフトウェアに、こういうアグレッシブな最適化を入れるの、ほんと凄いですね。話は正直よくわからんかった。

最後は、Field-Logging Write Barriersというのは、フィールド単位(Ruby でいうとインスタンス変数)ごとにライトバリアを効率良く入れる提案でした。Ruby 2.6(MRI)だと、オブジェクト単位でライトバリアを作っているんですが、さらに細かく、バリア、というか、バリアによって覚えておくものを効率良く記録する方法、みたいな話をされていました。むっちゃ既存研究ある中(発表中でも、既存研究こんなにあるよ、と紹介していた)で、さらに提案するのは凄い。

Gradual Write-Barrier Insertion into a Ruby Interpreter

私(笹田)の発表は、Ruby にライトバリア入れて世代別GCとか作ったよ、という Ruby 2.1 から開発を続けている話を紹介しました(Gradual write-barrier insertion into a Ruby interpreterスライド資料)。2013年に思いついたアイディアなので、こういう学会で発表するのはどうかと思ったんですが、ちゃんとこういう場で発表しておいたほうが、他の人が同じような悩みをしなくても済むかも、と思って発表しました。RubyKaigi などでしゃべっていた内容をまとめたものですね。

簡単にご紹介すると、Ruby 2.1 には世代別GC(マーキング)、2.2 にはインクリメンタルGC(マーキング)が導入されました。これを実現するために、"Write-barrier unprotectred object"という概念を導入して、ライトバリアが不完全でもちゃんと動く仕組みを作った、という話です(次回の Web+DB の連載「Ruby のウラガワ」でも解説しますよ。宣伝でした)。GC は遅い、という Ruby の欠点は、この工夫でかなり払拭できたんじゃないかと思います。まだ GC が遅い、というアプリケーションをお持ちの方は、ぜひベンチマークを添えて笹田までご連絡ください。

「Gradual WB insertion」というタイトルは、ライトバリアをちょっとずつ入れて良い、って話で、実際 Ruby 2.1 から Ruby 2.6 までに、徐々にライトバリアを入れていったという記録を添えて、ちゃんと「Gradual に開発できたよ」ということを実証しました、という話になります。

結構面白い話だと思うんだけど、アイディア自体が簡単だったからか、質問とかほとんどなくて残念でした。まぁ、あまり研究の本流ではないので、しょうがないのかなぁ(本流は、ライトバリアなど当然のようにある環境でのGCを考えます)。

PLDI 2019

PLDI は、Programming Language Design and Implementation の略で、プログラミング言語の設計と実装について議論する、世界で最高の学術会議の一つです。以前は、実装の話が多かったんですが、PLDI 2019から引用しますが、

PLDI is the premier forum in the field of programming languages and programming systems research, covering the areas of design, implementation, theory, applications, and performance.

とあって、設計と実装だけじゃなく、理論やアプリケーション、性能の分析など、プログラミング言語に関する多岐にわたる話題について議論する場です。言語処理系に関する仕事をしているので、一度は行ってみたかった会議です。ISMM出張のついでに出席させて貰いました。参加費だけでも6万円くらいするんですよね。

PLDI 2019 は、6/24-26 の3日間で行われました。ISMM 2019 は、この PLDI 2019 に併設されています。PLDI は言語処理系によるメモリ管理もスコープに入っているので、実は ISMM で発表するよりも PLDI で発表するほうが、他の人から「凄い」と言われます。どの程度凄いことかというと、283論文が投稿され、その中で76本が採択されたそうです(27%の採択率)。これでも、例年より高かったそうです。死ぬまでに一度は通してみたい気もしますね。まぁ、難しいかなぁ(例えば、日本人で PLDI に論文を通した人は、あんまり居ません)。

発表

三日間で最大3セッションパラレルに発表がされるため、あまりちゃんと追えていないのですが、印象に残った発表についてちょっとご紹介します。

ちなみに、以前は結構、がっつり実装の話が多かったんですが、今回の発表は、

  • 理論的な分析の話
  • 特定分野(例えば機械学習)の DSL の話

が多いなぁという印象であり、あんまり(私が)楽しい実装の話は少なかったように思います。

セッションは次の通り(これだけ見てもムッチャ多い)

  • Concurrency 1, 2
  • Language Design 1, 2
  • Probabilistic Programming
  • Synthesis
  • Memory Management
  • Parsing
  • Bug Finding & Testing 1, 2
  • Parallelism and Super Computing 1, 2
  • Type Systems 1, 2, 3
  • Learning Specifications
  • Reasoning and Optimizing ML Models
  • Static Analysis
  • Dynamics: Analysis and Compilation
  • Performance
  • Systems 1, 2
  • Verification 1, 2

いくつかご紹介します。

Renaissance: Benchmarking Suite for Parallel Applications on the JVM

発表は聞いてないんですが、JVM の並列実行ベンチマークについての発表だったそうです。よく DaCapo とかが使われていましたが、また新しく加わるのかな。

DSL

繰り返しになりますが、ある分野に対する DSL の話が沢山ありました。ちょっと例を挙げてみます。

  • LoCal: A Language for Programs Operating on Serialized Data は、シリアライズされた状態のままデータを操作する DSL
  • Compiling KB-Sized Machine Learning Models to Tiny IoT Devices は、IoT 環境みたいなリソースセンシティブな閑居杖、良い感じに整数で浮動小数点っぽい計算をする DSL
  • CHET: An Optimizing Compiler for Fully-Homomorphic Neural-Network Inferencing は、暗号化したまま計算する仕組みのための DSL/Compiler(多分。自信ない)。
  • FaCT: A DSL for Timing-Sensitive Computation は、タイミングアタック(計算時間によって秘密情報を取ろうというサイドチャンネルアタック)を防ぐために、計算時間を結果にかかわらず一定にするコードを生成するための DSL(多分)。

なんかがありました。もっとあると思います。適用領域が変われば言語も変わる。正しいプログラミング言語の用い方だと思いました。

メモリ管理

メモリ管理はわかりやすい話が多くて楽しかったです。

  • AutoPersist: An Easy-To-Use Java NVM Framework Based on Reachability は、Java (JVM) に、良い感じに NVM (Non-volatile-memory) を導入する仕組みを提案。
  • Mesh: Compacting Memory Management for C/C++ Applications C/C++ で無理矢理コンパクションを実現しちゃう共学のメモリアロケータの実装。
  • Panthera: Holistic Memory Management for Big Data Processing over Hybrid Memories は、NVM をでかいメモリが必要な計算でうまいこと使うためのシステムの紹介。

Mesh については、これまた Google 中川さんの論文紹介が参考になります(論文「MESH: Compacting Memory Management for C/C++ Applications」(PLDI 2019))。むっちゃ面白い。Stripe にも務めている(多分、論文自体は大学の研究)ためか、評価プログラムに Ruby があって面白かった。ちょっと聞いたら(発表後の質疑応答行列に30分待ちました。凄い人気だった)、Ruby のこの辺がうまくマッチしなくて云々、みたいな話をされてました。

Reusable Inline Caching for JavaScript Performance

V8のインラインキャッシュを、再利用可能にして、次のブート時間を短縮しよう、という研究でした。私でも概要がわかる内容で良かった。インラインキャッシュの情報って、基本的には毎回変わっちゃうんで、難しいのではないかと思って聞いてたんですが、巧妙に変わらない内容と変わる内容をわけて、変わらないものだけうまいことキャッシュして、うまくボトルネック(ハッシュ表の検索など)を避ける、という話でした。V8って某大なソースコードがありそうなので、Google の人に聞いたのですか、と聞いてみたら、全部独学だそうで、すごい苦労して読んだと言ってました。凄い。

Type-Level Computations for Ruby Libraries

RDL なんかを作っている Foster 先生のグループの発表で、Ruby では動的な定義によって、実行時に型が作られるので、じゃあ実行時に型を作ってしまおうという提案です。Ruby でも PLDI に通るんだなあ、と心強く感じます。Ruby 3 の型はどうなるんでしょうね。

A Complete Formal Semantics of x86-64 User-Level Instruction Set Architecture

x86-64 の全命令(3000命令弱といってた)に形式定義を K というツールのフォーマットで記述した、という発表で、ただただ物量が凄い。おかげで、マニュアルなどにバグを見つけたとのことです。成果は Github で公開されてます(kframework/X86-64-semantics: Semantics of x86-64 in K)。

おわりに

ISMMはPLDIに併設されたシンポジウムですが、PLDIもFCRC という、学会が集まった大きな会議の一部として開催されました。懇親会はボーリング場などが併設された会場で行われ、いろいろ規模が凄かったです。

f:id:koichi-sasada:20190717032623j:plain
懇親会の様子

こういう学会に出席すると、最新の研究成果に触れることができます。正直、しっかりと理解できないものが多いのですが、雰囲気というか、今、どういうことが問題とされ、どういうところまで解けているんだ、ということがわかります(まだ、malloc ライブラリの研究ってこんなにやることあるんだ...とか)。このあたりの知見は、回り回って Ruby の開発にも役に立つと信じています。立つと良いなぁ。

今回の論文執筆と参加をサポートしてくれたクックパッドに感謝します。

Grafana の scripted dashboards を利用してダッシュボードを自動生成する

$
0
0

技術部 SRE グループの鈴木 (id:eagletmt) です。

去年クックパッド開発者ブログでも紹介した hako-consoleの延長として、メトリクス表示に Grafana の scripted dashboards を利用するようにしているのでその紹介をしようと思います。

アプリケーション毎のダッシュボード

クックパッドではダッシュボードの作成に Grafana を利用しており、主に Amazon CloudWatch と Prometheus に保存されているメトリクスを Grafana で可視化しています。それ以外にも一部開発用のメトリクスは InfluxDB に保存しており、その可視化にも Grafana が利用されています。

Grafana の variables 機能を利用すればリソースの種類毎にダッシュボードを作成することは簡単です。 ELB のロードバランサー名、RDS のクラスタ名、ECS のサービス名を variable として受け取るようにして CloudWatch の dimensions や Prometheus の PromQL にその variable を入れるようにすれば、各リソースの状況を閲覧することができるようになります。

ではアプリケーション毎のダッシュボードはどうでしょうか。典型的な Web アプリケーションの状態やパフォーマンスを知りたいときには

  • ALB のリクエスト数やレスポンスタイムの95パーセンタイルはどうなっているか
  • cAdvisorから得られるコンテナの CPU 使用率やメモリ使用率はどうなっているか
  • RDS の CPU 使用率やクエリのレイテンシはどうなっているか
  • Memcached の CPU 使用率や Eviction はどうなっているか

等の情報を一覧したいでしょう。

hako-console がその一覧するための役割を担っていたのですが、私が実装した hako-console のメトリクス表示画面よりも Grafana でのダッシュボードのほうが圧倒的に見やすく使いやすいため、アプリケーション毎に Grafana にダッシュボードを自動生成する方法を考えました。

Scripted Dashboards

そこで Grafana の scripted dashboards 機能を利用することにしました。 これは Grafana サーバに public/dashboards/nanika.js という JS ファイルを設置して Grafana 上で /dashboard/script/nanika.js にアクセスすると設置した JS ファイルを評価し、その結果をダッシュボードの JSON 表現として解釈しダッシュボードを表示するという機能です。この JS ファイルでは任意の JS コードを実行できるため、以下のように別のサーバが返した JSON をそのまま返すような JS ファイルを設置することで、サーバから Grafana のダッシュボードを制御することが可能になります。

'use strict';

var ARGS;

return async function(callback) {const fallback = {
    schemaVersion: 18,
    title: 'Failed to load dashboard from hako-console',
    panels: [{
        id: 1,
        type: 'text',
        gridPos: {
          w: 24,
          h: 3,
          x: 0,
          y: 0,
        },
      },
    ],
  };
  try{const response = await fetch(`https://hako-console.example.com/grafana_dashboards/${ARGS.app_id}`, { credentials: 'include' });if (response.status === 200) {const dashboard = await response.json();
      callback(dashboard);
    }else{
      fallback.panels[0].content = `hako-console returned ${response.status} error.`;
      callback(fallback);
    }}catch (e) {
    fallback.panels[0].content = `Failed to fetch API response from hako-console: ${e}`;
    callback(fallback);
  }};

この例ではクエリストリングで app_id というアプリケーションの識別子を受け取り、それを hako-console に問い合わせています。hako-console はこの問い合わせに対してこのアプリケーションに関連する ALB、RDS、Memcached 等のリソースを見つけ、それぞれに対応するメトリクスを表示するようなダッシュボードの JSON 表現を返すようになっています。ダッシュボードの JSON 表現についてはドキュメントがあるのですが、すべてを網羅できているわけではないので、Grafana 上で実際にダッシュボードを作ってその JSON Model を見てそれに合わせる、と進めたほうが私は分かりやすかったです。 https://grafana.com/docs/reference/dashboard/

たとえばとある Web アプリケーションの自動生成されたダッシュボードは以下のようなものです。このダッシュボードを表示する JSON 表現として hako-console は https://gist.github.com/eagletmt/45f8c8bffcbe34f48e937a756aac2a34のようなレスポンスを返しています (※一部の値はマスキングしてます)。 f:id:eagletmt:20190724111907p:plainf:id:eagletmt:20190724112029p:plain Grafana はダッシュボードの JSON 表現を介して import することもできます。したがって自動生成されたダッシュボードでは物足りず、たとえばアプリケーションが独自に Prometheus に保存しているメトリクスも表示したい場合にも、簡単に拡張することもできます。hako-console 上で固定のメトリクスを表示していたときと比べて、この点も Grafana を利用するメリットだと思っています。

ダッシュボードの工夫

アプリケーション毎のダッシュボードを自動作成するにあたって、見やすさと実用性を重視するために各リソースについて頻繁に参照するメトリクスのみを表示するようにしました。たとえば ElastiCache Memcached の場合、使用可能な空きメモリの量 (FreeableMemory) やキャッシュされた容量 (BytesUsedForCacheItems) 等が役に立つこともありますが、多くのケースで役立つメトリクスは CPU 使用率 (CPUUtilization) や eviction の発生回数 (Evictions)、キャッシュのヒット率等でしょう。公式のドキュメントも参考になります。 https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/mem-ug/CacheMetrics.WhichShouldIMonitor.html

ちなみにキャッシュのヒット率は CloudWatch の基本的なメトリクスには含まれていませんが、GetHits と GetMisses から算出することができます。Grafana は CloudWatch の Metric Math 機能をサポートしているため、これを利用して GetHits / (GetHits + GetMisses) の値を Grafana 上に表示できます。

見やすさのために一部の主要なメトリクスに絞って表示することにしたとはいえ、主要でないメトリクスが手掛かりになることがあるのはたしかです。そこで Grafana パネルのリンク機能を使い、別途用意された詳細なメトリクスが表示されたダッシュボードへ移動できるようにしています。 この機能を利用すると現在のダッシュボードで選択されている time range をリンク先のダッシュボードに引き継ぐことができます。とくに1日以上前の障害を調査したり振り返ったりするときには time range の引き継ぎは便利でしょう。

また、このアプリケーション毎のダッシュボードにはデプロイのタイミングを annotationとして表示するようにしています。 f:id:eagletmt:20190724112414p:plainメトリクスの傾向が変化する原因の多くはデプロイです。プロモーション等によるユーザ数の急激な変化や下流のマイクロサービスのデプロイによるアクセス傾向の変化といった他の要因でメトリクスの傾向が変化することもありますが、それらよりもそのアプリケーション自身の変更が原因であることが多いでしょう。ダッシュボード上でデプロイのタイミングを分かりやすくすることで、意図しないパフォーマンス劣化が発生していないか、パフォーマンス改善を狙った変更がうまくいったかどうか、といったことが分かりやすくなることを狙っています。

なお、このデプロイのタイミングはどうやって取得しているかというと、クックパッドではほとんどのアプリケーションが ECS で動いているため、ECS の UpdateService API が実行されたタイミングをデプロイのタイミングとすることができます。そこで、S3 バケットに配信された CloudTrail のログファイルを加工して Prismに渡すことで Redshift Spectrum で読める状態にし、Redshift にクエリすることで UpdateService API が実行されたタイミングを取得して InfluxDB に保存し、Grafana からそれをデータソースとして annotation の query に設定しています。CloudTrail のログは他にも用途があるため、このように一度 Redshift に入れてからそれぞれが使うようになっています。 f:id:eagletmt:20190724112506p:plain

まとめ

Grafana の scripted dashboards という機能と、それを利用してどのようなダッシュボードを自動生成しているかについて紹介しました。Grafana は手軽に見やすいダッシュボードを作成できて便利な反面、variables 機能ではカバーできないような個別のダッシュボードを1つ1つ作るのが面倒に感じている方は、自由度の高い scripted dashboards 機能を利用してみてはどうでしょうか。

AWS re:Inforce 2019に参加してきました

$
0
0

技術部セキュリティグループの三戸 (@mittyorz) です。こんにちは。 去る6/25,26日に開催されたAWS re:Inforce 2019に参加しましたので、簡単ではありますが紹介させていただきたいと思います。 今回が初開催なため規模感や雰囲気などは未知数の中、クックパッドからはセキュリティグループの三戸・水谷 (@m_mizutani)とVP of Technologyの星 (@kani_b) のあわせて3名で参加いたしました。

AWS re:Inforce とは

AWS re:Inforceは、数あるAWSのカンファレンスの中でもセキュリティに特化したカンファレンスです。 セキュリティに関するイベントはre:Inventなどでもこれまであったわけですが、re:Inventが非常に巨大なイベントとなってしまったこと、クラウドベンダーとしてセキュリティにおいてどのような姿勢で望むのかを積極的にアピールする場としてre:Inforceが新たに用意されたのではないかなと思っています。 AWS以外にも様々なセキュリティベンダーがパートナーとして参加しており、ブースの様子などについては後ほど触れたいと思います。

Builderにとってのセキュリティ

初日、AWSのVPかつCISOであるSteve SchmidtによりKeynoteが行われました。この中で、セキュリティはセキュリティエンジニアだけが考えるものではなくサービスに関係する全てのメンバーが意識し関わるべきものであること、 サービスを作り上げるうえで必要なものを自分たちで選択し作り上げていくように、セキュリティにおいてもベンダーの製品をそのまま使うのではなく、要素を組み合わせ自分たちにとって本当に必要なものを作っていく必要があることが強く主張されていました。 その際に、AWSのクラウドサービスをどのように上手に選択するか、ということが大事になってくるわけです。

セキュリティベンダーの事例紹介でも同様で、単に顧客の環境に自社の製品を導入したという話ではなく、AWSのサービスと組み合わせることでどういうことが新たにできるようになったかということが説明されていました。 そのためには自分たちでセキュリティを作り上げていく必要があり、度々キーワードとして出てきた「Builder」にそのことが表れていると思います。

会場の様子

AWS re:Inforceは現地ボストンにおいて6/25,26の二日間に渡って開催されました。 AWSのカンファレンスといえばまずre:Inventを思い浮かべる方が多いかと思いますが、規模としてはそこまで巨大というわけではなくおおよそ一つの建物にまとまっていました*1

f:id:mittyorz:20190723152400j:plain
会場となったBoston Convention and Exhibition Centerの、おそらく正面入口

f:id:mittyorz:20190723152532j:plain
ベンダーによるブースなどが並ぶ、受付からすぐのスペース。会場は4階まであったので写っているのはごく一部と言えます

日本からは24日18時に成田から離陸して現地に24日18時に到着*2、翌日から二日間会場入りして、三日目木曜日には帰国に向け出発し金曜日夕方に日本に到着するというちょっと強行軍かなとも言えるスケジュールでした。 幸いにも恐れていた時差ボケにはならず、はじめての海外出張かつ大規模なカンファレンス初参加の割には落ち着いて見て回れたのではないかと自分では思っています。

セッションについて

日程は二日間でしたが、聴講タイプのいわゆるSessionの他、手を動かすWorkshopやSecurity Jam他、非常に沢山の ブースではベンダーの説明を聞いたり質問を行うことも出来ますし、待ち合わせ時間などに手元のPCから参加できるCTFも用意されていました。

1時間単位のセッションにいくつか参加したのと、4時間かけて点数を競い合うSecurity Jamに3名でチームを組んで参加しました。

以下、参加したセッションから特に印象に残った点などを簡単に紹介します。

Encrypting Everything with AWS (SEP402)

AWSの各サービスでどの部分でどのような暗号化をしているか、物理層の安全性をどう担保しようとしているか、について広く説明するセッションです。 ただ、時間のかなりの部分が「暗号論入門」という感じで、暗号アルゴリズム自体の説明になっていてちょっと期待とは違ったかなという感想でした。 セッションには難易度が設定されていて、これは「Expert」向けとされていたのですが、暗号アルゴリズムの数学的背景を予め知っているのであればだいぶわかりやすかったのではないかなと思います。 後半はAWSの各サービスでどのように暗号化が行われているかそのアルゴリズムも含め解説され、AWS SDKからの利用といったユーザが直接触れる部分から、インスタンス間やVPC間の通信をどのように行っているのか、更にはリージョン間の物理ネットワークの暗号化などレイヤーごとに説明されていました。 後述するNitro Controllerを用いて暗号化する際には、複数のキーストアから取得した鍵の「一部分」をNitron Controller内であわせることで実際に用いる鍵を生成し用いているという話が特に興味深かったです。

Firecracker: Secure and fast microVMs for serverless computing (SEP316)

LambdaおよびFargateの実行環境を効率的に隔離するために実装されたLinux KVMベースのHypervisior、Firecrackerについてのセッションです。 アカウントごとに実行環境をきちんと分離しつつ、オーバーヘッドの低減を両立させるのは難しいと説明しつつ、一方でFirecrackerを用いることでVMの起動をだいたい120ms以下で行い、一つのホストで1秒間に150VM起動できるようになると話していたのが興味深かったです。 Lambdaを実行している環境は、そのLambdaをデプロイしたアカウントごとに別々のゲストOSにHypervisorによって分離されているわけですが、このHypervisiorにFirecrackerを用いることで一つのホストによりたくさんの環境を詰め込める、ということが詳しく説明されていました*3。 Fargateについては、Firecrackerを用いることでEC2のインスタンスサイズの荒い粒度ではなくより細かくリソースが割り当てられるようになったこと、起動が非常に速いので「EC2 Warm Pool」を用意する必要がなくなり価格が大きく削減できたこと*4が説明されていました。 今後のロードマップでは、現在はIntel CPUのみがサポートされていますが、年内にはAMD CPUのサポートも計画されているとアナウンスされました。

Security benefits of the Nitro architecture (SEP401)

このセッションでは、Nitro Contollerと呼ばれる物理的なチップとそれを用いた全体のアーキテクチャについて説明がありました。 「Nitro」という言葉自体はAWSの最新世代のEC2インスタンスなどで用いられている仮想化技術というコンテキストでよく出てきますが、このシステムを構成するために通常のCPUや周辺機器とは独立して組み込まれているのがNitro Contollerです。 仮想化に伴うオーバーヘッドを低減するために、出来る限りNitro Controllerを用いて処理をオフロードする仕組みになっているのですが、インスタンスストレージへのアクセスを含めDisk I/O、Network I/OなどすべてのI/OはNitro Controllerを用いて透過的に暗号化される仕組みになっています。 また、暗号化に必要な情報はNitro Contoroller同士で自動的にやり取りされるとのこと。 Nitro Controller自体も独自に内部的なストレージを持っていて、ファームウェアのアップデートなどはシステムを再起動すること無く行えるようになっているそうです。 Nitro Controllerが改ざんされないようCPUからは書き込みができないようになっていたり、システムの起動時にはNitro Contorollerによってマザーボードのファームウェアがチェックされるようになっているなど、システムの健全性を担保するのにNitro Controllerが要になっていることがよく分かるセッションでした。

Security Jam

複数人でチームを組み*5、競技時間の4時間の間に出題された問題を出来るだけ問いて順位を競い合います。 競技時間終了と同時に出題ページがクローズされてしまったので記憶を元にちょっと紹介すると、

  • 複数のVPCにまたがって構成されたウェブサービスを再構成する問題
  • EKSのCI/CDと、EKS自体のセキュリティの設定を修正する問題

といったように、AWSの各サービスをきちんと理解していないと問題が解けないようにうまく設計されていて、普段の業務で触ってない部分の知識も容赦なく必要とされなかなか苦労しました。

f:id:mittyorz:20190723152620j:plain
全体で60席くらい用意されていましたが、オンラインで参加できるのでこのホール外から参加していたチームもあったようです

f:id:mittyorz:20190723152643j:plain
5-hour ENERGY という、ちょっとヤバそうなエナジードリンク

最終的に45チーム中11位という結果になりました。

セキュリティの競技だといわゆる「CTF」をまず思い浮かべる方が多いかと思いますが、普通のCTFと比較してみてAWSの環境自体を直す要素が強かったかなと思います。 また、AWSのアカウント・リソースが問題ごとに別々に用意されていて、出題ページからAWSのコンソールにログインすると独立した環境が自由に使えるようになったのは流石だなという感じでした。

ブース

企業ブースでは日本でもよく見る有名・老舗セキュリティベンダーがやはり多かったですが、一方で日本ではあまり見ないベンダのブースも多くありました。こういったブースだと直接担当の人と話して質問ができるのでなかなかカタログスペックなどから分からないことを聞けて面白かったです。全体の傾向としては、以下のような印象でした。 - コンテナ環境を対象としたサービスがメインストリームになりつつあり、逆に言うとEC2インスタンスなどにフォーカスするようなサービスの宣伝はあまり見なかった - クラウド環境全体の監査・コンプライアンスチェックを自動化するというサービスがかなり目立っていた

程度の差はありましたが、ブースエリアはそれほど混んでおらず気になった製品のブースで気軽に質問できるようになっていました。

おわりに

AWS re:Inforce 2019を簡単にですが紹介させていただきました。 AWS自身が提供するセキュリティ機能だけではなく、各社がクラウド向けに出してきているセキュリティ製品をどううまく活用していくのか、いうなれば「顧客が本当に必要だったもの」をきちんと構築できるようになりましょう、というメッセージを強く感じました。 新しい技術・製品がどんどん出てくる中で、適切な技術選択を行うのはとても難しいことですが、難しいからこそ面白い分野でもあると再確認できてとても良かったと思います。

クックパッドでは技術を用いてセキュリティを高め最高のサービスを提供することに興味のある仲間を募集しています。

セキュリティエンジニア

*1:もっとも、建物自体が巨大で、規模感としては東京国際展示場の各展示棟を一つにまとめたくらいをイメージすると近いのではないかなと思います

*2:搭乗時間と時差がちょうど一致

*3:動画で18:30頃から出てくる図を見るとわかりやすいかと思います

*4:https://aws.amazon.com/jp/blogs/compute/aws-fargate-price-reduction-up-to-50/

*5:一人でも参加可能なようでした

インフラのコスト最適化の重要性と RI (リザーブドインスタンス) の維持管理におけるクックパッドでの取り組み

$
0
0

技術部 SRE グループの mozamimyです。

クックパッドでは、 SRE が中心となって、サービスを動かす基盤の大部分である AWS のコスト最適化を組織的に取り組んでいるため、今回はそれについてご紹介します。

前半では、そもそもの話として「なぜコスト最適化が重要なのか」「何が難しいのか」「何をすべきなのか」といったことを述べます。これは、当たり前すぎて逆に陽に語られることが少ない (とわたしは感じています) トピックで、一度しっかり言語化しておいてもいいかなと考えたからです。内容のほとんどはわたしの脳内ダンプで、クックパッドという会社のコンテキストや組織としてのステージが前提になっているため、大多数の組織について当てはまる内容とは限りません。

後半では、コスト最適化の一例として、リザーブドインスタンス (以下 RI と略記) を維持管理するためのフローと、それを支えるモニタリングシステムについて述べます。こちらは AWS を利用するいかなる組織においても、今日から使えるテクニックとなるでしょう。もしかすると、似たような仕組みを整えている組織もあるかもしれません。

パブリッククラウドのコスト最適化の重要性

オンプレミスであれクラウドであれ、インフラにかかるコストを最適化し、できる限り無駄のない状態を保つことが重要なのは言うまでもないでしょう。とりわけクラウドでは、必要なときに必要な分だけお金を対価にリソースを得られる反面、簡単な操作でリソースを増やすことができるため、気づいたときには大きな無駄が発生していたという自体も起こりえます。

組織において、お金という限られたリソースは大変重要かつ貴重なものです。たとえば、インフラにかかるコストを年間 $10,000 節約できたとすれば、その $10,000 は投資やその他の重要な部分に回すことができます。インフラコストは投資と違って単に失われるだけなので、エンジニアリングリソースを割いて最適化することは、十分に理にかなっているでしょう。クラウドであれば、API を通してリソースを操作できるため、ソフトウェアエンジニアリングで解決できる部分が大きいです。

コスト最適化に見て見ぬふりをしていると、じわじわと組織の首を絞めていきますし、節約できていれば本来有意義に使えたお金が失われることになります。キャッシュが枯渇してから慌てていては、もう手遅れです。

コスト最適化を考えるにあたって、以下の 2 つの軸が存在するとわたしは考えています。

  • リソースプールのコスト最適化: RI を適切に保つ、スポットインスタンスを積極的に利用する、など。
  • リソースプールの利用に対するコスト最適化: 各サービスで利用しているインフラのキャパシティ (ECS サービスのタスク数、RDS インスタンス、ElastiCache Redis/Memcached クラスタなど) が、適切にプロビジョン・スケーリングしていて無駄遣いしていない状態に保つこと。

コスト最適化の基本的なポイント

たとえば AWSだと、以下のような取り組みがあげられます。これらは AWS の公式ページにも書かれています。

  1. スポットインスタンスを積極的に利用する。
  2. RI の購入によってある程度のキャパシティを予約・前払いし、オンデマンドインスタンスやマネージドサービスを割安で利用する。
  3. Cost Explorer や Trusted Advisor といったサービスを利用してオーバープロビジョニングなリソースを発見し、インスタンスサイズやその数を適切に保つようオペレーションする。

これらのうち、クックパッドでは 1 番と 2 番、すなわちリソースプールそのもののコスト最適化については非常に熟れており、理想に近い状態を維持することができています。3 番のリソースプールの利用については、これからも継続的に取り組んでいくべき課題となっています。

1. スポットインスタンスの積極的な利用

コンテナオーケストレーション基盤としてクックパッドでは早くから ECS を採用しており、SRE の鈴木 (id:eagletmt) による Cookpad Tech Kitchen #20での「Amazon ECSを安定運用するためにやっていること」という発表にあるように、スポットインスタンスを利用した ECS クラスタを安定して運用できています。Rails を始めとする HTTP をしゃべるアプリケーションサーバなど、状態を持たないサービスのほとんどは、スポットインスタンスで構成された ECS クラスタで動いています。これは、コストの最適化に大きく寄与しています。

2. RI (リザーブドインスタンス) の運用

このトピックは、記事の後半で述べるためここではいったん隅に置いておきます。

3. キャパシティを過不足ない適切な状態に保つ

文字で書いただけでは「いいじゃんやろうよ」という感じなのですが (ですよね?)、実際にはその道のりは非常に険しいです。たとえば...

  1. インスタンスサイズの変更のためにどうしても停止メンテナンスが必要になったらどうするか? 開発チームとの《調整》が発生。
  2. アプリケーションの改善でコストを最適化できる場合 (たとえばスロークエリを改善するなど) だと、SRE よりもそのアプリケーションを普段から触っていてドメイン知識のある開発チームが対応したほうが早い。
  3. SRE の限られた人的リソースですべてのインフラのリソースの状況をウォッチして対応し続けるのは組織の拡大に対してスケールしない。

といった問題があります。

これは、SRE という概念が解決しようとしてる問題に密接に関連しています。Google による SRE 本にも、コスト最適化のためのオペレーションはトイルとして扱われています。

エラーバジェットや信頼性といった真髄からはやや外れるため、コスト最適化に SRE という概念を絡めることには議論があるかもしれません。ただ、わたしは場当たり的なコスト最適化は技芸だと思っていて、それをエンジニアリングで解決することは立派な SRE way だし、SLO を守れる範囲でコストを切り詰めていくことは信頼性に深く関わることだと信じています。トイルをなくそうという SRE の方針とも合致しますね。

この問題はクックパッドの SRE で取り組んでいる、開発チームへの権限と責任の移譲が進み、適切にコストをモニタリングできる仕組みが整えば解決できる見込みがあります。少し前の記事ですが、権限と責任の移譲については、クックパッドの開発基盤、インフラ環境での開発で心がけているラストワンマイルでも触れられています。

各開発チームが、自分たちのサービスにかかっているインフラコストを把握できるようなり、自律的にリソースプールの利用を最適化できると、組織の拡大に対してスケールするようになるでしょう。開発チーム自身の手で実装起因の無駄を改善することができ、スケールダウンのための停止メンテナンスが必要になった場合でも、チーム内で調整を完結して行うことができます。

SRE はリソースプールそのものの最適化と、リソースプールの利用をモニタリングできる仕組みを提供することに専念し、必要に応じて開発チームを手伝うことはありますが、基本的には開発チームにコスト最適化の責任と権限を持ってもらうのです。

また、プロジェクトや部署ごとに、どの程度サービス運用のためのインフラコストがかかっているかを把握できるようなダッシュボードを作ることができると、財務管理上でもメリットになるでしょう。まるっと「インフラ代金」として予算管理しているものが、開発チームや部署、プロジェクトごとに予算を細かく設定し、追跡することができるのです。財務などのバックオフィスもインフラコストの状況を追跡できるようにしておく重要性はWhitepaper: The guide to financial governance in the cloudでも触れられています。

リソースプールの最適化に限界が見え始めた今、やるべきこと

SRE が責任として持っている、スポットインスタンスの利用推進や RI の適切な運用によって、開発チームが利用するリソースプール自体のコスト最適化は限界に近づきつつあります。次にやるべきことは、リソースプールの利用を最適化していくことで、これは組織全体として取り組めるように SRE がエンジニアリングで解決していく必要があります。

リソースプールの利用に対するコスト最適化はこれからの課題として、後半では、リソースプール自体のコスト最適の取り組みの一つとして、RI の維持管理のフローと、それを支えるモニタリングシステムについて説明します。

クックパッドにおける RI (リザーブドインスタンス) の維持管理と対応フロー

RI の費用削減効果を最大限に発揮させるためには、その状況の変化を察知して、以下のようなオペレーションによって理想の状態に戻るようにメンテナンスし続けるのが一般的です。

  • RI の追加購入
  • 動いているインスタンスタイプの変更
  • RI の exchange (2019-08-14 現在、convertible な EC2 の RI のみ可能)

これは CPU 利用率やキューの待ち行列の長さといったメトリクスに基づくキャパシティのスケールアウト・スケールインと似ています。RI の状況がしきい値を割ったときに、上述のリストにあげたようなオペレーションによって、理想的な状態に戻るようにメンテナンスし続けるのです。

クックパッドでは、後述の ri-utilization-plotter や github-issue-opener といった Lambda function を利用して、以下のような GitHub issue を使った対応フローをとれるようにしています。

  • RI の状況を監視し、変化があったことを検知してアラートをあげる。
  • アラートがあがると、自動的に GitHub のリポジトリに issue が作成される。
  • RI の追加購入などのオペレーションをする。
  • RI の状況が元に戻ったことを確認して issue を閉じる。

RI の状況ベースでの監視は、実際に状況が変わってからしか検知できないため、対応が後手に回ってしまいがちという弱点があります。RI の追加購入や exchange の際には、いくつ買っていくつ exchange するのかというプランを練ってチーム内でレビューする必要があるため、その作業が終わるまでは RI による費用削減効果が減ってしまうことになります。

少なくとも RI の失効は事前に知ることができるため、上述の RI の状況をベースとした監視に加え、以下のような対応フローも整えています。

  1. RI の失効が発生する 7 日前に PagerDuty の incident を発行してアラートをあげる。
  2. RI の追加購入を検討・実施し、incident を resolve 状態にする。

これらの仕組みにより、RI を理想に近い状態で運用することができています。

実際の対応フロー

百聞は一見にしかずということで、この仕組みを使った Grafana によるダッシュボードや、実際の対応フローを見てみましょう。

この仕組みでは、RI の状況は CloudWatch のカスタムメトリクスに蓄積されるため、以下のように Grafana から一覧することができます。

f:id:mozamimy:20190813143925p:plain
Grafana から確認できる CloudWatch カスタムメトリクスに集積された RI の状況

ちなみに、Cost Explorer API から取得できる値は、月に一回程度、変な値を示してしまう場合があるのですが、頻度も少ないですしそれは許容しています。このスクリーンショットでは、左上のグラフの谷になっている箇所ですね。

実際に RI の状況が変化してメトリクスがしきい値を割ると、GitHub に issue が自動的に立って場が作られ、そこで対応することになります。

f:id:mozamimy:20190813144027p:plain
RI の監視システムによってあがったアラートに GitHub 上で対応している様子

このスクリーンショットのシナリオでは、Amazon ES の RI カバレッジがしきい値を割ったことが検知されており、わたしがその原因を調べてコメントし、オペレーションによる一時的なものであることを確認して CloudWatch Alarm のしきい値を調整し、メトリクスがもとに戻ったことを確認して issue をクローズとしました。別のケースでは、RI を追加で購入したり exchange したりといったオペレーションをしたこともありました。

さて、ここからは実装の話に移ります。以下のトピックについて説明していきましょう。

  • RI の状況を知る
  • RI の状況が変わったときにアラートをあげる
  • RI の失効が近づいたときにアラートをあげる

RI (リザーブドインスタンス) の状況を知る

RI がどのような状況にあるかは、RI 利用率RI カバレッジによって知ることができます。

RI 利用率は、購入した RI がどの程度有効になっているか、すなわちどの程度余らせることなく有効に使えているかを示す割合です。RI カバレッジは、オンデマンドで動いているインスタンスの総量に対して、RI によってどの程度カバーされているかを示す割合です。

RI 利用率は 100% を維持できている状態が望ましいです。RI カバレッジも同様に、できる限り 100% に近い状態を保っているのが理想です。しかしながら、ある時刻において RI カバレッジが 100% になるように買ってしまうと、将来的な利用の変化に対応することが難しくなります。アプリケーションの改善により必要なキャパシティが減ったり、EC2 であれば、スポットインスタンス化やマネージドサービスに寄せることで RI が余るといったことも起こるでしょう。あまり考えたくはないですが、サービスのクローズによって不要になるリソースもあるかもしれませんね。そこで、ターゲットとなるカバレッジを、たとえば 70% などと決めておき、その値を下回らないように RI をメンテナンスするのがよいでしょう。

RI 利用率や RI カバレッジは Cost Explorer のコンソールや API から確認でき、フィルタを駆使することでリージョンやサービスごとに値を絞り込めます。

f:id:mozamimy:20190813145616p:plain
Cost Explorer から確認する RI 利用率の様子

この画面からは利用率だけでなく、RI によってどの程度コストを節約できたのかといった情報も表示できます。また、左側のメニューにある Reservation summary には、RI の情報をサマライズしたビューが用意されています。

f:id:mozamimy:20190813145723p:plain
Cost Explorer で RI のサマリを確認する

この画面では、すべてのサービスを横断した RI のサマリを確認でき、30 日以内に失効する RI も確認することができて便利です。

Cost Explorer の基となっているデータ (RI の情報だけでなく、請求に関するすべての情報を含む) は AWS Cost and Usage Report (以下 CUR と略記) によって S3 バケットに出力することができ、Athena などでクエリすることで Cost Explorer では実現できないような集計結果を SQL によって自在に引き出すことができます。

クックパッドでは、CUR を Redshift Spectrum からクエリできるような仕組みがデータ基盤グループによって整備されており、Tableau を用いてダッシュボードを作ることも可能になっています。

RI (リザーブドインスタンス) の状況の変化に対してアラートを仕掛ける

ここまでの説明で、Cost Explorer やその API、CUR などを駆使することで、RI の状況を確認できることがわかりました。次はそれらのメトリクスに対してアラートを仕掛けることを考えていきましょう。

なるべく AWS 標準の機能で済ませたい場合は、AWS Budgetsが利用できます。reservation budget をしきい値とともに作成し、アラートの設定に SNS トピックやメールアドレスを設定することで通知を HTTP エンドポイントや Lambda function 、特定のメールアドレスに送ることができます。試していませんが、少し前のアップデートによって、AWS Chatbot を利用して Slack や Chime に簡単に通知できるようになったようです。

Launch: AWS Budgets Integration with AWS Chatbot | AWS Cost Management

これはこれで便利ですが、以下の理由で今回の要件を満たせないと考え、自前で仕組みを用意しようと判断しました。

  • reservation budget を daily で判定するように設定すると、しきい値を割っている間、毎日通知が届いてしまう。
  • 通知の細かい制御ができない。たとえば「3 日以上しきい値を割り続けていたら」のようなアラートを設定できない。
  • メールや Slack の通知だけでは見落としが発生する可能性が高く、後半の冒頭で述べたような対応フローを実現できない。

今回は、ri-utilization-plotter という Lambda function を中心とした、以下のような仕組みを構成しました。仕組みとしては非常に素朴ですが、よく動きます。

  1. 12 時間ごとに起動する Lambda function から、Cost Explorer API をたたき、取得した RI 利用率と RI カバレッジを CloudWatch のカスタムメトリクスにプロットする。
  2. そのカスタムメトリクスに CloudWatch Alarm を仕掛け、しきい値を割ったときに SNS topic 経由で Lambda function を起動し、GitHub の SRE のタスクをまとめるためのリポジトリに issue を立てる。

ちなみに、CloudWatch にメトリクスを寄せることで、Grafana を使って Prometheus といった他の様々なメトリクスと組み合わせたダッシュボードを作ることもできて便利というメリットもあります。

実際には、たとえば Redshift の RI はデータ基盤グループが管理しているため、SRE 用とは別のリポジトリに issue を立てるようにするなど、もう少し複雑な構成になっています。そのため、上記の構成図はエッセンスを抜き出したものとなっています。

ri-utilization-plotter は、AWS サーバーレスアプリケーションモデル (以下 SAM と略記) に則り、CloudFormation stackを aws-sam-cliを利用してデプロイしています。

メトリクスをプロットする ri-utilization-plotter と github-issue-opener の stack が分かれているのは、github-issue-opener は「SNS トピックに届いた通知をもとに GitHub に issue を作成する便利 Lambda function」として、ri-utilization-plotter からだけでなく、汎用的に利用できるように実装しているからです。このような構成をとることにより、他の Lambda を中心としたアプリケーションで GitHub Appを用いたコードを自前で実装しなくても、SNS トピックに通知するというシンプルな操作だけで issue を立てることができるようになっています。

それでは、ri-utilization-plotter と github-issue-opener の中身を見ていきましょう。

ri-utilization-plotter

ソースコードはこちらにあります。https://github.com/mozamimy/ri-utilization-plotter

template.example.yml に記述されている RIUtilizationPlotter という論理名を持つ AWS::Serverless::Functionリソースが本体で、make コマンドを経由して SAM CLI を実行して手元で実行できるようになっています。

この function は、event.example.json にあるように、

{"region": "us-east-1",
  "service": "Amazon ElastiCache",
  "linked_account": "123456789012",
  "granularity": "DAILY",
  "namespace": "RIUtilizationTest",
  "metric_name": "UtilizationPercentage",
  "ce_metric_type": "utilization"
}

のようなイベントを、CloudWatch Events から受け取ります。namespace に CloudWatch カスタムメトリクスのネームスペースを指定し、metric_name にメトリクスの名前を指定します。この例では RI 使用率をプロットする設定になっていますが、metric_name を CoveragePercentage のようにし、ce_metric_type を coverage にすると、RI カバレッジもプロットすることができます。ce_metric_type は Cost Explorer API のオプションとして渡す文字列で、utilization (RI 使用率) と coverage (RI カバレッジ) に対応しています。

上述のリポジトリには開発時にテスト用途として SAM CLI を利用することが前提の CloudFormation テンプレートしか含まれていません。これは意図したもので、Lambda function のソースコードと AWS の実環境へのデプロイの設定を分離するためにこうなっています。したがって、AWS の実環境にデプロイする場合には、そのための CloudFormation テンプレートを別途用意する必要があり、これは社内固有の設定も含むため社内のリポジトリで管理されています。

社内で管理されている実環境用の CloudFormation テンプレートは、Jsonnetを利用して JSON に変換することで生成しています。

たとえば、この場合だとリージョンとメトリクスのタイプ (utilization/coverage) の組み合わせごとに CloudWatch Event ルールが必要となるため、以下のように event.ruleという関数を定義することで DRY になるようにしています。./lib/event.libsonnet というファイルに event.rule の定義を出力する rule という関数を作り、CloudFormation テンプレートの Resourcesの中でそれを呼び出すイメージです。

また、実行ファイルを含む zip ファイルは GitHub の release にあるため、デプロイ時に https://github.com/mozamimy/ri-utilization-plotter/releases/download/v1.0.0/ri-utilization-plotter.zipから curl でダウンロードし、jsonnet コマンドを実行するときに jsonnet template.jsonnet --ext-str codeUri=${PWD}/tmp/ri-utilization-plotter-v1.0.0.zipのようにオプションを与え、std.extVar('codeUri')のようにして実行可能ファイルを含む zip ファイルのパスを差し込むようにしています。

{
  AWSTemplateFormatVersion: '2010-09-09',
  Transform: 'AWS::Serverless-2016-10-31',
  Resources: {
    MainFunction: {
        // 省略
        Properties: {
            CodeUri: std.extVar('codeUri'),
            // 省略
        },
    },
    DLQ: {
        // 省略
    },
    EventRuleUtilizationApne1: event.rule(alias, 'ap-northeast-1', 'utilization'),
    EventRuleUtilizationUse1: event.rule(alias, 'us-east-1', 'utilization'),
    EventRuleCoverageApne1: event.rule(alias, 'ap-northeast-1', 'coverage'),
    EventRuleCoverageUse1: event.rule(alias, 'us-east-1', 'coverage'),
  },
}

Jsonnet はパワフルなテンプレート言語で、クックパッドでは ECS を使ったアプリケーションのデプロイを簡単に行うためのツールである Hakoでも採用されています。

パワフルがゆえに何でもできてしまうという側面もあり、用法用量を間違えると複雑怪奇な設定ファイルができてしまうというダークサイドもありますが、個人的にはとても気に入っています。実は JSON は YAML として評価しても合法なため、設定ファイルに YAML を用いるソフトウェアでも Jsonnet を使うことができます。

github-issue-opener

ソースコードはこちらにあります。https://github.com/mozamimy/github-issue-opener

これは SNS topic で受けた通知をもとに GitHub に issue を立てる Lambda function です。

ISSUE_BODY_TEMPLATEISSUE_SUBJECT_TEMPLATEといった環境変数に issue のタイトルや本文の内容を Go のテンプレートして記述することができます。たとえば、{{.Message}}のように設定した場合、SNS メッセージのメッセージがそのままレンダリングされます。 https://github.com/mozamimy/github-issue-opener/blob/master/template.example.jsonを見れば、雰囲気が掴めるでしょう。

通知を送る側から issue のタイトルや本文を指定したい場合は、message attribute が使えます。https://github.com/mozamimy/github-issue-opener/blob/5699d16606c4da13edc40c2c674c7110aaec43e7/event.example.json#L18-L42を見れば雰囲気が掴めるかと思います。

SNS メッセージの本文に JSON 文字列が入っていて、それをパースして issue テンプレートで利用したい場合は、PARSE_JSON_MODE環境変数を 1 に設定することで実現できます。その場合、

{"Foo": "bar"
}

のような JSON 文字列がメッセージ本文に含まれていた場合、テンプレートからは {{.ParsedMessage.Foo}}のようにして値を取り出してレンダリングすることができます。

こちらも ri-utilization-plotter と同様に、AWS 実環境へのデプロイ用の CloudFormation テンプレートは社内のリポジトリで Jsonnet として管理されています。

issue テンプレートは

# RI Utilization Alert

{{.ParsedMessage.NewStateReason}}

- **Alarm Name**: {{.ParsedMessage.AlarmName}}
- **Namespace**: {{.ParsedMessage.Trigger.Namespace}}
- **Metric Name**: {{.ParsedMessage.Trigger.MetricName}}
{{- range .ParsedMessage.Trigger.Dimensions }}
  - **{{.name}}**: {{.value}}
{{- end }}

Please check current utilization and fix it. 

のような形で ./issue_template/body_ri_notify.md.tmpl に保存されており (一部実際のものから変更を加えています)、CloudFormation テンプレートからは

{
  AWSTemplateFormatVersion: '2010-09-09',
  Transform: 'AWS::Serverless-2016-10-31',
  Resources: {
    MainFunction: {
      // 省略
      Environment: {
        Variables: {
          // 省略
          ISSUE_BODY_TEMPLATE: importstr './issue_template/body_ri_notify.md.tmpl',
        },
      },
    },
  },
}

のような形で、Jsonnet の importstr でファイルを読んだそのままを ISSUE_BODY_TEMPLATE環境変数に設定しています。

RI (リザーブドインスタンス) の失効が近づいたときにアラートをあげる

ri-utilization-plotter のように、RI の失効が近づいたときにアラートをあげる仕組みを内製しようとしていましたが、AWS Cost Explorer で予約の有効期限切れアラートを提供開始にあるように、Cost Explorer 自体に 60 日前、30 日前、7 日前、当日にメールを送信する機能が実装されたため、それを利用することにしました。

ただし、単にメールを送るだけだと、後半の冒頭で述べたような対応フローをとることができません。見逃してしまうこともあるでしょう。

そこで、今回は PagerDuty の email integration のメールアドレスをこの通知に設定することで、low urgency でアラートを飛ばし SRE の誰かにアサインされるようにしました。週次のミーティングでも low urgency に設定されたアラートが残っていないか振り返りを行っているため、作業が漏れることもありません。万が一漏れた場合でも、ri-utilization-plotter を中心とした仕組みにより、後手になりつつも異常に気づくことができます。

RI (リザーブドインスタンス) の購入・exchange プランを練るときの細かい話

RI を追加購入したり exchange したりする場合は、現在どの程度オンデマンドインスタンスの利用があるのかを把握して、プランを立てる必要があります。

その際には、Cost Explorer の画面で、過去何日間かを指定してインスタンスタイプごとの RI 利用率や RI カバレッジを確認したり、今この瞬間に動いているインスタンスの一覧を得るために https://github.com/mozamimy/toolbox/blob/master/ruby/list_on_demand_ec2_instances/list.rbのようなスクリプトを利用したりしています。このスクリプトは、bundle exec ruby list.rb t3のように実行すると、t3 ファミリの EC2 インスタンス一覧を出した上で、

Total instance count: 49
Normalized total count: 110.0
    as t3.small 110.0
    as t3.large 27.5

のように、トータルで何台オンデマンドインスタンスが動いているのかと、.small および .large に正規化した台数を算出します。

現状はこれらの値をもとに SRE が職人の技で丹精を込めてプランを立てていますが、将来的には自動化したいと考えています。素朴にやろうとするとすべての購入プランを網羅して最適な解を選ぶことになりますが、それはおそらく現実的ではありません。問題を適切にモデル化して既存のアルゴリズムを利用するなど、賢くやらねばなりません。ある意味で競技プログラミングのようですね。

まとめ

前半ではパブリッククラウドのコスト最適化についての考えを述べ、後半では、その取り組みの一部である RI の維持管理のための仕組みについて説明しました。

RI は、スポットインスタンスの積極的な利用に次ぐコスト最適化の強力な武器ですが、日々の手入れを怠るとその力を十分に発揮することができません。この記事では、クックパッドではその武器をどのように手入れしているかということについて説明しました。RI の管理にお悩みの方は、参考にしてみてはいかがでしょうか。

Google Play Billing Client 2.0における消費型商品の決済の承認(acknowledgement)について

$
0
0

ユーザ・決済基盤部の宇津(@uzzu)です。

クックパッドでは複数のAndroidアプリでGoogle Play決済(定期購読、消費型商品)を利用しており、 ユーザ・決済基盤部ではそれらのアプリの決済情報を取り扱う共通決済基盤サービスクライアントライブラリを日々開発しています。 直近ではGoogle I/O 2019にて発表されたGoogle Play Billing Client 2.0にも対応し、Cookpad.apk #3のLT枠にてどのように対応していったか発表させて頂きました。

speakerdeck.com

本記事では同発表にて時間が足りず深堀りできなかった、消費型商品における決済の承認(acknowledgement)対応について解説します。 スライドと合わせて読んで頂ければ幸いです。

消費型商品における2.0とそれ以前との違い

2.0以前の消費型商品の購入フローは概ね以下の図のようになっていたかと思います。

f:id:himeatball:20190815060223p:plain
2.0以前の購入フロー

2.0からはこれに加えて、決済の承認が必要になります。 Google Play決済自体は決済処理時に走る(Pending Purchaseを除く)のですが、3日以内に開発者が決済の承認を行わない場合返金されます。 通信断や障害等で購入フローが正常に完了せず商品が付与されなかったユーザが自動的に救済されるようになるのは、サポートコスト削減の面でも非常に良いですね。

f:id:himeatball:20190815060447p:plain
2.0での購入フロー

一見、購入フローに処理が1ステップ追加されただけのように見えます。加えてリリースノートにも

For consumable products, use consumeAsync(), found in the client API.

とあるように、アプリ上でconsumeAsyncを呼び出す事で消費(consume)しつつ決済の承認も行われるので、図に追加した⑤については特にやる事はないのでは?と思われた方もいるかと思います。 しかしながら、商品付与が行われるタイミングにおける決済の承認状態は2.0では未承認、それ以前では承認済という違いがあり、 この違いによってアプリ改ざんに対するリスクを考慮する必要性がでてきます。

consumeAsyncを呼び出さないようにアプリを改ざんされる事を想定した場合、購入処理を実施すると以下のように処理されます。

  1. 消費型商品の購入ボタンを押す
  2. 購入フローに則り商品が付与される
  3. consumeAsyncを呼び出さない為消費が行われないが、商品は既に付与されている(決済が未承認の間、商品は消費されない為、再購入はできない)
  4. 3日後、決済が未承認の為返金される
  5. 返金されると消費型商品が再度購入可能になる
  6. 1に戻る

つまり、2.0以前の購入フローの実装のまま愚直に2.0対応してしまうと、アプリ改ざんによって3日毎に消費型商品を無料で取得する事ができてしまいます。

対策A: サーバサイドで決済を承認する

決済の承認はレシート検証同様にサーバサイドで行いたいという需要に応えるように、Purchases.products: acknowledge
が用意されています。 クックパッドの共通決済基盤サービスではこれを利用して決済を承認しユーザと決済情報の紐付けが正常に行われた上で、各サービスで商品の付与ができる状態とするようにしています。 商品付与後、アプリ上でconsumeAsyncします。

この対策方法はアプリ改ざんに対するリスク、及び決済の承認に関連するアプリ上での購入フローの実装が2.0とそれ以前とで変わらないのが利点です。 ただし、クックパッドのような決済サービスと商品を販売しているサービスが分離されている環境下においては、 決済状態と商品付与状態の整合性の担保ができている前提での対策方法になると考えています。 クックパッドの共通決済基盤サービスにおける整合性についての取り組みは以下の記事を参考にしてください。

https://techlife.cookpad.com/entry/2016/06/01/070000

加えて決済を承認するタイミングについて、商品を付与した上で決済を承認するか、あるいはレシート検証を終えた段階で一旦決済を承認した後に商品を付与するかを検討するかと思います。

クックパッドの共通決済基盤サービスではどうしているかというと、消費型商品においては購入フローの完了処理である所の消費処理がアプリ上でしか実施できない為、購入処理の冪等性を担保できるよう後者を選択しています。 定期購読においてはGoogle Play決済を終えた以降の購入フローをサーバサイドで完結できる為、前者で且つレスポンスタイムを上げる為にJob Queueで非同期に決済の承認を実施しています。

対策B: アプリ上で決済の承認を実施してからサーバにレシートを送信し、サーバ間通信で決済の承認状態を検証する

Billing Client Libraryに決済の承認をするためのメソッド(BillingClient#acknowledgePurchase)が用意されています。 Google Play決済を実施後にこれを呼び出してまず承認してしまい、その上でサーバにレシートを送信し、サーバサイドでPurchases.products: getを呼び出してacknowledgementStateを確認し、 承認済か否かを検証した上で商品を付与した後、アプリ上でconsumeAsyncするような購入フローにします。

この対策方法ではアプリ上に実装されている購入フローはもちろん、通信断等で滞留した決済の再開処理にも手を入れる必要がある為、対策Aよりは手がかかるものの、 サーバ間通信で決済の承認状態を検証する為、対策A同様に介入される余地はないと考えています。

その他の対策方法

例えばdeveloper notificationを頼りに商品を付与する方法があるかと思いますが、developer notificationは現在定期購読のみサポートしているのと、 仮にサポートされるようになったとしても、消費型商品において大半のユーザは購入完了したら遅延なくすぐに商品を使用したい為、 その仕組みを整えるのはそれなりに開発コストがかかりそうです。

決済処理フローはそのままにアプリ改ざん対策に本腰を入れていくとしても、アプリ改ざん対策はいたちごっこになってしまう為、運用コストの増大が予想されます。 素直に前述の対策Aないし対策Bを適用するのが良さそうです。

まとめ

本記事ではGoogle Play Billing Client 2.0における消費型商品の決済の承認(acknowledgement)について解説しました。 弊社において利用していない機能もあり(定期購読のupgrade/downgrade等)、決済の承認に関する網羅的な解説とまではなっていないですが、 Google Play Billing Client 2.0導入の手助けとなれば幸いです。

クックパッドではアプリ内課金をやっていくエンジニアを募集しています

UICollectionViewでページングスクロールを実装する

$
0
0

 こんにちは。新規サービス開発部の中村です。普段は「たべドリ」アプリの開発をしています。「たべドリ」は料理の学習アプリです。詳細はこちらの記事をご覧ください。本記事では UICollectionView でページングスクロールを実装する方法について解説します。

概要

f:id:nkmrh:20190807175935p:plainf:id:nkmrh:20190807175941p:plainf:id:nkmrh:20190807175952p:plain

 上記画像が今回解説する iOS アプリのUIです。左右のコンテンツが少し見えているカルーセルUIで、以下の要件を満たすものです。

  • 先頭にヘッダーを表示する
  • セルが水平方向にページングスクロールする

色々な実装方法があると思いますが、今回はヘッダーがあるため複数の異なる幅のViewを表示させながら、ページングスクロールを実現する方法を解説します。実装のポイントは以下の2点です。

  • UICollectionViewFlowLayoutのサブクラスを作成しtargetContentOffset(forProposedContentOffset:withScrollingVelocity:)メソッドをオーバーライドしてUICollectionViewcontentOffsetを計算する
  • UICollectionViewdecelerationRateプロパティに.fastを指定する

以降、実装方法の詳細を解説していきます。

ベースとなる画面の作成

 まずベースとなる画面を作成します。UICollectionViewControllerUICollectionViewFlowLayoutを使いヘッダーとセルを表示します。UICollectionViewFlowLayoutscrollDirectionプロパティは.horizontalを指定し横スクロールさせます。ヘッダーとセルのサイズ、セクションとセルのマージンは任意の値を指定します。

ViewController.swift

overridefuncviewDidLoad() {
    ... (省略)
    flowLayout.scrollDirection = .horizontal
    flowLayout.itemSize = cellSize
    flowLayout.minimumInteritemSpacing = collectionView.bounds.height
    flowLayout.minimumLineSpacing =20
    flowLayout.sectionInset = UIEdgeInsets(top:0, left:40, bottom:0, right:40)
}
    
... (以下 UICollectionViewDataSource は省略)

f:id:nkmrh:20190807174701g:plain

ここまではUICollectionViewControllerの基本的な実装です。

isPagingEnabled プロパティ

 ページングスクロールさせたい場合、最初に試したくなるのがUIScrollViewisPagingEnabledプロパティをtrueに指定することですが、この方法ではセルが画面の中途半端な位置に表示されてしまいます。これはcollectionViewの横幅の単位でスクロールされるためです。この方法でもセルの幅をcollectionViewの横幅と同じ値に設定し、セルのマージンを0に指定することで画面中央にセルを表示させることが可能です。しかし、今回はセルの幅と異なる幅のヘッダーも表示させる必要があるためこの方法では実現できません。

f:id:nkmrh:20190807174812g:plain

セルを画面中央に表示する

 そこでUICollectionViewFlowLayoutのサブクラスを作成しtargetContentOffset(forProposedContentOffset:withScrollingVelocity:)メソッドをオーバーライドします。このメソッドはユーザーが画面をスクロールして指を離した後に呼ばれます。メソッドの戻り値はスクロールアニメーションが終わった後のcollectionViewcontentOffsetの値となります。このメソッドを以下のように実装します。

FlowLayout.swift

overridefunctargetContentOffset(forProposedContentOffset proposedContentOffset:CGPoint, withScrollingVelocity velocity:CGPoint) ->CGPoint {
    guardletcollectionView= collectionView else { return proposedContentOffset }

    // sectionInset を考慮して表示領域を拡大するletexpansionMargin= sectionInset.left + sectionInset.right
    letexpandedVisibleRect= CGRect(x:collectionView.contentOffset.x- expansionMargin,
                                      y:0,
                                      width:collectionView.bounds.width+ (expansionMargin *2),
                                      height:collectionView.bounds.height)

    // 表示領域の layoutAttributes を取得し、X座標でソートするguardlettargetAttributes= layoutAttributesForElements(in:expandedVisibleRect)?
        .sorted(by: { $0.frame.minX <$1.frame.minX }) else { return proposedContentOffset }

    letnextAttributes:UICollectionViewLayoutAttributes?
    if velocity.x ==0 {
        // スワイプせずに指を離した場合は、画面中央から一番近い要素を取得する
        nextAttributes = layoutAttributesForNearbyCenterX(in:targetAttributes, collectionView:collectionView)
    } elseif velocity.x >0 {
        // 左スワイプの場合は、最後の要素を取得する
        nextAttributes = targetAttributes.last
    } else {
        // 右スワイプの場合は、先頭の要素を取得する
        nextAttributes = targetAttributes.first
    }
    guardletattributes= nextAttributes else { return proposedContentOffset }

    if attributes.representedElementKind == UICollectionView.elementKindSectionHeader {
        // ヘッダーの場合は先頭の座標を返すreturn CGPoint(x:0, y:collectionView.contentOffset.y)
    } else {
        // 画面左端からセルのマージンを引いた座標を返して画面中央に表示されるようにするletcellLeftMargin= (collectionView.bounds.width - attributes.bounds.width) *0.5return CGPoint(x:attributes.frame.minX- cellLeftMargin, y:collectionView.contentOffset.y)
    }
}

// 画面中央に一番近いセルの attributes を取得するprivatefunclayoutAttributesForNearbyCenterX(in attributes:[UICollectionViewLayoutAttributes], collectionView:UICollectionView) ->UICollectionViewLayoutAttributes? {
    ... (省略)
}

velocity引数をもとにユーザーが左右どちらにスワイプしたか、またはスワイプせずに指を離したかの判定をしています。スワイプしていない場合は画面中央に近いUICollectionViewLyaoutAttributesをもとに座標を計算します。ユーザーが左にスワイプした場合は取得したUICollectionViewLayoutAttributes配列の最後の要素、右スワイプの場合は最初の要素をもとに座標を計算します。これでセルを画面中央に表示できます。

f:id:nkmrh:20190807174938g:plain

 セルの位置は期待通りになりましたが、スクロールの速度が緩やかなのでスナップが効いた動きにします。UIScrollViewdecelerationRateプロパティを.fastに指定するとスクロールの減速が通常より速くなりスナップの効いた動作となります。

collectionView.decelerationRate = .fast

f:id:nkmrh:20190807175325g:plain

1ページずつのページング

 これで完成したように見えますが、スクロールの仕方によっては1ページずつではなくページを飛ばしてスクロールしてしまうことがあります。これを防ぎたい場合targetContentOffset(forProposedContentOffset:withScrollingVelocity:)メソッドで取得しているUICollectionViewLayoutAttributes配列の取得タイミングを、スクロールする直前に変更し、それをもとに座標を計算することで解決できます。以下のように実装を追加・変更します。

FlowLayout.swift

... (省略)

privatevarlayoutAttributesForPaging:[UICollectionViewLayoutAttributes]?

overridefunctargetContentOffset(forProposedContentOffset proposedContentOffset:CGPoint, withScrollingVelocity velocity:CGPoint) ->CGPoint {
    guardletcollectionView= collectionView else { return proposedContentOffset }
    guardlettargetAttributes= layoutAttributesForPaging else { return proposedContentOffset }

    ... (省略)
}

... (省略)

// UIScrollViewDelegate scrollViewWillBeginDragging から呼ぶfuncprepareForPaging() {
    // 1ページずつページングさせるために、あらかじめ表示されている attributes の配列を取得しておくguardletcollectionView= collectionView else { return }
    letexpansionMargin= sectionInset.left + sectionInset.right
    letexpandedVisibleRect= CGRect(x:collectionView.contentOffset.x- expansionMargin,
                                      y:0,
                                      width:collectionView.bounds.width+ (expansionMargin *2),
                                      height:collectionView.bounds.height)
    layoutAttributesForPaging = layoutAttributesForElements(in:expandedVisibleRect)?.sorted { $0.frame.minX <$1.frame.minX }
}
ViewController.swift

... (省略)

extensionViewController:UICollectionViewDelegateFlowLayout {
    overridefuncscrollViewWillBeginDragging(_ scrollView:UIScrollView) {
        letcollectionView= scrollView as! UICollectionView
        (collectionView.collectionViewLayout as! FlowLayout).prepareForPaging()
    }
    ... (省略)
}

UIScrollViewDelegatescrollViewWillBeginDragging(_:)が呼ばれたタイミングでprepareForPaging()メソッドを呼びます。このメソッドでスクロール直前のUICollectionViewLayoutAttributes配列をlayoutAttributesForPagingプロパティに保存しておき、targetContentOffset(forProposedContentOffset:withScrollingVelocity:)メソッドの中で保存した配列をもとに座標を計算するように変更します。これで1ページずつページングできるようになりました。

おわりに

 本記事では UICollectionView でページングスクロールを実装する方法を解説しました。このようなUIを実装することは稀だとは思いますが、何かの参考になれば幸いです。

サンプルプロジェクトはこちらhttps://github.com/nkmrh/PagingCollectionViewです。

料理のやり方を1から学んでみたいという方は、ぜひ「たべドリ」を使ってみて下さい!!

apps.apple.com

クックパッドでは新規サービス開発もやりたい、UI・UXにこだわりたいエンジニア・UXエンジニアを募集しています!!!

info.cookpad.com


Android cookpadLiveで採用してる技術 2019夏

$
0
0

メディアプロダクト開発部の安部(@STAR_ZERO)です。

Android cookpadLiveで採用してる技術について紹介したいと思います。

cookpadLiveとは

cookpadLiveは、料理上手な有名人や料理家がクッキングLiveを生配信しています。一緒に、Live配信でリアルタイムに料理が楽しめるアプリです。

ダウンロード: Android アプリiOS アプリ

ぜひ、ダウンロードしてLive配信を見てください!

基本環境

基本となる環境です

  • Kotlin
  • minSdkVersion 21
  • targetSdkVersion 28
  • AndroidX

特別な箇所はないですが、最新に追随するように努めています。

比較的新しいアプリなので、最初からすべてKotlinで記述されています。

targetSdkVersionについてはそろそろ29に対応する予定です。29にすることでの影響を調査している状況です。

Android Studio 3.5

Android Studio 3.5はbetaの段階から導入しています。理由としてはIncremental annotation processingを使いたかったためです。

cookpadLiveでは全面的にDataBindingを採用しているため、これの恩恵は非常に大きいものになります。 これまでは、レイアウトファイルを変更してビルドし直さなければコードが生成されず効率がよくありませんでした。3.5からはレイアウトファイルを変更すると同時にコード生成も行われるのでビルドによる待ち時間が減り効率よく開発できるようになりました。

Jetpack

現在、cookpadLiveではJetpackを積極的に採用しています。

DataBinding、LiveData、ViewModelについては、最初は使用されていなかったのですが、徐々に導入を進めて今では全面的に使用しています。

意外と便利だったのが、ViewModelをActivityに関連付けることでActivityとFragment間、それぞれのFragment間でのデータやイベントのやりとりが可能になる機能です。この機能のおかげ実装がだいぶ楽になったこともありました。 例えば、以下のようにActivityのイベントをFragmentでも受け取ることが簡単にできます。

class HogeActivity: AppCompatActivity() {
    privateval viewModel by lazy {
        ViewModelProviders.of(this).get(HogeViewModel::class.java)
    }
    fun someEvent() {
        // イベント発行
        viewModel.somaEvent.value = someValue // someEventはLiveData
    }
}

class HogeFragment: Fragment() {
    privateval activityViewModel by lazy {
        // thisではなく、Activityを指定することで共通のViewModelを使用できる
       ViewModelProviders.of(requireActivity()).get(HogeViewModel::class.java)
    }
    overridefun onActivityCreated(savedInstanceState: Bundle?) {
        activityViewModel.somaEvent.observe(viewLifecycleOwner) {
            // Activityのイベントを受信
        }
    }
}

Pagingはだいぶ癖があるライブラリですが、ちゃんと理解して使う非常に便利です。これも早い段階から導入しています。現状ではネットワークにはライブラリ側で対応はされてないですが、今後される予定らしいので、楽しみにしています。

Navigationについてはそこまで活用できないですが、部分的に使っています。全面的にSingle Activityにすることは考えてないですが、出来る箇所はFragmentに移行していきたいと考えています。また、SafeArgsは画面間の値の受け渡しが便利になるので積極的に使っていこうと思います。

RoomはDBまわりの処理には欠かせないくらい便利です。RoomはLivaData、RxJavaと簡単に連携することができるため、既存の実装に組み合わせることが簡単にできました。SQLも補完とシンタックスハイライトが効くので非常に助かります。

DI

DIについて Daggerを使用しています。こちらも最初は使用されてなかったのですが、徐々に導入をすすめました。

Daggerについては非常に難しい印象がある人が多いと思いますが、一度使うと便利すぎて手放せません。 DaggerでRepositoryクラスなど生成するようにして、あとは使いた箇所でInjectするだけです。ViewModelなどで必要なRepositoryが増えた場合も、生成するコードを意識せずパラメータに追加するだけ済みます。 最初の設定さえうまくやってしまえば、あとは楽になるはずです。

以前、部内でやったDagger勉強会のチュートリアルコードのリンクを貼っておきます。(まだ@Component.Factoryには対応してないです…)

STAR-ZERO/dagger-tutorial

AppSync

cookpadLiveではライブ中のコメントやハートなどのリアルタイム通信にAWSの AppSyncを使用しています。

f:id:STAR_ZERO:20190826153800p:plain:w300

この部分が一番特徴的かもしれません。

AppSyncはAWSのマネージド型GraphQLサービスです。 ユーザーが送信したコメントやハートをAppSync(GraphQL)のSubscriptionの機能を使い受信するようにしています。

ライブによって非常に多くのコメントやハートを受信することになりますが、受信のたびに画面に描画するのではなくRxJavaのbufferを使ってある程度まとめて画面に描画するようにしています。このあたりはうまくRxJavaと組み合わせて実装しています。

AppSyncの話は以下の記事の後半部分を見ていただければと思います。

CookpadTVのCTOが語る、料理動画サービス開発の課題と実装 - ログミーTech

その他ライブラリ

このあたりよく使われてるライブラリですね。これらももちろん活用しています。

設計

MVVM + Repositoryパターンを採用しています。Googleが公開してる Guide to app architectureとほぼ同じです。

元々はVIPERだったのですが、DataBindingやLiveDataとViewModelを導入していくと同時にMVVMに移行していきました。今ではすべてMVVMで実装されています。 私個人の経験からもJetpackを導入することで、開発効率と品質に大きく貢献することは明確だったので、これらを導入しました。

VIPERはAndroidではあまりの馴染みがないかもしれませんが、MVPパターンのようにInterfaceを使って各レイヤー間の処理を呼び出します。 すごく簡単な例ですが、以下のような感じです。(例ではViewとPresenterしか登場してないです)

// HogeView.ktinterface HogeView {
    fun show()
    fun hide()
}

// HogeFragment.ktclass HogeFragment: Fragment(), HogeView {

    privateval presenter by lazy { HogePresenter(this) }

    overridefun onCreateView(/** */): View? {
        // ...
    }
    overridefun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        presenter.fetch()
    }
    overridefun show() {
        // View.VISIBLEにする
    }
    overridefun hide() {
        // View.GONEにする
    }
}

// HogePresenter.ktclass HogePresenter(privateval view: HogeView) {
    fun fetch() {
        valdata == // ...if (data != null) {
            view.show()
        } else {
            view.hide()
        }
    }
}

このようにViewへの処理を呼び出すのにInterfaceを使ってPresenterからViewへの処理を呼び出しています。

一見、問題がなさそうですが、この時点で既に問題があります。例えば、Presenterでデータ取得中にActiivty/Fragmentが破棄された場合はどうなるでしょう。破棄されてるオブジェクトにアクセスすることになり、場合によってはクラッシュします。これはPresenterがActivity/FragmentのLifecycleについて何も知らないからです。これを解決するにはPresenter側に破棄されたことを教えてあげる必要があります。

では、これを今の実装で書き換えた場合です。

// HogeFragment.ktclass HogeFragment: Fragment() {

    privateval viewModel by lazy {
        ViewModelProviders.of(this).get(HogeViewModel::class.java)
    }

    privatelateinitvar binding: FragmentHogeBinding

    overridefun onCreateView(/** */): View? {
        // ...
    }

    overridefun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner
        viewModel.fetch()
    }
}

// HogeViewModel.ktclass HogeViewModel: ViewModel() {
    val isShow = MutableLiveData<Boolean>()

    fun fetch() {
        valdata == // ...
        isShow.value = data != null
    }
}
<!-- fragment_hoge.xml --><layout><data><variable name="viewModel"type="...HogeViewModel" /><import type="android.view.View" /></data><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="..."android:visibility="@{viewModel.isShow ? View.VISIBLE : View.GONE}" /></layout>

ここではDataBindingを使用していて、ViewModelが保持しているLiveDataとバインドしています。このLiveDataの値を変更すると自動的にView側にも反映される仕組みになっています。 ここで重要なのは、LiveDataはLifecycleのことを知っているので、Activity/Fragmentがアクティブな状態のときしかデータを流しません。そのため、さきほど説明したActivity/Fragmentが破棄されたときの対応を特別にせずとも問題が起こることがありません。またViewModelおいては回転でActivityが再生成されたときもViewModelは状態をもっているため、データ取得を中断することなく処理を継続することができます。

この設計にすることでActivity/FragmentはViewModelの状態を反映すれば良いだけになり、責務もしっかり分かれて見通しが良くなりました。また単純にVIPERはファイル数が多くなるため、コードを追う時にコードジャンプであちこち飛ばなければならず、個人的にはコードが追いにくい感じでした。

他にも様々な面で効率・品質を向上させるのに貢献してくれています。その他便利なJetpackライブラリも簡単に導入できるようになっています。

今ではすべての画面が同じような感じになってるので、どういう処理をしてるのかを理解しやすくなっています。

この設計変更ですが、新規画面については新しい設計でやり、既存については少しずつ進めていました。またUIを大きく変更するタイミングもあったので、その時に一気に直した箇所もありました。出来ることからコツコツやってこともあり、大きくコストをかけることなく変更できました。

CI環境

CIに関しては、Jenkinsを使っています。やってることは以下になります。

  • Pull Request
    • テスト、lintを実行
    • 社内テスト環境にAPKをアップロード
  • masterマージ後
    • 社内テスト環境にAPKをアップロード
    • Google Play内部テストへアップロード

可能な限り早い段階でリリース版をビルドして触ることで、問題があったときに早めに気づくことができるようにしています。特にProguardまわりは見落としがちになるを防げます。

リリースするときは内部テスト版を製品版へ昇格するだけになっています。今ここは手作業でやってるのですが、ChatOps等で出来るようにしたいと考えています。

マルチモジュール

現状では、スマホ、AndroidTV、FireTVで共有するようなモジュールと、featureモジュールをいくつか分割しています。

図にすると以下のような感じです。

f:id:STAR_ZERO:20190826154302p:plain:w300

  • core
    • 全モジュールで共通処理
    • APIアクセス、Repository、データモデルなど
  • appcore
    • スマホアプリ共通処理
    • 共通View、ログ、リソースなど
  • feature
    • 各機能を分割したモジュール
  • app
    • スマホアプリメイン
  • smarttv
    • TVアプリ共通処理
    • 共通View、ログ、リソースなど
  • androidtv
    • AndroidTVメイン
  • firetv
    • FireTVメイン

まだfetureモジュールは分割できる箇所があるので、少しずつでも進めていきたいと思います。

課題と今後

テスト

正直、まだそこまでうまく書けてる状況ではないので、なんとかしていきたいと思っています。 せっかくなので、ライブラリの選定から考えようとも思っています。Truth良さそうですね。

StyleとTheme

StyleとThemeについては結構ちらかってる状態なので、整理したい思っています。画面数もそこそこあるので、だいぶ大変な作業になる気配がしています。まずは、どういうふうに整理するかを検討してから少しずつやっていく感じになりそうです。

Navigation

前に書きましたが、まだまだ活用できる箇所があります。すべてSingleActivityとは考えてないですが、Fragmentでの遷移で良い箇所もあるので、そういった箇所に対応していきたいと考えています。

Coroutines

Coroutinesについては、どうするかを検討している段階です。現状でCoroutinesじゃないと困るような場面は出てきていませんが、JetpackもCoroutinesの対応が進んでいて実装するのに困ることはないと考えています。また、今後Coroutinesによって実装コストが下がるような機能なんかも出てくる可能性ありそうです。 メンバーと会話して、導入する気持ちはありますが、進め方やどこから導入するのかを考えています。

まとめ

cookpadLiveでは積極的にJetpackを使っていき、Googleが推奨しているやり方にどんどん乗っかっていっています。 今後もJetpackも改善されていくと思うので、それにいつでも追随できるような状態を保つようにしています。

これからもcookpadLiveでは新しい技術を積極的に取り入れていきますし、やりたいこともまだまだたくさんあります。

興味がある方いらっしゃいましたら、気軽にお声がけください。一緒に色々チャレンジしていきましょう。

info.cookpad.com

iOSDC Japan 2019 に社員2名がLT枠で登壇&ブース企画のご案内

$
0
0

こんにちは!広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。
「今年は梅雨が長いな」なんて思っていたらあっという間に暑くなり、気づけば本当の夏も過ぎ……。夏の終りの気配が見えてきましたね。

さて、iOSと周辺技術を題材としたカンファレンス、iOSDC Japan 2019が今年も9月5日(木)〜9月7日(土)に開催されますね!

クックパッドは、プラチナスポンサーをさせていただいており、ブースを出展いたします。今年は、@giginet@hiragramがLT枠で登壇し、@_sgr_ksmt@natmarkがiOSDCのスタッフとして関わってくれます。カンファレンスには、他にも多くの社員が参加いたしますので、会場でクックパッド社員をお見かけの際には、お声がけいただけますと嬉しいです。

参加予定社員一覧

@kanny, @slightair, @giginet, @hiragram, @_sgr_ksmt@natmark, @ichiko_revjune, @iceman5499, @sagaraya, @to9nariyui

登壇スケジュール

クックパッドの社員2名は、day2 - 9/7(土)に登壇いたします。 以下、スケジュールと登壇内容のご紹介です。

day2 - 9/7(土)

15:45〜 Track A @hiragram俺たちのARKitでめちゃめちゃ表情豊かなVTuber向け表情トラッカーを作るぞ

Animojiにも使われているTrueDepthカメラを使って3Dモデルの表情を動かす表情トラッカーを作りました。webカメラを用いて顔認識する他のシステムよりも精度高く、細かく、感情表現に必要な顔のパラメータを取得できるTrueDepthカメラの本気をお見せします。 表情トラッキングの精度以外にも、ARKitのおかげでバーチャルYouTuberを運用するにあたって地味に嬉しい機能をたくさん獲得しているので、プロデュースの現場の目線から面白おかしく紹介できればと思います。

16:55〜 Track A @giginet令和時代のゲームボーイ開発 👾

1989年に発売したゲームボーイは、今年30周年を迎えました。 そんな今だからこそ、実機で動くゲームボーイ開発をしてみましょう! 30年の時を経て、ゲームボーイが最新の技術で蘇ります。

ブース

今年のクックパッドブースではグッズの配布はもちろんですが、2つの企画を行います! 

f:id:tokunarigyozadaisuki:20190827145810j:plain
昨年のブースの様子

クックパッドのコード全部見せます大質問会

day2 - 9/7(土)の11:00-13:00にアンカンファレンストラックにて「iOS版クックパッドアプリのコード全部見せます大質問会」を開催します!

この会は、実際のプロダクトコードを見せながら、ディスカッション形式で、クックパッドのiOS開発について紹介します。 例えば以下のような質問を歓迎しています。

  • どういう開発体制でやってるの?
  • リリースフローやQAについて見せて
  • アーキテクチャはどうなってるの?
  • Podfile見せてください

なにか見たい部分や、開発上での質問がある方は、day1 - 9/6(金)にクックパッドブースへお越しください!当日はその質問を中心にご紹介します。飛び入りでの質問も歓迎です。

クックパッドエンジニアとの「カジュアルトーク」

エンジニアリングマネージャーや、現場で活躍するエンジニアと直接話せるカジュアルトーク企画を実施します。開発に関する話からオープンな場で聞きにくいキャリアについての話などを1on1のようなスタイルでお話しましょう! あんなことやこんなことまで……どんなことでもお気軽に。 参加メンバーとスケジュールは以下のとおりです。

エンジニアリングマネージャーと話せる枠

クックパッドブースにお越しいただき、希望する時間を選んでいただくと、その時間に VP of Tech 星 北斗 @kani_b、モバイル基盤部 部長 茂呂 智大 @slightairと話せます。

  • day1 - 9/6(金)11:00-18:00:星・茂呂
  • day2 - 9/7(土)11:00-18:00:茂呂

▼ RubyKaigi 2019 実施時の様子

特定の技術に詳しいエンジニアと話せる枠

以下の時間は、特定の技術について豊富な知見を持っている社員がブースにおりますのでぜひ情報交換しましょう!

day1 - 9/6(金)12:50-14:20 fastlane / Carthage:@giginet

fastlaneCarthageのコミッターやってるのでチョットできます。 その他にもSwiftPMやCocoaPods, XcodeGenなどの開発ツール全般についても知見あります。上記の話題にかかわらず、興味のある技術トピックがあればわいわいしましょう 🙌

day1 - 9/6(金)14:20-15:10 モバイルテスト自動化/QA @ichiko_revjune

参考:https://techlife.cookpad.com/entry/2018/12/12/120000

day1 - 9/6(金)14:20-15:10 SwiftUI @iceman5499

参考:https://techlife.cookpad.com/entry/2019/06/25/120000

おわりに

クックパッドはiOSエンジニアを募集しています。クックパッドで働くことに少しでもご興味をお持ちの方は、お気軽にブースまでお越しください! みなさまにお会いできることを楽しみにしております。

Cookpad Summer Internship 2019 10 Day Techコースを開催しました

$
0
0

こんにちは、サマーインターンシップ実行委員長の赤松( @ukstudio )です。

クックパッドでは毎年恒例となっているサマーインターンシップのうち「10 Day Tech コース」を 8月19日から8月30日にかけて開催しました。今年もたくさんの優秀な学生の方が参加し、10日間毎日真剣に取り組んでくれました。本当にありがとうございます。

前半

10 Day Techコースでは前半5日間の講義・実習を行い、後半5日間では更にOJT(実務体験)とPBL(サービス開発実習)の2つに分かれるという構成でした。

まずは前半の講義について簡単にご紹介致します。

1日目: オリエン・Web開発基礎

初日はオリエンテーションとしてクックパッドの取り組みの紹介や自己紹介、貸与PCのセットアップなどを行い、午後から講義に入りました。

講義ではインターンシップ全体を通して必要となるGit/GitHubの入門を行い、Webアプリケーション開発の基礎としてRackアプリケーションの実装、TypeScriptの入門を行いました。

f:id:ukstudio:20190819152258j:plain

ハンズオン資料

2日目: サービス開発講義

2日目はサービス開発講義として、午前中にクックパッドのサービス開発に対する考え方や開発プロセスについて学びました。午後は午前で学んだことをベースにインターン生同士でチームを組み、与えられたテーマを元にユーザーインタビューや価値仮説、アイデア出しからプロトタイプを作るというところまで実践し、最後に講師からの講評を行いました。

2日目は前半の中で唯一コードを書かない日ですが、みなさんかなり頭を使ったようで夜にはへとへとになっているようでした。

f:id:ukstudio:20190820181326j:plain

3日目: API

今年の10 Day Techコースの技術面でのテーマは「クックパッドらしさ」というのが実はあるのですが、3日目から5日目にかけてMicroservices、適材適所な技術選定、クラウド技術などを意識した講義になっています。

3日目〜5日目の講義の中でもこの3日目がテーマが一番色濃くでたのではないかと思います。3日目ではNode.js(TypeScript)を用いてBFF層(Backend-For Frontend)となるGraphQLサーバーの実装を行いました。ボリュームも密度も濃い講義でしたが、みなさん無事に乗り切ることができました。

f:id:ukstudio:20190821140252j:plain

4日目: モバイル

4日目では3日目に実装したBFFのGraphQLサーバーにクエリを投げ、受け取ったデータを表示するためのクライアントアプリを実装しました。今年はiOSとAndroidの二手に分かれ(希望制)、SwiftとKotolinで実装をしてもらいました。

iOSとAndroidの希望を聞いた時に、「経験したことのない方に挑戦してみよう」と伝えていたので未経験の方も多いようでしたが、スムーズに実装を終えることができていました。

f:id:ukstudio:20190822104350j:plain

ハンズオン資料

ハンズオン資料

5日目: インフラ

最終日となる5日目ではクックパッドにおいてSREがどうインフラの問題を解決してきたのか、また3日目ではあまり説明のなかったAPIのインフラレイヤーについて講義を行いました。講義の後は3日目に実装したAPIサーバーにパフォーマンスや可用性において問題が含まれていたので、その問題の解決に取り組みました。

ちょうどこの日にAWSに障害があり、少々ざわついたのですが幸い講義にはあまり影響なく無事終えることができました。講義では簡単に障害の内容についても説明されました。

f:id:ukstudio:20190823101042j:plain

以上が前半に行なった講義・実習となります。今年の講義はどれも密度が濃くかなり大変だったと思いますが、全員無事に乗り切ってくれました。本当におつかれさまでした!!

後半

PBLでは6 Day Design Productコースのデザイナーとペアを組み、サービス開発の実習を行いました。PBLについてはふじけんの Cookpad Summer Internship 2019, 6 Day Product Designコースを開催しました|Cookpad|noteをご覧ください。

OJTではクックパッドの様々な部署に配属され、メンターの指導の元サービス開発を実践してもらいました。配属される部署はクックパッドマートを運営・開発する買物事業部、Komercoを運営・開発するKomerco事業部などのサービス系から、モバイル基盤や技術部のSREグループ、クックパッドサービス基盤グループなどの基盤系など様々でした。

最終日には5日間の各自の偉業をメンターやOJTに配属されたインターン生全員の前で発表してもらいました。5日間という短い中で進捗を出すのは大変だったと思いますが、全員無事にやり遂げてくれました。ありがとうございました!

f:id:ukstudio:20190830141025j:plain

まとめ

簡単にですが2019年のサマーインターンシップの10 Day Techコースについて簡単に紹介させて頂きました。今年のインターンシップは実行委員長の自分から見てもかなりキツいインターンシップだったと思います。参加して頂いたみなさん、本当におつかれさまでした!!

f:id:ukstudio:20190830203305j:plain

今年の10 Day Techコースはかなりクックパッドの現場に近い内容となっています。クックパッドの現場に少しでも興味がでた方は新卒・中途問わず、ぜひ遊びに来てください!!

info.cookpad.com

2019 年度版:クックパッド x 広告領域の紹介

$
0
0

こんにちは。メディアプロダクト開発部の我妻謙樹です。サーバーサイドエンジニアとして、広告配信システムの開発・運用を担当しています。入社以来広告領域を担当するグループに所属しています。

クックパッドと広告

クックパッドでは、PS に次ぐ売上高を占める主力事業として、広告事業があります。

過去にも、"クックパッドの広告エンジニアは何をやっているのか"(公開日:2015-11-26)という記事が公開されたことがありますが、当時とは技術要素やチーム構成はもちろん、事業をめぐる環境が大きく変わっています。

しかし、上記記事でも述べられている、以下の原則は変わっていません。

クックパッドの広告は、昔から、ユーザさんと広告出稿企業さん、そして私たちクックパッドの3者ともが幸せになる形を模索し続けてきています。 クックパッドを通して、最終的には広告も「価値ある情報」としてユーザさんに届けば、それは広告単価の上昇にも繋がるからです。

広告は、広告を出稿してくださる企業の課題を解決するために存在し、ユーザーにその価値を届けるために存在しています。いわゆるアドネットワークを支える DSP/SSP を開発するのではなく、自社で配信システムを内製し、貴重なユーザーのデータを保持するからこそできる独自の広告事業の面白みが、クックパッドにはあります。

本記事を通して、クックパッドにおける広告事業、及びそれを支える私達の部・グループについて少しでも理解の一助となれば幸いです。

運用保守対象のサービス一覧

私達のグループでは、広告が入稿されてから配信されるまで一貫した自社システムを保守・運用しています。細かいシステムは多数ありますが、主要なコンポーネントのみを表示させたシステムアーキテクチャ概観は以下の図の通りです。

f:id:itiskj:20190913125033j:plain
msdev-system-overall
各システムについて、簡潔に紹介します。

Ad creative admin service

  • いわゆる「入稿」システム
  • 弊社の業務推進チームが、受注に従ってクリエイティブを入稿
  • 純広告とネットワーク広告の配信比率の調整、ターゲティングの設定、広告枠のリアルタイムレポートなど多機能

New ads delivery service

  • いわゆる「配信」システム
  • 新世代。クックパッド アプリケーション自体に内在されていた配信ロジックを別アプリケーションとして切り出したシステム
  • 初期は Rails アプリケーションだったが、機能の肥大化及びインフラコストの削減&開発速度の向上を目指し、一部の機能を Go に Microservices として切り出し

Legacy ads delivery service

  • いわゆる「配信」システム
  • 旧世代。初期は Cookpad アプリケーション自体に配信ロジックが内在されていた
  • 現在はほとんどの機能が新世代に移行済み、新規開発することは殆どない。旧バージョンアプリの互換性のために残していたが、物理削除含めて近日中に完全廃止予定。

Machine Learning services

  • いわゆる「最適化」システム
  • Ruby/Rails で実装された入稿システムにおいて、別言語ランタイムである Python と機械学習ライブラリを利用し、配信比率の最適化や在庫予測を行うため、AWS APIGateway + Lambda を利用した Microservices として実装されている

Logging (Lambda Architecture)

batch layer

  • いわゆる「DWH(Data Warehouse)」
  • すべてのログが格納されており、配信比率の最適化、レポーティング用集計など、入稿システムのあらゆる箇所で利用されている
  • サービスオーナーは DWH チーム
  • 業務推進チームやディレクターは、Tableau を利用しダッシュボードで可視化して業務に利用している

speed layer

  • サービスオーナーは我々のグループ
  • ストリーミングパイプラインによるリアルタイムログ基盤
  • 入稿システムにおけるリアルタイムレポートや、配信比率の最適化処理における精度向上に利用されている

Tracking service

  • いわゆる「DMP(Data Management Platform)」
  • サービスオーナーは DMP チーム
  • EAT(Extreme Audience Targeting)を始めとし、エリアターゲティングや検索キーワードターゲティングといった機能も提供している

技術スタック

先ほど紹介した各サービスで利用されている技術スタックは、以下の通りです。

f:id:itiskj:20190913125210j:plain
msdev-tech-stack

技術選定

前節で技術スタックについてご紹介しました。私達のチームでは、以下の図に表される評価軸に沿って、プロジェクトや事業の成果を達成するために最適な技術スタックを選択することを心がけています。

f:id:itiskj:20190913125235j:plain
msdev-tech-selection

「会社がこの技術を押すから」といった会社目線での観点や、「チームでこの技術を使っている人が多いから何となく・・・」といったチーム目線での観点、「この技術を使いたいから」といった技術的成長目線の観点だけで技術を選択することは有りません。以下の三点を総合的に判断することを心がけています。もちろん、技術選定に失敗したこともありますし、この評価軸が完璧ではありませんが、考える軸にはなります。

Tech - 技術的観点

技術自体の正しさを評価する軸です。例えば、ある課題に対して奇想天外な技術を選択することは、どれだけその技術を導入する難易度が高かったとしても、優れた設計では有りません。適切な「課題」に対して適切な技術を「解決」策として適用することこそが求められています。

そのために、数多くのミドルウェア、クラウドサービスに対しての知識を深め、少しでも引き出しを増やし、純粋に比較検討できるスキルが、技術選定に責任を持つテックリードに求められています。

その他にも、以下の観点を評価します。

  • 技術が開発されているエコシステムの発展性
  • 開発をサポートするツールの充実生
  • グローバル及び日本における技術トレンド
  • その技術を支える日本でのコミュニティ

Company - 会社的観点

次に、会社全体のその技術への関わり方を評価します。

例えば、クックパッドは Ruby/Rails をヘビーに利用する会社です。Ruby コミッターの方々も働いており、サポートも手厚いです。しかし、だからといって全てのサービスが Ruby であるべきか、Rails であるべきかというと違います。「技術的観点」および「チーム的観点」の比重を優先し、Ruby/Rails 以外の技術を選択することは往々にしてあります。

その他にも、以下の観点を評価します。

  • 会社のミッションに対する適合性
  • その技術を選択すると事業の成功にどれだけ貢献できる可能性が高いのか
  • 会社全体(他のチーム)で利用されている技術とは親和性が高いか
  • 会社のエンジニアカルチャーと適合するか
  • 採用の観点から、その技術を選択する優位性はあるか

Team - チーム的観点

最後に、チーム的観点から評価します。具体的には、部およびチームがどのような技術方針を持っているかという観点に加え、新卒からシニアメンバーまでそれぞれのメンバーの現在のスキルセットや目指すキャリアパスを考慮して総合的に判断します。

具体的には、以下の観点で評価します。

  • チームメンバーのその技術に対する成熟度
  • チームメンバの現在のスキルセットから想定できるその技術の吸収力
  • 各メンバーが目指すキャリアパスへの貢献具合
  • チームがスケールしたときに対応できる学習コストやサポート体制
  • その技術を選択することへのモチベーション

チーム体制

「領域:マーケティングサービス領域」を担当する部署が 4 つ存在しています。

f:id:itiskj:20190913125258j:plain
msdev-team

  • マーケティングサポート事業部
    • 国内におけるマーケティングサービスの企画、開発、運用及び営業に関する業務を担当する。営業グループの他、社内のデータを検証して新商品開発や営業提案資料を作成するデータチーム、日々の入稿作業やレポーティングを支える業務推進チームが所属
  • マーケティング企画制作部
    • マーケティングサービスの企画立案・制作・進行に関する業務を担当する。タイアップ広告を企業とともに作り上げる制作グループが所属している。
  • マーケティングプロダクト開発部
    • 国内に置ける広告事業及び企業向け事業に関する企画、商品開発に関する業務を担当する。主にディレクターが所属。
  • メディアプロダクト開発部
    • 国内の行ける企業向けプロダクト開発に関する業務を担当する。私達が所属している部署。

メディアプロダクト開発部

メディアプロダクト開発部では、以下の 3 つのグループから成立しています。9 割がエンジニアの組織です。広告領域を担当する私が所属しているのが、「マーケティングサービス開発グループ」通称 msdevです。

  • マーケティングサービス開発グループ(通称:msdev)
    • 広告領域を担当しています
  • プロダクト開発グループ(通称:pdev)
    • 動画領域を担当しています
  • プロダクトデザイングループ
    • デザイナーとディレクターが所属しています

プロダクト開発グループとの協同体制

プロダクト開発グループでは、動画領域を中心とし、数多くのサービスを開発しています。最近の開発については、以下の発表やブログが参考になるでしょう。

グループは違いますが、msdev と pdev は席も近く、部署も同じですので、頻繁にコミュニケーションがあります。プロジェクトによっては、片方のグループメンバーが片方のグループのプロジェクトを手伝う、といったこともあります。

これによって、広告領域に携わりながらも動画領域で利用されている技術に触れることができる、という大きなメリットがあります。例えば、私も広告領域のプロジェクトを担当する傍ら、過去に以下のプロジェクトに携わらせていただいたことがあります。

また、msdev/pdev それぞれのグループで勉強会を開催しています。もちろん横断して参加が可能です。過去には、以下のような内容で勉強会が開催されています。

  • 基礎技術詳解
  • AWS 各サービス詳解
    • DynamoDB. Parameter Store, API Gateway+Lambda, AWS IoT, CloudFront, etc.
  • 利用サービス詳解
    • terraform, Stripe, Tableau Desktop, etc.
  • カンファレンス参加報告
    • RubyKaigi 2019
  • 自分たちの保守運用するシステムのアーキテクチャ詳解
    • 動画配信サーバー, 広告配信サーバー, etc.

まとめ

広告領域は、技術的にチャレンジングな課題も多く、かつ事業の売上貢献に直結することが多い、非常にエキサイティングな領域です。また、アドネットワークではなく、自社の事業で専用の配信サーバーとユーザーデータを保持するからこその事業の面白さもあるため、事業開発に興味・関心が高い人にとっても活躍の可能性が大いにある場です。

メディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、以下からエントリーをしてください。

Ruby中間表現のバイナリ出力を改善する

$
0
0

Ruby 開発チームに4週間インターン生として参加いたしました、永山 (GitHub: NagayamaRyoga) です。 私は「Ruby中間表現のバイナリ出力の改善」という課題に取り組み、Railsアプリケーションのコンパイルキャッシュのサイズを70%以上削減することに成功しました。以下ではこの課題の概要とその成果について述べたいと思います。

InstructionSequenceの概要

まず、RubyVM 内で実行される命令の中間表現、InstructionSequence (以下 ISeqと省略) について簡単に説明します。

通常の Ruby プログラムは、以下のような手順で実行されます。

  1. ソースコードを構文解析し、抽象構文木を作る。
  2. 抽象構文木をコンパイルして、ISeqを作る。
  3. RubyVM (YARV) で ISeqを解釈し、実行する。

ISeqは、このように RubyVM で解釈される命令列に関する情報を含んだ一種の中間表現です。

ISeqに関する API は RubyVM::InstructionSequenceとしてその一部が外部に公開されているため、Ruby プログラムからも (ごく簡単な操作に限ってですが) 取り扱うことが可能です。

# 文字列をコンパイルして ISeq を得る
iseq = RubyVM::InstructionSequence.compile("p 42")

# 得られた ISeq を RubyVM で実行する
iseq.eval
# => 42# ISeq に含まれている命令列を出力する
puts iseq.disasm
# => == disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,4)> (catch: FALSE)#    0000 putself                                                          (   1)[Li]#    0001 putobject                    42#    0003 opt_send_without_block       <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>, <callcache>#    0006 leave

また、#to_binaryメソッドを呼び出すことで ISeqをバイナリデータにシリアライズすることができます。

bin = iseq.to_binary
p bin
# => "YARB\x02\x00\x00\x00\a\x00\x00\x00D......"

もちろん、シリアライズされたバイナリデータから ISeqに戻すことも可能です。

iseq2 = RubyVM::InstructionSequence.load_from_binary(bin)
iseq2.eval
# => 42

コンパイルキャッシュとBootsnap

では、上の機能がどのように活用できるのかについて説明したいと思います。

先程も述べた通り、Ruby プログラムは実行されるたびにスクリプトファイルの構文解析が行われます。

  1. ソースコードを構文解析し、抽象構文木を作る。
  2. 抽象構文木をコンパイルして、ISeqを作る。
  3. RubyVM (YARV) で ISeqを解釈し、実行する。

しかし、スクリプトファイルが変更されていなけば、コンパイル結果として得られる ISeqが実行ごとに変化するようなことはありません。 同じ ISeqが得られるにも関わらず、構文解析やコンパイルが行われるのは冗長です。

特に、短時間に何回も実行されるようなプログラムや、多数のスクリプトファイルで構成される巨大なアプリケーションではコンパイル結果 (ISeq) をバイナリデータとしてキャッシュしておくとその起動速度を向上できるかもしれません。

Rails5.2以降ではデフォルトでプロジェクトにインストールされる Bootsnapという gem は、前項で説明した #to_binaryメソッドを使って、スクリプトファイルのコンパイル結果を自動的に ./tmp/以下のディレクトリにキャッシュしてくれます。 Bootsnapはこの他にもautoloadしたファイルのパスなどをキャッシュすることでRailsプロジェクトの起動時間を50%〜70%程度縮めることに成功しています。 例えば、$ rails newによって生成されただけの空のRailsプロジェクトでは、Bootsnapによって起動時間が約65%短くなるのを確認できました。

課題

さて、この #to_binaryですが、その出力にはかなりの無駄があります。

iseq = RubyVM::InstructionSequence.compile("p 42")
p iseq.to_binary.length
# => 580

p 42というごく小さいコードから生成されたバイナリにも関わらず、その出力は 580byte という大きさになってしまいました (出力のサイズは環境によって異なります)。

当然、より大きいコードからは大きいバイナリが生成されます。 さきほどの空Railsプロジェクトであれば、Bootsnapが1632個の.rbファイルをキャッシュしており、そのキャッシュファイルの合計サイズは 32MB ほどになりました。

というわけで、本課題の目的はこの #to_binaryの出力するバイナリのサイズを小さくすることです。 #to_binaryの出力が小さくなると単純にストレージや転送時間の節約になるほか、Bootsnapがコンパイルキャッシュにアクセスする際のディスクアクセスが少なくなるため、Railsアプリケーションの起動時間が短くなることが期待されます。

方法

#to_binaryの実装の大部分は Rubycompile.cに書かれています。

今回のインターンシップではこの実装を読みつつ、部分部分を書き換えていくことで徐々に出力のサイズを小さくしていきました。

特にバイナリサイズの削減に寄与した変更は主に以下の2つです。

1. 不要な構造体フィールドの出力の削除

従来の実装では ISeq の情報を格納した構造体の、本来出力する必要のないものや、常に同じ定数が出力されているフィールドなどが存在していました。 コードを読み解いて、それらを出力に含めないようにすることでバイナリのサイズを削減しました。

2. 整数値の符号化方法を変更

また、出力に含まれていたあらゆる整数値はほぼすべてが固定長で符号化され、4byteや8byteのデータ長で出力されていました。 しかし、出力される整数値はその出現頻度に大きな偏りがあり、多くが 01などの少ないbit数で表現できる値です。 そこで、UTF-8を参考に可変長な整数の符号化方法を考え、導入することにしました。

0x0000000000000000 - 0x000000000000007f: 1byte | XXXXXXX1 |
0x0000000000000080 - 0x0000000000003fff: 2byte | XXXXXX10 | XXXXXXXX |
0x0000000000004000 - 0x00000000001fffff: 3byte | XXXXX100 | XXXXXXXX | XXXXXXXX |
0x0000000000020000 - 0x000000000fffffff: 4byte | XXXX1000 | XXXXXXXX | XXXXXXXX | XXXXXXXX |
...
0x0001000000000000 - 0x00ffffffffffffff: 8byte | 10000000 | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX |
0x0100000000000000 - 0xffffffffffffffff: 9byte | 00000000 | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX |

この方法では、7bitで十分に表現できる値は1byteに、14bitで表現できる値は2byteに、というように符号化する整数の大きさによって必要なバイト長を変化させています。

UTF-8では1byte目の上位bitを使って後続のバイト数を表しているのに対して、この符号化方法では下位bitの連続する0bitの個数でバイト数を表現しています。 このような形式を採用した理由は、x86_64などの命令セットではbsftzcntといった命令を用いることで後続のバイト数が1命令で数えられるためです。

評価

これらの変更によって、バイナリの読み込み速度を損なうことなく #to_binaryの出力のサイズを平均して 70%から75% 程度小さくすることに成功しました。 上記の空Railsプロジェクトでは、キャッシュファイルのサイズは合計 9.4MB (元の約70%) になりました。

f:id:NagayamaRyoga:20190926141101p:plain

その他の詳細なデータに関しては以下のチケットにまとめてあります。

https://bugs.ruby-lang.org/issues/16163

苦労した点

以下、今回の課題に取り組むにあたって苦労した点です。

プロジェクトの規模が大きいこと

Rubyは20年以上も継続して開発が続けられているプロジェクトであり、1万に近い個数のファイルによって構成されています。 特にC言語で記述されたソースコードの中には1ファイルが1万行を超えているものもあり、 (今回の実装に関連する部分はそのごくごく一部とは言え)処理の流れを把握するのが大変でした。

インターン期間の最初の1日は、ソースコードを読みながらバイナリデータを手でデコードし、おおよその処理の流れとデータ構造を理解していきました。

マルチプラットフォームなソフトウェアであること

Rubyは様々なOS、CPU、etcで実行される可能性のあるプログラムです。 そのため、どのような環境であっても正しく動作をするようにプログラムを書く必要がありますが、 C言語はその言語仕様の詳細 (例えば整数型のサイズと表現可能な数値の範囲、式の評価方法、評価結果など) の一部を"処理系定義"としています。

"処理系定義"の動作はコンパイラや環境によって異なる可能性があるため、 ある環境では動作をするが別の環境では動作しない、というようなことが起こらないように常に意識をする必要がありました。

まとめ

バイナリの読み込み速度を損なうことなく、そのサイズを70%以上も削減することに成功しました。 2019年12月リリース予定のRuby 2.7にこれらの変更が取り込まれ *1、実際のRailsアプリケーション上で動作するようになります。

世界的に有名なOSSに対して1ヶ月という短期間で貢献できたことは非常に貴重な経験になりました。 この場を借りて、メンターである笹田さんと遠藤さんに御礼を申し上げます。

退職処理を可能な限り自動化する

$
0
0

技術部 SRE グループの id:itkqです。2019 夏アニメで一番好きな作品は Re:ステージ!ドリームデイズ♪ です。この記事では SRE が運用している退職処理の自動化について説明します。

退職処理とは

入社後に業務のための様々なアカウントを作成するのと反対に、退職時にはそれらのアカウントを無効化する必要があります。これを退職処理と呼んでいます。SRE が管轄している典型的な例では、SSO に対応していない SaaS のログインアカウント・AWS の IAM User・データベースの個人ログインユーザなどが該当します。これらのアカウントは社員によって要否が異なったり必要な権限が異なるため、入社時に一括で用意せず必要に応じて申請してもらう形をとっています。一方で退職時にはそれらのアカウントをすべて無効化する必要があります。 退職処理は繰り返され、自動化の余地のあるタスクです。また、SRE 以外でも退職処理を行うチームがあることは分かっていたため、退職処理の自動化のための共通の仕組みを考えることにしました。自動化のための第一歩として、退職のイベントを扱いやすい形で発生させる必要があります。

退職イベントを発生させる

クックパッドでは、ヘルプデスク1が社内 IT のアカウントを管理しており、退職時はヘルプデスクが退職者の Active Directory (以降 AD と略記) アカウントを無効化します。入社時のアカウントの作成は人事システム経由で自動化されている一方で、退職時は退職者によってタイミングなどが複雑であることがしばしばあるため、「人間による無効化」によって退職処理を開始させるようにしています。 AD とは別に、SSH や GitHub Enterprise ログインなどに使う目的で OpenLDAP によるアカウント管理も運用しています。これまでヘルプデスクと SRE でそれぞれ AD, OpenLDAP を運用してきましたが、手を取り合って AD に一本化する計画を進めています。移行期間中は AD と OpenLDAP 間で齟齬が起きることが予想されたため、AD と OpenLDAP 間で属性を同期し、パスワード変更時は AD と OpenLDAP 間で同時に変更させることで、将来の統合を楽にする目的の pasuwado というシステム (id:sora_h作) が稼働しています。 pasuwado は AD と OpenLDAP 間の属性の同期をバッチ処理で行っています。AD のアカウントを無効化情報を pasuwado に管理させることは、本来の責務から外れすぎず、同じようなバッチ処理で実装できる見込みがあったたため、AD アカウントの無効化タイミングを保存しつつ退職イベントを扱いやすい形で発生させる機能を pasuwado に組み込みました。実運用では、誤って AD アカウントを無効化してしまうヒューマンエラーや、アカウントの無効化やリソースの削除を遅らせたい要求があることを考慮する必要があります。そこで「無効化が発見された直後」、「無効化を発見してから 3 日後」、… のようにいくつかのタイミングでイベントを発生させ、受け取る側では都合のいいようにフィルタする設計にしました。SRE では「無効化を発見してから 3 日後」のタイミングで退職が確定したとみなすようにしています。このイベントは pub/sub メッセージングサービスである Amazon SNS に送信します。次に示すのは SNS に送信されるメッセージの例です。detected_at は pasuwado のバッチが AD アカウントのサスペンドを発見した時刻、elapsed はサスペンドを発見してから経過したおおよその秒数です。

{"name": "mana-shikimiya",
    "detected_at": "2019-04-23T00:00:00+09:00",
    "elapsed": 0}

退職イベントを受け取り自動処理する

退職イベントが送信される SNS を購読することで、自動化する退職処理のトリガーにすることができます。SNS を使うことで購読方法は選択肢の中から好きなものを選ぶことができ、また自動化処理同士を独立させられます。実際に自動化されている処理について例を挙げながら説明します。

例1: GitHub に issue を立て Slack に通知する

AD アカウント無効化直後のイベントを受け取ると、退職処理をまとめる GitHub リポジトリに issue を立てるジョブを実装しました。この issue は他の退職処理の結果をコメントしていき、スタックする用途のものです。また、誤って AD アカウントを無効化してしまったことに気づきやすいように Slack への通知も同時に行っています。 このジョブは barbeque2ジョブとして実装しています。barbeque は ECS を基盤とする非同期ジョブ実行環境で、ジョブのリトライなどの管理を任せながら、SNS を購読し ECS で動作するジョブの実装を少ない手間で行うことができます。次の図は pasuwado から barbeque ジョブまでの動作フローです。

f:id:itkq:20191009220409p:plain
pasuwado から barbeque ジョブ実行までの動作フロー

例2: IAM User を削除する

AWS の IAM User は miamという IaC ツールで Git 管理しています。master ブランチが更新されると CI が走り AWS 上のリソースが変更されます。 無効化から 3 日後のイベントを受け取ると退職が確定したとみなし、IAM を管理するリポジトリをクローンし、退職者の IAM User を発見したらそれを削除する Pull Request を出しマージするジョブを同様に barbeque ジョブとして実装しました。この Pull Request は、例1 で述べた issue に紐づけています。

その他

上で挙げた例以外に、SRE が持つ稼働中の自動化ジョブは以下のものがあります。

  • MySQL 個人ログインユーザの削除
  • GitHub Enterprise アカウントのサスペンド
  • PagerDuty のユーザーをチームから削除
  • Amazon SNS Topic の個人 email subscription の削除
  • github.com のアカウントを cookpad organization から削除

また、他のチームの利用事例として、DWH チームによる Redshift の個人ログインユーザの削除・個人スキーマの削除があります。

自動化による SRE の退職処理運用の変化

退職処理の自動化によって、運用がどう変わったかについて説明します。

自動化以前

週次で SRE のうち一人がアサインされ、スプレッドシートに記録された退職者のアカウント情報を元に手動で処理していました。具体的には、SRE が管理するサービスのアカウントやリソースに該当アカウントがないかチェックし、見つかればそれを削除したり無効化してスプレッドシートに作業内容を記録していました。SRE 管轄のアカウントはそれなりに量があるため、場合によっては面倒な作業でした。

自動化以後

退職処理を伴う退職者が存在したかどうか週ベースで自動チェックし、存在した場合は SRE のうち一人がアサインされます。アサインされる issue には、退職処理済みのアカウントに対応する、例1 で述べた issue が紐付けられています。この issue には、自動処理の結果と自動化しきれず手動で行うべき処理が書かれており、これを手動で行ったら issue をクローズし、紐付けられたすべての issue をクローズしたら作業完了です。 元々あったスプレッドシートの運用は、作業内容を誰がいつ行ったかを記録する目的のものだったため、それが issue 上で行われるようになった現在では不要となりました。また、自動化できない処理とは例えば、SaaS のアカウントを無効化したいが無効化を行う API が無いなどです。いい感じの API が生えてくれることを願っています。

まとめ

SRE で運用している退職処理自動化の仕組みについて説明しました。汎用的な仕組みとして設計したため、この仕組みを活用して退職処理を自動化している SRE 以外のチームもあります。 繰り返される自動化可能なタスクを可能な限り自動化していくことにより、本質的な作業にかける時間を増やすことができます。この仕組みを導入してから 1 年以上が経過しており、自動化を実装する時間と比較しても退職処理にかける時間をだいぶ省けている実感があります。退職処理が面倒だと感じている方はこの記事で述べたような自動化の仕組みを検討してみてはいかがでしょうか。


  1. 通称。正式にはコーポレートエンジニアリング部サービスデスク・インフラグループ

  2. https://techlife.cookpad.com/entry/2016/09/09/235007を参照

データ分析プロジェクトの品質をキープしつつ効率的な検証をサポートする一時ファイル群の管理

$
0
0

研究開発部の takahi_iです。本稿はデータ分析、 機械学習関係のプロジェクトで数多く生成される一時オブジェクトおよびそれらのオブジェクトを保持するファイル(一時ファイル)を管理する取り組みについて解説します。

本稿の前半はデータを分析するプロジェクトの一般的なフローと起こりがちな問題(コードの品質管理)について解説します。後半はプログラム上で生成されるオブジェクト群をファイルに自動でキャッシュを管理するツール(Hideout)を使って、コードを整理整頓しやすくする施策について紹介します。

データを分析するプロジェクトの一般的なフロー

まずデータを処理するプロジェクトや機械学習プロジェクトの典型的なフローについて考えてみます。まずは単純に機械学習器を取得した入力に対して適用するプロジェクト、次にもう少し複雑な事例、アプリケーションで利用するデータを生成するプロジェクトのフローについて見てゆきます。

単一の機械学習器を適用するプロジェクト

データ分析するプロジェクトの典型例として機械学習器を入力データに対して適用するプロジェクトを考えます。

学習用のデータのように静的に変化しないデータはレポジトリに同梱されることもありますが、Webサービスのデータが対象の場合、推論用のデータの多くは日々変化するため、データベース(DWH)から動的に取得します。

機械学習を適用するプロジェクトは取得したデータを多段に変換しながら出力となる判別結果を生成してゆきます。

以下の図は一般的な機械学習を利用したプロジェクト検証スクリプトのフローです。

f:id:takahi-i:20191015114010p:plain
単一の機械学習器を適用するプロジェクト

上記のプロジェクトでは必要なデータを取得、学習、推論(テスト)と連続して処理がおこなれています。各処理ごとにデータは変換され、生成されます(例えば学習後にはモデルオブジェクトが生成されます)。

たとえば、レシピに含まれるステップを本来のステップとそれ以外(コメントなど)に分類する バッチ処理(こちらを参照)はちょうどこのような内容になっています。このバッチでは一部のデータを切り出して処理しているのですが、それでも取得、学習、推論(テスト)ステージでかなりの時間がかかっています。

アプリケーション用データを生成するプロジェクト

さらに自然言語のような非定形データを扱う中でも複雑なタスク(対話や質問応答)ではDWHから取得した データを多くのオブジェクトを生成しつつ多段で加工してゆくことも一般的です(End-to-Endの学習で一度に処理してしまう場合もありますが)。

f:id:takahi-i:20191015114231p:plain
多段に処理をするプロジェクト

クックパッドの研究開発部においてこのような多段の処理を適用するプロジェクトに レシピのMRRへの変換や、 クックパッドのAlexaスキルが提供する調理補助用の質問応答で利用されているKnowledge Baseの生成プロジェクトがあります。

これまで紹介した2つのタイプのプロジェクト(単一の機械学習器を適用するプロジェクト、アプリケーション用データを生成するプロジェクト)のどちらのプロジェクトタイプでも、まず一つ以上の入力データを抽出します。データは入力データ、必要なリソース(辞書)、機械学習器が出力したアノテーション結果などがあります。そしてその後のデータ変換、集約処理が多段に続きます。各処理では一時オブジェクトを入力として別のオブジェクトを生成します。

そしてこのオブジェクトの集約、加工、生成処理の実行はそれぞれ時間がかかります。このことがデータ分析プロジェクトを中長期メインテナンスする場合に品質上の問題を引き起こします。

データ処理スクリプトに対する修正要求とコード品質問題

データを分析、加工するプログラムでも普通のプログラムと同じように機能追加の要求にさらされ修正され続けます。 多くの場合、修正するべき箇所はプログラムの中の一部に過ぎませんが、修正が問題をはらんでいないかをチェックする にはE2Eテスト(機械学習の学習、Inferenceなど)を走らせます。また、自然言語を入力とするタスクの場合にはテスト しきれないことがどうしても発生するため、一部のデータを使ってプログラムを実行して変更が問題を発生していないかも確認したくなります。

このような検証目的の実行時に扱うデータの規模は大規模ではなくても、各ステージごとの処理に十秒から数分かかってしまいます。

結果、微細な修正をした後にコードに問題がないかを確認するだけでも結構な時間を消費してしまいます。 実行に時間がかかりコードを修正をするコストが大きくなるため、コードを修正するサイクルが大きく(試行できる回数が少なく)なり、コードを整理するハードルが高くなってしまいます。このような状況ではプログラムの検証実行や、テストに時間がかかりすぎるためコードの品質をキープしづらくなります。

不十分な解決方法

検証時の実行に時間がかかってしまう問題に対するする解決策として、以下のような対処方法が考えられます。

一時ファイルの手動追加

データの取得、生成時間を省略するために、データベースから切り出した入力データや機械学習器が出力したモデルファイルのような一時データを保持するファイル(一時ファイル)をレポジトリや、ローカルディレクトリに同梱しているプロジェクトを見かけます。データ分析プロジェクトの一時ファイルは、モデル、データベースから抽出した辞書リソース、前処理済みの入力データなどがあります。

たとえば以下のプロジェクトでは入力ファイルに前処理を適用したファイル(preprocessed_input1.txt)と、機械学習器が生成したモデルファイル(validation_model1.dat)をレポジトリに同梱しています。

.
├── Makefile
├── README.md
├── config
│   ├── __init__.py
│   └── env.py
├── data
│   ├── dictionary.dic
│   ├── preprocessed
│   │   ├── preprocessed_input1.txt
│   │   └── preprocessed_input2.txt
│   ├── models
│   │   ├── validation_model1.dat
│   │   └── validation_model2.dat
...

もちろんこれらの一時ファイル群は本来はレポジトリに含まれるべきではありません。それでも、こういった一時ファイルを利用することでプログラムの動作検証の速度を向上できます。

しかし、このように安易に加工済み入力ファイルやモデルファイルをレポジトリに同梱してしまうと問題が発生します。

問題の一つは加工済み入力データの生成方法がコードから分離してしまい、プロジェクトが進むにつれデータの加工方法と乖離してしまう点です。 入力データも含め、プログラムで扱うデータやオブジェクトはコード修正とともに変化してゆきます。 たとえばモデルファイルのような生成されたデータを一時ファイルから読み出して検証的に実行している場合、コードの修正によってデータがファイルから読み出しているものから変化し本来は実行時に問題が発生していることがあります。残念ながら一時ファイルを利用して検証実行している場合、このような問題に気がつくのは難しいです。というのも修正した部分(たとえば検証用に切り出した小規模データでの学習処理)はキャッシュファイルを使うと実行されず、テストやローカル環境での実行は中途半端にうまく動作してしまうのです。

さらにテストがレポジトリに添付されたモデルファイルを利用してしまっている場合には、CI環境でもコードの修正にともなうバグを検知できません。 このような状況で問題が発覚したときにはコミットすでにがかなり積まれてしまい、問題箇所を同定するのが難しくなっていることがあります。

もう一つの問題は、レポジトリを中長期メインテナンスすると発生します。本来は一時的な目的でVCSレポジトリに追加されたはずの中間オブジェクトを保持するファイル群は、役割を全うした後も消されることなく(消し忘れ)レポジトリにとどまり続けることがあります。このような消し忘れファイル群はプロジェクトの開発時には問題にならないのですが、時間経過(半年、一年)を経るとエンジニアは生成方法を記憶していないため特に引き継ぎ時に大きな問題になります。

キャッシュ処理の追加

必要な一時ファイルを活用するためのもう少しましな解決方法に、関数へのキャッシュ処理の追加があります。 たとえばMRRの生成プロジェクトでは、入力、中間データ(それぞれが数十〜数百MBのデータ)をファイルにキャッシュをすることで、 検証時の実行速度を向上して開発速度を高めました。以下はMRRの生成プロジェクトで利用されているメソッドの一部です。

defget_ingredient_id_map(cache_file_path):
    if os.path.exists(cache_file_path) andnot self.force:
        withopen(cache_file_path, mode='rb') as f:
            return pickle.load(f)

    ingredient_id_map = _get_ingredient_map_impl()

    ifnot os.path.exists(cache_file_path) ornot self.force:
        withopen(cache_file_path, mode='wb') as f:
            pickle.dump(ingredient_id_map, f)
    return ingredient_id_map

この関数は、キャッシュファイルがあればロードしたものを返し、なければ生成したうえで、(キャッシュ)ファイルに保存します。 この関数を使うことでローカル環境で(2回目の実行以降)オブジェクトを生成するコストは低減できます。

大きめのオブジェクトを生成する関数にキャッシュファイルを生成する処理を付与することでテストや検証目的に実行していたプログラムの実行時間が、数分から10秒程度に減少できました。これによりコードを積極的に整理できるようになりました。 またCIで簡単な学習➡推論をすることで、学習プロセスに問題ないかを常時クリーン環境でテストし続けられるという 利点があります。

しかしこのやり方にも問題があります。数多く存在する中間オブジェクトごとに上記のようなキャッシュする処理をつけるのは面倒ですし 、処理内容とは関係のない内容で関数を埋めてしまうのにも抵抗があります。また、多くの機能追加の要請で必要な修正は コードの一部のコンポーネントに限られます。そのため一部のキャッシュだけは効かせたくない場合がありますが、 各変換処理にキャッシング処理をベタ書きした状態では対応が難しいです。

こういった問題を解決するため、最近はオブジェクトのファイルへのキャッシュ処理を自動化する Hideoutという簡素なツールを作って利用しています。

Hideout: データ分析プロジェクト用、ファイルキャッシュ

Hideoutはオブジェクトを生成するタイミングでキャッシュファイルもあわせて生成するツールです。 実行時に環境変数で指定するキャッシュ設定がオンになっていてかつ生成されたキャッシュファイルが存在すれば、 キャッシュファイルをロードしてオブジェクトを返し、なければ指定された生成用関数を呼び出します。

基本的な使い方

たとえば以下の generate_large_object関数はオブジェクトを生成するのに時間がかかります(人工的なサンプルですが)。

defgenerate_large_object(times):
    sleep(1000)
    returnmap(lambda x: x*2, range(times))

この関数から生成されるオブジェクトを Hideout でキャッシュするには以下のように記述します。

large_object = hideout.resume_or_generate(
    label="large_object",
    func=generate_large_object,
    func_args={"times": 10}
)

funcにはオブジェクトを生成する関数、func_argにはfuncを実行するのに必要な引数を辞書として渡します。

HideoutはデフォルトではキャッシュがOffになっています。そのため、 デフォルトではキャッシュはされず単にfuncに指定された関数を実行してオブジェクトを生成します。

キャシュをOnにしてオブジェクトを使いまわしたい場合には環境変数、HIDEOUT_ENABLE_CACHETrueを設定します。 ローカルで検証しているときにはコマンドを実行するターミナルで環境変数を指定します。

使用例

クックパッドのAlexaスキルで使用している質問応答用のKnowledge Baseを生成するプロジェクトではHideoutを利用して 生成される中間オブジェクトの一部をキャッシュしています。

該当レポジトリは Cookiecutter Docker Scienceテンプレートで生成されているので、 Makefileをワークフローの管理に使用しています。MakefileにはKnowledge Base生成用のターゲットを登録してあります。 ローカルにおける検証では以下のようにキャッシュをOnにして実行しています。

$ make generate BATCH_SIZE=500 HIDEOUT_ENABLE_CACHE=True 

テストではモデルファイルを使ったE2Eのケースも含まれていますが、同じように make コマンドに HIDEOUT_ENABLE_CACHE=Trueを指定した上で実行すると数秒で終わります。

$ make test HIDEOUT_ENABLE_CACHE=True

Hideoutにおいてキャッシュ設定はデフォルトではOffになっているので、CIやプロダクション環境で誤ってキャッシュファイルが生成されることはありません。 そのため修正時にはPull Requestをこまめに作り細かくコミットをプッシュすると、キャッシュが効いていない環境でテストが走るため、思わぬ不具合に気づけて便利です。

ステージごとにキャッシュOffを指定

本稿の前半で紹介したようにデータ処理をするプロジェクトには複数のデータソースを複数のステージで多段に加工するものがあります。

f:id:takahi-i:20191015114414p:plain
複数のステージから成るプロジェクト

上記の図ではデータを抽出した後「Preliminary1」や「Preliminary2」、「Transform」というステージがあります。 このような少々の複雑さをもつプロジェクトであっても機能拡張依頼が来たとき、多くの場合には修正する箇所は ソースコードの一部でしかありません。

このようなとき修正が必要な一部のステージだけキャッシュファイルをロードする処理をオフにしたいことがあります。この目的のために HideoutはHIDEOUT_SKIP_STAGESという環境変数を提供しています。たとえばキャッシュした ファイルを利用して実行したいが、Preliminary2Transformステージだけはキャッシュを Offにしたい場合が考えられます。 このような場合、make build HIDEOUT_ENABLE_CACHE=True HIDEOUT_SKIP_STAGES=Preliminary1,Transformと キャッシュをしないステージを指示します。

Hideoutにおいて指定するステージ名はhideout.resume_or_generatelabelオプションで付与します。

large_object = hideout.resume_or_generate(
    label="Preliminary1",
    func=generate_preliminary_object,
    func_args={"times": 10}
)

今後

Hideoutを利用するユーザはプログラム中の関数ではなく、キャッシュファイルを生成している部分に適用する部分に処理を追加します。これはキャッシュする部分を追いやすくするためで、インターフェース名もHideoutが利用される箇所がわかりやすくなるように長く(resume_or_generate)なっています。 ただ最近、同僚から「デコレータでやったほうがシンプルなんでは」というコメントを頂きました。そこで今後デコレータのインターフェースも提供してみたいと考えています。

まとめ

本稿はデータ分析をするプロジェクトにおける一時オブジェクトを保存したファイル(一時ファイル)の扱いについて解説しました。一時ファイルはデータ分析の結果を高速に検証するのに便利ですが、安易に レポジトリに追加するとプロジェクトの保守性が下がります。

また、必要な入力データや中間オブジェクトをファイルにキャッシュする処理を追加するのも コストがかかります。そこで本稿の後半では、このようなプロジェクトを保守するのに役立つキャッシュツール、Hideoutについて紹介し、一時ファイルを利用しつつもレポジトリをクリーンに保つ方法について解説しました。


クックパッド社内に工房(Fab)を作ってプロトタイプ開発をした話

$
0
0

今年1月に研究開発部から分離して発足しましたスマートキッチン事業部の山本です。
スマートキッチン事業部では、クックパッドが提供するレシピ情報を様々な家電機器と連携させて、料理体験をより楽しく快適にする、スマートキッチンサービス OiCyの開発をすすめています。

クックパッド社内に工房(Fab)を作りました

スマートキッチンサービスOiCyは、レシピ情報と家電機器の連携で生み出されるサービスで、サービス開発に加えてサービスと連携する家電機器が必要になります。そのため、家電メーカーとの連携をすすめていますが、同時に自前での家電機器開発も行っています。そして、自前の家電開発を効率的に行なえるようにする目的で、社内で加工製作ができる工房(Fab) を、恵比寿のクックパッドオフィス内に立ち上げました。工房には、3Dプリンタやレーザーカッターなどの加工設備が設置されており、業務内外を問わず社員の利用が可能になっています。(要安全講習)

f:id:ymmttks:20191016150422j:plain:w420
工房の様子

工房生まれの自前(改造)家電たちの紹介

この工房で生まれた、クックパッド自前(改造)家電の一部が、先日開催されたスマートキッチンサミットジャパン2019(SKSJ2019)で公開されました。

OiCyService動画

SKSJ2019クックパッドデモの動画
↓SKSJ2019の関連記事はこちらを参照
・PC Watch ロボット化する家電から寿司シンギュラリティまで、人を食でエンパワーする「スマートキッチンサミット2019」 森山 和道
・CNET JAPAN 未来の台所を創造する「SKSJ 2019」から見えてくるもの 近藤克己

OiCy Water

f:id:ymmttks:20191016150433j:plain:w320
OiCyWater外観
OiCy Waterは、水の『硬度』と『分量』をレシピに合わせてボタン一つで出してくれる、電動ウォーターサーバーです。スマートフォンのアプリ上でレシピを閲覧すると、そのレシピに書かれている水の『硬度』と『分量』(※)が自動的に装置に転送されます。ユーザーは装置上のボタンを押すだけで、閲覧しているレシピで使うための適切な水を得ることができます。『硬度』と『分量』は、ジョグダイヤルを回して手動で調整することもできます。

※現状ではクックパッドのレシピ全てに硬度に関する記述があるわけではありません。

↓水の硬度が料理に与える影響についてはこちらを参照
SKSJ2019 ⾃分の「おいしい」を⾃分でつくれる感動を クックパッド 金子晃久

OiCyWaterの構成

f:id:ymmttks:20191016150453p:plain:w420
OiCyWaterの構成
本機は、2つのチューブポンプを用いて、『硬水』と『軟水』2つのボトルから水を排出する装置です。使用したチューブポンプは3.5ml刻みで排出量を制御、最速で1分間に1.4Lの水を排出することができます。
制御用のマイコンシステムにはM5Stackを使いました。理由は、技適が取れていてかつとても安価、ネット上に参考にできる情報が豊富にあって、ライブラリも充実しているからです。周辺デバイスへの信号は、モーターの速度制御をするPWM信号のみM5StackのGPIOから直接出していますが、それ以外はI2C接続したGPIOエクスパンダを経由してやり取りをしています。GPIOエクスパンダ側で、信号の変化を割り込み制御をする予定でしたが、WiFiのライブラリと同時使用するとファームにリセットがかかるという現象があり、イベント直前で時間がなかったためこの問題解析は保留して、割り込みなしのポーリングで、ジョグやモーターの回転を拾う処理になっています。
モーターの回転は、チューブポンプの回転部分とポンプ外装の間に隙間があるので、100円ショップのネオジウム磁石をそこに接着、ポンプ外側に設置した磁気センサから非接触で回転検出をしています。この方法では回転方向は分からないのですが、チューブポンプは負荷が非常に大きくギア比の大きなモーターが付いており、外部から強制的に回すことはほぼ不可能です。そのためポンプ回転部分はモータードライバに入れている信号の向きにしか回らないため、特に回転方向を検出する必要はありません。磁石の貼り付け位置を、チューブポンプのローラー部分にすることで、水の排出綺麗に途切れるところで正確にモーターを止められる”位置制御にも利用しています。
f:id:ymmttks:20191016150442p:plain:w320
OiCyWaterのメカ構造
チューブポンプはモーター部分が長く突き出した構造をしていて筐体へのおさまりが悪いため、ベルトとプーリーを用いてモーター部分をポンプ本体とタンデム構造にし、2つのチューブポンプを向かい合わせに対向させるメカ構成にしました。2つのチューブポンプ、2つのモーターは2mm厚のステンレス製の背骨に固定されて、チューブポンプを回す強力なモータートルクに負けない強靭な剛性を持たせました。工房のレーザーカッターでは、金属の切り出しはできないため、この背骨部分の制作のみ外注先に頼んで特急で作っていただきました。ペットボトルを下向きに指すジョイント部分については、弁のついたペットボトルキャップと交換する部分はペット用の給水器の部品を流用、刺さる側の部品は3DプリンタをつかってABS樹脂で成形しました。2Lの水の水圧がかかっても水漏れをしない構造を作るのには試作検証改良を繰り返す必要がありましたが、3Dプリンタが手元にあることは短期間での開発にとても役立ちました。

OiCyサービス対応電子レンジ

f:id:ymmttks:20191016150446j:plain:w420
OiCyサービス対応電子レンジ SIGMA
メーカー様からお叱りを受けるかもしれないのであまり大きな声では言えないのですが、市販の電子レンジを改造して、スマートフォンのアプリ上で閲覧しているレシピに書かれている 『加熱ワット数』、『加熱時間』をWiFiを通じて自動的に装置に転送されるようにしたものが、OiCyサービス対応WiFi電子レンジ(開発名:SIGMA)です。

SIGMAの構成

f:id:ymmttks:20191016150457p:plain:w420
SIGMAの構成
電子レンジは、強電系に非常に高圧な回路と大容量コンデンサを搭載しており、改造には危険を伴います。専門的な知識のない場合には絶対に真似しないようにお願いします。今回の改造は、できる限り装置の深い制御部分に手を入れず、UI部分を乗っ取る形でHackしました。こうすることで、強電系の回路に一切触れずに欲しい機能を実現することができました。
電気量販店で、改造しやすそうな電子レンジを探すところから、開発は始まります。改造用の電子レンジを選択する上でのポイントは、液晶表示やタッチパネルなどを用いてるものは、現在の状態を正確に把握するための難易度が高いため避けます。LEDのみ、物理スイッチのみでUIが構成されていて、かつスイッチに複数の機能が割り当てられていないものが好適です。
OiCy Waterと同様に、制御用のマイコンシステムにはM5Stackを使いました。この電子レンジは、UIが、LEDと2つのジョグスイッチだけで構成されていたので、これらの入出力と扉の開閉センサをロジック回路処理(時分割表示のLED信号の復調回路)を噛ませてGPIOエクスパンダに接続し、M5Stackからは全体が一つのI2Cデバイスとして見えるようにしました。 外装は、操作パネル部分のジョグやLEDをすべて外し、レーザーカッターで加工した3mm厚の乳白色のパネルで覆い、表面上からはM5Stackの表示パネルとボタンのみしか見えない構造になっています。スマホからのレシピ情報転送以外に、M5Stackのボタン操作によるレンジ設定変更が可能です。加熱のスタートやキャンセルはドアの開閉で行い、ボタン操作は不要です。

OiCyサービス対応IHプレート

f:id:ymmttks:20191016150426j:plain:w420
OiCyサービス対応IHプレート OMEGA
こちらもメーカー様からお叱りを受けるかもしれないのであまり大きな声では言えないのですが、市販のIHプレートを改造して、スマホ端末で閲覧しているレシピに書かれている『火力』、『加熱時間』をWiFiを通じて自動的に装置に転送されるようにしたものが、OiCyサービス対応WiFi IHプレート(開発名:OMEGA)です。

OMEGAの構成

f:id:ymmttks:20191016150502p:plain:w420
OMEGAの構成

改造用のIHプレートを選ぶポイントも電子レンジとほぼ同じです。リバースエンジニアリングの結果、この機器は本体部分とUI部分をクロック同期式の変則的な双方向シリアル通信で構成されていることが分かったので、UI基板を取り外し、代わりにロジック回路処理(入出力信号の分離)を噛ませてGPIOエクスパンダに接続して、M5Stackからは全体が一つのI2Cデバイスとして見えるようにしました。
外装は、UIパネル幅に対してM5Stackの方が奥行方向に長いため、張り出した顎の部分を3Dプリンタで成形して、表面処理をした上で塗装したものを取り付けてあります。パネル部分は、電子レンジと同様にレーザーカッターで加工した3mm厚の乳白色のパネルで覆って統一感を持たせてあります。

社内に工房がある強み

 製品やサービス、機能など、我々がこれから作ろうとしているものが、本当に顧客にとって価値があるのか?モノができてから実際にユーザーに提供してみたら、期待した価値がなく失敗・・・のでは時間と資本を大きくロスしてしまいます。そこで、我々は、Googleのスプリントに則って、3〜5日で課題から仮説を導きだし、ソリューションを立て、プロトタイプを作り、ターゲットユーザに当てて仮説検証を行う、という方法をよく用いています。
 ↓高速コンセプト開発メソッドについてはこちらを参照
 ・SKSJ2019 一週間で回す高速コンセプト開発メソッド教えます クックパッド 佐藤彩香
 
 スマートキッチン事業部でも、短いものでは3日程度で、ハードウェアのプロトタイプを作って、実際にユーザーに使ってもらって、顧客価値を検証しています。今回紹介した3つの家電デバイスは、各2週間程度の開発期間がかかっていますが、顧客価値検証用に現場で実際に使用し、継続的に改良が行われています。こういった機器の開発改良は、外部のリソースに頼っていてると短時間での開発は難しく、価値検証に時間がかかってしまいます。使いたいときにいつでも加工・製作に使える『場』と『機材』が社内にあることは、超高速ハードウェアプロトタイピングでは極めて重要です。そして、この『場』と『機材』を活かしきれる、メカ設計〜加工〜回路設計〜回路製作〜ファームウェア設計実装〜実機デバッグといった一連のプロトタイプ開発を一人で一貫してできる、フルスタックエンジニア人材を絶賛募集中です。

まとめ

『その程度のプロト、俺がやれば1週間でできる!』『3日でできる!』というプロトタイプエンジニアスキルをお持ちの猛者のかたは、是非我々のプロジェクトにJoinしてください。
クックパッド キャリア採用 職種:プロトタイプエンジニア(スマートキッチン)

同時に、こんなイカれたデバイスを操作するiOSアプリを書きたいという、キワモノ好きのiOSエンジニアとデバイスとアプリをユーザーとつなぐ素敵なUI/UXデザインを担当するデザイナーも絶賛募集中です。
クックパッド キャリア採用 職種:iOS エンジニア
クックパッド キャリア採用 職種:UI/UXデザイナー(スマートキッチン)

データ活用基盤の今 〜DWH外観図〜

$
0
0

こんにちは、今年の1月に会員事業部から技術部データ基盤グループへ異動した佐藤です。先日、京まふ2019前夜祭イベントに参加するために人生で初めてピカピカ光る棒を買いました。

新卒で入社してから2年ほど分析作業をしていた身から、データ活用基盤を作る側へ立場を変えました。今回は新たに身を移したデータ活用基盤の外観を説明したいと思います。

2017年にも同内容の記事が投稿されていますので、当時との違いを中心に説明していきます。

外観図

以下が2019年10月現在におけるクックパッドのデータ活用基盤の全体像です。

クックパッドのDWH外観図
クックパッドのDWH外観図

masterデータのインポートがMySQL以外にも複数種対応し始めたことと、PrismとSpectrum(S3+Glue)周りと、Tableau Serverが大きな変更点となっています。2017年の図にDmemoはありませんでしたが、記事本文にある通り当時から活用していました。

図が煩雑にならないよう、レシピサービスを中心にまとめていますが、クックパッド社が運営する全サービスについて同様のワークロードでRedshiftにデータを集約させています。また、図では省略しましたが、Firebaseなどの外部サービスから得られるデータもRedshiftに集めています。

一つ一つの処理をみていきましょう。

入力: マスターデータの取り込み (master data flow)

クックパッド社内でサービスのマスターデータを管理するDBとして利用するDBMSは主にMySQL・PostrgreSQL・DynamoDBがあります。最も多いのがMySQLで、最近DynamoDBが増えつつあります。

MySQLについてはpipelined-migratorという独自開発の専用システムを利用しています。 こちらは管理コンソール用の専用Webサイトが社内ネットワーク上にあり、テーブル取り込みの様子を確認できます。また、ボタンひとつでRedshiftに取り込むテーブルやDBの追加・削除が手軽にできるようになっています。

pipelined-migratorのコンソール
pipelined-migratorのコンソール

PostgreSQLについてはAWS Database Migration Service(以下DMS)を利用しています。pipelined-migratorがまだ現段階ではMySQLにしか対応しておらず、かつ社内でPostgreSQLの利用が比較的少ないため、一時的にDMSを利用しています。PostgresSQL対応版も開発中であり、今後PostgreSQLからのテーブル取り込みもpipelined-migratorに一本化していく予定です。

DynamoDBについてはまだ実績が少ないのもあり、定型化しているものの手作業でインポートしています。 DynamoDB Streamでデータの更新を検知し、LambdaとKinesis Firehose経由で、S3に追加データを吐き出します。S3に配信後は後述するログデータと同様にRedshift Spectrumでクエリアクセスができるようになります。

DynamoDBデータの取り込みフロー
DynamoDBデータの取り込みフロー

pipelined-migratorはbricolageとmys3dumpを組み合わせて作られています。

入力: ログデータの取り込み (log data flow)

ログデータの取り込みには、2017年の記事に書かれたStreaming Loadシステムと昨年末に書かれた記事に登場したPrismの両方が使われています。 クックパッドのDWHではRedshift Spectrumを中心にして構築するようにデータ移行作業を実施中です。このため、Streaming Loadシステムからのログ取り込みはゆくゆくは退役する予定です。 現在は移行期であるため外観図にはStreaming LoadからRedshift内部へのロードとPrismからRedshift外部(Spectrumでアクセス可能なS3バケット)の2経路が同時に存在していますが、次にまたDWH外観図を書く頃にはPrismに一本化されていることでしょう。

サービス開発者側から「新規にログを取り始めたい」となった場合、以下の手順でロードを行います。

  1. *.strdefというYAML形式のファイルにログ定義を書く
  2. tech/dwhというDWHが管理するリポジトリへ上記ファイル追加PRを出してもらう
  3. ログ関係者&DWHチームメンバーがログ定義に関するレビューをする
  4. アプリケーションから送られてきてS3にログが到達したしたことを確認し、strdefファイルの適用を行う

サービス開発者に1~3までを行ってもらい、DWHでは3~4を担当します。 こうしてみるといちいちログをとるのに手順が多く、面倒に思われるかもしれません。このフローで運用している背景には、ログを取りたいと思った人にstrdef定義を通してきちんとログ設計をしてもらいたいという意図があります。これはDWHに限らない話ですが、ロギングが始まってしまえばログは修正できません。プログラムのリファクタ感覚でカラム名や型定義は変更できないのです。どうしてもやむを得ず発生することはありますが、問題を先送りにした場合はツケが回ってきます。ログ取り自体は気軽に行えるが、その設計にはきちんと考える時間を取りましょうという思いがあって、こういった手順となっています。

Streaming Load・Prismのどちらもコンソールが用意されており、ログが順次ロードされていく様子を確認できます。

Streaming Loadのコンソール
Streaming Loadのコンソール
Prismのコンソール
Prismのコンソール

Streaming Loadの実装はbricolage-streaming-preprocssorとbricolage-streaming-loaderとして公開されています。

Redshift内部での加工処理

Prism登場によりRedshift Spectrum活用が進んできましたが、Redshift内部の処理に関しては既にだいぶ完成していたため新たに手を加える必要はありませんでした。 2017年当時と同様、bricolageを用いて書かれたSQLバッチをKuroko2というジョブ管理システムで定期実行しています。Redshift内部のデータアーキテクチャについても従来通りの入力層・論理DWH層・論理データマート層の3層区切りとなっています。ただ、2年間運用してきたことでこれら各層に格納されているデータは充実してきており、2年前と比べて3層ともに成長しています。

Redshift内部の加工処理はbricolageとKuroko2を組み合わせて作られています。

出力: アドホックな分析

社内からのアドホックな分析に用いられるツールに関しては特に変化がなく、Bdash・Postico・Jupyter がそのほとんどです。あるいはTableau Desktopでアドホックな分析作業を行っているかもしれません。これら各ツールは全て内部テーブルと同じようにSpectrumテーブルへアクセスできるため、各自分析者が自分の使いたいツールを自由に選べる状態になっています。

分析者は後述するDmemoやSlackにおけるデータ分析お悩み相談チャンネル、#data-analysisを駆使して社内のデータを分析しています。日々、#data-analysisやissue上で分析用SQLのレビューが行われています。

BdashはこちらのGithubリポジトリで公開されています

出力: ダッシュボード用BIツール

from redash to tableau

2017年の記事においてRedashからTableauへの移行を検討中と書かれていましたが、現在では完全に移行体制が整い社内のほとんどのダッシュボードはTableau Serverに移行されました。移行に至った理由は過去の記事にも書かれた通り、Redashのキューまわりの実装に難があったためです。

Tableau Serverに移行した今では各部署で活用されており、エンジニア以外にも職種を問わず広く利用されています。

ただし、Tableau ServerとTableau Desktopではライセンスが別契約となるため、実際にダッシュボードを作りたい人はライセンス申請が必要となります。この申請フローは定型化されており、ヘルプデスクへ申請を出すだけでライセンスキーが割り当てられるようになっています。

Redashは諸事情がありまだ完全退役とはなっていませんが、アクセス&更新がなされているダッシュボードはほぼありません。

出力: DWH内のデータベースドキュメント

データベースドキュメント管理システム dmemo のご案内にて登場したDmemoも現役で活躍中です。 Dmemoは毎晩Redshiftにアクセスし、DWH内部にある最新の全テーブル情報を取り込みます。取り込んだDB・スキーマ・テーブル・カラムの各階層ごとに説明を書くことができ、その履歴も残せます。Dmemoに十分情報が蓄積されていれば、PR上でのやり取りやデータ分析についてSlack上で聞かれたときなどにDmemoのURLを貼って一言二言伝えるだけでスムーズにデータに関する情報伝達が行えるようになります。

Dmemo操作例
Dmemo操作例(Techlife用にローカル開発環境上でダミーデータを用いて撮影したものです)

新しく入社した社員やインターン生にデータに触れてもらうときに一旦DmemoのURLを共有しておくとその後の話が円滑に進むようになります。また、ログに関する何かしらのインシデントが発生した際には「v1.0.0のアプリケーションではIDがズレている」等々のメモを書いておくことで、後でログデータを集計して奇妙な結果が出たときに即座に気づくことができるようになります。

Redshift Spectrum移行作業に合わせて、外部テーブルやRedshift特有の機能であるlate-binding viewにも対応しました。

DmemoはこちらのGithubリポジトリで公開されています。

出力: バッチ処理用バルクエクスポート

こちらも2017年の記事と変わらず、Queueryとredshift_connectorが使われています。他のサービスからも利用できるようなDWHを構築しておくことで、データ活用基盤が分析のみならず様々なサービスやプロダクトにまで活用されるようになります。

過去にTechlifeでご紹介したデータ活用基盤を利用したシステム運用の記事を下記に載せておきます。

Redshiftから外部システムへのバルクエクスポートはQueueryとredshift_connectorが使われています。

DWHの課題

上記がデータ活用基盤の外観図となります。しかし、まだこれで完成ではなくこれからも開発を続けていく必要があります。最後に、DWHに残っている主な課題について述べたいと思います。

Redshift Spectrum移行

ログデータ取り込みの項目でも書きましたが、現在は内部テーブルへロードする旧方式のStreaming Loadと外部テーブルへロード(S3へのParquet変換)する新方式のPrismの両システムが並列で稼働しています。 Streaming Loadでのロードを廃止するためには内部テーブルに依存している全ジョブの停止・外部テーブルへの移行が必要となります。 186のジョブと284テーブルを一つ一つ検証した上で移行と削除を行っていく作業は自動化ができない、泥臭い手作業となります。その中で歴史的経緯に基づく仕様を発見したり、現状のジョブが間違っていたり等が発見されていきます。 この移行作業についてはDWH総出で丁寧に移していく他無く、地道にやっていくことになります。

Tableau Server運用

Tableau Serverの運用も依然として作業コストがかかっており、なんとかすべき課題です。 まず、Tableau ServerはTableau社側から新しいバージョンが提供されるたびにアップグレードしていく必要がありますが、ダッシュボードのポータルサイトという重要なサービスであるためこの作業は慎重に行う必要があります。アップグレード作業の際には事前に社内アナウンスを出し、メンテナンス時間を確保して行っています。 過去にアップグレード作業に失敗し、Tableauサポートと連絡をとりつつアップグレードをするということもありました。このアップグレード作業をより低コストに抑えることができないか、というのがTableau Server運用における1つ目の課題です。

また、TableauはSlackとの連携が弱いという弱点があります。一応、メール通知とZapierという外部のオートメーション化サービスを組み合わせることで自動通知を実現できますが、こちらにも実は問題があります。現在クックパッドでは専用botでの通知運用をしつつ、Tableau公式によるSlack連携機能がくる日を待ちわびています。

さらにライセンス管理についても課題があります。先にはダッシュボード用BIツールの項目で「ヘルプデスクへ申請を出すだけでライセンスキーが割り当てられる」と書きましたが、定型化したとはいえここは裏側で手作業が発生しています。根本的な負荷削減にはなっていないため、できる限りライセンス付与の作業負荷がなくなるよう自動化をしたいところです。

データ活用の推進

データ活用基盤が整いつつある今、基盤業務のその先のデータ分析の啓蒙活動に比重を置くフェイズに突入しつつあります。 今までも社内でSQL勉強会が開かれたり等、各所でデータ分析の民主化は進んできていました。ですが、草の根運動に頼るだけでなく、DWHが組織として継続的にデータ活用を推進していく必要があります。 こちらに関しては具体的な方策やマイルストーンもありませんが、来期からの一番の課題となることでしょう。

まとめ

今回は以前お伝えしてから2年経ったデータ活用基盤の全体像をお話しました。一度に書ききるには多すぎるため、前回との差分を中心に書きましたのでぜひ2017年に公開した記事と比較してみてください。

私達、技術部データ基盤グループは「クックパッドの全社員がデータに基づいた意思決定を行えるようにする」を目標に日々の業務に取り組んでいます。 分析者の望む最強のデータ分析環境を提供したい方、余計な手間をを要せず分析作業にのみ集中できる最高のデータ分析環境に浸ってみたい方、ぜひ私達と一緒にデータを駆使してより良いサービス作りをしてみませんか。

クックパッド株式会社 | クックパッド株式会社 採用サイト

クックパッドアプリ(Android)の開発効率化のためにやったこと/やっていること

$
0
0

モバイル基盤部のこやまカニ大好き(id:nein37)です。 モバイル基盤部では、CI環境の改善やアプリのリリースサイクル自動化といった開発・リリースフローの効率化に加え、アプリのビルド速度改善や開発のしやすさを改善する様々な取り組みを行っています。 今回はその中から、クックパッドアプリ(Android)に対して行った開発効率化の取り組みの一部を紹介したいと思います。

あわせて読みたい : Android版クックパッドアプリで採用している技術の現状確認 2018年版

日々のメンテナンス系

不要になったソースコードやリソースの削除

気がつくとどこからも参照されなくなったソースコードやリソースはどうやっても発生するので定期的に消しています。 特にモバイル基盤部のタスクと決まっているわけではないですが、単に綺麗になって嬉しいこととapkサイズが少しでも小さくなれば良いという気持ちで手の空いた時に Android Studio の Analyze ツールや konifar/gradle-unused-resources-remover-pluginで検出されたものをシュッと消しています。

Lint設定の最適化/Lint警告の除去

クックパッドアプリでは以前、朝Lintという取り組みで細かいLint指摘事項への対応を行っていました。 この取り組みは警告も減るしなんとなく成果が出た気持ちになって良いものだったのですが、最終的に修正コストが異常に高かったり本当に対応が必要かと思えるような警告が残って誰も手を付けられなくなり、やがて途絶えてしまいました。

僕もすっかり朝Lintのことを忘れていたのですが、ある時なんとしても令和の朝Lintというプルリクエストを出したいと思い手元で Lint を動かしてみるとおよそ280件の指摘事項があり、うちいくつかはクックパッドアプリでは考慮しなくても良いようなものだとわかりました。 lintOptionsを見ると、これは逆に対応したほうが良いと思えるものもいくつかあります。 この lintOptionsをクックパッドアプリの現状に合わせたものを令和の朝Lintとしてプルリクエストにしました。

f:id:nein37:20191021143111p:plain

このときのプルリクエストでは各指摘事項の変更に対して、なぜ enable/disable にするのか、どのように修正すれば良いのかをコメントしておいたので、今後 Lint 設定を再度見直すときにも利用できると考えています。

f:id:nein37:20191021143140p:plain

同じくモバイル基盤メンバーの吉田さんが社内ブログでも朝Lint活動について広報してくれていて、朝Lintという習慣がひっそりと復活しつつあります。

※ クックパッドアプリではプルリクエストに含まれる変更に関する Lint や ktlintの指摘事項は Danger によってプルリクエスト中に指摘され修正するようになっており、朝Lintの対象となるコードを増やさないような仕組みづくりも同時に行っています。

画像リソースのWebP化/WebPおじさん化

あるときどうしても apk サイズを小さくしたくなり、画像リソースをまとめてWebPに変換しました。 Android Developers にはWebPはサイズが小さくて最高とか画像リソースをWebPに変換するとビルドが速くなるといった夢のようなことが書いてあり、それを信じて変換しました。 Android Studio には WebP の変換ツールが組み込まれていて、アプリ内の画像を一括で WebP に変換することができます。 また、クックパッドアプリは minSdkVersion21 になっているため、ロスレスや透過といった WebP の機能をフルに活かすことができるのも利点でした。 Lossy WebP への変換では画質が劣化するために以前のバージョンと比較する必要がありますが、Lossless WebPへの変換であれば理論上画質の劣化はありません。 WebP に乗り換えるためにすべての画面のすべての画像の画質チェックをしなくても良いのは便利でした。

f:id:nein37:20191021143203p:plain

6月頃に一括でアプリ内の画像およそ1000個の画像を Lossless WebP に変換し、その結果、apk サイズを1.2MB縮小することが出来ました。 それ以降は主にプルリクエストレビュー時に WebP おじさんとして活動しています。

f:id:nein37:20191021143229p:plain

得られた知見として、 Lossless WebP への変換ではほとんどの場合画像サイズが小さくなりますが、以下の2パターンではうまく縮小できませんでした。

  1. 元画像が十分に小さい Indexed Color のみで縮小されたPNGである場合
  2. 元画像がまったく軽量化されていない巨大な画像である場合

1.はWebPに変換した場合に仕組み的に縮小される余地がないためか、ほぼ誤差範囲ですがPNGよりもWebP画像のほうが大きくなる事があります。 Android Studio の変換ツールに容量が節約できない場合は変換をスキップするという設定があるため、この場合は変換対象から外しています。

2.は Lossless WebP ではなく Lossy WebP で変換すべきものです。正直見た目では Lossless にすべきか Lossy にすべきかわからないので、現状は解像度とファイルサイズをみて判断しています。

将来的にはLossless/Lossy WebPへの変換はDangerに指摘させることができると良いなと思っています。この記事を書いている途中でプルリクエスト内に png, jpg や大きすぎる WebP が含まれている場合は Danger に指摘させるような修正を入れたのでWebPおじさん業はなくなりました。

Danger により機械化されたWebPおじさんの様子 f:id:nein37:20191021143256p:plain

なお、ビルド速度への影響は計測できないほどわずかでした。

minSdkVersion 21 後の変更

Ripple 対応

これまで backgroundリソースを StateListDrawableで切り替えてタッチフィードバックを実装していたような箇所を Rippleによるタッチフィードバックに置き換えていきました。 API 21から RippleDrawableも使えるようになっていて、この部分は素直に minSdkVersion21 の恩恵を受けられた部分でした。

android:elevation の指定で影をつける

当たり前なんですが古い端末での挙動とか何も考えずに elevation で影が落ちるというのが本当に楽で良いのです。

*-v21系代替リソースの整理

Style や Theme 系リソースに *-v21で分岐させていたリソースがあったので、minSdkVersion 21 を期に整理しました。 これまで Material Design のバックポートに関する知識がないと Theme を変更するのが難しかったのですが、minSdkVersion21 になったことで Theme や Style の編集はだいぶ簡単になりました。

ツール導入など

AndroidKTX導入

クックパッドアプリ内の Kotlin 比率が高くなって来たのでAndroidKTXを導入しました。 去年の11月時点ではクックパッドアプリのおよそ20%が Kotlin でしたが、現在ではさらに Kotlin への置き換えが進み半分ほどがKotlinで書かれている状態です。 f:id:nein37:20191021143324p:plain

Firebase Performance Monitoring

去年突然アプリのパフォーマンス監視がしたくなりFirebaes Performance Monitoringを導入しようとしました。 このときはクックパッドアプリが依存していた一部のjarと競合してうまく導入できなかったのですが、今年になり Android Gradle Plugin を更新したりFirebase Performance Plugin 自体が更新されたりした結果、いつのまにか導入できるようになっていました。 現在は社内の主要なアプリでは大体有効になっていて、特にレスポンスが遅い API の特定やアプリ起動時間の測定に使われています。

Stetho から Flipper への乗り換え

もともと Stetho を利用していたのですが、Mirrativ tech blogさんの記事を参考に Flipper を試してみたら良かったので乗り換えました。 アタッチしなくても良いのは本当に素晴らしく、アプリのデータ削除などでプロセスキルを挟んだ場合でもアプリを立ち上げれば自動でログを見られるようになるのはとても便利です。

R8導入

Android Gradle Plugin v3.4 でデフォルト有効になったR8ですが、クックパッドアプリではそれ以前からR8を利用して難読化処理を利用するとビルド時間が倍になるという問題が発覚していました。 BetaやRCでバージョンが上がるたびに試していましたがまったく改善せず、とうとう v3.4 が stable になっても解決しなかったため一時的にR8を無効化していました。 そのままでもアプリのビルドはできるのですが、標準ツールが自分のアプリで利用できないのは悲しいので、定期的に時間をとって調査していました。

やるぞ!という気持ちのissue f:id:nein37:20191021143401p:plain

あるとき annotations.jarという古代のProguard設定の仕組みがビルド時間に影響をしていることに気が付き削除することで無事に有効化することができましたが、普段から最新のビルドツールや設定を試して問題を検出することの重要性を感じました。

マルチモジュール関連

以前マルチモジュールにしていく話をしてから一年以上経ち、クックパッドアプリも25モジュール構成になりました。 最初の頃は StyleThemeなどをまとめたUIモジュールやログ用の仕組みをまとめたログモジュールなど、比較的変更の少ない静的なモジュールばかり切り出していましたが、これらのモジュールは切り出した後もほとんど変更が入っておらずビルドキャッシュを最大限利用できた上、位置づけもわかりやすいので早めに切り出しておいてよかったと思います。 そこから各種機能をモジュールに切り出す作業を進めていますが、:legacyに依存しないモジュールで機能を実装できるようになるまではまだもう少し掛かりそうです。

モジュールの命名

最近 Google Developers の日本語ブログにもAndroid のモジュールのパスに関するちょっとしたヒントという記事が出ていましたが、クックパッドアプリでもAndroidビューにおけるモジュール表示がわかりにくいという問題は早い段階で発覚していました。 上記の記事で紹介されていたような projectDirによる解決も検討したのですが、結局 features_のような prefix をモジュールにつけることにしました。 クックパッドアプリでは単純な解決方法を選ぶという方針でこのようにしましたが、このあたりはチームやモジュール分割の方針によって最適解が変わると思います。

モジュール切り替えによるアプリの設定変更

以前のクックパッドアプリはビルドバリアントによって接続先とデバッグ機能の有無を切り替えていました。

  • stage flavor dimension(開発用設定の切り替え)
  • mode flavor dimension(接続先設定の切り替え)
    • internal(社内ステージングサーバ向け、Hyperionなどの開発用機能あり)
    • external(本番サーバ向け、リリース用設定)
  • buildType
    • debug(minify,Proguard なし)
    • billingBeta(決済確認用の特殊なビルド)
    • release(minify, Proguard あり、リリース用証明書)

上記設定の組み合わせによってビルド時に必要な設定を利用していましたが、マルチモジュール構成のプロジェクトではアプリがこれらのビルドバリアントを設定している場合、依存しているライブラリプロジェクトにも同様のビルドバリアントを設定する必要があります。 (ライブラリプロジェクトに同名のビルドバリアントが存在しない場合、 ./gradlew testProdInternalDebugUnitTestのようなテストコマンドでライブラリプロジェクトのテストが実行されなくなる場合があります)

モジュールが増えていくにしたがってこの設定が面倒になり、 Android Studio 上でのビルドバリアントの切り替えも大変になってきたことから、 flavor ではなく依存先モジュールの切り替えによって接続先の切り替えや開発用機能の追加を行うように切り替えました。 変更後の各モジュールの依存は以下のようになっています。(dev flavor は minSdkVersion 21 化したことにより分岐がほぼなくなったので不要になりました)

  • :app_cookpad (本番サーバ向けビルドをリリースするためのモジュール)
    • :settings_external (本番サーバの接続先情報モジュール)
  • :app_cookpad_internal社内向けアプリをビルドするためのモジュール)
    • :settings_internal (社内サーバの接続先情報モジュール)
    • :features_debug (開発用機能モジュール)
  • :app_cookpad_billingBeta決済確認用のアプリをビルドするためのモジュール)
    • :settings_internal (社内サーバの接続先情報モジュール)
    • :features_debug (開発用機能モジュール)
    • アプリモジュールとして定義したことにより buildType billingBeta は廃止しました

この変更により、今まで ./gradlew assembleProdExternalReleaseという呪文のようだったビルドコマンドが ./gradlew :app_cookpad:assembleReleaseだけで良くなります。 AndroidStudio 上でもビルド対象のモジュール(=必要なアプリの種類)とbuildType(=minify,proguard,証明書)だけ意識すればよくなり、GUIでの操作もかなり簡略化されました。 クックパッドアプリはこれまでの長期間の開発で Gradle ファイルがかなり複雑化していたのですが、ビルドバリアントの整理とモジュール分割によってそれぞれ設定を書く場所がわかりやすくなり、多くの部分を共通化してシンプルな構造になっていきました。 この方式を採用するとモジュール数はどうしても増えていくのですが、クックパッドアプリのような大きいプロジェクトでも ./gradlew testDebugUnitTestのような基本的なコマンドが何も考えなくてもちゃんと動くというのは開発のしやすさという点で非常に重要だと思っているので、今年やっておいてよかった変更の一つだと考えています。

おわりに

僕の趣味で比較的地味な変更ばかり紹介してしまいましたが、今年はこの他にも多くの(機能追加以外の)変更が行われています。 モバイル基盤部ではこれからも新機能を簡単に開発し、素早くユーザーに届けるためにモバイルアプリの開発効率化を続けていきます。

興味がある方はぜひ一度クックパッドオフィスに遊びに来てください。 https://info.cookpad.com/careers/

Firebase In-App MessagingのUIをカスタマイズして運用する

$
0
0

Komerco事業部エンジニアの岸本(@_sgr_ksmt)です。
昨年Cloud Firestoreのrulesのテストを全てローカルエミュレータを使うように書き換えた話を書いてからだいぶ間が空いてしまいましたが投稿します。

今回はFirebase In-App Messagingを利用する際にカスタムUIを適応して運用している話をしたいと思います。

f:id:sgrksmt:20191025105713p:plain:w300

In-App Messaging

Firebase In-App Messaging(以下FIAMと呼びます)は、指定した条件で絞り込んだアクティブユーザーに対して、
メッセージやボタンのアクションを設定し、アプリ内で表示するためのFirebaseの一つの機能です。
表示形式としてはいわゆる「ポップアップ」「画面上部のバナー」といった形式で表示することが可能です。

ユーザーに出すための条件にアプリのターゲット、バージョン、オーディエンス、ユーザープロパティといった情報を活用することができるので、特定のユーザーにプロモーションを行うことが容易にできます。

また、メッセージを表示するUIはSDK側が標準で提供してくれるので、クライアント側はSDKをインストールするだけで実装完了になります。 また、FIAMの設定では文字の色、ボタンのアクションなど、ある程度カスタマイズすることも可能になっています。

f:id:sgrksmt:20191025105732p:plain:w600

標準で使う場合の難点

しかし、標準のまま活用するとUIに関して次のような問題点がでてきます。

  • 文字の大きさ、フォントを変更することができない
  • 文字や画像の並びを変更することができない
  • 一部の表示形式だとボタンの背景色を変更できない
  • ボタンの大きさを変更することができない


表示用UIを標準で用意してくれているのは大変うれしいのですが、どうしてもサービスのUIと比べると浮いてしまうことと、カスタマイズ可能な範囲が狭いというのが難点になってきます。
もしかしたら読者の中にはその標準UIが微妙でFIAMの使用を断念してしまった方も居るのではないでしょうか。 というわけで、次項からカスタムUIを適応していく方法を紹介します。 (※ちなみにKomercoはiOSアプリのみ配信している関係で以降の内容はiOSでのカスタムUIの適応の話になります。

カスタムUIを適応する

公式のドキュメントにあるこちらの内容を紐解きながら解説していきます。

Podfileの編集

Podfileを開いて、Firebase/InAppMessagingDisplayFirebase/InAppMessagingに変更してインストールし直します。

- pod 'Firebase/InAppMessagingDisplay'+ pod `Firebase/InAppMessaging'


CustomMessageDisplayComponentクラスの作成

次に、InAppMessagingDisplayプロトコルに適合したCustomMessageDisplayComponentクラスを作成します。
displayMessage(_:displayDelegate:)メソッドを実装し、引数で渡ってくるmessageのデータを判別し「Card」「Modal」「Banner」「Image Only」それぞれ表示するUIを出し分けるようにします。 以下はCardタイプの場合に、サービス内で使用しているPopupクラスを活用して表示する例を示しています。

import Firebase
import Foundation

privateenumIAMDisplay {
    case unknown
    case card(InAppMessagingCardDisplay)
    case modal(InAppMessagingModalDisplay)
    case banner(InAppMessagingBannerDisplay)
    case imageOnly(InAppMessagingImageOnlyDisplay)

    init(_ messageForDisplay:InAppMessagingDisplayMessage) {
        switch messageForDisplay.type {
        case .card:self= (messageForDisplay as? InAppMessagingCardDisplay).map { .card($0) } ?? .unknown
        case .modal:self= (messageForDisplay as? InAppMessagingModalDisplay).map { .modal($0) } ?? .unknown
        case .banner:self= (messageForDisplay as? InAppMessagingBannerDisplay).map { .banner($0) } ?? .unknown
        case .imageOnly:self= (messageForDisplay as? InAppMessagingImageOnlyDisplay).map { .imageOnly($0) } ?? .unknown
        @unknowndefault:self= .unknown
        }
    }
}

finalclassCustomMessageDisplayComponent:InAppMessagingDisplay {
    funcdisplayMessage(_ messageForDisplay:InAppMessagingDisplayMessage, displayDelegate:InAppMessagingDisplayDelegate) {
        DispatchQueue.main.async {
            displayDelegate.impressionDetected?(for:messageForDisplay) // ★switch IAMDisplay(messageForDisplay) {
            caselet .card(card):
                Popup.show(
                    title:card.title,
                    body:card.body,
                    image:URL(string:card.portraitImageData.imageURL),
                    primaryButton:card.primaryActionButton.buttonText,
                    secondaryButton:card.secondaryActionButton?.buttonText,
                    buttonActionHandler: { button inswitch button {
                            case .primary:
                                print(card.primaryActionURL)
                                // URLを開く処理case .secondary:
                                print(card.secondaryActionURL)
                                // URLを開く処理
                        }
                    }   
                )
            caselet .modal(modal):// Modalタイプの場合の表示実装
            }
        }
    }
}



messageForDisplay.typeを見ることでどの表示形式か判定できるのでそれを活用し、更にそれぞれの表示形式で扱うクラスにダウンキャストして使用します。 各種表示形式でアクセスすることが出来る情報(プロパティは次のようになっています)

f:id:sgrksmt:20191025105720p:plain:w600


また、カスタムUIを表示する際はで示しているdisplayDelegate.impressionDetected?(for:)メソッドを呼び出す必要があります。 例で示しているPopupクラスはUI含めてご自身で実装してください。

messageDisplayComponentを指定する

CustomMessageDisplayComponentを実装できたら、In-App Messagingに適応します。 次のコードをFirebaseApp.configure()の呼び出し以降で設定します。可能であればこの呼出の直後に次のコードを書くと良いでしょう。

InAppMessaging.inAppMessaging().messageDisplayComponent = CustomMessageDisplayComponent()


これで、In-App Messagingの配信をアプリが受け取った際にカスタムUIで表示することが可能になります。

カスタムUIを使う際のルールを決めておく

Komercoでは、カスタムUI側でフォントの色などを指定して運用するようにしたため、 FIAMでのメッセージ配信の設定画面では色に関する設定はしない(無視する)ようにしています。

f:id:sgrksmt:20191025105735p:plain:w600

Before/After

ここまで実装ができると、同じ設定でも変更前後でこのようにUIが変わります。

Before After
f:id:sgrksmt:20191025105659p:plain:w300f:id:sgrksmt:20191025105650p:plain:w300

デバッグがしやすくなるTips

ちょっとしたTipsですが、FIAMは指定したアナリティクスイベントを発火させないと表示されないですが、次のようにコードでメッセージ配信設定したアナリティクスイベントの名前を指定してあげると即座に表示させることができます。

InAppMessaging.inAppMessaging().triggerEvent("show_product_detail")



- 参考: In-App-Messagingのキャンペーンを手動で呼び出せるようになった

メッセージ表示やボタンタップ時のイベントをアナリティクスに別途送りたい

カスタムUIでメッセージ表示をしたり、ボタンを押した時に別途アナリティクスイベントを収集する場合は、messageForDisplay変数からキャンペーン名を取得することができるため、これを活用するとどのキャンペーンでのイベント発火だったのか判断することができます。

f:id:sgrksmt:20191025105741p:plain

letcampaignName= messageForDisplay.campaignInfo.campaignName // 設定したキャンペーン名が取得できる
Logger.postLog(.showInAppMessaging(campaignName:campaignName))


注意点

displayDelegate.impressionDetectedの呼び忘れに注意

displayDelegate.impressionDetected?(for:)メソッドを呼び忘れすと、SDK側でユーザーが見たかどうかの集計が行われないため、条件に設定しているアナリティクスイベントが発火するたび何度もユーザーに表示されてしまいます。

ポップアップの表示制御が必要なら別途実装する

もしカスタムUIで表示するポップアップのクラスをサービス内の別の場所で使用していたり、FIAMの制御外でも非同期通信を経て何かしら表示する可能性があったりする場合は自信で重複して表示されないように制御ロジックを実装しておきましょう。

funcdisplayMessage(_ messageForDisplay:InAppMessagingDisplayMessage, displayDelegate:InAppMessagingDisplayDelegate) {
    DispatchQueue.main.async {
        if Popup.isAlreadyShown { return }
        // カスタムUI表示処理を続行
    }
}


カスタムUIを適応すると標準UIは使えなくなる

この方法でカスタムUIを適応した場合、標準UIを呼び出すことはできなくなります。 例えば、Card、Modalタイプであれば用意したUIを使い、Banner、Image Onlyタイプであれば標準のUIを呼び出す、といったことは不可能です。
もし標準のUIを実装したい場合は、「SDK側」のソースコードを参考に作成するか、そのタイプの使用を諦めるのも一つの手になります。

KomercoではBannerタイプ、Image Onlyタイプの配信は行わない事にしたので表示実装はしていません。

まとめ

カスタムUIを適応してあげることで、よりサービスに馴染んだ形でメッセージ配信を行うことができるのでよりプロモーションに活かせるようになると思います。
標準UIを敬遠して使ってなかった方、そこがネックでFirebaseを使っているにも関わらず自前でポップアップの配信機能を実装をしていた方、これを機にカスタムUIを適応してFIAMを使ってみてはいかがでしょうか。

クックパッド採用説明会「クックパッドはサービスの作り手を採用したいんです。」を開催しました!

$
0
0

こんにちは、メディアプロダクト開発部の長田(おさだ)です。

クックパッドは、エンジニア、デザイナーを絶賛大募集しています。先日「クックパッドはサービスの作り手を採用したいんです。」というイベントを開催したのでその時の様子をお伝えします。
https://cookpad.connpass.com/event/149581/

開始

まずはクックパッドの紹介から始まりました。

f:id:osadake212:20191107175307j:plain

ご存知ない方もいらっしゃるかと思うのですが、クックパッドではレシピサービス以外にも、スマートキッチンサービスの OiCyクッキング Live 配信が視聴できる cookpadLiveおいしい食べ方を学習できる たべドリ生鮮食品 EC プラットフォームの cookpad mart料理が楽しくなるマルシェアプリ komercoなど、たくさんのサービスを開発しています。

また、レシピサービスは全世界に展開しており、2019年10月末時点で73カ国/地域、30言語に対応しています。全世界の月間利用者数は1億人近く、投稿されているレシピ数は590万品を突破しています。

完成されたサービスに見えるとよく言われるクックパッドですが、実はやりたいことの1%もできていないのが現状です。
「毎日の料理を楽しくする」ことで世の中をよくしていきたい我々は、レシピサービスにとどまることなく、 いくつもの新しい機能や新規事業を立ち上げ拡大している最中です。

このイベントは、そんなクックパッドで一緒にサービスを作ってくれる仲間を探すために開催されました。
発表のセクションでは、クックパッドのサービス開発の様子をクックパッドマート、 cookpadLive の開発を通して紹介しました。

発表① クックパッドマート ディレクターのいない◯◯な開発スタイル

クックパッドマートからは、長野 佳子(@naganyo)・米田 哲丈(@tyoneda)による、開発スタイルについての発表を行いました。

f:id:osadake212:20191107175316j:plain

クックパッドマートでは「なにをつくるか」をどのように決めるのか、またそれをどのように実現しているのかについて紹介しました。

「なにをつくるか」は KGI ブレークダウンで決まるのがベースになります。
さらにそれだけではなく、サービスに対する「気づき」を得る機会を増やし様々な観点からサービスを俯瞰することで、なにをつくるかが日々生まれています。
生まれてきた「なにをつくるか」は事業的な観点から絞り込まれ、開発・リリースされていきます。

また、「どうやってつくるか」についてはディレクターがいないので、全員で起案し、デザインも開発も同時に進めています。
アイデアの可視化、開発しながら機能をブラッシュアップ、リリース後にチューニングするなど、職種にとらわれない「サービスの作り手」が集まっています。

詳しくはこちらの資料をご覧ください。
https://speakerdeck.com/tyoneda/20191030-kutukupatudomato-deirekutafalseinaioonakai-fa-sutairu

発表② cookpadLive 短期間で行うサービス開発術

cookpadLive からは、若月 啓聡(@puzzeljp)・長田 卓哉(osadake212)による、こちらも開発スタイルについての発表を行いました。

f:id:osadake212:20191107175325j:plain

cookpadLive では、短期間でサービスを実現するために、デザイナー・エンジニアがどのような動きをしているのかをそれぞれの視点で工夫していることを紹介しました。

デザイナーはビジネス要件(納期)も考慮しながらエンジニアがより高速に開発ができる取り組みをしています。
取り外し可能なデザインを作成することで、サービスの価値を損なわないことを担保しながら、エンジニアの実装の都合に合わせて柔軟にデザインを変更することが可能になります。

エンジニアは、設計や実装だけではなく役割を越えて、アイデアだし・仕様・画面遷移の検討・オペレーションの検討を行い、サービス開発に積極的に関わっています。
職種にとらわれず、全員がサービスの成長に対してできることを実行しながら開発を進めています。

詳しくはこちらの資料をご覧ください。
https://speakerdeck.com/osadake212/cookpadlive-duan-qi-jian-dexing-usabisukai-fa-shu

Q&Aパネルディスカッション

このセクションでは、イベント開始前に受け付けていた質問や、発表を聞いて気になった質問に対して社員が答える形式で、サービス開発や社内の様子等についてディスカッションしていきました。

f:id:osadake212:20191107175329j:plain

ありがたいことに、タイムテーブルの40分では答えきれないくらいの質問をいただきました。
質問内容は、クックパッドマート・cookpadLive のサービスについて、クックパッド全体のサービス開発チームの雰囲気について、入社してからの働き方についてなど、サービス開発だけではなく幅広くご質問をいただきました。

個別面談・懇親会

f:id:osadake212:20191107175311j:plain

「イベントでいきなり個別面談ってなに?」となりそうですが、本イベントでは希望者の方に、エンジニア採用責任者の一人である勝間(買物事業部 副部長)、デザイナー採用責任者の一人である倉光(デザイン戦略部 部長)と個別に一対一でお話ができる場を設けました。
一人10分弱の面談枠をそれぞれ4つ用意していたのですが、いったい何人の方に個別面談していただけるのだろうか・・・と不安だったのですが、なんと全ての枠が埋まり8名の方と面談することができました。

懇親会では、弊社のサービス開発に関わっているエンジニア、デザイナー6名がそれぞれテーブルにわかれ、ざっくばらんに参加者とお話させていただきました。
私、長田のテーブルでは、クックパッドの開発基盤の話から、サービスのグロースの話、チーム・プロジェクトマネジメントの話など、本当にたくさんのことについて話すことができ、弊社の様子をより伝えることができたのではないかと感じます。

まとめ

冒頭でも触れたように、クックパッドでは一緒にサービスを作ってくれる仲間を募集しています。
現在募集中のポジションはこちらからご確認いただけます。
https://info.cookpad.com/careers/jobs/

「まずは話だけでも聞いてみようか」というのも大歓迎なので気軽にお声がけいただけると幸いです。
recruit@cookpad.com

Viewing all 802 articles
Browse latest View live