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

買物プロダクト開発部の中村です。クックパッドマートという生鮮食品の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を使ったアカウント管理をすることで柔軟に対応できる体制を実現できています。 もし少しでも興味がある方がいらっしゃいましたら是非ご連絡ください。

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

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

クックパッドマートの開発に携わっているソフトウェアエンジニアの塩出(@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
# 新旧の認証方法に関わらず、現在選択中(ログイン中)の店舗を返却するメソッド
def current_shop
  if 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で認証を受けている場合、その店舗を返却するメソッド
  end
end

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

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

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

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

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

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

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

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

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

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

法務の観点

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

運用の観点

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

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

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

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

最後に

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

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

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

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

info.cookpad.com

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

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

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

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

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

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

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

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

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

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

ログインユーザー

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

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

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

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

ログイン処理の流れ

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

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

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

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

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

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

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

# ユーザー
class User < ApplicationRecord
  has_many :orders
  has_one :user_location, dependent: :destroy
end

# 注文履歴
class Order < ApplicationRecord
  belongs_to :user
end

# ユーザーの受け取り場所
class UserLocation < ApplicationRecord
  belongs_to :user
end

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

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

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

class Order < ApplicationRecord
  belongs_to :user
  
  def self.migrate_user_account(source_user:, destination_user:)
    source_user.orders.update_all(user: destination_user)
  end
end

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

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

class UserLocation < ApplicationRecord
  belongs_to :user
  
  def self.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)
    end
  end
end

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

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

ユーザー情報の削除

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

class User < ApplicationRecord
  def self.migrate_user_account(source_user:, destination_user:)
    source_user.destroy!
  end
end

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

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

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

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

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

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

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

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

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

ログインの高速化

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

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

おわりに

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

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

https://cookpad.careers/services/cookpad-mart/

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