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

【開催レポ】Cookpad Tech Kitchen #15 〜料理動画・広告のBtoB領域の開発事情〜

$
0
0

こんにちは!人事部の冨永です。

2018年3月28日に「Cookpad Tech Kitchen #15 〜料理動画・広告のBtoB領域の開発事情〜」を開催しました。クックパッドではこのイベントを通して、技術やサービス開発に関する知見を定期的に発信しています!

第15回の今回は、最近リリースをしたクッキングLIVEアプリ「cookpadTV」をはじめとした料理動画事業の開発の裏側や、クックパッドの広告配信周りの開発について等をテーマに、7名が登壇しました。アプリからバックエンドまで具体的な事例を用いてたっぷりとご紹介したイベントの様子をお届けします🎉

cookpad.connpass.com

クックパッドの料理動画事業をご紹介🎬

まずはクックパッドの料理動画事業を簡単にご紹介します。

クッキングLIVEアプリ「cookpadTV」ライブ配信を見ながら、プロの料理家やシェフから料理を学んだり、有名人と一緒に料理を楽しむことができるアプリです。コメント機能を使ってわかりづらいポイントをその場で質問でき、双方向型コミュニケーションを実現した新しいクッキングアプリとなっています!また、クックパッドに投稿されているレシピの1分動画もご覧いただけます。ぜひ使ってみてくださいね ♪ App Store: https://itunes.apple.com/app/id1344736966 Google Play: https://play.google.com/store/apps/details?id=com.cookpad.android.cookpad_tv

「cookpad storeTV」全国のスーパーマーケットなどの流通チェーンと連携し、店頭の生鮮売り場にクックパッドオリジナルのサイネージ端末設置して料理動画を配信するサービスです。2018年1月より、大手食品・飲料メーカーの商品を活用した料理動画広告のトライアル配信も開始しております。あなたのお家の近くの店舗でも見られるかも!

詳細はこちら:~クックパッド、料理動画事業に本格参入〜第1弾は『cookpad storeTV』大手流通チェーンと連動し、売場で料理動画を配信12月より日本全国のスーパーマーケット約1,000店舗にてスタート

「cookpad studio」クックパッドスタジオは、クックパッドのレシピ投稿者が自分のレシピを動画にするために、無料で利用できる動画撮影スタジオです!動画の撮影経験がない初心者の方でもクオリティの高い料理動画の制作が可能です。完成動画はクックパッド内で公開できるほか、Instagramをはじめとする個人のSNSにも投稿することが可能です。昨年12月に東京・代官山に1号店をオープンし、今年5月には心斎橋に2号店をオープン予定です!公式ページはこちら:「cookpad studio」

これらの事業に沿って、それぞれのメンバーが何を開発しているのか、どんな技術を使っているのか、何を目指しているのかといった内容をお話しました。

f:id:mamiracle:20180423214451j:plain(cookpad storeTVのサイネージ端末)

cookpadTVのライブ配信の裏側

まずはじめにAndroidエンジニア石本と、サーバーサイドエンジニア長田からクッキングLIVEアプリ「cookpadTV」のコンセプトやアプリ構成についてお話しました。LIVE配信・コメント・いいね機能などに関する技術的課題と工夫についてお話しました。

speakerdeck.com

speakerdeck.com

cookpadTVのアプリ開発〜現状とこれから〜

iOSエンジニアの鶴川からは、クッキングLIVEアプリ「cookpadTV」の技術構成や、今後の展望についてお話しました。現在はプラットフォーム間での挙動や体験の違いを、技術的アーキテクチャの統一により解決することを目標にしており、それを前提に置いた基盤開発の工夫についてもご紹介しました。

speakerdeck.com

cookpad storeTVの開発事例

サーバーサイドエンジニアの三浦からは、動画サイネージ事業「cookpad storeTV」の開発事例をご紹介しました。ユーザー(店舗でのサイネージ端末運用者)が使いやすいサービスの設計や、それを実現するために工夫した技術についてお話しました。

speakerdeck.com

cookpad studioでの撮影環境について

iOSエンジニアの氏からは、ユーザー向け料理動画撮影スタジオ「cookpad studio」での、カメラ選定や通信方法などといった撮影環境構築についてお話しました。撮影に適した環境をつくるまでのガジェット選定や設計の工夫などについて詳しくご紹介しました。

speakerdeck.com

動画事業でのデータ収集、分析、活用

サーバーサイドエンジニアの今井からは、動画サイネージ事業「cookpad storeTV」のログの活用方法についてお話しました。当時苦戦してた課題から、現在導入しているTableau®の事例までをご紹介しました。

speakerdeck.com

cookpad storeTV 広告配信 いままでとこれから

サーバサイド / Androidエンジニアの中村からは、動画サイネージ事業「cookpad storeTV」における広告配信についてお話しました。店舗に設置したサイネージに動画広告の配信をしており、これからは imp ベースで行っていきます。

speakerdeck.com

付箋形式でお答えするQ&Aディスカッション😊

Cookpad Tech Kitchenではより気軽に質問をしていただくために、付箋で質問を集めています。この日はメディアプロダクト開発部長 渡辺が司会となってQAディスカッションを行いました。

f:id:mamiracle:20180423214502j:plain(付箋に質問を貼ってもらいます)

アプリのアーキテクチャでAndroidとiOSで統一することでUXを統一できる理由が知りたいです

アーキテクチャを統一することで、API へのリクエストタイミング、エラーハンドリング等の設計がプラットフォーム間で共有出来るようになるからです。そうすることで、ローディングの時間やエラーダイアログの表示等をあわせやすくなり、結果的に UX を似せることが出来ます。

cookpadTVのアプリ開発が少数精鋭で驚きました。レビューはどうして回していたのでしょうか?

レビューに関しては正直自分達のチームだけでは回しきれない部分があったので、技術部モバイル基盤チームのメンバに手伝って貰っていました。

ログの量が多そうですがデータ量削減のための工夫はしていますか?(圧縮形式など)

クックパッドのログ基盤はしっかりしているので、サービス側から送るログの量を制限して欲しいという依頼は来たことがありません。そのため、データ量削減の工夫はしていません。

ほんの一部ではありますが、上記のような質問にお答えしました!

シェフの手作り料理🍳

Cookpad Tech Kitchen ではおもてなしの気持ちを込めてシェフ手作りのご飯を振る舞っています!食べながら飲みながらカジュアルに発表を聞いていただけるように工夫しています。みなさまも次のイベントはぜひ遊びに来てくださいね。

f:id:mamiracle:20180423214534p:plain(オリジナルロゴケーキと陳麻婆豆腐焼きそば)

f:id:mamiracle:20180423214521p:plain(いつも美味しいご飯を作ってくださる川嶋先生)

クックパッドでは仲間を募集しています😊

今回のイベントでは、最近リリースした、料理動画事業や広告事業に関する新規サービスについてご紹介しました。ぜひこれを機にみなさまにもクックパッドの料理動画サービスを使っていただけたら嬉しいです。

そしてクックパッドでは料理動画事業や広告事業、その他新規事業、レシピサービス事業などに携わる新しい仲間を募集しています!クックパッドで「毎日の料理を楽しみにする」ことに興味を持ってくださった方は、ぜひ下記のリンクからご応募もお待ちしております。

Android アプリケーションエンジニア(料理動画・広告配信)

バックエンドエンジニア(料理動画・広告配信)

iOSアプリケーションエンジニア(料理動画・広告配信)

また、今後のイベント情報についてはConnpassページについて随時更新予定です。イベント更新情報にご興味がある方は、ぜひメンバー登録をポチっとお願いします!

cookpad.connpass.com


cookpadTV ライブ配信サービスの”突貫” Auto Scaling 環境構築

$
0
0

 インフラや基盤周りの技術が好きなエンジニアの渡辺です。

 今回は私が開発に関わっている cookpadTVの Auto Scaling 環境を突貫工事した事例をご紹介します。 同じチームのメンバがコメント配信回りについても書いていますので興味があれば合わせて読んでみてください。

クッキングLIVEアプリcookpadTVのコメント配信技術

 本エントリは Amazon EC2 Container Service(以降 ECS) をある程度知っている方向けとなっています。 細かいところの説明をしているとものすごく長文になってしまう為、ご了承頂ければ幸いです。

Auto Scaling について

 一般的に Auto Scaling は CPU やメモリ利用量によって増減させるのが一般的です。*1私が属している部署で開発保守している広告配信サーバも、夕方のピークに向けて CPUUtilization の最大値がしきい値を超えたら指定台数ずつ scale-out するように設定しています。 scale-in も同じく CPUUtilization が落ち着いたら徐々に台数を減らしていっています。

 クックパッドもサービスとしてはピーキーなアクセスが来るサービスで、通常だと夕方の買い物前のアクセスが一番多いです。 ECS での Auto Scaling 設定当初は 5XX エラーを出さず、scale-out が間に合うように細かい調整を行っていました。

イベント型サービスと Auto Scaling

 ライブ配信の様にコンテンツの視聴出来る時間が決まっていて特定の時間にユーザが一気に集まるサービスでは、負荷に応じた scale-out では間に合わないことがあります。 立ち上げ当初はユーザ数もそこまで多くなかったということもあり、前述の負荷に応じた Auto Scaling 設定しか入れていませんでした。 その為、一度想定以上のユーザ数が来たときに scale-out が間に合わずライブ配信開始を 40 分ほど遅らせてしまいました。 基本的に cookpadTV のライブ配信の番組は週一で放送されていますので、1 週間以内に対応しないと同じ番組でまたユーザに迷惑を掛けてしまいます。 そこで cookpadTV チームではこの問題を最優先対応事項として改善を行いました。

まずはパフォーマンス・チューニング

 Auto Scaling の仕組みを改善する前にまずは API 側のコードを修正することで対応を行いました。 こういう細かい処理のロジックを見直すことで最適化、高速化することももちろん効果的ですが、ピーキーなアクセスで最も効果的なのは短期キャッシュです。 キャッシュは適切に使わないと思わぬトラブルを生むことが多く、出来れば使用は避けたいものです。 しかし短期キャッシュであれば情報の更新頻度次第ではあまり問題にならない場合があります。 そして今回のケースにおいてはライブ配信の時間は事前に決定し変更はされない、レシピ情報も事前に確定することがほとんです。 その為、短期キャッシュを入れる事でキャッシュのデメリット無くアプリケーションのスループットを上げることが出来ると判断しました。

 想定外のユーザ数が来た時に一番問題になったのは cookpadTV の更に先にある別の Micro Service(サービス B) への API リクエストでした。 サービス B は Auto Scaling 設定が入っておらず、大量にアクセスが流れてレスポンスタイムが悪化、cookpadTV API の unicorn worker が詰まって Nginx がエラーを返し始めるという状況でした。 ここの部分の API コールを短期キャッシュすることでスループットを大幅に上げることが出来ました。

f:id:wata_htn:20180426204103p:plain

Auto Scaling の改善

 さて、アプリケーション自体の改善は 1 週間以内で出来ることはやりました。 次は Auto Scaling 側の改善です。 冒頭でも記載した通り、負荷が上がった後では間に合わないのでライブ配信が始まる前に scale-out を済ませておく必要があります。 傾向からしてもライブ配信開始 15 分前ぐらいから徐々に人が来はじめ、直前ぐらいに push 通知を受けてユーザが一気に来ます。 その為、何かあったときに備え、余裕を持って 15 分前には scale-out を済ませておきたいと考えました。

クックパッドでの ECS Service の Auto Scaling

 クックパッドでは ECS Services の desired_count、pending_count、running_count を定期的にチェックして、pending_count を解消できるように EC2 インスタンスが scale-out されるようになっています。 その為 desired_count を何かしらの仕組みで増やすことが出来れば、後は EC2 インスタンスも含め scale-out されていきます。

単純に desired_count を増やせば良いわけではない

 ただし、単純にライブが始まる 30 分前に desired_count を増やすだけではまだ負荷が高くない為、徐々に scale-in されてしまいます。 さらに、API サーバは全配信で共有のため、複数番組同時に放送されると時間帯によっては単純に「番組配信前に指定 task 数増やす」だけではうまく行きません。 事前に scale-out したのが配信前に scale-in してしまっては意味が無いので、desired_count を単純に上げるのではなく min_capacity をライブ開始 30 分前に指定し、ライブ開始時間に min_capacity を元に戻す方式を採用しました。 この時限式に min_capacity を調整するのは、Aws::ApplicationAutoScaling::Client#put_scheduled_actionを使用して実現しています。

 コードとしては以下のような形です。

defschedule_action(episode)
  scale_out_time = [episode.starts_at - 30.minutes, 1.minute.after].max
  scale_in_time = episode.starts_at
  aas.put_scheduled_action({
    service_namespace: "ecs",
    schedule: "at(#{scale_out_time.getutc.strftime('%FT%H:%M:%S')})", # UTC で指定する必要がありますscheduled_action_name: "EpisodeScaleOut##{episode.id}",
    resource_id: "service/xxxx/cookpad-tv-api",
    scalable_dimension: "ecs:service:DesiredCount",
    scalable_target_action: {
      min_capacity: reserved_desired_count(from: scale_out_time), # ここで scale-out するための min_capacity を計算
    },
  })
  aas.put_scheduled_action({
    service_namespace: "ecs",
    schedule: "at(#{scale_in_time.getutc.strftime('%FT%H:%M:%S')})", # UTC で指定する必要がありますscheduled_action_name: "EpisodeScaleIn##{episode.id}",
    resource_id: "service/xxxx/cookpad-tv-api",
    scalable_dimension: "ecs:service:DesiredCount",
    scalable_target_action: {
      min_capacity: reserved_desired_count(from: scale_in_time + 1.second), # ここで scale-in するための min_capacity を計算
    },
  })
enddefaas@aas ||= Aws::ApplicationAutoScaling::Client.new
end

 やっている事を図で表すと以下の通りです。

f:id:wata_htn:20180426204251p:plain

 min_capacity が引き上げられると結果的に desired_count も引き上げられ、running task 数が増えます。 上の図の結果、running task 数は以下の図の赤線の推移をします。

f:id:wata_htn:20180426204317p:plain

 そして、ライブ配信が被った時間帯に配信されることも考慮して、重複した時には必要数を合計した値で min_capacity をコントロールするようにしました。 勿論 min_capacity を戻す時も被っている時間帯の配信も考慮して計算しています。 先程の図に番組 B が被った時間に配信されるとすると以下の様になります。

f:id:wata_htn:20180426204341p:plain

running task 数で表現すると以下となります。

f:id:wata_htn:20180426204406p:plain

 そして、番組毎に盛り上がりが違うので、それぞれ違う task 数が必要です。 そこで番組毎の必要 task 数を、以前の近しい時間帯に配信された番組の視聴ユーザ数から割り出すようにしました。 前回とかでなく「近しい時間」としているのは、番組によっては週によって配信時間が変わったりし、そして平日だとお昼よりも夜の方が来場数が多かったりするからです。

以前の配信のユーザ数等のデータ処理

 さらっと「以前の近しい時間帯に配信された番組の視聴ユーザ数」と書きましたがこれは以下のように利用できるようにしています。

  1. ユーザの視聴ログから bricolageでサマリを作成(Redshift -> Redshift)
  2. redshift-connector + Queuery を使って MySQL にロード

 クックパッドでは全てのログデータは Amazon Redshiftに取り込まれるようになっていて、そのデータを Tableauを使って可視化しています。 それをデータ活用基盤を利用して加工、アプリケーションの MySQL まで取り込んでいます。

 後は番組情報が作成、更新されたらその付近で配信予定の番組も合わせて min_capacity が再計算されるようになっています。 これらによって予約された Auto Scaling を管理画面からも確認出来るようにしました。

f:id:wata_htn:20180426204438p:plain※画像はイメージです

初回と突発的な対応

 以前の配信がある場合はこれで対応出来ますが、初回や突発ケースにはこれだけでは不十分です。 初回はさすがに読めない部分が多いのですが、SNS での拡散状況等や、番組のお気に入り数等から来場ユーザ数を人が推測し予め設定出来るようにしました。 来場ユーザ数が指定されていると、それに必要な task 数を計算し、上記の流れと同じ様に Auto Scaling が行われます。 なかなか完全にはシステム化は難しく、こういう人のが介在する余地はまだまだ必要だったりします。 (勿論 SNS の情報を引っ張ってきて、人の予測ロジックをアルゴリズム化しても良いのですが)

退出

 忘れていけない事として、ライブ配信開始時の突入だけでなく退出があります。 ライブ視聴を終わったユーザが一気にアプリの他の画面に移動するため、ライブ開始時と同じぐらいの負荷が来ます。 ここをクリアするために scale-in はゆっくり目にして、一気に task 数が減らないようにして対処しています。 ライブ配信の終了時間は読めないため、ここは予定を組んで置くことが出来ないためです。

 参考までに突入と退出の負荷がどれくらいなのかリソース利用量のグラフを貼っておきます。

f:id:wata_htn:20180426204505p:plain

今後の発展

 同時アクセスが大量に来るのは push 通知によっても発生することがあります。その為、今後は remote push 通知を送信する前に送信件数をベースに Auto Scaling する仕組みを導入していく想定です。

補足

 Micro Services でサービスを開発していると、他チームの Service に依存して、自分達の Service だけ Auto Scaling してもサービス全体としては成り立たないことがあります。 その為その境界線を意識し、自分達の開発しているサービス内でカバーできるような設計にしていく必要があります。 キャッシュやリトライ戦略は各 Service が個別に開発するというよりはサービスメッシュ*2によって統合管理が達成出来るのではと考えています。

最後に

 イベント型サービス向けの Auto Scaling が必要になってから、突貫で作った形ではありますがクックパッドの既存の基盤のおかげでなんとか運用が回る Auto Scaling 環境が出来ました。 この辺りの基盤がしっかりしている事は今回非常に助かりました。

 さて、今回の事例紹介は以上ですが自分だったら、もっと改善出来る!という方で一緒にやっていきたいと思ってくださった方は是非私までお声がけください。

S3に保存したログファイルをストリーム処理するサーバーレスアプリケーションの紹介

$
0
0

インフラストラクチャー部セキュリティグループの水谷(@m_mizutani)です。

クックパッドでは現在セキュリティ監視の高度化に取り組んでおり、その一環としてセキュリティ関連のログ収集およびその分析に力を入れています。ログ収集の部分では可用性などの観点からAWSのオブジェクトストレージサービスであるS3に一部のサービスやサーバのログをまず保存し、後から保存されたファイルを読み込んで分析などに利用しています。分析のためにS3に保存したファイルを前処理する方法としてAWS Glueなどを用いたバッチ処理がありますが、到着したログをなるべくストリームデータのように扱いたい場合もあります。特にセキュリティ関連のログでは以下のようなユースケースで利用しています。

  • アラートの検出: ログを検査してその中から危険度の高いと考えられるログを探し出し、アラートとして発報します。アラートの具体的な例としてはオフィス環境からの不審な通信やスタッフ用クラウドサービスでの不審な活動、スタッフPC上の不審イベントなどが挙げられ、ログの各種パラメータをチェックして危険度を判定します。
  • ログの変換と転送: S3に蓄積されたログの形式を変換して別のデータソースへ転送します。具体的にはGraylogに転送してログ分析をしやすいようにしたり、Athenaによって検索できるように変換して別のS3バケットに配置するといったことをしています。

こうした処理はバッチで対応できないこともありませんが、例えばセキュリティ分析の文脈においては、2, 3分程度の遅延であれば許容できても、1時間単位の遅れが発生するのは少し困ります。できる限り到着したものから順次処理して分析までのレイテンシを短くしたいところです。本記事ではこのような処理に対し、S3に保存したログファイルをなるべくレイテンシが短くなるように処理するためのアーキテクチャ、そしてそのアーキテクチャのためのフレームワークを紹介したいと思います。

最もシンプルなS3オブジェクトの処理構成とその課題

AWSの環境において、S3に到着したログを処理するのに最もシンプルな構成はどのようなものでしょうか? おそらく下の図のように、S3にオブジェクト(ファイル)が生成されたというイベントによってサーバーレスコンピューティングサービスであるAWS Lambdaが起動し、その後起動したLambdaがオブジェクトそのものをダウンロードして処理する、という構成かと思います。

f:id:mztnex:20180502110255p:plain

この構成はシンプルで構築しやすいのですが、実際に運用してみるといくつかの課題が見えてきました。

  • S3のObjectCreatedイベントの送信は、1つのS3バケットにつき1つの宛先にしか設定できない: 単独のサービスしか動かしていない場合は直接Lambdaを起動するだけで事足ります。しかし、S3にオブジェクトが生成されたタイミングで何か処理をしたいという要求が2つ以上でてくると、直接Lambdaを起動するのではなく別の方法を考えないとなりません。
  • 流量制限が難しい: 例えばS3に保存されたオブジェクトを変換して別にデータストアに投入するというタスクでは、投入する先のデータストアに対する流量を気遣う必要があります。データストアの種類にもよりますが流量を超えすぎると全体のパフォーマンスが低下したり、データをロスするといったことが起きる可能性があります。当然、予想される流量に対して受け側のキャパシティを確保してくのは重要なのですが、ログの種類によっては一過性のバーストがしばしば起こります。S3から直接Lambdaを起動しているとこういった任意のタイミングで外部から流量をコントロールするのは難しいです。AWSの機能でLambdaの同時実行数を制限してスロットリングさせる方法もありますが、処理順序が時系列と大幅にずれたり複数回失敗して自前で再試行しないとならないといったところで煩雑になってしまいます。
  • エラーのリトライが大変: システムやサービスを常に更新・拡張していると実運用におけるエラーの発生は避けられません。よってエラーが起きたリクエストに対して再試行するという場面はたびたびあるのですが、これをなるべく簡単にやりたいという思いがあります。例えばエラーの発生はCloudWatch Logsに実行時ログとして残すことができますが、実行時ログの量が多くなるとそもそも検索が難しくなるなどの問題があります。また、どのエラーに対して再試行したのか・していないのかを手動で管理することになり、煩雑になってしまいます。

実際に使われているサーバーレスアプリケーションの構成図

f:id:mztnex:20180502110311p:plain

いくつかの課題を解決してスムーズな運用を試行錯誤した結果、上記の図のような構成に落ち着きました。シンプルな構成に比べるとそれなりに複雑なサーバーレスアプリケーションに仕上がりましたが、課題となっていた部分を解決しています。S3にオブジェクトが作成されてからどのように処理されるかというワークフローを、順を追って以下で説明します。

  1. S3のイベントはLambdaで直で受けるのではなくSNSで配信するようにしています。これによって複数の処理を実行したい場合でもSNSからイベントを受け取ることができるようになります。
  2. S3のObjectCreatedイベントを受けた EventPusherというLambdaが一度これをKinesis Streamにイベントを流します。
    • Kinesis Streamは一度データを投入すると指定時間(デフォルトで24時間)保持されるので、これ以降の処理を一時的に止めなければいけなくなってもイベントを蓄積し、あとから自由に読み出すことが可能です。
    • Kinesis Streamには Fast-laneSlow-laneという2つを用意しています。前者が高優先、後者が低優先にS3オブジェクトを処理するもので、EventPusherが送られてきたイベントの中身に応じてどちらかに振り分けます。それぞれのStreamに設定の違いはありませんが、この先にあるDispatcherの役割が異なります。
  3. それぞれKinesis Streamからイベントを受け取ったFastDispatcher と SlowDispatcherMainを非同期で呼び出します。Mainはシンプルな構成のLambdaに該当し、最終的にユーザがやらせたい処理を請け負います
    • 便宜上、 FastDispatcherSlowDispatcherと名前をつけていますが、構成に違いはありません。そのかわり、 EventPusherでなるべく遅延なく処理したいと思うものを Fast-laneからの FastDispatcher、 多少遅延してもいいものを Slow-laneからの SlowDispatcherにそれぞれ振り分けます
    • 各 Dispatcher には DELAYという環境変数が設定されており、これに整数値 Nをセットすることで Mainを呼び出した後に指定した N秒 sleep した後に Dispatcher が終了します。Kinesis StreamからはLambdaは直列&同期的に呼び出されるため、時間単位で呼び出される回数が抑制され、全体の流量が抑制されます。基本的には SlowDispatcherの遅延を増やすことを想定しています。
    • 特に流量制限はこのサーバーレスアプリケーションの外部のメトリック値(例えばDBに実際に投入されるデータの流量など)を参照するためこの図には流量制御の仕組みはでてきませんが、例として定期的にメトリック値をチェックしたりCloudWatch Alarmのような仕組みで遅延を調整するという方法が考えられます。
  4. 起動した Mainはイベント情報として作成されたS3オブジェクトのバケット名とキーが引き渡されるので、実際にS3からオブジェクトをダウンロードして必要な処理をします。
  5. Mainの処理が適切に終了しなかった場合、2回まで再試行されますが、それでも正常終了しなかった場合はDLQにエラーの情報が引き渡されます。ここでは実装上の都合などからSNSを使っています。
  6. DLQ(SNS)経由で Reporterが呼び出され Lambda の requset_id と呼び出し時の引数(EventPusherから射出されたイベント)が引き渡されます
  7. Reporterは渡されたエラーの内容を粛々と ErrorTaskTableに投入します。ここまでで自動的に処理されるデータフローは一度終了します。

これ以降は保存されたエラーをどのように対応するか、というワークフローになります。

  1. 任意のタイミングでユーザが Drainを起動します。瞬間的なバーストや不安定なリソースによる失敗は (5) の2回までの再試行である程度解消されると期待されますが、それ以外のコード由来のエラーなどは何度試行しても同じ結果になるためループさせるとシステム全体の負荷になってしまします。なので、ここまでエラーで残ったものについてはユーザが確認してから再度処理のワークフローに戻すのが望ましいため手動で発火させる仕様にしています。
  2. 起動した DrainErrorTaskTableからエラーを全て、あるいは選択的に吸い出してます。
  3. Drainは吸い出したエラーになったイベントを再度Kinesis Streamに放流します。この時送信するイベントは EventPusherが作成したものですが、 EventPusherはKinesis Stream投入時にどちらのstreamに投入したかの情報を付与しているため、もともとのstreamに戻されます。

このようなワークフローで処理することで、エラーが起きた場合でも少ない手間で修正・再実行し開発と運用のサイクルを回していくことができます。

実装

上記のサーバーレスアプリケーションを展開するために slipsというフレームワークを実装して利用しています。このフレームワークはPythonのライブラリとして動作し、導入したプロジェクトを先程のサーバーレスアプリケーションの構成で動かすための大部分をサポートしてくれます。サーバーレスアプリケーションを作成する時に苦労するポイントの1つにコンポーネントの設定や各コンポーネント同士のつなぎ込みといった作業があるかと思います。slipsは「S3からログファイルを読み込んで順次処理をする」というタスクを実行するサーバーレスアプリケーション作成におけるコンポーネントの設定、および統合をサポートしてくれます。これによって開発者がサーバーレスアプリケーションのコンポーネントを構成する際の消耗を最小限におさえ、ログデータを処理する部分の開発に集中できます。

slipsの実態としてはCloudFormationのラッパーになっています。サーバーレスアプリケーションのコンポーネントを制御するフレームワークとしては他にもAWS Server Application ModelServerless Frameworkが挙げられますが、ここの部分を自作したのは以下のような理由によります。

  • 構成の共通化: 冒頭で複数の処理に利用したいと書きましたが、最終的に読み取ったログの処理する部分以外はほとんどが共通した構成のサーバーレスアプリケーションになります。また、構成そのものをアップデートしてよりよい処理に変えていく必要があるため、複数のほぼ同じ構成を管理するのは冗長でメンテナンスの負荷が大きくなってしまいます。そのため、大部分の構成の定義を自動的に生成してCloudFormationで展開するというアプローチになっています。
  • 環境による差分の調整: ほとんどが同じ構成と上述しましたが、一方で環境ごとに微妙な構成の差もあるためこれを吸収する必要があります。例えばある環境ではコンポーネントを自動生成できるが、別の環境では専用の承認フローに基づいて作成されたコンポーネントを利用する必要がある、といった状況を吸収する必要があります。既存フレームワーク内にこういった環境ごとの差分を簡単なロジックで書き込むことは可能ですが、数が多くなったりロジックが複雑になったりすると管理が難しくなるので、割り切ってPythonのコードとしてロジックを作成しています。

このフレームワークを利用することで、先程のサーバーレスアプリケーションにでてきたAWS上の構成をほぼ自動で展開してくれます。先程のアーキテクチャの図を元に説明すると以下のような構成になります。

f:id:mztnex:20180502110328p:plain

S3へのログ保存とSNSへの通知設定、そしてS3に蓄えられたログをパースした後の処理(例えば、形式を変換したり、どこかへ転送したり、ログの内容を検査したりなど)を実施するためのコードは自前で用意する必要がありますが、それ以外の部分のコンポーネントをCloudFormationを利用してシュッと構築してくれます。これを利用することで、S3のログで何か処理をしたいユーザ(開発者)はS3に保存されたログが途中どのように扱われるかを意識することなく、最終的な処理の部分に集中することができるようになります。

導入と設定ファイルの作成

まずPythonのプロジェクト用ディレクトリを作成します。適当に作成したディレクトリに入って virtualenv などで環境を作成し、pipenv で slips を導入します。

$ virtualenv venv
$ source venv/bin/activate
$ pipenv install -e 'git+https://github.com/m-mizutani/slips.git#egg=slips'

次に設定ファイルを作成します。ファイル名を config.ymlとします。サンプルはここにありますが、本記事では要点のみ解説します。

backend:
  role_arn:
    event_pusher: arn:aws:iam::1234xxxxxxx:role/LambdaSlamProcessorEventPusher
  sns_topics:
    - name: SecLogUplaod
      arn: arn:aws:sns:ap-northeast-1:1234xxxxxx:seclog-event

上記は構築するサーバーレスアプリケーションの各要素の設定を記述します。ObjectCratedのイベントが流れてくるSNSトピックの指定は必要ですが、 role_arnの項では既存のIAMロールを割り当てることもできますし、省略すると必要なIAMロールをCloudFormationで自動的に追加します。同様にKinesis StreamやDLQに使われるSNSも、ARNを指定すれば既存のものが利用され、指定しなければ必要なものが自動的に生成されます。

これは組織や環境によって各リソースの管理方法が異なるため、別途作成されたリソースを使う場合とCloudFormationで自動的に生成する場合を切り替えることができるようにしています。例えばクックパッドでは本番環境のIAM権限などいくつかのリソースはGithub Enterprise上のレポジトリで厳密に管理されていますが、開発環境では比較的自由にリソースを作成できるようになっています。このような環境にあわせて容易に展開できるような実装になっています。

handler:
  path: src/handler.py
  args:
    sample

上記の設定は自分が実行させたい処理についての定義をしています。pathで実行させたいファイルのパスを指定し、argsで実行時に渡したい引数の内容を指定します。

bucket_mapping:
  mizutani-test:
    - prefix: logs/azure_ad/signinEvents/
      format: [s3-lines, json, azure-ad-event]
    - prefix: logs/g_suite/
      format: [s3-lines, json, g-suite-login]

上記のパートでどのS3バケットにどのようなフォーマットのオブジェクトがアップロードされるかを指定します。これを記述しておくことで、自分で書くスクリプトはパース済みのデータを受け取れるようになります。backet_mapping以下のキーがS3バケット名、その下のリストでS3キーのプレフィックスとパースするためのフォーマットを指定します。上記例にでてくる指定は以下のような意味になります。

  • s3-lines: S3のオブジェクトをテキストファイルとして1行ずつファイルを読み込む
  • json: 1行をJSON形式としてパースする
  • azure-ad-event: AzureADのログ形式だと仮定して、タグ付けしたりタイムスタンプを取得する

コードの準備

設定の準備ができたら、自分が処理したいコードを用意します。デフォルトだと ./src/以下においたコードがzipファイルにアーカイブされてAWS上にアップロードされるので、今回は ./src/handler.pyというファイルを作成します。サンプルとしてログの件数をCloudWatchのカスタムメトリクスに送信するコードを書いてみます。

import boto3
import slips.interface

classLogCounter(slips.interface.Handler):
    defsetup(self, args):
        self._namespace = args['namespace'],
        self._count = 0defrecv(self, meta, event):
        self._count += 1defresult(self):
        metric = {
            'MetricName': 'LogCount',
            'Value': float(self._count),
            'Unit': 'Count',
        }

        cloudwatch = boto3.client('cloudwatch')
        cloudwatch.put_metric_data(Namespace=self._namespace, MetricData=[metric])

        return'ok'# Return some value if you need.

slips.interfaceモジュールに含まれている slips.interface.Handlerクラスを継承してクラスを定義すると、これが読み出されて実行されます。新しく作成するクラスには以下のメソッドをそれぞれ定義します。

  • def setup(self, args): 最初に1度だけ呼び出されます。argsには handler設定項目内の argsで指定した構造データが渡されます
  • def recv(self, meta, event): パースしたログ1件に対して1度呼び出されます。metaはパースする過程で得られた付加情報が格納されています。eventは辞書型のパース済みログデータが渡されます
  • def result(self): 終了時に呼び出されます。returnで値を返してテストなど利用します

デプロイ

以上の準備ができたらAWSの環境にデプロイできます。ディレクトリには以下のようになっているかと思います。参考までに、サンプルの構成をgithubにあげておきます。

$ tree
.
├── Pipfile
├── Pipfile.lock
├── README.md
├── config.yml
├── src
│   └── handler.py
└── venv
    ├── bin
(省略)

この状態で slipsdeployコマンドを実行します。実行にはCLI用のAWSのCredential情報が必要です(詳しくはこちら)以下が実行結果になります。

$ slips -c config.yml deploy
2018-04-13 14:40:07.379 INFO [cli.py:466] Bulding stack: log-counter
2018-04-13 14:40:07.379 INFO [cli.py:154] no package file is given, building
2018-04-13 14:40:07.736 INFO [cli.py:470] package file: /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmpem2eyhgf.zip
2018-04-13 14:40:07.736 INFO [cli.py:474] no SAM template file is given, building
2018-04-13 14:40:07.763 INFO [cli.py:481] SAM template file: /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmp1bahl7dn.yml
2018-04-13 14:40:07.763 INFO [cli.py:418] package command: aws cloudformation package --template-file /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmp1bahl7dn.yml --s3-bucket home-network.mgmt --output-template-file /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmpa1r9z4e7.yml
2018-04-13 14:40:08.652 INFO [cli.py:431] generated SAM file: /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmpa1r9z4e7.yml
2018-04-13 14:42:11.138 INFO [cli.py:461] Completed (Applied)
2018-04-13 14:42:11.138 INFO [cli.py:559] exit

これがうまくいくとバックグラウンドでCloudFormationが実行され、対象のアカウントに必要なリソースがバーンと展開されます。同時に、CloudWatchのダッシュボードも自動生成されるので、それを見るとこのサーバーレスアプリケーションの実行状況がある程度把握できます。

f:id:mztnex:20180502110345p:plain

まとめ

AWS Lambdaや各種マネージドサービスを使ったサーバーレスアプリケーションは便利に利用できる反面、実行の制御やエラー処理などの勝手がインスタンス上とは異なり、実行の制御やエラー処理をスムーズに実施できるような構成にする必要があります。これをサポートする機能やサービスもAWSには充実しており、クックパッドでは日々それらを活用しながら改善に取り組んでいます。

今回はS3からログを順次読み取って処理するサーバーレスアプリケーションの構成と、それを補助するためのフレームワーク slipsを紹介しました。今後、新たにサーバーレスアプリケーションを構築する方の参考になれば幸いです。

Cookpad Spring 1day Internship 2018 を開催しました

$
0
0

技術広報を担当している外村です。

今年クックパッドでは、2月から3月にかけて、一日で最新の技術を学ぶインターンシップを以下の4コース開催しました。

  • サービス開発コース
  • インフラストラクチャーコース
  • Rustプログラミングコース
  • 超絶技巧プログラミングコース

Cookpad Spring 1day Internship 2018

たくさんの学生に参加していただき、真剣に課題に取り組んでくれました。参加していただいが学生のみなさん、ありがとうございました。

それぞれのコースの内容について簡単に紹介します。

サービス開発コース

サービス開発コースはクックパッドのサービス開発プロセスを一日で学ぶという内容のコースです。「大学生のおでかけの課題を解決するスマホアプリ」をテーマに、エンジニアとデザイナーがペアになって課題解決のためのサービスを考えてもらるという内容でした。

ユーザーインタビューやペルソナの作成から始まり、価値仮説やユーザーストーリを考えて最終的にペーパープロトタイピングを作ってもらいました。全チームがプロトタイプを作成するところまでやり終え、クックパッドのサービス開発プロセスを体感してもらうことができました。

インフラストラクチャーコース

インフラストラクチャーコースでは、AWS を使った Web アプリケーションインフラ構築を行いました。小さな Rails アプリケーションを題材に、サービスの成長に沿って必要になってくる要素について解説し、それを構築して使う、というものです。

実際の流れは以下のようなものでした。

  • まずは小さい EC2 インスタンスにとりあえずアプリケーションをデプロイ
  • Itamae を使ってインスタンスをセットアップしてみる
  • systemd を触ってみる
  • Rack サーバを production 向きのものに変更する
  • コマンドやプロファイラを使ってパフォーマンスモニタリングをする
  • RDBMS のチューニングをする (インデックスの作成や N+1 クエリへの対処)
  • Web サーバ (nginx) のチューニングをする
  • DB を Amazon RDS に分離し、アプリケーションサーバをスケールアウトする
  • ロードバランサを導入する
  • memcached などのキャッシュを導入する

構築しているアプリケーションは常にベンチマーカーによってチェックされており、作業によるパフォーマンスアップを実際に感じることができるようになっていました。

「なかなかインフラに関わることってなくて...」という方に多くご参加いただけたので、なかなか新鮮な体験をしていただけたようです。

Rustプログラミングコース

Rust プログラミングコースでは、プログラミング言語 Rust を使ってリバーシゲームを作りました。

午前パートでは Rust の鬼門と言われる Ownership、 Borrowing、 Lifetime という概念の解説をしました。いかにして Rust が GC なしで安全なメモリ管理を実現しているのかについて、スクリプト言語の経験しかない受講者にも理解できるよう、ヒープとスタックの違いなどといったプロセスのメモリモデルの説明から紐解いていきました。

午後のパートでは実際にコードを書き、講師が用意したコードの穴を埋める形でリバーシゲームを実装していきました。実際に遊べるゲームが完成することで、受講者は達成感を感じられていたようです。早めに完動させることに成功した受講者は各々自由な改造をして楽しんでいました。

講義で利用したコードは以下になります。

KOBA789/rust-reversi

超絶技巧プログラミングコース

超絶技巧プログラミングコースは、あなたの知らない超絶技巧プログラミングの世界の著者で、Rubyコミッタでもある遠藤を講師として、技巧を駆使した実用性のないプログラムを作成する手法を学ぶという内容の講義でした。

前半の講義ではプログラムをアスキーアートにする方法や、Quine(自分自身を出力するプログラム)を書く方法について学び、後半は実習として各自に超絶技巧プログラム書いてもらいました。テトリスや将棋のQuineなど、皆さん超絶技巧を使って面白い成果物を作成されていました。

Summer Internshipのご案内

Cookpad Summer Internship 2018

クックパッドでは、毎年恒例になっているサマーインターンシップを今年も開催いたします!今年のインターンシップは、サービス開発エンジニア向けに 10 Day Tech インターンシップ、リサーチエンジニア向けに 5 Day R&D インターンシップ、デザイナー向けに 5 Day Design インターンシップを行なうというかたちになっています。

10 Day Tech インターンシップでは、モバイルアプリケーションからサーバーサイド、インフラストラクチャーまで、フルスタックエンジニアとして必要な技術をぎゅっと前半の講義・課題パートに詰め込んでいます。後半では、クックパッドの現場に配属されて研修を行なうOJTコースと、前半で学んだ内容をチームで実践するPBLコースに分かれてインターンシップを行ないます。

5 Day R&D インターンシップは、機械学習や自然言語処理を専攻とする修士課程・博士課程の方を対象に実施します。クックパッドの膨大なデータという大学の研究では経験することが難しい生のデータに触れることのできる、貴重な機会です。

また、5 Day Designは、参加者同士でチームを組み、料理に関する課題を解決するサービスを発想し形にします。メンターからサポートを受けながら、ユーザーが抱える課題をインタビューを通して理解し、その解決策のデザイン・プロトタイピングを行ないます。実践と平行して、クックパッドが持つサービスデザイン手法やノウハウを講義形式で学びます。

皆さまのご応募をお待ちしております!

Service Mesh and Cookpad

$
0
0

こんにちは、開発基盤の Taiki です。今回は、マイクロサービスで必須のコンポーネントとなりつつあるサービスメッシュについて、クックパッドで構築・運用して得られた知見についてご紹介できればと思います。

サービスメッシュそのものについては以下の記事や発表、チュートリアルで全体感をつかめると思います:

目的

クックパッドでは主に障害対応やキャパシティプランニング、Fault Isolation に関わる設定値の管理といった、運用面での課題を解決すべくサービスメッシュを導入しました。具体的には:

  1. サービス群の管理コストの削減
  2. Observability*1*2の向上
  3. より良い Fault Isolation 機構の構築

1つ目については、どのサービスとどのサービスが通信していて、あるサービスの障害がどこに伝播するのか、ということを規模の拡大とともに把握しづらくなってるという問題がありました。どことどこが繋がっているかの情報を一元管理することでこの問題は解決できるはず、と考えました。

2つ目については (1) をさらに掘ったもので、あるサービスと別のサービスの通信の状況がわからないという課題でした。例えば RPS やレスポンスタイム、成功・失敗ステータスの数、タイムアウトやサーキットブレーカーの発動状況などなど。あるバックエンドサービスを2つ以上のサービスが参照しているケースでは、アクセス元のサービス毎のメトリクスではないため、バックエンドサービス側のプロキシーやロードバランサーのメトリクスでは解像度が不十分でした。

3つ目については、「Fault Isolation がうまく設定できていない」という課題でした。当時はそれぞれのアプリケーションでライブラリを利用して、タイムアウト・リトライ・サーキットブレーカーの設定を行っていましたが、どんな設定になっているかはアプリケーションコードを別個に見る必要があり、一覧性がなく状況把握や改善がしづらい状況でした。また Fault Isolation に関する設定は継続的に改善していくものなので、テスト可能であったほうが良く、そのような基盤を求めていました。

さらにもっと進んだ課題解決として、gRPC インフラの構築、分散トレーシング周りの処理の委譲、トラフィックコントロールによるデプロイ手法の多様化、認証認可ゲートウェイなどのような機能もスコープに入れて構築しています。このあたりについては後述します。

現状

クックパッドでのサービスメッシュは data-plane として Envoy を採用し、control-plane は自作、という構成を選択をしました。すでにサービスメッシュとして実装されている Istio を導入することも当初は検討したのですが、クックパッドではほぼ全てのアプリケーションが AWS の ECS というコンテナ管理サービスの上で動作している関係上 Kubernetes との連携メリットが得られないことと、当初実現したいことと Istio のソフトウェア自体の複雑さを考慮して、小さく始められる自作 control-plane という道を選びました。

今回実装したサービスメッシュの control-plane 部分はいくつかのコンポーネントによって構成されています。各コンポーネントの役割と動作の流れを説明します:

  • サービスメッシュの設定を中央管理するリポジトリ
  • kumonos*3という gem を用いて上記の設定ファイルから Envoy xDS API*4用のレスポンス JSON を生成
  • 生成したレスポンス JSON を Amazon S3 上に配置して Envoy から xDS API として利用する

中央のリポジトリで設定を管理している理由は、

  • 変更履歴を理由付きで管理して後から追えるようにしておきたい
  • 設定の変更を SRE 等の組織横断的なチームもレビューできるようにしておきたい

という2点です。

ロードバランシングについては、基本的には Internal ELB に任せるという方式で設計したのですが、gRPC アプリケーション用のインフラ整備も途中から要件に入った*5ので、SDS (Service Discovery Service) API を用意して client-side load balancing できるようにしています*6。app コンテナに対するヘルスチェックを行い SDS API に接続先情報を登録する side-car コンテナを ECS タスク内にデプロイしています。

f:id:aladhi:20180501141121p:plain

メトリクス周りは次のように構成しています:

  • メトリクスは全て Prometheus に保存する
  • dog_statsd sink*7を使用してタグ付きメトリクスを ECS コンテナホスト(EC2 インスタンス)上の statsd_exporter*8に送信
    • 固定タグ設定*9を利用してノード区別のためにアプリケーション名をタグに含めています
  • Prometheus からは EC2 SD*10を利用して statsd_exporter のメトリクスを pull
    • ポート管理のために exporter_proxy*11を間に置いています
  • Grafana、Vizceral*12で可視化

ECS, Docker を利用せずに EC2 インスタンス上で直接アプリケーションプロセス動かしている場合は、Envoy プロセスも直接インスタンス内のデーモンとして動かしていますが、構成としてはほぼ同じです。直接 Prometheus から Envoy に対して pull を設定していないのは理由があり、まだ Envoy の Prometheus 互換エンドポイントからは histogram メトリクスを引き出せないからです*13。これは今後改善される予定なのでその時は stasd_exporter を廃止する予定です。

f:id:aladhi:20180502132413p:plain

Grafana 上ではサービス毎に各 upstream の RPS やタイムアウト発生等の状況が見れるダッシュボードと Envoy 全体のダッシュボードを用意しています。サービス×サービスの粒度のダッシュボードも用意する予定です。

サービス毎のダッシュボード

f:id:aladhi:20180501175232p:plain

Upstream 障害時のサーキットブレーカー関連のメトリクス

f:id:aladhi:20180502144146p:plain

Envoy 全体のダッシュボード

f:id:aladhi:20180501175222p:plain

サービス構成は Netflix が開発している Vizceral を利用して可視化しています。実装には promviz*14と promviz-front*15を fork して開発したのもの*16を利用しています。まだ一部のサービスにのみ導入しているので現在表示されているノード数は少なめですが、次のようなダッシュボードが見れるようにしています:

リージョン毎のサービス構成図と RPS、エラーレート

f:id:aladhi:20180501175213p:plain

特定のサービスの downstream/upstream

f:id:aladhi:20180501175217p:plain

またサービスメッシュのサブシステムとして、開発者の手元から staging 環境の gRPC サーバーアプリケーションにアクセスするためのゲートウェイをデプロイしています*17。これは hako-console という社内のアプリケーションを管理しているソフトウェア*18と SDS API と Envoy を組み合わせて構築しています。

  • Gateway app (Envoy) が gateway controller に xDS API リクエストを送る
  • Gateway controller は hako-console から staging 環境かつ gRPC アプリケーションの一覧を取得して、それを基に Route Discovery Service/Cluster Discovery Service API レスポンスを返す
  • Gateway app はレスポンスを基に SDS API から実際の接続先を取得する
  • 開発者の手元からは AWS ELB の Network Load Balancer を参照し Gateway app がルーティングを行う

f:id:aladhi:20180502132905p:plain

効果

サービスメッシュの導入で最も顕著だったのが一時的な障害の影響を抑えることができた点です。トラフィックの多いサービス同士の連携部分が複数あり、今までそれらでは1時間に5,6件ほどのネットワーク起因の trivial なエラーが恒常的に発生していた*19のですが、それらがサービスメッシュによる適切なリトライ設定によって1週間に1件出るか出ないか程度に下がりました。

モニタリングの面では様々なメトリクスが見れるようになってきましたが、一部のサービスのみに導入していることと導入から日が浅く本格的な活用には至ってないので今後の活用を期待しています。管理の面では、サービス同士の繋がりが可視化されると大変わかりやすくなったので、全サービスへ導入することで見落としや考慮漏れを防いでいきたいと考えています。

今後の展開

v2 API への移行、Istio への移行

設計当初の状況と、S3 を配信バックエンドに使いたいという要求から xDS API は v1 を使用してきましたが、v1 API は非推奨になっているのでこれを v2 へ移行する予定です。同時に control-plane を Istio へ移行することも検討しています。また、仮に control-plane を自作するとしたら、今のところの考えでは go-control-plane*20を使用して LSD/RDS/CDS/EDS API*21を作成することになると思います。

Reverse proxy の置き換え

今までクックパッドでは reverse proxy として NGINX を活用してきましたが、内部実装の知識の差や gRPC 対応、取得メトリクスの豊富さを考慮して reverse proxy や edge proxy を NGINX から Envoy に置き換えることを検討しています。

トラフィックコントロール

Client-side load balancing への置き換えと reverse proxy の置き換えを達成すると、Envoy を操作してトラフィックを自在に変更できるようになるので、canary deployment や traffic shifting、request shadowing を実現できるようになる予定です。

Fault injection

適切に管理された環境でディレイや失敗を意図的に注入して、実際のサービス群が適切に連携するかテストする仕組みです。Envoy に各種機能があります*22

分散トレーシングを data-plane 層で行う

クックパッドでは分散トレーシングシステムとして AWS X-Ray を利用しています*23。現在はライブラリとして分散トレーシングの機能を実装していますが、これを data-plane に移してネットワーク層で実現することを予定しています。

認証認可ゲートウェイ

これはユーザーリクエストを受ける最もフロントのサーバーでのみ認証認可処理を行い、以降のサーバーではその結果を引き回し利用するものです。以前はライブラリとして不完全に実装していましたが、これも data-plane に移すことで out of process モデルの利点を享受することができます。

終わりに

以上、クックパッドでのサービスメッシュの現状と今後について紹介しました。すでに多くの機能を手軽に実現できるようになっており、今後さらにサービスメッシュの層でできることが増えていくので、マイクロサービス各位に大変おすすめです。

*1:https://blog.twitter.com/engineering/en_us/a/2013/observability-at-twitter.html

*2:https://medium.com/@copyconstruct/monitoring-and-observability-8417d1952e1c

*3:https://github.com/taiki45/kumonos

*4:https://github.com/envoyproxy/data-plane-api/blob/5ea10b04a950260e1af0572aa244846b6599a38f/API_OVERVIEW.md

*5:gRPC アプリケーションはすでに本仕組みを利用して本番環境で稼働しています

*6:単純に Internal ELB (NLB or TCP mode CLB) を使った server-side load balancing ではバランシングの偏りからパフォーマンス面で不利であり、さらに取得できるメトリクスの面でも十分ではないと判断しました

*7:https://www.envoyproxy.io/docs/envoy/v1.6.0/api-v2/config/metrics/v2/stats.proto#config-metrics-v2-dogstatsdsink最初は自前拡張として実装したのですが後ほどパッチを送りました: https://github.com/envoyproxy/envoy/pull/2158

*8:https://github.com/prometheus/statsd_exporter

*9:https://www.envoyproxy.io/docs/envoy/v1.6.0/api-v2/config/metrics/v2/stats.proto#config-metrics-v2-statsconfigこちらも実装しました: https://github.com/envoyproxy/envoy/pull/2357

*10:https://prometheus.io/docs/prometheus/latest/configuration/configuration/

*11:https://github.com/rrreeeyyy/exporter_proxy

*12:https://medium.com/netflix-techblog/vizceral-open-source-acc0c32113fe

*13:https://github.com/envoyproxy/envoy/issues/1947

*14:https://github.com/nghialv/promviz

*15:https://github.com/mjhd-devlion/promviz-front

*16:NGINX で配信したりクックパッド内のサービス構成に合わせる都合上

*17:client-side load balancing を用いたアクセスを想定している関係で接続先の解決を行うコンポーネントが必要

*18:http://techlife.cookpad.com/entry/2018/04/02/140846

*19:リトライを設定している箇所もあったのだが

*20:https://github.com/envoyproxy/go-control-plane

*21:https://github.com/envoyproxy/data-plane-api/blob/5ea10b04a950260e1af0572aa244846b6599a38f/API_OVERVIEW.md#apis

*22:https://www.envoyproxy.io/docs/envoy/v1.6.0/configuration/http_filters/fault_filter.html

*23:http://techlife.cookpad.com/entry/2017/09/06/115710

総合職・デザイナー向け技術基礎研修 2018

$
0
0

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

先日、新卒の総合職・デザイナー向けに技術基礎研修を行ないました。 そこで研修をするにあたってどのようなことを考えて何をしたか、担当者の視点から書いてみようと思います。

なぜやるのか

研修を担当することになったとき、はじめに「なぜやるのか」「この研修の目的は何なのか」を考え直してみました。 ぼんやりとした「技術についても少しは知っておいてほしい」という気持ちはありましたが、研修内容を考えるにあたって目的を明確にする必要がありました。

研修を受けてもらうのは総合職・デザイナーの人たちです。 エンジニアに対して技術研修があるのは自然ですが、技術職ではない人たちに技術研修を受けてもらうのには然るべき理由があるはずです。

理由の言語化を試みたところ、「研修を受ける人たちは技術職ではないが、テクノロジーカンパニーの一員であることに変わりはない」というところに思い当たりました。 本人が技術職であるか否かにかかわらず、我々のサービスは様々な技術によって支えられていますし、日々の仕事も様々な技術なしには成り立ちません。

そこで「テクノロジーカンパニーの一員として、日頃使っている技術や今後触れるであろう技術を知り、活かしていけるようになる」を大きな目的に設定しました。

目的

上記の目的だとまだ少しぼんやりしているので、使っている技術や将来使う技術についてブレークダウンして以下の3つを目的として置きました。

  • クックパッドがどのように動いているか理解する
  • 適切なツールを用いてコミュニケーションや情報共有できるようになる
  • データを元に物事を考えるためのスキルを身につける

社内では「データ分析からUI改善」や「ディレクターがSQLを使えてよかった話」などのように、ディレクターやデザイナーがデータ分析をしてその結果を各種ツールを使って共有・議論しつつサービスを開発していくということが一般的になっており、その実態を反映した内容になっています。

指標

目的をもとに、研修後に受講者がどうなっていてほしいかという指標を考えます。

  • クックパッドがどのように動いているかイメージできる
  • 社内で使われているツールでスムーズにコミュニケーションができる
    • Groupad(Wiki+Blog のような社内ツール)や GHE(GitHub Enterprise)など
  • コミュニケーションや情報共有を行なう際に目的に応じて適切な方法を選択することができる
  • SQL を使用して簡単なデータを取得できる*1

これらの指標は定性的で厳密な評価などは難しいのですが、それでも指標を言語化しておく価値はあります。 構成を考えたり資料をつくったりする途中で「何を伝えるべきだろうか」「この内容は必要だろうか」といったことを考える機会が数えきれないほどあるのですが、そのときは指標と目的に立ち返って考えることになります。

内容

本研修に割り振られた日数は3日間・各日7時間*2で計21時間程度でした。 内容の分量なども考慮して検討した結果、3つある目的それぞれに1日ずつ割り振ることにしました。

各日の簡単な内容は以下のようなものです。 なお、昨年まではプログラミング研修が含まれていましたが、昨年以前のフィードバックを参考にしつつ限られた時間の中での優先順位を検討した結果、泣く泣く今年の内容からは削ることにしました。

1日目「クックパッドを支える仕組み」

クックパッドを例にとりつつ、日頃何気なく使っている「インターネット」はどのように動いているかについて、「クライアントとサーバがネットワーク越しに通信している」ということを軸に以下のようなトピックについて話しました。

  • コンピュータ
  • クライアント
  • サーバ
  • リクエストとレスポンス
  • ネットワーク
  • 社内のエンジニア
  • セキュリティ

この日に使った資料は本エントリの最後に公開・紹介します。

2日目「コミュニケーションと情報共有」

前述の Groupad や GHE で使われる Markdown の書き方について説明した上で実習した後、自己紹介を題材にして、実際に GHE の Web 画面を使って様々な操作を行なってみました(issue を立てる、PR を送る、レビューしてマージするなど)。

f:id:s_osa:20180502184359p:plain

また、社内ではコミュニケーションや情報共有のために他にも様々なツール(Slack、メール、Google Drive など)を使っているので、それぞれの特徴や使い分けるための考え方などについて話しました。

3日目「データ分析の第一歩」

日常的に行われているデータ分析の例を示した後、ハンズオン形式で実際のデータ(DWH)を触りながら SQL について学んでもらいました。 SQL の実行には社内でも広く使われている Bdashという BI ツールを使用し、適宜グラフなども描きながら進めていきました。

f:id:s_osa:20180502183205p:plain

分析が目的なので内容を select文に絞り、select, from, whereから始めて group by, having, joinなどひと通りの句について説明しつつ、実際にクエリを書いてもらって答え合わせをしながら進めました。

ただし、次の日から SQL をガリガリ書いてもらいたいというわけではなく、基本的なクエリを通して SQL の強力さを体感してもらい、将来必要になったときに「SQL を書く」という選択肢が視野に入るようにしてもらうにするのが主目的でした。

大切にしたこと

構成を考えたり資料をつくったりする際には、いくつかのことを常に頭に置きながら進めていました。

頭の中に地図をつくる

3日間という限られた時間の中で覚えられることには限界があります。

そこで、隅々まで覚えてもらうことはあきらめて、全体像を掴んでもらうとともに将来何かあったときに「これ研修でやったやつだ!」となってもらえるようにすることに集中しました。

また、新しく知ることをただ丸暗記するのではなく、知ったことの関係性を掴みながら頭の中に地図をつくってもらうために以下のようなことを意識していました。

全体から細部へ

繰り返しになりますが、研修を受ける人たちには全体像を掴んでほしいのであって、隅々まで知ってほしいわけではありません。

そこで、全体像をイメージできるようになってもらうことを優先するために、必要に応じて(できるだけ嘘にならない範囲で)細部を捨てて説明しました。 細部の説明が必要な場合にも、一度全体を説明して全体像を掴んでもらってから細部に立ち入るようにしました。

身近なところから裏側へ

普段使っている例やこれから使う機会など、できるだけ受講者が身近に感じられるユーザー目線から話を始めて、必要に応じて裏側の仕組みなどを説明しました。

既存の知識と結びつける

新しい知識を単体で覚えるのは難しいので、具体例として普段使っているアプリの例を出したり、それまでの研修でやった内容と関連付けたりと、受講者が持っている既存の知識と結びつけながら話しました。 受講者が自身の知識をもとに具体例を使って質問してくれたときは最高のチャンスなので、そのような質問には必ず具体例を活かしながら答えるようにしました。

実物を見せる

概念だけを説明されて理解するのは難しいので、ネットワーク機器やサーバールームを見てもらう、開発者ツールを使って実際の HTML や CSS を覗いてみてもらうなど、可能な限り実物を見てもらうことによって少しでもイメージしやすくなるようにしました。

手を動かして身につける

最終的に手を動かして使う類のスキルは知ることよりも使えるようになることが重要なので、実際に身につくように手を動かす時間を多く取るようにしました。

寄り道も大切にする

先述の通り、全体的には細部にはあまり触れないようにしましたが、何かを学ぶ上で知的好奇心は非常に重要です。 そこで、受講者が質問をしてくれたり興味を持ってくれた点については、適宜、細部や背景も含めて説明しました。

結果

研修後、受講者に対して各指標についてどの程度達成できたかというアンケートを5段階で取ったところ、全体を通した平均が4.44という数値になりました。 この数値自体には大した意味はないのですが、感想なども含めて概ね良い研修だったとのフィードバックをいただいています。

この研修にどれだけの価値があったのかは、配属後に実際に仕事をしていく中でわかっていくものだと思います。

今回の研修が少しでも役に立つことを願いつつ、実務に入ってからのフィードバックを含めて来年の研修に活かしていきたいと考えています。

資料

2日目および3日目の資料については社内固有の情報が多く含まれているため公開が難しいのですが、1日目の資料については一般的な情報なので公開しておきます。

研修では目の前の受講者に最適化するために、口頭での例示や補足、ホワイトボードを使って図示しながらの説明などを多用しましたが、スライドから概要や雰囲気だけでも感じていただければ嬉しいです。

*1:データ分析を行なうためには SQL の知識だけではなく、取得したデータをどう扱うかという知識が必要不可欠ですが、本研修のあとに統計研修が控えていたため本研修ではデータの取得に的を絞っています。

*2:就業時間は8時間ですが、朝夕に振り返りなどの時間が毎日1時間設定されていました。

AWS Elemental MediaLive を使用したライブ動画配信アプリの基盤開発

$
0
0

技術部開発基盤グループの @ganmacsです。 クッキング LIVE アプリ cookpadTVのライブ動画配信基盤(以下配信基盤)を AWS Elemental MediaLiveを使用して開発した話を紹介します。

cookpadTV 上のライブ動画配信基盤の役割と機能

cookpadTV では配信基盤を使ってライブ動画機能を実現しています。 cookpadTV とは料理家や料理上手な有名人による料理番組のライブ配信を視聴できるアプリです。 Cookpad Tech Kitchen #15や、すでにクックパッド開発者ブログに書かれた記事 1, 2を見るとどのようなアプリかをイメージがしやすいと思うのであわせてご覧ください。 配信基盤は cookpadTV 用というよりも様々なサービスで使える共通基盤になっています。 cookpadTV と配信基盤との関係は以下の図のようになっています。

f:id:ganmacs:20180509160937p:plain

配信基盤の大きな機能として

  • ライブ動画を配信できるようにする機能
  • ライブ動画を保存しておき後から見られるようにする機能

があります。ライブ動画を配信するときの配信基盤の動きは次のようになります。 配信基盤が番組の配信開始時間と終了時間を受け取って配信用 URL と購読用 URL を返します。 配信用 URL とはライブ配信したい動画を配信する URL で、購読用 URL とは配信された動画を視聴できる URL のことです。 配信プロトコルには RTMP を、購読には HLS をそれぞれ使用しています。 配信基盤は受け取った配信時間と終了時間をもとにライブ動画を配信できるように準備をします。 開始時間になると配信用 URL に対して動画のストリーミングが始まるので、配信基盤はその動画を様々な解像度にエンコードして配信しています。

ライブ動画を保存しておき後から見られるようにするために、ライブ動画を S3 に保存しておきます。 その動画を社内の動画変換サービスを使用して HLS に変換することでライブ動画をあとから見られる形にしています。

設計方針

動画を受け取りエンコードしているサーバから視聴者に配信するのではなく、配信についてはすべて S3 のようなマネージドサービスに任せられるように設計しました。 通常の動画配信の場合、コンテンツ全てを CDN で返してしまえばオリジンサーバへのアクセスをほぼ無くすことは可能です。 しかし、HLS でライブ動画を配信するにはプレイリストを頻繁に更新する必要があるため長期間キャッシュできないエンドポイントが存在し、オリジンにも高頻度にアクセスされる可能性があります。 そのような大量のアクセスに耐えられる必要があるため、ライブ動画やプレイリストを一度 S3 などにアップロードしてそこから配信できるように設計しました。 こうすることで、急に大量のアクセスが来た場合にも配信サービスのスケールをマネージドサービスに任せられるので運用が非常に楽になります。

採用理由

配信基盤で AWS Elemental MediaLive(以下 MediaLive) を使用してライブ動画配信を実現しています。 MediaLive を採用したのは、はじめに決めた設計方針を素直に実現できるからです。また、他にも以下の採用理由もあります。

  • マネージドサービスに任せられる部分は任せたい
  • S3 / AWS Elemental MediaStore(後述) とのインテグレーション
  • その他 AWS サービスとのインテグレーション

MediaLive は動画の配信先を AWS Elemental MediaPackage(以下 MediaPackage), AWS Elemental MediaStore (以下 MediaStore), S3 などから選べます。 MediaPackage は今回使用していないので紹介は割愛しますが、 MediaStore はメディア向けに最適化された S3 のようなもので、動画コンテンツをより効率よく配信可能です。 デフォルトで S3 や MediaStore を使用した配信をサポートしていることで、他のアプリケーションを使用した場合に比べてシンプルな実装で要求を実現可能なのが採用の最大の理由です。 また、MediaLive もマネージドサービスなので管理しているサーバのリソースを気にしなくていいこと、配信中のメトリクス3が Amazon CloudWatch(以下 CloudWatch) を使用して確認できることなども採用理由の一つです。

他の方法で実現不可能だったのか

MediaLive 以外にも、Wowza Streaming Engine(以下 Wowza) や nginx-rtmp-module などを使用することでライブ動画配信を実現するアプリケーションを作ることは可能です。 実は配信基盤の開発当初は MediaLive はまだ発表されていなかったため Wowza で動画配信をしようと考えており、実際にプロトタイプを作っていました。 しかし、Record HLS Packet and Upload to S3 each time.にあるように S3 へのアップロードは公式でサポートがされていないことや、その他アプリケーションを自分で管理しなくてはいけないことなどもあり、MediaLive を使用したアプリケーションに作り直しました。 MediaLive が発表された当初は、MediaLive についての記事などはほとんどなく知見が溜まっていないという欠点もありましたがドキュメントがしっかりと書かれていたので特に困りませんでした。

最終的なアーキテクチャ

配信基盤の大まかな役割として、ライブ動画を流すこと、そのアーカイブ動画を後から見れるようにとっておくことがあります。 それぞれについて最終的にどのように実現したかについて説明します。 以下がアーキテクチャの概要図になります。

f:id:ganmacs:20180509161423p:plain

配信基盤は配信の準備のために、開始時間より少し前に MediaLive を放送可能状態に変更したり、放送開始/終了検知をするために CloudWatch のアラームを設定したりします。 これらはバッチ処理によって行われています。 余談ですが配信が開始されたか/終了したかを取得する API が MediaLive には存在しません。 配信基盤では開始は S3 Event を、終了は CloudWatch を使用して、それぞれの情報を元に配信検知用のデーモンを起動しておき放送開始/終了を検知しています。 開始時間になると 配信用 URL に RTMP で動画が流れてくるので MediaLive が受け取り、設定した解像度に変換して MediaStore にアップロードします。 そして MediaStore をオリジンサーバとして CDN を経由して視聴者まで配信するようにしています。

アーカイブ動画は MediaLive の Archive Output Group を使用して、S3 にアップロードしておき、それを配信可能な形に変換して配信しています。 ライブ配信の終了時間になると、バッチ処理で S3 に保存されている動画をすべて集めてきて社内の動画変換サービスで変換して、アーカイブ動画として使用しています。 アーカイブ動画もライブ動画と同様に S3 をオリジンサーバとして CDN を経由して視聴者に配信されるようになっています。

まとめ

cookpadTV のライブ動画配信基盤の機能と、開発するうえで気をつけたことを紹介しました。 クックパッドでは AWS を利用して新たなサービスを作り出していける仲間を募集しています

Androidアプリ の minSdkVersion を21にした話

$
0
0

技術部モバイル基盤グループの こやまカニ大好き( id:nein37 ) です。今回はクックパッドにおける Android アプリの minSdkVersion を 21 にした話を紹介します。

クックパッドのモバイルアプリではユーザーが5%存在するプラットフォームではサービスを維持するというルールが存在していて、ここ数年はこのルールに従って minSdkVersion を決めてきました。 最後に更新されたのは2016年7月のことで、このときは Android 4.0.x (API level 14-15) のシェアが 5% を下回ったため minSdkVersion を 16 に更新しました。 その後、 Android 4.1 (API level 16) のシェアが5%を下回った際に minSdkVersion を見直す機会はありましたが、同じく Jelly Bean である 4.2 のシェアが高く 4.1 だけサポート外にしてもあまり効果が見込めないことから minSdkVersion の更新は行いませんでした。

そのような状況が1年近く続いていたのですが、最近クックパッドアプリだけでなく国内向けアプリ全体の minSdkVersion ポリシーを見直す機会があったため、その内容を書いていこうと思います。

minSdkVersion の定期的な更新が必要な理由

Android には Support Libraryという古いバージョンのOSに新しい機能をバックポートするためのライブラリがあります。(Google I/O 2018 ではさらに新機能も追加され Jetpackという枠組みが生まれました) また、 Google Play サービスや Firebase といったライブラリも独立したライブラリとして提供されているため、Android 4.0 (API level 14) 以上であればほとんどの機能を利用することができます。 Android 開発ではこれらのライブラリによって古いOSでもあってもある程度不自由なく開発や運用ができるようになっていますが、やはり限界は存在しています。

新機能のバックポートが遅い、または不十分である

最近では新しいOS(API level)の Developer Preview 提供とほぼ同時に新しい Support Library の alpha が提供されるようになりました。 しかし、その中でも古いOS向けにバックポートされていない新機能があり、どのOSバージョンでも最新OSと同じ機能を提供できるというわけではありません。 たとえば、 ImageView#setImageTintList()というメソッドは Android 5.0 (API level 21) から提供されていますが、 Support Library の AppCompatImageViewsetImageTintMode()が追加されたのは2017年7月リリースの v26 からで、 v25 で入った background tint のサポートからは7ヶ月遅れています。

また、同じくAndroid 5.0 (API level 21) で導入された JobSchedulerは Android 5.0 以上でしか利用できず、過去のOS向けのバックポートである GcmNetworkManagerFirebase JobDispatcherでその機能をすべて置き換えることはできません。 その一方で JobScheduler以前に利用されていた AlarmManagerWakefulBroadcastReceiverなどの制限はOSバージョンアップのたびに厳しくなっており、ひとつの実装で全てのOSに同じ機能を提供することが難しくなっています。

このように古いOSが存在することでアプリの構成自体が複雑化していってしまうため、アプリの健全な開発効率を維持するためにも minSdkVersion の定期的な見直しは必要です。

バックポート不可能な機能の差異が存在する

例えば、以下のようなOSバージョンごとの差異は Support Library では埋めることができません。

  • WebView の挙動
    • Android 4.4 (API level 19)より前のバージョンではOSに組み込まれたWebViewコンポーネントが利用される
    • Android 4.4 (API level 19) では Chromium ベースになったがバージョンは固定で更新されない
    • Android 5.0 (API level 21) 以降では Chromium ベースの最新のコンポーネントが提供される
  • メディアサポート
    • 動画や静止画のサポート状況はOSバージョンによって異なる(ただし、 ExoPlayerなど独自のメディアサポートを提供するライブラリは存在する)
    • MediaSessionなどの再生関連UIは 5.0 から追加された
  • TLS 1.1, 1.2サポート
    • TLS 1.1, 1.2 の実装は Android 4.1 から含まれているが、デフォルトで有効になったのは 5.0 から

これらの機能に強く依存したサービスの場合、 minSdkVersion を上げる以外の選択肢はなくなります。

スマートフォン・タブレット以外のプラットフォームサポート

Android Auto や Android TV といったプラットフォームは Android 5.0 (API level 5.0) から追加されました。これらの機能はより minSdkVersion の低いスマートフォン向けのアプリに同梱することもできますが、それぞれの機能は古いOSの端末から呼び出されることを想定していません。

これは極端な例ですが、Android TV で実際に発生した問題について説明してみましょう。Android TVではTV端末の判定のために UI_MODE_TYPE_TELEVISIONを参照するように公式ドキュメントに書いてあります。 ところが、このフラグ自体はAPI level 1から存在するものであり、一部のSTB型端末はこのフラグが有効になっているため、 Android 4.0(API level 14) の端末であるにも関わらずTV端末として判定されます。 通常、TV はホームアプリが参照する category がスマートフォン・タブレットと異なるため画面が分離されていますが、上記のフラグだけに頼って TV 判定を行って leanback ライブラリの機能を呼び出したため、端末のAPIレベルに存在しないメソッドを呼び出してクラッシュすることがありました。(leanback ライブラリの minSdkVersion は 17 に設定されており、これより古いOSから呼び出した場合の動作は保証されません)

このような事故を防ぐために、 新しいプラットフォームをサポートする場合は minSdkVersion を見直したほうが良い場合もあります。

サポート外となったOSはどうなるのか?

これは新しいアプリのリリース後、以前のAPKをどうするかによって変わってきます。

何もしなかった場合、 minSdkVersion や uses-feature が異なるAPKが配信されると過去のAPKと新しいAPKは同時に配信され続けます。 この状態では最新のAPKでサポート外となった端末でも以前のAPKが新規にインストールできます。 この状態ではユーザーからは普通に自分の端末でアプリのインストールや利用ができるため、自身の端末がサポート外となったことはわかりません。

一方、Playコンソールから古いAPKを無効にすることもできます。 その場合、最新のAPKでサポートされなくなった端末ではアプリのインストールができなくなり、アプリのサポート外であることがわかるようになります。

OSのサポートバージョンを変更する方法として、 minSdkVersion の切り上げを行いつつ古いAPKは有効にしておき、古いOSのシェアが低くなった時点で過去のAPKも無効にする、とう方法も取ることができます。

minSdkVersion をどの値にするべきか?

minSdkVersion の設定値を決めるための基準は2つあります。

OSバージョンが一定のシェアを下回っているものをサポート外とする

minSdkVersion を上げる理由は主に開発・運用の効率化のためですが、当然サポート外となったOSバージョンにはアップデートにより最新のサービスを届けることができなくなってしまいます。 また、サポート外となったOS向けに配信されていたアプリに致命的なバグがあった場合、アップデートによる解決を行うこともできなくなります。

これらの問題によるユーザーへの悪影響を最小限にするため、クックパッドでは対象のOSバージョンが 5% を下回った場合に minSdkVersion を更新してもよい、というルールを設けています。 直近ではクックパッドアプリのOSバージョンごとのシェアは大まかに以下のようになっていました。

OSバージョン API level シェア
5.0.x 21 13.60%
4.4.x 19 7.9%
4.3.x 18 0.16%
4.2.x 17 3.67%
4.1.x 16 0.87%

Android 4.1-4.3 は一般に Jelly Bean と呼ばれているバージョンで、以前検討した際には「サポート対象外にするときはなるべく一緒のタイミングでやりたい」という判断にしていました。 以前は Andoroid 4.2(API level 17) のシェアが 5% を上回っていたため見送りましたが、今回は Jelly Bean 全体で合計しても 5% を下回っており、サポート外とすることができそう、という判断になります。

機能面・開発効率で比較して大きなメリットがありそうなものを閾値とする

前述の通り、通常の 5% ルールでは Jelly Bean をサポート外にできそうということがわかりました。 しかし、Android 4.4 (API level 19) もよく見るとシェア 7.9% という低めの値で、しかもひと月ごとに 0.5% を上回るペースで減少し続けていました。このままだと半年以内に 5% を切りそうです。 そこで、 minSdkVersion を Android 4.4 (API level 19) とした場合と Android 5.0 (API level 21) とした場合で簡単に比較してみることにしました。

  • Android 4.4 (API level 19)
    • AlarmManager の挙動変更やストレージ関連の変更など、挙動変更の閾値となる部分は多い。
      • ただし Android 5.0 では JobSchedulerが導入され AlarmManagerの用途が狭まっている
    • WebView が Chromium ベースになっており、 ウェブページ側の改修が楽
      • ただし WebView コンポーネントのアップデートは Android 5.0 から
  • Android 5.0 (API level 21)
    • JobScheduler 、Camera2 API など過去のOSでは利用できない大きな変更が多数含まれている。
    • Material Design にネイティブ対応しており、レイアウトXMLでの属性指定などにSDK側のものを利用できる。
    • 現状 JobSchedulerを利用している料理きろくなどでOSバージョンごとの機能差が存在しているが、このバージョンまで minSdkVersion を引き上げることで内部の分岐がなくなる

上記を踏まえチーム内で議論した結果、今回の見直しで minSdkVersion を Android 5.0 (API level 21) とした場合、もっとも開発効率を引き上げることができるという結論になりました。ちょうど新規のアプリの開発・リリースがいくつか控えていたこともあり、半年後に再度 minSdkVersion を見直すよりも半年前倒しにして社内の国内向け全アプリに minSdkVersion 21 を適用することで大きなメリットがあると判断したためです。

社内でどのようにバージョンシェアの変更議論を進めたか

前述の通り「開発効率・運用工数の改善」という観点でみた場合、最も効果がありそうな閾値は Android 5.0 (API level 21)でしたが、Android 4.4(API level 19) の 7.9%のユーザーというシェアはかなり大きいものです。 すでに多数のユーザーを抱えるクックパッドアプリではサービス面での責任をもっている部署と何度も相談を重ねて慎重に進めていくことになりました。

一方これからリリースする新規のアプリでは既存ユーザーへの影響を考えなくて良いため、まずはそちらのチームにminSdkVersion を 21 から始めることのメリットについて「Android アプリの minSdkVersion(最小サポートOSバージョン) は Android 5.0 以降 にすべき」というブログを書いたり開発チームに直接説明したりして共有しました。 これらの取り組みの結果、cookpadTVアプリクックパッドMYキッチンアプリのいずれも minSdkVersion 21 からのスタートとなりました。今後リリースされる新規のアプリに関しても全て minSdkVersion 21 以上となる見込みです。

クックパッドアプリにおける適用は当初 Android 4.4 (API level 19) のユーザーシェア 7.9% という割合の多さから見送られそうになりましたが、アプリ全体のリファクタリングのための期間が始まるためその期間前に適用することがベストなタイミングであることを説明したり、ユーザーシェアの減少率の傾向やWebページの差し替えによる改善は引き続き可能であることを説明したり、経営層との「クックパッドアプリの開発を高速化するためにはどうすればよいか一旦数字を度外視して考えてみる」という場で取り上げたりした結果、近日中に minSdkVersion 21 に引き上げることになりました。 現在細かいリリース日時を調整中で、開発環境にももうすぐ反映見込みです。 minSdkVersion の更新後、代替リソースの整理やレイアウトファイルの見直しなどやりたいことがいっぱいで今からとても楽しみです。

おまけ

今回の取り組みの最中に minSdkVersion という謎のアカウントが値を 21 に更新していました。 世界的に minSdkVersion 21 の流れが来ているのだと思います。

最後に

今回はモバイル基盤の取り組みとしてAndroidアプリの minSdkVersion を 21 にした話を紹介しました。 モバイル基盤では今後も引き続きユーザーサポートとのバランスを取りながら開発効率を高める取り組みを行っていく予定です。 クックパッドではモバイル基盤と一緒に minSdkVersion 21 でアプリ開発を行いたい仲間、開発を効率化する仕組みづくりに興味がある仲間を募集しています


iOSアプリのサブミット自動化と証明書管理の効率化

$
0
0

こんにちは。技術部モバイル基盤グループの @です。

fastlaneCore Contributorを務めており、 社内ではプロのコードサイン解決者 *1としての職務経験を積んでいます。

今回はクックパッドでのfastlaneを使ったiOSアプリのサブミット自動化と、証明書管理についての事例を紹介したいと思います。

CIによるiOSアプリサブミットの自動化

クックパッドでは、昨年の春頃よりiOSアプリのサブミットをチャットbot経由で行っています。

このように、Slack上でサブミットジョブを実行すると、CIでアプリがビルドされ、審査提出までを完全自動で行ってくれます。

f:id:gigi-net:20180516173922p:plain

審査提出には、ビルドや処理待ちの時間を含めると多くの工数がかかり、人為的なミスが起こる可能性もありましたが、 完全な自動化により、高頻度のアプリリリースに耐えられるようになりました。

アーキテクチャは以下の図のようになっており、チャットbotからJenkinsのジョブを実行し、そこでfastlaneを利用しています。

f:id:gigi-net:20180516173935j:plain

詳しく知りたい方は下記の資料をご覧ください。

この仕組みは、昨年まではクックパッドアプリでのみ行っていました。

しかし、今年に入ってからライブ配信アプリのcookpadTVや、レシピの投稿者がより使いやすいクックパッド MYキッチンなど、新規アプリの開発が活発になり、ほかのアプリでも自動サブミットの仕組みを導入する必要が出てきました。

このような仕組みをスケールするに当たって、一番の障害になるのがやはりコードサインです。 クックパッドでは、複数台のMac端末をCIサーバーとして運用しており、その全てに、多くのProvisioning Profileを配布、更新する必要がありました。 これらを手動で管理するのは現実的ではありません。

fastlane/matchを使った証明書管理

そこで、fastlaneのユーティリティの1つであるmatchを利用して、証明書やProvisioning Profileの管理、配布を自動化しました。

matchの仕組み

matchは、証明書や秘密鍵、Provisioning Profileを、git管理し、複数の環境で共有できるようにするツールです。 Apple Developer CenterのAPIを叩いて証明書やProvisioning Profileを作成し、暗号化を施してgitリポジトリに共有してくれます。

f:id:gigi-net:20180516173944p:plain

まず、iOSアプリケーションのリポジトリにMatchfileという設定ファイルを設置します。 これで、match利用時にデフォルトで設定されるパラメータを指定できます。

ここでは、コミット先のリポジトリを予め指定しています。

git_url "git@example.com:cookpad/certificates.git"

次に、開発者は開発環境からProvisioning Profileを作成します。matchはCLIを提供しているため、それを使うのが便利です。

typeを指定することで、ストア配布用のほか、AdHocビルドや、Enterprise配布用のProvisioning Profileも作成できます。

$ fastlane match --type appstore \
                 --app_identifier com.cookpad.awesome-app,com.cookpad.awesome-app.NotificationService

この操作で、Provisioning Profileが生成、コミットされました。

暗号化、復号化に利用するパスフレーズは、初回起動時のみ対話的に聞かれ、以後はmacOSのキーチェーンに保存されます。 また、MATCH_PASSWORDの環境変数で指定することもできます。

CIサーバーでビルドする際は、fastlaneを用いて、以下のように簡単に証明書の取得、コードサインを行うことができます。

Fastfile上に以下のように記述します。

# Sync certificates and Provisioning Profiles via git repository
match(
  app_identifier: ["com.cookpad.awesome-app", "com.cookpad.awesome-app.NotificationService"],
  type: 'appstore',
  readonly: true,
)

# Build iOS app with the profiles
build_ios_app

readonlyは、リポジトリやApple Developer Centerに変更を加えないようにするための設定値です。 CIサーバーからのProvisioning Profileや証明書の不用意な更新を防げます。

これにより、手元で証明書の更新、追加作業を行うだけで、全てのビルド環境で最新の証明書類が利用できるようになりました。

f:id:gigi-net:20180516173953p:plain

複数ライセンスでのmatchの利用

また、クックパッドでは、AppleのDeveloperライセンス(チーム)も、ストア公開用のライセンスのほか、社内配布用のEnterpriseライセンスを始めとした複数のライセンスを利用しています。

matchではライセンスごとに別のgitブランチを作成することで、複数のライセンスの証明書類を、1つのリポジトリで管理することができます。 CLIでは、以下のようにgit_branchオプションを渡します。

$ fastlane match --type enterprise \
                 --app_identifier com.cookpad.awesome-app,com.cookpad.awesome-app.NotificationService \
                 --git_branch enterprise \
                 --team_id $ENTERPRISE_TEAM_ID

この場合も以下のようにビルド時に証明書類を取得できます。

# Sync certificates and Provisioning Profiles via git repository
match(
  app_identifier: ["com.cookpad.awesome-app-for-inhouse", "com.cookpad.awesome-app-for-inhouse.NotificationService"],
  git_branch: 'enterprise',
  team_id: enterprise_team_id,
  type: 'enterprise',
  readonly: true,
)

# Build iOS app with the profiles
build_ios_app

運用してみての問題点

一見便利なmatchですが、今回大規模に運用してみて、下記のような問題に直面しました。

1ライセンス当たり同時に1つの証明書しか扱えない問題

matchの一番の問題点は、同時に管理できる証明書が1つに制限されてしまうという問題です。

Apple Developer Centerでは、1ライセンス当たり同時に2つの証明書を作成することができますが、matchでは、新しく証明書を作成したい場合は、match nukeと呼ばれる機能を使い、既存の証明書と、それを利用しているProvisioning Profileを全てrevokeする必要があります。

そのため、証明書がexpireする前に、新旧2つの証明書を用意し、徐々に切り替えていくという方法を取ることができません。

この問題はissueにもなっており議論されていますが、今のところ対応されておりません。

Provisioning Profile作成時に証明書のIDを渡すことで複数の証明書を管理できる仕組みを個人的に検討しており、そのうち開発したいと考えています。

Enterpriseライセンスの証明書更新で困る問題

サブミット用の証明書やProvisioning Profileは、revokeしても、すでにApp Storeにリリースしているアプリは影響を受けません。 上記の問題の影響を大きく受けるのがApple Developer Enterpriseライセンスです。

Enterprise証明書でApp Storeを経由せずに配布しているアプリは、証明書がrevokeされた瞬間に、全てのインストール済みの端末でその証明書を使って署名したアプリが動作しなくなります。

例えばクックパッドでは、最近Cookpad Studioという、ユーザーさんが実店舗で料理動画を収録できるサービスを展開しています。

こちらでは、全国のスタジオでEnterpriseライセンスで配布した業務用アプリを利用しているのですが、証明書のrevokeにより動作しなくなる危険性があります。

多くの端末を利用しているので、全国で同時に更新作業をするのは容易ではありませんが、現在のmatchでは即時のrevokeしかできないため、証明書の更新時に問題が発生することが予想されます。

この問題も、上記と同様に複数証明書の存在を許容することで解決できるでしょう。

複数ライセンス利用時にコミット先が制約できない問題

複数ライセンスの運用についても洗練されていない部分が目立ちました。

現在のmatchでは、Provisioning Profile作成時の操作ミスにより、1つのブランチに複数のDeveloperライセンスを混在させることができてしまいます。 これにより、不要な証明書が発行されてしまったり、解決が面倒な状態が発生してしまいます。

これは各ブランチにTeam IDを指定するファイルを含んでしまい、別のDeveloperライセンスで作成した証明書のコミットを禁止するなどの機能で対応できそうなので、このような仕組みを提案、実装したいと思っています。

まとめ

このように、iOSアプリのコードサイン周りは非常に複雑で、特殊な訓練や知識が必要になりますし、プロダクト開発において本質的ではない問題が発生しがちな領域です。

全てのアプリ開発者が開発に注力できるよう、今後もfastlaneの開発などを通して、生産性向上へ貢献していきたいと思っています。

クックパッドのモバイル基盤チームでは、アプリ開発者の生産性を向上させたいエンジニアを募集しています。

iOS アプリケーションエンジニア(開発基盤)Android アプリケーションエンジニア(開発基盤)

*1:Professional iOS Code Signing Issue Resolver. fastlaneのauthorである@の役職でもあります

Lambda@Edge で画像のリアルタイム変換を試してみた

$
0
0

技術部の久須 (@) です。クックパッドではモバイル基盤グループにて Android 版クックパッドアプリの開発・メンテナンスに携わっています。

今回は普段の業務とは少し異なるのですが、個人的に興味があった AWS の Lambda@Edge でリアルタイムに画像を変換する仕組みについて試してみたので、構築した環境の内容やコードをここで紹介したいと思います。

注意:この仕組みで実運用している訳ではなく調査用の AWS 環境で動かしている段階なので、もし参考にされる場合はご注意ください。ちなみにクックパッドにはこのような本番環境とは切り離された調査、検証用の AWS 環境があり、エンジニアは自由に AWS の各種コンポーネントを試すことができます。

概要

Lambda@Edge とは、公式ドキュメントにも説明がありますが CDN である CloudFront の入出力 HTTP リクエスト・レスポンスを操作できる Lambda 関数です。今回は CloudFront のオリジンとして S3 を指定し、S3 からの画像レスポンスを Lambda@Edge で変換する仕組みを構築しました。

f:id:hkusu:20180524153623p:plain

この仕組みでは画像へのリクエストに応じてその場で画像変換を行うので、サービスの運営において様々なバリエーションの画像が必要な場合であってもそれらを予め用意しておく必要がなく、画像を変換する為のサーバも必要としません。S3 に画像ファイルさえ置けばよいのでサーバサイドのアプリケーションの種類や言語を問わず、たとえ静的な WEB サイトであったとしても様々なバリエーションの画像を提供することができます。

変換後の画像は CloudFront にキャッシュされるので、変換処理が行われるのは CloudFront にキャッシュがない場合のみです。Amazon Web Services ブログでは変換後の画像を S3 に保存する方法が紹介されていますが、今回の方法では変換後の画像は CloudFront のみに持つ構成としています。そもそも CloudFront のキャッシュ期間を長く設定しておけばよいという話もありますが、たとえ CloudFront のキャッシュがきれた場合でも画像変換を再度実行するのではなく CloudFront のキャッシュの保持期間を延長することで変換コストを抑えることができます(この方法については後述します)。また変換後の画像をキャッシュでしか保持していないので、後から画像変換の仕様が変わったり不具合があったりしたとしても S3 上の画像を消す等のオペレーションを必要とせず、CloudFront 上のキャッシュを消す(CloudFront に invalidation リクエストを送る)だけで対応できます。

Lambda@Edge を利用する上での注意点

まず Lambda@Edge 用の関数の開発で利用できる言語は Node.js かつバージョンは 6.10 のみです。旧来の Lambda 関数の開発で利用できる 8系 は現時点で対応していません。また Lambda@Edge 用の関数を作成できるのは米国東部(バージニア北部)リージョンのみです(ただし作成した関数は各 CloudFront のエッジへレプリケートされます)。

そのほか制限は公式ドキュメントの Lambda@Edgeの制限のとおりです。注意すべきは Lambda@Edge でオリジンレスポンス(今回の構成では S3 からの画像レスポンス)を操作する場合、操作後のレスポンスのサイズはヘッダー等を含めて 1MB に抑える必要があることですが、通常のWEBサイトやモバイルアプリでの用途としては十分な気がします。タイムアウトまでの制限時間は 30 秒と長く、メモリも最大 3GB ほど使えることから画像を扱う環境としては問題なさそうです。

また、これは Lambda@Edge 用の関数の実装時の制約なのですが、関数からオリジンレスポンスを取り扱う際、関数からはレスポンスBody(画像データ)にアクセスできません。よって、改めて関数から S3 へアクセスし画像ファイルを取得する必要があります。

環境の構築 (CloudFront、S3)

特筆すべきことはなく S3 のバケットを作成し、それをオリジンとして CloudFront を設定すれば問題ありません。ただし、クエリ文字列はフォワード&キャッシュのキーに含める(クエリ文字列が異なれば別ファイルとしてキャッシュする)ようにしてください。これは後述しますがクエリ文字列で画像の変換オプションを指定する為です。また CloudFront のキャッシュ期間も適宜、設定しておきます(動作確認中は1分など短くしておくとよいです)。

f:id:hkusu:20180524154845p:plain

環境の構築 (Lambda@Edge)

Lambda@Edge の環境構築については 公式ドキュメントに詳しいでここではポイントのみ述べます。

ロールの作成

AWS の IAM にて予め Lambda@Edge 用の関数の実行ロールを作成しておきます。

アタッチするポリシー

{"Version": "2012-10-17",
    "Statement": [{"Effect": "Allow",
            "Action": ["logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {"Effect": "Allow",
            "Action": ["s3:GetObject"
            ],
            "Resource": ["arn:aws:s3:::*"
            ]}]}
  • S3 の画像を参照する権限を追加

信頼関係

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

関数の作成

Lambda@Edge 用の関数の実装を用意する前に、管理コンソール上で関数の枠組みだけ作成しておきます。米国東部(バージニア北部)リージョンの Lambda のメニューから関数を作成します。

f:id:hkusu:20180524154855p:plain

[関数の作成] ボタンを押下し関数を作成します。選択した実行ロールの情報を元に、本関数からアクセスが可能な AWS のリソースが次のように表示されます。

f:id:hkusu:20180524154933p:plain

問題なければ一旦、枠組みの作成は完了です。

関数の実装の用意

実装例として、サンプルコードを私の方で作成しました。GitHub に置いてあるので参考にしてください。
hkusu/lambda-edge-image-convert

サンプルコードの説明

画像のリサイズと WebP 形式への変換の機能を提供します。仕様は次のとおりです。

  • 変換元の画像は JPEG 形式の画像のみ

  • リサイズの際に画像のアスペクト比(横幅と縦幅の比率)は変更しない

  • 変換オプションはクエリ文字列で指定する

    キー デフォルト 最大値 補足
    w   最大横幅(ピクセル)を指定 1200    1200   変換元の画像より大きな値は無効
    (=拡大しない)
    h 最大縦幅(ピクセル)を指定 同上 同上 同上
    p t (true):WebP 形式へ変換する
    f (false):WebP 形式へ変換しない
    f (false) - -
    • https://xxx.com/sample.jpg?w=500&p=t
    • whで「最大」としているのは最終的に適用される値はアスペクト比を維持しながら決定される為
  • 変換後の画像品質(quality)は一律で変換元画像の 80% とする

  • 変換後の画像のメタデータは全て削除する(意図せず位置情報等が露出するのを防ぐ為)

この仕様だとメインロジックは index.js 1ファイルに収まりました。メインロジックを少し補足をします。

l.7〜l.12

let sharp;
if (process.env.NODE_ENV === 'local') {
  sharp = require('sharp');
}else{
  sharp = require('../lib/sharp');
}

画像変換には sharpというライブラリを利用しています。このライブラリのランタイムは実行環境により異なる為、ローカルでは開発環境の構築時にインストールされた node_modules/sharpを利用し、AWS 上で Lambda 関数として実行する際は lib/sharpディレクトリのものを利用するようにしています。

サンプルコードのリポジトリには lib/sharpディレクトリは含まれていません。AWS 上で Lambda 関数として動かすには、EC2 等を構築して Amazon Linux 上で $ npm install sharpを実行し、生成された node_modules/sharpディレクトリを中身ごと libディレクトリ配下へ配置してください。

l.18〜l.19

exports.handler = (event, context, callback) => {const{ request, response } = event.Records[0].cf;

関数への入力として渡される eventオブジェクトから requestオブジェクト、responseオブジェクトを取り出しています。

l.36〜l.39

if (response.status === '304') {
  responseOriginal();
  return;
}

CloudFront 上のキャッシュがきれた場合、S3 に対して ETag 付きの条件つきリクエストを送ってきます。S3 からは 304 コードが返ってくるので、この場合は何もせずレスポンスをスルーして終了します。CloudFront が 304 レスポンスを受け取った場合、キャッシュの破棄ではなくキャッシュの保持期間の延長が行われます。

l.77〜l.82

s3.getObject(
  {
    Bucket: BUCKET,
    Key: options.filePath.substr(1), // 先頭の'/'を削除})
  .promise()

非同期の処理を行うにあたりコールバックのネストが深くなってしまうので、可読性の向上を目的に Promise インタフェースを利用しています。

l.85

return sharpBody.metadata();

変換前の画像のメタデータを取得しています。ただし取得は非同期です。

l.95

sharpBody.resize(options.width, options.height).max();

画像のリサイズを行っています。ここで .max()を指定することにより画像のアスペクト比が維持されます。

l.99〜l.101

return sharpBody
  .rotate()
  .toBuffer();

sharp では .withMetadata()を指定しない限り、変換後の画像のメタデータは全て削除されます。この際、画像の orientation(向き) の情報も削除されてしまう為、変換後の画像をブラウザ等で表示すると画像の向きが反映されていません。 .rotate()を指定すると、画像の向きが合うよう画像データそのものが回転されます。

また、今回 .quality()での画像品質の指定は行っていないので、デフォルトの 80 が適用されます。

l.104〜l.112

response.status = '200';
if (options.webp) {
  response.headers['content-type'] = [{ key: 'Content-Type', value: 'image/webp'}];
}else{
  response.headers['content-type'] = [{ key: 'Content-Type', value: 'image/jpeg'}];
}
response.body = buffer.toString('base64');
response.bodyEncoding = 'base64';
callback(null, response);

画像をレスポンスするコードです。 Content-Lengthヘッダはここで設定しなくても AWS 側で自動で付与されます。

今回、S3 からのレスポンス(response変数)をそのまま利用し必要な箇所だけ上書きしている為、ETagLast-Modifiedヘッダはここで再設定しない限り S3 から返されたものがそのまま CloudFront に渡ります。変換オプション毎に変換後の画像データは異なる為、ETagLast-Modifiedヘッダも変換オプション毎に変更した方が良いと考えるかもしれません。ただ CloudFront でクエリ文字列込の URL ベースでキャッシュするようにしている場合は、ETagLast-Modifiedヘッダは共通で問題ありません。変換オプションが異なれば URL も異なるので、別ファイルとしてみなされるからです。

もし、レスポンス時にクライアント側や CloudFront のキャッシュを制御する場合は response.headers['cache-control'] = [{ key: 'Cache-Control', value: 'max-age=604800, s-maxage=31536000' }]等とします。ただ s-maxageは CloudFront 側の設定との兼ね合いがある為、ここでは設定せず CloudFront 側のキャッシュ期間の設定に委ねた方が安全かもしれません。

l.145

class FormatError extends Error {}

Promise チェーン中で発生したエラーを区別する為のカスタムエラーです。

ローカル開発環境について

サンプルコードのリポジトリを見てもらえば分かると思いますが、特にフレームワーク等は使っていません。ただし機能の開発中にローカルでも実行できるようにはしてあります。オリジナルレスポンスはダミーの JSON で代替していますが、関数から S3 には実際にアクセスして画像を取得します。開発中はダミーの JSON の中身を適宜変更し、画像ファイルはテスト用の画像を S3 に置いてください。

ローカルの Node.js のバージョンは AWS 上の Lambda の実行環境と合わせて 6.10 としてください。またローカルから S3 へアクセスする為に、プロジェクトディレクトリの一つ上の階層に AWS SDK をインストールしておきます。

$ npm install aws-sdk

ローカルで関数を実行するにはコンソールで次のようにします。

$ npm run local-run

変換後の画像については base64 エンコードされた文字列がコンソールへ表示されます。この環境を拡張して画像を保存・表示するようにするとより良いかもしれません。

AWS の管理コンソールへアップロードする為のアーカイブ(***.zipファイル)を作成する場合は、コンソールで次のようにします。

$ npm run create-package

ローカル開発環境については下記のスライドにも書いたので、よろしければ参照ください。これは以前に私が 東京Node学園 でトークした際の資料です。Lambda@Edge 用でなく通常の Lambda 関数の開発について説明した資料ですが、14ページ以降のローカル環境についての記載は Lambda@Edge でも共通です。

関数のアップロードと動作確認

作成した関数を実際に AWS 上で動かすには、先の手順で作成した関数の枠組みを開き、アーカイブをアップロードします。今回のサンプルコードではメインロジック index.jssrcディテクトリ配下に置いてあるので、「ハンドラ」には src/index.handlerを指定します。

f:id:hkusu:20180524154940p:plain

また「メモリ」「タイムアウト」も適宜、変更しておきます。元画像の大きさによりますが、経験的にはメモリは 1024 MB、タイムアウトは数秒あれば十分そうですが、ここでは余裕をもってそれぞれ 2048 MB、15 秒を指定することにします。このあたりは元画像と画像変換の内容によるので適宜、調整してください。

f:id:hkusu:20180524154946p:plain

設定を保存したら、新しい「バージョン」を発行します。

Lambda 関数はコードと設定をひとまとめにして履歴管理できます。この操作は1つの履歴のバージョンとして保存するという意味です。

f:id:hkusu:20180524154951p:plain

バージョンが作成されたら、このバージョンのコードおよび設定を CloudFront と関連づけします。トリガーとして CloudFront を選び次のように設定を行います。

f:id:hkusu:20180524154955p:plain

f:id:hkusu:20180524155001p:plain

設定を保存し、CloudFront へ反映されるのを少し待った後、ブラウザで CloudFront のホスト + 画像の URL へ変換オプションのクエリ文字をつけてアクセスしてみます。指定したサイズの画像が表示されれば OK です。

f:id:hkusu:20180524155006p:plain

おわりに

Lambda@Edge で画像をリアルタムに変換する仕組みについて紹介しました。今回のサンプルコードは画像のリサイズと WebP 形式への変換というシンプルなものでしたが、更に画像のフィルター加工(ぼかし等)や画像のクロップ(切り抜き)、また画像の合成等を実装してみると面白いかもしれません。

冒頭のとおりまだ実運用では試してないので、今後もし実際に運用する機会があったらそこで発生した問題や解決方法、知見をまた紹介したいと思います。また画像変換に関わらず Lambda@Edge を実際に運用してみた、などの事例がありましたら是非ブログ等で紹介いただければ幸いです。

参考にしたサイト

クックパッドは、RubyKaigi 2018 でみなさんにお会いできることを楽しみにしています!

$
0
0

こんにちは! 広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

クックパッドは RubyKaigi 2018にRuby Committers SponsorとNetwork Sponsor として協賛します。 Ruby Committers Sponsor とは、「Ruby Committers vs the World」に参加されるRubyコミッターの交通費をサポートするものです。 また、Network Sponsor に関しては、会場ネットワークの設計・構築・運用などを @sorahが担当しております。

そして、クックパッドに所属する5名(@pocke@riseshia@wyhaines@ko1@mame)が登壇し、4名(@nano041214@asonas@sorah@mozamimy)が運営として関わってくれています。

ブース出展やドリンクアップ開催もいたしますので、そちらも合わせて紹介します。 クックパッド社員は約40名参加しますので、みなさまと交流することを楽しみにしています。

登壇スケジュール

はじめに、社員が登壇するセッションのスケジュールを紹介します。

1日目 5月31日(木)

  • 16:40-17:20 桑原仁雄(@pocke):A parser based syntax highlighter
    @pockeが作成したIroというgemについてお話します。このgemは、Rubyのシンタックスハイライターです。今回はその特徴と実装についてを紹介します。
  • 17:30-18:30 ライトニングトーク
    • Sangyong Sim(@riseshia):Find out potential dead codes from diff
      Rubyで静的に未使用コードを探す時に間違って検出してしまうのを減らす方法について紹介します。

2日目 6月1日(金)

  • 10:50-11:30 Kirk Haines(@wyhaines):It's Rubies All The Way Down
    通常、Webアプリケーションスタックでは、アプリケーションそのものの処理にRubyを使用し、それ以外のレイヤーはRuby以外の言語で書かれているものをつかいます。発表では、その他のレイヤーについても、Rubyにしてみたらどうなるかを見ていきます。
  • 13:00-13:40 笹田耕一(@ko1):Guild Prototype今開発中の、Ruby 3 の並列並行処理のための新機能 Guild について、そのプロトタイプと実装方法を紹介します。
  • 16:40-17:20 遠藤侑介(@mame):Type Profiler: An analysis to guess type signatures
    Ruby3 の静的解析の構想をお話します。特に、Ruby プログラムから型情報を推定する型プロファイラの試作や検討状況に関する報告です。
  • 17:30-18:30
    • Cookpad Presents:Ruby Committers vs the World こちらの時間では、Cookpad Ltd CTOの Miles Woodroffe がご挨拶いたします。また、笹田耕一と遠藤侑介が司会を務めます。

3日目 6月2日(土)

  • 16:40-17:40 遠藤侑介(@mame):TRICK FINAL
    TRICK FINALとは、@mameが主催する変な Ruby プログラムで競い合うプログラミングコンテストです。 こちらの時間ではその結果を発表し、入賞作品を解説します。全く読めない、何の役にも立たない、実現不可能としか思えない珠玉の Ruby プログラムたちを楽しみましょう。

ブース

RubyKaigi 2018ではブースの出展もしております。下記スケジュールの通りライブコーディングや登壇者へのQ&A タイムなど、様々なプログラムを予定しております。グッズの配布も行いますので、ぜひお立ち寄りくださいね! 

1日目 5月31日(木)

  • 15:20-15:50 午後休憩: Cookpad live coding by @hokaccha
    @hokacchaがクックパッドのサイトを変更・デプロイする様子をブースでライブコーディングします。

2日目 6月1日(金)

  • 12:00-13:00 ランチ休憩:Q&A タイム by @pockeこの時間は、クックパッドブースに@pockeがおりますので、1日目 5月31日(木)16:40-17:20 A parser based syntax highlighter に関するご質問がある方は、ぜひこの時間にブースにて、本人に聞いてみてください。
  • 15:20-15:50 Global Office Hoursクックパッドは、海外事業の全てを統括する第二本社をイギリス・ブリストルに開設しサービス展開を進め、展開国数は現在68カ国となりました。本時間には海外勤務経験のある社員がブースにおります。海外で働くことに興味がある方は、ぜひお気軽に話しを聞いてみてください! 

3日目 6月2日(土)

  • 12:00-13:00 ランチ休憩:Q&A タイム by@wyhainesこの時間は、クックパッドブースに@wyhainesがおりますので、2日目 6月1日(金)10:50-11:30 It's Rubies All The Way Down に関するご質問がある方は、ぜひこの時間にブースにて、本人に聞いてみてください。
  • 15:20-15:50 午後休憩:Ruby interpreter development live @ko1@mameによるRuby インタプリタのライブコーディングです。登壇時の発表内容に関してご質問がある方も、この時間にお声がけください。 

Drink Up

Cookpad X RubyKaigi 2018: Day 2 Party

Cookpad international HQ team is hosting a party in the evening on Day 2 of RubyKaigi 2018. Come along to network, meet other Rubyists and perhaps a Ruby committer or special guest or two. How exciting!

www.eventbrite.com.au

Asakusa.rb × Cookpad「Meetup after RubyKaigi 2018」

Asakusa.rb × Cookpadのコラボレーションで、RubyKaigi 2018のアフターイベントを弊社オフィスにて開催します。美味しいお酒とご飯を食べながら、RubyKaigi 2018について振り返りましょう。 懇親会の時間もたっぷり取っていますので、お楽しみいただけたら幸いです。詳細は下記よりご確認下さい。

※ご好評を頂き、全て満席となりましたのでご了承下さい。たくさんのお申し込みありがとうございました。 cookpad.connpass.com

おわりに

会場でクックパッド社員をお見かけの際には、ぜひお声がけください。また、発表内容へのご質問やクックパッドにご興味をお持ちの方は、上記スケジュールをご確認の上、お気軽にブースまでお越しください。みなさまにお会いできることを楽しみにしております。

Railsアプリケーションでフォームをオブジェクトにして育てる

$
0
0

ユーザーエンゲージメント部の諸橋 id:moroです。

わたしはずっと、ユーザー登録やログイン周りという、サービス的には基盤的なところ、技術スタック的にはアプリケーション寄りのところに取り組んできました。関連する話を何度かこの開発者ブログにも書いています。

今日はそのあたりの開発を通じて考えた、Railsアプリケーションでのフォームオブジェクトやサービス層といったものが何であるか、という問いに対する、現在の自分のスタンスを紹介します。

サービス層、サービスオブジェクト、フォームオブジェクト

もともと Railsは Web 画面から DB 構造までをあえて密に結合させることで、簡単なサービスを高速に開発するフレームワークとしてデビューしました。と同時に、 Web アプリケーションフレームワークとしての使い勝手の良さや時流も手伝って、そう単純でないサービスを作るのにも使われるようになりました。

そうした背景も踏まえて、この数年は Rails の設計に関する興味も高まってきており、MVC だけでないレイヤの導入や DDD の諸アイディアの適用への興味も高まっているように思います。 中でもよく参照される考え方は、Code Climate 創業者の @brynary さんによる 7 Patterns to Refactor Fat ActiveRecord Models*1でしょう。

この記事では、モデルにまとめて書かれがちな処理を、Form Object や Service Object に分けていくことが提案されています。

あるいは、Trailbrazer や Hanami といった after Rails 世代のフレームワークにおいても、Operation や Interactor と呼ばれるレイヤやコンポーネントによって、Rails の素朴な MVC におさまりきらない処理を記述する場が用意されています。

筆者も、「ユーザー基盤の切り出し」として新たに Rails アプリケーションを作るにあたり、このあたりのアイディアをおおいに参考にしました。その上で、これらはつまり

『ActiveRecord::Base を継承し、永続化を中心に、バリデーションやコールバック、アクセサといった責務が詰まったモデル』や『HTTP リクエストを受けたり、処理結果のレスポンスを組み立てる責務をもったコントローラ』といったコンポーネントをそれぞれの責務に集中させたい、そのうえで必ずしもそのように分類できないアプリケーションのドメイン独自のロジックを書く場所がほしい、ということでないかと考えました。

このあたりで考えていることは、前回記事でも触れています。 そこで、分割した責務を配置する層を便宜上「フォームオブジェクト」と呼び、全体として収まりが良いコードになるように育てていくことにしました。 *2

またその際、新たなフレームワークを導入するのではなく、勝手知ったる Rails の上に app/formsというレイヤを作りできるだけプレーンな Rails (とは?) の構成ではじめました。

このレイヤのオブジェクトに求めるもの

フォームオブジェクトやサービス層、あるいは Interactor や Operation などいろいろな呼び方やそれに伴う視点の違いはありますが、共通しているのは以下の点です。

  • 外部入力(Railsで典型的なのはWebリクエスト、より具体的に言うと ActionController::Base#requestや 同#params )とドメインロジックを分離する。
    • ドメインロジックへの入力が単純なオブジェクトになり、入出力とロジック部分の境界がはっきりする。
    • ActiveJob として起動される非同期ジョブにしたくなった場合も、入出力境界がはっきりしているため、簡単に移行できる。
    • ユニットテストも書きやすくなる。
    • より高レベルなテストにおけるデータセットアップにて本物のロジックを呼び出せる。テストでも「本物の」データグラフを使うことができる。
  • Rails の素朴なアクティブレコードパターンだけでは収まらない処理を担う。
    • 複数のARモデルを一度に永続化するときに、データを組み立てる場がある。
    • 上記のシーンで頻出するものの扱いの難しい accept_nested_attributes_forを避けられる。
    • ActiveRecord モデルクラス (以下ARモデル) ではなくコンテキストに依存にする、バリデーション・コールバックを扱う。
    • 入力がリクエストパラメータやHTTPヘッダではなく、ただの文字列や数値になるため。

逆にいうと、処理を行うクラスを app/models/*に配置しつつ上記のようなアプリケーション内での境界に留意するのであれば、無理に新しいレイヤを導入する必然性は低いでしょう。

そのあたりは、チームの中で合意形成するのがよいと思います。

以降、実際のアプリケーションへのフィーチャ追加を通じて、このフォームオブジェクトの有り様を抽出しブラッシュアップしていった過程、並びにそこから考えたことを紹介します。

サービスの形をオブジェクトにする

以前に紹介したように最近、電話番号でもクックパッドへのユーザー登録できる、という大きなフィーチャをリリースしました。 これは、従来メールアドレスの登録を前提としていたクックパッドへのユーザー登録を、SMSで所有確認をした携帯電話番号でも登録できるようにする、というものです。

ひとくちに電話番号でのユーザー登録といっても、このフィーチャを実現するには文字通りの「登録」だけでは足りません。実際は以下のような機能がすべて必要になります。

  • 未登録のサービス利用者が、電話番号で新規にユーザー登録できる機能
    • 完全に新規の場合と、電話番号登録前にすでに有料サービスを利用開始している(システム的にはユーザーデータが存在する)場合がある。
  • メールアドレスとパスワードで登録したユーザーが、電話番号も追加登録できる機能
  • 電話番号を登録したユーザーが、その電話番号を変更できる機能
  • パスワードを忘れたユーザーが、電話番号の所有確認をしてパスワードリセットできる機能

できるだけ素直にフォームオブジェクトにしようとしていたため、「電話番号での新規ユーザー登録」の時点は無理に共通化しようとせず、完全新規の場合とすでに有料サービスを利用しているケースでそれぞれ一揃い個別のフォームオブジェクトを作りました。

最初の2例程度は良かったのですが、さすがに冗長に感じてきたため「電話番号の追加登録」の実装に入るタイミングでもう一度よく考えてみました。

すると、これらの機能群はすべて「ユーザーが入力した電話番号に対し認証コードを含むSMSを送信し、その認証コードが一致していたら電話番号を所有している本人であるとみなす」(以下『認証コード突合』)という振る舞いを含んでいることに気づき、その方向でコードを整理していくのがよさと考えました。

とはいえ、コードの字面や現時点での動きが似ているからという理由だけでコードを安易に共通化すべきではありません。たまたま現在の挙動が同じであるのか、それとも対象ドメインで同一でみなして良いものであるのかをよく考え、共通化する範囲や方法を考えるべきです。そこでコードの事情からはいったん離れ、ドメインエキスパートと一緒に共通化の方向性を探ることにしました。

結果として「『認証コードの突合』と"何か"をする」という継承 + テンプレートメソッドパターンの作りではなく、「何かする過程において共通の『認証コード突合』をし、続きを行う」というコンポジション的な構造となるように共通化するようにしました。ドメインエキスパートとともにユーザーから見える振る舞いを考えても、「『電話番号での登録』is a『電話番号を確認してなにかする』である」「『電話番号の追加』is a『電話番号を確認してなにかする』である」というのはピンとこず、コンポジションになっているほうが違和感がないとのことでした。

こうではなく f:id:moro:20180529182315p:plain

こう f:id:moro:20180529182320p:plain

見出した形に向けてリファクタリングする

技術的にもサービスの概念的にもこの『認証コードの突合』が抽出できそうということが分かってきたので、その方向にリファクタリングしていきます。まず「電話番号の追加登録」の構造は以下の3種類の画面に分割することができそうです。

  1. 最初にユーザーが電話番号を入力するフォーム (PhoneNumberAdditionForm) のある画面
  2. 認証コードSMSを送信し『認証コードの突合』をするフォーム (PhoneNumberAddition::VerificationForm) のある画面
  3. 突合に成功したあとに、電話番号データを永続化し、そのあとで正常に追加できた旨を表示する画面

このうち 2.の『認証コードの突合』を行うフォームに必要な振る舞いを詳細に見ていくと、次のようになります。

  • 規定回数・時間内に、正しい認証コードを入力したとき、次のステップに進む
  • 一定の有効期限を過ぎてから照合された場合、最初から入力を促す
  • 間違った認証コードが入力された場合、規定回数以下であれば再入力を促す
  • 規定回数を超えて間違った認証コードが入力された場合、最初から入力を促す

そのあたりを踏まえて、このような構造にしました。

  • コード照合、その結果取得、失敗回数などによる再入力可否の判定を ::PhoneNumberVerificationFormとして抽出した。
  • 「次のステップ」を導出するために必要な振る舞いを PhoneNumberAddition::VerificationFormに残した。
  • コントローラからは PhoneNumberAddition::VerificationFormを参照する。
    • 照合結果取得メソッドなどを ::PhoneNumberVerificationFormに委譲する。
    • 委譲の宣言を含め、最終的に結合させる部分のみ、小さな module にして mixin する。
  • コントローラは、 #認証に成功した?が真の場合、 #次の遷移先を表すリソースを取得してリダイレクトする。

このようにすることで、個別のフォームクラスの責務がはっきりし、コードも整理できました。

f:id:moro:20180529182411p:plain

その後引き続き「電話番号の変更」や「電話番号でのパスワードリセット」を実装していきましたが、目論見通り PhoneNumberVerificationFormの修正はほとんど不要でした。これまた振る舞いの観点から言い換えると、すでに作って共通化された電話番号の所由確認の仕組みを使い、電話番号変更機能に必要なぶんのみ実装することで、機能追加できたことになります。

横展開のイメージ

f:id:moro:20180529182430p:plain

「フォーム」とはなにか

このように、アプリケーションのドメインを見ながらコードをリファクタリングしていった結果、あるフォームオブジェクト PhoneNumberAddition::VerificationFormの中から別のフォームオブジェクト PhoneNumberVerificationFormを呼び出す構造となりました。アプリケーションへのおさまり具合はよかったものの、これは「フォームオブジェクト」という名前から想像する、画面の入力項目を表す「フォーム」の形からは大きく異なります。

そこで、もしかして「フォーム」のもともとのニュアンスは違ったりしないかな、と思い調べたり Twitter でも聞いてみたりしましたが、やはり入力フォームからきているようでした。*3そのため独自研究の単語連想ゲームにはなってしまうのですが、この「フォーム」をアプリケーションにおけるドメインの「形」を写し取ったものと解釈できないかと思っています。

題材としているフィーチャには、電話番号の登録や変更という「形」があり、これらは一連の機能のエントリポイントであるため目に付きやすいです。いっぽうその一部分と認識されがちな『認証コード突合』フローも、それ自体が多くの機能を持った大事な「形」として扱い、抽出して独立させました。

さらにフィーチャ全体では、電話番号の変更や、電話番号によるパスワードリセットといった機能も必要となりました。この場合でも共通する『認証コード突合』をそのまま使いつつ、エントリポイントとして各機能を実装しました。結果、コードの追加量的にも、必要な工数的にも納得できる程度でつくることができました。フィーチャに対してよいモデルを作れたのではないかと思います。

冒頭でも触れたように、このようなRailsコントローラ層とモデル層の間にもう一層設け、ドメインの複雑な処理をそこに配置するという設計手法は、一般的になってきました。それを「フォームオブジェクト」と呼ぶか、あるいは「サービス層」と呼ぶかに関しては、筆者は実はそこまでのこだわりはありません。

一方、大事だと思うのは、そこにドメインの形が現れてくるように作る、あるいは現れるように継続的に手を入れていくことです。今回の例ではこのように『認証コード突合』を抽出しましたが、今後また新たな要求を実現すべく眺めた場合、別の形(フォーム)が浮かび上がってくるかもしれません。それを見逃さずに、柔軟に育てていけるようでありたいと思っています。

まとめ

ドメインのありように注意を払いながら、フォームオブジェクトを育てていった話をしました。

  • ドメインに関する処理を Web の入出力と分けるためにフォームオブジェクトを導入した。
  • 継続的にリファクタリングしコードを育てているうちに、フォームオブジェクトの構造とドメインの構造が一致した。
  • その経験から、「フォーム」という語について考察してみた。入力フォームとしてだけでなく、ドメインの形(フォーム)であることを意識すると、エンジニアだけでないチームみんなで同じ視点からソフトウェアを見られた。
    • スッキリハマったときは、とてもたのしかった!

こういうふうにアプリケーションの形を彫り出してみると、コードもスッキリするし、テストもしやすいし、ドメインエキスパートはじめチームの色んな人と認識が一致して、とても楽しい体験だった、という一つの小さなストーリーを紹介しました。お題となったフィーチャ自体はとてもニッチかと思いますが、なんらかみなさんのお役に立つとうれしいです。

明日 5/31 から RubyKaigi 2018 ですね。クックパッドでも、社員が発表したり、各種パーティーなどを企画しています。ブースなどもありますので、ぜひお立ち寄り下さい。もちろん、筆者も参加予定です。

たのしい RubyKaigi と、その後も続くよいソフトウェア開発を!

*1:@hachi8833 さんによる 邦訳

*2:前回記事の時点だと「サービス層」と読んでいましたね。後述のように、そこの呼称じたいへのこだわりはあまりなくなってきています

*3:https://twitter.com/moro/status/965444586276466690

オフィス・AWS環境をセキュリティ監視するためのログ収集

$
0
0

インフラストラクチャー部セキュリティグループの水谷 (@m_mizutani) です。

現在、クックパッドのセキュリティグループではセキュリティ監視を高度化に対して取り組んでいます。サービスに関連する部分の監視は以前からやってきたのですが、ここしばらくはそれ以外のインフラやオフィスで発生するセキュリティ侵害を検知することを目的とした監視基盤の構築に力を入れています。

昔は一般的にオフィス、インフラのセキュリティ監視と言えば、イントラネット内に閉じた環境でのログ収集から分析まで完結していたケースも少なくなったと考えられます。しかし現在だとインフラとしてクラウドサービスを多用したり、業務で使うツールをSaaSによって提供するという場面も増えているかと思います。このような状況だとセキュリティ監視のために見るべき箇所がばらけてしまうといったことが起こります。クックパッドでも積極的にSaaSやAWSを利用しており、個別のサービス毎にログは蓄えられているものの、それぞれを活用しきれていない状況が続いていました。この状況を改善するため、下記の図のように各所から必要なログをかき集めて利活用できる状態にすることが、「監視の強化」になります。

f:id:mztnex:20180529104148p:plain

必要なログを一箇所に集めてセキュリティ監視に利用できる状態を作ることで、以下のようなメリットがあります。

  • アラートの通知・対応フローを一本化できる: 監視している機器・サービスにおいて危険度の高いイベントが発生した場合にはセキュリティの担当にアラートとして通知してほしいですが、通知の方法が機器・サービス毎に異なると設定変更の漏れや、通知の受け取りをミスしてまう可能性もあります。例えば通知方法で言えば、メール通知のみ、syslogへの出力のみ、APIに問い合わせないとわからない、というようにバラバラなことはよくあります。また、通知するメンバーをローテーションしたりするといった管理もとても煩雑になってしまいます。これを一本化することで、どのような機器・サービスで発生したアラートでもスムーズに対応できるようになります。
  • ログの検索や分析が統一されたコンソールから実施できる: インシデントと疑わしい事象が発生したらそれが本当に影響があったのかをログを見ながら調査・分析しなければなりませんが、ログが適切に保存されているとしてもアクセスするためのインターフェース(コンソール)が分断されていると、検索や分析に支障がでます。経験したことがある方はわかるかもしれませんが、複数のコンソールをいったりきたりしながら様々な可能性を検証するという作業はかなりのストレスになります。また、作業が煩雑になり見るべきログを見逃したりログの相関関係を読み間違えることもありえます。これに対して、統一されたコンソールが用意されていれば同じキーワードで全てのログを一括して検索可能になりますし、データが正規化されていればまとめて分析することもでき、分析にかかる負担を大きく減らすことをできます。
  • ログの保全管理が容易になる: セキュリティ関連のログは監査の目的であったり、後から発覚したインシデントを調査するため、一定期間保全しておく必要があります。通常、各サービスのログもしばらくの間保全しておく機能を備えていますが、多くの場合は保全する期間がばらばらだったり、保全のポリシーが異なります。そのため管理が煩雑になり、必要な時にあるべきログが無いといったミスが発生しやすくなります。これを防ぐため、統一管理されたストレージにログを保全することで、保全の期間やポリシーの制御が容易になります。

この記事ではセキュリティ監視をするにあたって、実際にログの収集をどのように実施しているかを紹介し、さらに集めたログをどのように利用しているのかについても簡単に紹介したいと思います。

ログの収集

セキュリティに関連するログとして、主にインフラ(AWSサービス周り)のログ、オフィスネットワークのログ、スタッフが使う端末のログ、そしてスタッフが使うクラウドサービスのログという4種類を収集しています。こちらの資料でも少し触れていますが、全てのログは原則としてAWSのS3に保存し、その後S3から読み込んで利用するよう流れになっています。全てのログをS3に経由させるという構成は、以下の3つのポイントをもとに判断しました。

  • 可用性が高く、スケールアウトするか: ログに限りませんが大量のデータを継続的にストレージ入れる際、一時的に流量がバーストするというのは稀によくあることであり、ストレージ側はそれに対応するような構成になっている必要があります。特にDBなどは負荷をかけすぎるとそのままサービスが死んでしまうこともありえますし、かといって普段から余分にリソースを割り当てるとお金がかかります。一方、AWSのS3は投入しただけだと何もできませんが、高い可用性とスケールアウトを実現しているストレージサービスです。「ひとまず保存する」という用途ではスケールアウトを気にする必要もほとんどなく、安定して利用できるというメリットがあります。
  • ログ送信元と疎結合な構成にできるか: ログの収集・処理ではログの送信元からログを処理し終えるまでの一連のフローを考える必要があります。例えば一気通貫でストリーム処理をするような構成だと途中の処理に失敗したらまたログの送信元からやり直しということになってしまいます。しかし、一度S3に保存しておくことで何かあっても送信元まで遡る必要がなくなります。また、送信元のサービスを開発・運用しているのが別の主体だったとしても「とりあえずS3に保存してもらう」というお願いができれば、セキュリティのチームはそこから先の面倒だけ見ればよくなり容易に責任分解ができます。
  • 遅延が許容範囲に収まるか: 到着したログを順次処理していくシステムを作る場合、処理の目的によってどの程度の遅延まで許容できるかが設計に関わってきます。セキュリティ監視の観点で考えると一定のリアルタイム性が求められますが、その時間単位は必ずしも短くありません。例えば日本で大手のSOCサービスで監視および分析専属のスタッフがいる場合でも、通知や初動対応のSLAは10分〜15分程度となっています。クックパッドでも監視と分析をしていますが、専属のアナリストがいるというわけではないため対応に数分の遅れが出るのは運用上許容できると考えることができます。したがって、S3に一度ファイルを保存してから処理を実行しようとすると数分の遅延が発生する可能性がありますが、上記の理由により問題にはならないと判断しました。

加えて、S3にどのようなログを収集しているのかと、その収集の方法について紹介します。

インフラ(AWSサービス周り)のログ

クックパッドではサービスのためのインフラとして主にAWSを利用しています。インスタンスからのログ収集も必要ですが、それ以外にもAWSが様々なセキュリティ関連のサービスを提供しており、これも同様に収集して活用しています。

インスタンスのsyslog

各EC2インスタンスで発生するログを集約し、これもGraylogへと転送しています。こちらもここからインシデントを発見すると言うよりは、後からの調査に利用しています。インスタンスの種類によって細かい収集方法は様々ですが、基本的にはfluentdに集約され、その後でS3に保存されます。

CloudTrail

AWS上でのアクティビティをログとして記録してくれるサービスです。AWS上でのユーザやサービスの動きを追うことができるため、セキュリティインシデントに関連した分析だけでなく、デバッグやトラブルシュートにも利用されています。

CloudTrailは標準でS3にログを保存する機能があるため、これをそのまま利用しています。

GuardDuty

AWS上で発生した不審な振る舞いを通知してくれるサービスです。不審な振る舞いも深刻度でレベル分けされており、深刻度が低いものはEC2インスタンスに対するポートスキャン行為など、深刻度が高いものは外部の攻撃者が利用してるサーバと通信が発生していることを通知してくれます。

GuardDutyが検知した内容はCloudWatch Eventsとして出力されるため、これをLambdaで受け取ってS3に保存する、という構成で運用しています。

VPC flow logs

VPC内のインスタンスから発生する通信フローのログを保存しています。Flow Logsはインスタンスのネットワークインターフェースから発生した通信のフロー情報を全て記録してくれるため、アラート分析やインシデントレスポンスにおいて攻撃の影響判断や通信発生の有無を確認するのに役立ちます。

Flow logは通常CloudWatch Logsに書き出されるため、S3に格納するためにログデータのエクスポートを利用しています。

オフィスネットワーク関連のログ

NGFWのログ

通常のファイアウォールの機能に加え、通信の中身の一部も検査して脅威を検知・遮断してくれるNext Generation Firewall (NGFW) でオフィスネットワークの出入り口を監視しています。これによって怪しいサイトやマルウェアを配布しているようなサーバへのアクセスが検知され、場合によって自動的にブロックされます。

NGFWからは通信フローのログ、および脅威の検知やブロックのログの両方を収集しています。現在利用しているNFGWはsyslogでログを飛ばせるので、fluentdを経由してS3 output pluginを利用し、S3に保存しています。

DNSキャッシュサーバのログ

弊社で利用しているNGFWは通信のTCP/IPフローのレベルの情報は残してくれますが、IPアドレスとポート番号および通信量などの情報しかわからないので、どのような通信がされていたのかは把握が難しい場合があります。そこで名前解決のためのDNSのログをとっておくことで、分析のための材料を増やしています。IPアドレスだけだと判断が難しくても、どのドメイン名のホストと通信したかがわかることでインシデントを追跡できることがしばしばあります。

オフィスではDNSのキャッシュサーバとして unboundを利用していますが、出力されるログの形式が利用しづらいことから、unboundのログは直接利用していません。代わりにキャッシュサーバ上で packetbeatを動かしてパケットキャプチャし、DNSのログを収集したものを fluentd + fluent-plugin-beatsを経由してS3に保存しています。

DHCPサーバのログ

現在クックパッドではスタッフが使うPCに端末管理のソフトを導入していますが、このソフトは端末がどのIPアドレスを利用していたかを過去にさかのぼって調べられません。そのため、DHCPのログも別途保存しておくことで、後から分析する場合でもそのIPアドレスを使っていたのがどの端末だったのかを追跡できます。

クックパッドでは kea DHCP server を運用しています。標準で出力されるログに必要な情報が載っているので、fluentdのtail input pluginでAWS S3にログを保存しています。現状では kea DHCP server ログを読み込むための成熟した専用 fluentd plugin は無いようだったのでひとまず1行1ログとして扱い、後からパースして必要な値を取り出すようにしています。

スタッフが使う端末のログ

アンチウィルスソフトのログ

クックパッドではアンチウィルスソフトとしてCylance PROTECTを採用しており、マルウェアと疑わしいファイルの検知情報をアラートとして利用しています。

この製品はマルウェアと疑わしいファイルや振る舞いを検知した後にログをSaaS側で収集してくれます。現状ではLambdaから定期的にSaaSのAPIをポーリングし、新しいイベントが発生していることを検出した場合はS3に新たにログファイルを生成するようにしています。

スタッフが使うクラウドサービスのログ

G Suite

書類、メール、カレンダーなどを一通り利用しているG Suiteでは各操作のログを取得しています。これによって何か問題が発生したときにスタッフが業務で利用している書類がどこかへ持ち出された形跡があったかどうかなどを調べることができます。

G SuiteではReports APIが用意されており、これを使ってAdmin activity, Google Drive activity, Login activity, Mobile activity, Authorization Token activityを取得することができます。これらAPIにLambdaで定期的にアクセスし、ログデータとしてS3に保存しています。

Azure AD

Azure Active Directory (AD) は Single Sign On のサービスとして利用しており様々な外部サービスを利用する際の認証のハブになっています。こちらも何か問題があった場合にどの端末からどのサービスへのアクセスがあったかを辿ることができます。

Azure ADも監査APIが用意されており、ログインなどに関連するログを抽出することができます。これをG Suiteと同様に定期的に実行し、新しく取得できたものについてログデータとしてS3に保存しています。

集めたログの利用

本記事のテーマはログの収集ですが、集めた後にどのように利用しているかについても簡単に紹介します。S3に集められたログは現在は主にアラート検出とログ検索・分析の2つに利用されています。

アラート検出

ここでいうアラートとは「影響があったかどうか確定ではないがセキュリティ侵害があったかもしれない」という状況を指します。現在、開発の都合で複数のログを組み合わせたいわゆる「相関分析」まではできていませんが、インシデントの懸念があるログが到着した場合にアラートとしてセキュリティグループのメンバー(担当持ち回り)で発報して対応を促します。具体的には以下のようなログを利用しています。

  • NGFWがアラートとして不審なサイトなどへのアクセスを捉えた時
  • Cylanceがマルウェアと疑わしいファイル・プロセスを発見した時
  • GuardDutyが一定以上の深刻度があるイベントを報告してきた時

これらのログが発見された場合、統一された形式にログを変換した上でアラートとして発報されます。担当のセキュリティグループのメンバーは誤検知か否かを分析し、影響があるかもと判断された場合は然るべき人に協力を仰いだり、PCそのものやログの追加調査を実施します。

f:id:mztnex:20180529104217p:plain

検出されたアラートはPagerDutyによって担当のメンバーに通知される

ログの検索・分析

冒頭で述べた通り、集めたログは横断的にログを検索できることが真価の1つになります。クックパッドではログ検索のために Graylog を導入して検索コンソールを構築しています。現状、VPCのフローログやサービスのログといった劇的に流量が多いログについては投入せずにAthenaで検索できるようにするなど工夫をしていますが、それ以外については概ね先述したログをカバーできるようになっています。

これらのログはセキュリティグループのメンバーだけでなく、スタッフは(一部を除いて)ほぼ全てのログにアクセスできるようになっています。そのため、セキュリティの用途だけではなくインフラなどが関係するトラブルシュートにおいても利用されることがあります。

日常的な運用でこのログをひたすら眺めるというようなことはしていませんが、アラートを検出した時に関連するログを調べるために利用するケースが多いです。そのアラートの前後に発生したログや関連するキーワードが含まれるログを読み解くことで、そのアラートは我々にとってセキュリティ侵害の影響をおよぼすものなのか?ということを判断しており、このような作業をログの分析と呼んでいます。複数のコンソールをとっかえひっかえすることなく、キーワードと時間帯を指定するだけで関連するログの分析ができることで、アラート検出時に必要な作業量・時間を大幅に短縮しています。

例えば、GuardDutyから「あるユーザが普段と異なるネットワークからAPIを操作している」というアラートが発報されたとします。その時、Graylog で該当ユーザ名とその前後の時間帯を検索すると例として G Suite でサービスにログインしたログ、そのユーザ名についてメールでやりとりされたログ、AWSで他のAPIを発行したログを一気通貫で見ることができます。これによって、そのユーザがその時間帯にどのような活動をどのネットワークからしていたのかがワンストップで確認できるようになり、それが正規のユーザの活動だったのか、それとも外部の悪意あるユーザがなりすましていたのかを容易に判断できるようになります。

f:id:mztnex:20180529104231p:plain

関連するキーワードと時刻を入力すると複数種類のログを時系列順に一括して見えるのでスムーズに全体像を把握することができる

実装の構成

ここまで解説したログの収集・分析の基盤は以下の図のような構成になります。

f:id:mztnex:20180529104346p:plain

ログ収集のパートで説明したとおり様々な手段を使っていますが、ログは必ずS3に集約するようになっています。S3のバケットはログの種類やサイズ、他のコンポーネントとの兼ね合いで分かれており、流量が大きいものは専用のバケットを用意して、そうでもないものは1つのバケット内でprefixだけ分けるようにしています。

S3にログが保存されると AWS Simple Notification Service (SNS) にイベント通知が飛び、そこからログの転送やアラート検知に関する処理が実行されます。それぞれの機能はAWS CloudFormation (CFn) によって構成されています。このCFnの中身については別記事にて解説していますので、興味がありましたらご参照いただければ幸いです。

まとめ

今回の記事ではオフィスやSaaS、それにインフラ(AWS)からなぜログを収集するのか、そしてどのようなログをどのように収集しているのかについて事例を紹介させて頂きました。クックパッドでは現在、ログ収集だけでなくアラートの検出やアラート発生時の対応に関する取り組みもセキュリティ監視の一環として進めているため、それらについてもいずれ紹介できればと考えています。

iOSアプリの大規模なCustom URL Schemeを支える技術

$
0
0

こんにちは。技術部モバイル基盤グループの@です。

今回は、iOSアプリでCustom URL Schemeを簡単に処理するライブラリを公開しましたので紹介します。

Custom URL Schemeは、アプリの特定の画面に遷移させることができるリンク(ディープリンク)を提供する機能です。

f:id:gigi-net:20180530205333g:plain

アプリ開発をしていると、Custom URL Schemeを用いたディープリンクを実装したい需要は多いでしょう。 特にクックパッドのような、ブラウザ版を提供するWebサービスですと、アプリとWebページの行き来のため非常に多くのCustom URL Schemeを処理する必要が出てきます。

現に、クックパッドアプリでは、30以上のパターンが遷移先として実装されています。

渡ってきたURLのパーサーを愚直に書いていくのは、コードの記述量も増えますし、どのようなURL Schemeが有効なのか簡単に見通すことは難しいです。

Crossroad

そこで、複雑なCustom URL Schemeのルーティングを簡単に実現するライブラリをOSSとして公開しました。

例えば、あなたがiOS上で「ポケモンずかん」を実装する仕事を請け負ったとしましょう。

Crossroadを用いると、以下のような記述でCustom URL Schemeのルーティングが行えます。

letrouter= DefaultRouter(scheme:"pokedex")
router.register([
    ("pokedex://pokemons", { context inlettype:Type? = context.parameter(for:"type")
        presentPokedexListViewController(for:type)
        returntrue 
    }),
    ("pokedex://pokemons/:pokedexID", { context inguardletpokedexID:Int= try? context.arguments(for:"pokedexID") else {
            returnfalse
        }

        guardletpokemon= Pokedex.find(by:pokedexID) else {
            returnfalse
        }

        presentPokemonDetailViewController(of:pokemon)
        returntrue
    }),
])
router.openIfPossible(url)

このように、Ruby on Railsのroutes.rbのようなルーティングを記述することができます。

この仕組みをクックパッドアプリでは、1年以上前から運用していたのですが、今回、別のアプリでも使いやすい形で提供するためにOSS化しました。

同様のライブラリはいくつか公開されていますが、Crossroadはこれらに比べ、パラメータをType-Safeに、そして簡単に取り扱うことができます。

使い方

Crossroadの基本的な使い方を見ていきましょう。

URLのルーティング

iOSでは、Custom URL Schemeからアプリが起動されると、UIApplicationDelegateapplication(_:open:options:)が呼び出されます。

基本的な使い方は、AppDelegateで、ルーティングを定義した Routerを生成し、そこでopenIfPossibleを呼び出すだけです。

import Crossroad

classAppDelegate:UIApplicationDelegate {
    letrouter:DefaultRouter= {
        letrouter= DefaultRouter(scheme:"scheme")
        router.register([
            ("scheme://search", { _ inreturntrue }),
            // ...
        ])
        return router
    }()

    funcapplication(_ app:UIApplication, open url:URL, options:[UIApplicationOpenURLOptionsKey: Any]) ->Bool {
        return router.openIfPossible(url, options:options)
    }
}

:から始まるパスは任意の文字列にマッチし、あとでマッチした値を参照できます。 URLパターンは定義順に上からマッチするかどうかが判定され、ブロックから trueを返された時点で探索を終了します。 複数のURLパターンにマッチしうる場合も、最初にtrueを返した物のみが実行されます。

パラメータの取得

:から始まるパスにマッチした文字列は Argumentとして扱われ、ブロックから取得することができます。

("pokedex://pokemons/:pokedexID", { context in// URLからポケモンずかん番号を取得guardletpokedexID:Int= try? context.arguments(for:"pokedexID") else {
        returnfalse
    }

    // 該当するポケモンを取得するguardletpokemon= Pokedex.find(by:pokedexID) else {
        returnfalse
    }

    // ポケモン詳細画面を表示する
    presentPokemonDetailViewController(of:pokemon)
    returntrue
})

ArgumentはGenericsを利用しているので、任意の型として受け取ることができます。

例えば、pokedex://pokemons/25のURL Schemeからアプリを起動した場合、ずかん番号25番のポケモンが表示されます。

enumの値を取得する

Argumentを利用することで、それぞれのポケモンの詳細画面へ遷移するURL Schemeを実装することができました。

今度はポケモンを検索する画面を作ってみましょう。

URLのクエリとして渡された値は Parameterとして扱われ、Argumentと同様にContextから取得することができます。

ここで、ポケモンのタイプを示すenum Typeを定義してみましょう。 Crossroadでは、Extractableというプロトコルに準拠させることで、任意の型をContextから返却することができます。

enumType:String, Extractable {
    case normal // ノーマルタイプcase fire // ほのおタイプcase water // みずタイプcase grass // くさタイプ// ...
}

enumを表す型であるRawRepresentableは、すでにExtractableに準拠しているため、これだけで文字列をenumにマッピングすることができます。

("pokedex://pokemons", { context inlettype:Type? = context.parameters(for:"type")

    // ポケモン一覧画面を表示する
    presentPokemonListViewController(of:type)
    returntrue
})

これで、pokedex://pokemons?type=fireというURL Schemeからアプリを起動すると、ほのおポケモンのみを表示する画面へ遷移することができます。

一般的な検索画面を実装する場合は、キーワードや並び順などをパラメータで受け取る実装が考えられるでしょう。

pokedex://search?keyword=ピカチュウ&order=asc

複数の値を取得する

ポケモンずかんを実装するに当たって、今度は複合タイプのポケモンをURL Schemeから検索したいという需要が出てくるでしょう。

Crossroadは、パラメータに渡されたカンマ区切りの文字列を配列としてマッピングする機能も提供しています。

// pokedex://pokemons?types=water,grasslettypes:[Type]? = context.parameters(for:"types") // [.water, .grass]

これは、Swift 4.1から利用可能になった、Conditional Conformanceを用いて、[Extractable]Extractableに準拠させることで実現しています。

extensionArray:Extractablewhere Array.Element:Extractable {
    staticfuncextract(from string:String) ->[Element]? {
        letcomponents= string.split(separator:",")
        return components
            .map { String($0) }
            .compactMap(Element.extract(from:))
    }
}

独自の型を取得する

もちろん、独自の型を取得することもできます。Contextから取得したい型をExtractableに準拠させましょう。

structPokemon:Extractable {
    letname:Stringstaticfuncextract(from string:String) ->Pokemon? {
        return Pokemon(name:string)
    }
}
// pokedex://pokemons/:nameletpokemon:Pokemon= try? context.arguments(for:"name")

このように、Crossroadでは、柔軟にパスやクエリパラメータの取得を行うことができます。

Dynamic Member Lookupを使ったインターフェイス

最後に、Swift 4.2から実装される新たな言語機能であるDynamic Member Lookupを使ったインターフェイスの構想を紹介します。

Dynamic Member Lookupは、動的なプロパティ生成を提供するシンタックスシュガーです。 クラスや構造体に@dynamicMemberLookupを宣言することで、ランタイムで評価されるプロパティを生成することができます。

Dynamic Member Lookupを宣言すると、subscript(dynamicMember:)の実装が要求され、プロパティアクセスを行ったときに、プロパティ名が引数に渡され実行されます。

@dynamicMemberLookupstructContainer {
    letvalues:[String: Any]subscript<T>(dynamicMember member:String) ->T? {
        ifletvalue= values[member] {
            return value as? T
        }
    }
}

letcontainer= Container(values:["name": "Pikachu"])
letname:String= container.name // Pikachu

本稿執筆時点では、Swift 4.2の正式版はまだリリースされていませんが、Swift.orgからdevelopmentのToolchainをダウンロードすることで、Xcode 9.3でも利用することができました *1

この機能をCrossroad.Contextに適用してみると、以下のように Argumentを取得できるようになりました。

// match pokedex://pokemons/:pokedexIDletpokedexID:Int? = context.arguments.pokedexID

この実装はまだmasterへマージしていませんが、別ブランチで公開しているので、興味のある方は見てみてください。

まとめ

今回はCustom URL Schemeを簡単にルーティングするライブラリを紹介しました。 ぜひ利用を検討してみてください。もちろんPull Requestもお待ちしております。

技術部モバイル基盤グループでは、OSSを通して問題解決をしていきたいエンジニアを募集しています。

iOS アプリケーションエンジニア(開発基盤)

Android アプリケーションエンジニア(開発基盤)

*1:普通にビルドすることはできますが、静的解析の時点ではシンタックスエラーが発生します

AlexaでE2Eテストを書けるようにした話

$
0
0

研究開発部の伊尾木です。

研究開発部では、Alexaのスキルを公開しています(Google Assistantも公開していますよ!)。

今回はAlexaスキルのテストを便利にするKuchimaneというツールを公開したので紹介したいと思います。

E2Eテストが難しい

音声UIの開発はまだまだ新しい分野で知見やツールがそろっているわけではありません。 特に E2E (End To End) テスト、RSpecでいうところの Feature spec に相当するようなテストを行うことがとても困難でした。

AlexaでのE2Eテスト

以下のような一連の会話があったとします。

あなた「クックパッドを開いて」
Alexa「クックパッドへようこそ」
あなた「大根のレシピを教えて」
Alexa「大根ですね。サラダ、ナムル、スープのどのレシピがいいですか」
あなた「スープ」
Alexa「大根のスープですね。レシピを送信しました」

Alexaでは、この一連の会話「クックパッドを開いてから、レシピを送信するまで」をローカルでテストする方法がありません。 (Alexaのデモ環境に都度リクエストを投げればテストはできますが、やっぱりローカルだけでやりたいですよね)

会話の一部だけ、一回のやりとりだけのテストなら可能です。

例えば、「クックパッドを開いて」->「クックパッドへようこそ」 の組み合わせのみテストするといったことは可能です。 が、全部通したテストを書くことはできません。

通常のWebアプリのテストでいえば、 「一回のHTTPリクエストごとのテストは可能だけど、複数HTTPリクエスト、あるいは複数画面にまたがるテストが書けない」 という状況と同じです。

とても不便ですよね。

なぜできないのか

Alexaでは、ユーザの生の発話を、開発者が直接操作することはありません。 一旦、内部的なインテントとよばれるユーザの意図を表している処理に変換します。

例えば「クックパッドを開いて」という発話は、内部的に LaunchRequest というインテントに変換されて処理を実行します。 Webアプリとの対比でいえば、URLからコントローラ・アクション名に変換するルーティング処理と同じような感じです。

このルーティング処理が、Alexa内部に隠蔽されているため、ローカルでテストすることができないのです。 どうしてもローカルで一連の会話をテストしたい場合、ルーティング処理を自前で処理する必要があります。

Kuchimane

私達も当初、インテント単位のテストだけで乗り切ろうとしていましたが、複数インテントが絡む処理はテストできないため、エラーが起きやすい状況でした。

そこで、一連の会話をテストするための Kuchimaneを開発しました!

じゃぁ Kuchimane では、さきほどのルーティング処理をどうしているのかというと、これまた自前で実装しています。

より正確にはsatori-flowというルーティング処理用のライブラリを開発しています。 KuchimaneがAlexaの会話モデル定義を解析し、このsatori-flowに「どんな発話がどのインテントになるのか」を登録します。

というわけで、以下のようなコードが書けるようになります!

const intents = { LaunchRequest, SearchDishIntent, SearchRecipeIntent };
const kuchimaneRunner = Kuchimane.runner(intents, __dirname + '/kuchimane_config.json');

it('searchRecipe', () => {return kuchimaneRunner.talkCheck('クックパッドを開いて', (message) => {
      expect(message).to.include('クックパッドですね')
    })()
    .then(kuchimaneRunner.talkCheck('大根のレシピを教えて', (message) => {
      expect(message).to.include('大根ですね。サラダ、ナムル、スープのどのレシピがいいですか');
    }))
    .then(kuchimaneRunner.talkCheck('スープ', (message) => {
      expect(message).to.include('大根のスープですね。レシピを送信しました');
    }))
  }
);

最初の行でintentsというオブジェクトを生成していますが、ここのLaunchRequestSearchDishIntentSearchRecipeIntentがインテント関数になります。 次にkuchimaneRunnerというインスタンスを、さきほどのintentsとKuchimane用の設定ファイル(Alexaのモデルへのパスなどを書く)から生成しています。

kuchimaneRunnertalkCheckというメソッドがE2Eテスト用のメソッドになります。第1引数がユーザの発話、第2引数がチェック用の関数になります。

talkCheckメソッドはユーザの発話を受け取ると、それを satori-flow に渡してインテント名に変換してもらいます。 そして、kuchimaneRunnerの生成時にもらったintentsの中から、インテント名にマッチする関数を取り出して実行し、Alexaのレスポンスをチェック用の関数に渡してテストを実行します。 最後にtalkCheckメソッドは、Promiseを返しますので、thenで会話を繋げていきます。

一連の会話をテストで書けることがわかりますね! 便利ですね!!

おわりに

AlexaのE2Eテストのための Kuchimaneの紹介でした。

バグの多くは機能の組み合わせ部分に潜むと言われますが、実際私達も複数の会話、複数のインテントが絡む部分でよくエラーが起きていました。 Kuchimane以前は、このような部分をテストすることができなかったのですが、Kuchimaneのおかげで複数の会話が絡む部分をテストできるようになり 品質向上に一定の効果があるなと感じています。

ちなみに、まだまだKuchimaneの完成度は高くありません。例えばASK SDK v2 にも対応できていませんし、私達にとって必要な部分を優先的に実装しているため、フォローできていないケースもあります。これらの点については今後拡充していく予定です。

また現状ではGoogle Assistantに対応していませんが、こちらも今後対応する予定です!


大きな Rails アプリケーションをなんとかしよう。まずは計測と可視化からはじめよう。

$
0
0

こんにちは、技術部開発基盤グループの id:hogelogです。

RubyKaigi 2018 楽しかったですね。僕はおそらく RubyKaigi 2010 以来の久しぶりの参加でした。ああいう場の楽しさを思い出し、また今回はスポンサーブースから RubyKaigi に参加するという学生の頃は知らなかった楽しみも新たに知り、RubyKaigi を満喫させていただきました。

さて今回はそんな RubyKaigi で取り戻した Ruby に対する感情と関係あるようなないような、最近自分が取り組んでいるお台場プロジェクトとプロジェクト内で実施している計測と可視化について紹介します。

お台場プロジェクトの発足

クックパッドの開発といえば数年前までは cookpad_all という一つのリポジトリの中に詰め込まれた巨大なモノリシック Rails アプリケーションを社内のエンジニアが寄ってたかって開発するというのが典型的な開発スタイルでした。世界でも類を見ない規模の巨大な Rails アプリケーションの開発であるため、もちろん多様な技術的困難が発生していましたが様々な技術を用いてアプリケーションをメンテナンスし Rails の良さを損なわず開発が進められるように努力していました。*1

しかしその後クックパッドでも徐々にモノリシックアプリケーション構成から Microservices 構成への移行が進んでいきました。 techlife.cookpad.com

そして気づけば cookpad_all は社内に数多く存在する他のアプリケーションと比較してずいぶんと古臭い、触ることが忌避されがちなアプリケーションの代表となっていました。 https://cookpad.com/のバックエンドの大部分を支える重要なアプリケーションであるというのは変わっていないのに。そこで始まったのがお台場プロジェクトでした。お台場プロジェクトとはなんなのか。その全貌を語るのはまた別の機会としますが、実施することは端的に言えば cookpad_all というアプリケーションの実装の改善です。

お台場プロジェクトではレガシーなシステムの削除、未使用コードの削除、システム分割など様々なことをおこなっており、 id:riseshiaが取り組んだ Ruby の lazy loading の仕組みを利用して未使用の gem を探す - クックパッド開発者ブログや RubyKaigi 2018 LT で発表した Find out potential dead codes from diffもその一環です。

以下ではお台場プロジェクトを進めるにあたって取り組んだ cookpad_all 関連メトリクスの計測について紹介します。

cookpad_all 関連メトリクスの計測

cookpad_all の開発における困難を改善するといってもどう改善されているのか記録し、可視化しなければなにもわかりません。

そこでお台場プロジェクト開始初期にまず cookpad_all に関するメトリクスを計測し、社内で稼働させている InfluxDB に記録し、Grafana でダッシュボードを作成しメトリクスを可視化できている状態を作りました。

f:id:hogelog:20180607161915p:plain

具体的には現在 cookpad_all では以下のようなメトリクスを可視化し、改善を進めながら経過を観測し続けています。

  • CI Duration
  • App Load Time (development / production)
  • Loaded File Count
  • Code Statistics
  • GemCollector Up-to-date Point
  • Dependent Gem Count

CI Duration

これは Jenkins で実行している CI にかかった時間の計測です。気をつけることとしては失敗した時の実行時間は不安定になることが多いので、成功した時の時間のみ記録していることです。上記の図で示すように 2017/7 〜 2018/6 現在に至るまで、長いものでは 10 分程かかっていたものが 7分程度まで実行時間が削減されています。

App Load Time

開発者が手元で bin/rails sした時にアプリケーションが動き出すまでの遅さはわかりやすく辛い箇所です。cookpad_all の各アプリケーションでは定期的に以下のようなスクリプトを実行しアプリケーションのロードにかかった時間を計測しています。

defprofile_app_load_timeBenchmark.measure do
    system("./bin/rails r '1;'") orraise"error"endend# Warming disk cache, ...
puts profile_app_load_time

3.times do
  result = profile_app_load_time
  puts result
  influx.write_point("cookpad_ci_app_load_time", tags: { app: app }, values: { load_time: result.real })
end

Loaded File Count

これはアプリケーションのロードが終わった時点での $LOADED_FEATURESの数です。この数字は依存 gem の追加や削除、大規模なコード削除などで大きく数字が動き、アプリケーションになにか大きな変更があったことの観測に役立っています。

Code Statistics

bundle exec rake stats*2の数字を記録するものです。この数字も時々誰かがどこか外部で「クックパッドの巨大 Rails アプリケーション」の発表をする時に計測する程度で、定点観測はおこなわれていませんでした。

f:id:hogelog:20180607172035p:plain

大きなシステム分割などにより時々グッと下がっている以外にも、日常的なコード掃除などで地道ながらもコード削減が進んでいることがダッシュボードを見るだけでわかるようになりました。

Dependent Gem Count

依存している gem の数の記録です。数が増えれば増えるほど gem の依存関係が深くなり、新規 gem の導入や既存 gem の更新などが難しくなっていきます。

f:id:hogelog:20180607161805p:plain

このメトリクスは git のログを遡り 2011 年頃からの値を計測してみましたが、依存する gem 数はお台場プロジェクトが始まるまでは増える一方でありアプリケーションを小さくしていこうという開発の流れはほぼ存在していなかったことがわかります。

ちなみに一瞬依存 gem 数 が400個を超えたところがあるのが目を引くかもしれないので説明しておくと、これは aws-sdk を v2 -> v3 にアップグレードし、その後で必要な aws-sdk-* のみに絞るよう修正したためです。

GemCollector Up-to-date Point

これは この gem を使っているアプリケーションを探す - クックパッド開発者ブログで紹介した GemCollector で出している gem の最新度を記録しているものです。

f:id:hogelog:20180607161828p:plain

この値は相対的なものであるため、gem のバージョンアップに追従していかないとどんどんポイントが下がっていきます。対応をおこたっていくといどんどんアプリケーションがレガシーになっていく状況を把握するのに非常に便利なグラフになっています。

まとめ

クックパッドでは現在巨大モノリシック Rails アプリケーションに頼った開発から Microservices 構成のアプリケーション群を組み合わせて使ったサービス開発への急速な移行段階にあります。その中で最後に残されている巨大 Rails アプリケーションを改善していくためのメトリクス収集と可視化ダッシュボードについて紹介しました。

私達はそういうことを一緒にやっていく仲間をもっともっと求めています。定型文じゃなくて本当に求めています。採用への応募またはどんな会社なのか聞くために遊びに来たいみたいなお声がけ、お待ちしております。

*1:どんな技術を用いていたか詳しくは Ruby on Ales 2015 で @amatsudaが発表した The Recipe for the World's Largest Rails Monolithなどで詳しく説明されています

*2:実際にはちょっと特殊なディレクトリ構成に対応するため cookpad:stats という独自タスクを定義しています

Service Mesh and Cookpad

$
0
0

This ariticle is a translation of the original article which was published at the beginning of May. To make up for the backgroud of this article, Cookpad is mid-size technology company having 200+ product developes, 10+ teams, 90 million monthly average users. https://info.cookpad.com/en


Hello, this is Taiki from developer productivity team. For this time, I would like to introduce about the knowledge obtained by building and using a service mesh at Cookpad.

For the service mesh itself, I think that you will have full experience with the following articles, announcements and tutorials:

Our goals

We introduced a service mesh mainly to solve operational problems such as troubleshooting, capacity planning, and keeping system reliability. In particular:

  • Reduction of management cost of services
  • Improvement of Observability*1*2
  • Building a better fault isolation mechanism

As for the first one, there was a problem that it became difficult to grasp as to which service and which service was communicating, where the failure of a certain service propagated, as the scale expanded. I think that this problem should be solved by centrally managing information on where and where they are connected.

For the second one, we further digged the first one, which was a problem that we do not know the status of communication between one service and another service easily. For example, RPS, response time, number of success / failure status, timeout, status of circuit breaker, etc. In the case where two or more services refer to a certain backend service, resolution of metrics from the proxy or load balancer of the backend service was insufficient because they were not tagged by request origin services.

For the third one, it was an issue that "fault isolation configuration has not been successfully set". At that time, using the library in each application, setting of timeout, retry, circuit breaker were done. But to know what kind of setting, it is necessary to see application code separately. There is no listing and situation grasp and it was difficult to improve those settings continuously. Also, because the settings related to Fault Isolation should be improved continuously, it was better to be testable, and we wanted such a platform.

In order to solve more advanced problems, we also construct functions such as gRPC infrastructure construction, delegation of processing around distribution tracing, diversification of deployment method by traffic control, authentication authorization gateway, etc. in scope. This area will be discussed later.

Current status

The service mesh in the Cookpad uses Envoy as the data-plane and created our own control-plane. Although we initially considered installing Istio which is already implemented as a service mesh, nearly all applications in the Cookpad are operating on a container management service called AWS ECS, so the merit of cooperation with Kubernetes is limited. In consideration of what we wanted to realize and the complexity of Istio's software itself, we chose the path of our own control-plane which can be started small.

The control-plane part of the service mesh implemented this time consists of several components. I will explain the roles and action flow of each component:

  • A repository that centrally manages the configuration of the service mesh.
  • Using the gem named kumonos, the Envoy xDS API response JSON is generated
  • Place the generated response JSON on Amazon S3 and use it as an xDS API from Envoy

The reason why the setting is managed in the central repository is that,

  • we'd like to keep track of change history with reason and keep track of it later
  • we would like to be able to review changes in settings across organizations such as SRE team

Regarding load balancing, initally, I designed it by Internal ELB, but the infrastructure for gRPC application went also in the the requirement *3, we've prepared client-side load balancing by using SDS (Service Discovery Service) API *4. We are deploying a side-car container in the ECS task that performs health check for app container and registers connection destination information in SDS API.

f:id:aladhi:20180501141121p:plain

The configuration around the metrics is as follows:

  • Store all metrics to Prometheus
  • Send tagged metrics to statsd_exporter running on the ECS container host instance using dog_statsd sink*5
  • All metrics include application id via fixed-string tags to identify each node*6
  • Prometheus pulls metris using EC2 SD
  • To manage port for Prometheus, we use exporter_proxy between statsd_exporter and Prometheus
  • Vizualize metrics with Grafana and Vizceral

In case the application process runs directly on the EC2 instance without using ECS or Docker, the Envoy process is running as a daemon directly in the instance, but the architecture is almost the same. There is a reason for not setting pull directly from Prometheus to Envoy, because we still can not extract histogram metrics from Envoy's Prometheus compatible endpoint*7. As this will be improved in the future, we plan to eliminate stasd_exporter at that time.

f:id:aladhi:20180502132413p:plain

On Grafana, dashboards and Envoy's entire dashboard are prepared for each service, such as upstream RPS and timeout occurrence. We will also prepare a dashboard of the service x service dimension.

Per service dashboard:

f:id:aladhi:20180501175232p:plain

For example, circuit breaker related metrics when the upstream is down:

f:id:aladhi:20180502144146p:plain

Dashboard for envoys:

f:id:aladhi:20180501175222p:plain

The service configuration is visualized using Vizceral developed by Netflix. For implementation, we developed fork of promviz and promviz-front*8. As we are introducing it only for some services yet, the number of nodes currently displayed is small, but we provide the following dashboards.

Service configuration diagram for each region, RPS, error rate:

f:id:aladhi:20180501175213p:plain

Downstream / upstream of a specific service:

f:id:aladhi:20180501175217p:plain

As a subsystem of the service mesh, we deploy a gateway for accessing the gRPC server application in the staging environment from the developer machine in our offices*9. It is constructed by combining SDS API and Envoy with software that manages internal application called hako-console.

  • Gateway app (Envoy) sends xDS API request to gateway controller
  • The Gateway controller obtains the list of gRPC applications in the staging environment from hako-console and returns the Route Discovery Service / Cluster Discovery Service API response based on it
  • The Gateway app gets the actual connection destination from the SDS API based on the response
  • From the hand of the developer, the AWS ELB Network Load Balancer is referred to and the gateway app performs routing

f:id:aladhi:20180502132905p:plain

Results

The most remarkable in the introduction of service mesh was that it was able to suppress the influence of temporary disability. There are multiple cooperation parts between services with many traffic, and up to now, 200+ network-related trivial errors*10 have been constantly occurring in an hour*11, it decreased to about whether it could come out in one week or not with the proper retry setting by the service mesh.

Various metrics have come to be seen from the viewpoint of monitoring, but since we are introducing it only for some services and we have not reached full-scale utilization due to the introduction day, we expect to use it in the future. In terms of management, it became very easy to understand our system when the connection between services became visible, so we would like to prevent overlooking and missing consideration by introducing it to all services.

Future plan

Migrate to v2 API, transition to Istio

The xDS API has been using v1 because of its initial design situation and the requirement to use S3 as a delivery back end, but since the v1 API is deprecated, we plan to move this to v2. At the same time we are considering moving control-plane to Istio. Also, if we are going to make our own control-plane, we plane to build LDS/RDS/CDS/EDS API*12 using go-control-plane.

Replacing Reverse proxy

Up to now, Cookpad uses NGINX as reverse proxy, but considering replacing reverse proxy and edge proxy from NGINX to Envoy considering the difference in knowledge of internal implementation, gRPC correspondence, and acquisition metrics.

Traffic Control

As we move to client-side load balancing and replace reverse proxy, we will be able to freely change traffic by operating Envoy, so we will be able to realize canary deployment, traffic shifting and request shadowing.

Fault injection

It is a mechanism that deliberately injects delays and failures in a properly managed environment and tests whether the actual service group works properly. Envoy has various functions *13.

Perform distributed tracing on the data-plane layer

In Cookpad, AWS X-Ray is used as a distributed tracing system*14. Currently we implement the distributed tracing function as a library, but we are planning to move this to data-plane and realize it at the service mesh layer.

Authentication Authorization Gateway

This is to authenticate and authorize processing only at the front-most server receiving user's request, and the subsequent servers will use the results around. Previously, it was incompletely implemented as a library, but by shifting to data-plane, we can recieve the advantages of out of process model.

Wrapping up

We have introduced the current state and future plan of service mesh in Cookpad. Many functions can be easily realized already, and as more things can be done by the layer of service mesh in the future, it is highly recommended for every microservices system.

*1:https://blog.twitter.com/engineering/en_us/a/2013/observability-at-twitter.html

*2:https://medium.com/@copyconstruct/monitoring-and-observability-8417d1952e1c

*3:Our gRPC applications already use this mechanism in a production environment

*4:Server-side load balancing which simply use Internal ELB (NLB or TCP mode CLB) has disadvantages in terms of performance due to unbalanced balancing and also it is not enough in terms of metrics that can be obtained

*5:https://www.envoyproxy.io/docs/envoy/v1.6.0/api-v2/config/metrics/v2/stats.proto#config-metrics-v2-dogstatsdsink . At first I implemented it as our-own extension, but later I sent a patch: https://github.com/envoyproxy/envoy/pull/2158

*6:This is another our work: https://github.com/envoyproxy/envoy/pull/2357

*7:https://github.com/envoyproxy/envoy/issues /1947

*8:For the convenience of delivering with NGINX and conforming to the service composition in the Cookpad

*9:Assuming access using client-side load balancing, we need a component to solve it.

*10:It's very small number comparing to the traffic.

*11:Retry is set up in some partes though.

*12:https://github.com/envoyproxy/data-plane-api/blob/5ea10b04a950260e1af0572aa244846b6599a38f/API_OVERVIEW.md#apis

*13:https://www.envoyproxy.io/docs/envoy/v1.6.0/configuration/http_filters/fault_filter.html

*14:http://techlife.cookpad.com/entry/2017/09/06/115710

Header Bidding 導入によるネットワーク広告改善の開発事情

$
0
0

こんにちは。メディアプロダクト開発部の我妻謙樹(id:kenju)です。 サーバーサイドエンジニアとして、広告配信システムの開発・運用を担当しています。

cookpad における広告開発

2015年11月に、"クックパッドの広告エンジニアは何をやっているのか"というタイトルで、広告開発部の開発内容について紹介する記事が公開されていますが、それから 2 年余り経過し、広告配信システム周りの状況も大きく変化しました。はじめに、現在の cookpad における広告開発の概要について、軽くご紹介します。

まず、私が所属しているメディアプロダクト開発部では、広告配信システムに加え、動画配信サービスの開発も担当しています。過去には同じチームから、動画配信周りの技術について以下のような投稿もありますので、そちらもご覧ください。

広告配信システムの開発で担当しているサービスの一覧は、以下の通りです:

  • 広告配信サーバーの開発 (Rails)
  • 社内向け広告入稿システムの開発 (Rails)
  • 広告ログ基盤の運用(Python, Kinesis Streams, DynamoDB, Lambda)
  • 広告配信用 SDK の開発(各プラットフォームに準拠。WEB 向けは JavaScript)

プロジェクト別で言えば、以下がスコープです:

  • 既存自社広告商品の改善
  • 新広告商品の新規開発
  • ネットワーク広告商品の開発や改善

例えば、昨年末から今年はじめにかけて、とある新広告商品の開発に携わっていたのですが、その時のプロジェクトの一部についてスライドが公開されているので、そちらもご覧ください。

今回は、その中でも「ネットワーク広告商品の開発や改善」における一プロジェクトについてご紹介します。

背景

cookpad では、以下の 2 種類の広告を配信しています。

  • 自社広告 1
  • ネットワーク広告 2

この内、ネットワーク広告においては、 Supply Side Platform (以下、SSP)各社と連携して複数のアドサーバー経由で広告を配信しているのですが、それらの広告は頭打ちになってきています。そのため、広告の収益改善に取り組む必要がありました。

安易に広告枠を増やすことはユーザー体験の低下やネットワーク負荷増加に繋がるため、避けなければなりません。したがって、現在配信されている広告の買付け額や配信フローを改善させる必要があります。

そこで、近年日本でも導入が進んできている Header Bidding と呼ばれる仕組みを導入することになりました。

About Header Bidding

Header Bidding とは、アドサーバーに広告のリクエストをする前に、SSP 各社に広告枠の最適な額を入札します。

仕組み的には、<head>タグ内(= Header)で事前に入札リクエスト(= Bidding)を行うことから、"Header Bidding"と呼ばれています。

Without Header Bidding

例えば、Header Bidding を経由しない、従来のパターンでの広告枠買付け方式を見てみましょう。ここで図の用語の定義は以下の通りとします:

  • Client ... 広告を表示させる側ここでは cookpad の本体サイト
  • Ad Server ... アドサーバー
  • SSP ... SSP各社
  • Floor ... フロアプライス 3
  • Bid ... 入札結果 4
  • Winning bid ... 買付けに成功した入札価格

f:id:itiskj:20180613203016p:plain

既存のアドサーバーの入札ロジックは、基本的にウォーターフォール方式(5)で買付けが行われます。したがって、上記図のケースの場合、

  • 広告枠に対してフロアプライスが $1.0
  • SSP α の入札結果が $0.8(フロアプライス以下)
  • SSP β の入札結果が $1.2(フロアプライス以上)

上から順番に問い合わせていった結果、最初にフロアプライス以上の価格で入札してきた「SSP β」の広告が表示されることになります。

ここで「SSP δ」の入札結果が、$2.0 であることに着目してください。もし、「SSP δ」入札結果を反映できていたら、その広告枠の価値は $2.0 になります。つまり、広告枠本来の価値は $2.0といえます。しかし、ウォーターフォール形式の仕組み上の制約によって、$1.2 の入札結果が反映されてしまいました(差し引き $0.8 の機会損失ですね)。

これを解決するのが、Header Bidding です。

With Header Bidding

次に、入札サーバを挟む場合で見ていきましょう。なお、この場合は後述する Server-to-Server 方式で説明していきます。

f:id:itiskj:20180613203037p:plain

この場合、

  1. Client -> Bid Server ...Header Bidding を入札サーバにリクエスト
  2. Bid Server -> SSP ... 入札。このとき、各社への入札は 同じタイミングで入札される
  3. Bid Server -> Client ... 入札結果を返す。ここでは、「SSP δ」が $2.0 で買付けをすレスポンスを返してきたので、Winning bid が $2.0 になる
  4. Client -> Ad Server ... 入札結果が返ってきてから広告をリクエストする。このとき、Winnig bid を伝えることで、「SSP δ」広告が返却されることになる 6

という順番で処理が実行されます。Header Bidding を利用しないケースと違って、一番買付け額が高い SSP の入札結果が反映されたことがわかります。

ポイントは、

  • アドサーバーにリクエストする前に入札を行うこと
  • SSP 各社へのリクエストが並行に行われること

です。これによって、本来失われてしまっていた入札価格を最適化することができます。

Client vs S2S Header Bidding

Header Bidding には

  • Client Header Bidding
  • Server-to-Server Header Bidding

の2種類の方式が存在します。

Client Header Biddingは、クライアント側で入札を行う形式です。技術的には、<script>タグ内で、入札先の SSP 一覧を指定して、それぞれに入札リクエストを行います。それらの入札リクエストの結果を待って、一番 eCPM の優れた入札結果を選択する形式です。

Server-to-Server Header Bidding は、サーバー側で入札を行う形式です。Client Header Bidding との違いは、入札サーバーに 1 回だけリクエストを送信すれば良い点です。また、入札ロジック(例:SSP 各社からの入札結果の待ち合わせ、タイムアウト処理、入札結果の比較)を入札サーバーが担ってくれるので、クライアント側の責務が大幅に削減されることです。

現在は、Server-to-Server 方式が主流です。

Header Bidding Services

なお、Transparent Ad Marketplace(以下、TAM)という、Amazon が提供する Header Bidding の広告サービスを採用しています。

設計・実装

基本的には、TAMの提供するドキュメントに沿って、<head>タグ入札サーバーにリクエストするスクリプトを埋め込めば、導入は完了します。

しかし、弊社の場合

  • 独自の広告入稿・配信サーバーを介して自社広告・ネットワーク広告すべての広告を配信している
  • しかも、ページごとに配信される広告スロット(7)は静的ではなく動的に変化する

といった制約のため、スムーズな導入ができず改修が必要でした。

以下の図が、Header Bidding を行うまでの大幅な流れです。ここで、

  • ads ... 社内広告配信サーバ
  • display.js ... 広告表示用の JavaScript SDK
  • cookpad_ads-ruby ... display.js を埋め込むための Rails 用ヘルパーを定義した簡易な gem
  • apstag ... TAM の提供する Header Bidding 用ライブラリ
  • googletag ... DFP の提供するアドネットワーク用ライブラリ

だとします。なお、googletag の公式ドキュメントは、https://developers.google.com/doubleclick-gpt/referenceからご覧になれます。

f:id:itiskj:20180613203104p:plain

Header Bidding を実行するまでの大まかな流れは、以下の通りです:

  1. JavaScript SDK が、広告配信サーバーから表示すべき広告リクエストする
  2. 広告にネットワーク広告が含まれている場合、Header Biddingの一連の処理を開始する
  3. まずは、apstag, googletag それぞれの初期化を行う(例:デフォルトのタイムアウト設定)
  4. apstag を用いて TAM に Header Bidding リクエストを送る
  5. 入札結果をもとに、DFP に広告リクエストを送る
  6. DFP から広告リクエストが返却されたら、広告を表示する

ポイントは、Header Bidding をリクエストしている間、

  • googletag.pubads().disableInitialLoad()で DFP リクエストを中断し、Header Bidding を行う
  • 入札結果が返ってきたら、googletag.pubads().refresh([opt_slots...])で広告のレンダリングフローを再開する

という点です。

結果

以上を持って、Header Bidding を導入するまでの一連の流れを説明してきました。具体的な数字はここでは伏せますが、今回の導入によってネットワーク広告の収益改善を実現することができました。

新たな課題

広告レンダリングフローのパフォーマンス悪化

しかし、ここで新たな課題も発生してしまいました。

それは、Header Bidding リクエストの分、ネットワーク広告が表示されるまでのレイテンシが増加してしまった、という点です。

広告が表示されるまでの一連のレンダリングプロセスを、以下の図に示しました。

Processing, DOMContentLoaded, loadは、ブラウザが HTML/CSS をパースしてレンダリングするまで一連のフローの一般的用語です。気になる方は、Ilya Grigorik(8) による"Measuring the Critical Rendering Path"をご覧ください)

f:id:itiskj:20180613203121p:plain

ぱっと見て気づくのは、社内広告配信サーバの ads へのリクエストから、Header Bidding 、そして DFPへのリクエストまですべてがシリアルに実行されていることです。今回 Header Bidding を導入したことによって、その分レイテンシが増加したのです、大体 150 ~ 400 (ms) と、かなり致命的なパフォーマンス低下になってしまいました。

広告レンダリングフローの可視化がされていない

筆者の肌感で「150 ~ 400ms」と説明しましたが、実はクライアント環境で実行されるまでの広告レンダリングフローは、今まで計測・可視化されていませんでした。

上記で挙げた広告レンダリングフローのパフォーマンスを改善したいものの、ボトルネックが正確にはどこになるのか、計測するまでわかりません。計測できないものは改善できないと言われるように、まずは計測・可視化のフローを導入しました。

ここで幸いなことに、Fluentd にログを流し、分析可能なデータウェアハウス(cookpad の場合は、現状 Redshift)にテーブルを構築するまでの仕組みはすでに存在していました。したがってクライアント側と多少のテーブル定義を書くだけで実現できました。

(補足:cookpad におけるデータ活用基盤については、"クックパッドのデータ活用基盤"をご覧ください。)

以下は、取得したログデータをもとに可視化してみた様子です。ログを先日から取得し始めたばかりなので、可視化のフローはまだ未着手です。社内で推奨されている BI ツールにダッシュボードを作り、定点観測できるところが直近のゴールです。

f:id:itiskj:20180613203133p:plain

広告レンダリングのクリティカルパスは任意のタイミングでロガーを仕込むだけですが、DFP の場合、googletag.events.SlotRenderEndedEventを利用すると、広告枠が表示されたタイミング、広告が "Viewable"(9)になったタイミングでイベントを取得できます。

対策と今後の展望

以上が、Header Bidding の導入から、新たに浮上した課題への対策の説明でした。直近ですと、以下に取り組んでいく予定です。

  • 広告レンダリングフローの可視化フェーズ
  • 広告レンダリングフローの最適化

広告レンダリングフローの最適化

「広告レンダリングフローの最適化」では、自社の広告配信サーバーへのリクエストのタイミングを、今より前倒しにする方針で設計及び PoC の実装を行っている段階です。

具体的に言うと、現在 HTML ファイルの <body>下部で広告レンダリングフローを開始しているのですが、それを <head>タグの可能な限り早い段階で開始するように改善をする必要があります(過去の設計の都合上、広告配信サーバーへのリクエストは、各広告スロットの HTMLElement 要素が レンダーツリー10に挿入され、実際に描画されるタイミングにブロックされている)。

f:id:itiskj:20180614112206p:plain

パフォーマンスの可視化及びレンダリングフローの最適化についても、また別の機会にご紹介したいと思います。

まとめ

アドテク関連のエンジニア目線での事例紹介や技術詳解はあまり事例が少ないため、この場で紹介させていただきました。技術的にチャレンジングな課題も多く、非常に面白い領域です。ぜひ、興味を持っていただけたら、Twitterなどからご連絡ください。

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


  1. 自社広告 … 自社独自の営業チームが、直接広告主と契約を結び配信している広告。自社で配信されるクリエイティブを運用できるため、意図しない広告が配信されることがない。

  2. ネットワーク広告 … 他社の広告配信会社が提供している広告配信サーバーを経由して、広告の買付け・配信を行う広告。各社が提供する <script>タグを HTML に埋め込み、返却された広告を <iframe>にレンダリングする形が一般的。

  3. フロアプライス … 最低落札価格のこと。例えば、「フロアプライス $0.8」広告をリクエストしたとき、$0.8 以下の広告枠の買付けは行わない。

  4. 入札結果 …SSP 各社が、広告枠をいくらで買い付けるかを示す価格。これがフロアプライスより低い場合、広告枠に広告が表示されることはない。

  5. ウォーターフォール方式 … SSP 各社定義した順番で一つ一入札していく方式。「滝」語義が表すとおり、上から順に問い合わせ得ていく様子からこの名前で呼ばれる。

  6. アドサーバー側にどのように入札結果を伝えるかは、各アドサーバー側の仕様や実装に依存する。例えば TAM が DFP に対して Header Bidding を行う場合、Key/Value Targetingの仕組みを使っている。

  7. 広告スロット … 広告枠が表示される枠のこと。

  8. Ilya Grigorik … Google のエンジニアで、“High Performance Browser Networking”の著者、と言えばわかる方も多いかもしれません。完全に蛇足ですが、尊敬しているエンジニアの1人で、彼の著作をきっかけ Web の裏側に興味を持ちました。

  9. Viewable Impression … 広告が「ユーザーに見える状態」になったかどうかでインプレッションを測定している。詳細については、“Viewabiliity and Action View”を参考のこと。

  10. Render Tree … https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction

無理をしないコードレビュー

$
0
0

会員事業部の三吉です。 クックパッドでは、GitHub Enterprise の Pull Request を使ったコードレビューを広く実施しています。 この記事では、私がコードレビューすることに対する苦手意識をなくすために意識したことを紹介します。

クックパッドでは、テックリードや新卒、インターン、バイトといった肩書きに関係なく、誰もがレビュワー・レビュイーになります。 チームやプロダクトによって開発ルールは少しずつ異なりますが、私の所属する会員事業部では、PR を出したときに GHE やチャットで部内のエンジニアにメンションして、その時にレビューできる人がレビューするという形を取っています。

私は、昨年2017年に新卒入社したのですが、それまでは個人開発や研究用のコードしか書いたことがなく、短期インターンシップを除くチーム開発の経験がありませんでした。 配属当初からコードレビューすることは求められていのですが、はじめの数ヶ月間はレビューすることに対して苦手意識があり、なかなか積極的にレビューに参加することができませんでした。

コードレビューの難しさ

コードレビューに対する苦手意識は、自分のレビュー内容に自信が持てないことによるものでした。 コードレビューは、真剣に取り組もうとすると非常に難しい作業です。 以下、「見るべき項目の多さ」「文脈によるレビュー内容の変化」「開発速度とのトレードオフ」の3点からコードレビューの難しさを見ていきます。

見るべき項目の多さ

コードレビューでチェックするべき点は非常に多いです。 以下に、ざっくりとレビューの観点をカテゴライズしました(順序に深い意味はありません)。

  • 挙動(意図どおり動作するか)
  • バグ
  • セキュリティ
  • 可読性
  • テスト
  • パフォーマンス
  • ドキュメンテーション
  • 設計 *1
  • ……などなど

これらは、それだけで分厚い本が書けるようなカテゴリであり、それぞれについて一般的な原則や社内のルールがたくさんあります。 ちょっとした変更であればともかく、毎回のコードレビューでそれらを厳密に網羅することは不可能で、どこかで妥協する必要があります。

文脈によるレビュー内容の変化

では、どこで妥協するのかというと、それはレビュー対象のコードの文脈に依存します。

極端な例を挙げると、ユーザーテスト用のプロトタイプと、決済周りのデータ処理を行うジョブとでは、どのくらい細かくレビューするべきか変わってきます。 前者は、あくまでプロトタイプであって、テストするのに問題なければ最悪バグが含まれていても構いません。 それに対し、後者にバグが含まれていると、深刻なデータ不整合が起きたり、ユーザーに不利益が出たりすることになります。 後者をレビューするときには、テストの内容やエンバグの可能性の丁寧なチェックが必要です。

他にも、変更頻度の高い箇所であればメンテナビリティを考慮したり、検索などリクエストの多い部分であればパフォーマンスを意識したりと、コードの位置する文脈に依存して、重点を置くべき観点が変わってきます。

開発速度とのトレードオフ

レビュー時どこに重点を置くべきかは、文脈だけでなく、レビューに割ける時間によっても変わります。

コードレビューは思いのほかコストのかかる作業です。 レビューしているあいだレビュワーは他の作業をすることができず、また、レビュイーにも指摘箇所の修正だけでなく、レビューがつくたびに発生するコンテキストスイッチの負担があります。

しかし、コードレビューにはそのコストをかけるだけの価値があります。 私たちがコードを書く目的は、ユーザーに価値を届けることであり、コードレビューの目的も変わりません。 コードレビューは、届ける価値の品質を担保するために必要な作業です。

とはいえ、サービス開発においては、その価値をすばやく届けることも非常に重要です。 レビューに不必要なまでに時間をかけて、高速な開発サイクルを回せなくなっては本末転倒です。 品質と速度とのトレードオフから、レビューにどの程度コストをかけるべきか考える必要があります。

コードレビューをするときに意識していること

以上にみてきたように、コードレビューは、無数の項目について、コードが置かれた文脈から優先順位をつけ、開発速度と品質を最大化するような時間でチェックしていく、というとても困難な作業です。

……と、これが理想のコードレビューかもしれませんが、人間が意識的にできるものではありません。 重要なのは、そういった難しさがあることを知った上で、できる範囲でやることです。 とはいえ、コードレビューに慣れない頃は、できる範囲でやったレビューには多くのヌケモレがあるように思えて不安になったり、しっかりレビューしたときには時間をかけ過ぎではないかと不安になったりしていました。 ここからは、そうした不安や、それに起因する苦手意識をなくすために私が行っている工夫を紹介します。

レビューした範囲を明示する

自分のレビューが不十分に感じられたときは、何を見たか、あるいは、何を見られていないかを Review summary などに書くようにしています。

f:id:sankichi92:20180618184907p:plain

こうすることで、他のレビュワーに重点を置いて見てほしい場所を伝えています。 広く薄く見ただけであれば「ざっと見ました」というコメントでも構いません。

また、自信のないときは、素直に他のレビュワーに依頼します。

f:id:sankichi92:20180618184918p:plain

重要な定数などについて、仕様と照らし合わせて正しいことを指差し確認するのも、ヌケモレを防ぐのに大事です。

f:id:sankichi92:20180618184933p:plain

わからなかったら質問する

もちろん、他のレビュワーが現れることを期待できない場合は、ひとりで全範囲をレビューする必要があります。 そういう時に自信を持てない箇所、わからない箇所を見つけたら、わかった気になるまで読むのではなく、質問することが重要です。

次のように、質問に答えることで、実装者がミスや考慮漏れに気づくということも少なくありません。

f:id:sankichi92:20180618184946p:plain

コードから汲み取ったことを言語化して、その認識であっているか確認するだけでも効果があります。 込み入った内容であれば、実装者と一緒にペアレビューするという方法も有効です。 ペアレビューで理解した内容を PR にコメントするとなお良いです。

コードレビューには、問題点を見つけるだけではなく、そのコードの理解者を増やして属人性をなくす機能もあります。 GHE に残った質問のログが数年後に再び役立つということも珍しくありません。

nits や IMO, MUST といったラベルを利用する

先にも述べたように、サービス開発ではスピードも重要です。 本質的でない修正に時間をかけるよりも、先にリリースした方が良い場合もあります。

とはいえ、レビューしていると、どうしても細かいところが気になってしまうものです。 そうしたときは、[nits] や [IMO] といったラベルをレビューコメントの先頭につけて、修正の判断を実装者にゆだねます*2

f:id:sankichi92:20180618184957p:plain

逆に、どうしても修正してほしい場合は [MUST] ラベルをつけます。 すぐに修正できない問題であれば、その PR で修正するのではなく、Issue にして後日修正するのも有効です。

おわりに

以上が、私の意識している「無理をしないコードレビュー」です。 レビューすることに慣れて、日常化すれば特に意識する必要はなくなります。 しかし、1年前の私にとってそれはとても難しいことだったので、当時の私に伝えるつもりでコードレビューするときのコツを書いてみました。

コードレビューする機会が増えて感じるのは、レビュワー側も非常に勉強になるということです。 レビューする時の視点は、単にコードリーディングする時の視点とは違います。 エンバグはないか、テストは必要十分か、などなど普段以上に神経をとがらせて見ることになります。 そして、その視点はそのままコードを書く時にも活かすことができます。

また、今回はコードレビュー「する」ときの工夫に焦点を当てました*3が、「される」ときの工夫も重要です*4。 他にも、クックパッドでは、コードレビューのコストを下げるための自動化等の取り組みも行っています*5

コードレビューのスタイルは、組織や開発するプロダクトの性質によって変わってくるものだと思いますが、この記事が少しでも参考になれば幸いです。

*1:大きな変更であれば事前に設計レビューを行います。

*2:リポジトリにもよりますが、クックパッドでは実装者がマージを行うことが多いです。

*3:コードレビューすることに関する記事として、他にも たのしくなるコードレビューがあります。

*4:開発速度を上げるための Pull-Request のつくり方など。

*5:Android開発のコードレビューbotを乗り換えた話など。

RubyKaigi 2018 ありがとうございました!

$
0
0

人事部の@mamiracle__です。好きな Ruby のメソッドは Enumerable#entriesです。みなさまからの entriesをお待ちしています💖

さて、クックパッドは先日の RubyKaigi 2018に Ruby Committers Sponsor と Network Sponsor としてスポンサーをいたしました。私たち人事メンバーも、ブースやドリンクアップを通じて、RubyKaigiを盛り上げることに貢献できたのではないかとおもっています。

会期中には、クックパッドに所属する桑原仁雄(@pocke)、Kirk Haines(@wyhaines)、 笹田耕一(@ko1)、遠藤侑介(@mame)がスピーカーとして登壇し、심 상용(@riseshia)がライトニングトークを行なっています。また、RubyKaigi運営では、オーガナイザーとして @nano041214@asonas@sorahが、スタッフとして @mozamimyが活躍してくれました。

イベント参加のご報告として、当社所属のメンバーの発表やブースの紹介をいたします!!!

発表

A parser based syntax highlighter

桑原仁雄(@pocke)からはパーサーベースのシンタックスハイライターであるIroに関して発表がありました。通常のシンタックスハイライターで利用されている正規表現では、複雑なプログラム表記でうまく機能しなくなってしまいがちですが、パーサーベースであればそういった問題が生じないそうです。また、gemとして提供されているので、Vimだけではなく色々なエディタでも活用できるとのことです。

また、RubyKaigi効果で意識が高まった本人からも力強い宣言がでておりますので、ぜひ今後にご期待ください!

It's Rubies All The Way Down

自身が2001年以来の Rubyist であると紹介した Kirk Haines(@wyhaines)からは、主に Web アプリケーションを動かすときのテクノロジスタックについて、それぞれの時代における Ruby で作られたソフトウェアの紹介や、概念実証(Proof of Concept)が示されたりしました!

特に Web アプリケーションにおいては Rack の登場がキーポイントになり、ミドルウェアとWeb サーバーの関係がそれ以前と比べてシンプルになったという説明は納得できました。また、10年以上前の Ruby では、ロガーやロードバランサーなどをやるには遅すぎると言われることがありましたが、近年の Ruby では十分なパフォーマンスが確保できることも示され、Ruby の進化がよくわかりました。

Guild Prototype

Ruby の新しい並行プログラミングモデルである Guild について、笹田耕一(@ko1)から発表がありました。冒頭では、シングルプロセスで合計40コアのCPUを使いきるデモがされていて、未来感ありましたね。

従来の並行プログラミングでは、データを共有するために競合状態をプログラマが頑張るか、データを共有しないという手法が主にとられてきました。Guild では、メンバーシップという概念を導入することで、容易にデータを共有しつつも、マルチコアを活用できるようにしています。Rubyで実用段階になるのが、今から楽しみです!

Type Profiler: An analysis to guess type signatures

遠藤侑介(@mame)の発表では、提案されている複数のRubyの型システムを概観したあと、Ruby 3に必要となってくる型データベースのために、型プロファイラーの導入が提案されました。型プロファイラーは、いくつかの手法で型を推測するための機構です。静的解析や動的解析をつかって、いい感じに型を推測できないかという試みです。それぞれの手法にメリットやデメリットがあって、まだまだ難しいところも多いようですが、 Ruby の改善が着実に進んでいる様子が伝わってきました。

また、ブースの質疑応答タイムでは、海外のファンからチョコレートのプレゼントをもらうなど、大人気ぶりを発揮していました!

ライトニングトーク

Find Out Potential Dead Codes from Diff

Cookpad のような長期でメンテナンスされているコードでは、どうしてもどこからも呼び出されないデッドコードが生まれてしまいます。심 상용(@riseshia)のライトニングトークでは、未使用コードの差分からデッドコードを検出する手法について発表がありました。クックパッドでも実際に使われて効果がある手法とのことです。

ブースやグッズ

クックパッドが海外展開している国を示した世界地図や、「Cookpad storeTV」、ユーザーボイスやミッションなどを詰め込んだオリジナルボードなどを展示しました。

f:id:cookpadtech:20180614152425j:plain(地図にはクックパッドが展開している68カ国を⭐シールで示しました)

ノベルティには今年初めて作った「ロゴ入りお箸」や、仙台をイメージした「ずんだ餅どら焼き」を用意しました。どら焼きは、仙台の老舗和菓子屋さん「こだまのどら焼き」さんに相談をして作っていただきました。控えめな甘さのずんだ餡と、もちもちの食感が美味しくてあっという間に完売してしまいました!

【RubyKaigi 2018のスポンサーで仙台にきています♦️】 クックパッドは毎年、Rubyというプログラミング言語のカンファレンス #RubyKaigi に協賛しています🤗 2018年は5/31〜6/1まで #仙台 での開催なので、 #ずんだ餅どら焼き を作ってみました❤️ 仙台の老舗 #こだま さんのどら焼きです! クックパッドでは、ユーザーさんにより速く良い価値を届けるために、技術もすごく大事にしているんですよ! 今後はそういったこともご紹介できればと思います🎶 #クックパッド #テクノロジーカンパニー #どら焼き #ずんだ餅 #仙台名物 #こだま #cookpadorayaki #クックパッどら焼き #rubykaigi #rubykaigi2018 #cookpad #makeeverydaycookingfun #毎日の料理をたのしみにする #♦️ #💎 #🖥 #🥞 #🤗

クックパッドHRさん(@cookpad_hr)がシェアした投稿 -

その他ブースでは、クックパッドでRubyフルタイムコミッターとして活躍する遠藤侑介(@mame)と、仙台のずんだ豆にちなんで「mameさんの豆つかみ」も実施!CTOの成田一生(@mirakui)をはじめとして、まつもとゆきひろさん(@matz)さんやAaron Pattersonさん(@tenderlove)さんも参加してくださる盛況なイベントとなりました!

f:id:mamiracle:20180614153621j:plain(今回の最速王はクックパッドHRメンバーで叩き出したタイムは0:06:92でした!)

Cookpad X RubyKaigi 2018: Day 2 Party

2日目の夜に開催したパーティには、90名近くのゲストが参加してくださいました!RubyKaigiの会期中にパーティを企画するのは、クックパッドにとってこれが初めてでドキドキしていましたが、多くの方にお楽しみいただけて本当に良い機会となりました!

【Asakusa.rb × Cookpad】Meetup after RubyKaigi 2018

RubyKaigi 2018終了後の翌火曜日には、Asakusa.rbとのコラボレーションイベントを開催しました!Railsコミッターであり、Rubyコミッターの松田明さん(@amatsuda)に司会を務めていただき、RubyKaigi 2018 の余韻を楽しみました。

この日は「Rubyコミッターによる RubyKaigi 2018の見どころ振り返り」「RubyKaigi 2018会期中に決まったRubyの次の方向性について」というふたつのテーマを設定。笹田耕一(@ko1)と遠藤侑介(@mame)を中心に、飲みながら食べながら語り合いながら、会場全体で楽しむことができました。

f:id:mamiracle:20180614153523j:plain(クックパッドイベント恒例のキムラシェフの絶品ご飯)

おわりに:Rubyとクックパッドについて

クックパッドは、2008年にRuby on Rails へとリニューアルしてから、世界的にも大規模な通用事例として知られてきました。Ruby への貢献を現在も強化しており、二人のフルタイムコミッターを迎え入れクックパッドで次世代Ruby の開発に取り組んでいます。

また、技術をわたしたちが使うことはもとより、社外にも共有することで価値を生むようなソフトウェアやライブラリは、積極的にオープンソース化を行い公開しています。

今後もRuby を含む、さまざまなオープンソースソフトウェアの発展に貢献できるよう、クックパッド一同頑張りたいと思います!

次は8月30日から開催されるiOSDC Japan 2018に参加する予定です。みなさまにお会いできることを楽しみにしています😄

f:id:mamiracle:20180618181226j:plain(来年こそはRubyKaigiに参加した社員全員で集合写真撮るぞ!)

クックパッドでRubyを書きたいなと思ったら... - バックエンドエンジニア(料理動画・広告配信) - UXエンジニア - バックエンドエンジニア(決済基盤) - Webアプリケーションエンジニア - セキュリティエンジニア - ソフトウェアエンジニア (Site Reliability)

Viewing all 726 articles
Browse latest View live