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

fastText in Cookpad

$
0
0

研究開発部の原島です。去年からはレシピサービス開発部も兼務しています。そちらの話(検索の話)はおいおいするとして、今日は研究開発部の話(機械学習の話)をします。

fastText

単語の分散表現、重要ですよね。ニューラル全盛期の現代において、使わないという選択肢はほとんどないように思います。

最初に話題になったのは、2013 年に発表された word2vec でしょう。「king」のベクトルから「man」のベクトルを引き、「woman」のベクトルを足したら「queen」のベクトルになったという話は有名です。一方、最近は、2018 年に発表された BERT(及び、それに類するモデル)の話題で持ちきりですね。

fastTextは、ご存知の方も多いと思いますが、分散表現を学習するためのライブラリです。学習のアルゴリズム自体を指すこともあるように思います。fastText の論文は以下です。2017 年に発表されたものなので、発展が速いこの業界においてはもう古い論文なのかもしれません。

  • Enriching Word Vectors with Subword Information. Piotr Bojanowski, Edouard Grave, Armand Joulin, Tomas Mikolov.

なぜ fastText なのか?

クックパッドでは fastText をよく使っています。では、なぜ fastText なのでしょう?上でも触れたように、word2vec や BERT などの選択肢もあります。もちろん、fastText も主要な選択肢の一つではありますが、どうして fastText なのでしょうか?

様々な理由がありますが、まとめると、「性能と運用のバランスがよい」といったところでしょうか。

性能の面では、サブワード(部分文字列)が考慮できる分、word2vec よりは fastText がよいでしょう。一方、文脈を考慮した表現が学習できる分、fastText よりは BERT がよさそうです。もちろん、これらは一般論です。実際にはタスクや学習データによって話が違ってくるでしょう。

一方、運用の面では BERT より fastText や word2vec がよいでしょう。BERT は事前学習が大変です。クックパッドでも何度かトライしていますが、お金も時間もかかります。学習データ、単語分割器、サブワード分割器、whole word masking、マスク確率、...。試行錯誤するだけでもかなりのお金と時間がかかります。

もちろん、ファインチューニングで済ますという手もあります。ありがたいことに、世の中には事前学習済みのモデルが沢山あります。これらを使えば、事前学習する必要はありません。しかし、結局、デプロイするにはモデルが大きかったり、API として使うには推論が遅かったりといった問題が残ります。

このように、性能と運用のバランスを考えると、fastText はいまでも非常に優れた選択肢だと思います。

fastText を使っている取り組み

クックパッドで fastText を使っている取り組みとしては、たとえば、以下があります。

  • 単語埋め込みを利用した商品に対するキーワードの予測(to appear). 山口泰弘, 深澤祐援, 原島純. 言語処理学会第 28 回年次大会発表論文集.

こちらは、クックパッドマートの商品名から、食材を表すキーワードを予測する取り組みです。キーワードや商品名をベクトルに変換するのに fastText を使っています。予測結果はクックパッドマートの管理画面で使われています。

余談ですが、こちらの取り組みは今年の言語処理学会で委員特別賞をいただきました。ありがとうございます。

こちらは、レシピのタイトルから、そのレシピで使われるであろう食材を予測する取り組みです。タイトル中の単語をベクトルに変換するのに fastText を使っています。予測結果はレシピの投稿画面で使われています。

こちらは、レシピのタイトルから、そのレシピのカテゴリ(e.g., 肉料理、魚料理、野菜料理、...)を予測する取り組みです。こちらも、タイトル中の単語をベクトルに変換するのに fastText を使っています。予測結果は、近日中に、レシピのブックマーク画面で使われる予定です。

その他、まだ実験段階の取り組みでも fastText をよく使っています。

fastText の学習・利用フロー

以下は、クックパッドにおける fastText の学習・利用フローです。Redshift から学習データを取得し、fastText を学習した後、モデルを S3に保存するというのがおおまかな流れです。たいしたことはしていません。ちょっと変わったことがあるとすれば、学習データが Redshift にあることくらいでしょうか。

f:id:jharashima:20220418092004p:plain:w300

1. 学習データの取得

fastText の学習にはテキストが必要です。日本語の場合、さらに、単語分割が必要です。

クックパッドの場合、全レシピのテキスト(e.g., タイトル)が Redshift に保存されています。また、その分割結果も Redshift に保存されています。詳細は以下の記事をご覧ください。fastText の学習にはこれを使っています。

分割結果の取得には Queuery(きゅうり)というシステムを使っています。Queuery は、UNLOAD を使うことで、Redshift やクライアントに負荷をかけずに SELECT を実行できるシステムです。Queuery は去年末に OSS 化されました。詳細は以下の記事をご覧ください。研究開発部の山口による Python クライアントもあります。

2. fastText の学習

Python スクリプトに以下の 2 行を書くだけです。fastText、便利すぎますね...。

import fasttext
model = fasttext.train_unsupervised('data.txt', model='skipgram')  # cbow でも可

全レシピ(2022 年 4 月時点で約 367 万品)のテキストを使っても、学習は約 10 分で終わります。メモリも 2GB 程度で済んでいます。学習には EC2 のスポットインスタンスを使っています。

パラメータは特にいじっておらず、デフォルトのままです。たとえば、ベクトルの次元数は 100 です。パラメータのチューニングは今後の課題(後述)です。

3. モデルの保存

モデルは S3 に保存しています。ロールバックできるように、過去に学習したモデルも残してあります。幸い、実際にロールバックが必要になったことはありません。まだ特に困っていませんが、ライフサイクルくらいは設定してもいいかもしれません。

4. モデルのダウンロード

学習済みのモデルを使いたいアプリケーションに対して S3 の該当フォルダへの Read アクセスを許可します。これで各アプリケーションでモデルをダウンロードできます。

以上が fastText の学習・利用フローです。その他、補足事項として以下があります。

fastText はレシピのフィールド(e.g., タイトル、材料、...)毎に学習しています。これは、fastText を使うタスク毎に着目するフィールドが違うためです。タイトルに着目するタスク(e.g., レシピの分類)ではタイトルで学習したモデルが使えるように、材料に着目するタスク(e.g., 材料の分類)では材料で学習したモデルが使えるようにしています。

ジョブスケジューラーやデプロイツールには Kuroko2hakoを使っています。実行は基本的に月次です。学習時間が短いので、日次で実行したところで、特に問題はありません。ただ、分散表現はそんなに変わらないだろうと思うので、月次としています。もしかしたら年次でもいいのかもしれません。

今後の課題

最後に、今後の課題を三つほど挙げておきます。

一つ目は、「fastText の学習」でも触れたように、パラメータのチューニングです。学習アルゴリズムや学習データ、学習率、ベクトルの次元数、サブワードのレンジなど、チューニングの余地はたくさんあります。この辺りは腰を据えて取り組んでいきたいです。

二つ目は分散表現の評価です。一つ目の話とも関連するのですが、どのような分散表現がよいかは自明ではありません。基本的には、後段のタスクにおける評価指標を最適化する分散表現がよい気がします。ただ、後段のタスクにもいろいろあるので、悩ましいところです。

三つ目は代替モデルの調査です。「なぜ fastText なのか?」でも触れたように、本番での運用まで考えると、BERT のようなモデルが fastText より明らかによいとは言えません。一方、この業界の発展は速く、様々な懸念を払拭するモデルが明日にも発表されるかもしれません。業界の動向には常にアンテナを張っていきたいです。

おわりに

そういえば、つい最近、就業形インターンシップに「機械学習コース」を開設しました。上で挙げた課題はもちろん、クックパッドにおける機械学習に興味がある方は是非ご応募ください。

中途採用のご応募もお待ちしております。


クックパッド生鮮 EC お届けの裏側 2022 年版

$
0
0

クックパッドマートでバックエンドエンジニアをしている奥薗 ( @mokuzon ) です。クックパッドマートは生鮮食品の EC サービスで、商品を届ける流通を内製しています。そんなクックパッドマート流通にエンジニアがどう貢献しているかを今日から 4 日連続でご紹介していきます。

今回はクックパッドマートの裏側の流通がどんな要素で構成されているかを一気にご紹介します。このエントリを読んでおくと今後クックパッドマートの関連エントリを読むときにより具体的にイメージしやすくもなると思います。

項目が多いので全体を俯瞰しやすいよう目次を貼っておきます。

クックパッドマートについて

クックパッドマートは 2018 年 9 月 20 日にリリースされた生鮮食品の EC プラットフォームです。リリースから 3 年以上経ち、新規事業ならではのスピードを維持しつつサービス拡大のため試行錯誤を日々続けています。

https://cookpad-mart.com/

クックパッドマートは iOS と Android の専用アプリと、最近ではクックパッドのレシピアプリでも利用可能になっています。このアプリで商品を購入して、近所の受け取り場所 ( ステーションと呼んでいます ) で受け取れます。有料で自宅配送するオプションもあります。

取り扱っている商品はクックパッドが生産・在庫を持っているわけではありません。市場の卸売業者の方々、農家の方々、小売店の方々など様々な販売者がクックパッドマートを通して商品を販売しています。これが生鮮 EC ではなく生鮮 EC プラットフォームを名乗っている理由です。

クックパッドマートではこれらの販売者が出荷した商品を前述のステーションまで運ぶ流通を内製しています。この流通についてより詳しくご紹介します。

クックパッドマートの流通

資材

コンテナ

商品単位ではなくコンテナ単位で流通させています。これは効率よく物流を回すためのセオリーで、コンテナ物語*1という有名な本があるので興味がある方は読んでみてください。

Docker*2に代表されるコンテナ技術と同様、規格化には物流にも絶大な恩恵があります。

シッパー

冷蔵機能のない車で肉や魚をはじめとしたチルド商品を運ぶために使っています。断熱材で出来ていて、この中に業務用蓄冷剤を入れて使用しています。 1 つのシッパーには上記のコンテナが 4 つまで入ります。

ラベル

商品やコンテナに貼り付けるラベルシールはマート流通で非常に重要なユーザーインターフェースです。現実の物体とソフトウェア上のデータを紐付けたり、直接ラベルに作業指示を印字したり、活用方法は様々です。

QR コード*3を印字しているものもあります。なお、このラベルを印刷するプリンターをマートのネットワークに繋ぐハードウェアの制御部分は内製です。

プリンターについて過去にご紹介したエントリーはこちらです。 https://techlife.cookpad.com/entry/2019/04/10/180000

ドライバー

クックパッドには自前のドライバーはいません。数社の運送会社と契約し、クックパッドマートの配送業務を担っていただいています。車両は軽カーゴ ( いわゆる軽バン ) と 2t トラックを使い分けています。

拠点

商品を流通させるにあたって、一時的に商品を保管したり配送効率を向上させるための拠点が必要です。ユーザーから近い順に

  • ステーション
  • ハブ
  • ローカルスポット

の 3 種類あります。

ステーション

先程も紹介したユーザーが商品を受け取りに行く拠点です。

  • コンビニ
  • ドラッグストア
  • コインランドリー
  • マンション
  • 銀行

など、様々なところに設置させて頂いています。現在 1 都 3 県で 700 箇所超あり、どんどん増えています。 実際の受け取り場所は https://cookpad-mart.com/about/maps/stationsで確認することが出来ます。

冷蔵庫についている A, B + 冷蔵庫内部の両サイドに貼られている数字のシールで番地を表現しています。

ハブ

マートが持つ最大の物流拠点です。一般的な物流用語だとデポと呼んだりします。販売者が出荷した商品を集約し、ここを基点に各ステーションに配送を行っています。 現在 7 箇所あります。

ハブ内でユニークな通し番号が振られた番地札がついています。

冷蔵庫とスチールラックの棚があります。冷蔵の必要のない商品は冷蔵庫を使わないほうが空間効率もコスト効率も圧倒的に高いため、スチールラックの活用は重要です。

ローカルスポット

原則販売者は上記のハブに商品を出荷しに来ますが、ハブは 7 箇所しかなくすべての販売者がハブに直接出荷しに来ることは現実的には難しいです。そこで、各地に設けた一時置き場に販売者が一時的に商品を置いておき、マートのドライバーが回収しハブに運ぶ運用を一部しています。この一時置き場をローカルスポットと呼んでいて、状況で増減しますが約 150 箇所あります。

ルート

3 箇所の拠点を紹介しました。これらを 3 種類のルートで結んでいます。ユーザーから近い順に

  • ステーション便
  • ハブ便
  • 出荷サポート便

があります。

このルートの組み方、通称ルーティングには大きな技術的チャレンジがあります。これは 2 日後に詳細にご紹介するとして、ここでは簡単に種類だけご説明します。

ステーション便

7 箇所のハブそれぞれから 700 箇所強のステーションへ繋がるルートです。ハブで商品を集荷し、5-7 箇所のステーションへ運びます。車両は軽カーゴです。

ハブ便

7 箇所のハブ同士を繋ぎ、マートの商圏のどこでも商品を届けられるようにしています。実際にはもう少し効率的な組み方をしていますが、環状線のようなイメージです。一般的な物流用語では横持ちと呼んだりします。この便は物量が多いので 2t トラックを使っています。

出荷サポート便

ローカルスポットからハブに商品を運ぶ便です。軽カーゴを使っています。

タイムライン

前述した 3 つの便は時間帯が決まっています。並べると以下のようになります。

販売者は 00:00-21:00 の間に出荷し、順次運ばれていきます。

現状のタイムラインだと販売者はユーザーから注文があった後に商品を出荷するため、それがこのタイムラインに乗ってステーションに届きユーザーが受取可能になるのにほぼ 2 日近くかかってしまうというのがサービス上大きな課題となっています。

より効率的な運び時間を短縮したり、時間枠をユーザーの生活時間にあわせて最適化するなど、今も様々な改善策を考えているところです。

なお、見て分かる通り配送はほぼ 24 時間動き続けています。システムにトラブルがあると即配送遅延に繋がるため、バックエンドエンジニアはシフトを組んで即応出来るようにしています。これについては以前エントリーを書いていますので興味のある方はどうぞ。 https://techlife.cookpad.com/entry/introduce-mart-on-call

アプリケーション

ひたすら物理世界の説明をしてきて、ついにアプリケーションの話です。開発者ブログとはいったい...いえ、これが面白いんですよ。

ドライバー向けアプリケーション

ドライバーにはルートの種類ごとに別のアプリケーションを提供しています。こういったパートナー向けアプリはモバイルアプリで提供されていることが多いと思いますが、マートではこれらはいま全て web フロントエンド技術で実装したアプリケーションを提供しています。これについては明日詳細にご紹介します。

クックパッドスタッフ向け admin

これは特筆することはありません、典型的な管理画面です。

ラベル

ラベルの内容の定義方法についてご紹介した過去のエントリーです。 https://techlife.cookpad.com/entry/2021/08/18/100000

これ以外にも、Bluetooth プリンター向けに Flutter*4で書かれた専用のモバイルアプリもあります。

温度監視

食品を扱う以上、温度監視はとても重要です。一部構成が異なるものもありますが、各温度センサーから温度をリアルタイムでネットワーク越しに収集し、Grafana*5で表示したり Prometheus*6でアラートを飛ばすシステムを構築しています。

おわりに

クックパッドマートの流通の構成要素とそれに付随するアプリケーションについてご紹介しました。我々クックパッドマートの流通エンジニアが戦っている物理世界がどういうものであるか、イメージして頂けたら幸いです。

もう少しラフに流通エンジニアについて紹介している note がありますのでこちらもあわせてどうぞ。 https://note.com/cookpad_mart/n/nc38d718a5d90

今回は物理世界の説明が中心になってしまいましたが、明日からはより技術的に突っ込んだお話をしていきますのでお楽しみに。

クックパッドマートは流通エンジニアを大募集しています。少しでも興味を持っていただけましたら、Twitter で @mokuzonに声をかけていただいてもよいですし、カジュアル面談も実施していますのでぜひご応募ください。

2022年クックパッドマート連載の他のエントリ

  • クックパッド生鮮 EC お届けの裏側 2022 年版 ( 本エントリ )
  • クックパッドマートのドライバー向けWebアプリケーション
  • クックパッドマートの配送ルートを自動生成している仕組み
  • クックパッドマート最難解ロジック!?「採番処理」

クックパッドマートのドライバー向けWebアプリケーション

$
0
0

クックパッドマートの開発に携わっていますバックエンドエンジニアの中村です。

クックパッドマートは生鮮食品のECサービスで、流通の仕組みを自分たちで作っています。当然ですが商品を流通させるには、物理世界でものを動かす必要があります。実際にものを運ぶのはドライバーと呼ばれる人が行っており、このドライバーに向けていつ・何を・どこに運ぶといった指示をする必要があります。このエントリではそんなドライバーへの指示のために開発しているWebアプリケーションを紹介します。

各便のアプリケーション

クックパッドマートの流通の中には販売者が出荷してからユーザーの手元に届くまでに、タイムライン別に複数の流通方法が存在しています。タイムライン順に 「出荷サポート便」,「ハブ便」,「ステーション便」 といった流通方法が存在しておりそれぞれの便で個別にWebアプリケーションを用意しています。各流通方法を含む流通全体についてはこちらのエントリを見ていただけるとわかりやすいかと思います。

出荷サポート便Web

ローカルスポットという販売者の一時出荷拠点からハブという最大の流通拠点に運ぶ際に使用します。ドライバーはその日立ち寄るローカルスポットを確認し、各ローカルスポットで指定された商品を集荷します。その後指定されたハブの番地に商品を納品します。

ハブ便Web

あるハブから別のハブに運ぶ際に使用します。ドライバーは立ち寄るハブを確認し、それぞれのハブで指示されたコンテナを集荷・納品します。また集荷の際には行き先を示したラベルに張り替えるため、コンテナ用のラベルの印刷を行います。

ステーション便Web

ハブからステーションというユーザーが実際に受け取りを行う拠点に運ぶ際に使用します。ドライバーは集荷するハブを確認し、ハブでコンテナを集荷します。集荷の際には納品先のステーション番地のラベルに張り替えるため、コンテナ用のラベル印刷を行います。その後指定されたステーションに立ち寄りながらコンテナを納品していきます。

使用技術

開発時期によって違いはありますが、最近は以下のような構成にしています。

いわゆるBFF(Backend For Frontend)は配置せず、クライアントから直接APIを呼んでいます。パブリックに公開しているサービスではないのでSEO対策が不要なこととそこまでシビアなパフォーマンスを求められるわけではないので基本的にはSSR(Server Side Rendering)を使用せずシンプルな構成にしています。 SSG(Static Site Generator)も検討しましたが、一部動的に処理したいものがあったためバックエンドと同じく社内基盤のAWSのECS上にデプロイしています。

MUIを活用

ドライバー向けという性質上、パブリックに公開しているサービスに比べてリッチな独自デザインの必要性は低いため、流通チームには専属のデザイナーがいません。 そこで、MUIをほぼデフォルトで使いきることでドライバーの使用に問題ないクオリティのUIを素早く実装できるようにしています。

ネイティブアプリからの移行

実は元々Android/iOSのネイティブアプリを提供していた時期もありました。しかしチーム内にはサーバーサイドのエンジニアしかおらず、アプリを改修したいとなると他のチームに依頼をしてリソースを調整するといったことが都度発生し、どうしても足が遅くなってしまっていました。日々流通の形が進化していくので、こうした足の遅いアプリの開発がボトルネックになってしまうことが多くなってきました。しかし、一方で常にアプリ開発のタスクがあるわけではなく散発的に改修が必要になる状況で、流通チーム内にアプリエンジニアを常駐させておくほどではありませんでした。それならいっそ全てWebに移行して、サーバーエンジニアだけで開発を完結できるようにしようという決断をしてネイティブアプリからWebアプリにリプレイスしました。

Next.jsを選択したのもネイティブアプリで提供していたUI/UXに遜色ないものを実現したかったというモチベーションが大きかったからです。Reactベースのフレームワークは他にも選択肢がありますが社内でも実績・ノウハウのあるNext.jsを選びました。

ネイティブ特有の機能

リプレイスの際、大きなハードルだったのはネイティブアプリ特有の機能をどうするかということです。幸いにも最近のブラウザはかなりの機能を実現できるようになってきており、絶対にネイティブアプリでなければならないということが少なくなってきました。

カメラ

集荷・納品時の検品のためにQRコードを読んだり、納品結果の撮影のために使用しています。

一番苦労したのはカメラの権限の許可設定でした。対象のドメインとブラウザ自体の2箇所で権限を許可する必要があり、全ドライバーに権限を許可してもらうよう展開するのが開発より大変でした。

プッシュ通知

プッシュ通知も実現できるようになりつつありますが、iOSは未サポートだったりと全端末で活用できるわけではありません。代わりに全ドライバーをSlackに招待してSlack上でメンションすることによって通知するようにしました。

その他にもSMSを送信することも検討していて、実現の目処も立ってはいたのですが、別文脈でドライバーをSlackに招待してコミュニケーションをとるようにする取り組みがあり、機能としてもかかる工数としても都合がよかったため利用した形です。

位置情報

使っていません。ネイティブアプリでは取得していたのですが、結局活用しなかったのでリプレイス時にはオミットしました。仮に使いたくなった場合はドライバーの位置情報から配送の進捗を見たいといった用途が考えられるので、Webだとバックグラウンドで取得できないことが高いハードルになりそうです。

最後に

マートのドライバー向けWebアプリケーションについて紹介してきました。

途中にも書きましたが日々流通の形が進化しており、この先もまだまだ改善していく必要があります。単なるWebアプリケーションではなく物理世界と密接に関係している開発ができるのはなかなか刺激的で楽しいので、少しでも面白そうだなと思って頂いた方はぜひご連絡ください。

2022年クックパッドマート連載の他のエントリ

クックパッドマートの配送ルートを自動生成している仕組み

$
0
0

こんにちは、クックパッドマート流通基盤アプリケーション開発グループのオサ(@s_osa_)です。

生鮮食品の EC サービスであるクックパッドマートでは、「1品から送料無料」をはじめとするサービスの特徴を実現するために、商品の流通網を自分たちでつくっています。

このエントリでは、商品をユーザーに届けるための配送ルートを自動生成している仕組みについて紹介します。

解決したい問題

配送ルートとは

クックパッドマートにはいくつかの流通方法がありますが、ここでは「ステーション便」と呼ばれるものについて解説します。他の流通方法などを含む全体像が気になる方は以下のエントリがオススメです。

クックパッド生鮮 EC お届けの裏側 2022 年版 - クックパッド開発者ブログ

ステーション便では、ハブと呼ばれる流通拠点からユーザーが商品を受け取りに行く場所であるステーションへと商品を運びます。東京都、神奈川県、埼玉県、千葉県の一部地域に700以上のステーションがあり、それらのステーションに対して複数のハブから配送をおこなっています。 ステーションはコンビニやマンションなどに設置されている冷蔵庫で、クックパッドマート配送の大きな特徴のひとつです。

クックパッドマートのステーション(冷蔵庫)

サービスの利用方法は次のような流れになります。

  1. アプリから商品を注文する
  2. 最寄りのステーションに商品が届く
  3. 都合の良いタイミングで受け取る

この仕組みにより、1品から送料無料を実現できたり、配達時間に在宅しておく必要がなくなったりといった特徴が実現されています。

一部地域を抜粋してハブとステーションを地図にプロットすると以下のようになります。白抜きされた大きな点がハブ、小さくて黒い点がステーションです。

ハブとステーションの位置

すべてのステーションに商品を届けるためには、ハブ(白抜きされた大きな点)を出発してステーション(黒い点)を網羅するルートを作る必要があります。

たとえば、以下のような感じです。

配送ルートの例

配送ルートに求めるもの

配送ルートを組むにあたり、どんなルートでも良いかというと当然そんなことはなく、いくつかの制約や目標があります。具体的には以下のようなものです。

  • 所要時間(所定の時間内に配送完了できる)
  • 積載量(商品をあふれさせず積みきれる)
  • ステーションの営業時間
  • コスト効率
  • スケーラビリティ

それぞれの性質について簡単に解説します。

所要時間

現在のクックパッドマートでは12:00から商品を受け取れるようにしており、08:00から12:00までの4時間でステーションへの配送をしています。生成するルートはこの時間内に配送を終えられるように組む必要があります。

積載量

車に積める量には限度があります。

クックパッドマートでは商品をコンテナ(カゴ)に入れて運んでいます。常温商品はコンテナをそのまま積み込めば良いですが、冷蔵商品はコンテナをシッパーと呼ばれる保冷容器に入れた上で蓄冷材と一緒に運ぶ必要があるのでより多くの体積を必要とします。

シッパー(保冷容器)に収まっているコンテナ

こういった温度帯ごとの体積の違いといった事情も考慮した上で商品を車両に積みきれるようにルートを組む必要があります。

ステーションの営業時間

配送先のステーションには、納品できる時間帯に制限があるステーションも存在します。

たとえば、マンションで09:00以降にならないと管理人さんがいないため入れない、ドラッグストアの営業時間が10:00から、といったケースなどがあります。

こういった各ステーションの営業時間を考慮して、納品できる時間に着くようにする必要があります。

コスト効率

配送は車両と人間を動かす必要があるので、かなりコストがかかります。毎日ルートが1本減ればその1本分、月で延べ30本分のコストが削減できます。

したがって、できるだけ少ないルート数で配送できるようにしたいという要求があります。

スケーラビリティ

上記のような制約や目標を考慮しながらルートを組むのは人間にはあまりにも難しいです。

いにしえの時代、クックパッドマートの配送ルートは人間によって手組みされていましたが、ステーションの数が300くらいの頃で、熟練のルート職人が2日かけてやっとルートを組み終わるといった状況でした。

サービスは今後も拡大していくため、ステーションが増加してもルートを組み続けられる仕組みが必要です。

ブロック分割

さて、これからルートを組んでいくわけですが、複数のハブから700箇所のステーションに運ぶとなると「それぞれのステーションにはどのハブから運ぶのか」ということが問題になってきます。

そこで、ルート生成に先立って、それぞれのステーションをハブに対応付ける前処理を入れます。我々はこれをブロック分割と呼んでいます。

先にブロック分割の結果を図示しておくと次のようになります。同じ色の点が同じブロックに属するステーションです。

ブロック分割の例

ブロック分割の方針

基本的な考え方はとてもシンプルで「それぞれのステーションに近いハブから運ぶ」というものです。この方針はわりと素直に受け入れてもらえるものだと思います。

ただ、この方針を実装していくにあたり、考慮・対応すべきことがいくつかあるので、それらの点について書いていきます。

「近い」とは

「近いハブから運ぶ」と言っても、本当に関心があるのは距離ではありません。実は「ルートに求めるもの」のところでも距離には一切触れていません。

では代わりに何に触れているかといえば、時間です。所定時間内に運びきるという制約を考えるためには距離よりも時間に着目する必要があります。

移動時間の求め方

最も素朴な移動時間の求め方はハブやステーションの緯度経度から直線距離を求めて、平均速度で割るというものです。この方法は正確性は低いですが実装が楽で早くリリースできるので最初期はこの方法で計算していました。

ただし、この直線距離を使う方法には大きな問題があります。最も顕著な例としては、東京や神奈川から千葉に商品を運ぼうとすると車が東京湾の海上を走れることになってしまいます。言うまでもなく車は海上を走れないので、実際には沿岸部の道路だったり東京アクアラインだったりを通って迂回する必要があり、計算結果にかなり大きな誤差が生じてしまいます。また、東京湾ほど大きくないものだと「川は橋がある箇所でしか渡れない」「行きたい方向に走っている太い道路がない」などの誤差要因もあります。

そこで、道路を考慮した移動時間を計算するために Open Source Routing Machine (OSRM) を使っています。

http://project-osrm.org/

OSRM は OpenStreetMap (OSM) の地図データを用いて経路計算をしてくれるルーティングエンジンです。Google Maps の経路検索をイメージしてもらうとわかりやすいと思います。

OSRM はフロントエンドまで含めたプロジェクトになっていますが、バックエンドの API だけでも利用することが可能です。クックパッドマートでは社内にデプロイした OSRM の API を使って移動時間を計算しています。また、この API は数十msと比較的高速にレスポンスを返してくれますが、ルート生成の過程では大量の移動時間の計算が発生するため、必要になる移動時間は事前に計算して DB に入れておいて、計算時には一括でメモリに載せてしまうなどのパフォーマンス上の工夫もいくつかおこなっています。

各ハブのキャパシティ考慮

近くのハブから運びたいというのは間違いなく真です。ただし、「運びたい」と「運べる」は別の話です。

現実の各ハブが無限の荷量をさばけるかというと決してそんなことはなく、実際にはさばける荷量(キャパシティ)には上限があります。そこで、各ハブがさばける荷量に収まるようにステーションの紐付け先を調整します。

この調整はわりと素朴なアルゴリズムでおこなっています。具体的には、事前に各ハブに移動時間に対する係数を持たせておいた上で、キャパシティを超えているハブがあった場合、そのハブの係数を増やします。そして、変更後の係数を使って移動コスト(係数を反映した重み付き移動時間)を再計算した上で、紐付け先を計算し直してキャパシティに収まっているかチェックして……というのを繰り返します。細かいところの動作は全然違いますが、k-means 法などをイメージしてもらうとわかりやすいかと思います。

ルート生成

ブロック分割によって、それぞれのステーションにどのハブから商品を運ぶかということが決まったので、各ブロック内でルートを組んでいきます。

初めにも貼りましたが、再掲しておくと、たとえばこんなルートになります。

配送ルートの例(再掲)

配送ルートの組み方

配送ルートを組む問題を一般に配送計画問題(Vehicle Routing Problem, VRP)と呼びます。組み合わせ最適化問題の一種で、最適解を現実的な時間(多項式時間)で求めることはできない問題です。

一方で、VRP という名前がついているくらいには有名な問題なので、近似解を求めるライブラリなどは存在しています。クックパッドマートでは OR-Tools というライブラリの Ruby wrapper を使用しています。公式の C++, Python 実装ではなく非公式の Ruby 版を使っているのは既存実装との繋ぎ込みやチームのメンバー構成などを考慮した結果です。

VRP の最も難しい部分である解の探索はライブラリがやってくれるので、我々アプリケーション開発者は自分たちのサービスが必要としているルートの条件を整理・変形して、ライブラリが解ける一般的な問題に落とし込んでいきます。

一般的な問題への落とし込み

基本的な考慮事項は前述の所要時間・積載量・ステーションの営業時間などです。これらを OR-Tools が求めるカタチにして渡していきます。

たとえば、移動時間の計算に OSRM を使う話をしましたが、OSRM が返す移動時間は実際にかかる移動時間よりも少なめに出ることが多いので、その差を補正するための係数をかけています。

また、移動時間は OSRM で算出することができますが、実際には移動時間とは別に各ステーションでの納品にかかる作業時間があります。そこで、作業時間を考慮したトータルの所要時間を入力として渡すようにします。

さらに、一般的な VRP は配送完了後に出発地点に帰ってくるルート、つまり1周するルートを組むようにできています。しかし、クックパッドマートで必要な制約は「12:00までにステーションに配送完了」であって「12:00までにハブに戻る」ではないので、最終配送の後の戻りルートを時間計算に含めないようにする必要があります。

こうして、自分たちのサービスで必要なことをひとつずつ一般的な問題に落とし込んでいきます。

運送会社へのルートの割り当て

クックパッドマートでは実際の配送業務自体は運送会社に外注しています。また、外注先の運送会社は複数社あります。そこで、生成したそれぞれルートをどの会社に割り振るかということを考える必要があります。いわゆるマッチング問題の一種ですが、ここでもいくつかの考慮事項があります。

たとえば、それぞれの運送会社には「行きたいハブ」と「行きたくないハブ」があります。これは運送会社の立地や営業エリアなどの特性によって生じるものです。運送会社と継続的な関係を結んでいくためには各運送会社にとって無理がないルートを割り振る必要があります。

この問題を解決するために、確保しているそれぞれの車両とハブの組み合わせについて「行きたくない度」(選好)を事前に登録しておき、その点数ができるだけ小さくなるようにルートを割り当てています。

また、「あるステーションは入館証が必要で、入館証を持っているのはA社のみ」のような制約もあるので、そういった点も考慮して各ルートを運送会社に割り振っています。

やっていないこと

ここまでルート生成でやっていることについて書いてきましたが、一方で意図的にやっていないこともあります。

代表的なものが具体的な走行ルートの指示です。各ステーションの配送順序や住所とその住所の Google Maps URL などは提供していますが、具体的な走行ルートは指定・指示していません。これは交通状況は常に変化するため効果的なルート指定が難しい上、運送会社やドライバーの方は運転・配送についてはプロなので指定する必要性が低いというのが理由です。

また、それぞれのブロックは独立しているのでルート生成は並列化可能ですが、今のところ特に困っていないので直列で計算しています。

おわりに

クックパッドマートで配送ルートを自動生成している仕組みについて簡単に紹介してきました。

どの領域でもそうだと思いますが、流通というドメインにも特有の問題や難しさ、そして楽しさがあります。また、問題解決のためには事業ドメインと技術ドメインの両方を考慮して解決方法を探っていく必要があります。現実世界のモノを運ぶという課題に対して、ソフトウェアを軸足にして取り組んでいくのは非常に楽しいです。

このエントリで紹介できたのは流通という領域のごく一部です。少しでも興味が湧いた方がいたらぜひご連絡ください。採用サイトからの正規ルートでも良いですし、@s_osa_まで雑に DM していただくなどでも大丈夫です。よろしくお願いします。

2022年クックパッドマート連載の他のエントリ

クックパッドマート最難解ロジック!?「採番」

$
0
0

クックパッドマート流通基盤アプリケーション開発グループでバックエンドエンジニアをしている奥薗 ( @mokuzon )です。今日まで 4 日間連載でクックパッドマートの流通についてご紹介してきました。最後のこのエントリーではマート内で 1,2 を争う難解かつ重要な処理と言われている「採番」についてご紹介します。

先に クックパッド生鮮 EC お届けの裏側 2022 年版を読むとよりイメージがつきやすいです。

採番とは

マートでは

  1. 商品はハブという大規模拠点に出荷され
  2. ハブ便でハブからハブへ移動し
  3. ステーション便でハブからステーションと呼ばれる拠点に移動して
  4. ユーザーはステーションに商品を受け取りに行く

というのが基本の流通になっています。

マート流通の概略図

採番とは、この商品がどのようなスケジュールで出荷されどういった経路でユーザーまで運ばれるかを計画する処理です。多くは注文時にオンラインで、一部特定時刻にバッチ処理で行っています。

出荷された商品やそれが入ったコンテナには以下のようなラベルが貼られています。

枠で囲った部分は実際のハブとステーションにある番地名です。商品の配送計画をするにあたってこのように番地などを決定していくため「採番」と呼ばれています。

この採番はドメイン知識の集合体かつ非常に手続き的な処理のため、物理的な流通の仕組みが大きく変わるとこの採番もほぼ作り直しになるなど、改善も含めてこれまでに 3 回リプレースしました。

なかなかに難解な処理でずっと職人と呼ばれる人たちの独断場でしたが、3 年強の運用で大分こなれて勝ちパターンが見えてきたのでこのタイミングでご紹介することにしました。

なにが難しいか

ひとえにマート流通のあらゆるドメイン知識が詰め込まれていることです。3 日かけてマート流通を紹介してきましたが、これだけでも複雑ですし、実際には紹介しきれないぐらい細かいより複雑な要件が沢山あります。この複雑な流通を計画するという採番の性質上、どうしてもこの難しさとは向き合っていく必要があります。

採番するにあたりどのようなことを考慮するかをざっと挙げると

  • キャパシティ管理
    • コンテナに収まるか
    • ハブ便の 2t トラックに収まるか
    • ステーション便の軽カーゴに収まるか
    • 冷蔵商品を入れるシッパーに収まるか
    • ハブの番地数は足りるか
    • ステーションの番地数は足りるか
  • どのコンテナに収めるか
    • 商品が出荷されるハブはどこか
    • ハブ便で移動先のハブはどこか
    • 最終的に到達するステーションはどこか
  • 温度管理
    • 常に冷蔵が必要な商品か
    • 冷蔵でも常温でもいい商品か
      • 夏は? 冬は?
    • 冷蔵 NG な商品か
  • 出荷日
    • 販売者の営業日か
    • 商品の消費期限内に流通させられるか

のような条件を一つ一つクリアしていきます。

設計の工夫

現実世界では密結合でもデータでは疎結合にする

配送のフェーズによって商品がいる場所は以下のように変化します。

  1. ハブのコンテナの中
  2. ハブ便のコンテナの中
  3. ハブのコンテナの中
  4. ステーション便のコンテナの中
  5. ステーションのコンテナの中

すべてのコンテナは同一のコンテナで、コンテナに収まるかのキャパシティ管理とそのコンテナの経路だけ決めればいいようにするのが理想です。具体的には以下のイメージです。

しかし、現実には

  • ステーションに直接出荷する特殊な出荷方法
  • ユーザーの受け取り期限を延長し、ステーションのコンテナが入れ替わっても特定商品はそのまま残したい
  • 配送拠点で商品を仕分けし、それぞれの配送都合に最適な形でコンテナに詰め直したい

このような要望があり、綺麗に 1 度コンテナに商品を入れただけでは完結しないシチュエーションが多々あります。そこで、以下のように各拠点や各便を疎に表現するようにしています。

こうすることで、ある 1 点での特殊な状態が少ない影響で表現しやすく、また将来的に流通の構成要素に変更があった際も、変更範囲が相対的に少なく抑えられると期待しています。

留まることの表現

採番では「この地点にこの体積のものがいつからいつまで留まる」という表現が多発します。

以前は「この便に乗っているものは何時から何時」というドメイン知識から同じ番地を使い得るもの同士で「これとこれは被る」「これとこれは被らない」というのをすべて網羅して判定していました。一例として挙げると

  • 今日出荷された商品は昨日出荷された商品が最初に到達するハブでは時間帯が被らない
  • 今日出荷された商品は昨日出荷された商品が最後に到達するハブでは時間帯が被る
  • etc…

というような条件を愚直に十数件判定していました。

すべて網羅していたというのは嘘で、実際にはどんどん複雑化して考慮漏れがあり、現実世界で番地競合を起こし配送で大事故が起きたことがありました。

これ以外にも似たようなパターンのミスが起きていて、共通しているのは留まる期間が 1 日を超え、別の日に扱うもの同士が干渉しうる状態になっていたことです。複雑度がここまで来ると扱いきれなくなるという指標ができました。

そこで、留まることを具体的に表現するテーブルを作る方針に変更しました。 以下の ER 図の assignmentsテーブルです。

  • 始点をキリの良い時刻で表現したほうが運用上便利
  • データを <, >より <=, >=で扱えた方が直感的

という理由で 13:00-14:00 のような期間は実際には start_at: 13:00:00, end_at: 13:59:59 のように保存しています。

このテーブルがあると、例えば以下の図で X start_at と X end_at の間に被るレコード B, C, D は

assignments.start_at <= X end_at and X start_at <= assignments.end_at

という簡単なクエリで導出出来ます。

注意点としては我々が DB で採用している B-tree index だと範囲指定を複数のカラムで使ってる場合、片方の index しか効きません*1

end_at <= X start_atは時間が経つほど過去のレコードが積み重なり参照する行数が増えていきます。なので未来のレコードを参照し相対的に桁数が少ない assignments.start_at < X end_atの index が使用されるようにしています。

チェックバッチ

採番はナマモノです。採番後に前提としていたデータが再入稿されたり突然ステーションが臨時休業になったりして、一時は正しかった採番データが不正なデータになったりします。

また、この採番処理は注文時にオンラインで実行していてパフォーマンス上の事情で DB でトランザクションを張れていないため、マートが定期的に開催するタイムセールなど注文が殺到するとどうしても競合データが生まれてしまいます。

こうした不正なデータ対策として、データのチェックバッチを運用しています。

考えられ得る限りの不正な状態を実装してチェック、想定外なパターンが出てくればそれも実装してチェックバッチを育てています。簡単なものなら自動でデータ修正もしています。

育てやすくするためにチェックバッチはプラガブルな設計になっています。これはどこかで改めて詳細にご紹介できればと思っています。

Slack にチェックバッチの結果が通知されている様子

リニューアル方法

現在マートは 24 時間 365 日注文を受け付け続けていて、この採番処理もいつでも動きえます。

本当はメンテナンスを挟んで移行したいところですが、事業場注文や流通を止めたくはないですし、この採番データは流通のあらゆるシステムで参照されていて変更箇所が膨大になるので、ビッグバンリリースを避ける観点でもオンラインで変更していくようにしています。

方法としてはこの手の移行だとセオリー通りかと思いますが、新旧ロジックがあるとして

  1. 旧ロジックの裏で、新ロジックも裏で動かす
    • テーブルも新設して新旧両方のテーブルに書き込む状態にする ( Dual Write )
    • この時点では各所では旧ロジックの結果のみ参照する
    • 先述のチェックバッチなどで、新ロジックの結果が意図通りになっていることを 1, 2 週間運用して確認する
  2. 新ロジックの結果を元に旧ロジックが書き込むテーブルにデータをコピーする
    • 旧ロジックは止める
  3. データの参照先を旧テーブルから新テーブルに徐々に切り替えていく

という手法で行っています。

おわりに

クックパッドマート随一難解な採番処理についてご紹介しました。

正直文章だけでお伝えしきるのは難しいものなので、少しでも興味を持っていただけましたら Twitter で @mokuzonに声をかけていただいてもよいですし、カジュアル面談も実施していますのでぜひお気軽にご応募ください。

cookpad-mart-careers.studio.siteinfo.cookpad.com

2022年クックパッドマート連載の他のエントリ

オフラインイベント「Cookpad Tech Kitchen #26 数千万レコードをリアルタイムに捌く生鮮EC事業開発」を開催しました!

$
0
0

買物プロダクト開発部部長の勝間(@ryo_katsuma) です。3/24に「Cookpad Tech Kitchen #26 数千万レコードをリアルタイムに捌く生鮮EC事業開発」をWeWork 渋谷スクランブルスクエアで開催しました。

イベントにはクックパッドの新規事業「クックパッドマート」を開発するエンジニア、デザイナーが参加し、クックパッドマートの最新の開発状況や組織についていろいろお話させていただきました。今回は当日の様子を発表資料も交えて紹介させていただきます。

クックパッドマートの概要とチャレンジ by 勝間

まず、1番めのトークとして私、勝間からクックパッドマートの事業紹介、および2022年3月現在の事業や組織規模、開発体制などについてお話させていただきました。

外から見るとある意味ほぼ完成されている(?)とも見えかねないサービスですが、実際は既存機能の価値向上や新たな価値づくり、流通の柔軟性の向上など多くの挑戦を行っています。

この日は、その中でも料理の楽しみを広げるための新たな挑戦として、3月にリリースした「グループ」機能の紹介もさせていただきました。

生鮮ECのタイムセールを耐え抜いてきた話 by Leo

2番目のトークは、ECアプリケーション開発グループ長のLeo(@lchin)から、イベントのタイトルでも触れている「数千万の生きたレコード」を、タイムセールというイベントを切り口に、高いパフォーマンスを保ったまま捌くためのトライについてお話させていただきました。

「商品配送先エリアや配送曜日毎に購入可能商品が変わる」「タイムセールの特徴上、負荷が集中する期間が短期間」「データ量もアクセス数も増え続ける」という、事業構造やサービス固有の状況に対して、モニタリングやその分析など、地道に、かつ着実に問題を1つ1つ解決していく事例をいくつか紹介いたしました。

物理世界でモノを運ぶための仕組み by 長

最後のトークは長(@s_osa)より、クックパッドマートの流通の仕組みについてお話させていただきました。

クックパッドマートの流通は、実際の配送こそいくつかの運送会社様のサポートを受けていますが、どのドライバーさんがいつ、何を、どこからどこへ物を動かすかは、自分たちで設計し、仕組みを実現しています。この日は、その中でも流通に必要な商品に貼り付ける「ラベル」や、オペレーションをサポートする「Webアプリケーション」にフォーカスし、私たちなりの工夫についてお話させていただきました。

この日に話せなかったテーマについて、ちょうど開発者ブログにも先日流通の取り組みについて執筆させていただいたので、興味ある方はこちらもご覧ください。

パネルディスカッション

トークの後は、スピーカーの3人にデザイナー統括マネージャー兼、クックパッドマートリードデザイナーの米田(@tyoneda)も加わり、会場にいらっしゃった方からの質問への回答を中心にパネルディカッションを行いました。

実は、質問が全くこなかった場合に備えて、汎用的なトークテーマをいくつか裏で用意していたのですが、実際は「事業構造」「組織カルチャー」「パートナーのアカウント管理」「キャッシュ戦略」...etc など、想定をはるかに超えたいろんなジャンルの質問を多くいただき、用意していたテーマについて話す時間が無かったのは嬉しい誤算でした。同時に、いただいた質問についてもすべてお話しできなかったのだけは残念。。

なお、当日の質問の様子はSli.doのページにアーカイブが残っています。当日回答できなかった質問に対する回答も追記していますので、ぜひあわせてご覧ください。

まとめ

今回のイベントは、クックパッドとしては約2年ぶり(!)のオフラインイベント、またコロナ禍でのイベント開催ということもあり、感染対策に気を配りつつもどうすれば価値のあるイベントを運営できるか、運営側も当日ぎりぎりまでかなり試行錯誤が多かったのは本音です。また、参加される方たちにとっても相当久々のオフラインイベントで、参加すること自体がかなりハードルが高かったと思います。

一方で、イベント前に受付で軽く雑談をさせていただいたり、イベント後に各スピーカーや来場者同士での意見交換をさせていただくなど、オフラインイベントならではの熱量を持った場を久々に取り戻すことができたのは運営側にとっても嬉しい限りでした。

まだまだ段階的ではありますが、今後クックパッドは今回のようなオフラインイベントも少しづつ再開していこうと考えています。イベントの最新情報はTwitterで公開していますので、ご興味ある方はぜひ@cookpad_techをFollowお願いいたします!

また、クックパッドマートはエンジニアを絶賛募集しています。今回のイベントでも話したEC領域、流通領域において、どのような環境で開発をしているか、どのようなエンジニアを募集したいかについてnoteにまとめていますので、中の様子が気になる方は、こちらもあわせて参照いただければ幸いです。

Android App Linksの複数ホスト対応と、Android App Links対応Webサイトに対して複数アプリを関連付けるための知見

$
0
0

こんにちは、クックパッド事業本部 買物サービス開発部の佐藤(@n_atmark)です。 普段はクックパッドiOS/Androidアプリ向けの買い物機能の開発に従事しています。

さて、タイトルにある通り、今回は Android App Linksの話を書こうと思います。

付録: ディープリンク用語まとめ

この記事ではDeepLinkに関連する用語がいくつか登場するため、説明のためにできるだけわかりやすく図示してみました。

DeepLink関連用語の図示

Android App Links とは

Android App Linksの動作イメージ

アプリとウェブサイトURLの両方の所有権を証明することによって自動的にURLインテントをアプリにルーティングする仕組みで、特定のウェブページに関連づけられたアプリがインストールされている場合に、ウェブサイト URL から直接 Android アプリ内の対応コンテンツを開けるようになります。アプリがインストールされていない場合はブラウザ上で表示を継続します。

iOSでは同様の仕組みとして Universal Linksがあります。

Android App Linksを検証する

アプリとウェブサイトURLの両方の所有権を証明するために以下の手順が必要です。

  • アプリの自動リンク検証を有効にするために、AndroidManifest.xml でintent-filterに autoVerify=trueを設定する (これによって intent-filter で使用されているURLドメインに属しているか検証されるようになる)
  • Webサイトとintent-filterの関係を宣言するために、Digital Asset Links JSONファイルを https://domain.name/.well-known/assetlinks.jsonのパスでホストする

詳しくはこちら Android アプリリンクを検証する | Android Developers

Android App Linksの複数ホスト対応と、Android App Links対応Webサイトに対して複数アプリを紐づけたくなった動機

現在私はクックパッドアプリの「買い物機能」の開発に従事しています。これは、生鮮食品ECサービス「クックパッドマート」のインフラを用いたものです。

一方で、クックパッドマートの利用に特化した専用アプリ(以下: マートアプリ)もリリースされています。

クックパッドアプリとマートアプリ

また、レシピサービスクックパッドおよび、生鮮食品ECサービスクックパッドマートそれぞれ cookpad.comおよび cookpad-mart.comというホスト名でWebページを提供しています。

cookpad.com と cookpad-mart.com

クックパッドアプリやマートアプリでは、アプリ外からの流入導線として、Android App LinksやFirebase Dynamic Linksを用いており、下のような経路でアプリを起動することができるようになっています。 *1

既存の外部流入フロー

ここで、クックパッドアプリの買い物機能のことを考えてみます。

Firebase Dynamic LinksやAndroid App Linksを設定する場合、実際にコンテンツを表示しているWebサイトのURLを利用することが多いはずです。

例えば、クックパッド買い物機能で販売しているクックパッドマートの商品などのコンテンツへの流入導線を設けたい場合、Webの商品ページは https://cookpad-mart.com/products/:idで提供しているため、バックエンドの構成を変えずにクックパッド買い物機能への流入導線を作ろうと思うとこのようになります。*2

クックパッドAndroidアプリの買い物機能を考慮した外部流入フロー

クックパッドアプリから見た時に「複数のホスト( https://cookpad.comおよび https://cookpad-mart.com ) に対するAndroid App Links対応」と、「Android App Links対応Webサイト (https://cookpad-mart.com) に対して複数アプリ (クックパッドアプリ、マートアプリ)を関連付ける」場合の挙動や実現方法について調べてみました。

複数のホストに対するAndroid App Links対応

例えば https://cookpad.comおよび https://cookpad-mart.comに対応する場合、その両方を intent-filterに追加する必要があります。

詳しくは複数のホスト用のアプリリンク機能をサポートする | Android アプリリンクを検証するに記載があり、基本的にこのドキュメントに沿って準備をすれば良いのですが、注意点があり

システムは、マニフェスト内のすべてのホストで条件に合致する Digital Asset Links ファイルが見つかった場合に限り、アプリを指定 URL パターンのデフォルト ハンドラとして確立します。 たとえば、次のインテント フィルタを持つアプリの場合、https://www.example.com/.well-known/assetlinks.jsonhttps://www.example.net/.well-known/assetlinks.jsonの両方で assetlinks.json ファイルが検出されなかった場合、検証は失敗になります。

という記述が日本語ドキュメントにあります。 例えばディープリンクに対応する場合も対応したディープリンクURLをintent-filterに記述する必要があるのですが、この表記を見るとintent-filterに追加されている全てのホストでAndroid App Links対応がされていない場合、Android App Linksの検証に失敗してしまうような記述があります。

上記のドキュメントの英語版であるSupporting app linking for multiple hosts | Verify Android App Linksを見ると

For example, an app with the following intent filters would pass verification only for https://www.example.com if an assetlinks.json file were found at https://www.example.com/.well-known/assetlinks.json but not https://www.example.net/.well-known/assetlinks.json

Digital Asset Linksが見つかったホストに関してのみ検証に成功するような文脈になっていて、日本語ドキュメントと文脈がやや違った記述をされています。

実際に試してみたところAndroid App Links対応されていないホストを追加した状態で、Android App Links対応されている https://cookpad.comの流入導線は正常に動作しており、英語版ドキュメントの記述が正しそうでした。

Android App Links対応Webサイトに対して複数のアプリに関連付ける

1つのウェブサイト ( https://cookpad-mart.com ) に対してマートアプリとクックパッドアプリ両方を関連づけるためにはDigital Asset Linksに二つ分のアプリ情報を記載する必要があります。こちらもドキュメントが用意されており、ドキュメントに沿って準備をしていきます。

[{"relation": ["delegate_permission/common.handle_all_urls"],
    "target": {"namespace": "android_app",
      "package_name": "com.cookpad.android.martec",
      "sha256_cert_fingerprints": ["FF:AE:BE:09:C7:A8:E3:D5:43:BD:89:21:2B:45:6D:28:DA:24:26:1A:24:88:70:DF:E1:0D:2D:AF:44:5E:BD:15"
      ]}},
  {"relation": ["delegate_permission/common.handle_all_urls"],
    "target": {"namespace": "android_app",
      "package_name": "com.cookpad.android.activities",
      "sha256_cert_fingerprints": ["14:D6:67:33:06:3D:79:53:31:35:3B:08:0C:C5:03:60:F6:96:8F:58:28:45:65:C5:73:6D:5F:89:C7:6D:2F:B8"
      ]}}]

https://cookpad-mart.com/.well-known/assetlinks.jsonに上記のようなDigital Asset Linksを配置しました。ここでは専用アプリであるマートアプリを先頭にしています。

Android App Links対応Webサイトに対して複数のアプリに関連付ける知見

ここで一つ気になる点があります。 https://cookpad-mart.comに対してクックパッドアプリとマートアプリ二つを紐づけた状態で、両方のアプリがインストールされている場合一体どのような挙動になるのでしょうか。 私たちとしては、あくまでもクックパッドマートのWebサイトなので、マートアプリがインストールされている場合、常にマートアプリが優先度高く開かれる状態になってるのが望ましいと考えており、理想の挙動になっているかどうか気になりました。

挙動の確認のために、クックパッドアプリ/マートアプリがそれぞれインストールされている状態でマートのWebページを開いてみました。

また、Android 12以降ではAndroid App Linksのドメイン検証に関しての変更が入っているため、Android 12未満とAndroid 12以降でそれぞれ試してみました。

詳しくは ウェブ インテントの解決 | Android 12

Android 12未満 Android 12以降
マートアプリのみインストールマートアプリが開くマートアプリが開く
クックパッドアプリのみインストールクックパッドアプリが開く開かない
クックパッドアプリ→マートアプリの順でインストールマートアプリが開くマートアプリが開く
マートアプリ→クックパッドアプリの順でインストールクックパッドアプリが開くマートアプリが開く

ここまでの結果より

  • Android 12未満だと、一番直近インストールされたアプリがデフォルトで開く
  • Android 12以降だとクックパッドアプリのみインストールされている状態でもクックパッドアプリが開かない(assetlinks.jsonの一番先頭に記述されたアプリのみがデフォルトでウェブインテントを紐づけられる権限を持っている)

ということが分かりました。

また、Android 12未満において直近インストールされたアプリがデフォルトで開くのですが アプリ > (ウェブインテントが紐づいた)アプリ詳細 >規定で開く >このアプリ対応のリンクを開く「毎回確認」 にした状態で、

Android App Links対応されているページを開くと、該当ページを開くアプリケーションの選択ダイアログが表示されます。

ここでウェブインテントを紐づけたいアプリを選ぶことで、ユーザー設定によって紐づけるアプリを変えられることも確認できました。

Android 12以降の端末に関しては、クックパッドアプリのみインストールされている状態でも「サポートされているリンク」はデフォルトでオフになっていました。

これを、明示的にオンにした場合のみクックパッドアプリが開くようになることが確認できました。

また、クックパッドアプリでサポートされているリンクで cookpad-mart.comをオンにした場合でも後からマートアプリを入れると、 cookpad-mart.comに対するウェブインテントの紐付けはマートアプリに移ることが確認できました。

Android 12以降に関しては原則 assetlinks.jsonの記述順に優先度が割り当てられていそうなことが分かりました。

まとめ

今回は Android App Links の複数ホスト対応と、Android App Links対応Webサイトに対して複数アプリを関連づけるための知見について紹介しました。

こういったケースは実際はそう多くは発生しないはずで、私たちも結果として今回の方法は採用しなかったのですが、調査した結果として分かった開かれるアプリの優先度や条件などは知見としてなかなか世間に出回っていないはずなので、今後似たようなケースに遭遇したエンジニアの役に立てれば幸いです。

最後になりますが、クックパッドではAndroidエンジニアを大募集しています。

カジュアル面談や学生インターンシップなども随時実施していますので、ぜひお気軽にご連絡ください。

info.cookpad.com

【宣伝】6/6 19:30より、買い物機能のAndroid開発に携わっているエンジニア2名が雑談形式で話すイベントを開催します。

cookpad.connpass.com

*1:実際には2022年5月時点ではクックパッドマートアプリではAndroid App Linksを使っていませんが、説明を簡単にするために記載しています

*2:例えば https://cookpad.com/kaimono/products/:idのようなルーティングを用意して https://cookpad-mart.com/products/:idへのリダイレクトを実装したり、https://kaimono.cookpad.com/products/:idのようなAndroid App Linksに対応したサブドメイン上にルーティングを用意してhttps://cookpad-mart.com/products/:idへのリダイレクトを実装するなどの方法を取れば、今回のようなケースは発生しない。

XcodeでSwift Package Manager実用段階

$
0
0

こんにちは、モバイル基盤部のヴァンサン(@vincentisambart)です。

Swift Package ManagerはAppleがXcodeで公式にサポートしている唯一のパッケージマネージャーです。Xcode公式サポートの他に、Swift Package Manager形式でのみ提供されているswift-algorithmsswift-atomics、将来的に期待されているswift-async-algorithmsといった準標準ライブラリを利用できるようになるという大きなメリットがあります。

クックパッドiOSアプリ(以下クックパッドアプリ)で一部の依存パッケージをXcodeのSwift Package Manager対応を使って入れるようにしました。この導入で得たいくつかの知見をまとめました。

XcodeのSwift Package Manager対応

本来のSwift Package ManagerがSwiftプロジェクトの一部です。コマンドラインでswift packageで使えます。活用するプロジェクトの構成をPackage.swiftで定義します。

ですが、今回話したいのはSwift Package ManagerのパッケージをXcodeのプロジェクトで使う時の話です。依存されているパッケージのプロジェクト構成がPackage.swiftで定義されていますが、メインのプロジェクトの構成がXcodeプロジェクト(xcworkspaceまたはxcodeproj)で定義されています。

Swift Package Managerがオープンソースであるのに対し、Xcode側の実装はオープンソースのSwift Package Managerの一部を使いながらもクローズドソースです。

プロジェクト構成の定義がPackage.swiftではないので、swift packageコマンドが使えません。Xcode内ではプロジェクト設定のPackage Dependenciesタブで依存パッケージを変更できます。

ターゲットの設定では、Build PhasesのLink Binary with Librariesに使いたいパッケージのライブラリを指定します。

また、XcodeのFile > Packagesメニューにキャッシュリセットやパッケージ更新用のコマンドがあります。

パッケージ自体はディレクトリ構造をSwift Package Managerの期待した構造に合わせると、Package.swiftが割りと作成しやすいと思いますが、もっと詳しく説明すると長くなるので今回はしません。1つだけ不自然な点を挙げると、なぜかPackage.swiftで未対応なOSを指定できないようです。platformsiOSだけを指定しても、macOSが未対応になるわけではなく、macOSに関して最低OSバージョンがデフォルトのものになるだけです。

最近までの流れ

以前からクックパッドアプリでSwift Package Managerを導入をしようとしていました。hiragramの記事で説明されているように2020年12月に試みましたが、クックパッドアプリで使える状態にまだ達していなかったため断念しました。

2021年12月リリースのXcode 13.2.1では、複雑な依存関係だと手元でのビルドに問題なかったが、AdHocやApp Store配布用のエキスポートが失敗していました。ですが、先月2022年4月にリリースされたXcode 13.3.1でXcode 13.2.1で起きていた問題が解消されて、クックパッドアプリでついに使えるようになりました。

社内ライブラリで使うには

最初に試したとき、GitHub.comなどに公開されているパッケージはXcodeのデフォルト設定でSwift Package Manager対応を利用してビルドできましたが、社内ライブラリではうまく動きませんでした。これだと導入のメリットがだいぶ限られてしまいます。

Xcodeが非公開レポジトリを取得するとき、デフォルト設定では、~/.sshに入ったシステム標準の設定が使われるのではなく、Xcode独自の仕組みが使われます。クックパッドでは、リモートで働くとき、VPNまたはSSHトンネル使う必要があります。開発者のマシンでシステムのSSHが既に設定されていますし、複雑なSSH設定はXcode独自の仕組みでできないので、XcodeがシステムのSSHの設定を使ってほしかったです。幸いなことに、このような設定があります。すべての開発者の手元で以下のコマンドを実行すればXcodeの設定を変えられます。

defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES

この件に関するネットでの記事を見ると、上記のコマンドの頭にsudoが入った記事もありますが、僕の試した限りでは逆にsudoが入っていると効果がありませんでした。YESの代わりに1を使っても問題ありません。

すべての開発者の手元で実行されるようにしなければいけないのは不便なので、プロジェクト作成、環境設定、依存パッケージインストールのようなスクリプトがあれば、そこでやるのがおすすめです。

# 現在の設定を確認する
# (`|| true`は`set -e`を使ってもエラーにならないため)
using_system_ssh=`defaults read com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM 2> /dev/null || true`
# まだ設定されていない場合のみ設定を変える
if [ "$using_system_ssh" != "YES" ] && [ "$using_system_ssh" != "1" ]; then
  defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES
fi

Package.resolved

パッケージマネージャーはユーザーが基本的にどういうパッケージのどういうバージョンを使いたいのか指定しますが、バージョンの指定は固定にできるとはいえ、メジャーバージョンが変わらなければ更新しても良いこともよくあります。

パッケージマネージャーは指定を満たすバージョンと明記されていないけど依存されているパッケージを解決します。Swift Package Managerでは、解決されたパッケージのリストとそれぞれのバージョンがPackage.resolvedというJSONファイルに入ります。全ての開発者がそれぞれのパッケージの同じバージョンを使わないとややこしいので、アプリはこのファイルをレポジトリに入れるのを強く推奨します。CocoaPodsでいうとPodfile.lock、CarthageでいうとCartfile.resolved、RubyGemsでいうとGemfile.lockと同じ役目です。

本来のSwift Package Managerでは、Package.resolvedPackage.swiftと同じディレクトリに入りますが、Xcodeではメインで使われているのがxcworkspaceかxcodeprojかによってディレクトリが少し違います。xcworkspaceの場合、MyProject.xcworkspace/xcshareddata/swiftpm/Package.resolvedですが、xcodeprojの場合、MyProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedです(もちろん「MyProject」は自分のプロジェクト名に置き換えます)。

xcworkspaceの場合、必ずxcworkspaceを開けるように気を使いましょう。xcodebuildもいつもxcworkspaceを指定するように。

Xcodeでパッケージがうまく取得できていない時

Xcodeでプロジェクトを開く時、必要であればXcodeがプロジェクトを取得しようとしますが、それが失敗する時はたまにあります。そういう時、XcodeのメニューにあるFile > Packages > Reset Package Cachesがおすすめです。

xcodebuild

Xcodeのユーザー体験はXcode自体のUIがメインですが、CIやスクリプトは基本的にxcodebuildを使います。xcodebuildとXcodeのSwift Package Manager対応絡みで気を使う必要のある点がいくつかあります。

SSH認証

Xcode自体と違って最近xcodebuildがデフォルトでシステムのSSH設定を使うようになっています。念のために明記したい場合、-scmProvider systemでできます(SCM=Source Code Management)。(逆にXcode独自の認証方法は-scmProvider xcodeで指定できます)

依存パッケージ解決

Package.resolvedがない場合、依存パッケージ解決がまず依存パッケージやバージョン指定を見てPackage.resolvedを生成してくれます。Package.resolvedの情報があれば、それに従って依存パッケージ解決が必要なパッケージバージョンを取得してくれます。

Xcodeでプロジェクトを開く時やxcodebuildでビルドをする時に必要であれば依存パッケージ解決が自動的に行われるが、xcodebuildで依存パッケージ解決だけをしたい場合以下のコマンドでできます。

xcodebuild -resolvePackageDependencies -workspace MyProject.xcworkspace -scheme MyProject

明確でありたければ-scmProvider systemを指定できますし、取得されたパッケージの保存先を-clonedSourcePackagesDirPathで指定できます。

キャッシュ

クラウドで動くCIはスピードを出すにはキャッシュをうまく活用するのが大事です。xcodebuildで依存パッケージの取得を簡単にキャッシュできます。上記に説明されたようにxcodebuildで依存パッケージ解決を実行して、-clonedSourcePackagesDirPathで指定されたディレクトリをキャッシュすれば良いです。その後xcodebuildでビルドコマンドなどを実行する際、改めて-clonedSourcePackagesDirPathで同じディレクトリを指定するのをお忘れずに。

Package.resolvedの更新

XcodeのメニューにFile > Packages > Update to Latest Package Versionsでパッケージを更新できますが、現時点でxcodebuildにこういう機能がありません。

クックパッドアプリは毎週自動的に全てのパッケージを更新してPRを出すCIジョブがあります。このジョブのためxcodebuildでパッケージを更新する方法が必要でした。

更新のコマンドがないとはいえ、少し強引ではありますが、方法がないわけではありません。Package.resolvedがなければ、依存パッケージ解決が最新パッケージを取りに行きます。試してみると、キャッシュがあれば最新のバージョンではなく以前と同じバージョンになってしまうのでxcodebuildがキャッシュを見に行かないようにする必要があります。

# Package.resolvedを消す
rm -f MyProject.xcworkspace/xcshareddata/swiftpm/Package.resolved
# 空っぽなテンポラリディレクトリを作成する
tmpcache=`mktemp -d`
# 依存パッケージ解決をやる。残っていたキャッシュが使われないために、キャッシュディレクトリに作ったばかりのテンポラリディレクトリを指定する
xcodebuild -resolvePackageDependencies -workspace MyProject.xcworkspace -scheme MyProject -scmProvider system -clonedSourcePackagesDirPath "$tmpcache"
# テンポラリディレクトリを消しておく
rm -rf "$tmpcache"
# Package.resolvedに指定を満たすパッケージの最新のバージョンが使われるようになったはず

XcodeGenを使う場合気をつけること

以前giginetが説明したようにクックパッドアプリのプロジェクト構成はXcodeGenで定義しています。

プロジェクト構成を直接xcworkspaceまたはxcodeprojで定義する場合、Xcode内で依存関係に変更を加えるとPackage.resolvedが自動的に更新されますが、XcodeGenのプロジェクト設定を変えるだけでPackage.resolvedが更新されません。依存関係に変更を加えて、XcodeGenを実行してから、Package.resolvedを更新するにはXcodeを開くかxcodebuildで依存パッケージ解決をするか、が必要がです。気をつけないとPackage.resolvedの変更が入っていないPRを出すリスクが出てしまいます。

また、最近クックパッドアプリでライセンス管理にLicensePlistを使用していますが、LicensePlistが見ているのもPackage.resolvedです。Package.resolvedが更新されていないとLicensePlistの生成したファイルもプロジェクトファイルに入った依存関係設定と一致しません。

プロジェクト生成のスクリプトではXcodeGenを実行してからLicensePlistを実行していましたが、上記の理由で、LicensePlistを実行する前にxcodebuildの依存パッケージ解決をするようにしました。パッケージ解決が既に完了している場合プロジェクト生成スクリプトの実行時間が2~3秒伸びるのは少し残念ですが、分かりにくい状態を避ける方が大事だと思います。

他のパッケージマネージャーとの絡み

クックパッドアプリはCocoaPodsとCarthage両方を使っていましたが、ライブラリが複数なパッケージマネージャーをサポートしている場合、ライブラリを導入するときはどれを使うべきか悩んでしまいます。特定なパッケージマネージャーに寄せた方が運用しやすいです。

最近Appleプラットフォーム開発界隈が依存関係をSwift Package Managerに寄せつつあるように感じるので、クックパッドアプリもできるだけSwift Package Managerに寄せることにしました。

別のパッケージマネージャーからSwift Package Managerに移行するとき、ライブラリがPackage.swiftを既に提供していると、アプリのプロジェクトに参照のやり方を変えるだけのはずですが、他の変更も必要になることがあります。

例えば、Carthageを使って作成されたxcframeworkからSwift Package Managerに移行したら、クックパッドアプリでimportを足す必要があったSwiftファイルがありました。モジュールの扱い方の違いで、Carthage版だとライブラリをimportするだけでFoundationやUIKitが暗黙的にimportされることがありました。Swift Package Manager版だとそんなことがないので、そのimportを明記する必要があります。

また、クックパッドアプリのようにアプリが複数のモジュールで構成されている場合、パッケージマネージャーを変えることで、どのモジュールがどのパッケージを参照するのか少し変える必要があることがあります。基本的にSwift Package Managerの場合、参照をもっと多くの箇所で明記する必要がありました。

パッケージマネージャーとの絡みでのハマりどころといえば、XcodeのSwift Package Manager対応とCocoaPodsを併用している場合、XcodeでターゲットのBuild PhasesのDependenciesにSwiftパッケージが明記されていると、CocoaPods実行時に例外が発生してしまいます。CocoaPodsが使っているXcodeprojライブラリのバグです。修正はしましたが、現時点でこの修正が入ったXcodeprojのバージョンがまだリリースされていません(現時点で最新のリリースが昨年8月にリリースされた1.21.0)。ワークアラウンドとして、SwiftパッケージがターゲットのBuild PhasesのLink Binary with Librariesに入っていれば、Dependenciesに入っていなくても暗黙的に依存されるのでLink Binary with Librariesから消せば良いはずです。一応Xcodeprojの特定なコミットハッシュに依存するのも選択肢の1つです。

最後に

課題点がいくつかありましたが、それを越えればXcodeのSwift Package Manager対応が割りと便利だと思います。パッケージ作成はCocoaPodsと違ってスペックをどこかにアップロードする必要ありませんし、Package.swiftでパッケージ構成定義も楽です。

クックパッドアプリでは少しずつSwift Package Managerの利用を増やそうとしています。今週のリリースでCarthageを使わなくなってSwift Package Manager + CocoaPodsの構成になりました。


CookpadTV の開発スタイルとエンジニアリングマネージャーの役割

$
0
0

メディアプロダクト開発部の長田(@osadake212)です。
私の主な仕事は、ライブ配信サービスの cookpadLive などを運営しているクックパッドグループの CookpadTV 株式会社のサービス開発をすることです。CookpadTV ではサービス開発部の部長としてエンジニア組織の運営を通してサービス開発に関わっています。
本記事では、CookpadTV で取り組んでいることと、そこでのエンジニアチームの動き方や、エンジニアリングマネージャーの役割についてざっくりお話しします。

CookpadTV?

CookpadTV はクックパッドグループで動画関連サービスを提供している会社で、2018年4月に設立されました。もともとはクックパッドの事業部の一つだったのですが、素早く意思決定し多くのチャレンジをするために分社化しました。

CookpadTV は料理が持つ人を幸せにする力を信じていて、今まで良いきっかけがなくて料理を楽しめていない人たちに向けて、既存の考え方に囚われない新しい切り口でサービスを提供することで、私たちが信じている価値を届けることにチャレンジしています。
そんな私たちが運営しているサービスの一つに cookpadLive というクッキング Live アプリがあります。このサービスは 2018年3月にリリースされ、本記事の執筆時点で5年目に突入しました。

AppStore
GooglePlay

cookpadLive ではアイドル・声優・お笑い・俳優など、さまざまなカテゴリの著名人のクッキング Live を視聴することができます。今まで料理をする良いきっかけがなかった人たちにむけて、自分の推しを通じて料理の楽しさを届けており、Live 配信前後の Twitter の反応を見ていると、「普段は料理をしないけど、〇〇さんの配信を見て作ってみました!」のような投稿を見かけることがあります。普段料理をしない人にとって料理をするという行為はとてつもなくハードルが高いものですが、そのハードルを少しでも下げるきっかけを与えることができています。

今年の1月には「cookpadLive cafe 表参道」をオープンしました。自社の配信スタジオにカフェが併設されていて、配信の様子を観覧しながらキャストと同じ料理を食べて楽しむことができる体験ができるスペースになっています。
有名人に会えるLive観覧カフェ「cookpadLive cafe」が東京出店! CAMPFIREにてクラウドファンディングを開始!

またこのカフェでは様々なアニメ作品とコラボした「AniCook」という企画が開催されており、コラボカフェ史上最高クオリティの「美味しさ」に挑戦し、アニメファンの皆さんはもちろん、アニメの版元企業からも大絶賛いただくメニューを次々に生み出していこうとしています。

サービス開発部の様子

2021年の開発の様子

サービス開発部には現在、私を含め5人のエンジニアが在籍しています。CookpadTV では大小含め多くのサービスを展開していて、50以上のサーバーアプリケーション(staging 環境や社内マイクロサービスを含む)と 9つの iOS/Android アプリケーション(独自 MDM 配信を含む)を開発・保守しています。

エンジニアの人数に対して管理しているアプリケーションの数が多いので、サーバー・クライアントのどちらの開発もできる人材が多いのがチームの特徴です。
とはいえ、並行して開発できるプロジェクトには限りがあるので、優先順位を決め、事業的にチャレンジする価値が高いものから順番に開発に取り組んでいます。

私の部では半年に一度振り返りのタイミングを設けていて、半年間でできたこと、できなかったこと、これからやりたいことの認識合わせをしています。
以下はその振り返り会の2021年の資料の抜粋なのですが、チームで開発・リリースしたものについてまとめたものです。

この他にも細かい修正や小規模な開発は常に行われており、同時期に複数の開発プロジェクトが走っている状態です。

各プロジェクトの開発の流れ

普段の開発は以下のような流れで行われています。

  • アイディア出し
  • 要件整理
  • 仕様整理
  • 設計
  • 実装
  • テスト
  • リリース
  • 運用

特別珍しい開発工程が含まれているわけではないのですが、全ての工程にメンバーが関わっています。エンジニアの人数もそうなのですが、開発・配信ディレクターやデザイナーの人数も多くはないので、開発メンバーそれぞれの守備範囲が広いのが特徴です。

開発サイクルは1週間を区切りとしていて、以下のような定例をぐるぐる回しています。

  • 各サービスの意思決定者が集まる定例(ディレクター会)
  • 決定事項の共有と1週間の開発予定を計画する定例(開発定例)
  • プロジェクトのフェーズ毎に必要に応じて打ち合わせ(分科会)

この他にも短い進捗報告の場があったりするのですが、基本的にはこれらの会を軸に開発を進めています。

ディレクター会はその名の通り、開発ディレクターが集まってアイディア出し、問題報告、要件、仕様整理、開発の進捗報告、などを行っています。プロジェクトによってはこの会でエンジニアがオーナーとなり、そのプロジェクトを運用まで持っていくことに責任を持っています。

例えば、運用フェーズでも運用者に渡す前・渡した後にエンジニアが運用することがあり、自分たちで作った運用フローが業務を圧迫していないかや、事故を未然に防ぐことができているかなどを確認しています。
具体的には、実際に Live 配信の現場に立ち会って、急にトラブルが起きたらどうするかをイメージしてシステムを設計したり、 Live 配信中に大量に販売した商品を梱包して発送する作業をして、発送ミスが起きないようにするにはどういう仕組みが必要なのかを考えたりしています。

開発定例会はエンジニアが1週間をどう過ごすかを計画する場として運用しています。
ディレクター会で展開された情報をキャッチアップしつつ、1週間のタスクをどういう温度感で取り組むべきなのかをそれぞれの開発メンバーが意識できるようにしています。
また、システムで発生したエラーの振り返りや AWS のコスト振り返り、障害が発生したら障害の振り返りなども行い、システム面でも対応漏れが起きてないかを確認したりしています。

分科会は開発工程に応じてさまざまな会が都度開催されています。
設計前雑談、設計レビュー、Pull Request 補足説明、動作確認会、使い方説明会、夕会など、さまざまです。設計は Google Docs を使って情報共有をすることが多いです。

プロジェクトによって内容は変わるのですが、だいたい上の図のオレンジで囲んだような項目をプロジェクトをリードする人がまとめて設計レビューで展開します。

この設計レビューを開催する前に設計前雑談というのもやっていて、準備なしで「さぁどうしますかねー」から話始める短い打ち合わせをやっているのも特徴です。

エンジニアリングマネージャーの仕事

エンジニアリングマネージャーとしては上述の仕事を視座・視野・視点を変えて取り組む必要があります。
開発だけじゃなく、マネジメント・経営・ビジネスについて理解できていないことや、そもそも知らないことも本当に多いのですが、この会社・このチームのエンジニアリングマネージャーとして、私が普段から心がけていることを紹介します。

社内で起こっていることをちゃんと把握する

CookpadTV はエンジニア以外にも多くの役割の社員が在籍しており、 cookpadLive をはじめ、全てのサービスは他の事業部の人と協力して開発・運営しています。すごく当たり前のことを書いているのですが、最近になって改めて重要なことだと気付かされています。

CookpadTV のように複数の新規事業を立ち上げたり、立ち上げた事業を成長させようとしてるフェーズの企業の開発では、開発するものの仕様・タイミング・優先順位が重要になります。
事業計画に沿って新しいチャレンジをするためにはいつまでにどういう検証・機能が出来ている必要があるかを把握していないと、その時に必要なシステムや仕組みが何なのか、将来発生するリスクや開発は何なのかを判断することができないはずです。
この判断を大きく間違えてしまうと、せっかく開発したサービスを使うタイミングがなかったり、マネタイズの機会損失をして事業を撤退しなければいけなくなったりします。

CookpadTV の開発スタイルでは、エンジニアが要件・仕様整理することが大小問わずあるので、上で述べたような事業の状況に合わせた判断が必要になるのですが、これを開発メンバー全員が行うのは無理があります。マネージャーとしては、いろんな情報をキャッチアップしておいて、できるだけこの判断がいい感じにできるように心がけています。
例えば、ユーザー向けの新機能は Live 配信の企画に沿って運用できるものになっているのかを配信ディレクターの様子から伺ったり、季節のイベントに合わせた大型企画を最大限成功させるためにできる工夫がないかを探ったり、トライアルに間に合わせるために作った暫定的なシステムを用意した時に発生しうるイレギュラー対応を先回りして仕組み化しておくべきものは無いかを探ったりしています。

こういう情報は、隣の席で何気なく話されていることから分かるものもあったりするので、一見無駄かもしれないようなことでも隙があれば積極的に関わるようにしていて、オフィスでオープンに行われている打ち合わせに聞き耳を立てたり、なんとなく雑談していることから読み取れることはないかを意識しています。

この仕事は誰のどういう成長・成果に繋がるかを考える

プロジェクトがあったとして、それを誰がどのように取り組むのかをアサインする権限を私は持っています。単純にチームの最高速度が出せるように開発リソースをアサインする場合もありますが、期初にチームの目標を設定するときに、その人の Will・Can・Must を整理しているので、基本的にはそこに合うように計画を立てます。

例えば、「システム全体の設計するスキルを高める」という Will を持った人がいれば、その1年の山場となりそうなプロジェクトを任せる前に、早めに小さいプロジェクトから素振りをしてもらったり、「プロジェクトの管理をする」という Can を身につけて欲しい人がいれば、他部署の打ち合わせに同席してもらったりと、単純に他の人がやったほうが早いものであってもチームの成長・成果に合わせて仕事を設計しています。

ちょっと話は逸れますが、やる気があってモチベーションが高い人が一番成長するというのを実感しているので、「どうしてもこの仕事やりたいけど、今着手しているものが終わってない...」みたいな状況の人がいたとして、その仕事をやったほうが成長に繋がると感じた場合は、今着手しているものをなんとか整理して仕事を引き剥がしたりもします。

まぁこのへんの判断を助けてあげるのがマネージャーの仕事かなーと思ってます。

最後は自分がなんとかするという覚悟をする

これは結構大事だと思ってます。自分ができることは限られていますが、やれる範囲のことはなんとかするし、範囲外のことはどうしたら解決できるかを考えます。
Live 配信中に現場でトラブルが起きても、全然知らないシステムで大障害が起きても、自分の守備範囲外のスキルを持ったメンバーが辞めても、最後の砦としてやっていく気持ちが必要です。(でも実際は、真の最後の砦が後ろに何個かありますw)
そういう気持ちで取り組んでいると自然と守備範囲が広がりますし、自分の成長にも繋がるし、チーム的にも強くなっていくはずです。

ちょっと話は逸れますが、逆にこれは課題になってると感じることもあって、自分の能力の上限がチームの能力の上限になってしまいます。自分が見えていることをうまく伝えつつ、この課題が解消されるように試行錯誤しています。言いたいことは「マイクロマネジメントは悪か?よりよい組織をつくるためのマネジメント形態についての考察」という同僚の新井(@SpicyCoffee66)さんの記事にまとまってるので見てみてください。

キャリアについて

「キャリアについて」とかいう、また釣り針の大きい話をしてしまう割には面白いことは話せないのですが、「どう作る」のかではなく「何を作る」のかが重要だと思っています。
何を今更当たり前のことを言ってるんだ、と思われる方も多いかと思うのですが「技術は目的ではなく手段」というのを CookpadTV の開発を通して痛感していて、そういう志向のエンジニアは、マネージャーというキャリアは非常に財産になるんじゃないかと思っています。

一般的に、マネージャーになるとコードを書く時間がなくなってしまう、とか、技術力のないマネージャーは使えない、とか言われるのでとてもネガティブなイメージがあるかと思います。(し、それが間違ってるとも思わないです。)
実際のところ一般的な開発業務と比較すると、プロジェクトの進行管理、障害・サポート対応、他の事業部のキャッチアップ、メンバーの成長の設計、採用活動などの業務の優先順位が上がってしまうので、システムの設計や開発から離れがちになってしまうのですが、既存システムの設計や実装に携わらない期間が長くなったり、最新技術のキャッチアップを怠ったりすると、すごいスピードで置いて行かれている感覚があります。
なので、バランスをとりながら仕事をする必要があります。

幸いにも、私のチームは同じメンバーで長く働けていて、マネジメントコストが高くないことや、CookpadTV の方針である、やらないことを潔く決めてチャレンジすることに重きを置いていることや、各個人の強みを活かしてチャレンジすることが推奨されていることなどのお陰で、私自信も継続的に開発に関わることができています。
2021年に私が作った pull request は 489 件で、1日1回は少なくとも何かしらコードを書くことを心がけていました。

かなりタフネスが求められる仕事ではありますし、がっつり設計したり実装したりする時間はとりづらいのですが、「何か」をチームで生み出す筋肉が鍛えられているのを実感できていて、その経験は普遍的なものになり得ると感じているので頑張ってます。
クックパッドにはこういうタイプのエンジニアもいるんですよ、というのが少しでも伝われば幸いです。
まぁ嫌になったらまたメンバーに戻る選択肢も無いわけではないですし、実際クックパッドにはそういうキャリアを歩いて活躍している人が何人かいます。

さいごに

クックパッドではエンジニアを募集しています。特に、自分の守備範囲を超えて活動するのが苦じゃない人は大活躍できる環境があります。そこに興味がある方はまずカジュアルにお話ししましょう。お待ちしています!
@osadake212
クックパッド株式会社 | クックパッド株式会社 採用サイト

モダンウェブフロントエンド勉強会を開催しました

$
0
0

こんにちは、クックパッドで最近はモバイルアプリを離れもっぱらウェブアプリを作っている @morishinです。

先日、社内で「モダンウェブフロントエンド勉強会」と題して React, Next.js, Core Web Vitals, SSR, CSR, SSG, ISR, SSR Streaming, React Server Component といったキーワードに触れつつ、昨今のウェブ開発事情について話をしました。せっかくなのでその内容の共有と、勉強会を開催した動機などを紹介したいと思います。

背景・動機

クックパッドのウェブアプリケーションは10年以上もの間 Ruby on Rails で開発されてきましたが、2020年から一部のページは Next.js のアプリケーションがホストするようになりました。具体的な構成については次の記事をご覧ください。

techlife.cookpad.com

Next.js を使うようになると全てのビューは React コンポーネントで記述し、スタイルも JavaScript で記述するようになり、これまでの Rails での開発とは大きく異なるものになりました。それに加えてパフォーマンスを意識する必要性も増し、React の再レンダリングの回数を気にしたり、SSR (サーバーサイドレンダリング) を活用したり、バンドルサイズを気にしたりしなければならなくなりました。Rails の時は気にしなくてよかったというわけではないですが、ブラウザで実行される JavaScript がぐっと増えたことや、開発者の工夫の余地が増えたことで、パフォーマンスについて考えることが増えたのを実感しています。

そんな中 React 18 や Next.js 12 という大幅なアップデートがやってきて、このアップデートの内容や背景を理解するには必要な前提知識が多すぎると思ったため、自分の理解を補強する目的も込みで勉強会を開催しようと考えました。

内容

というわけで内容としては「React 18 や Next.js 12 の新しい提案の裏にある動機を理解する」をゴールに置き、そのためにウェブ技術のこれまでの進化を順を追って紹介していくというものにしました。資料のみで恐縮ですがここに貼っておきますので、興味のある方はご覧ください。

資料 URL: https://static.cookpad.com/techlife/202205-web-frontend-study/2

喋った内容の情報源はこのあたりです。資料の内容が疑わしかったりわからないところがあればこちらの一次情報源もご参照ください。

社内勉強会

クックパッドではエンジニアがしばしば野良勉強会を開催していて、最近だとこれの他に GraphQL 勉強会をやったりしました。他の人がやっていたのだと「RBS勉強会」「検索勉強会(社内の検索システムが対象)」「Production Ready GraphQL輪読会」などが開催されているのを見かけました。勉強会がある会社って楽しいですよね。そう思って自分も積極的に開催しています。

クックパッドでは技術を学んだり好きな技術について早口で喋ったりするのが好きなエンジニアを募集しています!クックパッドのエンジニアと早口オタクトークをしたい方はカジュアル面談でもどうですか。下記の Meety からご連絡いただけます。(僕もいます)

meety.net

クックパッドで働くことに興味を持ってくださった方はこちらもご覧ください。

info.cookpad.com

Fluentd 集約ノードのオートスケール

$
0
0

こんにちは、技術部 SRE グループ アルバイトの小川です。この記事では、クックパッドでコンテナログの処理に利用している Fluentd ノードのオートスケール対応について紹介します。

クックパッドでは Amazon ECS を用いてコンテナ化されたアプリケーションをデプロイしています。クックパッドでの ECS の利用については過去の記事をご覧ください。

ECS 上で動くコンテナのログを閲覧するために、標準的には Amazon CloudWatch Logs を利用する方法があります。しかし、クックパッドではログ量やコストの問題で CloudWatch Logs は利用せず、独自のログ配送基盤を構築して運用しています。具体的には、ECS のコンテナインスタンスで実行している Fluentd から複数の Amazon EC2 インスタンスで構成される Fluentd 集約ノードにログを転送し、最後に Fluentd 集約ノードが Amazon S3 にログを圧縮して転送、最終的に Amazon Athena 経由でログが閲覧できるようになっています。この仕組みについては Cookpad Tech Kitchen #20 での Amazon ECS の安定運用についてのスライドが詳しいです。

ECS コンテナインスタンスと Fluentd 集約ノード

各コンテナインスタンスから直接 S3 にログを転送するのではなく集約ノードを挟んでいるもっとも大きな理由は、コンテナインスタンスを跨いだバッファリングが必要だからです。コンテナインスタンスから出力されるログの合計は平均して約 60MB/sec (150TB/month) にものぼり、これをそのまま S3 に格納することはコスト上の観点から好ましくありません。そのため gzip 圧縮をおこなっているのですが、各コンテナインスタンスの中で圧縮をして直接アップロードを行うと仮定するとログ配送が遅延するかコストが嵩むかのどちらかを選ぶ必要に迫られます。すなわち、各コンテナインスタンスでバッファを大きくとるとログ配送が遅延してしまいますし、バッファを小さくとると圧縮率が下がってストレージコストが嵩み、また集積率が下がってオブジェクト数が増えリクエストコストが嵩んでしまいます。集約ノードを用意してそこで圧縮とアップロードを行うことで、遅延の少ないログ配送とコスト削減を両立しています。現在ログの遅延は最大 2 分に、S3 のデータ増加は約 5MB/sec (14TB/month) に抑えられています。

さて、ここで問題になるのが Fluentd 集約ノードの負荷です。すべてのコンテナインスタンス*1からログが流入し、先に紹介したとおり負荷が大きくなるのに違いはないのですが、時間帯によってログ量が大きく異なっています。下にログ量のメトリクスを示します。サービスへのアクセスが増加する夕方は 150MB/sec ほどのログがありますが、夜間は 20MB/sec 程度までログ量が減っています。

Fluentd 集約ノードに流入するネットワークトラフィック量

これまで、Fluentd 集約ノードはピークタイムの負荷に耐えることができるよう 8 台構成で動いていました。しかし、夜間や早朝などピークタイムを外れると 8 台もインスタンスは必要ありません。こういった状況から、オートスケールによって必要のない時間帯でインスタンス数を減らすことで半分程度のコスト削減が見込まれます。Fluentd 集約ノードができた当時は sd_srv プラグイン(後述)がなかったためオートスケールが困難だったのですが、現在はそういった問題もないため、コスト削減のためにオートスケールを行うことにしました。

オートスケールのために必要なこと

前節の図からわかるとおり、複数ノードに対するログ転送におけるロードバランスは転送ノード(コンテナインスタンス)側で行われています。これは Fluentd の out_forward プラグインによって実現されていますが、そのままでは転送ノードの Fluentd 設定ファイルに直接転送先サーバーのリストを記述することになります。一方でオートスケールを行うためには転送先のサーバーのリストは転送ノードの外側から動的に管理する必要があります。Fluentd 集約ノードの仕組みが整備された当時はこの部分を上手くやる方法がなかったのですが、Fluentd v1.10.0で登場した sd_srv プラグインを用いることで現在は便利にサーバーのリストを外部から管理できるようになりました。sd_srv プラグインでは、あるホスト名に対する DNS の SRV レコードを集約ノードのサーバーリストとして用いています。つまり、集約ノードを追加する場合(サービスイン)には該当ノードを指す SRV レコードを追加し、集約ノードを削除する場合(サービスアウト)には該当ノードを指す SRV レコードを削除するような仕組みを用意する必要があります。

さらにサービスアウト時、該当集約ノードのバッファに残ったログが空になるまでシャットダウンを待つ必要があります*2。もちろん集約ノードにログが流入し続けている状態ではバッファが空になることは期待できないので、まず SRV レコードを削除し、ログの流入を止めたのちにバッファが空になるのを待機します。

これらが実装できればサービスイン・サービスアウトを自動で実行することができます(i.e. 自動でインスタンス数を制御できる)。これは EC2 の Auto Scaling グループとしてインスタンス群を制御することができることを意味し、各種 CloudWatch メトリクスの値にしたがって簡単にインスタンス群をオートスケールさせることができるようになります。また、Auto Scaling グループとしてインスタンス群が制御できるとインスタンスの入れ替え作業が簡単になるため、特定のインスタンスに関する障害への対応が迅速に行えるようになるといったメリットもあります。

実装

全体として上図のような構成・処理の流れをとります。Auto Scaling グループでインスタンスがサービスイン・サービスアウトするときにイベントを発生させてサービスイン・サービスアウトを待機させることができる Lifecycle Hook という機能を用います。Lifecycle Hook の開始を知らせるイベントを Amazon EventBridge 経由で受け取り (1)、サービスイン・サービスアウトの処理を行います。

まず検討する必要があるのが SRV レコードを追加・削除する仕組みについてです。今回は AWS Cloud Map を利用して SRV レコードを更新することにしました。Cloud Map はサービスディスカバリのためのマネージドサービスで、インスタンスの登録・登録解除に合わせて DNS レコードを変更することができます。これは今回のユースケースにピッタリです。この Cloud Map に対する登録・登録解除は Lifecycle Hook のイベントに反応して EventBridge から呼び出される Lambda 関数で行います (2), (3)。

ただし Cloud Map に対するインスタンスの登録・登録解除は非同期に行われます。すなわち、Cloud Map に対する API コールはすぐにレスポンスが戻りますが登録が完了したか・その成否はわからず、そのレスポンスに含まれる ID を使って GetOperation APIで問い合わせる必要があるということです。今回は登録時に SQS キューにメッセージを送信し (4)、登録完了を確認する Lambda 関数を SQS から呼び出す (5) ことでこの登録待ちを実現しました。登録が完了していなかった場合に Lambda 関数が失敗する (6) ことで、SQS の可視性タイムアウトののちに再度 Lambda 関数が実行され、登録が完了するまでのリトライが実現されます。登録解除についても同様です。

先に説明したバッファの掃き出し待ちは、社内で利用している Prometheus インスタンスにバッファ長を問い合わせることで行いました (7)。Fluentd 集約ノードのメトリクスは fluent-plugin-prometheus を用いて Prometheus からモニタリングをしています。普段はこれをアラーティングや状態の可視化に用いているのですが、今回の掃き出し待ちにも利用することにしました。こちらの掃き出し待ちも先に説明した SQS と Lambda を用いたリトライによって実現しました。

最後に、サービスイン・サービスアウトのための条件が整ったら、Lifecycle Hook の完了を Auto Scaling グループに通知します (8)。これで Auto Scaling グループ上でインスタンスが正常にサービスイン・サービスアウトされたことになります。

監視

社内の全クラスタのコンテナログが Fluentd 集約ノードを経由するため、Fluentd 集約ノードのキャパシティは非常に重要です。スケールアウトに失敗してピークタイムに十分な数のインスタンスが立ち上がっていないと、ログの配送が失敗したり遅れたりしてしまう可能性があります。

今回は、スケールイン・スケールアウトの失敗を監視するために、各 Lambda 関数の実行が一定回数失敗したら DLQ (Dead-Letter Queue) にメッセージが入るようにしました*3。そして、DLQ の長さにアラームを設定しておくことで、スケールイン・スケールアウトの失敗に気づくことができるようになっています。

Lifecycle Hook は一定期間完了されないとタイムアウトして自動的に該当インスタンスが終了*4されます。これは、サービスアウトに失敗して一定期間経過すると該当インスタンス内のログが消失する可能性があることを示しています。そのため、サービスアウト失敗時の対応については特に詳細な手順書 (Runbook) を用意し、万が一の事態に備えています。

動作

この仕組みの動作例として、Auto Scaling グループの Desired capacity を減らした際の動作の様子を下に示します。Desired capacity が 2 に減らされた直後に Auto Scaling グループがサービスアウトするインスタンスを決定し、Lifecycle Hook を発行します。その後今回実装した仕組みにより Cloud Map 経由で SRV レコードから該当インスタンスが外され、ログの流入が止まり、出力バッファ長が減少を始めます。ログ量によりますが、バッファ長が 0 になるまで(ログが掃き出されるまで)5 分ほど時間がかかります。さらに、今回の実装では念の為 5 分間連続してバッファ長が 0 になっていること確認してからサービスアウトに進んでいます。そのため、サービスアウトには大体 10 分程度の時間がかかっています。サービスアウトが完了すると Auto Scaling グループ内の該当インスタンスが終了され、インスタンス数が減少し、Desired capacity と一致するようになります。

オートスケール

Auto Scaling グループを利用したインスタンスの制御が実装できたので、あとはスケーリングポリシーを設定するだけです。今回はピークタイムのネットワークトラフィック量を基準に、ターゲット追跡スケーリングポリシーを設定してオートスケールを有効にしました。ターゲット追跡スケーリングポリシーを有効にすると、メトリクスがターゲット値に近い値を取るようにインスタンス数が自動で増減されます。

オートスケールの結果

オートスケール導入前の Fluentd 集約ノードのメトリクスを下に示します。やはり時間帯によって負荷に差があることが読み取れ、例えば CPU 使用率はピークタイムでは 40% 近くにのぼっていますが深夜帯は 10% にも満たないことがわかります。

次にオートスケール導入後のメトリクスを下に示します。狙い通り、全体的にピークタイム程度の負荷でメトリクスが平されている様子が見て取れます。

コスト

Fluentd 集約ノード用インスタンスのコストの変化を下に示します。5/13 に移行を開始し(移行中は旧構成と両立しているためコストが増えています)、6/7 あたりに移行を終えました。下のグラフから読み取れるとおり、Fluentd 集約ノードにかかるコストを約半分に削減することができました。

まとめ

AWS Cloud Map を利用して Fluentd 集約ノードをオートスケールさせる仕組みについて紹介しました。今回はコンテナログの集約に用いている集約ノードでオートスケールを実践しましたが、仕組み自体は汎用的なもので他にもオートスケールの必要がある集約ノードがあればすぐに応用が効くものになっています*5。Fluentd 集約ノードのオートスケール事例として参考にしていただければ幸いです。

最後になりますが、SRE グループでは一緒に働く仲間を募集しています。 カジュアル面談や学生インターンシップなども随時実施していますので、ぜひお気軽にご連絡ください。

info.cookpad.com

*1:プロダクションで動いているのは 300-500 台程度

*2:Fluentd の flush_at_shutdown オプションでは十分ではなく、例えばログ掃き出し中に問題が発生したとしてもすぐにインスタンスが強制的にシャットダウンされてしまい対処の猶予がなくなってしまうなどの問題があります

*3:上の図における “ハンドラ関数”は Lambda の非同期実行で実行されるため Lambda に DLQ を設定しており、”待ち関数” は SQS キューから実行されるため SQS キューに DLQ を設定しています

*4:DefaultResult=ABANDON の場合。また、サービスアウト時にはどちらにしろインスタンスは終了します

*5:実際、この仕組みの大部分は Terraform module として実装されており簡単に導入できるようになっています

オフラインイベント「Cookpad Tech Kitchen #27 Rails/Next.js/IoTによる食品流通」を開催しました!

$
0
0

買物プロダクト戦略部の勝間(@ryo_katsuma) です。6/30に「Cookpad Tech Kitchen #27 Rails/Next.js/IoTによる食品流通」をWeWork リンクスクエア新宿で開催しました。 イベントではクックパッドの新規事業「クックパッドマート」の流通を開発するエンジニアをはじめ、ECアプリケーションエンジニアやCTOも参加し、クックパッドマートの流通を中心とした最新の状況や組織についていろいろお話させていただきました。今回は当日の様子を発表資料も交えて紹介させていただきます。

クックパッドマートの失敗したデータ設計 Before / After 大放出 by 奥薗

まず、1番めのトークとして、奥薗(@mokuzon)から流通開発におけるデータ設計の自分たちの知見についてお話させていただきました。

「営業日」という比較的馴染みのある概念から、商品が「動く」「留まる」「買える」など、抽象的な概念まで、様々な観点での設計の歴史についてお話しました。 最初の作りが不十分で、要件が増える中で実装が複雑化してしまう、ということは多くのエンジニアが経験することだと思います。そんな中で、メンバー間でお互いに作り直しに寛容でありながらも、どのように失敗から学んできたか?の実例をまとめて紹介いたしました。

Next.jsで作るドライバー向けWebアプリ by 中村

2番目のトークは、中村(@peto_tn)から、流通の具現化で重要なドライバー向けのWebアプリの開発についてお話させていただきました。

ドライバー向けアプリは、「SEOが不要」「実用性重視」など、一般的なWebサービスとは異なる要件があります。その上で、最初はネイティブアプリで作っていたアプリを、Webアプリに作り直していく中で、認証・認可やレポジトリ構成、実装の一例など、具体的な実現方法をについてご紹介しました。

食品をお届けするための温度監視 by 石川

最後のトークは石川(@nekketsuuu)より、流通における商品の温度監視の仕組みについて、「大規模拠点からの配送時」と「受け取り場所」での2つの場面においてどのように実現しているかをお話しました。温度監視といっても、それぞれの場面で必要になる場面やコンテキスト、活用方法は大きく異なります。

この条件下で、SORACOM IoTプラットフォームや、PrometheusやAlertmanagerをはじめとするサーバー監視ツールなど、既存の仕組みやプロダクトをいかに組み合わせ、活用しているかについて、紹介いたしました。(石川の発表資料は公開いたしません)

LT

トークの後は、弊社CTO成田(@mirakui)と塩出(@solt9029)から、流通からは大きく変わって、中の人たちの人なりをお伝えすることを目的にしたLTを行いました。

成田からは「ドライイーストを使わずにパンを焼けるか? 〜天然酵母のパン作りを支える技術〜」、塩出からは「最近作ったもの紹介」と、それぞれ業務から離れたプライベートの開発(?)について紹介をいたしました。

2テーマとも業務外のテーマながらも非常に濃い本気の内容で、場は非常に盛り上がっていました。特に塩出のテストを音ゲーのように扱う様子はTwitterでも話題になっていました。

まとめ

前回のイベントから約3ヶ月ぶりとなったオフラインイベントでしたが、今回もトーク中はもちろん、来場者の方たちとの意見交換などで非常に盛り上がったイベントとなりました。引き続き、まだまだ試行錯誤中ではありますが、クックパッドは今後もこのようなオフラインイベントを定期的に開催予定です。(次回は2022年9月頃に実施を計画しています)ご興味ある方はぜひ@cookpad_techのFollowをお願いいたします。

また、クックパッドマートではエンジニアを引き続き募集しています。 今回のイベントでも話した流通領域や、ユーザーフェイシングなEC領域など、どのような環境で開発をしているか、どのようなエンジニアを募集したいかについて、noteにまとめています。中の様子が気になる方はこちらもあわせて参照いただければ幸いです!

クックパッドマートを支えるアカウントたち

$
0
0

クックパッドマートの開発に携わっているソフトウェアエンジニアの塩出(@solt9029)です。

生鮮食品ECサービスのクックパッドマートでは、注文ユーザー向けのECアプリを中心として、商品を販売する店舗向けの管理画面、生鮮食品の流通を支えるドライバー向けのWebアプリなど、様々なアプリケーションを開発・提供しています。

今まで利用者の課題を解決するために、それぞれのアプリケーションの認証機能やアカウントの仕組みについて、色々な工夫や挑戦をしました。本記事をはじめとして、2022年7月19日〜7月26日(平日のみ)にかけて、クックパッドマートの様々なアプリケーションを支えるアカウントの仕組みやそれらを用いて解決した課題、より良いユーザー体験を実現するために工夫した点を紹介する予定です。

本記事では、クックパッドマート自体の紹介をはじめ、クックパッドマートで日々開発しているアプリケーションの概要と、それぞれに用いられているアカウントの仕組みについて簡単に紹介します。

クックパッドマートとは

クックパッドマートは、弊社が力を入れて取り組んでいる新規事業の1つです。生鮮食品を中心として扱っているECプラットフォームで、街の販売店や地域の生産者がクックパッドマートに参加しています。コンビニエンスストア・ドラッグストア・駅・マンションなどの様々な場所にユーザーの受け取り場所として専用の冷蔵庫が設置されており、ユーザーはアプリから注文を行い、専用の冷蔵庫から商品を受け取ることができます。

クックパッドマートの専用冷蔵庫

ECアプリ

クックパッドマートで生鮮食品を中心とした商品を注文するためのモバイルアプリです。iOSとAndroidの両方で「クックパッドマート」のアプリを提供しています。商品の注文だけでなく、お気に入り商品の登録やレビュー投稿など、様々な機能を提供しています。

ECアプリ

ECアプリでは、インストール後に気軽に買い物を始めてもらうため、利用開始時にユーザー登録のフローを設けていません。しかし、ユーザー登録無しの状態では、機種変更時のデータの引き継ぎができない点や、ユーザーとのコミュニケーション手段がプッシュ通知に限られてしまう点などの課題がありました。

そこで、ECアプリにログイン機能を導入することにしました。その際、ユーザー登録無しの状態で利用していたデータを、ログイン後も引き続き利用するために、ログイン後のユーザーに持ち越す仕組みをバックエンド側で実現する必要がありました。また、その処理は非同期で数十秒〜数分の実行時間がかかるものであったため、ECアプリ上で見せ方や状態管理を工夫する必要がありました。

7月20日(水)・21日(木)に公開予定の記事では、ECアプリのログイン機能導入の背景や体験設計およびその実装を紹介します。また7月22日(金)に、ログイン後のユーザーデータ統合の処理について、バックエンドの仕組みを紹介します。

店舗向け管理画面

クックパッドマートでは、商品を販売する店舗向けに管理画面を提供しています。店舗向け管理画面では、商品の登録・営業日の管理・日々の出荷作業に必要な情報の確認・売上情報の閲覧など、様々な機能があります。

店舗向け管理画面

今までは、店舗向け管理画面の認証のために、ユーザー登録の仕組みを設けていませんでした。専用のチャットアプリ上で管理画面のログインURLを都度発行する方式を取っていました。このログイン方式では、「店舗スタッフごとの権限管理や操作ログの監査ができない」という明確な課題や、「店舗とクックパッドのユーザーは分離された概念となり、クックパッドにおけるサービス共有資産が活用できない」という中長期的な課題がありました。

そこで店舗の認証方法を、クックパッドのユーザーを利用したものに移行しました。認証方法の移行の際、複数の認証方法の並存の実現や、データ設計やログイン体験の変更など、多くの困難な課題を解決する必要がありました。

7月25日(月)に公開予定の記事では、店舗の認証方法の移行の背景や経緯、実現のために求められた要件や解決した課題について紹介します。

ドライバー向けWebアプリ

クックパッドマートでは、生鮮食品の流通を担当するドライバー向けのWebアプリを開発しています。ドライバー向けWebアプリでは、配送経路・時間・対象商品などを確認する機能や、配送状況を随時共有するための機能などがあります。ドライバーの役割に応じて大きく異なる情報や操作が必要となるため、ドライバー向けWebアプリとして複数のアプリケーションが存在します。

ドライバー向けWebアプリ

ドライバー向けWebアプリでは、アカウントの仕組みを整える上で、ECアプリや店舗向け管理画面とは大きく異なる要件がありました。例えば、全てのアカウントをクックパッド側で管理する必要がある点・運送会社や役割に応じて詳細に権限管理をする必要がある点などです。

そこでドライバー向けWebアプリでは、クックパッドのユーザーを利用せず、弊社内で利用している認証サービスであるAzure ADを利用することにしました。Azure ADを利用することによって、クックパッド側でのアカウント管理や権限管理など、運用しやすい状態に保つことができました。

7月26日(火)に公開予定の記事では、ドライバー向けWebアプリの詳細や、Azure ADを使った権限管理や運用体制の詳細について紹介します。

最後に

本記事では、クックパッドマートで開発・提供している代表的なアプリケーションを紹介しました。次回以降の記事では、それぞれのアプリケーションのアカウントの仕組みについて、以下の日程でより詳細を紹介する予定です。

  • 7/20(水): クックパッドマートアプリにおけるログイン体験の実現
  • 7/21(木): クックパッドマートアプリのログインの裏側
  • 7/22(金): クックパッドマートにおけるアカウントの統合
  • 7/25(月): クックパッドマートでの店舗の認証方法移行の取り組み
  • 7/26(火): クックパッドマートのドライバー向けWebサービスのアカウント

最後になりますが、クックパッドマートでは事業成長のためにスピードを高めて開発に取り組んでおり、様々な技術に触れる機会も多くとても楽しい環境です!弊社では絶賛エンジニア募集中なので、興味を持って頂けた方はぜひ採用情報をご覧ください。

info.cookpad.com

クックパッドマートアプリにおけるログイン体験の実現

$
0
0

こんにちは。買物プロダクト開発部の大川(@aomathwift)です。クックパッドマートのiOSアプリ(以下マートアプリ)の開発に携わっています。

マートアプリ

この記事は、「クックパッドマートを支えるアカウントたち」連載2本目の記事で、マートアプリでの認証にフォーカスを当てたものです。 シリーズの全貌については以下の記事を御覧ください。

クックパッドマートを支えるアカウントたち - クックパッド開発者ブログ

本稿では、2022年4月に完全導入されたマートアプリでのログイン体験の実現について紹介します。

クックパッドマートにおけるユーザー登録

多くのECサービスでは、最初にユーザー登録をして決済情報や配送先情報等を登録した上で利用開始するものが多いと思います。 しかし、マートアプリでは利用開始時にユーザー登録のフローはありません。アプリインストール後に気軽に買い物を始めてもらうため、クレジットカード情報と商品を配送するマートステーションという名の冷蔵庫の場所(以下受け取り場所)、受け取り名*1のみを入力するだけで商品を購入できる体験設計をしていました。

「ユーザー登録無し」の課題

しかし、この「ユーザー登録無し」という方針には課題があります。 まず、機種変更の際のデータ引き継ぎができないという点です。マートアプリでは、明示的なユーザー登録を行わないため、購入履歴や注文状況といったユーザーデータは端末に紐付いて保存されます。そのため、アプリがインストールされている端末が変わった場合、必然的にデータを参照できなくなり、強制的に新規ユーザーとしてやり直すことになります。すなわち、機種変更の際のデータ引き継ぎができません。

また、明示的なユーザー登録をしない場合、電話番号やメールアドレスといったユーザーの連絡先情報を取得できず、ユーザーへのコミュニケーション手段としてメールやSMSを選択できません。プッシュ通知は利用できますが、これはユーザー側でオフにすることができるため、連絡手段として堅牢なものとは言えないでしょう。

これらの課題が、ユーザー数の増加・アプリの機能拡大とともに顕在化してきました。そのため、「ユーザー登録無し」という方針を見直し、マートアプリにログイン機能を導入することにしました。

ログイン時のユーザーデータの統合

さて、ログインを導入してアプリを利用してもらう際に課題となるのが、登録無しの状態で利用していた際のデータを登録後のユーザー(以下登録ユーザー)に持ち越せるのか、という点です。これに関しては、ユーザー登録無しの状態で作成された購入履歴やお気に入り商品といったユーザーデータを、新規に作成した登録ユーザーに統合する、ということを実現しています。詳しくは後日投稿予定の「クックパッドマートにおけるアカウントの統合」という記事で詳しく解説されますので、そちらをご覧ください。

マートアプリにおけるログインの見せ方

データ統合の際に生じるマートアプリ特有の課題

前節で述べたユーザーデータの統合は非同期に実行され、一定の実行時間(数十秒から数分)がかかります。レシピアプリでは、登録ユーザーとして新しくログイン後、ユーザーデータの統合を待たずしてアプリ内コンテンツが表示されることに大きな違和感はありません。生じるタイムラグの間も、アプリのコアであるレシピの閲覧機能を利用することができるからです。

ところが、これがマートアプリではそうはいきません。 マートアプリでは、アプリを初めて起動したタイミングで受け取り場所の設定を行います。受け取り場所が設定されていることで、どの商品をどの配送に乗せるかが確定し、選択された日付にその商品を受け取ることができるかどうかが決まるようになっています。 そのため、登録ユーザーでログイン後、ユーザーデータの統合が未完了の状態でアプリを起動すると、ユーザー目線では設定済みの受け取り場所が突然未設定の状態に戻ったように見えてしまいます。その結果、ユーザーがログイン前に設定していた受け取り場所が突然消えたように見えてしまう上、受け取り場所未設定の状態ではアプリの主要な機能をほとんど利用することができません。これは、アプリの体験として非常に悪いものです。 したがって、マートアプリ上でこの課題を解決する見せ方をする必要がありました。

画面遷移での工夫

上記課題の解決策として、画面遷移での工夫を施しました。 具体的には、マートアプリのログインフローを、以下の図のように構成しています。

画面遷移

ポイントは、ユーザーデータの統合完了までの間、ユーザーのアプリ上での行動をブロックするためのポーリング画面を設置した点です。この画面で、裏で何が起きているのかをユーザーに端的に伝えつつ、完了までの時間アプリ内の購入・商品検索といった行動をできないようにしました。

ポーリング画面

数十秒から数分の時間、ユーザーの行動をブロックするという決断をするまでに、

  • 一時的に仮の受け取り場所が設定されている状態にする
  • エラー画面を出すのをやめて商品検索だけをできるようにする

といった案も出ましたが、受け取り場所を選択してそこに届くものを買い物するというサービスのコアとなる体験を損ねるよりも、主要機能が使えるようになるまでユーザーに待ってもらった方が良いという判断から、ユーザーデータの統合をポーリング画面で待ち合わせるという手段をとりました。

この実装をするにあたり、エラーケースとして以下のような場合を考慮する必要があります。

  • ユーザーデータの統合中にアプリがバックグラウンドに移行した場合
  • データ統合そのものに失敗した場合

たとえば前者の場合、フォアグラウンド状態に戻ったときにユーザーデータの統合処理が継続中なのか、無事完了済みなのか等、どのようなステータスにあるかを適切に確認して、ユーザーにその現状が改めて伝わるようにしなければなりません。

このような場合をカバーするには、アプリ側での状態管理とエラーハンドリングが非常に難しくなります。実際にどうやってこれを実現したかという実装に関する話は後続の記事「クックパッドマートアプリのログインの裏側」にて詳細に記述されるので、そちらも是非併せてご覧ください。

また、現在は、この非同期に行われるユーザーデータの統合の処理自体にも改善が入り、この待ち合わせ時間はほぼ一瞬でおわるように修正されています。いずれにしても統合の時間を待つという設計である点に変わりはないのですが、このような継続的なユーザー体験の改善が行われています。

おわりに

マートアプリにおけるログイン機能の実現について紹介しました。ECプラットフォームを運用するにあたって、やはりユーザー登録というのはあって然るべきで、これをアプリの機能が増えてきた途中の段階で入れるというのはなかなか難しいことだったと感じます。それでも、既存の体験を極力壊さず、今世の中のECサービスにおいて当たり前の機能の一つである「ログイン」を導入できたことで、試せる施策の幅を広げることにも繋がりました。

今後もクックパッドマートでは、今回紹介したようなユーザー基盤の整備をはじめ、機能追加や体験改善まで様々な開発を行っていきます。興味をお持ちの方は是非以下キャリアサイトから採用情報にアクセスしていただけると幸いです。

info.cookpad.com

アカウント連載の記事一覧

  • クックパッドマートを支えるアカウントたち - クックパッド開発者ブログ
  • クックパッドマートアプリにおけるログイン体験の実現(本記事)
  • クックパッドマートアプリのログインの裏側(7月21日公開予定)
  • クックパッドマートにおけるアカウントの統合(7月22日公開予定)
  • クックパッドマートでの店舗の認証方法移行の取り組み(7月25日公開予定)
  • クックパッドマートのドライバー向けWebサービスのアカウント(7月26日公開予定)

*1:受け取りの際に商品に印字される、受け取るユーザーを識別する名前

クックパッドマートアプリのログインの裏側〜Android アプリの実装を添えて〜

$
0
0

こんにちは。クックパッドマートの Android アプリを開発しています、門田です。
この記事は「クックパッドマートを支えるアカウントたち」の連載記事3日目です。

1日目: クックパッドマートを支えるアカウントたち - クックパッド開発者ブログ
2日目: クックパッドマートアプリにおけるログイン体験の実現

今回は、2日目の記事の中であった「ログイン画面の状態管理」について、実際の Android アプリの実装の紹介を交えながら説明します。

おさらい

クックパッドマートにログイン機能が導入されました。 これは、メールアドレスや電話番号を利用してクックパッドのユーザーを作成して、そのユーザーにログインすることで、機種変更時などにユーザーデータを引き継げるようにするものでした。 その際、ログイン前のユーザーデータをログイン後のユーザーに統合する必要があり、その処理が非同期で行われるために画面の見せ方などを工夫しているという話を前日の記事でご紹介しています。

クックパッドマートでは、アプリを利用し始める際に「受け取り場所」を設定する必要があります。これは、サービスの特性上それぞれの「受け取り場所」によってその日配送できる(注文できる)商品が異なる場合があり、どういう商品が受け取れるのかをユーザーに正しく見せるために設定しています。

たとえばここで、ログイン先のユーザーが一度もクックパッドマートを利用したことがないとします。 その場合、ユーザーデータを統合する処理が非同期で行われているため、ログインをしたときにユーザーの「受け取り場所」が一時的に設定されていない状態になってしまいます。 このままアプリを利用するためには、もう一度ユーザーに「受け取り場所」を設定してもらう必要があります。しかし、それではユーザーの行動を阻害してしまうことになり、サービスのコアの体験を損なってしまうことが懸念として挙げられていました。

そこで、「ログイン時に非同期のデータ統合処理が完了するまで待ち合わせる」という方針で仕様を整理し、アプリの実装を行うことにしました。

ここから本題

さて、「非同期のデータ統合処理が完了するまで待ち合わせる」というのをどのように実現すると良いでしょうか。 サーバーは「データ統合が完了したか」をなんらかの手段でアプリに返す必要があります。アプリは、その情報を元に状態を更新していけば良さそうです。

しかし、データ統合の処理は数十秒〜数分かかる場合があります。一度問い合わせればそれで良いというものでもなく、かといって待ち続けていても必ず終わる保証がありません。サーバーで何らかの問題があり統合処理が失敗してしまう可能性もあります。 ユーザーが待ちきれずにアプリを閉じたり画面をオフにしたり、はたまた電波の状況が悪くなりサーバーから情報を取得できなくなることもあるでしょう。

これを解決するには、シンプルに考えて以下の方法があると思います。

  • 定期的な間隔で API を叩き、処理が完了したことをサーバーから受け取る
  • 双方向通信を行うような仕組みを導入し、サーバーからデータを受け取る

この画面のためだけに双方向の通信を実現するのはさすがにオーバースペックだったため、今回のケースでは前者を採用しました。 ただ、闇雲に繰り返しを実装するだけでは一生終わらない可能性もあります。ユーザーも呆れ果てて離脱してしまうかもしれません。ということで、この繰り返しには十分な期間を設け、その期間が終了するまでに完了を確認できなかった場合はエラーにするという形にしました。これによって、無限ループに陥る状況を回避できます。 データの統合に失敗してしまった場合は、失敗された内容を検出するためのログを送信しつつ、ユーザーにはお問い合わせへの誘導を行うようにしています。

ここから実装の話

さて、ここからは具体的な実装の話に移っていきます。 これまでの内容をもとに、ここまでで話されていなかった箇所のエラー処理も含めて、アプリの動作を状態遷移図を書いてみると、こうなります。

この状態を Android アプリでは実際に以下のようなコードで表現しています。

sealed interface LoginState {
    /* 画面を初めて開いたときの状態 */
    object Initial : LoginState

    /* ログインボタンを押してログインを試行している状態 */
    object Login : LoginState

    /* ユーザーデータの統合処理を待っている状態 */
    class DataMigrationWaiting(val startedAt: Long) : LoginState

    /* ログインが成功した状態 */
    object LoginSucceeded : LoginState

    /* ログインが失敗した状態 */
    class LoginFailed(failureReason: String): LoginState
}

ユーザーデータの統合処理を待つ状態は繰り返す可能性があるので、自分自身に状態を戻すように定義しました。そして、「ログイン失敗」と「タイムアウト」は LoginFailed としてまとめ、失敗理由で分岐できるようにしました。

そして、ログインフローの処理は以下のように書いています。

class LoginViewModel(private val apiClient: ApiClient) : ViewModel() {
    private val _loginState = MutableStateFlow<LoginState>(LoginState.Initial)
    val loginState = _loginState.asStateFlow()

    fun login(credentials: Credentials) {
        _loginState.update { LoginState.Login }

        viewModelScope.launch {
            _loginState.update {
                runCatching { apiClient.login(credentials) }
                    .map {
                        LoginState.DataMigrationWaiting(
                            startedAt = System.currentTimeMillis()
                        )
                    }
                    .getOrElse { LoginState.LoginFailed("login failed") }
            }

            waitDataMigration()
        }
    }

    private suspend fun waitDataMigration() {
        while (_loginState.value is LoginState.DataMigrationWaiting) {
            val state = _loginState.value as LoginState.DataMigrationWaiting
            val newState = _loginState.updateAndGet {
                when {
                    apiClient.isDataMigrationFinished() ->
                        LoginState.LoginSucceeded
                    isOverDataMigrationTimeLimit(state.startedAt) -> 
                        LoginState.LoginFailed("data migration time limit over")
                    else ->
                        dataMigrationWaitingState
                }
            }

            if (newState is LoginState.DataMigrationWaiting) {
                delay(DELAY_TIME) // 次のループまで適当に数秒待つ
            }
        }
    }

    fun confirmLoginFailedError() {
        _loginState.update { LoginState.Initial }
    }

    private fun isOverDataMigrationTimeLimit(startedAt: Long): Boolean =
        (System.currentTimeMillis() - startedAt).absoluteValue >= TIME_LIMIT
}

多少簡略化はしていますが、概ね上のような実装になっています。 注目ポイントはデータ統合待ちの部分で、 LoginState.DataMigrationWating にループの開始時間を持たせており、それをループ中で参照することで「一定時間以上経過したらエラー」を表現しています。 State が自分自身に返ってくる様も、状態遷移図で書いたとおりに出来ていますね。実際に実装し始める前にも、このように図に起こしてから実装していたので頭の中でこの辺の流れがしっかりとイメージできていました。

そして最後に、 View の実装です。これに関しては長すぎるので細かい部分は割愛しますが、特に表現したいのは「各状態の表示の出し分け」に関してです。 ログイン画面は Jetpack Compose を使って実装しており、各状態での表示の出し分けは非常に簡単に実装することが出来ました。

@Composable
fun LoginScreen(
    viewModel: LoginViewModel,
) {
    val loginState = viewModel.loginState.collectAsState()

    Scaffold { /* 省略 */ }

    when (loginState) {
        is LoginState.Initial -> { /* なにもしない */ }
        is LoginState.Login -> {
            Dialog() // 処理中はダイアログでユーザーの行動を抑制する
        }
        is LoginState.DataMigrationWaiting -> {
            Dialog()
        }
        is LoginState.LoginSucceeded -> {
            // 成功時のダイアログを表示
            AlertDialog()
        }
        is LoginState.LoginFailed -> {
            // 失敗時のダイアログを表示、エラー内容に応じて表示する内容を分岐
            AlertDialog(
                confirmButton = {
                    Button(
                        // ViewModel の状態を更新することでエラーダイアログを閉じる
                        onClick = viewModel::confirmLoginFailedError
                    ) {
                        Text(“OK”)
                    }
                }
            )
        }
    }
}

Jetpack Compose を使うことで、各状態の更新も深く考えずに実装できました。 特に Dialog の表示制御に関して、 Jetpack Compose でとても簡単に実装できるようになったのが本当に良いところだったと思います。 これまでのパターンだと DialogFragment を用意してボタンの onClick イベントは Fragment Result API を利用して〜〜と非常に複雑で面倒な手続きが多かったのですが、 Jetpack Compose では条件ごとに Composable の関数を呼び出し分けるだけで解決します。 また、 LoginState を sealed interface にしたことで、状態の管理(増減)がしやすく View の表示を変えるのも簡単だったなと思います。

状態を増やしてみる

実際のアプリでは、ログイン中〜データ統合待ちの間に、「データの移行を開始します」という表示をユーザーに見せています。 その状態を増やしてみることにしましょう。

LoginState に一つ状態を増やします。

sealed interface LoginState {
    /* 省略 */

    /* ユーザーデータの統合処理待ち前の状態 */
    object BeforeDataMigrationWaiting : LoginState

    /* 省略 */
}

View の実装では、 LoginState の分岐に一つ追加し、必要な表示を行います。 今回のケースでは、ダイアログで情報を表示し、ユーザーに確認してもらったら次の処理に進むような形で実装しています。

@Composable
fun LoginScreen(
    viewModel: LoginViewModel,
) {
    val loginState = viewModel.loginState.collectAsState()

    Scaffold { /* 省略 */ }

    when (loginState) {
        /* 省略 */
        is LoginState.BeforeDataMigrationWaiting -> {
            AlertDialog(
                confirmButton = {
                    Button(
                        onClick = viewModel::waitDataMigration
                    ) {
                        Text(“OK”)
                    }
                }
            )
        }
    }
}

when の中に一つの分岐を追加しただけで簡単に状態を追加することができました。 Kotlin の exhaustive when の機能を使えば、コンパイラレベルで状態の表現を強制できるため、実装漏れなどを防ぐことができそうですね。

まとめ

クックパッドマートのログイン画面の状態管理について、実装とともに紹介してみました。 記事にまとめてみた感想としては「思ったよりも複雑じゃなくて焦っている」です。しかし、マートアプリの実装の一端を伝えられるいい機会になったんじゃないかなと思います。

アプリの実装はある程度パターン化出来るものが多いですが、今回の実装では特に「非同期のデータ更新を待ち合わせる」パターンを実装できました。このパターンは現在別の画面でも応用し始めています。こういった実装パターンを集めていくことで、実装速度を向上させ、各施策の実現スピードを上げることが出来ると私は考えています。これからも、こういったパターン化できそうな実装要件があった際には、色々と試していきたいです。

最後に、クックパッドマートでは一緒にサービスを盛り上げてくれる心強い仲間を募集しています。今回のログイン画面での実装に限らず、開発の効率を上げるための様々な工夫をしています。少しでも興味を持ってくれた方がいらっしゃいましたら、ぜひお話しましょう。 https://info.cookpad.com/careers/

アカウント連載の記事一覧


クックパッドマートにおけるアカウント統合

$
0
0

こんにちは、買物プロダクト開発部の岸谷です。 クックパッドマートという生鮮 EC サービスのバックエンドエンジニアをやっています。

この記事は「クックパッドマートを支えるアカウントたち」の連載記事4日目です。

今回は、2-3 日目の記事にあったログイン機能にまつわるバックエンドの仕組みについて紹介していきます。

ユーザー登録無しでもアプリが使える、けど…

クックパッドマートのアプリはユーザー登録しなくても、アプリをインストールして、商品を選んで、受け取り場所を選択して、購入ボタンを押せば商品を購入することができます。 そしてこの情報は揮発することなく、前回買った商品を確認したり、次回のお買い物で同じ受け取り場所を使ったりすることができます。 当然ながらこういったユーザー情報はサーバーに格納されており、そのユーザー情報は iOS/Android の端末と紐付けられています。

端末と直接紐付いたユーザー

この認証の仕組みはユーザーにとって手軽なのがいいところですが、ユーザーが機種変更するときに情報を引き継ぐことができないという問題がありました。 そこでユーザーに明示的にアカウントを作成してもらい、そのアカウントで認証することによって、端末と切り離したユーザー情報を保持できるようにしました。 この仕組みをログインと呼んでいます。

ログインユーザー

ログインとアカウント統合

ユーザーに作成してもらったアカウントにユーザー情報を紐付けることによって、ユーザーは機種変更してもユーザー情報を持ち越すことができるようになりました。 ですが、ログインするとき、ログインするまで使っていた「端末と紐付いていた、ユーザー登録なしで使えていたときのユーザー情報」は揮発してしまうのでしょうか? それでは不便ですよね。 ここで出てくるのが「アカウント統合」という処理です。このアカウント統合は、

  1. 端末で認証していたユーザーが、
  2. 初めてログインしたとき、
  3. これまで使っていたユーザー情報を、ログイン後のユーザー情報にマージする

という処理です。これにより、購入履歴や受け取り場所といったログイン前のユーザー情報を、ログイン後も引き続き利用できるようになります。

ログイン処理の流れ

また、このアカウント統合処理は同じユーザー情報に対して複数回発生することがあります。 あるユーザーが複数台の端末を持っており、それぞれでクックパッドマートを利用していたケースです。 この場合、ユーザーはまず端末 A を使ってログインし、端末 A のユーザーと、ログイン後ユーザーが統合されます。 次にユーザーは端末 B を使ってログインし、端末 B のユーザーと、ログイン後ユーザーが統合されます。

複数端末でログインする場合

このようなケースも考慮してアカウント統合の処理を実装する必要があります。

アカウント統合のデータ処理

こう説明していくとなんだか難しく聞こえてきますが、実際のアカウント統合処理の骨子は以下のようにシンプルです。

  1. ユーザーモデルへの参照を持っているモデルを列挙する
  2. 各モデルについて、参照先ユーザーを統合元ユーザーから統合先ユーザーへ付け替える
  3. 統合元ユーザーを削除する

クックパッドマートのサーバー処理は主に Rails で書かれており、当然データモデルも ActiveRecord で記述されています。

# ユーザーclassUser< ApplicationRecord
  has_many :orders
  has_one :user_location, dependent: :destroyend# 注文履歴classOrder< ApplicationRecord
  belongs_to :userend# ユーザーの受け取り場所classUserLocation< ApplicationRecord
  belongs_to :userend

アカウント統合処理は、ユーザーモデルへの参照を持っているモデルクラスに migrate_user_accountというクラスメソッドを定義し、順番に呼び出して処理するような形にしました。 上記のモデル定義を例に取り、どのような処理になるかを見ていきます。

単純にユーザーを付け替えるだけの場合

たとえば注文履歴といったデータは、単純に持ち主のユーザーを付け替えるだけの処理で完了します。

classOrder< ApplicationRecord
  belongs_to :userdefself.migrate_user_account(source_user:, destination_user:)
    source_user.orders.update_all(user: destination_user)
  endend

ユーザーを付け替えるだけでは済まない場合

たとえばユーザーが設定している受け取り場所は、1 ユーザーにつき 1 つしか設定できません。 このため単純な移行ができず、何らかのロジックを使ってどちらかのみを残す必要があります。 以下の例では、最終更新時刻が新しい方を残す、つまり最後に設定した方を残すというロジックで、統合後にどちらの受け取り場所を利用するかを選んでいます。

classUserLocation< ApplicationRecord
  belongs_to :userdefself.migrate_user_account(source_user:, destination_user:)
    if source_user.user_location.updated_at > destination_user.user_location.updated_at
      destination_user.user_location.update!(location_id: source_user.user_location.location_id)
    endendend

モデルによってはもう少し難しい場合もあり、たとえば以下のようなケースです。こういったものもモデルごとに一つ一つ検討し、実装していきます。

クックパッドマートには「リスト」という機能があり、複数の商品をリストにまとめ、そのリストに名前をつけて保存することができる。統合処理時、両ユーザーが同じ名前のリストを持っていた場合、リストに含まれる商品を突き合わせて、重複が無いようリストを合体させる。 クックパッドマートのクーポンには利用回数制限のあるものがある。統合処理時、両ユーザーが同じ回数制限クーポンを持っていた場合、それぞれのクーポンの利用回数を足して、そのクーポンの残り利用可能回数を算出する。

ユーザー情報の削除

最後に、統合元となったユーザー情報を消去します。

classUser< ApplicationRecorddefself.migrate_user_account(source_user:, destination_user:)
    source_user.destroy!
  endend

これでアカウント統合処理が完了し、ログイン前のユーザー情報は無事ログイン後に引き継がれました。 実際には、クックパッドマートの User モデルは数十のモデルから参照されており、モデル一つ一つに対してデータ統合の方針を検討していく必要がありました。 どのみち数が多いので大変ではあるのですが、このようにモデルごとに分解して考えられるような設計にすると、処理全体の見通しは立てやすいのかなと思います。

クックパッドのアカウント基盤

「ユーザー登録をすることなく、端末で認証してユーザー情報と紐付ける」「ユーザーにメールアドレスとパスワードを使ったアカウントを作ってもらう」といった機能は、クックパッドマートで独自に開発したわけではなく、クックパッドのアカウント基盤を利用しています。 どういうことかというと、レシピサービスに登録しているメールアドレスとパスワードを使って、クックパッドマートにログインすることができます。(もちろん逆も然りです)

このような挙動は、クックパッドユーザー情報とクックパッドマートのユーザー情報を紐付けることで実現しています。 つまりクックパッドマートアプリでログインを行う際、実際にはクックパッドユーザーを使って認証を行い、それと紐付いたクックパッドマートのユーザー情報の利用を認可しています。

クックパッドユーザーと、クックパッドマートのユーザー

また、アカウント統合もクックパッドのアカウント基盤に実装されている機能です。 アカウント基盤は初回ログインを検知すると、次の流れでアカウント統合を実施します。

  1. クックパッドユーザー情報をアカウント統合する
  2. 他サービス (ここではクックパッドマート) にアカウント統合イベントを通知する
  3. 通知を受け取ったサービスは、自サービス内のユーザー情報をアカウント統合する

ログイン時のアカウント統合の流れ

このアカウント統合イベントは Amazon SNS のトピックとして実装されています。 社内基盤のジョブキューシステムである Barbequeは SNS トピックの購読機能があるため、通知が発生し次第 ECS タスクを立ち上げて、アカウント統合処理を開始することができます。 このようなアカウントシステムが整っている社内基盤を用いることで、社内で新しいサービスを立ち上げる際にも、認証・認可などの難しい設計に立ち入ることなく、サービスに関する設計・実装に集中することができています。

ログインの高速化

上記のアカウント統合イベントの通知の仕組みは、実装が簡単なためログイン機能の早期開発には役立ったのですが、問題がありました。 ジョブキューシステムを用いていることから本質的に処理が非同期であること、そして ECS タスクは起動に時間がかかってしまうことです。 実際にログイン機能の導入当初、クックパッドマートアプリでは初回ログインする際に 1 分程度「データ移行をしています」という画面を出し続ける状態になっていました。(このあたりの細かい話過去記事で解説しておりますので、ぜひご覧ください) この挙動はユーザー体験も悪く、またアプリの設計としても都度サーバーをポーリングしてアカウント統合の処理進捗を確認する、という難しい実装になってしまっていました。

これを解決するため、クックパッドのアカウント基盤を改修し、クックパッドマートアプリで初回ログインが発生した場合は、クックパッドマートに対してアカウント統合を実行する API を直接リクエストするようにしました。 幸いクックパッドマートのアカウント統合処理自体は 1 秒もかからないものであるため、ジョブキューシステムを用いた非同期的な処理から API のリクエストという同期的な処理に変更することにより、クックパッドマートマートの初回ログイン処理も、現在は一瞬で完了するようになりました。 このように、必要に応じてアカウント基盤自体の改修が実施できることも、社内基盤を用いている利点の一つです。

おわりに

クックパッドマートのログイン機能にまつわるサーバーサイドの処理を紹介しました。 アカウントの統合はあまり一般的な機能ではないかもしれませんが、クックパッドマートのようにログインレスとログインを両立させようとするとどうしても必要になってきます。 何気なく使ってるログイン機能でも、裏側ではこういう地道な処理が動いているんだなという雰囲気でも感じ取っていただければ幸いです。

最後になりますが、クックパッドマートでは一緒にサービスを開発してくれる仲間を絶賛大募集しています。 今回取り上げたログイン機能についても、ソーシャルログインの導入や、クックパッドのアカウント基盤を使ったコミュニケーションチャネルの整備など、まだまだやりたいことはたくさんあります。 EC サービスの開発って聞くとお堅いイメージがあるかもしれませんが、クックパッドマートは EC 機能を止めずに日に何度もデプロイが走るようなアグレッシブな開発環境で、きっと飽きさせることはないと思います。 EC サービスの開発に一家言ある人、認証・認可基盤の構築に興味がある人、とにかくユーザーに使ってもらうためのサービスの開発が好きな人等々、もし興味がございましたら以下から是非ご連絡お待ちしております。

https://info.cookpad.com/careers/

https://cookpad-mart-careers.studio.site/

クックパッドマートでの店舗の認証方法移行の取り組み

$
0
0

クックパッドマートの開発に携わっているソフトウェアエンジニアの塩出(@solt9029)です。「クックパッドマートを支えるアカウントたち」連載シリーズの5本目の記事です。本記事では、クックパッドマートでの店舗の認証方法移行の取り組みについて紹介します。

美味しい生鮮食品をユーザーにお届けするサービスであるクックパッドマートでは、日々街の販売店や地域の生産者が商品登録や出荷作業を行っており、それらの操作を行う際に、専用の管理画面を利用しています。

今までは、店舗向け管理画面の認証のために、ユーザー登録の仕組みを設けていませんでした。専用のチャットアプリ上で管理画面のログインURLを都度発行する方式を取っていました。このログイン方式では、「店舗のスタッフごとの権限管理や操作ログの監査ができない」という明確な課題や、「店舗とクックパッドのユーザーは分離された概念となり、クックパッドにおけるサービス共有資産が活用できない」という中長期的な課題がありました。

そこで店舗の認証方法を、クックパッドのユーザーを利用したOAuth認証に移行しました。認証方法の移行の際、多くの困難な課題を解決する必要がありました。本記事では、店舗の認証方法の移行の背景や経緯、実現のために求められた要件や解決した課題について紹介します。

認証方法の移行前と移行後の比較

背景

クックパッドマートは、弊社が力を入れて取り組んでいる新規事業の1つです。生鮮食品を中心として扱っているECプラットフォームで、街の販売店や地域の生産者がクックパッドマートに参加しています。コンビニエンスストア・ドラッグストア・駅・マンションなどの様々な場所に、ユーザーの受け取り場所として専用の冷蔵庫が設置されています。ユーザーはアプリから注文を行い、冷蔵庫から生鮮食品を受け取ることができます。

クックパッドマートでは、店舗が商品の登録や営業日の管理、日々の出荷作業などを行うための機能を提供する、店舗向け管理画面を開発しています。

店舗向け管理画面

クックパッド運営は、店舗の問い合わせサポートをするために、専用のチャットアプリを利用してやり取りしています。店舗向け管理画面では、今までユーザー登録の仕組みを設けておらず、チャットアプリ上での発言に応じて、ボットが管理画面のログインURLを都度発行する方式を取っていました。

チャットアプリ上でログインURLを都度発行する様子

店舗向け管理画面が誕生した当時、ミニマムの機能でなるべく多くの店舗にスムーズに使ってもらえるような形で検証を行いたく、ログインの手間を最小限に抑えた作りとしていました*1。実際にしばらく運用したところで、店舗向け管理画面はクックパッドマートを回すために必要不可欠な存在にまで成長しました。しかし、ログインURLを都度発行する方式では、以下のような課題が存在していました。

  • 1店舗の中でも複数のスタッフが商品登録や出荷作業を行っているケースがある一方で、どの操作をどのスタッフが行ったかを、ログから特定することが難しい。
  • クックパッドマートの注文ユーザー向けECアプリでは、レシピサービスであるクックパッドのユーザー基盤を用いて認証を行っている一方で、店舗の認証はクックパッドのユーザーから切り離されており、クックパッドにおけるサービス共有資産が一切活用できない。つまり、例えばクックパッドマートのECアプリ上では、店舗として活動できない状態であり、「クックパッドマートのECアプリ上で店舗として登録されたユーザー限定機能を提供する」といった施策は実現できない。

本来であれば、検証段階に作ったプロダクトをそのまま成長させるのではなく、認証方法を含めて一度あるべき姿で作り直すべきでした。しかし、時間を巻き戻すことはできないため、改めて認証方法の移行に踏み切ることにしました。上記に掲げた前者の課題から、何かしらのアカウントを利用した認証の仕組みが必要となり、後者の課題から、その「アカウント」はクックパッドのユーザーであるべきだと整理しました。そこで店舗の認証方法を、クックパッドのユーザーを利用したOAuth認証に移行することにしました。

設計・実装

店舗の認証方法の移行にあたって、機能として必要となった要件や設計、実装を紹介します。

認証方法移行に伴うデータ設計

今まで店舗ごとに、チャットアプリ上からログインURLを都度発行する方式をとっていたため、「店舗としての操作」という扱いになっていました。

実際には、背景・目的の章で述べたとおり、1店舗の中でも複数のスタッフが商品登録や出荷作業を行っているケースがあるため、1店舗に対して複数のユーザーを登録できる状態を実現する必要がありました。

また、1事業者が複数の店舗を運営しているケースもあります。例えば、「〇〇マート株式会社」という事業者が、「〇〇マート横浜店」と「〇〇マート川崎店」といった店舗を運営している場合です。この場合、例えば事業者の代表者が複数の店舗の売上情報を閲覧したいといった要望がありえるため、1ユーザーが複数の店舗に登録できる状態も実現する必要がありました。以上から、店舗とユーザーの関係は多対多として実現する必要がありました。

また法務や権限管理の観点から、1事業者は管理者としてのユーザーを1つのみ登録する必要がありました。そのため、「事業者」を表す概念もデータ設計時に組み込む必要がありました。これまでの内容を図解すると、以下のようになります。

移行前と移行後の認証の単位の比較

もともとは shops テーブルで完結していたものが、認証方法の移行にあたって以下のようなデータ設計になりました。大した複雑性ではありませんが、移行前と比較すればそれなりに登場人物の増えた状態になりました。

データ設計の変更

認証方法移行に伴うログイン体験の変化

今までは都度発行されるログインURLと店舗が1対1の関係であったのに対して、認証方法を移行した後は、前章の通り1ユーザーが複数店舗を保持できるようになります。そのため、クックパッドのユーザーを利用して店舗向け管理画面にログインしたとしても、「どの店舗の操作をしたいか」は確定しません。

実現方法は主に2つ考えられました。1つ目は、リソースを扱うようにパスなどで指定したIDに応じて操作対象の店舗を指定する方法です。ECサイトの管理画面などでたくさんの商品マスター情報を操作する考え方に近いです。2つ目は、セッションなどで操作対象の店舗のIDを指定・保持する方法です。厳密には根底の概念から違いますが、TwitterやFacebookなどのSNSに見られる「アカウント切り替え」の見せ方に近いです。

認証方法の移行にあたっては、2つ目の「アカウント切り替え」のような見せ方を採用しました。以前の店舗向け管理画面では、都度発行されるログインURLと店舗が1対1の関係であったことから、「店舗」主体での操作しかありえなかったため、管理画面の操作体験の変化を最小限に留めることや、移行のための実装の容易性を意識したことが主な理由です。

「アカウント切り替え」のような見せ方

複数の認証方法の並存

認証方法の移行にあたって、ログインURLを都度発行する方式からいきなり全てをクックパッドのユーザーを用いたOAuth認証に切り替えることは当然できません。認証方法の移行に関する告知を出した後、一定期間どちらの認証方法も受け入れられる状態を用意する必要があります。認証方法の移行が完了した店舗から随時、旧認証方法(都度発行されるログインURLの認証)が無効になる形にしました。

また、どちらか片方の方法で認証された状態だけでなく、両方の認証方法が同時に適用された状態が必要になる場面も存在するため、それを考慮した実装を行う必要がありました。具体的には、店舗の認証方法の移行手順を実施するタイミングです。

両方の認証方法が同時に適用された状態が必要になる場面

複数の認証方法があるだけでなく、その複数の認証方法を同時に扱わなければならない場面があったため、想定されるケース数も多く、実装も複雑になり、認証方法の移行にあたっての1つの大きな鬼門になりました。実装の詳細については軽く紹介するにとどめますが、大きく3つのメソッドが重要になりました。

  • 新旧の認証方法に関わらず、現在選択中(ログイン中)の店舗を返却するメソッド(current_shop
  • 旧認証方法によってログインしている店舗を返却するメソッド(current_shop_by_old_auth
  • ログイン中のクックパッドのユーザーを返却するメソッド(current_user
# 新旧の認証方法に関わらず、現在選択中(ログイン中)の店舗を返却するメソッドdefcurrent_shopif logged_in_with_cookpad?
    # ユーザーに登録された店舗が1つのみの場合、操作対象の店舗をわざわざ選択しなくとも、店舗は一意に定まるif session[:current_shop_id].nil? && current_user.authorized_shops.size == 1
      session[:current_shop_id] = current_user.authorized_shops.first.id
    end

    current_user.authorized_shops.find_by(id: session[:current_shop_id])
  else
    current_shop_by_old_auth # 都度発行されるログインURLで認証を受けている場合、その店舗を返却するメソッドendend

運営スタッフによる問い合わせサポートの考慮

今までは、店舗ごとにチャットアプリ上からログインURLを都度発行する方式をとっていました。店舗からの問い合わせサポートを行う際、クックパッドの運営スタッフは実際にログインURLを通じて、その店舗として店舗向け管理画面にログインして、画面状況を確認することが時折ありました。

認証方法の移行によって、クックパッドのユーザーによるOAuth認証が必要になりますが、当然セキュリティ上、クックパッドの運営スタッフはその店舗スタッフとして登録されたユーザーのIDやパスワードを把握できません。しかし、認証方法の移行後も引き続き、問い合わせサポートや緊急対応の目的で、クックパッドの運営スタッフが店舗向け管理画面を閲覧できる状態を維持したいと考えました。社内の管理画面でもほとんどの必要な操作はできますが、なるべく店舗スタッフが実際に日々利用している画面を、クックパッドの運営スタッフも閲覧できる状態とすることで、店舗スタッフの抱える課題を、より現場目線で把握しやすい状態にできると考えたからです。

そのため、運営スタッフが店舗向け管理画面を任意の店舗として閲覧できる仕組みを別途設けました。限られたクックパッドの運営スタッフは、あらかじめクックパッドのユーザーを社内の管理画面上で登録することによって、特別な権限を付与し、そのユーザーを利用すれば任意の店舗として店舗向け管理画面を閲覧できる仕組みにしています。

しかし、例えばそのクックパッドの運営スタッフが退職をした際に、退職後もその登録したクックパッドのユーザーを利用して任意の店舗としてログインできる状態となってしまっては、セキュリティ上の問題があるため、クックパッド社員退職後の自動処理の中で、その権限を無効化する機能を作りました。社員退職後の自動処理について、詳細は「退職処理を可能な限り自動化する」の記事にまとまっています。

ユーザー登録の解除の考慮

クックパッドのユーザー登録は解除することができます。しかし、一定の条件において、このユーザー登録の解除はブロックされています。例えば、クックパッドマートでの注文した商品の受け取りが完了していないケースでは、ユーザー登録を解除してしまうと受け取り不可能な状態となってしまうため、受け取りが完了するまでの間、解除がブロックされています。

同様に、例えばクックパッドマートで注文が入っているにも関わらず、未出荷の状態の店舗のスタッフが全員ユーザー登録を解除してしまうと、店舗向け管理画面に一切アクセスできない状態となってしまいます。そこで、クックパッドマートから退店をしない限り、かならず店舗のスタッフとして登録されたユーザーが存在するように、クックパッドのユーザー登録の解除についてブロック処理を設けました。

リリース・運用にあたって

これまで、実装や設計について紹介しました。実際にリリースや運用をするにあたっては、開発以外にも考慮すべき点が多数あります。本章では、その中でも特に注力したものを軽く紹介します。

法務の観点

クックパッドのユーザーは、基本的には個人が営利を目的とせずに利用することが想定されています。しかし、認証方法の移行によって、クックパッドマートに出店している営利目的の店舗(法人含む)がクックパッドのユーザーを利用することになります。法務チームと仕様の認識合わせを行いながら、最終的には営利活動における特約を定めることとしました。また、認証方法の移行にあたって一定期間の告知を設ける必要があるなど、その他にも多くの項目について連携を取りながら開発を進めました。

運用の観点

認証方法の移行にあたって、期限にある程度の余裕を持たせていましたが、なるべく早く、より多くの店舗が認証方法を移行した状態になることが望ましいと考えていました。そこで、社内の管理画面上で認証方法の移行の進捗状況がすぐに確認できる機能を作成し、認証方法の移行完了を目指して、運用チームがアクションのとりやすい状態をつくりました。また、店舗と直接コミュニケーションをとる機会の多い営業チームとも連携し、スピーディーな移行に努めました。

また、認証方法の移行時のトラブルも細かく考慮しながら進めました。認証方法の移行時のトラブルの一例として、1店舗において複数のスタッフが出荷作業を行っている場合に、誤ってあるスタッフが店舗内で何も周知せずに認証方法の移行を行ってしまった結果、旧認証方法(都度発行されるログインURLの認証)が無効になってしまい、他のスタッフが店舗向け管理画面にアクセスできなくなってしまうケースが想定されました。

認証方法の移行の際に想定されたトラブルの一例

店舗向け管理画面の中には、注文商品の出荷作業のような、数時間以内に行う必要のある緊急を要する作業を行うための機能も含まれており、認証方法の移行時に何かトラブルがあると、この出荷作業に影響が出てしまい、最悪の場合ユーザーに注文商品をお届けできなくなる恐れがあります。そのため、ドキュメント整備による認証方法の移行時のトラブル発生の未然防止や、すぐにトラブル対応できるようなサポート体制を整えることはもちろん、問い合わせベースで認証方法を一時的に元に戻せる仕組み作りなども行いました。

最後に

クックパッドマートでの店舗の認証方法の移行の取り組みについて紹介しました。認証方法の移行にあたって、一時的に複数の認証方法が並列して存在したり、店舗とユーザーの多対多の関係にする必要があったり、様々な困難がありました。リリース後の問い合わせは週数件のペースでありましたが、量も内容も想定していた範囲内に留まっており、大きなトラブルもなくスムーズに進めることができました。

本プロジェクトを通じて、「認証方法の移行」は避けられるのであればなるべく避けたいものだとも痛感しました。今まで紹介したように、認証方法の移行には開発はもちろん、多方面で非常に多くのコストがかかるからです。本記事で紹介したケースでは、検証用のミニマムの機能として開発したものをそのまま開発継続して広く普及させず、価値に確信できたタイミングで、早々に認証方法の移行や、新しく別のアプリケーションとして作り直すべきだったと感じました。

一方で、どうしても後から認証方法の移行をしなければならない場面も現実世界では多々あると思います。そんな場面に遭遇してしまった方に、本記事で紹介した実装時の考慮項目や運用が一例として参考になれば幸いです。

最後になりますが、クックパッドマートでは事業成長のためにスピードを高めて開発に取り組んでおり、様々な技術に触れる機会も多くとても楽しい環境です。弊社では絶賛エンジニア募集中なので、興味を持って頂けた方はぜひ採用情報をご覧ください。

info.cookpad.com

アカウント連載シリーズの記事一覧

*1:詳しくは過去の記事「街のお店や生産者が使ってくれる仕組みのつくりかた」をご覧ください。

クックパッドマートのドライバー向けWebサービスのアカウントの仕組み

$
0
0

買物プロダクト開発部の中村です。クックパッドマートという生鮮食品のECサービスでサーバーサイドエンジニアとして流通のシステム開発に携わっています。

この記事は、「クックパッドマートを支えるアカウントたち」連載6本目の記事で、ドライバー向けWebサービスのアカウントの仕組みについて紹介します。 シリーズの全貌については以下の記事を御覧ください。

クックパッドマートを支えるアカウントたち - クックパッド開発者ブログ

クックパッドマートでは実際に商品を購入するユーザーはもちろんのこと、商品を販売する販売者や、販売者からユーザーまで商品を運ぶドライバーなど様々な立場の人が関わってサービスが成り立っています。 それぞれの立場の人に向けて異なるシステムを開発して提供していますが、今回はその中でもドライバーが配送を行うために利用するサービスの概要とアカウントの取り扱い方について紹介します。

ドライバー向けのサービス

ドライバーは商品を届けるために、いつ、何を、どこから、どこに運ぶか知る必要があります。これらの情報をドライバーに提供するため専用のWebサービスを開発しています。商品が流通していく過程では複数の流通経路を辿っており、各流通経路で異なる運び方が必要になるため、それぞれ別のドライバーが担当し、Webサービスも流通経路毎に用意しています。
ドライバー向けのWebサービスについてはこちらの記事でより詳細に紹介しています。

特性と制限

前述のドライバー向けWebサービスの要件を満たすため、アカウントは次のような特性や制限があります。

全てのアカウントを管理

運送会社のドライバーに対して個別にアカウントを発行する必要があります。新しい担当ドライバーに対してアクセスする許可を与えたり、担当から外れたドライバーはアクセスできないようにしたりと細かく制御する必要があります。

流通経路別のアクセス権限をつける

流通経路別に別のサービスを提供しており、それぞれアクセスするドライバーが異なります。とはいえ、ドライバーによっては複数の流通経路を担当することもあったりします。そのため、どの流通経路のサービスにアクセスできるかという権限を設定できる必要があります。

運送会社別にアクセス権限をつける

配送は複数の運送会社の協力のもとで成り立っています。それぞれの運送会社の担当範囲以外にはアクセスできないよう制限をかける必要があります。

権限レベルをつける

アクセスする人にはドライバーの他にもいくつかの種類の人がいて、それぞれのアカウントは以下のような要件を満たす必要があります。

名称 説明 要件
ドライバー 商品を運ぶドライバー いつ、何を、どこから、どこに運ぶかという情報が必要
管理者 ドライバーを管理する運送会社の管理者 複数のドライバーの進捗管理などの管理者用の機能を使う権限が必要
クックパッドの配送管理者 クックパッド側の配送全体の管理者 全ての流通経路のサービス、全ての運送会社の情報にアクセス可能

IDaaSの選定

認証を自前で実装したくないので何らかIDaaSを使用したいと考え、Azure ADを採用しました。前述の特性や制限を満たす使い方ができるということの他に、Azure AD は元々社内の様々なサービスのSSOに利用されているため、

  • 社内の人間のアカウントがすでに存在する
  • 新たなIDaaSの契約が不要で作業工数やコスト面で有利

といったメリットがありました。

Azure AD

構成

ドライバーのアカウントはAzure AD内で以下のようなイメージで構成しています。

詳しく見ていきます。

グループ (Group)

運送会社の権限レベル別にグループを作成しています。つまり運送会社A,Bがある場合、以下の4つのグループを作成します。

  • 運送会社Aのドライバーグループ
  • 運送会社Aの管理者グループ
  • 運送会社Bのドライバーグループ
  • 運送会社Bの管理者グループ

管理単位 (Administrative Unit)

Azure AD のグループへのアカウントの追加・削除といった操作は社内のコーポレートエンジニアリング部門の管理者しか許されていません。
しかし、ドライバーの追加・削除はそれなりの頻度で発生するので、別部署の権限を持つ管理者の負荷が高くなりますし、ドライバーがサービスを使い始めるまでのリードタイムが長くなってしまいます。 そこで流通チーム内で自由にドライバーの追加・削除が行えるよう、Azure AD の管理単位 (Administrative Unit) を使用しています。管理単位はユーザーやグループの管理権限を他のユーザーに委任することができる機能で、ドライバー向けサービス用の管理単位を作成し、運送会社のグループを管理単位の対象として登録しています。
そして流通チームに管理単位のグループ編集権限を渡してもらうことで、流通チームだけでドライバーの追加・削除ができる運用体制を実現しています。

アプリ (App)

ドライバー向けサービス用のアプリを定義しています。アプリへは運送会社のグループと社内管理者(Admin)ユーザーを登録しています。
1つのアプリで全てのドライバー向けサービスの権限を扱っており、細かい権限管理は後述のアプリロールを使って実現しています。

アプリロール (Role)

アプリロールは、グループやユーザーにアプリへのアクセス許可を与える設定です。アプリロールにvalueを設定し、valueをドライバー向けサービス側でチェックすることでアクセス制御しています。valueには 対象のサービス, 運送会社, 権限レベルの情報を持たせており、 対象のサービス/運送会社/権限レベルという形で表現しています。
例えばサービスX(service-x)にアクセスできる運送会社A(company-a)ドライバー(driver)ロールの場合は以下のようになります。

service-x/company-a/driver

また、複数のドライバー向けサービスを単一アプリで扱っているので、理想としてはドライバー向けサービス別にロールを用意し、グループに許可したいサービスのロールを複数付与したくなります。
しかし残念ながら、ユーザーやグループには単一のアプリロールしか設定することができません。そこで、運送会社グループと権限レベル別にロールを用意し、各ロールのvalueには複数のドライバー向けサービスの情報を入力、ドライバー向けサービス側でvalueをパースして権限チェックするようにしています。
先ほどのロールにサービスY(service-y)のアクセス権限も加えると以下のようになります。

service-x/company-a/driver,service-y/company-a/driver

ここでサービスXに運送会社AのドライバーがアクセスするとAzure ADから以下のような情報が渡ってきます。

{"provider": "driver_service",
  "info": {"name": "Takuya Nakamura"
  },
  "extra": {"raw_info": {"name": "Takuya Nakamura",
      "roles": ["service-x/company-a/driver,service-y/company-a/driver"
      ],    }}}

サービスX側ではこの情報の rolesを確認します。

service-x/company-a/driver,service-y/company-a/driver

ここには複数のサービスのロールが含まれているので、パースしてサービスX自身に該当するロールを抽出します。

service-x/company-a/driver

さらにこれをパースし、サービス, 運送会社, 権限レベルに分割します。

service-x # サービス
company-a # 運送会社
driver # 権限レベル

これでサービスX(service-x)に運送会社A(company-a)のドライバー(driver)としてアクセスできることがわかりました。 この情報を元にリクエストに対する認可処理を行い、別の運送会社の情報にアクセスできないようにしたり、管理者権限が必要な操作をドライバーが行えないようにしたりしています。

まとめ

ドライバー向けWebサービスのアカウントについて紹介してきました。改めてまとめると、ドライバー向けアカウントの特性や制約を満たすことができるIdPとしてAzure ADを選択、認可はAzure AD上でサービス用のアプリと運送会社のグループを作成してグループにアプリロールを付加、サービス側でアプリロールに含まれる値をチェックすることで実現しています。
日々流通の形が進化することに伴い、ドライバー向けサービスも日々進化したり新たに生まれるといった変化が起こっていますが、Azure ADを使ったアカウント管理をすることで柔軟に対応できる体制を実現できています。 もし少しでも興味がある方がいらっしゃいましたら是非ご連絡ください。

アカウント連載の記事一覧

技術選定で失敗しない、正解にする力

$
0
0

プログラミングが好きなエンジニアの渡辺です。

先日 TechMTG という社内のエンジニアミーティングの場でお話させて頂いたことを書いてみようと思います。 表題の「正解にする力」というのは様々な意思決定に適用出来るものとして考えていますが、今回は技術選定という観点でお話します。

技術選定というと、世の中のデファクトだとか、新しい技術だとか、社内で実績のある枯れた技術とか色々な理由や基準で選ぶのが良いと、至るところで言われていると思います。 選定時に議論が平行線にならないように、判断基準を設けるべきというのもあるでしょう。 これらは重要であり、検討、準備することは必要ですが、それに加えて「正解にする力」というのも重要なのではないか?という提案です。

まず、組織における技術選定とは「正解を選ぶ」ことではないと思っています。 これはその技術選定結果はその人個人についてまわるのではなく、組織として維持し続けなければならないからです。 また、選定した人だけでなく、組織の他の人も同じように技術を活用している状態を作ることも必要です。 採用理由がいかに合理的であろうが、判断基準を満たしていようが、世の中のデファクトだろうが、賛同者がいなければ独りよがりになってしまいます。 なので重要なのはその選択を「正解にする力」なのです。 もちろん選択も「正解にしやすい」「正解にし続ける」ためにより確度が高い選択はあると思っています。 それは前に述べた、採用理由や基準というものです。 「正解にする力」とは分解すると「正解にしやすくする」と「正解にし続ける」となります。

「正解にしやすさ」とは

「正解にしやすさ」は賛同してもらいやすさとも言えます。 これは世の中のデファクトだったり、流行りだったり、社内実績がすでにあったりと賛同してもらうための条件がすでに整っている場合です。 もしくは提案者の技術的信頼があり、人に賛同して貰うという場合もあるでしょう。 勉強会などの啓蒙活動や実績を作って行くという行動がこれにあたります。

「正解にし続ける」ということ

「正解にし続ける」は組織や、仕組みとして成り立たせることとなります。 技術に完璧な銀の弾丸はありません。 見方を変えればみればどんな技術にもデメリットになるポイントもあるでしょう。 それを踏まえて総合的にメリットが多く、リスクが低いことがある意味技術選定の「正解」と言えるでしょう。 それは総合的なメリットやリスクを抑えるために、前提としている事柄を維持しないと「正解」ではなくなってしまいます。 これを維持するために組織の役割に組み込んだり、仕組み化をすることが「正解にし続ける」ことです。 例えば AWS を使っていなかった組織が AWS を使うようにしたとして、最初の導入は有識者が行ったとしても、それをサービスチームに移管した後、受け入れられる状態にしなければいけません。 サービスチーム側の組織に責任を持つ役割を作ったり、または基盤側の組織としてサポート体制を作ったり、権限管理やコスト管理を効率的に行えるようにしないと属人化したり、評価として行えない状態になってしまいます。 また、デメリットの理解を組織の共通認識にし、それに対する対応方法まで整備することも重要です。 そうしないと後述する技術負債を生むことにもなりかねません。

一人で「正解」にする必要は無い

大きな影響を与える判断を一人で「正解にする、し続ける」ことが難しい場合は協力が必要となります。 その協力を得られるかどうかも、その人の「正解にする力」の一部です。 協力を得るのはチームだけでなく、会社として、ひいては世の中全体までいけば最高ですね。 (今はデファクトでなくても自身がオープンソースなり設計思想なりで広めてしまえばいいということです)

正解を「選ぶ」人

「正解を選ぶ」だけの人はその判断理由や基準に対して正解ではなくなってきた時に何もすることができません。 これは正解とするための前提として置いた事柄が変わった場合の話ですね。 「前はそれで正しかったけど、今はこっちが正しい」となるだけとなってしまいます。 もしくは前提が変わったのに「この技術課題にはこの技術が正しい」と言い続けるだけです。 こういった人の技術選定は最初は採用されても採用され続けるのは難しいと思っています。 なぜなら、技術選定は一回判断すると切り替えるのが難しくコロコロ変わるとついていけないからです。 もちろん、世の中的なデファクトが長く続く技術を選べば「正解を選ぶ」だけでも結果的に正解になることもあります。 私としてはこの「選ぶ」と「正解にする」の違いを理解しているかどうかが、技術選定を任せられるかどうかの判断基準となっています。

今の技術に固執することではない

誤解してほしくないのが、本記事で書いている「正解にする力」は「特定の技術を何が何でも維持する」ということではありません。 変化を起こした方がいいことも、もちろんあります。 なので変化していくための技術革新を「正解」にすることも含んでいます。 そのときに、「今のやつをリファクタリングする時間を使ってでも、こっちにしよう」と働きかけたり、そのための理解を得るようにすることが「正解にする力」です。 理解を得るために、今まで前提としていた事柄を変えにいったり、変えたことによるメリットを最大化するために活動することでもあります。

選定基準

技術選定の基準に関しては本記事の範囲外ですがひとつだけ。 「正解を選ぶ」人はその「技術としての正しさ」を最重要視し、それ以外の事柄をそれに比べて軽視する傾向があると思っています。 「技術としての正しさ」は選定基準の前提であり、それが間違っている技術は候補に上がらないだけです。 技術的正しさというものに尺度があるとして、より技術的に正しいものを選定するのではなく、その候補から自身の組織に対してどれが一番メリットが大きく、リスクが低いかということでしょうか。 このあたりの話は他にもいろいろな技術選定基準や理由を書いてくれている記事はたくさんあると思います。

技術負債

技術負債という言葉には色々な意味があると思いますが、今回の話の文脈でということでお話します。 誰しもが見て「これは技術負債だ」と決まっているものはありません。 ただ、大多数が「これは技術負債だよね」となることはありますよね。 これは、「正解にする力」の「正解にし続ける力」がなかったことも要因の一つではないかと考えています。 技術に対する理解が全員に行き届かず、誤った使い方が蔓延してしまった その技術のデメリットを最小化することを怠った これらを維持するための仕組みがそもそもなかった きっちりその技術に対して、組織で、仕組みで正解にし続けることが出来れば大多数が「これは技術負債だよね」とはならないのでは無いかと思っています。 そしてデメリットの部分は承知の上なので、そこを見て「技術負債だよね」という人も減るでしょう。 そしてそれはその技術を選び続けることが一番のメリットであることを会社的にも証明、維持することと同義です。 そして変化に対応する必要になった場合は、「正解する力」によって変化していくことが出来るので、結果的に技術負債を抱えている期間が短くなるのではと思っています。

「正解にする力」を持っている人

ざっくばらんに言えば、この「正解にする力」を持っている人がテックリードであったり技術選定を任せられる人なのかなと思っています。 技術選定には対象の技術を正しく理解する力はもちろん必要ですが、それをいかに組織の「正解」とするかも同じぐらい重要だと思います。 そして単に、開発のことだけでなく採用や評価制度、育成に対してのメリットを最大化していく意識も必要です。

最後に

この「正解にする力」は技術選定だけではありません。 サービス、機能、組織、評価や評価制度等なんでも自らの「意思決定」に対して当てはまります。 それぞれの意思決定で「課題への正しい理解」「当事者意識」「巻き込み力」「やり抜く力」等をある意味一言で表した言葉です。 これは瞬間的な正解、不正解ではなく、再現性がある継続的な力です。

「正解にする力」という言い回しは、数学のように「正解とはすでに決まっていて、それを見つけるもの」というバイアスがかかっている「正解」に対して、それを自らの力で「正解にしてしまう」という表現にすることで簡潔に私の意図を伝えやすいかなと思ってこの言葉にしています。

いろいろ書きましたが皆様の人生で何かの参考になれば幸いです。 皆様の中での理想の技術選定を歩んでいってください。

クックパッドたんによる正解にする力の要約
クックパッドたんによる正解にする力の要約

要約の画像は知り合いに書いて頂いたものを使わせて頂いています。

twitter.com

【RubyKaigi発表予告】超絶技巧コードコンテストTRICK 2022結果発表

$
0
0

技術部の遠藤です。CookpadのフルタイムRubyコミッタの1人です。RubyKaigiまであと1週間ですね!

今年のRubyKaigiでは、"TRICK"の発表をします。TRICKとは何か。まずはこのRubyコードを見てください。

->\
&\
w{
a=
?a
b=
?b
c=
?,
d=
?.
e=
?e
g=
?g
h=
?/
i=
?{
l=
?l
m=
?_
n=
?$
o=
?1
q=
?'
r=
?<
s=
?s
t=
?>
u=
?u
v=
?v
x=
?(
y=
?)
z=
?-
_=
""
f=
w[
w,
z+
t+
i+
m+
r+
r+
m+
o+
d+
g+
s+
u+
b+
x+
n+
h+
c+
q+
q+
y+
?}
];
f[
'p
ut
s"
He
ll
o,
']
_\
<<
32
f[
'w
or
ld
!"
']
w[
w,
e+
v+
a+
l+
x+
m+
y]
}[
&[
*(
:\
a\
..
:\
ÀÀ
)%
(0
3\
**
7*
47
;)
][
+1
]]

もう一度いいますが、これは「Rubyコード」です。保存して実行すると、"Hello, world!"と出ます。

$ ruby hello.rb
Hello, world!

TRICKとは、こういう変なRubyコードで競い合うプログラミングコンテストです *1。読めないけれど面白い投稿作品(プログラム)が選定され、表彰されます。

TRICKは2013年、2015年、2018年とやってきて、今回は4年ぶり4回目の開催になりますが、いままでで一番レベルの高い戦いになったと思います。というのも、上のプログラムは、TRICK 2022に落選したプログラムです。*2

つまり、当日はこれよりすごい入賞作品たちが紹介されます。TRICK 2022 Resultsは1日目の夕方です。ご期待ください!

rubykaigi.org

おまけ

開発者ブログなので、上記のプログラムの技術解説を軽く書いておきます。

基本構造はこうなってます。

-> &b { b["dummy", "<main code>"] }[&:eval]

ラムダ式に:evalをブロックとして渡すことで、:evalが暗黙的にto_procでProcに変換されます。そのProcに"<main code>"が渡されることで、Kernel#evalが呼び出されて、最終的にmain codeが実行されます。

<main code>のところは、↓のような感じで縦長にしています。

a = ?p
b = ?u
c = ?t
d = ?s
a +
b +
c +
d   #=> "puts"

一番むずかしかったのが:evalというシンボルを作り出すところです。あっさりネタをばらすと、:aから始まるシンボルのRangeを列挙すると、102,789番目が:evalです。

(:a..:ÀÀ).to_a[102789] #=> :eval

:ÀÀというUnicodeなシンボルを使うところが肝。2文字分でありながら4バイト超なので、列挙が途中で打ち切られずに:aから:zzzzまでの配列が得られます。

あとは読み解いてみてください。

なお、実際に投稿したのはHello, world!ではなく、任意のRubyコードを縦長に変換するコードでした。完全な投稿は↓に置いたので興味あればどうぞ。

github.com

おまけのおまけ

ぜんぜん別の宣伝ですが、RubyKaigi当日はぜひ会場内のクックパッドブースに来てみてください! 今年もなんか企画が用意されているはず。

*1:Transcendental Ruby Imbroglio Contest for rubyKaigi の略ですが、この正式名称は遠藤も覚えてないです。

*2:投稿者には採否の連絡をしていないのですが、これは遠藤自身が投稿して落選したコードなので、特別に先出ししています。

Viewing all 802 articles
Browse latest View live