TECH PLAY

マーケティングオートメーション

イベント

マガジン

技術ブログ

はじめに こんにちは。データシステム部・MA推薦ブロックの住安( @kosuke_sumiyasu )です。 私たちのチームは、ZOZOTOWNのメール・LINE・プッシュ通知といったマーケティングオートメーション(MA)の推薦システムを開発・運用しています。目指しているのは、ユーザーひとりひとりに最適な配信を届けることです。 ZOZOTOWNで本番運用されている推薦モデルは、価格・ブランド・カテゴリ・カラーといった テーブル特徴量 のみを学習に用いていました。そのため、商品画像が持つ視覚情報(シルエット・質感・カラー・柄)を活用できていませんでした。「オーバーサイズシルエット」や「光沢感」「チェック柄」といった、人が画像から読み取れる「見た目の好み」を推薦に反映できていなかったのです。 下図は、四角い縁のメガネを好むユーザーを例に、画像から「見た目」を捉えることで目指した推薦の姿を示したものです。従来のモデルではカテゴリは「メガネ」で合っていても、丸縁やサングラスといった「見た目」の異なる商品が混ざってしまいます。一方、画像から「見た目」を捉えられれば、ユーザーが好みそうな四角い縁のメガネを中心に推薦できます。 そこで私たちは、 商品画像から視覚的特徴を捉えた画像特徴量を生成する仕組み を構築し、既存の推薦モデルに特徴量として組み込むことで、「見た目の好み」を捉えるマルチモーダル推薦システムを実現しました。実際に、この推薦モデルをあるメール配信施策に適用しました。A/Bテストの結果、メール経由サイト流入率(CTR)・メール経由購入率(CVR)・経由売上(メール経由で発生した売上)のすべてで有意な改善が得られました。しかもこの画像特徴量は特定の施策にとどまらず、全社のどの推薦・検索モデルからでも利用できる共通の基盤として提供しています。 本記事では、この取り組みの背景にある課題、画像特徴量を生成・提供する仕組み、そして推薦モデルへの特徴量の組み込みで工夫した点を中心に紹介します。マルチモーダルな特徴量を推薦に活かしたい方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 前提となる推薦システム 課題1: 推薦モデルが「見た目」を捉えられていない 課題2: 画像Embeddingを全社で利用できる基盤がない アプローチの全体像 画像Embeddingを安定供給する仕組みの構築 差分更新によるコスト削減 モデル・バージョンを管理し、VIEWで全社へ提供する 推薦モデルのマルチモーダル化によるパーソナライズ精度向上 モデルの選定 事前学習済みモデルを使用した理由 Item Towerへの組み込み Gated Multimodal Unit(GMU)で画像の寄与度を動的に制御する 特徴量単位の Dropout(Feature/Modality Dropout)で特定特徴量への依存を抑える 定量評価(オフライン) 効果 「見た目の好み」の反映による主要指標の改善 全社共通の画像Embedding基盤の整備 まとめ 今後の展望 最後に 背景・課題 前提となる推薦システム ZOZOTOWNのMAにおけるパーソナライズされたアイテム推薦の一部では、 Two-Towerモデル を使用しています。これは、ユーザーを表現するUser Towerと商品を表現するItem Towerの2つのニューラルネットワークからなります。学習済みの各Towerを使うことで、ユーザーと商品の特徴量をそれぞれEmbeddingに変換できます。このEmbeddingは、特徴を捉えた数値ベクトルで、意味の近いものほどベクトルも近くなる性質を持ちます。両Towerの出力を同じ潜在空間上にマッピングするように学習することで、ユーザーとアイテムの近さをコサイン類似度で測れるようになります。推薦時は、任意のユーザーのEmbeddingと各商品のEmbeddingの類似度を計算し、類似度が高い商品から順に推薦します。 ZOZOでは、このEmbeddingを Embedding基盤 として一元管理し、どの部署からでも利用できるようにしています。私たちの 汎用推薦システム も、この基盤を使用して配信する商品を選定しています。 課題1: 推薦モデルが「見た目」を捉えられていない このItem Towerの特徴量は、価格・ブランド・カテゴリ・カラーなどの テーブル特徴量 のみでした。そのため、 ユーザーの視覚的な嗜好を推薦に反映できない という課題が残っていました。同じカテゴリ・ブランドの商品でも、ユーザーが好むシルエットや柄、質感はさまざまです。しかし従来の推薦モデルは見た目の情報を持たないため、「興味のあるカテゴリやブランドは合っているけれど、見た目の趣味は違う」という結果になりがちでした。例えば筆者は、結婚式用に無地のパステルカラーのネクタイを探していたのですが、柄物ばかりが推薦されてしまい、改善の余地を感じていました。 課題2: 画像Embeddingを全社で利用できる基盤がない 商品画像が持つ視覚情報を推薦に活かすには、それを数値ベクトルに変換した 画像Embedding として扱うのが有効です。しかし当時は、商品画像すべてを画像Embedding化する仕組みも、それを全社で共有する基盤も存在していませんでした。そのため、各チームが検索や推薦で画像特徴量を使いたくても、それぞれが独自に実装する必要があり、開発工数の増加や品質のばらつきが生じます。そこで本プロジェクトでは、 画像Embeddingを常に使える状態で組織に提供し続ける基盤 を構築し、それを推薦モデルに組み込むことで「見た目の好み」を捉えられるようにすることを目指しました。 アプローチの全体像 課題を解決するために、大きく2つに取り組みました。 画像Embeddingを安定供給する仕組みの構築 :商品画像から視覚的特徴を表す画像Embeddingを日次バッチで生成し、BigQueryのVIEWで提供する。どの推薦・検索モデルからでも、常に最新の画像特徴量を利用できる 推薦モデルのマルチモーダル化によるパーソナライズ精度向上 :その画像Embeddingを推薦モデルのアイテム特徴量として組み込み、「見た目の好み」を捉えてパーソナライズ精度を高める マルチモーダル推薦は、次の3つのパイプラインで実現しています。 パイプライン 役割 generate-image-embedding 商品画像から画像Embeddingを生成し、BigQueryへ保存する train-product-recommendation 画像Embeddingを特徴量に加えてTwo-Towerモデルを学習する generate-product-embedding 学習済みモデルでユーザー・商品のEmbeddingを生成する このうち、train-product-recommendationとgenerate-product-embeddingは、もともと運用している既存のパイプラインです。今回はそこに、画像Embeddingを生成するgenerate-image-embeddingを新たに追加しました。あわせて、train-product-recommendationのモデルアーキテクチャと入力特徴量を変更しています。 これらのパイプラインで生成したユーザー・商品のEmbeddingを使って、施策ごとに配信商品を選定します。 以降では、本記事の中心である「画像Embeddingを安定供給する仕組みの構築」と「推薦モデルのマルチモーダル化によるパーソナライズ精度向上」を詳しく紹介します。 画像Embeddingを安定供給する仕組みの構築 画像Embeddingの生成パイプラインは、 Agent Platform Pipelines(旧Vertex AI Pipelines) 上に実装し、日次バッチで実行しています。全体像は次のとおりです。 処理は大きく4ステップで構成されます。 Embedding化の対象とするアイテム集合を取得する 商品画像を取得し、Cloud Storage(以下GCS)へ保存する 事前学習済みの画像モデルで画像Embeddingを生成する 生成したEmbeddingをBigQueryへ保存し、VIEWとして提供する 画像Embeddingの生成(ステップ3)には、 Hugging Face で公開されている事前学習済みモデル( SigLIP 2 )をGPU上で利用しています。画像の保存先にはGCS、Embeddingの保存先にはBigQueryを使っています。なお、SigLIP 2を採用した理由は、のちほど「モデルの選定」で説明します。 この中で工夫した「差分更新によるコスト削減」と「全社への提供」を順に紹介します。 差分更新によるコスト削減 ZOZOTOWNで扱う商品画像は、サイト上でアクティブな商品に限っても数千万枚の規模にのぼります。これらをすべてEmbedding化すると計算コストが大きいため、各商品(商品×カラー)につき代表の1枚に絞ってEmbedding化しています。それでも対象は数百万枚あり、さらに新着商品を考えると、毎日およそ数十万枚を新たにEmbedding化する必要があります。 これらを毎日すべて計算し直すと、GCSからマシンへ画像を転送するオペレーション料金や、推論時間の増加に伴うマシン料金がかさみ、個々は小さくても積み重なると無視できないコストになります。そこで、すべての画像を毎日計算し直す 全件更新 ではなく、未処理分のみを計算する 差分更新 を採用しています。具体的には、次の2つのステップで「まだ処理していないものだけ」を対象にします。 画像の保存(ステップ2) :すでにGCSへダウンロード済みの画像は除外し、未取得の商品画像のみを保存する Embedding生成(ステップ3) :すでに計算済みのEmbeddingは除外し、未計算の商品画像のみを対象とする これにより、新着商品だけを処理すればよくなり、ダウンロードコストと計算コストを抑えられます。また、GCSはAgent Platform Pipelinesの実行リージョンと同じRegionalバケットを使うことで、リージョン間レプリケーション費用やエグレス料金も抑えています。 モデル・バージョンを管理し、VIEWで全社へ提供する 画像Embeddingを全社の共通資産として提供するうえで重要になるのが、 モデルとバージョンの管理 です。精度改善のためにモデルを差し替えたり、複数のモデル・バージョンをA/Bテストで並行させたりすることがあります。そのたびに、利用者が「いまどのモデル名・バージョンが最新で有効か」を追いかけてクエリを書き換えるのは負担が大きく、更新への追従漏れも起こる可能性があります。そこで、利用者がそれらを意識しなくても、常に最新の有効なEmbeddingを取得できる仕組みを用意しました。 具体的には、次の3つのテーブル・VIEWでモデルとバージョンを管理しています。 テーブル / VIEW 種別 役割 product_image_embedding_raw テーブル 生成したEmbeddingを、商品ID・モデル名・モデルバージョン・生成日とあわせて追記する。過去分も残すため、複数のモデル・バージョンが共存する model_manifest テーブル 提供対象とするモデル・バージョンにアクティブフラグを立てる product_image_embedding VIEW model_manifestのアクティブなバージョンに絞り、商品ID × モデル名ごとに最新のEmbeddingを返す product_image_embedding_rawテーブルを直接参照する場合は、利用者がクエリのたびにモデル名やバージョンをWHERE句で指定する必要があります。これをVIEWにまとめることで、利用者はproduct_image_embeddingのVIEWを参照するだけで、常にアクティブなモデル・バージョンの最新Embeddingを取得できます。一方でproduct_image_embedding_rawテーブルにはバージョンごとの履歴が残ります。そのため、モデルのA/BテストではTreatment用のVIEWを用意することで、特定バージョンを指定した検証にも対応できます。 この仕組みによって、追跡性と再現性を確保しつつ、A/Bテストにも対応できます。当初の施策にとどまらず、検索や他の推薦面でも安心して利用できる全社共通の資産として提供できるようになりました。 推薦モデルのマルチモーダル化によるパーソナライズ精度向上 画像特徴量を活かしてパーソナライズ精度を高めるために工夫した点を紹介します。工夫したポイントは2つあります。1つ目が「画像Embedding生成モデルの選定」、2つ目が「生成した画像Embeddingを推薦モデルに組み込む方法」です。特に後者が重要で、画像特徴量は単純に足すだけでは効果が薄く、シンプルな2つの工夫を加えることでモデルの精度を大きく改善できました。 モデルの選定 画像Embeddingの生成には、事前学習済みの SigLIP 2 を採用しています。SigLIP 2は、 CLIP から派生したモデルです。CLIP系のモデルは、画像を扱うImage Encoderと、説明テキストを扱うText Encoderの2つから構成されます。学習時は、対応する画像と説明テキストのペアは近づけ、対応しないペアは遠ざけます。こうした対比的な学習をcontrastive学習と呼び、これにより画像と言語が同じ空間で結びつきます。なお、CLIPがsoftmaxベースの損失を用いるのに対し、採用したSigLIP系はこれをsigmoid損失に置き換えている点が特徴です。 画像が言語の意味と対応づけて学習されるため、得られる画像Embeddingは「柄」「シルエット」「質感」といった視覚的特徴を捉えやすいと考えられます。 CLIP系のモデルの中でSigLIP 2を選んだのは、論文記載のとおり、ゼロショットの分類・検索タスクのベンチマークで良い結果が示されているためです。 事前学習済みモデルを使用した理由 ZOZOの商品画像でファインチューニングする選択肢もありましたが、今回は事前学習済みモデルをそのまま使う方針としました。理由は次の3点です。 テキスト側の教師データがない :CLIP系の追加学習に必要な、画像とペアになる説明テキストを大規模に用意できていない まず有効性を検証したい :画像特徴量が推薦に効くかは未検証のため、まずは低コストに効果を確かめたい 基盤モデルの進化が速い :将来、高性能なモデルへ載せ替える余地を残したい Item Towerへの組み込み 画像Embeddingは、まずItem Towerの入力としてそのまま使えるように整えます。下図のように、画像EmbeddingをItem Towerの入力特徴量の1つ(image_embedding)として追加します。User Tower側は変更せず、Item Tower側にのみ画像特徴量を加えています。 使用した画像Embeddingは768次元です。これを価格やカラーといった他のテーブル特徴量とそのまま結合すると、画像だけで次元の大部分を占めてしまい、他の特徴量の影響が埋もれてしまいます。そこで、画像Embeddingを2層の多層パーセプトロン(768 → 256 → 128)で128次元に圧縮してから、他の特徴量と結合します。これにより、画像とテーブル特徴量の次元のバランスを取りつつ、画像から推薦に効く表現を学習できるようにしています。 ただし、この「圧縮してそのまま結合する」方法だけでは、期待したほどの精度改善が得られませんでした。そこで、さらなる精度改善に向けて次の2つの機構を導入しています。 Gated Multimodal Unit(GMU)で画像の寄与度を動的に制御する 次元を揃えて結合するだけでは、画像をどれだけ重視するかが全商品で一律になってしまいます。しかし本来、画像をどれだけ重視すべきかは商品によって異なります。例えば、Tシャツは柄が選択の決め手になるため画像を重視したい一方、靴下はカラーやブランドといったテーブル特徴量で十分なことが多いです。そこで、画像特徴量の寄与度だけをアイテムごとに動的に調整できるよう、 GMU を参考にしたゲート機構を導入しました。 論文の2モダリティ版GMUは2つのモダリティをゲート値で線形補間するため、片方を強調するともう片方が抑制されるトレードオフを持ちます。これに対して本実装は、 他の特徴量はそのままで、画像特徴量にのみsigmoidゲートを掛ける一方向型のゲート を採用しました。これは、2モダリティ版GMUからもう片方を抑制する項を取り除いた独自の変種で、アイテムごとに画像特徴量の重みづけだけを調整できます。これにより、カテゴリやブランドなどのアイテム情報から、その商品で画像特徴量をどれだけ重視するかを動的に決められます。 一方向型にした理由は、既存のテーブル特徴量(カテゴリ・価格など)は複数のA/Bテストで有効性が実証されており、その表現力をそのまま維持した状態で、画像特徴量を追加したかったためです。 特徴量単位の Dropout(Feature/Modality Dropout)で特定特徴量への依存を抑える もう1つの工夫が、学習のたび、入力の一部をランダムにマスクすることで、特定の特徴量への過度な依存を防ぐDropoutです。よく使われるDropoutは個々のニューロン単位でマスクしますが、今回は特徴量単位でマスクする Feature Dropout を行います。なかでも画像Embeddingは、モダリティ全体を1単位としてマスクし、これを特に Modality Dropout と呼びます。実際には、テーブル特徴量(価格・ブランド・カテゴリ・カラーなど)は各フィールドを、画像Embeddingはモダリティをまるごと1つの塊として、それぞれ独立かつランダムにマスクします。 なぜこれが効くのかを、カラーと画像Embeddingを例に説明します。カラーからもユーザーが好む大まかな色味は学習できますが、画像Embeddingを使えば、より詳細なカラーやシルエット、柄まで捉えられる可能性があります。しかし画像Embeddingは複雑で扱いが難しいため、モデルは学習しやすいカラーにばかり頼り、画像Embeddingを十分に活用しないことがあります。そこでカラーをマスクすると、モデルは画像Embeddingからも学ばざるを得なくなり、画像Embeddingの特徴が使われない状態を防げます。逆に、画像Embeddingに偏りすぎる場合も画像Embeddingをマスクすれば、カラーなどのテーブル特徴量から学べます。こうして、どちらか一方に偏らず、画像Embeddingも含めた幅広い手がかりをバランスよく使う、堅牢なモデルになります。 定量評価(オフライン) これらの工夫により、画像特徴量なしのベースラインと比べて、オフラインのRecall@100は段階的に改善しました。 構成 Recall@100(ベースライン比) ベースライン(画像特徴量なし) — + 画像特徴量あり(単純結合のみ) +1.06% + 画像特徴量あり(Feature/Modality Dropout) +11.3% + 画像特徴量あり(Feature/Modality Dropout + GMU) +12.0% 効果 「見た目の好み」の反映による主要指標の改善 構築したマルチモーダル推薦システムを、1配信あたり約700万人を対象とするメール配信施策のアイテム推薦ロジックに適用し、A/Bテストで効果を検証しました。Control(画像Embeddingなし)とTreatment(画像Embeddingあり)を比較し、CTR・CVRはz検定、経由売上はt検定を用いて有意水準5%で評価しました。 その結果、 CTR・CVR・経由売上のすべてで統計的に有意な改善 が確認され、TreatmentがControlを上回りました。以下はTreatmentのControlに対する相対改善率です。 指標 相対改善率 有意差 CTR(メール経由流入数 / 配信数) 約 9.9% あり(勝ち) CVR(メール経由購入数 / 配信数) 約 14.3% あり(勝ち) 経由売上(メール経由の受注金額 / 配信数) 約 10.3% あり(勝ち) ユーザーの「見た目の好み」を捉えた推薦が、実際の流入・購入・売上の改善に結びつくことを確認できました。この結果を受けて本番リリースを決定し、現在は本番環境で稼働しています。 全社共通の画像Embedding基盤の整備 共通基盤の構築により、画像Embeddingを使いたいチームは、生成パイプラインを自前で用意する必要がなく、VIEWを参照するだけで常に最新のEmbeddingを利用できます。これにより、検索や他の推薦面を担当するチームも、開発工数をかけずに効果検証を始められます。さらに、基盤側でモデルを改善すれば、利用側は追加対応なしでその精度向上を受けられます。モデルの差し替えやバージョン管理を基盤の内側に閉じ込めたことで、利用者は中身を意識せずに使い続けることができます。 まとめ 本記事では、商品画像の視覚情報を推薦に活かすマルチモーダル推薦システムの構築を紹介しました。事前学習済みモデルを活用し、少ない工数で画像特徴量を追加して、その有効性まで確かめられました。さらに、生成した画像特徴量を全社で利用できる資産として提供できたことも、大きな成果だと考えています。これにより、画像特徴量を試したい部署は、自分たちで実装しなくてもすぐに効果検証を始められます。そして「画像」という新しい特徴量の軸を手に入れたことで、ここを足がかりに推薦をさらに良くしていけるはずです。 今後の展望 画像Embeddingのさらなる活用と推薦の精度向上に向けて、次のような展開を考えています。 画像Embeddingの活用箇所の拡大 :整備した共通基盤を活かし、検索・他推薦面へも展開する 画像ベースの候補生成への活用 :閲覧・購入した商品と視覚的に似た商品を、推薦候補とする モデルの高度化 :事前学習済みモデルから、ZOZOのデータでファインチューニングしたモデルへ置き換え、ファッションに特化した表現の獲得を目指す 画像の前処理の工夫 :商品領域をバウンディングボックスで検出してクロップ(切り出し)し、周辺の背景ノイズを除いて視覚的特徴をより正確に捉える 最後に ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、MA部SREブロックの片桐です。MA部ではメルマガやLINE、アプリプッシュ通知を配信するためのマーケティングオートメーションシステムを開発・運用しています。 MA部ではDBとして主にCloud SQL for MySQLを利用しており、調査や不具合対応のために開発メンバーがDBにログインして各種SQLを実行する場面があります。 このとき、共用の特権DBユーザーとパスワード認証を利用していました。しかし、この方式ではパスワード管理が必要になるほか、DB上のログイン主体も個人に紐づけにくい状態でした。 これらの課題を解決するために、人間によるDBへのログイン方式を、共用の特権DBユーザーとパスワード認証から個人のGoogle Cloudアカウントを使ったIAM認証へ移行しました。 あわせて、IAM認証でログインする各ユーザーには通常時は参照権限のみを付与し、書込系権限が必要な場合だけGitHub Actionsの承認付きワークフローから一時付与する運用にしました。 本記事では、共用DBユーザーによる運用から個人のIAM認証を使った運用へ移行した背景と、MySQLロールの一時付与を実現するための構成例を紹介します。 目次 はじめに 目次 従来の運用の課題 強い権限が常時使える 個人単位で追跡できない 目指した状態 全体構成 IAM認証で個人ログインにする IAM認証の有効化 IAMデータベースユーザーの作成 Cloud SQLへのログイン権限の付与 MySQLロールでDB内権限を分ける 通常時と一時付与用のロール ロールの作成 参照用ロールの付与 GitHub Actionsを承認ゲートにして書込系ロールを一時付与する GitHub Environmentで申請者以外の承認を必須にする 権限操作用サービスアカウントの作成 ワークフロー設定例 一時付与した書込系ロールを剥奪する 手動でロールを剥奪する 定期実行でロールを剥奪する 運用上の注意点と今後の改善 一時付与したロールを使うときの注意 ワークフロー入力値の検証 SQL本文のレビューと監査 一時付与した権限の失効タイミングの厳密化 MySQLロールの粒度 おわりに 従来の運用の課題 従来の構成ではデータベース操作用の共用特権DBユーザーを作成し、パスワード認証でCloud SQL for MySQLへログインしていました。 構成としては次のとおりです。 ここで利用している Cloud SQL Studio は、Google Cloudコンソール上からCloud SQLへ接続してSQLを実行できるWebベースの画面です。 この運用では、複数人が同じDBユーザーを使ってログインします。そのため、主に次のような課題がありました。 強い権限が常時使える 日常運用におけるデータ調査であれば、多くの場合は SELECT を実行できれば十分です。 しかし、共用の特権DBユーザーを使うと、参照だけで済む作業時でも特権によりデータ変更やDDL操作まで実行できてしまいます。 強い権限を持った状態でSQLを実行すると、誤操作時に本来不要だったデータ更新やスキーマ変更まで起きてしまう可能性があります。そのため、通常時は参照のみを許可し、必要なときだけ書込系権限を一時的に付与する運用にしたいと考えました。 個人単位で追跡できない 共用ユーザーでログインするため、データベースから見ると誰が操作しても同じユーザーに見えます。 そのため、DB上のログイン主体を開発メンバー個人のGoogle Cloudアカウントと結びつけにくい状態でした。 人間のDBログインを個人のIAM認証に寄せることで、少なくともDBへのログイン主体は個人単位で扱えるようになります。 目指した状態 共用特権DBユーザーの課題を踏まえ、今回の移行では次の状態を目指しました。 人間によるDBへのログインを、共用ユーザーではなく個人のGoogle Cloudアカウントに紐づける 通常時は参照権限のみを付与する 書込系権限は常時付与せず、必要なときだけ一時的に付与する 書込系権限の付与申請を簡単に行えるようにする 書込系権限の付与には、申請者以外の承認を必須にする 付与した書込系権限は、作業後または定期実行で剥奪する 今回の構成では、Cloud SQLへのログインにはIAM認証を利用します。一方で、ログイン後にどのSQLを実行できるかはMySQL側の権限で制御します。 さらに、書込系権限は常時付与せず、必要なときだけ承認付きで一時付与して、作業後または定期実行で権限を剥奪します。 そのため、今回の構成ではログイン可否、DB内権限、権限の一時付与、付与後の剥奪を次のように分けて考えました。 項目 役割 利用する仕組み 認証 誰がCloud SQLへログインできるかを制御する Cloud SQL IAM認証 認可 ログイン後に何を実行できるかを制御する MySQLロール 一時付与 必要時だけ書込系権限を付与する GitHub Actionsの承認付きワークフロー 剥奪 一時付与した権限を戻す 手動または定期実行のREVOKEワークフロー この構成により、通常時は参照権限のみを使い、書込系権限が必要な場合だけ承認付きで一時的に付与する運用にしました。 全体構成 今回構築した仕組みは、通常時のログイン経路、必要時における書込系権限の一時付与フロー、一時付与した権限の剥奪フローに分かれます。 通常時は、開発メンバーがCloud SQL Studioから自分のGoogle CloudアカウントでCloud SQL for MySQLへログインします。 本記事ではCloud SQL Studioから接続する例で説明しますが、接続元はこれに限りません。IAM認証に対応した接続方式であれば、同じ考え方を適用できます。 Cloud SQLへのログイン可否はIAMで制御し、ログイン後に実行できるSQLはMySQLロールで制御します。通常時は、開発メンバーに参照用のMySQLロールのみを付与します。 IAM認証へ移行した後の通常時の構成は次のとおりです。 この状態では、開発メンバーはCloud SQL Studioから SELECT を実行できます。一方で、書込系権限は通常時には付与しません。 データ修正などで書込系権限が必要な場合は、GitHub Actionsの手動ワークフローを実行します。ワークフローはGitHub Environmentの承認待ちになり、申請者以外のメンバーが承認すると、対象ユーザーに書込系のMySQLロールを一時的に付与します。 書込系権限を一時付与する流れは次のとおりです。 一時付与した書込系ロールは、作業後の手動実行または定期実行で剥奪します。剥奪の流れは次のとおりです。 剥奪は権限を戻す操作であるため、今回の例では付与時のような承認ゲートは設けていません。手動実行または定期実行でGitHub ActionsからSQL実行基盤を起動し、MySQLロールの REVOKE を実行します。 以降のコード例では、次のプレースホルダーを使います。 プレースホルダー 意味 YOUR_PROJECT_ID Google CloudプロジェクトID YOUR_PROJECT_NUMBER Google Cloudプロジェクト番号 YOUR_MEMBER_NAME 開発メンバーのメールアドレスの @ より前の部分 YOUR_MEMBER_DOMAIN 開発メンバーのメールアドレスのドメイン YOUR_SA_NAME 権限操作用サービスアカウント名 YOUR_DB_NAME 対象のデータベース名 YOUR_TABLE_NAME 対象のテーブル名 YOUR_COLUMN_NAME 対象のカラム名 YOUR_WIF_POOL Workload Identity Pool名 YOUR_WIF_PROVIDER Workload Identity Provider名 YOUR_GITHUB_ENVIRONMENT_NAME 承認ゲートとして利用するGitHubのEnvironment名 IAM認証で個人ログインにする まず、個人のGoogle CloudアカウントでCloud SQL for MySQLへログインできる状態を作ります。 Cloud SQL for MySQLでIAM認証を利用するため、主に次の項目を設定しました。 Cloud SQLインスタンスでIAM認証を有効化する 開発メンバーごとのIAMデータベースユーザーを作成する Cloud SQLへログインするためのIAMロールを付与する Cloud SQL Studioを利用するためのIAMロールを付与する これらの設定は、Google Cloudコンソール、gcloud CLI、Terraformなどで行えます。MA部ではインフラ設定をTerraformで管理しているため、以降ではTerraformでの設定例を示します。 IAM認証の有効化 Cloud SQL for MySQLで IAM認証 を有効化するには、インスタンスのデータベースフラグ cloudsql_iam_authentication を有効にします。 resource "google_sql_database_instance" "main" { # name, database_version, region などは省略しています settings { database_flags { name = "cloudsql_iam_authentication" value = "on" } } } IAMデータベースユーザーの作成 次に、開発メンバーをIAMデータベースユーザーとして作成します。 人間のGoogle CloudアカウントをIAMデータベースユーザーとして作成する場合は、 type に CLOUD_IAM_USER を指定します。 resource "google_sql_user" "member" { project = "YOUR_PROJECT_ID" name = "YOUR_MEMBER_NAME@YOUR_MEMBER_DOMAIN" instance = google_sql_database_instance.main.name type = "CLOUD_IAM_USER" } この例では、IAMデータベースユーザーを YOUR_MEMBER_NAME@YOUR_MEMBER_DOMAIN として作成しています。 Cloud SQL for MySQLでは、IAMデータベースユーザーのメールアドレスの @ より前の部分をMySQL上のユーザー名として扱います。そのため、後続の GRANT では YOUR_MEMBER_NAME を指定します。 Cloud SQLへのログイン権限の付与 IAM認証でCloud SQLへログインするには、 cloudsql.instances.login 権限が必要です。この権限は、事前定義ロールの roles/cloudsql.instanceUser に含まれています。 resource "google_project_iam_member" "cloudsql_login" { project = "YOUR_PROJECT_ID" role = "roles/cloudsql.instanceUser" member = "user:YOUR_MEMBER_NAME@YOUR_MEMBER_DOMAIN" } また、本記事では開発メンバーがCloud SQL Studioから接続する前提のため、Cloud SQL Studioを利用するためのIAMロールも付与します。 resource "google_project_iam_member" "cloudsql_studio" { project = "YOUR_PROJECT_ID" role = "roles/cloudsql.studioUser" member = "user:YOUR_MEMBER_NAME@YOUR_MEMBER_DOMAIN" } ここまでで、開発メンバーが自分のGoogle Cloudアカウントを使ってCloud SQLへログインするための準備が整います。 ただし、IAM認証はCloud SQLへログインする主体を制御する仕組みです。ログイン後にどのSQLを実行できるかはMySQL側の権限で制御します。 MySQLロールでDB内権限を分ける Cloud SQLへのログイン可否はIAMで制御しますが、ログイン後にどのデータベースやテーブルに対して、どのSQLを実行できるかはMySQL側の権限で制御します。 MySQLにおけるロールは、複数の権限をまとめて管理してユーザーへ付与するための仕組みです。本記事では、Cloud SQL for MySQLのMySQL 8.0系でロールを利用する前提で説明します。 通常時と一時付与用のロール 今回は例として、次の2種類のMySQLロールを用意します。 ロール 用途 権限 viewer 通常時の参照用ロール SELECT editor 承認後に一時付与する書込系ロール SELECT , INSERT , UPDATE , DELETE 通常時は、開発メンバーに viewer のみを付与します。これにより、Cloud SQL Studioへログインした直後は参照のみ実行できる状態です。 一方、 editor は通常時には付与しません。データ修正などで書込系権限が必要になった場合だけ、後述するGitHub Actionsの承認付きワークフローから一時的に付与します。 ロールの分け方、付与する権限、対象範囲は、実際の運用や対象データによって調整が必要です。本記事では通常時の参照権限と、承認後に一時付与する書込系権限とで2つに分ける例として説明します。 ロールの作成 MySQL上にロールを作成して、参照や更新に必要な権限を付与します。 この例では、 YOUR_DB_NAME.* に対して権限を付与しています。実際の運用では必要以上に広い範囲へ権限を付与しないよう、対象データや作業内容に応じて、データベース単位、テーブル単位、権限種別を調整してください。 CREATE ROLE ' viewer ' , ' editor ' ; GRANT SELECT ON YOUR_DB_NAME.* TO ' viewer ' ; GRANT SELECT, INSERT, UPDATE, DELETE ON YOUR_DB_NAME.* TO ' editor ' ; 参照用ロールの付与 開発メンバーには、通常時の権限として viewer ロールを付与します。 また、データベースへのIAMログイン直後から標準で有効になるように、 viewer をデフォルトロールとして設定します。 MySQLユーザーは 'user'@'host' の形式で扱われます。本記事のサンプルでは 'YOUR_MEMBER_NAME'@'%' としており、 % は任意の接続元を表すhost部です。 実際の運用では、Cloud SQLへの到達経路やネットワーク制御に応じてhost部を調整してください。 GRANT ' viewer ' TO ' YOUR_MEMBER_NAME ' @ ' % ' ; SET DEFAULT ROLE ' viewer ' TO ' YOUR_MEMBER_NAME ' @ ' % ' ; GitHub Actionsを承認ゲートにして書込系ロールを一時付与する editor ロールが必要な場合は、GitHub Actionsの手動ワークフローから申請するようにしました。 この仕組みにおけるGitHub Actionsの役割は、Cloud SQLへの接続経路そのものではなく、書込系ロールを一時付与するための承認ゲートです。実際に GRANT を実行する処理はCloud SQLへ接続できる実行環境で行います。本記事ではCloud Buildを利用した例として説明します。 GitHub Environmentで申請者以外の承認を必須にする 今回利用するGitHub Actionsのワークフローの中では、 GitHub Environment を承認ゲートとして利用します。 Environmentには Required reviewers を設定し、 Prevent self-review を有効にします。ワークフローのjobで対象Environmentを指定すると、そのEnvironment上での実行前に承認を要求できます。 これにより、申請者以外のメンバーによる承認を挟んでから後続の処理を実行できるようになります。今回の例では、この承認ゲートを使って承認後に editor ロールを一時付与するようにしました。 権限操作用サービスアカウントの作成 承認後に GRANT を実行するため、権限操作用サービスアカウントを用意します。 権限操作用サービスアカウントは、GitHub ActionsからWorkload Identity Federation経由で利用します。承認後は、SQL実行基盤がこのサービスアカウントでMySQLへ接続し、対象ユーザーへ editor ロールを付与します。 このサービスアカウントもCloud SQLへIAM認証でログインできるようにするため、IAMデータベースユーザーとして作成しておきます。 resource "google_sql_user" "grant_sa" { project = "YOUR_PROJECT_ID" name = "YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com" instance = google_sql_database_instance.main.name type = "CLOUD_IAM_SERVICE_ACCOUNT" } この例では、権限操作用サービスアカウントを YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com として作成しています。 サービスアカウントの場合も、MySQL上では @YOUR_PROJECT_ID.iam.gserviceaccount.com を除いた部分をユーザー名として扱います。そのため、後続の GRANT では YOUR_SA_NAME を指定します。 権限操作用サービスアカウントがMySQL上で editor ロールを他ユーザーへ付与できるように、MySQL側では WITH ADMIN OPTION 付きで editor ロールを付与しておきます。 GRANT ' editor ' TO ' YOUR_SA_NAME ' @ ' % ' WITH ADMIN OPTION ; WITH ADMIN OPTION を付与されたユーザーは、そのロールを他のユーザーへ付与できます。つまり、この権限操作用サービスアカウントは書込系ロールの付与経路です。 そのため、Workload Identity Federationの条件やサービスアカウントの利用権限を絞る必要があります。想定したGitHubリポジトリ、ブランチ、Environment以外から利用されないようにしておきます。 承認後に実行するSQLは、最終的には次のような形です。 GRANT ' editor ' TO ' YOUR_MEMBER_NAME ' @ ' % ' ; ワークフロー設定例 GitHub ActionsからGoogle Cloudへの認証には、 Workload Identity Federation を利用します。 サービスアカウントキーをGitHub Secretsに保存せず、GitHub ActionsのOIDCトークンを使ってGoogle Cloudのサービスアカウントを利用できます。 GitHub Actionsの ワークフロー構文 を使った設定ファイルの例は次のとおりです。 name : grant-cloudsql-db-role on : workflow_dispatch : inputs : target_user : description : 対象ユーザーのIAMアカウントメールアドレス type : string required : true role : description : 付与するMySQLロール type : choice options : - editor required : true jobs : grant : runs-on : ubuntu-latest # このEnvironmentにRequired reviewersとPrevent self-reviewを設定する environment : YOUR_GITHUB_ENVIRONMENT_NAME permissions : contents : read # OIDCトークンを発行するために必要 id-token : write steps : - uses : actions/checkout@v4 # Workload Identity FederationでGoogle Cloudへ認証する - uses : google-github-actions/auth@v2 with : workload_identity_provider : projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_WIF_POOL/providers/YOUR_WIF_PROVIDER service_account : YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com - name : Grant DB role run : gcloud builds submit --config=grant.yaml --substitutions="_TARGET_USER=${{ inputs.target_user }},_ROLE=${{ inputs.role }} " ここでは、Cloud SQLがPublic IPを持たず、プライベート経路でのみ到達できる構成として、Cloud Build上で権限操作SQLを実行します。 今回の構成では、Cloud SQLへ到達できるVPC内に中継用Compute Engineインスタンスを配置しました。Cloud Buildからは、そのインスタンス上の Cloud SQL Auth Proxy を経由してCloud SQLへ接続します。 なお、SQLの実行経路は環境に依存します。Cloud SQLへの到達方式や既存の実行基盤によっては、GitHub Actionsのself-hosted runnerやCloud Run jobsなども選択肢になります。 一時付与した書込系ロールを剥奪する editor ロールは一時的な付与を前提としているため、作業後には剥奪できるようにします。 editor ロールの付与は権限を強める操作のため、GitHub Environmentによる承認を必須にしています。一方、 editor ロールの剥奪は一時付与した権限を戻す操作のため、今回の例では承認ゲートを設けていません。 ロール剥奪の方法には、手動実行と定期実行の2つを用意しました。 作業後に任意のタイミングで剥奪するための手動ワークフロー 戻し忘れを抑止するための定期実行ワークフロー いずれの場合も、GitHub ActionsからCloud Buildへ処理を渡し、権限操作用サービスアカウントでMySQLへ接続して REVOKE を実行します。 手動でロールを剥奪する 手動剥奪では、対象ユーザーを入力として受け取り、次のようなSQLを実行します。 REVOKE ' editor ' FROM ' YOUR_MEMBER_NAME ' @ ' % ' ; GitHub Actionsのワークフロー設定ファイルの例は次のとおりです。 name : revoke-cloudsql-db-role on : workflow_dispatch : inputs : target_user : description : 対象ユーザーのIAMアカウントメールアドレス type : string required : true jobs : revoke : runs-on : ubuntu-latest permissions : contents : read id-token : write steps : - uses : actions/checkout@v4 - uses : google-github-actions/auth@v2 with : workload_identity_provider : projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_WIF_POOL/providers/YOUR_WIF_PROVIDER service_account : YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com - name : Revoke DB role run : gcloud builds submit --config=revoke.yaml --substitutions="_TARGET_USER=${{ inputs.target_user }} " 定期実行でロールを剥奪する 手動での剥奪漏れを防ぐため、 editor ロールを定期的にも剥奪するように設定しておきます。 定期剥奪では、MySQL上の現在のロール付与状態を確認し、 editor ロールを保持しているユーザーを対象に REVOKE を実行します。 GitHub Actionsのワークフロー設定ファイルの例は次のとおりです。 name : revoke-cloudsql-db-role-scheduled on : workflow_dispatch : schedule : # 毎日 00:00 JST に実行する # GitHub Actions の cron は UTC 基準のため、15:00 UTC を指定する - cron : "0 15 * * *" jobs : revoke : runs-on : ubuntu-latest permissions : contents : read id-token : write steps : - uses : actions/checkout@v4 - uses : google-github-actions/auth@v2 with : workload_identity_provider : projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_WIF_POOL/providers/YOUR_WIF_PROVIDER service_account : YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com - name : Revoke all temporary DB roles run : gcloud builds submit --config=revoke-all.yaml この例でCloud Buildに渡している revoke-all.yaml では、MySQL上で editor ロールを保持しているユーザーを取得します。そのうえで、権限操作用サービスアカウントのDBユーザーを剥奪対象から除外し、残ったユーザーに対して順に REVOKE を実行します。 権限操作用サービスアカウントから editor ロールを剥奪すると、以降の GRANT や REVOKE を実行できなくなるためです。 運用上の注意点と今後の改善 今回の構成により、人間によるCloud SQL for MySQLへのログインを個人のIAM認証に寄せ、通常時に書込系権限を持たない運用にできました。 一方で、この仕組みをより安全に、かつ便利に運用するには、権限付与後のロールの使い方、ワークフロー入力値の扱い、ロール粒度などを継続的に見直す必要があります。 ここでは、今回の構成における運用上の注意点と、今後の改善余地を整理します。 一時付与したロールを使うときの注意 今回の例では、 editor ロールはMySQLユーザーに対するデフォルトロールとして設定していません。そのため、Cloud SQL Studioなどから一時付与された権限を使う場合は、実行するSQLと同じセッション内で SET ROLE を実行します。 特に、実行単位によってセッションが変わりうる接続方式では、 SET ROLE と対象SQLを同じ実行単位にまとめます。 SET ROLE ' editor ' ; UPDATE YOUR_TABLE_NAME SET YOUR_COLUMN_NAME = ' XXXXX ' WHERE id = XXX ; なお、Cloud SQL for MySQLでは activate_all_roles_on_login フラグを有効にすると、ログイン時に付与済みのロールを自動的に有効化できます。ただし、その場合は一時付与した書込系ロールもログイン時に自動で有効化されるため、通常時に有効化したいロールと一時付与するロールの扱いを踏まえて設計する必要があります。 ワークフロー入力値の検証 本記事では、GitHub ActionsからCloud Buildへ target_user や role を渡し、SQL実行基盤側で GRANT や REVOKE を実行する構成を例にしています。 ワークフロー入力値をもとにSQLを組み立てる場合、想定外のユーザー名やロール名がSQLに含まれる可能性があります。 そのため、GitHub Actions側で入力形式を絞るだけでなく、SQL実行基盤側でも許可したユーザー名やロール名だけを扱うように制御する必要があります。 SQL本文のレビューと監査 今回の構成では、GitHub Actionsから対象ユーザーへの editor ロールの一時付与を申請し、承認を挟むようにしています。 ただし、承認対象は editor ロールの一時付与です。承認後に実行されるSQL本文そのものは、今回の仕組みではレビュー対象にしていません。 SQL本文まで事前に確認する場合は、申請時に実行予定のSQLや作業内容を添付し、承認者による確認を挟む運用も考えられます。 実行後の追跡性を高めるには監査ログやDB監査機能を組み合わせ、誰がいつ、どの操作をしたかを確認できる状態にしておくことも重要です。 一時付与した権限の失効タイミングの厳密化 今回の例では、手動剥奪と毎日0時の定期剥奪で editor ロールを戻す構成にしています。 より厳密に制御したい場合は、付与時刻や申請IDを記録してユーザー単位で失効時刻を管理する設計が必要になります。 MySQLロールの粒度 本記事でのMySQLロールの例としては、書込系権限を editor ロールにまとめました。 ただし、 INSERT 、 UPDATE 、 DELETE 、各種DDLでは影響範囲が異なります。特に DELETE 、 DROP 、 ALTER のような操作は対象データや運用ルールによっては別ロールに分けるほうが安全です。 例えば次のように、MySQLのロールを細分化する余地があります。 ロール 権限 用途 viewer SELECT 通常調査 data_writer INSERT , UPDATE など 手動データ補正 data_deleter DELETE 削除が必要な例外対応 schema_editor CREATE , ALTER など スキーマ変更 schema_dropper DROP 破壊的DDL ただし、ロールを細かく分けるほど、申請フローや承認基準も複雑になります。そのため、対象データ、作業頻度、レビュー体制に応じてロール粒度を調整していく必要があります。 おわりに 本記事では、人間によるCloud SQL for MySQLへのアクセスを、共用の特権DBユーザーとパスワード認証から、IAM認証とMySQLロールを使った運用へ移行した例を紹介しました。 今回の構成では、Cloud SQLへのログインは個人のGoogle Cloudアカウントに寄せ、DB内の権限はMySQLロールで制御しています。通常時は参照用の viewer ロールのみを利用し、書込系権限が必要な場合だけGitHub Actionsの承認付きワークフローから editor ロールを一時付与する形にしました。 これにより、共用特権DBユーザーに依存した人間によるアクセスをやめ、強い権限が常時使える状態を避けられるようになりました。 SQL本文のレビューや監査、ロール粒度の細分化、失効タイミングの厳密化などは、引き続き改善の余地があります。 今回の対応を足がかりとして、運用負荷と安全性のバランスを見ながら改善に取り組んでいきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。医療プラットフォーム本部ビジネス基盤グループでエンジニアをしている熊本です。 ブログへの登場は久々となりますが、2019年に新卒で入社して以来、長らくプロダクト開発のエンジニアをしてきました。そんな私は現在、医療プラットフォーム全体のMA(マーケティングオートメーション)、SFA(営業支援)、CRM(顧客管理)といったITツールおよび業務フローの設計・改善を通じて、事業パフォーマンスの向上を担う開発組織のマネージャーを務めています。 メドレーでは、患者・生活者と、病院・有床診療所、医科診療所、歯科診療所、調剤薬局といった各医療機関向けに事業・プロダクトを展開していますが、今回は、これらの事業を支えるITツールの Salesforce を kintone へ移行したプロジェクトについてお話しします。 背景 SFAが抱えていた課題 弊社では長年、医療プラットフォームにおける複数の事業部門で共通のSalesforceを利用してきましたが、運用を続ける中で以下のような課題が顕在化していました。 最新・正しい情報の把握が困難 :正規化されていないデータや重複管理による情報の分散 データ分析の壁 :分析を見据えた設計になっておらず、プロダクト側の利用状況との突合など、柔軟なデータの加工に工数がかかっていた 非効率な業務フロー :複数ツールの併用や重複する項目の存在などを背景に、手動での転記やダブルチェックが発生している状態 システム管理の属人化 :目的が不明な項目が乱立し、メンテナンス・レポート作成ができる人が限られている状態 また、130個近くのライセンスを保有していたため、これらの課題を踏まえるとコスト過多な状況にも陥っていました。 全ての顧客体験をプロダクト側が理解し、設計責任を持つ 私たちは、 営業・カスタマーサクセスといった顧客活動から、契約・請求に至るまでを「一連のプロダクト体験」として捉え 、その設計・実装にエンジニアが深く関わることを大切にしています。 この考えに基づき、単なるツールのリプレイス(お引っ越し)ではなく、より良い顧客体験につながる合理的で整理された事業オペレーションを実現するため、 業務・データ・ツールを一体でエンジニアが設計し直す 。これが本プロジェクトの重要なポイントでした。 取り組み 体制構築とアーキテクチャ設計 前述の通り、Salesforceはこれまで複数の事業部門で活用してきました。1人で各事業の業務フローや必要データを把握し再設計するのは困難ですし、何より私たちが大切にしている考え方も踏まえ、プロダクトエンジニアが各事業に深く潜り込んで再設計すべきだと考えました。そこで、各事業領域からプロダクトエンジニアを1名ずつアサインする体制としました。私はPMとして全体設計や車輪の再発明が起きないよう各部門の連携をコントロールする役割を担い、各環境のデータ設計・構築はその事業領域担当のエンジニアが主導する形をとりました。 kintoneは責務分離やメンテナビリティを考慮し、事業領域ごとに環境を分けるアーキテクチャを採用しました。また、kintoneへの移行に伴い、コストや親和性を加味してMAツールの移行も行いましたが、本記事では詳細を割愛させていただきます。 医療プラットフォーム本部の組織体制 ツールの移行イメージ 業務理解と要件定義 まずは、日々の事業活動の中で発生するデータや、業務上必要となるデータ、およびそのフローの現状とあるべき姿を描くため、事業部とコミュニケーションを重ねました。長年利用してきたこともありデータ項目が膨大となっていたため、「このデータをどう活用するのか」を重点的に会話し、そもそも不要ではないか、より活用しやすくするにはどうすべきか、などの整理に時間をかけました。 データ設計 特に工夫したのは、DB観点での「正規化」とユーザー観点での「入力のしやすさ」のバランスをとったデータ設計です。kintoneはDBとUIが一体型であるため、このバランス調整にとても苦労しました。一般的なRDBMSではデータの履歴を残すために専用のテーブルを設けイミュータブルモデリングなどを採用する場面でも、kintoneだとテーブル≒アプリとなるため、テーブルを分けることは単純にユーザーの画面遷移や入力箇所を増やすことに直結してしまいます。こういった場合は完全な正規化をするのではなく、普段の商談管理で使用するアプリとその商談ステータスの履歴を管理するアプリを分け、それぞれのアプリに同類のフィールド(RDBMSのカラムをkintoneではフィールドと呼びます)を設置することを許容しつつも、JavaScriptを用いた自動転記の仕組みを作ることで、ユーザーは商談管理アプリだけに入力していれば良いようにするといった工夫をしました。 泥臭く戦ったポイント:データ移行と同期システム ここからは、エンジニアとして特に苦労した2つのポイントをご紹介します。 1. 冪等性と再現性を追求したデータ移行 100名以上が利用するシステムにおいて、特にデータ移行には慎重を期しました。具体的な方法としては、Salesforceのスキーマをkintoneのスキーマに変換し、データをマッピングした結果をCSVに出力するという一連の処理を、CLIコマンド等の整備によりスクリプト化しました。 データ移行では、一度kintoneに投入した後、UAT期間中に事業部からのフィードバックを受けて設計を見直し、再投入が必要になるケースがありました。また、リハーサル目的で、本番移行前にも何度でも再実行できる仕組みが必要でした。そのため、前述のように一連の処理をスクリプト化し、「再現性」を確保しました。さらに、「冪等性」も重要なポイントでした。今回のケースでは、それぞれのアプリにユニークキーを設けて常にUPSERT処理を行うことで、元データが同じであれば、何度実行しても同じ状態を再現できるようにしました。 kintoneへのインポート処理自体はレコード数に比例して一定の時間がかかってしまうものの、事前に手順化しリハーサルを重ねたことで、本番稼働前日の夜間作業で無事に移行を完了させることができました。ただし、(一定の想定範囲ではあったものの)移行作業直前にSalesforce側でデータの更新や削除が行われたことで、kintone側で関連データ同士の紐付けがうまく設定できないケースも発生しました。これはエラーログを見て個別に対処するしかなく、大変苦労した部分でもありました。 2. Salesforceとkintoneの双方向同期システム Salesforceは長年活用してきたことから、いわゆるSFAとしての機能だけではなく、請求や契約管理としての役割も担っていました。今回のプロジェクトでは事業部側が利用するSFAとしての機能はkintoneへ移行しつつも、請求や契約管理といったバックオフィス領域に関しては将来的な販売管理システムへの移行も見据えてスコープ外としていました。よって、これまでは一つのシステムの中で顧客・商談から契約・請求まで一元管理していたものを分離したため、システム間でデータを連携させる必要がありました。 詳細は省きますが、Salesforceに残った契約・請求関連データの一部は、営業やカスタマーサクセスなどの事業部側と契約・請求処理を担うバックオフィス部門の双方向による書き込みが業務上必要であったため、単にkintoneから必要なデータを一方向で送るだけでは要件を満たせませんでした。そのため、SFA領域とバックオフィス領域のハブとなる「商談」などのデータに関しては、Salesforceとkintone間で一部のスキーマ定義を揃え、両システムを一定間隔で同期させる仕組みとしました。 具体的な仕組みとしては、両システムのスナップショットを一定間隔で取得し、前回との差分を検知して互いのシステムへ反映し合う形をとりました。 ここで壁となったのが、「準リアルタイム性」の要求と「データの競合」です。両方のシステムで日常的にトランザクションが発生するため、「同じレコードの同じ項目が、それぞれのシステムで同時に別の値へ書き換えられる」可能性がありました。この競合状態を安全に解決するためのロジックを構築する必要があり、非常に苦労しました。 この対応にあたっては、当初想定していた対象項目が、本当に準リアルタイムで同期が必要なのかを関係者と徹底的に精査しました。その結果、同期間隔の調整や同期対象の大幅な絞り込みができ、現在は安定して稼働する仕組みを実現できています。 ※ データ移行や同期処理のディープな話は盛りだくさんな内容となってしまうため、また別の機会にブログ化できればと思います! 成果 80%以上のランニングコスト削減 契約・請求などを担う組織の必要分を除き、90個ほどのアカウントを削除することができました。kintoneの費用を加味しても、全体のランニングコストとして80%以上削減することができました。 BigQuery集約によるデータ分析・可視化の実現 Salesforceのレポート機能の制約から解放され、より柔軟なデータ活用ができるようになりました。 KPIダッシュボードの構築 :Looker Studioなどのアセットと連携したデータの分析・可視化が可能に 自律的な分析文化 :勉強会の開催やマニュアル整備のおかげもあり、事業部メンバーが生成AIも活用しながら、より自律的かつスピーディなデータ分析が可能に 横断的分析 :kintoneの顧客・商談データ、契約データ、プロダクトデータなどをBigQueryに集約し、多角的な分析を実現 また、横断組織にあるデータ戦略グループが、最近「自然言語でBigQuery等のデータを分析できる社内システム」をリリースしました。今回のプロジェクトによるデータ整備とこのシステムが組み合わさり、よりスピーディにデータ分析を行える環境づくりが加速しています。 関連記事: データ分析AIエージェントの実践 - Slack × Devin × Context Engineering 今後の展望 本プロジェクトによって多くの成果を得られましたが、私たちはまだ「業務やデータの一部をツールの移行と共に再設計し、基盤を整えた」に過ぎません。 今後はこの基盤を活かし、KPIなどの定量的なデータや現場ヒアリングを通じて事業の課題・ボトルネックを特定し、継続的に改善を回すサイクルを作っていきたいと考えています。 また、日々顧客と向き合う事業部のメンバー自身が、データ活用や拡張性を見据えて自律的にツールを改修できる状態こそが、組織のアジリティを最も高く保ちながらスケールできる理想の形だと考えています。その実現に向けて、エンジニアリングの専門性を持つ私たちビジネス基盤グループが、誰もが改修・改善しやすい仕組みづくりを牽引していきたいと考えています。 まとめ エンジニアが事業の深い部分に入り込み、業務・データ・ツールを一体で再設計するプロジェクトについてご紹介させていただきました。 メドレーでは、「医療ヘルスケアの未来をつくる」というミッションのもと、エンジニアがビジネスの根幹に関わり、プロダクトと事業を共に成長させる文化があります。自ら課題を発見し、設計から運用までをエンジニアとしてのリーダーシップを発揮しながら一気通貫で推進できる方を絶賛募集しています。 メドレーで働く|株式会社メドレー メドレーでの働き方や人事制度、求人情報など、採用に関する情報をご紹介します。 www.medley.jp 少しでも興味を持っていただけましたら、ぜひメドレーの採用ページをご覧ください!

動画

書籍