株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1033

はじめに こんにちは、データサイエンス部の大川( @o_tomo03 )です。私たちは、WEARにおける「似合う」をユーザーに届けるため、LLMやマルチモーダルAIを活用してコーディネートの特徴抽出や似合うに関する独自の判定処理のR&Dを行っています。 その中核を担うのが、全身のコーディネート画像から特徴を抽出するVLM(Vision Language Model)のプロンプトです。このプロンプトの精度を上げようとすると、地道な手作業のチューニングが延々と続きます。みなさんも一度は経験したことがあるのではないでしょうか? 当初、私が実際に回していたのは次のようなループでした。 LLMに推論させ、正解データと突き合わせる 間違えたサンプルを開き、LLMの出力・判断理由・画像を分析する 「なぜ間違えたか」を考え、プロンプトの定義や例示を書き直す もう一度評価し、評価指標が上がったか確かめる 効いた/悪化した施策を記録し、次に活かす この「 失敗を見る → 直す → 再評価する 」を、ファッション特徴の数(例:アイテム、素材、柄など)だけ繰り返します。1つのプロジェクトのチューニングに、数週間を費やすこともありました。 以前、関連する取り組みとして「LLMの構造化出力エラーを削減する」という記事を公開しました 1 。あちらは 出力の正しさ (定義外の値を出さないこと)を、バリデーションとエラーフィードバックで守る話でした。本記事はその一歩先、 "精度そのもの"を上げるチューニングのループごと、AIエージェント(Claude Code)に任せられないか という仮説です。 本記事で扱う問いはシンプルです。 人間が手で回していたプロンプトチューニングのプロセスを、Claude Code の自律ループで 再現 できるか。人と同等の精度に、どれだけ短い時間で到達できるか。 ポイントは、新しい最適化アルゴリズムを発明することではなく、 人の作業手順をそのまま機械へ移す ことです。 目次 はじめに 目次 サマリー 背景 ── ハーネスとループエンジニアリング アイデア ── 人のチューニングループを分解する reasoningを勾配として使う システム設計 ── 人の動きを sub-agent に割り当てる 実験セットアップ 対象タスク 推論・最適化モデル 実験設定 実験結果 実験結果1 ── 人の動きを再現できたか(T1) 工数:どれだけ短縮したか 実験結果2 ── スキーマ構造が異なる他タスクでも汎化するか(T1~T3) 実験結果3 ── reasoning は本当に効くのか(ablation、T1のみ) 実践から得た知見 エージェント設計は「自分がどう解いていたか」の移植から始める 過学習とプロンプト肥大化に注意する コストに注意する 今後の展望 まとめ おわりに サマリー 人手のプロンプトチューニング(失敗分析→書き換え→再評価)を、Claude Codeのsub-agentで 役割ごとに分解して自動化 した( /tune スキル)。 スキーマ構造が異なる 3タスク(T1~T3) で、特徴の定義文のみを列挙した初期プロンプトを出発点に自動最適化を実行。 結果: 精度:T1では自動最適化後のF1が人手チューニングをN=1ながら 同等以上の水準 を達成(train +0.024、test +0.017)。 工数:全体評価サイクルの工数を人手約3.5週間から約1週間に短縮。 汎化:スキーマ構造が異なるT1~T3で適応可能性を確認。 仕組みの核心:LLMの推論時にファッション特徴と 判断理由(reasoning) を合わせて構造化出力している。 reasoningを 「次にどう直すか」の信号(≒勾配) として使い、参照あり/なしのablationでその寄与を検証した。 背景 ── ハーネスとループエンジニアリング 近年、LLM活用の関心は「良いプロンプトを書く」から一段上がり、 エージェントを動かす仕組みそのものの設計 に移ってきました。 ハーネス(harness) :モデル本体を除いた周辺すべて。システムプロンプト、ツール、権限、コンテキスト管理、検証ループ、ログなど。「Agent = モデル + ハーネス」のモデル以外の側と覚えれば直感的。 ループエンジニアリング(loop engineering) :ハーネスを停止条件やスケジューラで 自律的に回し続ける よう設計する、一段上の制御プレーン。「自分でClaudeにプロンプトを出す」のをやめ、 「Claudeへプロンプトを投げるループ」を設計する という発想の転換がコア。 本記事でやるのは、まさにこのループエンジニアリングの実践例です。用語の整理や系譜の詳細は割愛します 2 。 アイデア ── 人のチューニングループを分解する 自動化のため、まず取り組んだのは 自分が手で行っていた作業を要素へ分解する ことでした。以下の表の(1)〜(5)が、そのまま /tune のsub-agentに対応します。 人手ループのステップ 人が何をしていたか 自動ループの担当 (1)失敗を見る 誤答サンプルのLLM出力・ reasoning ・画像を眺める analyzer (2)原因を考える 定義と判断のズレ・過検出/見落としを特定 planner (3)プロンプトを直す 定義補足・例示追加・曖昧さ排除 improver (4)再評価する 評価を回してF1を確認 run_eval.py (5)良し悪しを覚える 「前にこれで悪化した」を記憶し次に活かす retrospector reasoningを勾配として使う (1)で人が行っていたのは、 LLMの「なぜそう判断したか(reasoning)」を読んで、次にプロンプトをどちらへ動かすかを決める ことです。機械学習の言葉に置き換えると次のようになります。 F1スコア ≒ 損失 (どれだけ外したか、スカラー) reasoning ≒ 勾配 (どの方向に直せば良くなるか) つまりこのループは、 プロンプトという"パラメータ"を、reasoningという"勾配"で更新する勾配降下 とみなせます。同じ着想はTextGrad 3 やOPRO 4 / APO 5 にも見られ、LLMの出力を自己改善の信号として使う研究の一例です。 システム設計 ── 人の動きを sub-agent に割り当てる /tune は1イテレーションで次のフェーズを回します。 スキルのディレクトリ構成は次のとおりです。 .claude/skills/tune/ SKILL.md # オーケストレーター:ループ制御に徹し、処理は sub-agent に委譲 agents/ analyzer.md #(1)失敗分析 planner.md #(2)改善計画 improver.md #(3)プロンプト改善 retrospector.md #(5)振り返り:DO/DON'T を lessons.md に追記 ... references/ optimization-policy.md # 採用/棄却・停止条件(単一真実源) prompt-design-strategies.md # プロンプト改善パターン集(Gemini公式ドキュメント) ... scripts/ ... 設計上のポイントは4つです。 SKILL.md をオーケストレーターとする : SKILL.md を「全体フローの制御」のみに徹し、専門的な処理は外部( agents/ )へ委譲している。わずか115行で記述。 optimization-policy.md を単一真実源とする :改善の優先順位、採用/棄却の判定、停止条件を1ファイルに集約。すべてのsub-agentがこの方針に従うよう設計している。 lessons.md = 人の記憶 :各イテレーション後に retrospector がDO(効いた施策)/ DON'T(悪化させた施策)を追記している。人手でチューニングしていると徐々に知見が溜まっていくので、そのメモリー機能を再現している。 prompt-design-strategies.md :Gemini公式のプロンプト改善手法 6 の要約を常に参照させることで、改善手法の選択を体系的に行えるようにしている。 なお、実験パラメータ(モデル、LLMパラメータ、最大イテレーション、データセット、実験ログ保存先など)は experiment_config.json に集約しています。実行前にユーザーが確認して自動最適化を開始する仕組みです。 実験セットアップ 対象タスク 全身のコーディネート画像から複数の特徴を抽出するタスクです。例えばT1では、アイテム・着こなし・サイズ感などの特徴を1つのプロンプトでまとめて抽出します。特徴の性質に応じて2値分類や多クラス分類などを使い分けています。 タスク 特徴の種類 特徴数 データ規模 人手チューニング比較 T1 アイテム・着こなし・サイズ感など 数十程度 数百件規模 あり T2 トップス・アウター・バッグなどの着用アイテム 数十〜百近く 数千件規模 なし T3 素材・柄・装飾・シルエットなど 数十〜百近く 数千件規模 なし 推論・最適化モデル 推論モデルにはGemini、最適化モデルにはClaude Codeの claude-sonnet-4-6 を使用しています。タスクによって推論モデルが異なります。 タスク model_id temperature thinking_level max_output_tokens max_iter T1 gemini-2.5-flash-lite 0.0 null 8192 10 T2 gemini-3.1-flash-lite 1.0 MINIMAL 8192 10 T3 gemini-3.1-flash-lite 1.0 MINIMAL 8192 10 実験設定 今回の実験では、以下の3軸で自動最適化を評価します。 実験1 — 人の動きを再現できたか(T1) :プロンプトエンジニアリングを一切行わずに、特徴の定義文のみを列挙した初期プロンプトを出発点に自動最適化を実行します。これまでの人手チューニング結果(F1: train 0.817 / test 0.847)と比較し、精度が人手を上回るか、工数はどれだけ短縮されるかを確認します。 実験2 — スキーマが異なる他タスクでも汎化するか(T1~T3) :同様の手順でT2・T3にも適用し、初期からの改善幅とtestへの汎化を確認することで、ループの適用可能性を検証します。 実験3 — reasoning は本当に効くのか(ablation、T1のみ) :「reasoningを勾配として使う」という設計の妥当性を検証します。reasoningをClaudeに見せる条件A( /tune )と見せない条件B( /tune-no-reasoning )の2条件をT1で比較します。 各実験に共通する設定は以下のとおりです。 評価指標 : feature F1 (特徴ごとのF1、例:サイズ感)と leaf F1 (各ラベルのF1、例:「サイズ感=オーバーサイズ」)のマクロ平均。leaf F1はより細粒度で、本実験の主評価指標とする。 停止条件 :以下のいずれかを最初に満たした時点でループを停止する。 (1)全feature・leaf F1 ≥ 0.7(目標達成) (2)最大10イテレーション到達 (3)3イテレーション連続で収束(leaf F1改善幅<0.005かつleaf改善数が不変) 実験結果 実験結果1 ── 人の動きを再現できたか(T1) T1について、これまでの人手チューニング結果と自動最適化後のleaf F1スコアを比較します。 タスク データ 人手チューニング leaf F1 自動最適化後 leaf F1 T1 train 0.8165 0.8403 (iter3) test 0.8473 0.8641 (iter3) 所見 :train +0.024、test +0.017で自動が人手を上回りました。trainでの改善はtestにも汎化しており、feature F1が0.7未満のfeatureを6→2に削減しました。 工数:どれだけ短縮したか 工数の比較は2つの粒度で行いました。 チューニングサイクル(1回) はエンジニアがleaf F1を改善し、ドメインエキスパートへ定性評価依頼が出せる状態にするまでの工程です( /tune が自動化する範囲)。 全体評価サイクル(最大3回) は「チューニング → ドメインエキスパートによる定性評価 → フィードバック反映」を1セットとし最大3回繰り返すサイクルです。 人手チューニング 自動最適化 速度比 チューニングサイクル(1回) 約6時間 約2.5時間 約2.4倍速 全体評価サイクル(最大3回) 約3.5週間 約1週間 約3.5倍速 人的介在 常時 config確認のみ — イテレーション数(全体評価サイクル) 30(プロンプトバージョン) 10~20 — ※ 全体評価サイクルの速度比がチューニングサイクル1回の速度比を上回るのは、自動化によりイテレーション数が減少したことに加え、ドメインエキスパートとのスケジューリングや待機時間が発生するためです。数値はいずれも実測値です。 実験結果2 ── スキーマ構造が異なる他タスクでも汎化するか(T1~T3) T1~T3のスキーマ構造が異なる3タスクで汎化性能を検証した結果を示します。 タスク データ 初期プロンプト leaf F1 自動最適化後 leaf F1 改善量 T1 train 0.7916 0.8403 (iter3) +0.049(+6.2%) test 0.7842 0.8641 +0.080(+10.2%) T2 train 0.8498 0.9143 (iter9) +0.065(+7.6%) test 0.8481 0.9184 +0.070(+8.3%) T3 train 0.5683 0.7633 (iter10) +0.195(+34.3%) test 0.8721 0.8794 +0.007(+0.8%) 所見 :3タスクすべてでleaf F1が向上しました。T3のtrain改善幅(+34.3%)は最大ですが、train初期値(0.57)が他タスクより低くtest初期値(0.87)と乖離しており、改善幅はその初期値の低さを反映しています。これはtrain側に初期プロンプトが苦手とする難しいサンプルが偏っていたためと解釈しています。いずれのタスクでもtestでの劣化はなく、feature F1が0.7未満のfeature数はT1で6→2、T3で5→1に削減しました。 実験結果3 ── reasoning は本当に効くのか(ablation、T1のみ) 「reasoningを勾配として使う」という主張の裏を取るため、ablationを実施しました。最適化モデルにGeminiのreasoningを 見せる条件(A = /tune )と見せない条件(B = /tune-no-reasoning ) の2条件を比較しています。 iter別 leaf F1 推移(T1 / train・test) iter A train A test B train B test 1(初期) 0.7916 0.7842 0.7834 0.8005 2 0.8368 0.8635 0.7906 0.7990 3 0.8403 ← A best 0.8641 ← A test best 0.8239 0.8494 ← B test best 4 0.8359 0.8052 0.8354 0.8124 5 0.8377 0.8346 0.8256 0.7803 6 0.8341 0.8318 0.8299 0.8472 7 0.8385 0.8081 0.8513 ← B best 0.7983( 低下 ) 8 0.8377 0.8338 0.8202 0.7523 9 0.8296 0.7720 0.8144 0.7896 10 0.8376 0.8467 0.8152 0.8137 所見 :trainではB(no-reasoning)のbest(0.8513, iter7)がA(0.8403, iter3)を +0.011上回りました。しかし、testではAが 0.8641と汎化した 一方、Bは 0.7983に落ち込みtrain過適合 が明らかになりました。画像情報に加えreasoningを参照することで、VLMの思考をより詳細に把握でき、汎化しやすいプロンプト探索を助けていると考えます。 人手チューニングとの関係 :人手側はGeminiのreasoningと画像を参照しながら、約30イテレーションの試行履歴とドメイン知見も活用できました。 情報量・試行回数の両面で優位だったにもかかわらず 、条件Aの自動ループが同等以上の水準(実験結果1)に到達したことは、reasoningを活用するループが人間の作業を代替しうることを示唆します。 実践から得た知見 エージェント設計は「自分がどう解いていたか」の移植から始める エージェント設計は、いきなりアーキテクチャを考えるのではなく、 自分がそのタスクをどう解いていたかを細分化し、そのままエージェントに乗せる のが効くことを学びました。また、最初からこの設計だったわけではなく、回しながら lessons.md や prompt-design-strategies.md を足していき徐々に今の形になりました。完璧な設計を最初から目指すのではなく、まず小さいPoCを回して改善していく方が結果的に早いと思います。 過学習とプロンプト肥大化に注意する 過学習は最も注意した点です。ループごとに定量的な精度が向上する一方で、 プロンプトは徐々に肥大化していきました 。近年のVLMは多少プロンプトが長くなっても推論能力が大きく下がらない印象ですが、学習データに寄った記述が増えるほど過学習のリスクは高まります。最低限 学習データ・テストデータを用意しておく ことをお勧めします。 コストに注意する ループごとに全画像へ推論を回すため、API課金が積み上がります。改善のたびにまず少量サンプルで実験し、有望なときだけ全量評価する仕様にすると、コストは削減できるはずです。ループの設計を誤ると 予期せずバックエンドで推論し続け、コストが跳ね上がる ので注意が必要です。 今後の展望 DSPy等の既存フレームワークとの比較 :本アプローチで十分な精度を得られたため今回は試しませんでしたが、DSPy等(GEPA等のオプティマイザを含む)と精度・効率を比較したい。 N数を増やして再現性を確認する :今回の結果はN=1の速報値のため、実行間のばらつきを評価できていない。複数回実行し、数値の信頼性を確かめたい。 社内ツール化・全社展開 :本アプローチを特定タスク専用の仕組みで終わらせず、誰でも自分のタスクに使えるよう汎用化して展開することを目指す。 まとめ 人手のプロンプトチューニングをClaude Codeのスキルで代替することで、人手チューニングと同等以上の性能(leaf F1でtrain +0.024、test +0.017)を発揮しました。工数面では全体評価サイクルを約3.5週間から約1週間に削減でき、スキーマの異なる他タスクへの適用可能性も確認しました。今回の取り組みを通じ、エージェント活用のあり方は「人間がプロンプトで指示をする」から「ループを回すためのエージェント設計」へ移りつつあると実感しました。本記事がループエンジニアリングに取り組む際の参考になれば幸いです。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com LLMの構造化出力エラーを削減する - ZOZO TECH BLOG ↩ Claude "Getting started with loops" ↩ Yuksekgonul et al. "TextGrad: Automatic 'Differentiation' via Text" ↩ Yang et al. "Large Language Models as Optimizers" (OPRO) ↩ Pryzant et al. "Automatic Prompt Optimization with 'Gradient Descent' and Beam Search" (APO) ↩ Google "Gemini API プロンプト設計戦略" ↩
はじめに こんにちは、データシステム部MLOpsブロックの 木村 と、推薦基盤ブロックの 上國料 です。 ZOZOでは2025年7月より、Claude Codeをはじめとする各種AI開発ツールを利用できる制度を開始しました。 corp.zozo.com 現在ではこの制度のもと、数百名にのぼる社員がClaude Codeを活用しています。 Claude Codeの活用が進むにつれ、スキルやエージェントといった開発資産がチームごとに蓄積されていきます。しかし、それらの資産がチーム内に閉じたままでは、組織全体で見たときに「AI開発が進むチーム」と「遅れるチーム」の差が広がる一方です。 この差を埋めるには、チームをまたいで開発資産を共有できる場が必要です。理想は、他チームの開発資産であっても、自チームの開発資産と同じようにいつでも参照・流用でき、それを起点に開発を始められる状態です。 本記事では、2025年下期から複数チームで運用してきた1つのClaude Codeプラグインマーケットプレイスについて紹介します。共同運用で直面した課題とその対策、そして得られた効果をまとめました。 本記事の内容は2026年8月時点の情報であることにご留意ください。 目次 はじめに 目次 背景 Claude Code プラグインマーケットプレイスとは 複数チームで運用するときに直面した課題 1. プラグインのグルーピングとメンテナンス責任が不明確になる 2. 共通ファイル(marketplace.json)の編集がコンフリクトし、登録漏れも起きる 3. 構造の誤りがレビューで見逃され、全チームに波及する 4. 目的に合致するスキルを検索で見つけられず、同じスキルが再発明される 5. 影響範囲が見えず、既存プラグインの修正が先送りされる 5つの課題への個別対策 1. チーム単位のディレクトリで責任範囲を分ける 2. 各プラグインのメタデータからmarketplace.jsonを自動生成する 3. 静的バリデーションで構造の誤りを検出する 4. やりたいことからスキルを逆引きできるようにする 5. @claudeの自動チェックで影響範囲への不安を減らす 複数チームで共有した効果 他チームの動きが見えるようになった 資産の横展開が生まれた 属人化が減り、誰でもすぐに始められるようになった 今後の展望 カタログの肥大化にどう向き合うか プラグインの追加・変更の周知コストをどう下げるか Claude Codeの進化の速さにどう追随するか 運用サイクルそのものを改善の対象にする まとめ 背景 Claude Code プラグインマーケットプレイスとは プラグインマーケットプレイス とは、Claude Code用の「プラグイン」を配布し、発見・バージョン管理・自動更新までまとめて行えるカタログのことです。 プラグイン自体は、以下のコンポーネントを1つのパッケージにまとめたものです。なお本記事では、CLIツールとしてのClaude Codeを指す場合は「Claude Code」、その基盤となるAIモデルを指す場合は「Claude」と表記します。 要素 役割 Skills Claudeが参照するガイドラインや、 /create-pr のようなスラッシュコマンドの実体 Agents 特定の作業を自律的にこなすサブエージェント Hooks 特定のイベント(タスク完了時など)で発火する処理 MCP Servers GitHubやNotionなど外部サービスと連携するためのサーバー設定 プラグインを使い始めるまでの手順は、 公式ドキュメント にもあるとおり、マーケットプレイスの追加と、そこからのプラグインの個別インストールという2段階に分かれています。追加した時点ではカタログを参照できるようになるだけで、プラグインはまだ1つもインストールされていません。 この2段階は、実際には次の2つのコマンドで実行します。 /plugin marketplace add <org>/cc-plugin-marketplace /plugin install my-plugin@cc-plugin-marketplace リポジトリ全体は次のような構成です。 リポジトリルート/ ├── .claude-plugin/ │ └── marketplace.json # カタログ全体の定義(全プラグインを列挙) └── my-plugin/ ├── .claude-plugin/ │ └── plugin.json # このプラグインのメタデータ └── skills/ └── ... マーケットプレイスの実体は、Gitリポジトリのルートに置く .claude-plugin/marketplace.json という1つのJSONファイルです。ここに配布する全プラグインの名前・リポジトリ内のパスなどを列挙します。 一方、各プラグインのディレクトリには .claude-plugin/plugin.json を置き、そのプラグイン自身のメタデータ(名前・バージョン・説明など)を記述します。つまり、プラグイン単位の情報はplugin.jsonに、それらを束ねるカタログ全体の情報はmarketplace.jsonに持つという2階層の構成です。marketplace.json側のpluginsエントリは、plugin.jsonの内容にsourceパスを加えたものです。 実際のファイルの中身は次のとおりです。 my-plugin/.claude-plugin/plugin.json : { " name ": " my-plugin ", " description ": " PR作成を支援するスキル集 ", " author ": { " name ": " Taro Yamada " } } ※ versionフィールドはあえて省略しています。省略するとClaude CodeはGitのcommit SHAをversionとして扱うため、マージがそのまま最新版の配信になります。 公式ドキュメント も、開発が活発な社内・チーム向けプラグインには、最もシンプルな構成としてこのパターンを紹介しています。 .claude-plugin/marketplace.json : { " name ": " cc-plugin-marketplace ", " plugins ": [ { " name ": " my-plugin ", " source ": " ./my-plugin ", " description ": " PR作成を支援するスキル集 " } ] } 複数チームで運用するときに直面した課題 もともと各チームは、それぞれ個別にスキルやエージェントを開発し、チーム内だけで使っていました。それを、前述の .claude-plugin/marketplace.json の仕組みを使って1つのプラグインマーケットプレイスにまとめ、複数チームで共同構築・運用する形に切り替えました。複数チームで1つのマーケットプレイスを共有するようになると、便利さの一方でいくつかの課題に直面しました。 私たちの場合、プラグインは開発からチームを越えて使われるまでに、次のような流れをたどっています。 複数チームが1つのマーケットプレイスを共有すると、この流れの各所で課題が起きやすくなります。いずれも参加チームとプラグインの数が増えるほど発生頻度が上がります。次節では、上記の流れの各段階に対応する5つの課題を順番に説明します。 1. プラグインのグルーピングとメンテナンス責任が不明確になる プラグインを「(1)開発する」段階で最初に直面するのが、マーケットプレイスのリポジトリ内でプラグインをどうグルーピングするかという問題です。マーケットプレイスには、PR作成支援のように全チームで使えるプラグインもあれば、パイプライン構築のように特定チームの業務に固有のプラグインもあります。これらを区別なく、次のようにフラットに並べると、利用者は自分に関係のないプラグインに紛れて目当てのものを見つけにくくなります。 リポジトリルート/ ├── .claude-plugin/ │ └── marketplace.json ├── plugin-a/ ├── plugin-b/ ├── plugin-c/ └── ... さらに深刻なのはメンテナンス責任の所在です。あるプラグインが壊れたとき、それを直す責任がどこにあるのかが、ディレクトリ構造上どこにも表現されていません。plugin.jsonのauthor情報をたどれば作者は判明しますが、そのプラグインがチーム内利用を想定したものか、全チーム向けの共通利用を想定したものかまでは読み取れません。その結果、壊れたまま直されない状態が続いたり、他チームのプラグインだと気付かずに手を入れてしまったりします。 2. 共通ファイル(marketplace.json)の編集がコンフリクトし、登録漏れも起きる 「(2)PRで反映する」段階でまず起きるのが、marketplace.jsonのコンフリクトです。前述の通り、Claude Codeの仕様でmarketplace.jsonには全チームの全プラグインの情報が1ファイルに列挙されています。そのため、Aチームが自分のプラグインを更新するPRも、Bチームが新しいプラグインを追加するPRも、最終的に同じファイルを編集することになります。マージのタイミングが重なればコンフリクトし、各チームが自律的に開発を進めるほどコンフリクトの頻度も上がります。 加えて、plugin.jsonとmarketplace.jsonは名前や説明といった情報を重複して持つため、手動で編集する場合は両方の内容を一致させ続ける必要があります。特に、新しいプラグインを追加した際にmarketplace.json側への登録を忘れると、そのプラグインは誰からも発見されません。 3. 構造の誤りがレビューで見逃され、全チームに波及する 続く「(3)検証する」段階では、設定ファイルの構造的な誤りを見逃してしまいます。誤りの種類はさまざまです。 plugin.jsonの必須フィールドの欠落 JSONの構文エラー プラグイン名の重複 スキル定義(SKILL.md)やagentのフロントマターの不備 hooks.jsonの構文エラー こうした誤りはdiffの見た目には現れにくく、レビュアーの目視チェックだけでは防ぎきれません。 壊れた設定が一度マージされると、そのマーケットプレイスを参照している全チームの全利用者に波及します。たとえばmarketplace.jsonがJSONとして壊れていると、 /plugin marketplace update がマーケットプレイス全体で失敗し、誰も新しいプラグインを取得できなくなります。「自分のプラグインしか触っていないのに、マーケットプレイス全体が更新できなくなった」という事故は、共有リポジトリならではのリスクです。 4. 目的に合致するスキルを検索で見つけられず、同じスキルが再発明される 「(4)見つけて導入する」段階では、そもそも目的に合致するスキルを見つけられないことが問題になります。1つのプラグインが複数のスキルを含むこともあるため、探す単位はプラグインではなくスキル単位になります。マーケットプレイスはもともと、スキルをさまざまなチームで参照できるように作ったものです。しかし運用が進むにつれてスキルの数そのものが増え続け、検索して見つけ出すコストが高くなっています。 見つけられないと、すでに同じものがあることに気付かないまま自分で作ることになり、別チームで同じスキルが再発明されます。 検索で見つけられない原因は、スキルの数だけではありません。SKILL.mdのdescriptionは開発者が実装や設計の観点で書くため、「PRのレビュー観点を多段階でチェックする」のような文言になりがちです。しかし利用者が検索するときの言葉は「レビューしたい」「PRを作りたい」のような、やりたいこと寄りの言葉です。descriptionをそのまま検索対象にしても、利用者の言葉とは一致せず、目的のスキルにたどり着けません。 5. 影響範囲が見えず、既存プラグインの修正が先送りされる 最後の「(5)改善する」段階では、既存のプラグインへの修正が先送りされがちです。最大の理由は、「自分の修正が他のスキルや呼び出し元を壊すのではないか」という不安です。たとえばSKILL.mdのdescriptionや引数の形式を変えると、それを前提に動いている別チームのスキルやワークフローが壊れかねません。マーケットプレイスを介して他チームがどう使っているかは見えないため、この不安を確かめる術がなく、些細な改善でも先送りされます。加えて、「これくらい満たしていれば導入してよい」という基準もないため、着手するかどうかの判断はそのつど個人の裁量に委ねられています。 5つの課題への個別対策 ここからは、それぞれの課題にどう対処したかを順番に紹介します。段階ごとに性質の異なる課題があるため、構造化・自動化・CIによる強制といった、異なるアプローチを組み合わせて対処しています。 1. チーム単位のディレクトリで責任範囲を分ける 以前はチームやプラグインの区別なく、1つのディレクトリにすべてのプラグインをフラットに並べていました。各プラグインの責任は、それを作ったチームが持つことにし、その責任をディレクトリ構造で表すという設計原則のもと、リポジトリをチーム単位のディレクトリに分けました。構成は次のようになっています。 ├── .claude-plugin/ │ ├── marketplace.json # 自動生成(直接編集禁止) │ └── marketplace-meta.json # マーケットプレイス自体のメタデータ ├── common-plugins/ # 全チーム共通のプラグイン │ └── plugin-a/ ├── xxx-plugins/ # xxxチーム所有 │ └── plugin-b/ ├── yyy-plugins/ # yyyチーム所有 └── zzz-plugins/ # zzzチーム所有 各チームがメンテナンスするプラグインは、そのチームのディレクトリ配下に置きます。この基準によって、プラグインの置き場所を見ればメンテナンス責任者が一意に決まります。common-plugins/への配置は、複数チームでの利用が見込まれるかを基準として、PRレビュー時に判断しています。 このようにチームごとに自由にディレクトリを分けられるのは、marketplace.jsonの各プラグインエントリが、プラグインの場所を相対パス(sourceフィールド)で持っているためです。実際のエントリは、次のようにチームごとに異なるディレクトリを指せます。 { " plugins ": [ { " name ": " plugin-a ", " source ": " ./common-plugins/plugin-a " } , { " name ": " plugin-b ", " source ": " ./xxx-plugins/plugin-b " } ] } プラグインをどこに置いてもmarketplace.json側でパスを合わせれば参照できるため、決まった1箇所に並べる必要がありません。ただし、この相対パスで参照できるのはmarketplace.jsonと同じリポジトリ内のプラグインだけです。別リポジトリのプラグインをGitHubソースなどで参照する方法もありますが、CIやリポジトリ設定をチームごとに分散させたくなかったため、今回は1つのリポジトリにまとめる構成を選びました。 もう1つの設計原則が、プラグインの粒度です。共通プラグインでは、関連性のない役割を1つのプラグインに詰め込まないというルールを設けています。一方、各チーム固有のプラグインは、どこまで細かく分けるかをチームの裁量に委ねています。たとえば、PRの作成・レビュー・管理のように互いに関連する役割は1つのプラグインにまとめます。一方、コードレビューを支援するスキルとインフラのコスト調査を支援するスキルのように、互いに無関係な役割は別々のプラグインに分割します。プラグインの数が増えても、自分が使わないプラグインのスキルが意図せず発火したり、そうしたスキルの説明文がコンテキストを圧迫したりする事態を避けられます。 2. 各プラグインのメタデータからmarketplace.jsonを自動生成する 各プラグインのplugin.jsonを正とし、marketplace.jsonは機械的に再生成する構成にしました。各プラグインのplugin.jsonを合成してmarketplace.jsonを生成するスクリプトを、CIから呼び出しています。開発者がやることは、自分のプラグインディレクトリの中でplugin.jsonを書くことだけです。 再生成はGitHub Actionsで自動化しています。PRを作成すると、ワークフローがplugin.jsonの変更を検知し、marketplace.jsonを再生成して、botコミットとしてPRブランチにpushします。 再生成スクリプトの要点を抜粋すると、次のとおりです。 #!/usr/bin/env bash set -euo pipefail META = " .claude-plugin/marketplace-meta.json " MARKETPLACE = " .claude-plugin/marketplace.json " # 各チームディレクトリ配下のplugin.jsonを、パス順にソートしてから収集する plugin_entries =$ ( find . -maxdepth 1 -type d -name ' *-plugins ' -print0 | xargs -0 -I {} find {} -path ' */.claude-plugin/plugin.json ' -print | sort | while IFS = read -r plugin_json ; do plugin_dir = " ${plugin_json % /.claude-plugin/plugin.json } " jq --arg source " $plugin_dir " ' . + {source: $source} ' " $plugin_json " done | jq -s ' . ' ) # marketplace-meta.jsonの内容に、収集したプラグイン一覧をマージする jq --argjson plugins " $plugin_entries " ' . + {plugins: $plugins} ' " $META " > " $MARKETPLACE " やっていることは、各チームディレクトリ配下のplugin.jsonをパス順に集めて、マーケットプレイス全体の情報と合成するだけの単純な処理です。 工夫の1つは、marketplace.jsonを部分的に更新せず、毎回すべて作り直す構成にしている点です。既存のplugin.jsonの集合から都度組み立て直すため、プラグインを削除したときの消し忘れが起きません。更新を重ねても、marketplace.jsonの内容が現状のplugin.jsonからずれていく心配もありません。この前提のもとでは、marketplace.json自体に直接手を入れることはできません。しかし名称のような、個々のプラグインには属さないマーケットプレイス全体の情報は、どのプラグインのplugin.jsonにも属さないため、生成元となる原本がありません。そこで、そうした情報だけをマーケットプレイスの構築時に marketplace-meta.json へ書いておきます。プラグインを追加するたびに書き換える必要はありません。 .claude-plugin/marketplace-meta.json : { " name ": " cc-plugin-marketplace ", " metadata ": { " description ": " 複数チームで共有するClaude Codeプラグインマーケットプレイス " } , " owner ": { " name ": " Claude Code Marketplace Maintainers ", " email ": " maintainers@example.com " } } スクリプトが収集したプラグイン一覧をこのファイルにマージし、marketplace.jsonを生成しています。 もう1つの工夫は、plugin.jsonの情報をパス順にソートしてから合成することで、副次的にコンフリクトを起きにくくしている点です。各プラグインのエントリはパス順の決まった位置に並ぶため、複数のPRがそれぞれ別のプラグインを追加しても、生成されるmarketplace.jsonの差分は配列内の別々の行に現れます。たとえば、 aaa-plugins/ チームのPRと zzz-plugins/ チームのPRが並行していても、前者は配列の先頭付近、後者は末尾付近にエントリを挿入するため、互いの差分は重なりません。末尾への追記のように複数のPRが同じ行を取り合うことがないため、マージ時にコンフリクトが起きにくくなっています。 3. 静的バリデーションで構造の誤りを検出する marketplace.jsonやplugin.jsonのように、目視でのレビューでは見逃しやすいファイルの構造的な誤りは、CIで自動チェックして検出しています。対象は次のとおりです。 JSON構文 必須フィールドの欠落 プラグイン名の重複 SKILL.mdやagentのフロントマターの不備 hooks.jsonの構文 こうした構造チェックは、CIのワークフローで実行しています。チェック自体は、Claude Code公式のCLIコマンド claude plugin validate にほぼすべて委譲しています。自作しているのは、公式コマンドがカバーしない次の2点だけです。 READMEを見ればプラグインの使い方がすぐわかるよう、README.mdの存在を確認する チームディレクトリにplugin.jsonはあるのに、marketplace.json側へ登録されていないプラグインがないかを検出する これらに加えて、 claude plugin validate はsourceフィールドの値がマーケットプレイスのルート外を指す ../ のようなパスになっていないかも検証します。プラグインはインストール時にキャッシュディレクトリへコピーされる仕様のため、ルート外を指すsourceは構文の誤りというより、動作しないか意図しない場所を参照してしまうリスクです。他の項目とは性質が異なるため、個別に触れました。 4. やりたいことからスキルを逆引きできるようにする スキルを「やりたいこと」から逆引きできる「スキルインデックス」を、ブラウザから確認できるようにしました。Claude Codeに直接探させることもできますが、Webページなら作業を始める前にさっと眺められますし、カテゴリタブを眺めているだけで「こんなスキルがあったのか」という偶然の発見もあります。 実際の画面は次のとおりです。検索窓にやりたいことを入力するほか、「やりたいこと」のカテゴリタブやチームでの絞り込みからも探せます。 上のスクリーンショットの時点で、対象となるスキルは73件にのぼります。これだけの数があると、利用者は目当てのスキルのdescriptionを覚えていられません。それでも、自分がやりたいことをそのまま打ち込むだけで目当てのスキルにたどり着けるのが、この仕組みの価値です。たとえば「PRのレビュー観点を多段階でチェックする」というdescriptionを知らなくても、「レビューしたい」と検索すればヒットします。検索する言葉が思いつかないときは、「Git・PR・レビュー」のようなカテゴリタブから探すほうが早いです。 この検索を支えている仕組みは、次の図のとおりです。 ポイントは次の2点です。 各スキルに、そのスキルが使われそうな場面を表す短いタスクフレーズ(想定作業)を複数持たせ、検索ではこのフレーズを最も重く評価する 想定作業フレーズとカテゴリは、日次のGitHub Actionsが生成する。SKILL.mdの内容に変更があったスキルだけを対象に、LLMを使わない通常のスクリプトとClaudeを組み合わせて更新する たとえばPR作成を支援するスキルには、次のようなassumed_work_ja(想定作業)とcategoryを持たせます。 { " id ": " common-plugins:create-pr ", " name ": " create-pr ", " assumed_work_ja ": [ " PRを作成する ", " 変更をpushしてレビュー依頼する ", " コミットしてPRを出す " ] , " category ": " git-pr " } 利用者が「PR出したい」で検索しても、descriptionの原文と一致していなくても、この想定作業フレーズ経由でヒットします。categoryは検索の照合には使わず、「Git・PR・レビュー」のような大まかな分野を表すラベルとして、画面上のタブ絞り込みに使います。 生成された想定作業フレーズは、検証・レビューを経てマージされると、GitHub Pagesとして自動で再公開されます。ホスティング先にGitHub Pagesを選んだのは、検索自体が埋め込みやLLM推論を使わないクライアントサイドのキーワードマッチングで完結しており、静的サイトのホスティングで十分だったためです。 5. @claudeの自動チェックで影響範囲への不安を減らす 静的バリデーションのCIは、設定ファイルの構造の正しさは保証します。しかし、スキルの中身の変更が既存の利用者や呼び出し元を壊さないかまでは検出できません。組織横断でスキルを使えるようにした分、開発者は自分のスキルが他チームでどう使われているかを把握できないため、入出力の形式を変えるような変更をしても、どこかで悪影響が出ていることに気付けません。そこで、GitHubのPRコメントで@claudeと打つだけで、この見えない影響範囲を機械的にチェックする仕組みを作成しました(ローカル実行も可能です)。チェックは2軸です。 ひとつは破壊的変更チェックです。検出対象は次のとおりです。 スキルのname・descriptionのトリガー条件・引数形式・出力契約の変更 agentの入出力契約やstatus codeの変更 hooksやMCP設定の変更 SKILL.mdから参照されるreferencesやscriptsの削除など、既存の利用者・呼び出し元を壊す変更 もうひとつはマーケットプレイス規約チェックです。記述面では、SKILL.mdの行数やdescriptionのフォーマットを確認します。Progressive Disclosure(本文を小さく保ち、詳細はreferencesなど別ファイルに逃がす設計)といった規約も対象です。安全面では、allowed-toolsの過剰付与、MCPのサービス分離、シークレットの直書きといった規約への準拠も確認します。 この仕組みには割り切りがあります。プラグインの良し悪しは文字列のレビューだけでは判断しきれず、結局は導入して動かしてみないとわかりません。そのためレビューで品質を保証しきることは目指さず、「最低限動くか」「規約に沿っているか」「既存のものと重複していないか」という入口の最低ラインだけを機械的に保証し、あとは導入して確かめる方針です。品質を最終的に担保しているのは、これまでどおり必須のPRレビューです。破壊的変更が起きていないことを自動で確認できれば、修正への心理的ハードルは下がります。チェック結果がGitHub上に投稿されるため、レビュアーは自動チェック済みであることを踏まえてダブルチェックでき、レビュー負担が減ります。運用件数が増えても、この負担軽減の効果は大きいと考えています。 現状は、ひとまず初期の導入として、上記の最小限の観点のみチェックできるように実装した段階です。それでも「壊れていないことは自動で分かる」という安心感は、日々の修正のしやすさに直結しています。実際、これまでのマージ済みPRのうち、既存プラグインの改善・修正を目的としたものは新規追加より多く、全体の6割近くを占めています。プラグインは作って終わりではなく、運用しながら継続的に手が入れられています。CIによる機械的な保証と、心理的なハードルを下げるコミュニケーション寄りの工夫、その中間に位置する仕組みだと捉えています。運用しながらリファクタし、リポジトリ独自のルールが固まってきたらチェック項目として組み込んでいく予定です。 複数チームで共有した効果 約10か月にわたり運用したところ、複数チームで1つのマーケットプレイスを共有した効果が見えてきました。 他チームの動きが見えるようになった 以前は、チーム内でどんなスキルやエージェントを作っているかが、チームの外からはわかりませんでした。スキルインデックスという共通の窓口ができたことで、他チームのスキルを見つけてSKILL.mdを読めるようになりました。たとえば、定例をAIで代替する取り組みを進めているチームがあります。そのチームのスキルの中身を読むと、どんなハーネスを使い、どんな工夫をしているのか、どこをAIに任せているのかがわかります。自分のチームに持ち込むとしたらどんな工夫を付け加えられそうか、そこまで考えられるようになりました。 資産の横展開が生まれた 以前は、それぞれのチームがAI活用の方法を1から考えている状態でした。workflowやloop、Routinesといった機能の使いこなし方も、チーム内の詳しい人に聞かなければわからず、知識が特定の人に偏っていました。マーケットプレイスを複数チームで管理するようになったことで、他チームの工夫を自分のチームの工夫として取り込みやすくなりました。その恩恵として、他チームが作った共通プラグインをそのまま使ったり、各チーム固有の工夫でも横展開しやすくなったりしています。 たとえば、GitHub・Confluence・Slack・Jiraを横断して過去事例を検索するスキルを、あるチームが作りました。このスキルは検索を担う4つのエージェントを独立したファイルへ切り出す構成にしていたため、別チームが障害調査を支援するスキルを作る際に、この4つの検索エージェントをそのまま呼び出して使えました。ゼロから検索の仕組みを作り直す必要はありませんでした。 また、定期実行するAIエージェント機能のRoutinesや、loop、GitHub Actionsのワークフローで呼び出すスキルも、このリポジトリに置いて共有する運用にしています。以前は、こうしたよくあるスキルをどのリポジトリで管理すべきか所在が定まっていませんでした。それが、誰かが音頭を取ったわけでもなく、マーケットプレイスに置けばよいという運用に自然と落ち着きました。 チームごとに開発資産を抱え込んだままだったら、これらの事例は生まれなかったはずです。共有の場があるからこそ、1チームの工夫が他チームの出発点になっています。 属人化が減り、誰でもすぐに始められるようになった 新しく入ったメンバーのオンボーディングも、マーケットプレイスによって変わりました。以前は、スキルがそれぞれのローカル環境に散らばっていたため、SKILL.mdの書き方やプラグインの構成をゼロから調べながら自分で作る必要があり、導入コストが高くなりがちでした。マーケットプレイスに既存のスキルがまとまっている今は、まずインストールして動きを確認するところから始められます。実際に動くスキルを読みながら使い方や設計の型を掴めるため、自分で一から設計を考える前に土台をつかめます。 今後の展望 これまでの運用を通じて、今後取り組むべき課題も見えてきました。 カタログの肥大化にどう向き合うか プラグインはplugin.jsonとディレクトリを用意するだけで簡単に作れます。この手軽さは開発を促進する一方、使われなくなったプラグインが増えても気づきにくくなります。使われなくなったプラグインも検索結果に混ざり続けるため、スキルインデックスの探しやすさを保つ効果も薄れていきます。カタログ全体を俯瞰して現状を把握するコストは、プラグイン数に比例して大きくなります。またあるプラグインが複数箇所から参照されるようになると、依存関係も見えにくくなります。 依存関係が見えないままでは、共通のプラグインへの変更がどのチームの利用に響くのかを事前に判断できません。@claudeによる破壊的変更チェックはその一歩ですが、プラグインの品質そのものを保証する仕組みではなく、「これを満たせば安心して変更してよい」と言えるレベルの動作確認の仕組みには至っていません。今後はプラグインの動作確認用のスキルを整備し、新規追加時・リファクタ時それぞれで基準を明文化することで、仮説検証を繰り返しやすい環境を作っていきたいと考えています。 プラグインの追加・変更の周知コストをどう下げるか チーム内で完結するプラグインなら、追加や変更があってもチームメンバーに直接伝えれば済みます。しかし、他チームでも使えるプラグインとなると、その存在に気づいてもらうこと自体にコストがかかります。プラグインの追加や更新を知らせる専用のSlackチャンネルは用意しているものの、投稿は個人の判断に委ねられているため、周知そのものが形骸化しつつあります。 ベースブランチへのマージをトリガーに、この専用チャンネルへ自動で通知を流す案を考えています。ただし、更新のたびに毎回発火させると通知が埋もれてしまい、かえって見られなくなる懸念があります。どの粒度・タイミングで知らせるのが適切か、今後詰めていきたいところです。 Claude Codeの進化の速さにどう追随するか Claude Code自体の機能追加は速く、数か月前のベストプラクティスが過去のものになることも珍しくありません。新機能が次々と追加される一方で、それに合わせてスキルやワークフローの書き方を更新し続けなければ、マーケットプレイスの資産はすぐに古びてしまいます。新しい機能が出るたびに、その機能をどう使えばよいか調べ、動く形にするまで試行錯誤する時間がボトルネックになりがちです。 この変化のペースに個々のチームがそれぞれキャッチアップするのは非効率です。新しい機能が出るたびに、スキルやloopをすぐ作って試せる仕組みを、マーケットプレイス側で用意しておきたいと考えています。雛形やスクリプト生成を支援することで、「使ってみる」までのハードルをさらに下げていく予定です。 運用サイクルそのものを改善の対象にする 最後は、運用の進め方そのものです。プラグインはplugin.jsonとディレクトリさえ用意すれば作れるため、プラグインを作る速度はこれからも上がり続けます。一方、それを複数チームで安全に運用し続けるための仕組みづくりは後回しになりがちだという感覚があります。本記事で紹介した5つの課題への対策も、最初から仕組み化されていたわけではなく、困りごとが顕在化してから後追いで整備してきたものです。前述のカタログの肥大化や周知コストの課題も、まだ「今後詰めていきたい」段階にとどまっており、同じ構図の繰り返しだと感じています。作る側の障壁を低く保ったまま、運用する側の障壁だけを先回りして整えるのは難しいというのが実感です。マーケットプレイスの運用に、整備して終わりという区切りはありません。課題を見つけるたびに仕組みで対処し、また新しい課題が出てくるというサイクルです。このサイクル自体をどう効率化するか、つまり運用サイクルそのものをエンジニアリングの対象にすることが次のテーマだと考えています。 本記事で紹介した工夫は、これまでの運用で得られた1つの到達点にすぎません。今後も運用しながら課題を見つけ、改善を重ねていきます。 まとめ 本記事では複数チームで1つのClaude Codeプラグインマーケットプレイスを育てる取り組みを紹介しました。 複数チームでの共同運用では、責任の所在の曖昧さやカタログの不整合などさまざまな課題に直面しましたが、多くは構造・自動化・CIの工夫を組み合わせることで仕組みとして解決できました。 結果として、marketplace.jsonを自動生成する構成にしたことで、コンフリクトや登録漏れが起きにくくなりました。チーム単位のディレクトリ構成で、プラグインのメンテナンス責任も明確になりました。構造の誤りも、CIが利用者へ届く前に検出できます。スキルインデックスによって目的のスキルも検索で見つけやすくなり、@claudeの自動チェックで既存プラグインの修正への心理的ハードルも下がりました。 運用開始前は、チーム間の変更のコンフリクトや壊れた設定が全チームに波及することを懸念していました。しかし2025年下期の運用開始から約10か月が経過し、大きなトラブルなく運用を続けられています。 本記事で紹介した知見が、複数チームでのマーケットプレイス運用を検討している方の参考になれば幸いです。技術仕様は Claude Codeの公式ドキュメント を参照して執筆しました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、販促基盤ブロックの村井です。普段は販促基盤の開発を担当しています。 販促基盤は、ZOZOTOWNにおけるキャンペーン・割引・友だち紹介機能など販売促進の施策を横断的に支える共通基盤です。2025年に構築が始まり、初期スコープとして友だち紹介機能のリプレイスを行いました。友だち紹介機能は、既存会員が非会員を招待し、条件を満たすと双方にポイントを付与する機能です。本記事では、基盤の立ち上げ時に行ったO/Rマッパーの選定と、Auroraの読み書き分離でつまずいた話をご紹介します。 目次 はじめに 目次 背景・課題 O/RマッパーにDoma 3を選んだ理由 Domaでの実装パターン インフラ層のクラス構成 ドメインクラスで値オブジェクトを使用 Entityをテーブルの表現に徹させる Entityとドメインオブジェクトの変換 RepositoryImplでDBの例外を業務の例外へ変える 条件分岐をSQLテンプレートで書く 読み書きの振り分け 読み書きを分けた理由 1つ目の構成:判定の順序でつまずく 2つ目の構成:TransactionManagerを2つに分ける 調査:コネクションの取得回数を数える 3つ目の構成:DataSourceの参照先を揃える ThreadLocalを使う上での注意 別解:LazyConnectionDataSourceProxy まとめ 背景・課題 販促基盤には、エントリーや抽選といった複数種類の施策を1つの基盤で扱う構想を置いており、初期フェーズでは友だち紹介機能から着手しました。 友だち紹介機能を最初の対象に選んだ理由は2つあります。 1つは、既存の実装を流用できない状態だったことです。既存の友だち紹介機能は数年前に作った機能を暫定対応として運用しており、そのまま引き継げるソースコードがありませんでした。もう1つは機能不足です。ビジネス側が実施したい施策を、既存機能では実現できませんでした。そのため既存のソースコードとデータを引き継がず、新規開発として作り直す判断をしました。 この基盤では、立ち上げ時にJava 25とSpring Boot 3.5を土台にオニオンアーキテクチャを採用しました。ドメイン層を中心に置き、その外側にアプリケーション層、インフラ層、プレゼンテーション層を並べる構成です。層をまたぐ依存の向きはArchUnitのテストで検査しています。ドメイン層からインフラ層を参照するコードを書くと、このテストが失敗してビルドが通りません。 この方針を決めたあと、論点になったのがO/Rマッパーの選定です。求めたのは次の2点でした。 ドメイン層に永続化の都合を持ち込まない 発行されるSQLを自分たちで制御できる 1点目は、オニオンアーキテクチャを採用した時点で譲れない条件です。2点目は基盤の構想から来ています。扱う施策の種類が増えるほど、検索の条件は複雑になります。発行されるSQLの中身が見えないと、性能の問題が起きたときに原因を追えなくなります。 以下では、選定の過程と、採用したあとの実装の形を順に書きます。最後は、この構成で運用を始める前につまずいた話です。 O/RマッパーにDoma 3を選んだ理由 候補はSpring Data JPA、MyBatis、Domaの3つです。選定時に3候補それぞれの利点と欠点を洗い出しました。 候補 利点 欠点 Spring Data JPA Spring Bootとの親和性が高い。メソッド名からクエリを導出できる SQLを自分で書かないため、性能の問題が起きたときに原因を特定しにくい。関連データの取得方法を誤ると過取得やN+1を招く MyBatis SQLを自分で書ける。関連データのマッピングもできる XMLで書くと記述が冗長になる。XMLとアノテーションのどちらでも書けるため、書き方が割れると管理が複雑になる Doma SQLをファイルとして管理でき、そのままDBで実行して確認できる。SQLファイルの不足やDoma独自のコメント記法の誤りをビルド時に検出できる JPAのような暗黙的な関連取得や遅延ロードはない 最終的にDoma 3を採用しました。 決め手はSQLの扱いでした。DomaにはSQLテンプレートという仕組みがあります。公式ドキュメントで「two-way SQL」と呼ばれているもので、条件分岐やバインド変数をSQLのコメントとして書きます。SQLファイルをそのままDBのクライアントへ貼り付けて実行でき、書いたSQLとログに出るSQLもほぼ一致します。遅いクエリの原因を追いやすくなります。 また、クエリの自動生成に寄せたくない意図もありました。運用に入ったあと、どのようなSQLが出るかを把握できる状態を保ちたかったためです。Domaはこの点で扱いやすいと判断しました。 DomainConverterという仕組みによって値オブジェクトを永続化層まで持ち込める点も利点です。 Domaでの実装パターン 全体の層構成は次のとおりです。 層 役割 主な中身 ドメイン層 業務の言葉とルール 集約、値オブジェクト、Repositoryのインタフェース アプリケーション層 ユースケースの手続きとトランザクション境界 UseCaseのインタフェースとHandler インフラ層 技術による実現 Domaを使った永続化、DataSource、外部サービスとの連携 プレゼンテーション層 HTTPの受け口 Controller、リクエストとレスポンスのスキーマ 依存は内側へ向かう一方向です。ドメイン層は他のどの層も参照しません。Repositoryはインタフェースだけをドメイン層に置き、実装はインフラ層が持ちます。 ドメイン層には、紹介や報酬といった業務概念ごとにパッケージを分けて置いています。 本記事で扱うのは、このうちインフラ層です。 以降のコードには友だち紹介機能の概念が出てきます。各用語は以下のような意味です。 紹介者:招待した側 被紹介者:招待された側 紹介成立:紹介者と被紹介者が結びついた記録 報酬:付与するインセンティブ インフラ層のクラス構成 インフラ層は次のクラスで構成されます。 クラス 責務 DAO SQLファイルとの対応づけと、DB操作の宣言 Entity テーブル1行に対応するオブジェクト DomainConverter 値オブジェクトとDBの型の相互変換 RepositoryImpl Entityとドメインオブジェクトを組み立て、ドメインの操作として公開 DAO、Entity、DomainConverterはDomaが用意した仕組みです。RepositoryImplはこの基盤で用意したクラスです。 Entityはテーブルの構造をそのまま写した型です。Entityとドメインモデルを直接つなぐと、テーブルの変更がドメインモデルにも及びます。そこで両者の変換はRepositoryImplが行います。 ドメインクラスで値オブジェクトを使用 ドメインクラス は、カラムの値をJavaのオブジェクトとして扱うDomaの仕組みです。作り方は2つあります。 内部ドメインクラス:型そのものに @Domain を付け、対応するDBの型を valueType で指定する 外部ドメインクラス:型には手を入れず、 DomainConverter を実装したコンバータクラスに @ExternalDomain を付ける @Domain を選ぶと、ドメイン層の型にDomaのアノテーションが載ります。この基盤ではそれを避けたいので、外部ドメインクラスを選びました。コンバータクラスはインフラ層に置いています。次は紹介成立のIDを表す ReferralId のものです。 @ExternalDomain public class ReferralIdConverter implements DomainConverter<ReferralId, Long> { @Override @Nullable public Long fromDomainToValue( @Nullable ReferralId domain) { if (domain == null ) { return null ; } return domain.value(); } @Override @Nullable public ReferralId fromValueToDomain( @Nullable Long value) { if (value == null ) { return null ; } return ReferralId.of(value); // ReferralIdのファクトリメソッド } } 型引数がドメイン側とDB側の型の対応です。これを置くと、DAOの引数に値オブジェクトをそのまま使えます。 @Dao @ConfigAutowireable public interface ReferralDao { @Select List<ReferralEntity> selectReferrals( // 中略 @Nullable ReferrerId referrerId, @Nullable ReferredId referredId, SelectOptions options); @Insert (exclude = { "createdAt" , "updatedAt" }) Result<ReferralEntity> insert(ReferralEntity referral); @Select boolean existsById(ReferralId id); } DAOもDomaの仕組みです。 @Dao を付けたインタフェースの実装は、コンパイル時にアノテーションプロセッサが生成します。 @Select のメソッドは同名のSQLファイルと対応づき、 @Insert のようにSQLファイルを持たないものはDomaがSQLを組み立てます。 SelectOptions はページング、 Result は登録や更新の結果を受け取る型です。 外部ドメインクラスの方式では、値オブジェクト1つにつきコンバータクラスが1つ必要です。 Entityをテーブルの表現に徹させる Entityはテーブルまたはクエリの結果セットに対応するDomaの仕組みです。この基盤ではすべてrecordで書いています。以下は紹介成立を保持する referrals テーブルのものです。 @Entity (immutable = true , naming = NamingType.SNAKE_LOWER_CASE) @Table (name = "referrals" ) public record ReferralEntity( @Id @GeneratedValue (strategy = GenerationType.IDENTITY) @Nullable ReferralId id, // 中略 ReferrerId referrerId, ReferredId referredId, @Nullable LocalDateTime createdAt, @Nullable LocalDateTime updatedAt) {} referrerId や referredId などはDB上では BIGINT ですが、コード上は値オブジェクトとして読み書きできます。 id に付けた @GeneratedValue は、IDの採番をDB側の AUTO_INCREMENT に任せる指定です。登録するまでIDが決まらないため、 id だけ @Nullable にしています。 Entityとドメインオブジェクトの変換 ここからはDomaの仕組みではなく、この基盤の独自の話です。 ドメイン層の型は、IDの採番前と採番後で分けています。採番前は NewReferral のように、これから登録する内容だけを持ち、IDがありません。採番後は Referral でIDを持ちます。RepositoryImplは登録のときに採番前の型からEntityを組み立て、取得のときにEntityからドメインオブジェクトを復元します。 変換の中身は値の移し替えだけではありません。例えば報酬の実績を持つ rewards テーブルは、発行・付与・取消の日時を issued_at 、 granted_at 、 revoked_at の3列で持ちます。一方でドメイン側の Reward は RewardStatus という状態を持ちます。この状態に対応する列はなく、3つの日時から計算します。 private RewardStatus getStatus(RewardEntity entity) { if (entity.revokedAt() != null ) { return RewardStatus.REVOKED; } if (entity.grantedAt() != null ) { return RewardStatus.GRANTED; } return RewardStatus.ISSUED; } 状態の導出をドメイン層に置くと、テーブルの列構成がドメインへ漏れます。インフラ層に置くことで、それを防いでいます。 RepositoryImplでDBの例外を業務の例外へ変える referrals テーブルには複数の列の組に一意制約があり、各列に外部キー制約もあります。登録が失敗する理由はこの2つで、意味が違います。 なおDoma自体は UniqueConstraintException を投げます。これがSpringの DuplicateKeyException へ変わるのは、doma-spring-bootが例外を変換しているためです。 次は紹介成立を登録するRepositoryImplのメソッドです。 @Override public Referral create(NewReferral newReferral) throws ReferralAlreadyExistsException, ReferencedEntityNotExistsException { ReferralEntity entity = ... ; // NewReferral から Entity へ変換 try { Result<ReferralEntity> result = referralDao.insert(entity); return ... ; // Entity から Referral へ復元 } catch (DuplicateKeyException e) { throw new ReferralAlreadyExistsException( "Referral already exists: ..." , e); } catch (DataIntegrityViolationException e) { throw new ReferencedEntityNotExistsException( "Referenced entity does not exist: ..." , e); } } アプリケーション層はこの2つを別の例外へ変え、コントローラが409と400に振り分けます。 ただし DataIntegrityViolationException は DuplicateKeyException の親クラスで、整合性制約の違反を広く拾います。この基盤では外部キー制約の違反として扱っているため、他の制約に違反した場合も同じ例外になります。 条件分岐をSQLテンプレートで書く 紹介成立の検索は、紹介者や被紹介者を任意の組み合わせで絞り込めます。この分岐はSQLファイルに書きます。 SELECT id, -- 中略 referrer_id, referred_id, created_at, updated_at FROM referrals WHERE 1 = 1 -- 中略 /*%if referrerId != null */ AND referrer_id = /* referrerId */ 1 /*%end */ /*%if referredId != null */ AND referred_id = /* referredId */ 1 /*%end */ ORDER BY id コメントで書かれた命令をDomaはディレクティブと呼びます。 /*%if*/ と /*%end*/ が条件ディレクティブ、 /* referrerId */ がバインド変数ディレクティブです。 1 はコメントの外に置いたテスト用の値で、実行時にプレースホルダへ置き換わります。 この基盤では INSERT と UPDATE のSQLファイルを書かず、 @Insert と @Update の自動生成に任せています。 exclude で createdAt と updatedAt を外しており、登録時はMySQL側のデフォルト値で埋まります。SQLを手で書くのは、条件分岐や結合が必要な参照だけに絞る形です。 読み書きの振り分け ここからは実装後に判明した不備の話です。読み書きの振り分けを実装したつもりで、DataSourceのレベルでは切り替わっていませんでした。ユニットテストと結合テストは通っており、気付いたのは障害試験のときでした。 読み書きを分けた理由 データベースにはAurora MySQLを使っています。Auroraのクラスターは、書き込みを受け付けるWriterインスタンスと、読み取り専用のReaderインスタンスで構成されます。アプリケーションからはそれぞれのエンドポイントへ別に接続するため、接続プールも2つ用意しています。プールにはSpring Bootがデフォルトで使うHikariCPをそのまま採用しています。 参照をReaderへ寄せようとしたのは、以下の2つの傾向からでした。 1つはアクセスの傾向です。ユーザーの入り口が施策のLPであることが多く、公開期間の短いLPにはプッシュ通知やSNS配信による一時的なアクセス増が見込まれました。もう1つはデータの形です。キャンペーンやポイントのマスタを持つテーブルは読み取りの比率が高く、状態を追跡するテーブルでも読み取りのほうが多くなります。 この見込みに合わせて接続プールの上限を決め、Readerのほうを厚く取っています。 振り分けの入口はアプリケーション層に置いた TransactionService です。参照系のハンドラは readOnly 、更新系は required を呼びます。 @Override public SearchRewardsResult execute(SearchRewardsQuery query) throws SearchRewardsException { return transactionService.readOnly(() -> handle(query)); } 呼び出し側に経路の指定は出てきません。 readOnly と required のどちらを呼ぶかだけで、向かう先が決まる形を目指しました。 1つ目の構成:判定の順序でつまずく 最初は AbstractRoutingDataSource を1つ使いました。接続先を動的に選ぶためのSpringのクラスです。これを継承して determineCurrentLookupKey メソッドを実装すると、Springはコネクションを要求されるたびにそれを呼び、戻り値をキーとして接続先を引き当てます。キーとReaderやWriterの対応は、あらかじめ登録する仕組みです。 そこで、実行中のトランザクションが読み取り専用かどうかでキーを決めることにしました。この判定はSpringの TransactionSynchronizationManager.isCurrentTransactionReadOnly() で取得できます。 final var routingDataSource = new AbstractRoutingDataSource() { @Override protected Object determineCurrentLookupKey() { boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); return isReadOnly ? RouteFor.READER : RouteFor.WRITER; } }; しかし参照もWriterへ流れました。Springがトランザクションを開始する処理が2段になっているためです。前半でコネクションを取得し、後半で読み取り専用フラグをスレッドへ登録します 1 。接続先が決まるのは前半、フラグが立つのは後半です。キーを判定する時点ではフラグがまだ立っておらず、常にWriterが選ばれていました。 そこで判定のタイミングに依存しない構成としました。 2つ目の構成:TransactionManagerを2つに分ける 次に AbstractRoutingDataSource をやめ、ReaderとWriterそれぞれに TransactionManager を用意しました。 SpringTransactionService のコンストラクタで @Qualifier を使い、経路ごとのテンプレートを組み立てる形です。 public SpringTransactionService( @Qualifier ( "readerTransactionManager" ) PlatformTransactionManager readerTransactionManager, @Qualifier ( "writerTransactionManager" ) PlatformTransactionManager writerTransactionManager) { this .readOnlyTemplate = createTemplate(readerTransactionManager, template -> template.setReadOnly( true )); this .requiredTemplate = createTemplate(writerTransactionManager, _ -> {}); // 以下略 } 判定のタイミングの問題は消えました。ユニットテストと結合テストも通り、この構成でリリースの準備まで進みました。 この構成の不備は、障害試験中に判明しました。例外は出ておらず、ログにも警告はありません。SQLは正しく実行され、レスポンスも正常です。 不備の中身は2つありました。 1つは、行き先を切り替える仕組みが TransactionManager の選択しかなかったことです。 TransactionManager は生成時に1つのDataSourceと結びつきます。この構成ではReader用とWriter用を2つ用意し、 readOnly ならReader用、更新系ならWriter用を使う形にしていました。行き先が決まるのはこの使い分けだけで、DataSource自身は切り替える仕組みを持ちません。そのため TransactionManager を通らずにコネクションを取りに行く経路があると、そこは振り分けの対象外です。 もう1つは、参照系のトランザクションでコネクションを余分に取得していたことです。 調査:コネクションの取得回数を数える 使ったのはSpringのログです。 DataSourceUtils は、トランザクションに紐付いたコネクションを見つけられなかったときに Fetching JDBC Connection from DataSource を出力します。この行がトランザクション内で何回出るかを数えれば、余分な取得が起きているか分かります。 MySQLとRedisを起動し、アプリケーションのログをファイルへ出力しながら、全エンドポイントを順に呼び出します。呼び出しは調査用に書いた使い捨てのスクリプトで行いました。 docker compose up -d mysql redis ./gradlew bootRun > /tmp/bootrun_test_output.log 2 >&1 & 参照系のログは以下のようになっていました。 DataSourceTransactionManager - Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly DataSourceTransactionManager - Acquired Connection [HikariProxyConnection@425938879 ...] for JDBC transaction DataSourceTransactionManager - Switching JDBC Connection [...] to manual commit DataSourceUtils - Fetching JDBC Connection from DataSource DataSourceUtils - Fetching JDBC Connection from DataSource DataSourceTransactionManager - Initiating transaction commit DataSourceTransactionManager - Releasing JDBC Connection [HikariProxyConnection@425938879 ...] after transaction Fetching JDBC Connection from DataSource が2行並びます。更新系では1行でした。22のエンドポイントを実行したところ、参照系の10件が2行、更新系の11件が1行でした。残る1件はマスタの検索で、Redisのキャッシュに当たってDBへ行かないため0行です。 原因はDomaが受け取るDataSourceでした。このときReaderとWriterのHikariDataSourceを、それぞれ個別に TransactionAwareDataSourceProxy で包んでいました。トランザクションに紐付いたコネクションを返すプロキシで、これが2つある状態です。そしてDomaへ渡っていたのは、 @Primary が付いたWriter側のプロキシだけでした。 TransactionManager がコネクションを紐付ける相手と、Domaの問い合わせ先が食い違っていました。 3つ目の構成:DataSourceの参照先を揃える TransactionAwareDataSourceProxy を1つに統合し、その下に AbstractRoutingDataSource を置きました。 TransactionManager とDomaがどちらも同じ routingDataSource を起点にするため、コネクションを紐付ける側と参照する側が一致します。 @Bean public DataSource routingDataSource() { ReadWriteRoutingDataSource routingDS = new ReadWriteRoutingDataSource(); routingDS.setTargetDataSources( Map.of( ReadWriteRoutingDataSource.Route.READER, readerHikariDataSource(), ReadWriteRoutingDataSource.Route.WRITER, writerHikariDataSource())); routingDS.setDefaultTargetDataSource(writerHikariDataSource()); return routingDS; } @Bean @Primary public DataSource dataSource() { return new TransactionAwareDataSourceProxy(routingDataSource()); } @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(routingDataSource()); } TransactionManager も1つに戻しました。経路はThreadLocalで持ちます。 public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource { public enum Route { READER, WRITER } private static final ThreadLocal<Route> currentRoute = new ThreadLocal<>(); public static void setRoute(Route route) { currentRoute.set(route); } public static void clear() { currentRoute.remove(); } @Override protected Object determineCurrentLookupKey() { Route route = currentRoute.get(); return (route != null ) ? route : Route.WRITER; } } キーを設定するのは SpringTransactionService です。トランザクションを開始する前にThreadLocalへ書き込み、終わったら必ず消します。 private <T> T executeWithRoute( ReadWriteRoutingDataSource.Route route, TransactionTemplate template, Supplier<T> operation) { ReadWriteRoutingDataSource.setRoute(route); try { T result = template.execute(_ -> operation.get()); if (result == null ) { throw new IllegalStateException( "Transaction operation returned null" ); } return result; } finally { ReadWriteRoutingDataSource.clear(); } } template.execute より前にキーが決まるため、順序の問題も起きません。 修正後に同じスクリプトを実行すると、参照系の Fetching JDBC Connection from DataSource は2行から1行になりました。 ThreadLocalを使う上での注意 経路をThreadLocalに置いているため、別のスレッドへは伝わりません。現在この基盤は非同期処理を使っていないため問題になっていませんが、導入するときは経路の受け渡しを考える必要があります。 ThreadLocalが空のときはWriterへ倒す実装にしています。経路の設定を忘れた場合でも、読み取り専用の接続で更新を試みる事故は避けられます。 executeWithRoute は処理の終わりにThreadLocalを空にします。そのため readOnly から required を呼ぶような入れ子にすると、内側を抜けた時点で外側の経路まで消えます。この基盤ではこのような呼び出しをしていないため、問題にはなっていません。許す場合は、内側へ入る前の経路を退避し、抜けるときに書き戻す必要があります。 別解:LazyConnectionDataSourceProxy 判定の順序については、ルーティングDataSourceを LazyConnectionDataSourceProxy で包む方法もあります。物理的なコネクションの取得を最初のSQL実行まで遅らせるクラスで、キーの判定が読み取り専用フラグの設定より後になります 2 。 TransactionAwareDataSourceProxy のJavadocにも、中間プロキシとしてこのクラスへ委譲できると書かれています 3 。 この場合、読み取り専用フラグをそのままキーに使えます。ThreadLocalを自分で管理する必要がなくなるため、前節で挙げた注意も不要になります。現在の構成で問題なく動いているため移行はしていませんが、同じものをこれから組むなら検討の余地があると考えます。 Spring Framework 6.1.2以降は setReadOnlyDataSource も使えます。読み取り専用のDataSourceを直接指定でき、 AbstractRoutingDataSource を書かずに済みます。 いずれの構成でも、 TransactionAwareDataSourceProxy はプロキシの最も外側に置きます。そのうえで TransactionManager とDomaが同じDataSourceを見るようにします。 まとめ 販促基盤のO/Rマッパーには、発行されるSQLを自分たちで追えることを重視してDoma 3を選びました。外部ドメインクラスを使ったので、ドメイン層をDomaから切り離したまま値オブジェクトを永続化層で用いることができました。RepositoryImplではDBの制約違反をドメインの例外へ変えており、一意制約の違反と外部キー制約の違反でAPIのエラーを振り分けられます。 読み書きの振り分けでつまずいた原因は、 TransactionManager とDomaが別のDataSourceを見ていたことでした。この状態では、トランザクションに紐付いたコネクションを再利用できずに取り直します。両者が同じDataSourceを起点にする形へ直して解決しました。動作確認には Fetching JDBC Connection from DataSource のログ行数を数えました。これがいちばん手軽でした。 JavaでDDDを実践する際のO/Rマッパー選定の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 前半は DataSourceTransactionManager の doBegin メソッドです。後半は AbstractPlatformTransactionManager の prepareSynchronization メソッドです。この中で setCurrentTransactionReadOnly メソッドが呼ばれます。 ↩ Document LazyConnectionDataSourceProxy setup for routing datasource to act on transaction definition read-only flag ↩ TransactionAwareDataSourceProxy (Spring Framework API) 。「should be the outermost DataSource of a chain of DataSource proxies/adapters」と記載があります。 ↩
はじめに こんにちは、データシステム部MLOpsブロックの 木村 と、推薦研究ブロックの 小倉 です。 ZOZOTOWNでは、ユーザーが多様な商品の中から好みに合った商品と出会えるよう、さまざまな推薦機能を提供しています。その1つが商品詳細画面の「おすすめアイテム」であり、閲覧中の商品に関連する商品を推薦しています。以下に、ZOZOTOWNにおける商品詳細画面の「おすすめアイテム」のイメージを示します。 この「おすすめアイテム」は、おすすめ商品を返すAPI(以下、推薦API)が提供しています。推薦APIは、推薦ロジックとしてGoogle Cloudのマネージドサービスである AI Commerce Search API を利用しています。 AI Commerce Search APIのようなマネージドサービスでは、内部で動作する推薦モデルの挙動や更新のタイミングを利用側から直接観測できません。そのため、推薦品質が保たれているかを利用側で事前に担保する手段はなく、私たちはCTRなどユーザーの反応を表すオンライン指標を通じて品質を確認していました。しかしオンライン指標は日次で集計され、施策やセールイベントなど多様な要因で変動するため、推薦モデルに起因する品質の変化を短いサイクルで切り分けられませんでした。 実際に2025年5月、エラー率やレイテンシといったシステム指標に異常は現れないまま、推薦結果の分布だけが大きく変化しました。私たちがそれを異常と判断するまでには約1週間を要しました。 この経験を踏まえ、推薦APIのレスポンスログから推薦モデルの品質変化を検知する監視システムを構築しました。本記事では、監視指標と閾値の設計プロセス、運用体制の整備から、実際に運用する中で見えてきた課題とその改善までを説明します。マネージドサービスとして提供されるMLモデルを監視する事例はまだ少なく、これらの知見が推薦システムやMLモデルの品質監視を検討する際の参考になれば幸いです。 本記事の内容は2026年7月時点の情報であることにご留意ください。 目次 はじめに 目次 背景・課題 2025年5月に発生した推薦モデルの性能障害 検知に1週間を要した理由 本システムが担う監視レイヤー 推薦システムで起こりうる障害パターン 3つの障害パターン 監視指標と閾値の設計 監視する3つの指標 カバレッジ カテゴリ偏り率 返却アイテム数 集計ウィンドウ Warning・Criticalの2段階閾値 運用設計 アラート対応フロー 自動フォールバックを行わない判断 導入効果 今後の展望 まとめ 背景・課題 本節では、モデル監視システム構築のきっかけとなった2025年5月の事例と、その振り返りから明らかになった私たちの課題を説明します。 2025年5月に発生した推薦モデルの性能障害 2025年5月6日、推薦APIが返す推薦結果の分布が大きく変化しました。推薦される商品が極端に偏り、推薦経由のクリック数と売上が減少しましたが、この影響は5月13日に内製モデルへ切り替えたことで収束しました。その後、Google Cloudのサポートと連携して調査を進めました。推薦性能の回復をABテストで確認したのち、5月23日にAI Commerce Search APIへ切り戻しています。発生から切り戻しまで17日間を要しました。その大半は、私たちが異常と判断し、対応方針を決めるまでに費やした時間です。 システムエラーは一切発生しておらず、エラー率とレイテンシはいずれも平常時の水準を保っていたため、推薦APIは正常に動作しているように見えていました。そのため異常に気づくまでに約1週間を要しました。事象の時系列は次の通りです。 日付 事象 5月6日 事象発生 5月6日〜5月12日 ビジネスKPI低下の兆候が現れるが、原因不明のまま経過 5月13日 異常と判断し、暫定対応として内製の推薦モデルに切り替え 5月13日〜5月22日 Google Cloudのサポートと連携しながら原因を調査 5月23日 AI Commerce Search APIの推薦性能の回復を確認後、ABテストを経て正式に切り戻し 異常と判断した後の調査では、まず推薦APIへのリクエスト数やインプレッションログの総量、ログ収集・アクセス状況に問題がないかを確認しましたが、いずれも異常はありませんでした。一方で、推薦されたユニーク商品数は5月6日を境に急激に減少し、一部の商品ばかりが繰り返し推薦される状態となり、ユーザーのクリック数も落ち込んでいました。これらの結果から推薦モデルの出力側に起因すると判断し、Google Cloudへの問い合わせに至りました。 検知に1週間を要した理由 当時の監視状況を整理すると次の通りです。 監視対象 今回の事象での検知 システム指標(エラー率、レイテンシ等) 異常なしのため検知不可 ビジネスKPI(CTR、CVR、売上等) 低下は検知したが原因の切り分けは不可 ZOZOTOWNの推薦システムでは、以前からビジネスKPIをモニタリングする仕組みを運用しています。この仕組みの詳細は以下の記事で紹介しています。 techblog.zozo.com 今回の事象も、この仕組みが発報したビジネスKPIのアラートによって発覚しました。つまり異常の兆候そのものはSlackに届いていました。それにもかかわらず「明らかに異常である」と判断してアクションを起こすまでに1週間を要しました。要因は次の3つです。 1つ目は、ビジネスKPIだけではモデル起因と断定できないことです。ビジネスKPIは施策やセールイベント、季節性などさまざまな要因で変動します。アラートを受け取った時点で分かるのは「KPIが下がった」という事実だけであり、その原因がモデルにあるかどうかは切り分けられません。今回のKPI低下が急落ではなく緩やかな低下だったことも、判断を難しくしました。さらに、この仕組みの集計は日次であり、異常の発生から検知までに最短でも1日かかります。モデル起因かどうかを確かめるには、ユニーク商品数の集計といった調査を別途手動で行う必要がありました。 2つ目は、アラートからモデル切り替えの判断・実施までのフローが未整備だったことです。アラートを受けて誰が一次調査を担うのか、どの時点で内製モデルへの切り替えを検討するのか、サービス提供元への問い合わせを誰が起票するのか、いずれも曖昧でした。また、内製モデルへの切り替えは、モデルの実装からインフラのリリース作業まで複数の工程にまたがります。システム障害に対応するオンコール手順は整備していましたが、モデルの品質劣化を理由とした切り替えは想定しておらず、工程ごとの作業分担や手順書がありませんでした。そのため、アラートを見た担当者が調査を始めても、次に何をすべきかを都度相談しながら決める必要がありました。 3つ目は、何をもってモデルの品質劣化を「対応すべき異常」と判断するかの基準がなかったことです。どの指標がどの水準まで下がったら異常なのか、その状態がどれだけ続いたら対応を始めるのかを定義していませんでした。ビジネスKPIは日々変動するため、基準がなければ目の前の低下が通常の変動の範囲なのか、対応すべき異常なのかを区別できません。加えて、推薦モデルの変更は通常ABテストで効果を確認してから実施しますが、障害対応では検証しないまま切り替えることになります。切り替えによってビジネスKPIをかえって悪化させる可能性もあり、判断を裏付ける基準がないまま、このリスクを引き受ける意思決定はできませんでした。結果として、明確に異常と言い切れる材料が揃うまで対応を保留しました。 加えて、マネージドサービスを利用するうえでの備えも不足していました。モデル内部の状態が利用側から見えないことは責任分界点として当然であり、その前提に立てば、利用側には出力から品質を捉える手段と、切り替え先をあらかじめ用意しておくことが求められます。当時の私たちには内製モデルというフォールバック先はありましたが、出力から品質を捉える手段がなく、切り替えるべきかどうかの判断そのものに時間を要しました。 本システムが担う監視レイヤー 前述の通り、ZOZOTOWNの推薦システムには、これまで2種類の監視がありました。1つはシステム指標の監視で、エラー率やレイテンシから推薦APIが動いているかを捉えますが、レスポンスの中身は見ません。もう1つはビジネスKPIの監視で、レスポンスを受け取ったユーザーの行動を日次で捉えますが、変動要因を切り分けられません。 以上を踏まえ、私たちに欠けていたのは、この2つの間でレスポンスの中身そのものを短いサイクルで見るレイヤーだと整理しました。そこで構築したのが、推薦APIのレスポンスログから推薦品質を表す指標を集計し、モデル起因の異常を直接検知する監視システムです。3つのレイヤーの関係は次の通りです。 監視レイヤー 観測対象 サイクル 分かること システム監視(既存) エラー率・レイテンシ 常時 推薦APIが動いているか 品質監視(本システム) レスポンスの中身 60分 モデル起因の異常か ビジネスKPI監視(既存) ユーザーの行動 日次 事業影響が出ているか 本システムはビジネスKPI監視を置き換えるものではなく、両者を補完する関係にあります。あわせて、アラート発報後に誰が何をするかを定めた対応フローも整備しました。 以降ではまず推薦システムで起こりうる障害パターンを整理し、そのうえで監視指標と閾値の設計、運用体制の整備を説明します。 推薦システムで起こりうる障害パターン 本節では、監視指標の設計に先立って行った障害パターンの整理を説明します。 前節の事例は「推薦商品の種類が極端に減る」というものでした。しかし推薦システムの品質劣化がこの形で現れるとは限りません。そこで、まず障害がどのような形で現れうるかを洗い出し、それぞれをどう検知するかを検討しました。 3つの障害パターン 推薦システムで起こりうる障害パターンを次の3つに整理しました。 障害パターン 内容 例 多様性異常 通常時より、推薦される商品の種類が大幅に増える・減る 1000回の推薦で10種類の商品しか推薦されない 関連性異常 通常時より、閲覧中の商品と関連性の高い・低い商品ばかりが推薦される 閲覧中の商品と無関係な商品ばかりが推薦される レスポンス欠落 返却件数が減る、または空になる 通常100件返る推薦が0〜2件になる 2025年5月の事例は、多様性異常に該当します。推薦されるユニーク商品数が激減し、一部の商品ばかりが表示されていました。 関連性異常は、種類数だけを見ると正常に見えるパターンです。ユニーク商品数が保たれていても、推薦の分布が特定の商品やカテゴリに偏っていれば品質は劣化しています。種類減少の監視だけでは検知できません。 レスポンス欠落は、推薦結果そのものが返らなくなるパターンです。モデルの障害だけでなく、モデルへ連携するデータの欠損によっても発生します。 監視指標と閾値の設計 本節では、監視システムが集計する3つの指標と、その閾値の設計を説明します。 モデル品質の劣化を早期に捉えるため、レスポンスの中身に着目した指標を選びました。仕組みは、直近ウィンドウの指標をBigQueryのクエリで集計し、その結果を固定閾値と比較するというシンプルなものです。機械学習による異常検知は用いず、閾値を超えたかどうかだけで判断する構成としています。 監視する3つの指標 前節で説明した3つの障害パターンに対して、それぞれ以下の監視指標を割り当てました。 障害パターン 監視指標 多様性異常 カバレッジ 関連性異常 カテゴリ偏り率 レスポンス欠落 返却アイテム数 カバレッジ カバレッジは、直近ウィンドウ内で実際に推薦された商品のユニーク数を、ZOZOTOWNが扱う全商品のユニーク数で割った値です。 coverage = 推薦されたユニーク商品数 ÷ ZOZOTOWN全体のユニーク商品数 この指標は推薦商品の「網羅性」を見るためのものです。値が小さいほど、推薦が一部の商品に集中し、ロングテール商品が推薦機会を得られていない状態を示します。逆に値が大きい場合は、推薦ロジックが意図した絞り込みをできていない可能性があり、システム的な異常を疑うシグナルです。 カテゴリ偏り率 カテゴリ偏り率は、閲覧中の商品と推薦商品それぞれのカテゴリを突き合わせ、両者が一致する割合を算出したものです。ここでのカテゴリは、トップスやシューズといった大まかな商品分類を指します。 same_category_ratio = 同一カテゴリ推薦数 ÷ 推薦総数 この指標は推薦の「関連性」と「多様性」のバランスを見るためのものです。閲覧中の商品と同じカテゴリの商品が推薦されること自体は関連アイテムとして自然であり、平常時から高い割合を占めます。そのため見ているのは値の絶対的な高低ではなく、平常時の水準からの逸脱です。平常時より偏りが強い場合は、似たような商品ばかりを繰り返し提示することになり、発見体験を損ないます。逆に偏りが弱い場合は、閲覧中の商品と無関係な商品が返っている、つまり推薦モデルの性能そのものが劣化している可能性を示します。 返却アイテム数 返却アイテム数は、1リクエストあたりに推薦APIが返すアイテム数の平均値です。推薦システムには、リクエストされたサイズ分の推薦結果を返すことを期待します。しかし、推薦候補の生成に失敗したり、フィルタリング処理が過剰に働いたりすると、この平均値が低下します。 この指標は推薦の「充足性」を見るためのものです。平均値が下がっている場合は、推薦枠の一部が埋まらないままユーザーへ表示されている状態を示します。極端なケースでは返却件数が0に近づき、推薦商品がほとんど表示されません。カバレッジとカテゴリ偏り率が推薦の中身の質を見る指標である一方、返却アイテム数は推薦が量として成立しているかを見る指標です。 集計ウィンドウ 集計ウィンドウは、複数のウィンドウ幅で実際のログを集計し比較検証したうえで60分としました。 まず、ノイズ耐性を比較しました。ウィンドウが短いほど、深夜帯の低トラフィックや一時的なリクエスト偏りによるノイズを拾いやすく、指標が不安定になります。 次に、過去に発生した実際の事象のデータを使って検知速度を検証しました。ウィンドウを短くするほど検知は早まりますが、差はごくわずかでした。検知速度の差はわずかである一方、ウィンドウを短くするほど増加するBigQueryの実行コストは無視できない規模になることが分かりました。 Warning・Criticalの2段階閾値 閾値にはWarningとCriticalの2段階を設けています。 Warning:統計的に異常な値であり、調査を推奨するレベルです。過去の正常稼働データ(品質劣化が発生していた期間を除外済み)の分布から、下側・上側それぞれの裾に位置するパーセンタイル値をWarningの下限・上限としています。 Critical:明確な異常であり、即時対応が必要なレベルです。Warningよりもさらに外側の裾にあたる値を下限・上限としています。 この2段階は、アラート受信後の対応の重さに直結しています。推薦APIは複数のバックエンドを切り替えられる構成です。Warningでは担当チームが指標の推移を確認し、必要に応じて調査するにとどめます。Criticalでは即時に確認したうえで、フォールバックロジックへの切り替えを検討します。 閾値の設定根拠も、運用を重ねる中で変化しました。当初はCriticalの閾値を、過去に発生した事象の実測値に直接紐づけて設定していました。実測値を基準にすれば、少なくとも過去に起きた品質劣化は確実に検知できるためです。ただし、次に起きる品質劣化が同じ現れ方をするとは限りません。特定の事象の値に閾値を強く依存させると、異なるパターンの劣化を見逃すリスクがあり、閾値の意味も説明しづらくなります。そこで、特定の事象を基準にせず、劣化が発生していた期間のデータは除いたうえで閾値を設定する方針に変更しています。 この閾値は固定ではありません。長期のデータ分析の結果、カバレッジには下降トレンドがあると判明しました。推薦対象の商品数が増加する一方で推薦枠は有限であるため、カバレッジは構造的に下がりやすい性質を持ちます。固定閾値のまま運用すると、正常なトレンド変化をWarningとして誤検知するようになります。 この問題への対応として、蓄積されたデータから閾値を月次で自動再算出するGitHub Actionsワークフローを構築しました。このワークフローは、閾値の算出結果をPull Requestとして起票します。再算出時には過去にCriticalと判定されたデータポイントを除外し、品質劣化時の異常データが閾値を不当に広げないようにしています。 運用設計 背景・課題で整理した通り、2025年5月の事例ではビジネスKPIのアラートが発報されていたにもかかわらず、誰がいつ何をするかが曖昧だったために対応が遅れました。そこで監視システムの構築とあわせて、アラート発報後のフローを整備しました。 アラート対応フロー アラートは専用のSlackチャンネルに通知されます。本システムが検知するのはサービスの停止を伴う障害ではなく推薦品質の劣化であり、対応する場合も後述の通り人による判断と、切り替え・切り戻しに伴う複数工程の作業を要します。そのため即時の復旧作業を前提とせず、夜間・休日に発報したアラートは翌営業日に確認する運用としています。アラート発報後の対応は次の5ステップで進めます。 1. アラート確認 ↓ 2. 内製モデルへの切り替え判断 ↓ 切り替えが必要と判断した場合 3. 内製モデルへの切り替え実施 ↓ 4. ABテストでAI Commerce Search APIの性能回復を確認 ↓ 回復を確認できたら 5. AI Commerce Search APIへの切り戻し実施 アラート確認では、どの指標が閾値を超えたか、いつから発生しているか、継続的か一時的かを調べます。Warningであれば必要に応じて調査し、Criticalであれば即時に原因調査を開始します。 切り替えが必要と判断した場合は、フォールバック先である内製の推薦モデルへ切り替えます。まず内製モデルのAPIをスケールアウトし、推薦APIのデフォルトの推薦元を変更したうえで、ステージング環境での動作確認を経て本番リリースします。この手順は2025年5月の事例における暫定対応と同じものですが、当時は手順が整備されておらず対応者の判断に依存していました。現在は作業内容と担当をドキュメント化しており、誰でも同じ手順で切り替えられます。 AI Commerce Search APIの性能が回復したかどうかは、ABテストの実施とGoogle Cloud側への状況確認を通じて判断します。回復が確認できたら、AI Commerce Search APIへ切り戻します。 自動フォールバックを行わない判断 運用設計にあたり、品質劣化を検知した際に内製モデルへ自動で切り替える仕組みも検討しました。検討の結果、自動フォールバックは行わず、人間が判断する方針としました。 エラーやタイムアウトであれば、発生の事実そのものが切り替えの根拠になるため自動化と相性が良いです。一方、品質劣化はそれほど単純に判断できません。推薦モデルはマネージドサービスであるAI Commerce Search APIが提供しており、その内部の挙動を観測できません。そのため、指標の低下がモデル起因の品質劣化なのか、セールイベントなどによる一時的な変動なのかをアラート単体で断定するのは困難です。切り替えの判断はルールで完全に決められるものではなく、アラートの状況・指標の推移・ビジネスへの影響を総合的に見て担当者が判断する方針としました。判断の参考例は次の通りです。 参考例 具体例 推薦性能の著しい悪化 Critical閾値を超えた場合、または継続的にWarning閾値を超えた場合 サービス提供側での障害 Google Cloudサポートで障害が確認された場合 ビジネスKPIの明確な低下 ビジネスKPIモニタリングでの低下確認 また、偽陽性のアラートで自動切り替えが実行されると、正常なモデルを不必要に切り替えることになり、切り戻しのためのABテストを含む一連の運用コストが発生します。運用を開始した直後はアラートの偽陽性がどの程度発生するか未知数であり、自動化はアラートの信頼性を運用実績で確認できてから検討すればよいと判断しました。 導入効果 本節では、監視システムの導入によって得られた効果を紹介します。 1つ目は、検知速度の改善です。2025年5月の事例では、5月6日の発生から異常と判断するまでに約1週間かかりました。本システムは60分ごとに指標を集計するため、当時のデータを用いた検証では同規模の品質劣化を発生から約1時間で検知できます。検知が1週間から1時間に短縮される意味は、単に気づくのが早くなることだけではありません。アラート当日中に内製モデルへの切り替え判断まで進められるようになり、品質劣化が売上に影響し続ける期間を、日単位から時間単位に短縮できます。 2つ目は、判断基準の明確化です。従来は何をもってモデルの品質が「劣化している」とするかを定義していませんでした。現在はWarningとCriticalの2段階の閾値が、その基準を数値として与えています。アラートを受けた担当者は、指標がどちらの水準にあるかを見て、調査にとどめるか切り替えを検討するかを判断できます。サービス提供元へ問い合わせる際にも、どの指標がいつからどの水準にあるかを数値で示せるため、状況を正確に共有できます。異常と判断するまでの意思決定が、個人の経験や勘に依存しなくなりました。 3つ目は、対応フローの整備です。アラート発報後の確認・切り替え・切り戻しの手順と担当を文書化しました。2025年5月の事例では、内製モデルへの切り替えを対応者がその場で判断しながら進めていました。現在は同じ作業を、手順書に沿って誰でも実施できます。対応する時間帯とその考え方も明示したため、アラートを受け取った側が即時に対応すべきかを迷わずに済みます。 4つ目は、モデル品質の常時可視化です。アラートの有無にかかわらず、ダッシュボードでカバレッジやカテゴリ偏り率の推移をいつでも確認できます。異常の検知だけでなく、緩やかな変化の傾向把握にも活用しています。前述したカバレッジの下降トレンドのように、アラートには至らない変化もこの蓄積データから把握でき、閾値を見直す際の判断材料になっています。 一方で、本システムだけで推薦品質のすべてを担保できるわけではありません。種類・分布・件数がいずれも正常なまま推薦の中身だけが劣化するケースは推薦APIのログから検知できないため、この役割は引き続きビジネスKPI監視が担います。自動フォールバックも導入しておらず、切り替えの判断には人が介在します。偽陽性の発生状況に応じた閾値の調整も、運用しながら継続しています。 今後の展望 運用を続ける中で、WarningとCriticalいずれの閾値にも見直す余地があると分かってきました。前述の通りそれぞれの閾値の下限・上限には正常稼働データの分布の裾にあたるパーセンタイル値をそのまま用いています。これは、正常に稼働していても一定の割合で必ず発報することを意味します。実際に運用してみると、アラートの多くは調査しても異常が見つからず、ノイズとなっていました。 そこで、この知見をもとに、月次の閾値再算出にあわせてパーセンタイル値にマージンを加える方針や、経験的に異常と判断される値を閾値として設定し定期的に見直す方針を検討しています。 また、品質指標の劣化だけでは、切り戻しに踏み切るかどうかの判断材料としては不十分です。品質指標の劣化が必ずしもビジネスKPIの悪化に直結するとは限らないためです。しかし既存のビジネスKPIモニタリングは日次集計であり、アラートが発報した時点で参照できるのは前日までの値に限られます。そのため、60分周期で検知した異常について、その時点までにビジネスKPIへ影響が出ているかを確認する手段がありません。そこで、アラート対応フローの中で直近のビジネスKPIをその場で集計できるクエリを別途用意し、品質指標とあわせて確認したうえで切り戻しを判断できるようにしたいと考えています。 まとめ 本記事では、推薦APIのモデル品質を監視するシステムの構築背景から、指標・閾値の設計、運用体制の整備、運用を通じた改善までを紹介しました。 きっかけとなった2025年5月の事例では、エラー率とレイテンシはいずれも正常なまま、モデルの品質だけが劣化し、異常の認識までに約1週間を要しました。この経験から、システム監視とビジネスKPI監視の間を埋める品質監視のレイヤーを構築しました。起こりうる障害パターンを整理し、推薦APIのレスポンスログから集計できる3つの指標を定義し、約1年分のログ分析に基づいて集計ウィンドウと閾値を設計しました。あわせてアラート発報後の対応フローを整備し、同規模の品質劣化を約1時間で検知して対応を開始できる体制になりました。 また、閾値は一度設定して終わりではありませんでした。運用を通じてカバレッジの下降トレンドや偽陽性といった課題が明らかになり、固定閾値から定期レビュー、そして自動更新へと運用を段階的に改善してきました。監視システムを作ること以上に、閾値と向き合い続ける運用の設計に手間がかかると実感しました。 マネージドサービスとして提供されるMLモデルは、少ない運用コストで高い性能を得られる一方、内部の挙動を利用側から観測できません。これは責任分界点として当然の前提であり、だからこそ利用側が出力から品質を捉えるレイヤーを持つことで、マネージドサービスの利点を享受しながら安心して使い続けられると考えています。本記事で紹介した知見が、推薦システムやMLモデルの品質監視を検討している方の参考になれば幸いです。 最後に、ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロック・推薦研究ブロックともに絶賛採用しているため、ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。ZOZOでSREをしている三品、松石です。 6月9日〜6月10日に行われたDASH by Datadogへ、ZOZOのSREが2名参加しました。この記事の前半では熱気あふれる会場の様子を、後半では面白かったセッションについてご紹介します! 目次 はじめに 目次 DASH by Datadogとは 現地の様子 会場 入口 EXPO 朝食や昼食 基調講演(Keynote) アプリケーション、infrastructure運用におけるAI機能の紹介 アプリケーションエンジニア目線の新機能 その他 セッションレポート Beyond the Page Load: Observability for Figma’s Infinite Canvas 自己紹介 内容 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 自己紹介 内容 おわりに DASH by Datadogとは DASH by Datadog はDatadogが主催するDatadog最大のカンファレンスです。このイベントでは、Datadogが提供するクラウドアプリケーション向けにAIを活用したオブザーバビリティおよびセキュリティプラットフォームに関する新サービスやアップデートが発表されます。 2026年は世界中から約4,000人以上がニューヨークに集まりました。今年のDASH by Datadogでは、「DatadogのAIオブザーバビリティおよびセキュリティへの取り組み」を象徴するイベントでした。 現地の様子 会場 Javits Centerが会場となっており、KeynoteやEXPO、swag(ノベルティ)など様々なコンテンツがここに集合しています。そんな会場であったコンテンツをご紹介します。 筆者は本イベントへの参加は初めてだったのですが、会場の熱気や日本では味わえない雰囲気・規模に終始圧倒されました! 入口 会場の入口にはDASH BY DATADOGと書かれたWelcomeボードがありました。 会場入口の「DASH BY DATADOG」Welcomeボード EXPO こちらはEXPOです。今年は国外の企業数十社がブースを出しており、プロダクトのDemoを直接見たりできます。また、EXPOでは各プロダクトに精通するエンジニアや関係者に気軽に相談したり議論を交わしたりできます。 EXPO Hallの入口。ブース案内と「DASH BY DATADOG」のロゴが見える 各企業のブースに行って、議論を交わしたりサービスの説明を受けたりしました。 「DevOps Undercover」ブースでデモ画面を見る参加者たち Bits AIが日本語に対応したということで、そのデモを拝見していました。 「Ask Bits」ブースでBits AIのデモ画面を見る参加者たち 弊社でもPoCを実施しており、一次対応の自動化やアラート対応の簡略化が期待されます。 さらに、EXPOでは恒例のswag集めも活発に行われており、現地参加したメンバーがDatadogのswagを集めていました! Datadogのswagは可愛いですね! 集めたDatadogのswag(タオル、巾着、グラス、タンブラー) 朝食や昼食 DASHの会期中は、朝食や昼食が各会場で提供されており、参加者であれば無料で食べることができます。 朝食はビュッフェ形式、昼食はお弁当形式で、好きなものを選ぶことができました! 会場で提供されたサンドイッチのビュッフェ ラップとサンドイッチ、チップス、野菜が入ったランチプレート ビビンバボウルとラテのドリンク 基調講演( Keynote ) 今年のDatadog DASH Keynoteでは、AIを活用した新機能が数多く発表されました。 一方で、今回の発表を通して最も印象的だったのは、新機能そのものではなく Datadogが目指す方向性 です。 これまでDatadogはObservability Platformとしてメトリクス・ログ・トレースなどのテレメトリデータを収集・分析する基盤を提供してきました。今回のKeynoteでは、その豊富なコンテキストをAIが活用し、障害検知から調査、復旧までを自律的に実行する 運用の自動化 が大きなテーマとして紹介されていました。 本記事では、その中でも特に印象に残った機能を紹介します。 アプリケーション、infrastructure運用におけるAI機能の紹介 今回のDatadog DASHでは、アプリケーションやInfrastructureの運用を支援するAI機能として、 Bits Detection 、 Bits Memories 、 Bits Remediation 、 Bits Infrastructure Operations などが発表されました。 これらの機能を見て最も印象的だったのは、Datadogが単にAIアシスタントを追加したのではなく、 障害検知から調査、復旧までの運用フロー全体をAIが支援する「運用の自動化」 を目指している点です。 従来の障害対応では、SREや運用担当者がMonitorのアラートを起点として原因を調査し、Runbookや過去の障害対応を参考にしながら復旧作業を進めるのが一般的でした。一方、今回発表されたBitsシリーズでは、この一連の流れをDatadog上で自動化することを目指しています。 特に Bits Detection では、人間があらかじめ監視ルールを作成しなくても、新しく追加されたEndpointやService Topology、過去のテレメトリを基にAIが監視対象を理解し、自動で異常を検知できるようになっています。従来の「Monitorを作って監視する」という考え方から、「AIがサービスの変化を理解しながら監視する」という方向へ進化している点が印象的でした。 また、 Bits Memories では、Slackのやり取りやRunbook、Postmortemなどに蓄積された過去の障害対応をAIが学習し、次回の障害調査で活用できるようになります。これまで属人化しがちだった運用ノウハウをAIが再利用できるようになるため、調査時間の短縮やナレッジ共有にも役立つと感じました。 さらに、調査結果を基に実際の復旧作業を行う Bits Remediation や、Kubernetesの設定変更やPodの再起動など日常的なInfrastructure運用を自動化する Bits Infrastructure Operations も発表されました。一方で、本番環境ではすべての操作をAIに任せたいわけではありません。そのため、どの操作をAIが自律実行できるか、どの操作では人間の承認を必須とするかを制御できる Bits Guardrails も合わせて提供されており、安全性を担保しながらAIを運用へ組み込める設計になっていました。 Keynoteで発表された「Bits Detection」 / DASH 2026 Keynote 08:36より引用 Keynoteで発表された「Bits Memories」 / DASH 2026 Keynote 12:56より引用 Keynoteで発表された「Bits Remediation」 / DASH 2026 Keynote 15:31より引用 Keynoteで発表された「Bits Infrastructure Operations」 / DASH 2026 Keynote 19:03より引用 Keynoteで発表された「Bits Guardrails」 / DASH 2026 Keynote 20:37より引用 今回のKeynoteを通して、Datadogはこれまで強みとしてきたObservabilityのデータをAIの判断材料として活用し、「検知・調査・復旧」という運用サイクル全体を自動化する方向へ大きく舵を切ったという印象を受けました。 アプリケーションエンジニア目線の新機能 アプリケーションエンジニア目線で特に印象的だったのは、 Bits Database Optimization です。Datadogには従来からDatabase Monitoring機能があり、パフォーマンス改善が期待できるSQLをレコメンドする機能が提供されていました。今回発表されたBits Database Optimizationは、その機能をさらに発展させたものになっていました。 近年では、LLMにSQLの改善案を提案してもらうケースも増えています。しかし、提案されたSQLが本当に性能改善につながるのかを判断するには、実際に検証環境でベンチマークを実施する必要があり、結果的に人手による検証コストが発生してしまいます。 Bits Database Optimizationでは、Databaseのスキーマ情報や統計情報を基にシミュレーション用のデータベースを構築し、改善前後のSQLを実際に実行して実行時間や論理読み込み数などを比較・検証します。さらに、実行計画(Execution Plan)の比較まで行った上で、改善内容をPull Requestとして生成できるため、 LLMが提案した改善案を「本当に適用してよいか」まで自動で検証できる 点が印象的でした。 単にSQLの改善案を提示するだけではなく、その妥当性までDatadogが検証してくれるため、AIが提案した内容を安心して採用しやすくなる仕組みになっていると感じました。Observabilityで取得したデータを活用してAIの提案を裏付けるというアプローチは、Datadogらしい進化だと感じました。 Keynoteで発表された「Bits Database Optimization」 / DASH 2026 Keynote 36:35より引用 Keynoteで発表された「Bits Database Optimizationによる改善前後のSQLベンチマーク比較画面」 / DASH 2026 Keynote 38:05より引用 その他 このほかにも、今回のKeynoteでは印象的な機能が数多く発表されました。 その1つがJourney Monitoringです。 従来のDatadogでもRUMやProduct Analyticsを利用することで、ユーザーがどのような経路でサービスを利用し、どこで離脱したのかを分析することはできました。しかし、Journey Monitoringでは実際のユーザー行動からJourneyを自動的に生成し、コンバージョン率やエラー率、SLOなどをJourney単位で可視化できるようになりました。さらに、Journeyに異常が発生した場合はBits AIがバックエンドのテレメトリやデプロイ情報と関連付けて調査を行うため、ビジネス指標の変化から技術的な原因までを一連の流れで分析できる点が印象的でした。 Keynoteで発表された「Journey Monitoring」 / DASH 2026 Keynote 53:15より引用 Keynoteで発表された「Journey Monitoringのダッシュボード画面」 / DASH 2026 Keynote 53:21より引用 また、Observabilityの基盤となるメトリクスについても大きなアップデートがありました。今回発表された Infinite Cardinality Metrics では、従来はコストやシステム上の制約から扱いづらかった高カーディナリティなメトリクスを、より柔軟に扱えるようになっています。AIを活用した分析では、ユーザーIDやリクエストごとの属性など、より細かな情報を保持する価値が高まっているため、このアップデートは今後のAI時代を見据えた基盤強化だと感じました。 一方で、高カーディナリティなデータを扱えるようになったとはいえ、収集するデータ量が増えればコストにも影響します。そのため、分析に必要な粒度とコストのバランスを考えながら設計することが重要である点は、これまでと変わらないと感じました。 Keynoteで発表された「Infinite Cardinality Metrics」 / DASH 2026 Keynote 49:56より引用 今回紹介した機能以外にも、Network MonitoringやFederated Logs、Bring Your Own Cloud、AI Guardなど様々な機能が発表されていました。 今回のKeynoteを通して感じたのは、DatadogがAIを単なるチャットアシスタントとして提供するのではなく、長年蓄積してきたObservabilityのデータを活用し、開発・運用・セキュリティ全体を自律化するプラットフォームへ進化しようとしていることです。今後これらの機能が一般提供され、実際の運用でどのように活用されていくのか楽しみです。 セッションレポート ここからは三品、松石が気になったセッションを紹介します。 Beyond the Page Load: Observability for Figma’s Infinite Canvas 自己紹介 新規事業部バックエンドブロックの三品です。私が所属する新規事業部バックエンドブロックでは、ZOZOにおける「Near Fashion」領域の新規事業を立ち上げるため、日々PoCや開発、運用を行っています。 1 今回初めてDatadog DASHに参加して、私が業務の中で活かせそうだなと思ったセッションをいくつか紹介します。 内容 このセッションでは、Figmaが無限に広がるCanvasという特殊なプロダクトに対して、どのようにDatadogのRUMを導入し、フロントエンドのObservabilityを改善していったのかという取り組みについて紹介していました。 なお、この記事ではCanvas固有の実装には深く触れず、RUM導入の進め方という観点に絞って紹介します。 Figmaでは、RUM導入以前はSentryとプロダクト分析用のデータウェアハウスを利用しており、問題が発生した際にはユーザーからHARファイルや作業中のファイルを提供してもらったり、Sentryにカスタムエラーを追加したりして調査していたそうです。しかし、この方法はユーザー・サポート・エンジニアのいずれにとっても負担が大きく、十分な調査ができないまま対応が終わってしまうケースもあったと説明していました。 Beyond the Page Load: Observability for Figma’s Infinite Canvas 5:21より引用 次に、そこから確立していった「パフォーマンス戦略」について説明していました。2024年頃、急速な機能拡大に伴ってパフォーマンスリグレッションが相次いで発生していたものの、当初は「何が原因で、いつから発生したのか」を把握する手段がなく、リアクティブな対応を余儀なくされていたそうです。 そこで、フレームレートやロード時間、クラッシュイベントなどのシグナルを継続的に収集・監視し、デプロイやFeature Flagごとの差分を比較しながらリグレッションを検知できる仕組みを整備するとともに、エンジニア自身がユーザーセッションを調査できる環境を構築していったとのことでした。 その基盤としてDatadogのReal User Monitoring(RUM)を採用し、ビュー、エラー、リソース、Long Tasks、セッションリプレイなどのデータを収集できるようにしたそうです。また、バックエンドのトレースとも関連付けることで、サポートチームとエンジニアが同じユーザーセッションをもとに調査できる環境を整備していました。 Beyond the Page Load: Observability for Figma’s Infinite Canvas 11:55より引用 その後、収集したデータを社内のメンタルモデルに適合させるための取り組みについて説明していました。Figmaでは、Datadog RUMのAPIを直接利用するのではなく、社内向けのObservability Clientを用意し、カスタム属性やエラー、ユーザー操作などを共通の方法で記録できるようにしていました。これにより、エンジニアごとに異なる実装になることを防ぎ、ダッシュボードの整備や運用方法の共有を進めやすくしているとのことでした。 Beyond the Page Load: Observability for Figma’s Infinite Canvas 13:20より引用 Beyond the Page Load: Observability for Figma’s Infinite Canvas 14:33より引用 Beyond the Page Load: Observability for Figma’s Infinite Canvas 15:42より引用 最後に、サンプリングによるコスト削減と障害調査の両立について説明していました。通常はコストを抑えるためにRUMやトレースをサンプリングしていますが、問題の調査が必要なユーザーに対してだけ一時的にDebug Modeを有効化し、RUMやトレース、ログなどを100%収集する仕組みを紹介していました。これにより、普段はコストを抑えながら、問い合わせがあった際には必要な情報を取得し、より効率的に原因を調査できるようになったとのことでした。 Beyond the Page Load: Observability for Figma’s Infinite Canvas 24:44より引用 Beyond the Page Load: Observability for Figma’s Infinite Canvas 26:56より引用 Beyond the Page Load: Observability for Figma’s Infinite Canvas 28:15より引用 個人的に興味深かった部分は、RUMを導入するにあたって内部APIを作っていた部分でした。 個人的に最も印象に残ったのは、RUMを導入する際に Datadogの標準機能に合わせるのではなく、自分たちのプロダクトに合わせてObservabilityのレイヤを作っていた ことです。 私自身も新規事業部でDatadog RUMのPoCを行った際、本当に取りたい指標が標準機能だけでは取得できず、追加実装を行うか悩む場面がありました。そのときは運用コストを考え、標準機能を中心に利用する方針を選択しました。 一方、Figmaでは逆に、エンジニアが同じ考え方・同じ方法で計測できるよう、社内向けのAPIを整備していました。もちろん実装や保守のコストは増えますが、組織全体で一貫したObservabilityを実現するという考え方は参考になりました。単に「Datadogを導入した」のではなく、「自社の開発体験に合わせてObservabilityそのものを設計する」という発想を知れたことが、このセッションで一番の学びでした。 また、コスト観点についても興味深く聞いていました。 FigmaではRUM Explorerを中心に活用しているという話はありましたが、Session Replayについては「必要なときに使う程度」という位置付けでした。私自身もRUMを導入する際、コストが導入判断に大きく影響していたため、この点は特に興味深く聞いていました。 新規事業部では、まずRUMを導入し、コストが一定のラインを超えたタイミングでサンプリングを行う方針を検討しています。一方Figmaでは、普段はサンプリングによってコストを抑えつつ、問い合わせ対応など特定ユーザーの調査が必要になった場合だけDebug Modeを有効化し、RUM・Trace・Logsなどを100%収集するというアプローチを採用していました。 単純に「サンプリングする・しない」の二択ではなく、 通常時と調査時でObservabilityの粒度を切り替える という考え方は、コストと調査性を両立する設計として参考になりました。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 自己紹介 SRE部 会員ID基盤SREブロックの松石です。ZOZOTOWNの認証認可基盤のリプレイス・運用・保守に携わっています。 今回は、弊社でPoCを行っているBits Investigateについて、実際の活用事例として紹介されたセッションに参加しました。 内容 このセッションでは、American Express Global Business Travel(Amex GBT's)のNicolas Cortinezさんが、Datadog Bits Investigateを活用して、大規模かつ複雑なシステム環境におけるインシデント解決時間の短縮(MTTR)と持続可能な運用体制を構築した事例が紹介されました。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 2:57より引用 Amex GBTが持っていた課題として、多くのサービスや外部プロバイダーが依存し合う複雑なアーキテクチャのため、アラート発生時の情報収集や仮説の検証、影響範囲の特定に膨大な時間を要していることが話されました。 そのため、深夜のアラート対応など、情報過多な状況下での調査がエンジニアにとって大きな負担となっていたとのことです。 Amex GBTでは、このような課題からBits Investigateを導入したとのことでした。 ZOZOTOWNでも、複雑なアーキテクチャでマイクロサービスが依存し合うため、調査の難しさがあり、調査の簡略化に関しては、Bits Investigateに期待している観点です。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 6:36より引用 検証の段階では、14日間のフリートライアルで本番アラートに対してBits Investigateを有効化するように社内に展開し、費用対効果を確認していたとのことです。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 7:56より引用 実際に、Bits Investigateを活用した事例として、以前あった障害でアラートが発報した際に、モニターを確認すると、すでにアラートの解析サマリーが表示されており、その結果は正しいものだったとのことです。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 8:58より引用 また、詳細な調査結果を確認すると、調査に使用したAPMなども表示されており、障害調査に必要な情報は全て揃っていたとのことでした。 また、必要に応じて、調査フローを確認することで、Bits AIがどのような調査を行ったのかを確認できるとのことでした。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 10:08より引用 一方で、Bits Investigateを初期導入した際には、多くの課題もあり、正しい結果を得るためにコンテキスト情報の整理なども行ったことが紹介されました。 具体的には、障害が起きた際のログがBits Investigateには通常トラフィックとして判断されていたため、Bits Investigateのメモリーを使用することで学習させることができ、その結果正しい結果を得ることができたとのことでした。正しくFBを与え続けることが重要であるということも話されました。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 13:00より引用 Bits Investigateは優秀なAI Agentですが、Datadog NotebookやBits.mdなどで、Agentが推論しやすいような構成や仕組みを整えることも重要に思いました。 次に、ただFBを与えるだけでなく、コンテキスト情報を十分に与えることも重要であると話されました。 Bits.mdに共通のガイドライン設定やテレメトリリンクの明示的な記載を行い、全ての調査にこのBits.mdを提供したとのことでした。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 14:55より引用 次に、実際に組織内で活用され始め、全てのモニターに対してBits Investigateを有効にするチームがありました。その一方で、コストを気にして全くBits Investigateを使わないチームが存在しました。 この2つのケースは両方好ましいケースではないため、コストを抑制しつつ適切に使用できるガードレールを作成することにしたとのことでした。 具体的には、モニターのコストは定期的に観測し、Rate Limitsを設定したとのことでした。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 22:50より引用 Ratelimitは、1日に同じモニターがアラートになった際、毎回Agentを起動することを抑制したとのことです。これは、1日に同じモニターがアラートになる場合、根本的な原因は同じ可能性が高いためとのことでした。 また、組織全体でのAgentの起動を24時間で100回に設定したとのことでした。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 24:10より引用 さらに、どのモニターに対して有効にすれば良いかわからないという声が開発者からあり、ここではCriticalアラートに対してのみBits Investigateを有効にすることを推奨したとのことでした。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 25:35より引用 このセッションを通じて、弊社でもBits InvestigateのPoCを行っているため、コストと得られる効果についてPoC期間中に理解・判断することで、より良いオンコール対応とサービス信頼性の向上ができるように取り組んでいきたいと思いました。 特に、参考になったポイントとして、クリティカルアラートに対して有効にするという判断軸を作っていたことでした。必然的に、アラート自体の優先度も整備でき、組織全体のオブザーバビリティが整うため、良い取り組みに思いました。 また、1日あたりのAgentの起動回数をRatelimitで適切に制御することで、コスト爆発を抑制していることも学びになりました。 Beyond the Alert: How Amex GBT Accelerates Incident Resolution with AI 28:30より引用 AI Agentは大規模サービスや複雑なサービスにおいては有効ですが、Agentに与えるコンテキストやRunbookの整備は重要だと思いました。また、FBループを回すことでAgentはより賢くなっていきますが、そのループを回すためのコストや人間の認知コストなど導入をする上で乗り越える課題は多いと感じました。 おわりに セッションや展示ブースで多くのことを学べるのはもちろん、Datadogのエキスパートや他社のエンジニアの方々と交流し、多くの刺激を受けられるのが現地参加の醍醐味です。 今回得た知見を社内外に共有しながら、これからもDatadogを活用してプロダクトとビジネスの成長に貢献していきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com ZOZOの成長戦略 ↩
はじめに こんにちは、ZOZOMO部SREブロックの中村です。普段は ZOZOMO のSREを担当しています。 本記事では、CloudFrontでカナリアリリースを実現する方式の選定基準と、CloudFront FunctionsとKeyValueStoreの実装・運用で得られた実践的なノウハウをご紹介します。CloudFront + S3構成へのカナリアリリース導入を検討している方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 対象システムと従来の構成 満たすべき3つの要件 CloudFront Functions + KeyValueStoreの採用理由 アーキテクチャ 全体像 インフラ構成 リクエスト振り分けの実装(CloudFront Function) キャッシュキーの分離 GitHub Actionsによる運用フロー Build and Deploy to Canary Slot:カナリアスロットへのデプロイと10%リリース Promote Canary Slot to Active:完全な切り替え Rollback Canary Weight on Rejected Approval:承認却下時のロールバック まとめ 背景・課題 対象システムと従来の構成 私たちのチームが運用する配信基盤の1つは、Webページから読み込まれるJavaScriptやCSSなどの静的コンテンツを配信しています。 満たすべき3つの要件 この配信基盤には、次の3つの要件がありました。 カナリアリリースができること(社内の開発ガイドラインで必須) リソースをすべてIaCで管理できること(チームではCloudFormationを採用) S3へのデプロイをGitHub Actionsで自動化できること CloudFront Functions + KeyValueStoreの採用理由 CloudFront Functionsは、エッジロケーションで実行される軽量な関数です。CloudFront KeyValueStore(以下、KVS)は、CloudFront Functionsから低レイテンシで参照できるkey-value形式のデータストアです。値の更新はDistributionのデプロイなしに、通常数秒でエッジに反映されます( 公式ドキュメント )。 この組み合わせには次の利点があります。 カナリアへの流量を0〜100%の任意の値に設定できる Originの切り替えや流量変更がKVSの値の更新だけで完結し、GitHub Actionsからの自動化が容易 一方のトレードオフとして、振り分けロジックは自前で実装する必要があります。 なお、マネージドのカナリア機能であるCloudFront Continuous Deploymentも検討しました。しかし、CloudFormation管理下にあるDistributionの実体を直接書き換える必要があるため、テンプレートと実体の間にドリフトが生じます。IaC管理との両立が難しいと判断し、CloudFront Functions + KVSを採用しました。 アーキテクチャ 全体像 採用した構成の全体像です。 S3バケットを2面(slot1/slot2)用意し、CloudFrontに2つのオリジンとして登録する viewer-requestに関連付けたCloudFront Functionが、リクエストごとに配信先オリジンを選択する KVSは2つのキーを保持する。 active_origin (現在の本番スロット:slot1|slot2)と canary_weight (カナリアへの流量:0〜100) GitHub ActionsがS3へデプロイし、KVSの値を更新する インフラ構成 CloudFormationテンプレートで作成している主要なリソースは次のとおりです。 リソース 役割 S3バケット(slot1/slot2) コンテンツの配信オリジン。安定版とカナリアを保持する2面構成 CloudFront Distribution slot1/slot2を2つのオリジンとして登録 CloudFront Function viewer-requestでオリジンを振り分けるカナリアルーターの役割 CloudFront KeyValueStore リリース状態( active_origin / canary_weight )を保持 CloudFrontキャッシュポリシー x-canary-slot ヘッダーをキャッシュキーに含め、スロットごとにキャッシュを分離 CloudFront KeyValueStoreの値更新のみでカナリアリリースを実現できるため、インフラリソースの更新をリリース時に行う必要はありません。 リクエスト振り分けの実装(CloudFront Function) viewer-requestのCloudFront Functionの実装です。 import cf from 'cloudfront' ; async function handler ( event ) { try { const kvsHandle = cf . kvs () ; const activeOrigin = await kvsHandle . get ( 'active_origin' ) ; const canaryWeight = await kvsHandle . get ( 'canary_weight' ) ; if ( canaryWeight > 0 ) { const canaryOrigin = activeOrigin === 'slot1' ? 'slot2' : 'slot1' ; const hash = simpleHash ( event . viewer . ip ) ; if ( hash % 100 < canaryWeight ) { event . request . headers [ 'x-canary-slot' ] = { value : canaryOrigin } ; cf . selectRequestOriginById ( canaryOrigin ) ; return event . request ; } } event . request . headers [ 'x-canary-slot' ] = { value : activeOrigin } ; cf . selectRequestOriginById ( activeOrigin ) ; } catch ( e ) { event . request . headers [ 'x-canary-slot' ] = { value : 'slot1' } ; cf . selectRequestOriginById ( 'slot1' ) ; } return event . request ; } function simpleHash ( str ) { let hash = 0 ; for ( let i = 0 ; i < str . length ; i ++ ) { hash = (( hash << 5 ) - hash ) + str . charCodeAt ( i ) ; hash = hash & hash ; } return Math . abs ( hash ) ; } 実装のポイントは次のとおりです。 canary_weight が0より大きいときだけ、 active_origin とは違うスロットをカナリアとして扱う クライアントIPのハッシュ値でカナリア行きを判定するため、同一クライアントからの複数リクエスト間で新旧バージョンが混ざらない 実装は AWS公式ブログ で紹介されているコードを参考に作成した キャッシュキーの分離 キャッシュキーがslot1/slot2で共通のままだと、安定版のレスポンスがエッジにキャッシュされ、カナリア対象のユーザーにも安定版が返ってしまいます。逆方向の混在も起こります。 そこで、Functionが付与する x-canary-slot ヘッダーをキャッシュポリシーのキャッシュキーに含め、スロットごとにキャッシュを分離しました。 CloudFrontCachePolicy : Type : 'AWS::CloudFront::CachePolicy' Properties : CachePolicyConfig : # (中略) ParametersInCacheKeyAndForwardedToOrigin : HeadersConfig : HeaderBehavior : 'whitelist' Headers : - 'x-canary-slot' # (以下略) viewer-requestのFunctionはキャッシュの参照より前に実行されるため、Functionが付与したヘッダーをキャッシュキーとして利用できます。 GitHub Actionsによる運用フロー リリースはGitHub Actionsのワークフローで自動化しています。ジョブは「Build and Deploy to Canary Slot」→「Promote Canary Slot to Active」の2段階で進みます。承認が却下された場合のみ「Rollback Canary Weight on Rejected Approval」ジョブが実行されます。このほかに、結果をSlackへ通知する「Notify Deploy Results」ジョブがあります。 Build and Deploy to Canary Slot:カナリアスロットへのデプロイと10%リリース このジョブは次の順で処理します。 KVSから active_origin を取得し、その逆側をカナリアスロットに決定する カナリアスロットのS3バケットにのみ、新バージョンをアップロードする CloudFrontのキャッシュを削除する canary_weight を10に設定する ジョブの主要な定義は次のとおりです(実際のワークフローから一部を抜粋し、リソース名も変更しています。以降のコードも同様です)。 deploy-canary : name : 'Build and Deploy to Canary Slot' runs-on : ubuntu-latest outputs : canary-slot : ${{ steps.determine-slot.outputs.canary-slot }} kvs-arn : ${{ steps.determine-slot.outputs.kvs-arn }} steps : # (チェックアウト・ビルド・AWS認証のステップは省略) - name : 'Determine Canary Slot' id : determine-slot env : STACK_NAME : ${{ env.ENV }}-example-application-contents run : | kvs_arn=$(aws cloudformation describe-stacks --stack-name "${STACK_NAME}" \ --query 'Stacks[0].Outputs[?OutputKey==`CloudFrontKeyValueStoreARN`].OutputValue' --output text) echo "KVS_ARN=${kvs_arn}" >> "$GITHUB_ENV" echo "kvs-arn=${kvs_arn}" >> "$GITHUB_OUTPUT" active_origin=$(aws cloudfront-keyvaluestore get-key --kvs-arn "${kvs_arn}" \ --key active_origin --query 'Value' --output text) if [ "${active_origin}" = 'slot1' ] ; then canary_slot='slot2' elif [ "${active_origin}" = 'slot2' ] ; then canary_slot='slot1' else echo "Unexpected active_origin value from KVS: ${active_origin}" > &2 exit 1 fi echo "canary-slot=${canary_slot}" >> "$GITHUB_OUTPUT" - name : 'Upload Assets to Canary Slot S3 Bucket' env : S3_BUCKET : ${{ env.ENV }}-example-contents-${{ steps.determine-slot.outputs.canary-slot }} run : aws s3 cp --quiet dist/ "s3://${S3_BUCKET}/contents/" --recursive - name : 'Purge CloudFront Cache' env : STACK_NAME : ${{ env.ENV }}-example-application-contents run : | distribution_id=$(aws cloudformation describe-stacks --stack-name "${STACK_NAME}" \ --query 'Stacks[0].Outputs[?OutputKey==`CloudFrontDistributionID`].OutputValue' --output text) aws cloudfront create-invalidation --distribution-id "${distribution_id}" --paths "/contents/*" - name : 'Set Canary Weight to 10%' run : | etag=$(aws cloudfront-keyvaluestore describe-key-value-store --kvs-arn "${KVS_ARN}" \ --query 'ETag' --output text) aws cloudfront-keyvaluestore put-key --kvs-arn "${KVS_ARN}" --key canary_weight --value '10' --if-match "${etag}" Promote Canary Slot to Active:完全な切り替え カナリアの評価で問題がなければ、本番トラフィック全体を新バージョンへ切り替えます。GitHub EnvironmentのRequired reviewersを設定し、手動承認を昇格の条件にしています。 promote-canary : name : 'Promote Canary Slot to Active (Manual Approval Required)' runs-on : ubuntu-latest needs : [ deploy-canary ] environment : production steps : # (AWS認証のステップは省略) - name : 'Switch active_origin and Reset Canary Weight' env : KVS_ARN : ${{ needs.deploy-canary.outputs.kvs-arn }} CANARY_SLOT : ${{ needs.deploy-canary.outputs.canary-slot }} run : | etag=$(aws cloudfront-keyvaluestore describe-key-value-store --kvs-arn "${KVS_ARN}" \ --query 'ETag' --output text) aws cloudfront-keyvaluestore update-keys --kvs-arn "${KVS_ARN}" --if-match "${etag}" \ --puts Key=active_origin,Value="${CANARY_SLOT}" Key=canary_weight,Value='0' Rollback Canary Weight on Rejected Approval:承認却下時のロールバック 本番環境で承認者がRejectすると、ワークフローは失敗になります。これを if: failure() で検知して「Rollback Canary Weight on Rejected Approval」ジョブが実行され、 canary_weight だけを0に戻します。 active_origin は変更しないため、全トラフィックが安定版スロットへ戻ります。 rollback-canary : name : 'Rollback Canary Weight on Rejected Approval' runs-on : ubuntu-latest needs : [ deploy-canary, promote-canary ] if : failure() steps : # (AWS認証のステップは省略) - name : 'Reset Canary Weight to 0' env : KVS_ARN : ${{ needs.deploy-canary.outputs.kvs-arn }} run : | etag=$(aws cloudfront-keyvaluestore describe-key-value-store --kvs-arn "${KVS_ARN}" \ --query 'ETag' --output text) aws cloudfront-keyvaluestore put-key --kvs-arn "${KVS_ARN}" --key canary_weight --value '0' --if-match "${etag}" まとめ 本記事では、CloudFront FunctionsとKeyValueStoreによる、CloudFront + S3構成のカナリアリリースをご紹介しました。CloudFront + S3で配信するコンテンツへのカナリアリリース導入を検討している方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、データサイエンス部商品データサイエンスブロックの 荒木 です。我々のチームでは、AIやデータサイエンスを活用したプロダクト開発のため、研究開発に取り組んでいます。今回は、ZOZO NEXTのメンバーとともに2026年8月3日(月)から8月6日(木)にかけて出島メッセ長崎で開催された画像の認識・理解シンポジウム (MIRU) 2026に参加しました。本記事では、MIRU2026でのZOZO・ZOZO NEXTメンバーの取り組み、MIRU2026の様子や参加メンバーの気になった発表を報告します。 本記事は、データサイエンス部商品データサイエンスブロックの西山、同部コーディネートサイエンスブロックの大川、ZOZO研究所の和田、清水、古澤、サイ、平川、川島、武本、陳の協力のもと執筆されました。 目次 目次 MIRU2026 企業展示 全体の動向 昨年の記事との比較 ZOZO・ZOZO Researchメンバーの発表 [OS2D-10] Reference-Free Image Quality Assessment for Virtual Try-On via Human Feedback [IS1-149] MultiEmo-Bench: Multi-label Visual Emotion Analysis for Multi-modal Large Language Models [IS2-053] FashionOps-Bench: ファッションEC業務における視覚言語モデル評価ベンチマークの構築 [IS2-071] コーディネート推薦モデルのための集合関数の事前学習法 [IS2-077] 生成画像編集モデルによる服装の似合い度編集の人手評価に関する予備的検討 [IS2-080] 効率的マルチプロンプトVLM評価はいつ必要か:フロンティアモデルにおけるプロンプト感度の減衰 気になった研究発表 [IS1-105] Eコマース類似画像検索に向けたネイティブアスペクト比対応画像特徴モデルの構築 [OS1E-04] Quantifying Style Specificity of Font Impressions via Hyperbolic Co-Embedding [OS2D-02] HyDE Vector Mix: クエリと仮想回答の重み付き混合による高精度な密ベクトル検索 [OS3B-03] 数式駆動教師付き距離学習 [OS2E-10] CIRCLED: 一貫した対話と複数ドメインを備えたマルチターンCIRデータセット [OS1E-01] What-Where Transformer: 物体の意味と位置情報を分離し並行処理する画像バックボーン まとめ 最後に MIRU2026 MIRUとは、画像処理やその周辺の諸分野のトピックを取り扱う、国内において最大規模のシンポジウムです。毎年、コンピュータビジョンをはじめとしたさまざまな画像系の学術分野についての発表や、それに関する議論が活発に行われています。2026年の今回は、長崎市の出島メッセ長崎において開催されました。 MIRUの慣習として、1日目はチュートリアルセッションや特別講演を実施して、2日目以降で本会議がスタートするようなプログラムとなっていますが、今年は1日目の夜にウェルカムレセプションが開催されました。これは、MIRU史上初のイベントです。かつて、1日目の夜にはコミュニティの若手研究者のためのイベント「若手の会(現:若手プログラム)」が開催されていました。この時間帯に全員参加できるイベントが実施されるようになったことを鑑みると、MIRUおよび国内の研究コミュニティの多様性が広がっていることがうかがえます。 実際に、MIRUの参加者数は増加傾向にあり、今年は1,538名が参加しました。これは、2020年から2024年まで設けられていた遠隔聴講枠の人数を除くと、過去最大の参加者数です。さらに、発表件数も2020年から大きく増え続け、今年は932件の発表がありました。興味深いことに、参加者数は昨年から大きく増えたわけではありません(1,479名→1,538名)が、発表件数は約28.4%増(726件→932件)となっています。このこともあってか、今回は招待講演およびオーラルセッションが2会場でのパラレル実施となりました。聴講者として参加するだけでなく、実際に論文を投稿してコミュニティに貢献する方も増えていることが読み取れます。 そして、今年もZOZO NEXTはMIRU2026にゴールドスポンサーとして協賛いたしました。企業展示ブースの様子は次章にて紹介します。 企業展示 企業展示ブースでは、ZOZO NEXTの取り組みをポスター形式でご紹介しました。ZOZOの多角的なファッションサービスと多様なデータ資産に加え、ZOZO Researchが近年発表した論文、そして、機械学習を用いた実サービスへの応用事例についてご説明しました。今年も、多くの方々からご関心をお寄せいただき、お話をさせていただけたことを大変嬉しく感じています。 ブースにお越しいただいた皆様、各種トピックについてディスカッションしていただいた皆様にこの場を借りてお礼申し上げます。誠にありがとうございました。展示していたポスターはこちらです。 全体の動向 年に1回、画像を専門とする国内の研究者が一堂に会する会議、それがMIRUです。その性質上、参加するだけで業界の研究トレンドを横断的に把握できるのもMIRUの良さの1つです。去年のレポートでは、CLIPをはじめとする視覚言語モデル (Vision Language Model; VLM) を下流タスクに活用する研究が数多く見られたことを報告しました。 techblog.zozo.com 今年は、とにかくVLM、そしてマルチモーダル大規模言語モデル (Multimodal Large Language Model; MLLM) に関する研究を頻繁に見聞きしました。口頭発表およびインタラクティブセッションのどちらにおいても、これらを前提に議論が組み立てられている発表が本当に多く、もはや特定の研究トピックというより共通の道具になったという印象です。 その使われ方も、単純にVLM活用とは言えないほどに多様化しています。既存のVLMを特徴抽出器や推論エンジン、あるいはアノテータの代わりとして使う研究がある一方で、VLM自体の追加学習・軽量化・評価など、モデルそのものを対象にした研究が同じくらいの厚みで存在しています。特に目についたのは後者です。ハルシネーションの検出や軽減、出力の不確実性や信頼度の推定、MLLM自身を評価者として用いるMLLM-as-a-Judgeのバイアス分析、そして特定のトピックや領域に合わせた評価ベンチマークの構築といったテーマがありました。道具として定着したからこそ、その課題や限界を調査して解決する動きが出てきたのだと思います。 使われているモデルの顔ぶれが1年で大きく入れ替わっていたことも印象的でした。昨年はCLIPやGPT-4oを比較対象として見かけることが多かったのですが、今年はQwen3-VLやInternVL、GeminiやGPT-5系が並んでいます。視覚特徴の抽出についても、CLIP一択ではなくDINOv2やDINOv3、SigLIPを採用する研究が増えており、バックボーンの進化の凄まじさや選定の重要性についても考えさせられました。 一方で、3D再構成の研究も引き続き盛んに行われています。昨年目立った3D Gaussian Splattingは今年さらに広がり、少数視点からのfeed-forward推定や、反射・照明を扱う拡張など、応用の幅が明確に増えていました。VGGTやDUSt3Rといった3次元の基盤モデルを前提に用いる研究が増えているのも、基盤モデルの多様化が進んだ今年らしい変化です。加えて、ロボティクスやフィジカルAIに関する発表も増えていました。Vision-Language-Action (VLA) モデルやEmbodied AIのベンチマークなど、実世界を「見る」だけでなく「見て動く」ための研究が広がってきていることを肌で感じました。 また、推論コストやレイテンシ、モデルの軽量化、運用中のモデル更新といった、研究成果を運用に乗せるうえで避けては通れない論点を扱う発表も、昨年よりも多くなった印象です。研究と実装の距離がより近づいていることを感じさせます。 昨年の記事との比較 せっかくなので、 昨年の記事に書いた「全体の動向」 が、1年経った今どうなったかを調べてみました。 「CLIPをはじめとするVLMの活用が特に多い」 :この傾向は続いており、VLM・MLLMに言及する発表がさらに増えました。ただし、CLIPだけでなくDINOv2・DINOv3やSigLIPをベースラインに選ぶ発表がかなり増えました。視覚特徴を抽出する手法の有力な選択肢が複数登場しています。 「自らデータセットを作成、あるいは公開データセットを拡張して応用研究に取り組む」 :こちらは完全に定着しています。今年も多くの研究がベンチマークやデータセットの構築に言及しています。 「生成モデルの実用性とリスク管理、制御性や安全性」 :リスクの論点そのものが移動した印象です。著作権や権利保護に触れる発表は(完全に無くなったわけではありませんが)減った一方、モデル出力の安全性や公平性・バイアスを扱う発表は増えました。「生成物の権利をどう守るか」から「モデルの振る舞いをどう検証するか」へ、関心が移ったようなイメージです。 「2024年はNeRF、2025年は3D Gaussian Splatting」 :このトレンドも継続しています。3D Gaussian Splattingを扱う発表は増えていますが、NeRFは横ばいです。また、1日目のチュートリアルセッションでは、大阪大学の千葉先生が「三次元データ処理の動向」を講演しました。講演では、NeRFから3D Gaussian Splattingへトレンドが移り、現在は2D Gaussianや三角形メッシュなど、3D Gaussian以外のプリミティブへトレンドが移りつつあることが紹介されました。 昨年のレポートには書いていませんでしたが、昨年の時点ではVLMの活用がより深まっていくだろうと感じていました。ですが、実際はその予想を超える勢いで進んでいます。来年はこの「道具としてのVLM」をどう評価し、どう運用に載せるかという話がさらに前面に出てくると予想しています。また、マルチモーダル化やフィジカルAIに関しても、この勢いでどんどん進んでいくと考えています。 ZOZO・ZOZO Researchメンバーの発表 オーラルセッションにて1件、インタラクティブセッション5件の研究を発表しました。各研究の要約は以下の通りです。 [OS2D-10] Reference-Free Image Quality Assessment for Virtual Try-On via Human Feedback Yuki Hirakawa (ZOZO Research, Keio Univ.), Takashi Wada, Ryotaro Shimizu, Takuya Furusawa, Yuki Saito (ZOZO Research), Ryosuke Araki (ZOZO), Tianwei Chen, Fan Mo (ZOZO Research), Yoshimitsu Aoki (Keio Univ.) 本論文では、バーチャル試着(Virtual Try-On:VTON)画像の品質を、参照画像を必要とせず(reference-free)人間の知覚に近い形で自動評価する画質評価モデル「VTON-IQA」と、大規模な品質評価データセット「VTON-QBench」を提案しています。 バーチャル試着の評価指標として従来広く用いられてきたSSIMやLPIPSは、生成画像と正解画像の対応を前提としています。そのため、実際のECサービスのように正解画像が存在しない場面では利用が困難です。一方、FIDやKIDは画像集合における全体の品質を評価する指標であり、個々の試着画像の品質を評価できないという課題があります。さらに、人物のポーズや画像の拡大率が変化すると、人間には自然に見える画像でも低く評価される場合があります。 これに対し、提案手法であるVTON-IQAは、衣服画像、人物画像、生成された試着画像の3枚を入力します。評価するのは、衣服の形状や柄、ロゴなどを再現できているかを示す「衣服の忠実性」と、人物の顔や体型、背景などを保持できているかを示す「人物・背景の保持性」です。モデルにはInterleaved Cross-Attention(ICA)を導入し、試着画像を中心として衣服画像および人物画像との対応関係を効果的に学習する構造を採用しています。また、品質スコアを予測する回帰学習と、2枚の試着画像の優劣を学習するペアワイズ順位学習を組み合わせることで、人間の主観評価に近い品質予測を実現しています。 さらに、学習用データセットとしてVTON-QBenchを構築しています。これは、14種類の代表的なVTONモデルから生成した62,688枚の試着画像に対し、13,838人の評価者による431,800件の品質アノテーションを収集した大規模なVTON品質評価データセットです。実験では、VTON-IQAは人間評価との相関において既存指標を大きく上回る性能を達成し、未知のVTONモデルに対しても高い汎化性能を示しました。 本研究は、正解画像を必要とせず、人間の知覚に基づいて個々のバーチャル試着画像を評価できる新たな評価基盤を提供しており、今後のVTONモデルの開発や品質管理、性能比較への応用が期待されます。 発表資料は以下から閲覧できますので、ご興味のある方はぜひご覧ください。 speakerdeck.com [IS1-149] MultiEmo-Bench: Multi-label Visual Emotion Analysis for Multi-modal Large Language Models Tianwei Chen, Takuya Furusawa, Yuki Hirakawa, Ryotaro Shimizu, Fan Mo, Takashi Wada (ZOZO Research) マルチモーダル大規模言語モデル(MLLM)の感情理解を評価する際、既存ベンチマークでは「どの感情が喚起されるか」「どれが強いか」までしか測れず、視聴者集団における感情分布への適合度は評価できません。先行研究は離散判断・記述評定・強度順位付けの3パラダイムを提案しましたが、順位付け型は各感情の喚起量を問わず上位3感情のみを対象とするため、過剰予測の失敗が露呈しません。感情分布を持つ既存データセットにも、データ規模の小ささ(280〜1,980枚)、アノテーション密度の低さ(1枚あたり7人未満)、支配感情の偏り(59〜75%が同一カテゴリ)という課題があります。 そこで本研究では、EmoSetとFIのテストセットから10,344枚を再アノテーションしたMultiEmo-Benchを構築します。1枚あたり20人(関連研究の中で最多)がMikelsの8感情から複数選択する方式で計236,998票を収集し、感情ごとの周辺支持確率ベクトルを目標値に設定します。画像選定では3モデルに元ラベルの妥当性を判定させ、不一致5,279枚と層化抽出した一致5,065枚を結合します。これにより、最大カテゴリ約20%というバランス、および従来最高の密度を同時に達成しました。品質管理は資格タスク・注意チェック・A/Bテストの3段階で構成されています。これにより、20人のアノテータによる結果は193人による結果とPLCC 0.836で一致しており、新ラベルは元ラベルより58.7%対25.1%で支持されることを確認しています。評価は11指標・2プロトコル(感情ごとに個別質問するask each/8感情を一括質問するask all)の枠組みで実施しました。GPT系・Gemini系・Claude Sonnet 4・Qwen3-VL系の11モデルを同一条件で比較し、アノテータを10対10に分割した際の人間同士の一致度を、参照値として併置しました。 実験の結果、次に挙げる3点を確認しました。第1に、全モデルが系統的に感情を過剰予測し(総質量バイアス+1.08〜+3.09)、ほとんど誰も報告しない感情まで付与する一方、感情を一括で問うプロトコルではこの過剰がほぼ半減しました。第2に、順位一致(SRCC 0.59対人間0.66)は人間水準に近い一方で、総質量MAEは最良0.68対人間0.05と大きく乖離し、順位評価は分布評価の代替にならないことがわかりました。第3に、残るギャップは「認識」でも「順位」でもなく「何人が感じるかの見積もり」に集中しており、この傾向は選定バイアス検証用の一致・不一致サブセット双方で成立しました。 本論文からは、感情分布を予測するタスクにおけるMLLMの能力マップと、「強度推定が主要な残存課題であり、かつ微調整で解消可能な本質的でない限界である」という定量的示唆を得ることができます。 ※ポスター発表では最新の実験・分析結果を追加したため、MIRUの予稿とは一部内容が異なっています。 [IS2-053] FashionOps-Bench: ファッションEC業務における視覚言語モデル評価ベンチマークの構築 サイ・タウンカン(ZOZO研究所), 広渡朱莉, 伊澤遼平, 大川倫明, 桐島雅也, 椎橋怜史, 山﨑朋哉, 大島尚起, 石原百峰, 仲井誠明, 桜井詩音, 清水悠揮(ZOZO), 清水良太郎(ZOZO研究所) 視覚言語モデル(VLM)をファッションEC業務へ適用する際、汎用ベンチマークでは細粒度なファッション属性や業務直結の情報抽出を評価できないという課題があります。先行研究はフロントオフィス系2系統5タスクのベンチマークを提案しましたが、納品書などのバックオフィス業務が対象外であり、選別型・細粒度型タスクも欠けていました。 そこで本研究では、ベンチマークを3系統9タスクへ拡張したFashionOps-Benchを構築します。全身コーディネート画像・アイテム単体画像に加えてバックオフィス画像の系統を新設し、バーチャル試着品質チェック・サムネイル適正判定・アイテム自動タグ付け・納品書OCRの4タスクを新規追加しました。これにより、二値選別・構造化抽出・細粒度マルチラベル生成・生成画像の品質判定という多様な出力形式を網羅しています。評価は先行研究のマルチプロンプト評価枠組みを継承し、GPT-5系・Gemini 3.x系・Claude 4.x系の最新商用9モデルを同一条件で比較しました。 実験の結果、次の点を確認しました。第1に、最適モデルはタスク依存で、シーズン判定でgpt-5.4-mini、自動タグ付けでgemini-3.1-flash-liteなど、エントリーモデルがフラッグシップを上回る例が複数ありました。第2に、納品書OCRは全モデルでF1が0.5未満にとどまり、素材判定(最高0.285)とともに高難度タスクであることが分かりました。第3に、誤り傾向は主に実行モデルに固有で、プロンプト変更では動きにくく、自己最適化プロンプトの平均効果もほぼゼロでした。また提案プロンプトの他モデルへの転用効果は提案元に依存することも確認されました。 本研究からは、フロント・バックオフィス業務を横断するVLMの業務適性マップと、「プロンプト最適化よりモデル選定が支配的」という実運用への定量的示唆を得ることができます。 [IS2-071] コーディネート推薦モデルのための集合関数の事前学習法 堀田南(早大), 清水良太郎(ZOZO研究所, 早大), 平川優伎(ZOZO研究所, 慶大), 後藤正幸(早大) コーディネート推薦は、手持ちアイテムと候補アイテムの相性を推定する集合マッチング問題です。この問題を解くためには、学習に用いるための相性の良い商品画像集合を収集する必要がありますが、この収集は大規模かつ高コストという課題があります。 そこで本研究では、収集が容易な全身画像を活用した2段階学習法を提案します。事前学習では、まずは全身画像からキャプショニングモデルで各着用アイテムを言語記述として抽出します。そして、言語・画像で整合した表現空間を持つSigLIP2のテキスト特徴上でSet Transformerの集合関数をK-Pair-Set損失により学習します。事後学習では、少量の商品画像集合を用いて、画像特徴をテキスト特徴空間へ写像するアダプターを学習し、実コーディネートと置換負例の二値分類に適応させます。Garments2Lookデータセットで、入力形式・エンコーダ更新範囲・アダプター有無を組み合わせた7手法を比較しました。 実験の結果、few-shot設定では視覚情報を直接使うImage事前学習(upper bound)が最高精度を示しました。一方で、提案手法(Adapter)や固定テキスト特徴による事前学習も、事前学習なしのBaselineを明確に上回りました。逆に、テキストエンコーダを直接更新する手法は学習が不安定で、有効ではありませんでした。また、下流データが増えるとテキスト系手法が伸び、クロスドメインのrandom評価ではImageを上回りました。ただし、アダプターの改善効果は限定的で、カテゴリを揃えた難しい負例の設定では課題が残りました。 本研究からは、高コストな商品画像集合に依存せず、言語情報を介してコーディネートの組み合わせ知識を事前学習し画像推論へ転移するという設計の考え方と、その有効性・限界の定量的知見を得ることができます。 [IS2-077] 生成画像編集モデルによる服装の似合い度編集の人手評価に関する予備的検討 秦淇策(早大), 清水良太郎(ZOZO研究所, 早大), 平川優伎(ZOZO研究所, 慶應大), 陳天偉(ZOZO研究所), シモセラ・エドガー(早大) テキスト指示による画像編集モデルは、「より似合う服装」への編集が可能になってきていますが、編集後に似合い度が向上したかを自動で評価することは困難です。汎用編集モデルは、画質向上やしわの除去など、似合い度以外の要素も変化させます。そのため、評価モデルのスコア上昇が人間の「似合う」という評価と一致するとは限りません。 そこで本研究では、似合い度評価モデル(SigLIPベースの画像回帰モデル、専門家のペア比較に基づく約4万枚の似合い度スコアで学習)による自動評価と人手評価の一致度を検証します。10枚の着用画像に対し、GPT-image-2・Nano Banana 2・FLUX.2の3モデルで、アイテム変更の有無×似合う/似合わない方向の4条件、計120枚の編集画像を生成しました。12名の被験者が元画像基準で5段階の相対評価を実施し、計1,440件の評価と編集前後のモデルスコア差分との相関を分析しました。 実験の結果、全体では中程度の正の相関(Pearson r=0.403、Spearman ρ=0.375)が見られ、設定した編集方向は概ね人手評価に反映されていました。ただし、相関はモデル依存で、GPT-image-2(r=0.515)とNano Banana 2(r=0.561)は比較的高い一方、FLUX.2はr=0.147と弱い相関となりました。このように、似合わない方向の編集でも評価モデル差分が正になるなど、指示方向に関係なく画像全体の見栄えを改善する傾向が示唆されました。 本研究からは、似合い度評価モデルは生成画像の事前スクリーニングや傾向把握には有用である一方、人手評価を完全には代替できず、編集モデルごとの検証が必要という知見を得ることができます。 [IS2-080] 効率的マルチプロンプトVLM評価はいつ必要か:フロンティアモデルにおけるプロンプト感度の減衰 清水良太郎, サイ・タウンカン, 川島貴大, 北岸毅一 (ZOZO研究所) 複数プロンプトでのVLM性能評価は計算・API費用の面で高コストであり、PromptEvalなどの効率化手法が提案されてきました。しかしこれらはモデルが一定以上のプロンプト感度を持つことを暗黙に仮定しており、最新商用モデルの低感度化でその前提が崩れつつあります。 そこで本研究では、まず理論解析を実施しました。その結果、プロンプト感度(プロンプト間の性能分散)が小さいほど、プロンプト選択メカニズムの違いが平均の性能推定に与える影響も、感度の平方根オーダーで小さくなることを示しました。さらに、感度を低コストで同時に推定する枠組みを提案しました。標準プロンプト列と自己提案プロンプトの対角セルを必ず観測し、残り予算を一様ランダム観測に充て、指標ごとに異なる推定器(平均性能は直接平均、ばらつきは低ランク行列補完)で復元します。加えて、自己提案プロンプトが他モデル提案プロンプトをどれだけ上回るかを示す「自己提案バイアス」(モデルが自ら生成したプロンプトを過大評価する傾向)を感度の代理指標として導入しました。 提案法の有効性を確かめるために、16のVLM×ファッション系の5タスクで実験しました。その結果、効率化手法の優位性はモデルのプロンプト感度Sと強く相関し、低感度の最新商用モデルでは一様サンプリングを超える効率化の恩恵がほぼ失われることを実証しました。提案法は高感度群において、高精度で推定できました。また、自己提案バイアスは感度と正の相関を示し、高感度群と低感度群を概ね弁別できた一方、(プロンプト間性能の平均とばらつきに基づく)SN比は行列補完による推定に劣りました。感度推定のためのより良い指標については、今後の課題となります。 本研究からは、評価を効率化する手法の導入は「常に必要」ではなく、少数プロンプトで感度をパイロット測定してから適用可否を判断すべきという実務指針を得ることができます。 気になった研究発表 続いて、本会議を聴講していたZOZO・ZOZO NEXTメンバーが気になった研究発表をご紹介します。 [IS1-105] Eコマース類似画像検索に向けたネイティブアスペクト比対応画像特徴モデルの構築 土井賢治, 山下郁矢, 西村修平(LINEヤフー) Eコマースの画像検索は、ユーザーが撮った1枚の写真をクエリに、膨大な商品データベースから同一・類似商品を探し出す技術です。全商品画像を画像エンコーダでベクトル化して索引を構築し、クエリ画像も同じエンコーダでベクトル化します。その後、コサイン類似度に基づいて上位候補を返します。そのため、検索品質はエンコーダが生成する特徴表現の識別性と汎化性に左右されます。ここで問題となるのが、商品画像は縦長・横長・正方形とアスペクト比が大きくばらつく点です。多くのVision Transformer(ViT)は、正方形の固定解像度を前提としています。そのため、センタークロップや正方形パディングで入力を揃えると、対象物体の見切れや背景の増加が生じ、同一インスタンスを見分けるのに欠かせない細部の情報が失われかねません。 本研究では、大規模な視覚言語モデル「SigLIP2-NaFlex」を、商品インスタンス検索へ適応させる手法が提案されています。SigLIP2は、Sigmoid Lossによる視覚・言語の事前学習で強い表現を獲得したモデルです。そして、そのNaFlex版は画像を正方形に押し込めず、画像ごとに異なる縦横パッチ数(最大パッチ数Mの範囲内)を扱えます。位置埋め込みをアスペクト比に合わせて補間し、トークン数がMに満たなければパディングとattention maskでバッチ化するため、縦長・横長の画像でも形状と細部を残せます。著者らはこの基盤モデルを土台に、3つの工夫を組み合わせています。1つ目は、先述したように基盤モデルの特徴であるネイティブアスペクト比入力です。2つ目は学習設計で、クラス内凝集とクラス間分離を高めるArcFace型のmargin-softmaxを主損失に、特徴の過度な集中を抑えるKoLeo正則化を併用します。3つ目が「2段階fine-tuning」です。はじめに、neck/headやpooling head、position embedding、最終Transformer層など一部だけを学習します(Stage 1)。その後、モデル全体を小さな学習率で緩やかに調整します(Stage 2)。 同一物体インスタンスの検索能力を測る大規模評価セット「ILIAS」を用いて実験した結果、モデル全体を一気にfine-tuningした場合のmAP@20は0.3837となりました。一方、提案手法は社内評価をほぼ維持したまま、mAPを0.4122へ改善したと報告されています。特に興味深いのは学習対象レイヤのアブレーションです。backboneを完全にfreezeしてneck/headだけを学習するとタスク適応が不十分になる一方、patch embeddingまで学習対象に含めると社内評価・ILIASの双方で悪化しました。事前学習で得た汎用的な低レベル表現を保ちつつ、backbone headやposition embedding、最終Transformer 1層程度から段階的に更新する方針が有効だと示唆されています。 本研究により、基盤モデルをそのまま特徴抽出器として用いるだけでは、商品インスタンス検索には不十分であることがわかりました。入力処理・学習データ・fine-tuning対象を一体として設計することが検索品質を左右する、という知見を示しています。本研究の成果の一部は、商用利用が可能なApache-2.0ライセンスで公開モデルとして提供されており、商品検索だけでなく画像の汎用的な特徴抽出器として幅広く活用できます。 [OS1E-04] Quantifying Style Specificity of Font Impressions via Hyperbolic Co-Embedding Yugo Kubota, Kaito Shiku, Seiichi Uchida (Kyushu Univ.) フォントは「elegant」「cute」といった多様な印象を与えますが、印象語が許容するフォントスタイルの幅は語によって大きく異なります。例えば「elegant」は幅広いスタイルのフォントに当てはまる一方、「skinny」が指すスタイルはかなり限定的です。本研究では、印象語がフォントスタイルをどれだけ強く制約するかを「スタイル特異性(style specificity)」と定義し、これを定量化する手法を提案しています。 提案手法では、フォントと印象タグ集合を共有の双曲空間に共埋め込みします。双曲空間は原点から離れるほど体積が指数的に増大する性質を持ち、一般概念と具体概念の階層構造の表現に適しています。対照学習に加えて(i)印象がそれに適合するフォントを包含する、(ii)特異性の低い印象がより高い印象を包含する、という2つの包含関係をエンタイルメント損失として学習します。これにより、多様なスタイルと互換性のある印象ほど原点近くに配置され、原点からの距離がそのままスタイル特異性の指標として機能します。 MyFontsデータセットでの実験では、印象ベースのフォント検索において既存手法を多くの指標で上回り、特にタグ数の少ないクエリでの改善が顕著でした。タグ単位の分析も興味深く、原点付近には「retro」「elegant」のような形容詞的なタグが、遠方には「skinny」「typewriter」のような形状指向のタグが分布していました。一方、「heavy」のように幾何的な語でも特異性が低い例もあり、スタイル特異性は表面的な語義だけでは決まらないことが示されています。 この研究に興味を持ったのは、ZOZOで取り組んでいる、コーディネート画像から「クール」「親しみやすい」といった印象を予測するプロジェクトとの親和性を感じたためです。ファッションの印象も、幅広いコーディネートに当てはまるものからスタイルを強く限定するものまで制約の強さが異なるはずです。これを埋め込み空間の幾何的性質として定量化できれば、印象定義の分析や、印象を用いた検索・推薦における多様性の制御に応用できる可能性があると感じました。 [OS2D-02] HyDE Vector Mix: クエリと仮想回答の重み付き混合による高精度な密ベクトル検索 深田翔(東大), 欅惇志(一橋大), 松井勇佑(東大) テキストを用いた文書検索のタスクでは、検索クエリと文書をそれぞれ密なベクトルとして表現し、ベクトル空間上で類似度が高い文書を抽出する、いわゆるベクトル検索の手法が人気となっています。しかし、クエリと文書ではテキストの長さや内容が大きく異なるため、両者の類似度を正確に捉えることが難しいという課題があります。 本研究は、まず検索クエリから「どのような文書が回答となり得るか」をLLMで予測し、生成された仮想回答のベクトル表現と元のクエリのベクトル表現を重み付きで足し合わせて、クエリを表現します。これにより、仮想回答とクエリの両方を考慮した検索が可能になり、既存手法を大きく上回る検索精度を達成しました。 文書検索に仮想回答を用いるアプローチ自体はこれまでも注目されてきましたが、本研究ではクエリと仮想回答の両方のベクトル表現を組み合わせることを新たに提案しました。そして、提案手法はシンプルな手法ながらTREC 2025 RAG Trackという国際的なワークショップで12チーム中1位の検索精度を達成したという点に興味を抱きました。ZOZOでも、ZOZOTOWNをはじめとする各種サービスの検索精度を改善するための取り組みが日々行われているため、とても参考になりました。 [OS3B-03] 数式駆動教師付き距離学習 大竹ひな, 荒川深映, 田中啓太郎(早大), 福原吉博(産総研, 早大), 片岡裕雄(産総研, オックスフォード大), 森島繁生(早大) 画像認識モデルの事前学習には、ImageNetのような大規模な実画像データセットが広く利用されています。しかし、データ収集やアノテーションには大きなコストがかかり、プライバシーや社会的バイアスの問題もあります。そこで、数式から生成した画像だけを使ってモデルを学習する「数式駆動型教師あり学習 (FDSL)」が提案されています。FDSLは、画像生成に使ったパラメータを離散的なクラスラベルとして扱い、画像分類タスクとして事前学習します。一方、生成パラメータは本来連続的です。そのため、見た目がよく似た画像でも別クラスとして区別され、モデルが画像全体の形状ではなく細かなテクスチャなどに依存する場合があります。 本研究では、この問題に対して「数式駆動教師付き距離学習 (FDSML)」を提案しています。画像生成パラメータ間の距離を教師信号として利用し、パラメータが近い画像は特徴空間でも近く、遠い画像は離れるように学習します。言い換えれば、画像同士の「関係性」を理解するアプローチです。具体的には、アンカー画像と2枚の候補画像を選び、生成パラメータの距離に基づいてポジティブとネガティブを決定する「Parameter-Aware Triplet Loss」を導入しています。さらに、距離学習の効果を高めるために「FineFractalDB」を新たに構築しています。従来のFDSLの学習に用いられるFractalDBで見られた、反転しただけの類似画像や形状が崩れた画像を減らし、1000カテゴリ・100万枚からなる高密度で多様なデータセットを実現しました。 実験では、画像分類の性能を維持しながら、画像検索の性能が大きく向上しました。Stanford Online ProductsではRecall@1が従来手法の65.6%から74.0%へ、Stanford Carsでは56.2%から78.4%へ改善しています。加えて、画像のぼけや天候変化、デジタル劣化などに対する頑健性も向上しました。 本研究は、実画像を用いずMetric Learningに適したEmbedding表現を事前学習できる可能性を示しています。ZOZOTOWNでも 画像検索機能にMetric Learningを利用 しており、Embeddingモデルの事前学習や、構造変化に頑健な表現を獲得するために応用できそうな研究だと感じました。 [OS2E-10] CIRCLED: 一貫した対話と複数ドメインを備えたマルチターンCIRデータセット 武田朋久(東大), Yu-Chieh Lin(キオクシア), 野澤優治(キオクシア), Youyang Ng(キオクシア), 鳥井修(キオクシア), 松井勇佑(東大) CIR(Composed Image Retrieval)は、参照画像と修正テキスト(例:「これをもっとゆったりした赤いドレスに」)を組み合わせてクエリとする画像検索タスクです。当初は、キーワード検索より自然な「対話型ショッピングアシスタント」というユースケースが想定されていました。しかし、実際のユーザーは1回の検索で目的の商品にたどり着くとは限らず、検索結果を見ながら要件をテキストで追加していきます。そこで近年では、過去の画像・テキストペア履歴 {(I1,T1), …, (IL,TL)} 全体を使って検索するマルチターンCIRが提案されています。 本研究は、現時点で唯一公開されているマルチターンCIRのデータセットMulti-turn FashionIQが、シングルターンのクエリを単純に連結した構成であり、対話が正解へ段階的に近づく保証がないことを指摘しています。そして、正解の検索順位がターンごとに単調に改善することと、各修正テキストが過去のテキストと重複せず新情報を追加することを保証したデータセットを提案しています。提案データセットはFashionIQ・CIRR・CIRCOの3つのシングルターンCIRデータセットを拡張したものです。その構成は、ファッション+一般ドメインで22,608セッション・202,845画像(ターン長2〜6)のデータセット(CIRCLED)となっています。 LLM-as-a-Judgeでは5つの観点(自然さ・一貫性・目標進捗・冗長性の少なさ・総合)で評価しており、自然さを除く4つの観点では既存データセットを含む比較対象を上回っています。一方で、自然さのみ提案手法の品質フィルタリング後にわずかに低下することが報告されており、これはフィルタリングによる平均ターン数の減少に起因するとされています。なおデータセット生成時はGPT-4o-miniを使い、品質評価時はGPT-5-miniを使用することで自己選好バイアスを回避しています。また本データセットに対し既存手法を適用し、画像とテキストの両方を活用する手法が優位なこと、ターンを重ねるほど精度が向上すること(一貫性設計が機能していること)を確認しています。 ZOZOにおいても、より直感的に商品やコーディネートを探せる機能によってユーザ体験の向上を見込めるため、本データセットを用いた、より早く正解に辿り着くマルチターンCIRの手法の提案が期待されます。 [OS1E-01] What-Where Transformer: 物体の意味と位置情報を分離し並行処理する画像バックボーン 吉橋亮太, 加太将弘(科学大), 池畑諭(NII, デンソーアイティーラボラトリ), 川上玲(科学大), 佐藤育郎(科学大, デンソーアイティーラボラトリ) 物体発見(Object Discovery)や物体検出(Object Detection)、領域分割(Segmentation)タスクでは、画像中の「どこに何が写っているか」を扱います。そのため、「何が写っているか」を判断する画像分類よりもモデル構成が複雑になります。本研究では、その原因の1つとして、従来のViTが画像中の「何」に関する意味情報を中心に表現し、「どこ」に関する位置情報を内部に埋没させている点に着目しています。そして、物体の意味と位置を明示的に分離して扱う「What-Where Transformer (WWT)」を提案します。 WWTでは、画像パッチ由来のトークン列とは別に、想定する物体の最大数だけ「スロット」と呼ぶベクトルを用意します。そして、物体の意味を表す「what」をスロット、位置を表す「where」をスロットごとの注意マップ(ソフトマスク)として分離し、両者をペアで並行して処理します。一般的なViTでは注意マップはトークン更新の途中で使われるだけですが、WWTでは位置情報そのものをネットワーク内で保持し、最終出力として利用できるようにしています。この設計により、WWTは意味情報と位置情報の両方をバックボーンから直接取り出せます。そのため、簡単な後処理や比較的シンプルなヘッドを用いることで、画像分類だけでなく物体発見、物体検出、領域分割にも利用できます。特に物体発見では、学習済みのソフトマスクを閾値処理するだけで物体領域を取り出せるため、タスクごとに大規模なデコーダを用意する必要がありません。 実験では、ImageNet-1kの分類精度を従来のViTとおおむね同程度に維持(Smallではやや劣る性能)しながら、注意マップの説明性や領域分割の性能を向上させることがわかりました。ImageNet-Sの弱教師あり領域分割では、DeiT-Tinyの8.8%、Attention Rolloutを用いた場合の35.9%に対し、WWT-TinyはmIoU 49.1%を記録しています。また、ゼロショット物体発見でも、VOCでCorLoc 41.4%、COCOで30.2%を達成し、教師あり学習されたDeiTを一部上回っています。 本研究は、画像から得られる特徴を単一のEmbeddingとして表現するだけでなく、「何」と「どこ」を別々の表現として保持することで、1つのバックボーンをさまざまな視覚タスクに転用しやすくする研究です。シンプルな発想ながら、画像分類、物体発見、物体検出、領域分割といった複数のタスクで有効性を示しています。特に、分類用に学習したモデルから追加学習なしで物体位置を取り出せる点は興味深いです。商品画像に含まれる複数アイテムの発見や、説明可能性を持った画像認識モデルなどへの応用可能性を感じました。 まとめ 本記事では、MIRU2026の参加レポートをお伝えしました。今年もMIRUに参加することで、業界の最先端を俯瞰し、多くの新たな知見を得ることができました。また、発表を通じてフィードバックを得たり、スポンサーブースにて弊社の取り組みをご紹介できたりしたことは大変貴重な機会でした。変化の激しい業界ですので、今回得られた最新の知見を今後の研究開発へ積極的に取り入れ、さらなる成果を上げるために、そして業界の発展に貢献できるよう、今後も邁進していきます。 最後に ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com zozonext.com hrmos.co
.table-of-contents ul ul { display: none; } はじめに こんにちは。検索研究ブロックの諸田とコーディネートサイエンスブロックの清水です。私たちは、2026年7月20日から7月24日までオーストラリア・メルボルンで開催されたSIGIR 2026(Special Interest Group on Information Retrieval)に現地参加してきました。本記事では、ZOZOから発表した論文内容を紹介し、あわせて基調講演やワークショップ、各セッションにおいて特に興味深かったトピックをいくつか取り上げてご紹介します。 SIGIR 2026 セッション会場の様子 はじめに SIGIR 2026とは 主な研究動向 開催地と会場 メルボルンについて 会場施設 ZOZOによる発表 背景・課題 提案したVLMベンチマーク 実験結果・考察 ポスターセッションの様子 基調講演 初日: Emancipatory Information Retrieval 2日目: Question Answering Fit-for-Purpose 3日目: From Search to Ask to Act セッションレポート (SIGIR 2026 Best Paper Award)Why Advanced Encoders Lag on Sparse Retrieval? 感想 Building a Production Shopping Agent at Scale (Amazon Rufus) 感想 Pekka: MLLM-based Product Representation Learning with User Engagement Signals for E-Commerce 感想 Dual-Diffusional Generative Fashion Recommendation 感想 MLLMRec: A Preference Reasoning Paradigm with Graph Refinement for Multimodal Recommendation 感想 まとめ そのほか現地の様子 SIGIR 2026とは SIGIR(Special Interest Group on Information Retrieval) はACM(米国計算機学会)の情報検索特別研究グループが主催する、情報検索分野において最も権威ある国際会議です。1963年の開始以来、検索およびその他の情報アクセス技術の分野における研究、開発、教育の発展を牽引してきました。 第49回となる今回は2026年7月20日〜24日の5日間、オーストラリアのメルボルンで開催されました。会場はメルボルン空港からバスに乗って1時間程度のところにあります。 本会議は、論文発表、ポスターセッション、チュートリアル、ワークショップなど多様なプログラムで構成され、世界中から多数の研究者や開発者が参加しました。 sigir2026.org 主な研究動向 今回は234件のFull Papers、151件のShort Papersをはじめ、Industrial PapersやDemo Papersなど合計656件の研究成果が発表されました。 特に、RAGやAgentのトピックについて話題が多くなってきた印象があり、これらを扱うチュートリアルやワークショップが多数開催されました。Keynoteやパネルディスカッションでは、Agentなどのシステムが複雑なIRタスクを解けるようになったことで、従来の関連性の指標を測るだけでなく、どのように検索タスク全体の「良さ」を定義して測っていくかについて注目されているという印象でした。 sigir2026.org SIGIR 2026のページに「Data Explorer」というページがあり、論文の採択数やトピックの分布などの数字がグラフで見られるようになっていました。「THE TOPIC LANDSCAPE」の情報によると「LLMs」のkeywordが一番多く、「RAG」や「Reasoning」も上位になっていることがわかります。 トピックごとの論文数 - SIGIR 2026 — The Conference by the Numbers より引用 開催地と会場 メルボルンについて 開催地のメルボルンは、伝統的には「ナーム」と呼ばれる場所で、オーストラリア国内ではシドニーに次ぐ第二の都市です。ヴィクトリア朝時代の建築物がロンドンに次いで多く残っている都市であり、歴史的な雰囲気を感じる街並みでした。基本的に7月は南半球なので冬に当たりますが、1日の寒暖差が激しく、昼は17度で夜は6度といったような温度になります。ただ、日本の冬と比べるとそこまで寒くないなと思いました。市内には路面電車(トラム)が走っていて、無料で乗れるエリアが広いのでとても便利です。物価は日本と比べて3〜4倍高いように感じました。シンプルなハンバーガーのセットが3000円くらいですし、ペットボトルの水が1本500円くらいしてびっくりしました。 会場施設 今回の会場はMelbourne Convention Exhibition Centre(MCEC)でした。サザンクロス駅から徒歩10分ほどの場所にあり、とても行きやすい場所にあります。 会場の入り口 全体のプログラムは、1日目がTutorial、5日目がWorkshopでした。2〜4日目は、「Reranking」や「RAG System」などのトピックごとに6件ほどのFull Paperを発表するSessionと、120件ほどの論文が並ぶPoster Sessionが中心でした。 ランチタイムの様子 ZOZOによる発表 今回、私たちのチームは以下の論文を発表しました。 Preliminary Study of an Evaluation Benchmark for Vision–Language Models in Fashion E-Commerce Ryotaro Shimizu*, Sai HtaungKham*, Shion Sakurai, Yuki Shimizu(*Equal contribution) 背景・課題 ファッションECでは、検索フィルターやレコメンデーション、商品掲載の品質管理のために自動属性抽出が重要な役割を担っています。近年、Vision-Language Model(VLM)がこうした構造化抽出タスクの有力な候補として注目されています。しかし、MMMUやMMBenchといった一般的なVLMベンチマークは、ファッション固有の属性やEC特有の構造化抽出タスクを十分にカバーできていません。また、VLMの出力はプロンプトの書き方に敏感であることが知られています。一方で、「オーバーサイズ」「くすんだ青」のようなファッション属性はそもそも主観的です。このあいまいさとプロンプト感度がどう影響し合うかは、これまで検証されていませんでした。 提案したVLMベンチマーク 本研究では、コーディネート画像(WEARのユーザー投稿画像)とアイテム単体画像(ZOZOTOWNの商品画像)という、2つの画像ストリームにまたがる5つのタスクを定義しました。その上で、社内の業務データとドメインエキスパートによる正解ラベルを用いたベンチマークを構築しました。 Task1:タグ抽出(コーディネート画像・33ラベルからの複数選択) Task2:カラーマッピング(コーディネート画像・35色からの複数選択) Task3:画質判定(アイテム画像・4カテゴリ) Task4:季節推定(アイテム画像・春夏/秋冬/オールシーズンの3クラス) Task5:素材推定(アイテム画像・10カテゴリ) さらに、単一プロンプトによる評価の限界を踏まえ、マルチプロンプト評価フレームワークを導入しました。これは「Canonical Prompt(人手で設計した基準プロンプト)」と「各モデル自身が自己最適化のために提案したプロンプト」の両方を、すべてのモデルに適用するものです。これにより、次の4つの指標を算出できます。 Canonical Score(C) Model-Proposed Score(MP) Robust Score(R、全プロンプト平均) Sensitivity(S、全プロンプトの標準偏差) 評価対象は商用API 6種(GPT-5.2 / GPT-5.1 / GPT-5-mini / Gemini-3-pro-preview / Gemini-2.5-pro / Gemini-2.5-flash-lite)とOSSモデル2種(Qwen2.5-VL 3B / LLaVA-OneVision 8B)の計8モデルです。 実験結果・考察 主な知見は次の5点です。 画像ストリームによってモデルの序列が入れ替わる :コーディネート画像のタスク(Task1・Task2)ではgemini-3-pro-previewが最も高い性能を示しました。一方、アイテム画像のタスクではTask3でgpt-5-mini、Task4でgpt-5.1、Task5でgemini-2.5-proと、タスクごとに最良モデルが異なりました。軽量モデルが特定タスクで健闘するケースもあり、コスト最適なモデル選定にはタスク単位の評価が欠かせないと分かりました。 タスク難易度の差が大きい :Task2(カラーマッピング)は商用モデルすべてが0.94以上と実運用レベルに達しています。一方、Task5(素材推定)は最高でも0.279にとどまり、両者にはおよそ3.4倍の差がありました。素材はテクスチャという触覚的な情報に依存するため、画像だけからの推定は専門家でも難しいことが背景にあると考えられます。 プロンプト感度はタスク・モデルごとに異なる :多くの商用モデルはSensitivity(S)が0.02〜0.07程度に収まります。一方、gpt-5.1はTask2でCanonical Score 0.94〜0.95という高精度にもかかわらずS=0.288と大きくばらつきました。高精度に見えるモデルでも、プロンプトを変えるだけで性能が大きく崩れうるということです。 モデル更新は全タスクを一様に改善しない :Gemini 2.5-flash-liteから3-pro-previewへの更新で、Task3は+13.9pt、Task1は+7.3pt改善しました。一方でTask4は−15.7ptと悪化しています。モデルのバージョンアップ時にタスク単位で再評価しないと、本番環境での性能劣化に気づけない可能性があります。 エラー傾向はモデルファミリーごとに構造的に一貫している :t-SNEによる誤答パターンの可視化では、同一モデルはプロンプトが変わってもまとまって分布しました。GPT・Gemini・OSSの各ファミリーも、明確に分かれた領域を占めています。属性単位でモデルを使い分ける(ルーティングする)余地があることも示唆されました。 これらの結果から、VLMを本番のファッションECシステムに導入する際には、タスク単位の評価・プロンプト頑健性のチェック・継続的なモニタリングが実務上必要である、という結論に至りました。なお、本研究にはデータ規模や評価指標の面で限界もあります。今後はより大規模で多様なデータセットの構築や、公開可能なベンチマークの整備、LLM-as-a-Judgeなどの代替となる評価手法、継続的モニタリングパイプラインとの統合を検討していく予定です。 本研究の詳細については、ぜひ元論文をご覧ください。 Preliminary Study of an Evaluation Benchmark for Vision–Language Models in Fashion E-Commerce ポスターセッションの様子 ポスターセッションは7月23日の15:00〜16:15に開催されました。学生の方やIndustry Trackに出展している企業の方を中心に、多くの参加者が足を止めてくださり、盛況でした。「なぜこのベンチマークを整備しているのか」「今後はファインチューニング用のデータセットとして展開していく予定なのか」といった、今後の展開まで見据えた質問を多くいただいた点が印象に残っています。オープンなデータセットでは対応しきれない、ファッションEC特有の構造化抽出タスクに焦点を当てて整備している点に興味を持っていただくことが多くありました。「自分たちの業界でも同じような取り組みをしてみたい」といったポジティブな反応もいただけました。 ポスターセッションの集合写真 以降は基調講演や各セッション、ワークショップで特に興味深かったものをご紹介します。 基調講演 基調講演は、メインカンファレンス期間中の各日の冒頭で実施されました。 初日: Emancipatory Information Retrieval 初日は、元Microsoft社員で、現在は研究者のBhaskar Mitra氏による基調講演「Emancipatory Information Retrieval / Towards Critical IR Theories and Practices」でした。 「どうしたら検索システムが社会的善(societal good)の力になれるのか?」というのが講演全体を通しての問いでした。現在の検索システムが元々期待されていたような「認知を支援する中立的な技術」ではなく、「政府による情報統制や企業によるユーザー監視が行われる仕組み」になっているという問題を指摘しています。批判的IRへ転換するために、6つの提言(技術の社会的構築/構造分析/非支配/知識の社会的構築/技術者=解放者フレームの解体/プラクシス)と具体的な研究アジェンダまで提示されていました。 自身が19年間ビッグテックにいたという経験があったからこそかもしれませんが、自分が元いた場所を含むビッグテックへの問題点を指摘して改善するための行動を起こしているというのが印象深かったです。検索システムを改善する身としては考えさせられる内容でした。 2日目: Question Answering Fit-for-Purpose 2日目は、CSIRO(オーストラリア連邦科学産業研究機構)のCécile Paris氏による基調講演「Question Answering Fit-for-Purpose: A Perspective from Computational Linguistics and User Modelling」でした。 質問応答システムが本当に「目的にかなっている」かどうかは、利用者・タスク・文脈まで含めて評価しなければ分からない、というのが講演全体を貫く主張です。同じ「マイクを説明して」という質問でも初心者と専門家では適切な答えが異なる、という初期の対話システムの例を挙げながら、質問応答は常に相手の知識や文脈に依存すると論じられました。その上で、LLM時代においても計算言語学やユーザーモデリングの知見に立ち返ることの重要性が訴えられました。 評価に関する議論が個人的には興味深かったです。共有ベンチマークだけでなく実際の文脈の中で評価する必要があること、ベンチマークと実世界のシナリオにはギャップがあること、そして失敗が現実世界で悪影響を及ぼしうることが強調されていました。 3日目: From Search to Ask to Act 3日目は、中国人民大学 高瓴人工知能学院 教授・創設学院長のJi-Rong Wen氏による基調講演「From Search to Ask to Act: The Evolution of Information Access in the Age of Large Models and Agents」でした。 情報アクセスの進化を「Search(検索)→ Ask(質問)→ Act(行動)」の3段階として捉えた講演です。BM25や埋め込みによるランキング改善は「マッチング」の精度を上げるにとどまり、「理解」や「タスク完遂」には届かなかったという検索の歴史をまず振り返りました。その上で、RAGによって生成モデルと知識を再接続する流れが紹介されました。さらに、Plan→Search→Read→Reflect→Writeのループを自律的に回すDeep Search Agentへと発展してきた経緯が、ご自身の研究チームの成果を通じて語られました。Search-o1やWebThinker、DeepAgentといった一連の研究がその例です。最後は「次の検索エンジンはリサーチエージェントである」という言葉で締めくくられ、検索はリンクのリストを返すものから、完了した情報タスクを届ける自律的なプロセスへと変わりつつあると論じられました。 個人的に興味深かったのは、ハーネス(足場)の設計を工夫するだけでReActベースラインの正答率を54%から74%まで改善したSearchClawの結果です。モデルそのものの強さ以上に「エージェントをどう組み立てるか」が性能を大きく左右する、という点が印象に残りました。 セッションレポート 論文発表セッションやワークショップで聴講したトピックをいくつかピックアップします。 (SIGIR 2026 Best Paper Award)Why Advanced Encoders Lag on Sparse Retrieval? Zhichao Geng, Yang Yang arxiv.org こちらの論文はSIGIR 2026でBest Paper Awardを獲得した論文です。 密検索(dense retrieval)で性能が良い基盤モデルが、学習型スパース検索(learned sparse retrieval,LSR)ではほとんど性能が向上しないという異常な現象の根本原因が語彙ギャップ(vocabulary gap)であると特定した論文です。 ここで「語彙ギャップ」と呼んでいるものは、バックボーンのトークナイザの語彙がLSRと相性が良くない状態になっているというものです。ModernBERT / RoBERTa などの現代トークナイザは可逆的復元ができるように「小文字化しない」「アクセント除去などの正規化を持たない」、などの特徴があります。ただ、LSRだと「House」と「house」のようなほぼ意味が同じ2つの単語が内積0になるため、同じ意味だと認識できなくなります。モデル学習によって大量のデータで学習すればある程度は対応できるようになりますが、そのようなパターンをさまざまな語彙で認識する必要があるので、モデルの容量に対して効果のある語彙数が減ってくるという問題が起きます。密検索の場合は1つのトークンをembeddingで表現できるので、2つの単語を空間上の近いところに配置するということがやりやすいです。 他にも、スパース検索に適した語彙を用いてモデルをスクラッチから学習させるという方法も考えられますが、モデルの大きさや必要になるデータセットの大きさを考慮するとコストや計算量の観点から作成は難しくなります。 手法としては語彙転移(Vocabulary Transfer, VT)が提案されています。これは元のModernBERT 学習トークンの0.2% 未満を用いて、わずか 500 MLM ステップで最適に近い性能を達成するという効率的で効果的な方法です。ステップとしてはまずは正規化された小文字化済み語彙の bert-base-uncased をアンカーとして選びます。ModernBERTとの重複していない新規トークンに対しては重複しているアンカーのなかで近い語彙をいくつか選んで類似度を元に混ぜたものを埋め込みとし、ModernBERTの語彙の分布を元に分布を調整します。最後にTransformer 層を凍結し、埋め込み層のみを短いMLMで更新することで適合させます。 ベースラインとしてはBM25やColBERTv2、ModernBERT(dense)など確立された密・スパースなニューラル検索器と比較していました。 利用したデータセットはBEIRとMS MARCOです。評価指標は平均nDCG@10を利用していました。主な成果としては、本手法を使ったModernBERT-VTが5教師のアンサンブル学習パイプラインという複雑な仕組みを利用しているSPLADE-v3のスコアの52.0を上回る52.4というスコアを出したことです。また、バックボーンがRoBERTa-largeやbert-base-casedの際に、密検索と比べてSPLADEの精度が大幅に悪化していた問題についても、VTを使うことで精度が回復していることを確認しました。 他にも単純にMLMを使ったケースやlowercaseへの前処理をしただけのケースなどの手法と比べても、明らかに精度が良くなっていることを確認しています。 感想 SPLADEの課題点であったバックボーンを変えたときに性能が改善しない、むしろ悪化するということに対して問題の切り分けの仕方や、比較的軽量な学習で改善できるという解決策が実務で導入しやすいと思いました。もし業務としてSPLADEを使っていて、基盤モデルの進化に追従していきたいという場合には十分に使える技術だと思います。着眼点の良さや理論的な分析、実際に適用できる範囲の広さ、精度の高さと手軽さが揃った論文になっていて、Best Paperとしての完成度の高さを感じました。 Building a Production Shopping Agent at Scale (Amazon Rufus) Chen Luo, Jason Choi, Ziwei Dong, Rahul Dua, Cong Xu, Xuejing Lei, Yuchen Yan, Xin Zhang, Josef Valvoda, Gaurang Sinkar, Binit Jha, Yi Liu, Monica Cheng https://dl.acm.org/doi/10.1145/3805712.3808408 dl.acm.org この発表はAmazonの会話型ショッピングエージェントRufusを本番規模で1年間運用した経験の共有です。 Rufusシステムのエージェントアーキテクチャ、エージェントを実世界のトラフィックにスケールさせるための技術、ショッピングエージェントの評価方法、という3つの中核的な貢献について報告しています。 RufusシステムはReActスタイルのエージェント的フレームワークに従うエージェントで、中のLLMが「推論(reasoning)」と「ツール呼び出し(tool invocation)」を交互に行います。構造化されたエージェントコンテキストを構築した上で、制限されたツールからアクションを選択します。ツール呼び出しは反復的に行うものの、実際の本番設定では最大4ホップにしていて、過剰な呼び出しを防いでいるようです。エージェントの振る舞いは宣言的(declarative)なシステムプロンプトによって統制することで、誤ったアクション選択や根拠のない生成など望ましくない振る舞いを経験的に低減しています。 LLMベースのエージェントを大規模にデプロイする際の主要なボトルネックとして「レイテンシ」を取り上げているようです。本研究では意思決定品質を保持しつつ、モデルが目にする実効的な計算作業量を削減することに焦点を当てています。解決策として、以下4つの補完的な技術を統合しているようです。 入力トークン長を最小化するコンテキストサイエンス トークン効率とキャッシュ再利用を向上させるプロンプト最適化 推論ステップ数を削除するツール呼び出し最適化 実行間で繰り返される計算を償却するプロンプトキャッシング 各最適化手法に対して相対的なレイテンシ削減や初トークン到達時間削減を調べたところ、トータルではどちらも45〜60%を削減し、特にコンテキストサイエンスが最大の効果をもたらしていることがわかりました。 本番稼働ショッピングエージェントを大規模に評価するために、本研究では真値(ground truth)の検証のための人間評価と、スケーラブルな自動評価のためのLLMを審判とする評価(LLM-as-a-Judge, LLMaJ)を組み合わせたようです。Rufusでは2つの中核指標を定義していて、1つが推薦品質スコア(Recommendation Quality Score, RQS)で個々の商品評価を1-5のスケールに集約します。もう1つが有用性スコアで、ショッピングアシスタントとしてのエージェントの有効性を5段階のスケールで測定するようです。この2つのスコアを人間評価と自動評価の組み合わせを用いて算出しました。自動評価の自動アノテーター間の一致度は単一クエリだと82~84%、マルチターンだと64~74%でした。人間評価による有用性スコアおよびRQSスコアは、単一クエリ、マルチターンともに84%~88%になっていました。 ケーススタディとして、AmazonのショッピングエージェントはRufusチャットボットをサポートすることに加えて、Search Overview(検索概要)、Accordion(アコーディオン)、RQ Agent(関連質問エージェント)の機能も紹介されていました。 Search Overview(検索概要)は検索結果の上部に説明が載っているもので、商品検索を「検索(retrieval)」から「生成(generation)」へと転換する生成AIのショッピング体験です。単一商品タイプのクエリではRufusのエンゲージメントを16.33%も増加させたようです。 Accordionは検索結果ページ内に直接埋め込まれたインラインのFAQ体験と説明されています。キーワードあたり5つの質問を生成していて、質問ー回答ペアをキャッシュしているようです。Accordionの品質を改善することでRQ有用性や応答品質が95-98%の水準まで達していました。 RQ Agentは顧客がRufusとインタラクションする入り口で、検索結果ページや商品詳細ページで、1-3語のフレーズや30文字以内の簡潔な質問として現れます。RQ AgentはRufusのエージェントアーキテクチャの上に構築されていて、運用の複雑さを減らしているようです。米国でローンチされていて、本番ベースラインに対して一貫した品質向上を実証しています。 感想 Amazonが実際にRufusを運用してきたリアルな実態がわかる良い発表だと思いました。エージェントのアーキテクチャが整理されていることで、Chatbotのような体験だけではなく、そのアーキテクチャの延長で検索概要やAccordion、RQ Agentなどの出面や活用の幅を広げられるというのが興味深かったです。LLMを使ったショッピング体験はかなり多様なものになりそうだと感じました。評価指標としてはLLM as a Judgeを使いつつ、人間の評価は結構重視しているという印象でした。評価部分は機能の精度を向上させるための肝となる部分だと思うので、今後も動向を追っていきたいです。 Pekka: MLLM-based Product Representation Learning with User Engagement Signals for E-Commerce Rui Yan, Kevin Huang, Miao Li, Zhenyuan Liu, Mina Ghashami, Michael Schlichtkrull, Miguel Arduengo, Wenliang Gao et al. https://sigir-ecom.github.io/eCom26Papers/paper_788.pdf sigir-ecom.github.io SIGIRのworkshopであるECOM'26の内容です。 Meta社がPekkaという商品埋め込みフレームワークを提案しました。MLLMの表現学習に対比学習向けの高品質な共クリック商品ペアを使うことで、行動的検索、クロスモーダル検索、テキストのみの検索ランキング、マルチモーダル検索ランキングにまたがる公開ベンチマークで各分野のベースラインを上回る結果を出したというものです。 この技術はECのランキングにおいて有効な商品埋め込みを作成することを目的としています。商品埋め込みを作成する既存の手法では画像ーテキスト整合やクエリー文章関連性のような意味的教師信号で学習されているところを、Pekkaではセッション内の共クリックという行動的教師信号を使うところがユニークなポイントです。 Pekkaは以下の3つの構成要素からなります。 画像とテキストを共同モデル化するMLLMエンコーダ 閲覧ログから高品質な共クリックペアをマイニングするスケーラブルなパイプライン バッチ内負例(in-batch negatives)、オフラインでマイニングされたハード負例(hard negatives)、頻度重み付き教師信号を用いた対比最適 visionエンコーダはSigLIP2のような事前学習済みのモデルを用いて、MLLMバックボーンはQwen3-VLやLlama 3.2-11Bのようなモデルを利用しています。埋め込みは256次元です。 閲覧ログは有用な情報であるとともに、偶発的なクリック、ボット、弱い関連を含んでいるという前提があり、キュレーションの方法として2段階のパイプラインが提案されていました。1段階目は2回以上観測されたクリックのペアで絞り込み、2段階目はCLIPを用いた意味フィルタリングで0.65以上の類似度があるペアに絞り込んでいます。 実験では3つの独立したECプラットフォームからの4つの公開データセットを用いました。H&Mがテキスト+画像の行動的タスク、M5Productがテキスト+画像のクロスモーダル検索タスク、ESCIがテキストの検索ランキングタスク、SQIDがテキスト+画像の検索ランキングタスクとなっています。評価指標としては関連性が二値であるH&MとM5ProductについてはRecall@k(R@k)を使っていて、関連性が段階的であるESCIとSQIDについてはR@kに加えてnDCG@10が使われています。baselineとして使われているモデルはVision-OnlyのDINOv3 ViT-L/16、デュアルエンコーダ型のVision-Languageを扱えるCLIP ViT-L/14とSigLIP2-SO400M、MLLMベースのE5-VとQwen3-VL-2Bでした。 どのモデルもベンチマークのデータを学習せずに評価を行ったところ、Pekkaは他のベースラインの埋め込みよりも小さい256次元埋め込みを用いているにもかかわらず、総合的に最も強い性能を示しました。これによって、Pekkaの共クリックデータの学習がドメイン最適化ではなくクロスドメイン転移できるものであることがわかります。 商品埋め込みに対しての品質評価を行ったところ、属性の分類精度で、スタイル、色、カテゴリのどのラベルに関してもPekkaが最も良い結果となりました。 また、Meta社内の同一のECデータを使って学習をした上での評価を行ったところ、ResNeXt-101・Text Transformer・CLIP(fine-tuned)のモデルと比較してもPekkaが最良の結果を出しています。CLIPベースラインに対するPekkaのカテゴリごとのRecall@10の相対的改善を調べたところ、最大の改善があったカテゴリはHousing(+60%)で、次がClothing&Accessories(+37%)でした。 Pekkaは実際に本番環境へデプロイされているようで、社内のオフライン評価で第2段CTRの正規化交差エントロピーで0.10%、CVRで0.15%、第1段CTRで0.10%の相対的改善が得られているようです。 感想 画像とテキストのマルチモーダルなモデルは意味理解の方に重点が置かれていることが多く、ECの文脈でそのまま使うのは難しいなと思うことが多かったので、共クリックに着目したのは興味深いなと思いました。意味理解を損なわずに購入に対しても良い結果を出しているようだったので、活用できる可能性は高そうです。埋め込みの次元数が256という小さいサイズかつ性能が良いというのも実戦投入しやすい要素だなと思いました。ベンチマークとしてH&Mが購入をラベルとしたECのデータセットを使っていましたが、セッション内のランキングの情報はないので、ランキングタスクにするとどれくらい良いのかは気になります。また、オンラインでのA/Bテストの結果はない、オフライン評価での改善が0.1%ほど、そもそもどんなサービスで検証したのかがわからないという部分を考慮すると、ECの場面で取り入れたときにどれくらいの改善が見込めるかは未知数な部分も多いと感じました。 Dual-Diffusional Generative Fashion Recommendation Mingzhe Yu, Lei Wu, Qianru Sun, Yunshan Ma arxiv.org 既存の画像中心の生成型ファッション推薦は、履歴インタラクションから得られる暗黙的な視覚埋め込みのみに依存しているため、ユーザーの真の嗜好を十分にモデル化できていませんでした。また、生成結果が画像のみであるため、なぜそのアイテムが推薦されたのかを説明する手段がなく、説明可能性が欠如しているという課題もありました。 本研究が提案するDualFashionは、Dual-Diffusion Transformerをベースに画像ブランチとテキストブランチを並列に持つアーキテクチャです。条件付け信号には、ユーザー選好、コーディネート文脈とのマッチング条件、タスク定義の3種類を用います。画像ブランチでは連続拡散によりアイテム画像を、テキストブランチでは離散拡散により色・素材・デザイン・スタイルといった属性トークンを予測しながら、画像と説明テキストを同時に生成します。学習はウォームアップ、マッチング考慮のマルチモーダル学習、テキスト拡張ファインチューニングの3段階で行われます。特に3段階目では、LLMで1コーデにつき5種類の多様なキャプションを生成し、テキスト損失のみでファインチューニングします。これにより、画像レベルの強化学習に比べて計算コストを大きく抑えながら、生成の多様性を高めています。 評価はiFashionとPolyvore-Uの2つのデータセットで行われています。タスクはPersonalized Fill-in-the-BlankとGenerative Outfit Recommendationの2つです。iFashionのPFITBタスクでは、DiFashionと比べてIS(Inception Score)を3.7%改善しました。パーソナライゼーションスコアでも65.17とFashionDPO(60.39)を上回っています。アブレーションでは、テキスト拡張ファインチューニングの導入により、多様性が0.64から4.25へ大きく改善することも確認されています。これらの結果は、効率性と解釈可能性を両立できる手法であることを示しています。 感想 気になったのは、画像の構造化情報(属性レベルの構造化キャプション)を、生成モデルの条件付け信号として明示的に活用している点です。私たちがSIGIR 2026で発表したベンチマークでは、コーディネート画像から抽出したタグやカラーといった構造化属性を、主に検索フィルターやレコメンドの前段の特徴量として使うことを想定していました。しかしDualFashionの結果は、同じ構造化属性が拡散モデルの条件付けとして機能し、生成の互換性やパーソナライゼーションの向上に直接寄与することを示しています。つまり画像の構造化は、検索・レコメンドだけでなく、生成モデルそのものの安定化にも拡張できる可能性があるということです。WEARのコーディネート画像から抽出している構造化属性も、将来的なコーデ提案や着せ替え生成といった生成系機能の条件付けに転用できるかもしれない、という気づきがありました。 MLLMRec: A Preference Reasoning Paradigm with Graph Refinement for Multimodal Recommendation Yuzhuo Dang, Xin Zhang, Zhiqiang Pan, Yuxiao Duan, Wanyu Chen, Fei Cai, Honghui Chen arxiv.org マルチモーダル推薦は、ユーザーの行動履歴とアイテムのモーダル特徴(画像・テキストなど)を組み合わせることで、IDベースの推薦手法より優れた性能を示しています。しかし既存手法には2つの課題がありました。第一に、ユーザー表現の初期化が過去の行動を無視するか、無関係なモーダルノイズの影響を受けてしまうことです。第二に、広く使われているKNNベースのアイテム間グラフは、類似度の低いノイジーなエッジを含む一方で、同じユーザーに購入される組み合わせといった共起関係を捉えきれていないことです。 本研究が提案するMLLMRecは、まずマルチモーダル大規模言語モデル(MLLM)を用いてアイテム画像を高品質なテキスト記述に変換し、視覚とテキストのモダリティギャップを埋めます。続いて、ユーザーごとの行動履歴に紐づくアイテムの説明を時系列順で並べた「行動記述リスト」を構築します。これを再びMLLMに入力し、ユーザーの潜在的な相互作用の意図(なぜそのアイテムを選んだか)を推論させます。その結果として、モーダルノイズを除去した高品質なユーザー選好プロファイルが得られます。さらにアイテム間グラフも、低類似度のノイジーなエッジを刈り込む「閾値制御デノイジング戦略」と、ユーザー集合のJaccard類似度から共起関係を補完する「トポロジー認識強化戦略」の2つで洗練します。その上で、LightGCNベースのグラフ畳み込みにより高次のアイテム表現を学習します。 Amazon製品データセットのBaby・Sports・Clothingの3種類を用い、Recall@{10,20}とnDCG@{10,20}で既存手法と比較しています。実験の結果、MLLMRecは3データセットすべてで最も強いベースラインに対して平均21.48%の性能改善を達成しました。提案する2つのグラフ洗練戦略やMLLMによる選好推論が、既存手法の弱点を解消する上で有効であることも確認されています。 感想 気になったポイントは、MLLMによる「画像から言語的な意味記述への変換」を推薦の意図推論にまで応用している汎用性の高さです。本研究では画像をテキスト化するだけでなく、ユーザーの行動列全体をMLLMに読ませて「なぜこのアイテム群を選んだか」という潜在的な意図まで言語的に抽出し、それを推薦の入力表現として使っています。私たちがSIGIR 2026で発表したベンチマークも、画像からタグや色などの属性を言語的に構造化抽出する取り組みです。一方で本研究は、そうした言語的抽出が単体の属性ラベリングにとどまらず、ユーザー単位の嗜好推論やグラフ構造の補完にまで広く応用できることを示しています。LLMによる言語的な抽出は、検索・レコメンドの前処理としての用途だけでなく、より上流の「意図推論」や「関係性の補完」といった応用にも展開できる可能性がある、という気づきがありました。 まとめ 今回のSIGIR 2026では、生成AIが検索分野で利用されることがより当たり前になっていく未来を感じました。理論的な研究やEC企業の実用例をみて、LLMを使った手法でまだ確立されたものがあまりない中、それぞれの工夫で試行錯誤しているなと思いました。今回学んだ技術を実際に試したり、ドメインに合わせて改善したりなどで今後のプロダクトに活かしていきたいです。 次回のSIGIR2027は50周年記念大会で開催地はシリコンバレー そのほか現地の様子 夕方のメルボルンセントラル駅 街並みと路面電車(トラム) 会場周辺の風景 ウェルカムレセプションにいたコアラ 授賞式パーティーの様子 ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
なぜC++/JS間のメモリ管理は難しいのか 皆様、はじめまして。グローバルプロダクト開発本部 グローバルシステム部 技術推進ブロックの村木と申します。私たちのチームでは、OpenCVによる画像処理やニューラルネットワークによる画像認識を含む大規模なC++資産を、EmscriptenでWebAssemblyにコンパイルし、ブラウザへ移植するプロジェクトに取り組んでいます。 その中で私にとって難所となったのが、メモリ管理の理解でした。Emscriptenを使用する開発では、メモリの考え方が異なる2つの言語、C++とJavaScriptをまたいだ処理を実装することがあります。C++ではオブジェクトの寿命がコンストラクタとデストラクタによるRAIIで制御されるのに対し、JavaScriptではオブジェクトの寿命をGC(ガベージコレクタ)が管理し、通常、開発者が解放を意識することはありません。 このメモリモデルの違いに加え、C++のオブジェクトをJavaScript側へ渡すとき、その寿命や所有権に関する情報は引き継がれないことを、しっかりと理解しておく必要があります。 JavaScript側から見ると、C++のオブジェクトは単なる数値(メモリ上のアドレス)に過ぎず、この「数値」には、「誰が確保したのか」「いつまで有効なのか」「誰が解放すべきか」といった所有権の情報は含まれていません。また、JSのGCはC++側のメモリ領域を認識できません。 そのため、Wasmの境界を越えるオブジェクトについては、所有者と有効期間、解放の責任を明確にしたうえで実装する必要があります。この点を理解しておくことが、Emscriptenにおける開発の重要なポイントでした。 Emscriptenが提供するembindを使えば、C++のクラスや std::vector をJavaScriptのオブジェクトに近い形で扱えます。ただし、その抽象化されたAPIの背後で、オブジェクトの受け渡しと寿命管理がどのように行われているのかを理解しておく必要があります。 本記事では、まずWasmのメモリモデルの基礎を概観し、その上でembindによる実装を詳しく解説します。 目次 なぜC++/JS間のメモリ管理は難しいのか 目次 Wasmのメモリモデル C++オブジェクトの実体 境界を越えると寿命の情報が失われる embindは何を隠し、何を隠さないのか value_objectはコピー class_はハンドル typed_memory_viewはビュー おわりに Wasmのメモリモデル WebAssembly(Wasm)のメモリモデルは非常にシンプルです。Wasmモジュールは、 Linear Memory(線形メモリ) と呼ばれる、単一の連続したバイト列を自身のメモリ空間として持ちます。この線形メモリは、JavaScript側からは WebAssembly.Memory オブジェクトの buffer として見えます(型は ArrayBuffer )。 C++コードがWasmにコンパイルされると、グローバル変数やスタック領域、そして malloc / new によって確保されるヒープ領域は、すべてこの線形メモリの中に配置されます。したがって、C++オブジェクトの「実体」とは、この線形メモリ上の特定のアドレスから始まる領域にほかなりません。JavaScriptの視点で言い換えれば、C++のオブジェクトは、巨大な ArrayBuffer (= WebAssembly.Memory が内部に保持する一本の配列)の中に、バイト列として格納されているのです。 まずはこのことを、簡単なコードで確認していきます。 C++側で「指定されたアドレスから始まるバイト列の合計値を計算する」関数を実装します。 // C++側:ptrは線形メモリ内のオフセット(アドレス) extern "C" int sum_bytes ( const uint8_t * ptr, int len) { int total = 0 ; for ( int i = 0 ; i < len; i++) total += ptr[i]; return total; } JavaScript側では、Emscriptenが提供する _malloc で線形メモリ上の領域を確保し、 HEAPU8 を通じて値を書き込みます。このとき、関数に渡す ptr は単なる数値(アドレス)です。 const bytes = new Uint8Array ([ 10 , 20 , 30 ]) ; // 1. 線形メモリ上に3バイト確保 const ptr = Module . _malloc ( bytes . length ) ; // 2. JSから線形メモリへ書き込む Module . HEAPU8 . set ( bytes , ptr ) ; // 3. C++側で同じメモリ領域を読み取って合計を計算 const total = Module . _sum_bytes ( ptr , bytes . length ) ; // 4. メモリを解放 Module . _free ( ptr ) ; console . log ( total ) ; // 60 ptr はC++から見れば「ポインタ」ですが、JSから見れば HEAPU8 の「添字(number)」に過ぎません。 HEAPU8[ptr] と記述することで、C++側がポインタを通じてアクセスするのと、全く同じメモリ領域をJS側でも参照できます。 C++オブジェクトの実体 EmscriptenでコンパイルされたC++コードにおいて、 new や std::vector などによる動的なメモリ確保は、すべてWasmの線形メモリ内(のヒープ領域)に対して行われます。JS側から呼び出す _malloc も、同じヒープ領域からメモリを切り出します。 C++オブジェクトの「実体」とは、linear memory上の特定のアドレスから始まる、数バイトから数メガバイトの領域のことです。例えば、以下のクラスを考えます。 class ByteBuffer { public : explicit ByteBuffer ( size_t n) : data_ (n, 0 ) {} size_t size () const { return data_. size (); } private : std :: vector < uint8_t > data_; // 中身はヒープ上の別領域を指す }; new ByteBuffer(1024) を実行すると、まず ByteBuffer 本体(vectorのポインタやサイズ情報など)がヒープに確保され、その内部のvectorがさらに1024バイトをヒープに確保します。これらはいずれもlinear memory内に存在します。 C++の中で完結している限り、この寿命管理は言語仕様によって保証されています。スコープを抜ければスタック上のオブジェクトは破棄され、 std::vector が確保したメモリも自動的に解放されます。 delete を呼び出せばデストラクタが連鎖し、メモリは安全に回収されます。つまり、 RAII(Resource Acquisition Is Initialization)とデストラクタの仕組みが、メモリの確保と解放の対応を担保している のです。 しかし、この保証が有効なのは「C++のライフタイム管理が届く範囲にある」間だけです。オブジェクトへの参照がJS側に渡ると、この前提は崩れます。 境界を越えると寿命の情報が失われる JavaScriptでは、どこからも参照されなくなったオブジェクトはGC(ガベージコレクション)によって自動的に回収されます。しかし、GCが寿命を管理できるのは「JSヒープ上にあるJSの値」だけです。C++オブジェクトの実体は、前述のとおり線形メモリ(巨大な ArrayBuffer )の一領域にすぎず、GCから見れば「生存している1つの大きな箱」の内側でしかありません。その箱の内部のどこが有効で、どこが解放済みなのかを、GCは関知しないのです。 そのため、C++オブジェクトのアドレスを保持していたJS変数が使われなくなり、GCに回収されたとしても、線形メモリ上の実体は確保されたまま残り続けます。C++のデストラクタは呼ばれず、メモリはリークします。 逆のケースも同様で、C++側で free を呼んでメモリを解放しても、JS変数が握っているアドレス(ただの数値)はそのまま残ります。すでに無効になった領域を指し続けるその数値を使えば、解放済みメモリを読み書きする「Use-after-free」を引き起こします。 いずれもC++/JS間の境界を越えた際に、「このアドレスはByteBufferを指し、所有権はこちらにある」という型と所有権のメタ情報が単なる数値に変換されて失われます。その結果、「誰がいつ解放するのか」という所有権の所在が型システムからもランタイムからも見えなくなり、問題が発生します。 embindは何を隠し、何を隠さないのか これまで、WebAssemblyとJavaScript間でデータをやり取りするために、以下の低レイヤーな手順が必要であることを確認しました。 _malloc でメモリを確保する HEAPU8.set 等を介して手動でデータをコピーする 確保したメモリのアドレスを数値として管理する embindは、こうした低レイヤーの「配管」処理を宣言的な記述に置き換え、JavaScriptとC++の間のバインディング実装を省力化可能な、Emscriptenの強力な機能です。 embindを有効にするには、ビルド時に -lembind オプションを付与します。利用にあたっては、C++側で EMSCRIPTEN_BINDINGS ブロックを記述し、公開したい関数やクラスを明示します。 #include <emscripten/bind.h> using namespace emscripten; // 構造体:単純な値のセット struct Point { float x; float y; }; Point midpoint ( const Point& a, const Point& b) { return Point{(a.x + b.x) / 2 , (a.y + b.y) / 2 }; } // クラス:状態を持つオブジェクト class ByteBuffer { public : explicit ByteBuffer ( size_t n) : data_ (n, 0 ) {} size_t size () const { return data_. size (); } void fill ( uint8_t v) { std :: fill (data_. begin (), data_. end (), v); } private : std :: vector < uint8_t > data_; }; EMSCRIPTEN_BINDINGS (demo) { // 構造体をJSのプレーンなオブジェクトとしてマッピング value_object<Point>( "Point" ) . field ( "x" , &Point::x) . field ( "y" , &Point::y); function ( "midpoint" , &midpoint); // クラスをJSのクラスとしてマッピング class_<ByteBuffer>( "ByteBuffer" ) .constructor< size_t >() . function ( "size" , &ByteBuffer::size) . function ( "fill" , &ByteBuffer::fill); // 型登録(JSから操作可能にする) register_vector< float >( "VectorFloat" ); } この定義により、JavaScript側からは、あたかもネイティブなオブジェクトを扱うかのように、C++側の機能を利用できます。 // JS側からの利用例 const m = Module . midpoint ({ x : 0 , y : 0 } , { x : 10 , y : 4 }) ; console . log ( m . x , m . y ) ; // 5 2 const buf = new Module . ByteBuffer ( 1024 ) ; buf . fill ( 0xff ) ; console . log ( buf . size ()) ; // 1024 embindを利用することで、 _malloc による手動メモリ確保や HEAPU8 の直接操作といった低レベルな記述は隠蔽され、コードも簡潔になります。 しかし私は、前節で述べた低レイヤーにおける処理の理解は重要だと考えています。私自身について言えば、前節で見た線形メモリの挙動を一度自分の手で確かめることで、ようやくembindの宣言的な記法を使いこなせるようになったからです。また、いつハンドルを delete() すべきか、なぜ確保したはずのビューが壊れることがあるのかといった疑問にも、筋道を立てて考えられるようになりました。 以降では、embindを使ってJS/C++間でデータを受け渡す手法を見ていきます。この観点で整理すると、本記事が扱う代表的なパターンは、 コピー 、 ハンドル 、 ゼロコピーのビュー の3つに分けられます。 value_object はコピー value_object<Point> として公開した構造体は、JS/C++の境界を越えるたびに、JSのプレーンなオブジェクトへ詰め替えられます。逆方向も同じです。そのため、返ってきた m はC++側の実体への参照ではなく、独立した複製です。たとえば m.x = 99 と書き換えてもC++側には影響しません。使い終わったあとは、通常のJSオブジェクトと同じようにGCによって回収されます。 つまり、 value_object は、所有権を意識せずに扱える一方で、JS/C++の境界を越えるたびにコピーが発生する仕組みです。 この仕組みは、座標や計測値のような小さな値のまとまりを扱うのに向いています。一方で、大きな配列を内包する構造体を毎フレーム往復させるような用途には向きません。 また、 value_object は純粋な値の束であることが前提です。フィールドに生ポインタや所有権を持つリソースを含める設計は避けるべきです。コピーされた先で、そのポインタが何を指しているのか、誰が解放するのかという情報が失われ、単なるアドレス値だけが残ってしまうからです。 class_ はハンドル new Module.ByteBuffer(1024) を実行すると、C++のヒープ、すなわちlinear memory内に ByteBuffer の実体が new されます。JS側に返ってくるのは、その実体を指すハンドルを内蔵した薄いラッパーオブジェクトです。 第1部で見たとおり、C++オブジェクトの実体はlinear memory上にあります。JSの変数が保持しているのは、その実体そのものではなく、実体を指すハンドルです。 buf.fill(0xff) を呼ぶたびに、呼び出しはJSからWasm上の同じC++実体に届きます。ここではコピーは起きません。 その代わり、 実体の解放はJS側が引き受け、明示的に delete() を実行する 必要があります。 register_vector で公開した std::vector も、基本的には class_ と同じ系統です。返ってくるのは素のJS配列ではなく、 .size() 、 .get(i) 、 .set(i, x) 、 .push_back(x) などを持つハンドルです。 この仕組みのため、要素アクセスのたびにJSからC++側への呼び出しが発生します。よって、巨大な配列を .get(i) のループで一要素ずつ走査すると、JS/C++間呼び出しのコストがその回数分だけ積み上がります。また、ハンドル自体には明示的な破棄が必要です。 要素そのものはコピーによって安全にやり取りされますが、ハンドルの後始末はJS側が担う必要があります。次に見る typed_memory_view と対比すると、 register_vector は「中身の扱いは安全寄りだが、破棄の責務は残る配列」と位置づけられます。 typed_memory_view はビュー typed_memory_view が提供するのは、ゼロコピーのアクセスです。C++側の連続領域を一切複製せず、JSの TypedArray としてそのまま覗かせます。たとえば、カメラ画像のようなRGBAバッファを返す例を考えてみます。 #include <emscripten/val.h> class FrameBuffer { public : FrameBuffer ( int w, int h) : pixels_ (w * h * 4 , 0 ) {} // 内部バッファを指すビューを返す。コピーは発生しない emscripten::val pixels () { return emscripten:: val ( emscripten:: typed_memory_view (pixels_. size (), pixels_. data ())); } private : std :: vector < uint8_t > pixels_; // RGBA }; const frame = new Module . FrameBuffer ( 640 , 480 ) ; const view: Uint8Array = frame . pixels () ; // linear memoryを直接指す窓 view [ 0 ] = 255 ; // C++側のpixels_[0]が直接書き換わる この view の正体は、第1部で見た HEAPU8 の部分ビューです。linear memory上の pixels_.data() から始まる領域を、JSの Uint8Array として直接見ているだけです。コピーが発生しないため、メガバイト級のバッファでもほぼ一瞬でJS側へ「渡す」ことができます。 ただし、ビューはあくまで「ビュー」なため、その有効期限は、元のバッファの寿命に完全に支配されます。具体的には、次のいずれかが起きた時点でビューは無効になります。 元のオブジェクト、つまり frame が破棄された → viewは解放済み領域を指すことになる 内部の std::vector が再確保された → viewは古いアドレスを指したままになる 線形メモリが成長した 第1部で予告したとおり、 ArrayBuffer が差し替わり、古いビューはdetachされる。成長は別の場所の malloc をきっかけに起こることもあるため、自分のコードがそのバッファに何もしていなくても、ビューが無効になる可能性がある。 したがって typed_memory_view の鉄則は「 取得したら即座に読み書きし、持ち越さない 」です。保持して使用し続けたいデータは、 view.slice() などでその場でJS側へコピーし、切り離しておく必要があります。 3つの渡し方をまとめると、次のように整理できます。 コピー( value_object )は、所有権を考えなくてよい代わりにコピーコストを払う。 ハンドル( class_ / register_vector )は、コピーを避けられる代わりに、JS側が解放の責務を負う。 ビュー( typed_memory_view )は高速だが、元バッファの寿命と完全に結びつく。 おわりに 本記事では、まずWasmの線形メモリという土台を確認し、その上でembindが提供する3つのデータの渡し方を見てきました。 embindの宣言的なAPIは、 _malloc や HEAPU8 といった低レイヤーを隠蔽してくれます。しかし所有権と寿命の責務は、プログラマが十分に認識し、管理する必要があります。本記事では、低レイヤーの挙動から順を追って解説することで、embindを実装に採用した場合も、そのAPIの背後で所有権と寿命がどう扱われているかを見通せるようになることを目指しました。 同じようにEmscriptenと向き合う方にとって、本記事がメモリ管理を筋道立てて考えるための一助となれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ZOZOMO部OMOブロックの宮澤・多田・東谷です。私たちは2026年7月22日(水)・23日(木)の2日間、JPタワーホール&カンファレンスにて開催された「AI DevEx Conference 2026」に参加してきました。本記事では、会場や各ブースの様子に加え、特に印象に残ったセッションをご紹介します。 目次 はじめに 目次 AI DevEx Conference 2026とは 会場の様子 セッション紹介 Day1(7/22) AIと共存する世界のソフトウェアエンジニアリング─不変の本質とプログラミングの未来 AI時代のプロダクト開発変革をどう進めるか Day2(7/23) なぜ、AIが進化しても期待される生産性を上げることができないのか 『動くだけ』のその先へ ― AI駆動開発で品質と速度を両取りする温故知新な新手法 2026年のソフトウェアエンジニアリングを考える さいごに AI DevEx Conference 2026とは AI DevEx Conference 2026 は、ファインディ株式会社が主催する、AI時代の開発生産性と開発者体験(DevEx)をテーマにしたカンファレンスです。前身の「開発生産性Conference」から名称を改め、MetaやGitHub、Uber、LinkedInなど海外テック企業を含む50社以上から56名を超える方々が登壇しました。 会場の様子 会場のJPタワーホール&カンファレンスには、Room A〜Dの4つのセッションルームがありました。セッション以外の企画も充実しており、スポンサーブースエリアやポスター展示「AI DevEx Gallery」が設けられていました。さらに、スポンサーブースを回ってスタンプを集めると豪華賞品が当たる「巨大ガチャ」も用意されていました。国内外50社以上が登壇する規模だけあって、2日間を通して4トラックが同時進行し、会場内を回るだけでも熱気が伝わってきました。 JPタワーホール&カンファレンスの外観 会場入口の「AI DevEx Conference 2026」サイネージ AI DevEx Gallery(ポスターセッション) セッションの合間には、「開発生産性の先・DevEx」に関する各社の取り組みをポスター形式で展示する「AI DevEx Gallery」を自由に見て回ることができました。 KINTOテクノロジーズ 粟田啓介さんのポスター「AI時代に必要な『組織の成果』を作る人材育成」 このほか、Findy Team+によるサイクルタイム短縮事例(360時間→15.7時間)や、楽々明細 植木遼太さんによる「開発チームへの『無駄な依頼』が消えた話」なども展示されていました。各社の現場ならではの実践知が並び、立ち寄るだけでも学びの多いコーナーでした。 スポンサーブース スポンサーブースエリアの様子。各社のブースが並び、多くの参加者が足を止めていました 朝日新聞社は「伝統的新聞社は、AIエージェントで再定義できるのか」というポスター展示を行っていました。創業150周年の100日プロジェクトでCursorのハンズオンに224名が参加し、技術系エージェントの体験率93%、DORA Four Keysのデプロイ頻度+667%という実績が添えられていました。こうした事業会社側のAI活用実績が数多く紹介されていました。スタートアップだけでなく伝統的な大企業でも、AI活用が着実に進んでいることを実感しました。 朝日新聞社の事例ポスター「伝統的新聞社は、AIエージェントで再定義できるのか」 Postman株式会社の「APIテストは何でやっていますか?」「API仕様・ドキュメントはどこに格納していますか?」というシール投票企画も目を引きました。手動やcurl、Postmanでの管理など、テストへの向き合い方は企業によってさまざまでした。 Postman株式会社ブースの様子。「APIテストは何でやっていますか?」等のシール投票企画 また、株式会社カオナビのブースでは「このAIポンコツすぎるな…一体どんな挙動?」というお題で、参加者が実際に困ったAIの挙動を自由に書き込んでいく付箋ボードもありました。付箋には「同じ失敗を繰り返す」「コンテキストが長くなると指示を無視し始める」「気づいたら英語で話している」といった挙動が並んでいました。 「このAIポンコツすぎるな」付箋ボードに寄せられた参加者たちの実体験 真剣な講演の合間にこうした企画があることで、AIとの向き合い方について肩の力を抜いて話せる空気が、会場全体に流れていたのが印象的でした。 セッション紹介 ここからは、特に印象に残ったセッションを紹介します。 Day1(7/22) AIと共存する世界のソフトウェアエンジニアリング─不変の本質とプログラミングの未来 基調講演「AIと共存する世界のソフトウェアエンジニアリング─不変の本質とプログラミングの未来」の登壇の様子 宮澤です。Dave Farleyさんの基調講演「AIと共存する世界のソフトウェアエンジニアリング─不変の本質とプログラミングの未来」を紹介します。 制約理論を引きながら「コードはボトルネックではない(もしそうだったとしても)」と提起するスライド まず、セッションの冒頭で提起されたのが「コーディングはもうボトルネックではない」ということでした。生成AIにより、人間が読み、理解し、検証できる速度をはるかに超えてコードを生成できるようになりました。コードの作成はボトルネックではなく、生成されたものが適切かどうかを素早く検証することが次の最適化ポイントになっているということです。 この前提は、現場のソフトウェア開発に携わる私自身も感じていたものでした。 続いて、セッションの本題に入っていきます。Farleyさんは、プログラミング言語には3つのゴールがあると述べています。 人間が問題についての思考を整理する その理解を他の人と共有する 実行可能な指示としてコンピュータに伝える そして従来のプログラミング言語は、これらのゴールを達成するために3つの技術を採用してきたとしています。 単純で決定論的な形式文法(同じ命令に対して同じ結果が得られる) 意図の曖昧さのない表現 反復可能で決定論的な実行 しかし、AIを活用するようになったことで、これらの技術が機能しなくなり、以下3つの問題が新たに生じたとしています。 意図を正確に指定すること 得られた結果が意図通りかを検証すること 変化し続ける世界に適応するためのインクリメンタリズムを維持すること 例えば、AIに「セキュアなログインページを作れ」とバイブコーディングで指示するだけでは不十分で、具体的にどのような要件を求めているかを開発者が理解して明示する必要があります(1つ目の問題)。さらに、AIが大量にコードを生成できる時代において、すべてのコードを人間が確認するのは不可能です。生成されたコードが意図した要求を満たしているか検証する技術が必要になります(2つ目の問題)。 従来のプログラミングでは、達成しようとしていた実際の成果はコードの中に明示的に入っておらず、開発者の頭の中に暗黙的にあるものでした。これからのAI時代では逆になるとFarleyさんは述べていました。求めている成果は明示的に述べられて検証可能である必要があり、解決策の方が暗黙的になります。プログラミングは解決策を記述することではなく、より精密に成果を定義することになるという主張でした。 なお、Farleyさんはこれらの問題への対処がエンジニアリング思考に依存するとしていました。技術的規律の素養や、問題解決へのシステム思考的なアプローチが引き続き求められます。問題を分解し、要求を検証する能力が必要になるということです。 ここまでの話で、今後必要になるのは「求める成果を、明示的で検証可能な形で定義する」ということでした。その具体的な対応策としてFarleyさんが提唱した方法はBDD(振る舞い駆動開発)でした。達成したい成果を明示的にテスト可能な仕様として指定してAIにこの仕様を満たすコードを生成させるということです。 Farleyさんはこれを「第5世代プログラミング言語」と位置づけていました。パンチカード(第1世代)、アセンブリ言語(第2世代)、高水準言語(第3世代)、4GL(第4世代)に続いて、BDDによる検証可能な仕様が新しい世代の「プログラム」になるとのことです。 このBDDで用いるDSLには、解決策の言語(プログラミング言語)ではなく、問題領域を表す自然言語を使用します。そのため、開発者以外も仕様を読んで理解できるという利点があります。 しかし、コンピュータに指示するには、ユーザー価値以外の観点も必要になります。例えば、システムの応答速度や耐久性、セキュリティなどのアーキテクチャの観点です。Farleyさんは、これらも結局はシステムの振る舞いにすぎないと主張していました。「応答は一定時間内に返る」「送金の過程でお金が消滅しない」のような、非機能要件も同じく仕様として記述できるということです。 そのうえで、アーキテクチャ自体は「進化的アーキテクチャ」のアプローチで扱うことが重要とのことでした。これは、アーキテクチャを前もって固定するのではなく育てていくものとして扱い、決定を「責任を持てる最後の瞬間」まで遅らせ、設計を変更可能な状態に保つアプローチです。この「進化的アーキテクチャ」という用語は本カンファレンスの別セッションでもたびたび耳にするものでした。 この話で印象的だったのが、注意義務(duty of care)という言葉です。ユーザーが求めるものをそのまま叶えるだけでは不十分です。どれだけセキュリティを担保するべきか、どれだけ耐障害性や応答速度を満たすべきかという専門的な判断を持ち込む必要があります。そのうえでAIに対して詳細に指示することが、プロのエンジニアの責務だということです。 セッションの終盤では、冒頭の主張に立ち戻り「コーディングはもはやボトルネックではない。そしておそらく、最初からボトルネックだったことはなかった」と述べていました。これは、コーディングという工程が消えたのではなく、もともと優れたチームと平均的なチームを分けていたのはコーディングの速さではなかったことが、AIによってあらわになったということでした。 大切なのは、問題を深く理解し、ビジネスの文脈とゴールを把握することです。そして、プロダクトの方向性を考え、アーキテクチャの文脈を保つことです。「問題を深く理解し、携わっているビジネスのゴールを本当に理解しない限り、優れたソフトウェアは作れない」。これはAIの有無によらないプログラミングの本質だということでした。 また、「AIは増幅器(amplifier)である」という言葉も強調されていました(この言葉も本カンファレンスの複数のセッションで耳にしました)。AIは高パフォーマンス組織の強みを拡大し、苦しんでいる組織の機能不全も拡大します。AIが到来する前に機能していたやり方は、AI到来後もよりよく機能するということでした。 BDD、継続的デリバリー、進化的アーキテクチャを採用することで、AIの恩恵をより大きく受けられます。Farleyさんはこれを「Back to the future」と表現してセッションを結びました。古くからの実践に立ち返ることが未来への道になるという、温故知新にも通じるメッセージでした。 AI時代のプロダクト開発変革をどう進めるか 多田です。Notion Labs Inc. CTOのFuzzy Khosrowshahiさんの「AI時代のプロダクト開発変革をどう進めるか」を紹介します。 このセッションを聞いて、AIの登場によって開発の焦点が「形にすること」から「何を作るべきか」へと移りつつあるのだと改めて感じました。開発者の業務時間の約47%はコミュニケーションと調整に費やされ、1回のリリースには平均6つ以上のツールが関わっているといいます。この「意思決定の負荷」と「調整コスト」こそが、AIトランスフォーメーションを阻む「組織」「ワークフロー」「システム」という3つの壁の正体だと位置づけられていました。 3つの本質的な問題として挙げられた「個人への依存」「情報の分散」「スケーラビリティ」 エージェントを会社全体に拡張する段階で必ずぶつかるのが、「個人への依存」「情報の分散」「エンジニアリングとスケーラビリティ」という3つの問題だといいます。Notionはこれを理論としてではなく、自分たちが日々どう組織し動いているかという実践の中で解いてきた、というのが本セッションの軸でした。 多くの組織はいまだに年次計画や階層的な承認プロセスで動いています 1つ目、「組織」の壁の答えが JazzMode(ジャズモード) です。Fuzzy Khosrowshahiさんは、あらかじめ計画を立てて演奏するオーケストラと、共通のテーマだけを決めて即興で音を重ねるジャズバンドを対比しました。年次計画や階層的な承認を経てからでないと動けない組織は、オーケストラのように計画通りにしか動けない状態だといいます。JazzModeとは、そこから抜け出し、メンバー一人ひとりに裁量を与えながら信頼をベースに素早く学び動いていく運用リズムのことです。一部の得意な人だけにAIの価値を閉じ込めず、組織全体へ広げる必要があります。そこで、まず動き方そのものを、こうした信頼と裁量にもとづく形へ変えていくべきだという主張でした。AI活用が一部のメンバーに偏りがちな自分たちにとって、身につまされる思いでした。 2つ目、「ワークフロー」の壁の答えが Agent OS です。ツール・コンテキスト・ワークフローが断片化したままでは、AIはその溝を埋めるのではなく同じ断片化を引き継ぐだけだ、という指摘には心当たりがありました。実際、自分たちもSlackの会話とGitHubの記録を別々に探し回ることが多く、AIに聞いてもツールをまたいだ断片的な答えしか返ってこないことがあります。Data Scout、Office Q&Aなどの業務ロールに紐づくエージェントをカタログ化しているそうです。そのうえで「ナレッジを取り込む→答えを見つける→ワークフローを自動化する」流れを1つのAIオペレーションレイヤーとして提供している、と説明されていました。個々のエージェントを点在させず、1つのオペレーションレイヤーとしてつなぎ直すという発想は、ぜひ弊社でも参考にしたいと思いました。 エージェントが、チームと共に動くネイティブ環境を ここで印象に残ったのが、 AIのアクセスの重要性 です。エージェントは支援する相手と同じ「現場レベルの権限」の範囲内で動作します。このアクセス権限管理の正確さがAgent OSの必須要件だと言っていました。権限を与えるのは怖い一方、与えなければ結局人が動かなければならず時間がかかってしまいます。このバランスをどう取るか、自分たちも試行錯誤しているところです。 3つ目、「システム」の壁の答えが Software Factory です。創業者の言葉を引用し、「従来のソフトウェアを作ることは橋を作るようなもの。正しく設計すれば計画通りに作れる。しかしAIでプロダクトを作ることは日本酒の醸造に近い」と説明されていました。橋は設計図さえ正しければ計画通りに完成させられる仕事だが、酒造りは発酵の進み具合を見ながら条件を調整し続ける仕事だ、という説明でした。AIによるプロダクト開発も同じで、最初から完璧な設計図を引くのではなく、条件を整えては様子を見て調整を重ねる進め方に近づいている、ということのようです。醸造そのものも近代化されてきたように、個人の職人技に頼るのではなく、この「調整し続けるプロセス」自体を組織として仕組み化する必要があるという結論を、興味深く感じました。 組織・ワークフロー・システムの3本柱 アメリカの有名テック企業でも、また日本の一部の企業でも、すでにこうした基盤の上でAI時代の運用を築いているといいます。 最終メッセージは「ソフトウェアの次の章は、判断力とコンテキスト、そして新しい形の裁量を組み合わせられる人たちによってつくられる」というものでした。 組織はJazzModeというリズムで、ワークフローはAgent OSという1つのレイヤーで、システムはSoftware Factoryという再現可能な仕組みで支えるという整理でした。この3本柱は、複数チームでAI活用が進む弊社の状況にもそのまま当てはまる示唆だと強く感じています。ぜひ社内でも参考にし、今後のエージェント活用に活かしていきたいと思いました。 Day2(7/23) なぜ、AIが進化しても期待される生産性を上げることができないのか 多田です。2日目の基調講演「なぜ、AIが進化しても期待される生産性を上げることができないのか」を紹介します。登壇者はGitHub, Inc. Business Insights & Research AdvisorのEirini Kalliamvakouさんです。個人レベルではAIが劇的な生産性向上をもたらしている一方、組織全体ではその恩恵が測定できていない、という矛盾がテーマの講演でした。 講演はまず、一見矛盾する「2つの真実」の提示から始まりました。開発者個人のレベルでは、ポジティブな結果が出ていました。GitHub Copilot利用で55%の高速化、開発者の90%以上が毎日AIを使用(DORA 2025)、80%以上が生産性向上を実感といった数字です。一方、組織のレベルでは対照的な結果が示されていました。MIT NANDAレポートでは生成AIパイロットの95%がROIなしで、投資額300〜400億ドルに対し測定可能な成果は限定的でした。400社以上を対象にした別の縦断調査では、AI導入率が65%増えてもスループット向上はわずか8%で、デリバリーの安定性がむしろ低下しているケースもあったそうです。どちらの立場も嘘をついているわけではなく、AIから価値を生むプロセスは個人の加速から組織の成果へ単純には移行しない、というのが講演の出発点でした。 この矛盾を解くカギとして提示されたのが、「システム」「測定」「判断」という3つの複雑さの層です。まず「システム」の層では、ソフトウェア開発が「リレー競技」に例えられていました。一人が55%速く走れても、バトンパスが遅ければチーム全体のタイムは縮まりません。ここでのバトンパスは、レビューやビルド、デプロイ、要件定義といった工程です。開発者の時間のうち、コードを書くことに使われているのはわずか約14%だといいます。 WHAT AMPLIFIES AI – EVERY FACTOR IS TEAM-LEVEL 個人の成果を組織的な成果に変えるには、次の7つの要因が必要になるといいます。いずれも個人の作業環境だけでは完結せず、チームや組織全体で整備すべき条件です。 明確に共有されたAIへのスタンス 健全なデータエコシステム AIがアクセスできる社内データ 強固なバージョン管理の実践 小さいバッチでの作業 ユーザー中心の視点 質の高い社内プラットフォーム 実際、自分たちのチームでもAIによってコーディング自体は明らかに速くなりました。しかし、その分PRの数が増えてしまい、レビュアーの手も回らなくなってきました。しかもAIが書いたコードは一見良さそうに見えるため、かえって読み解くのに時間がかかります。この「バトンの受け渡しが重くなる」現象は、レビューという工程だけでなく、要件のすり合わせのようなより大きな単位でも起きると指摘されていました。講演を聞いて、自分たちの状況にもそのまま当てはまることに気づかされました。 次の「測定」の層では、従来の指標がAI時代のワークフローでは意味を変えてしまっていると指摘されていました。例えばマージ率の低下は「品質の悪化」ではなく、AIで複数の代替案を安価に試す探索的な行動が増えた結果かもしれません。逆にマージ率の上昇も「品質の向上」とは限らず、レビュー不足のまま通してしまっているだけの可能性があります。今後見るべき新しいシグナルとして、3点が挙げられていました。AI活用がワークフロー全体に組み込まれているか、品質を保ったままエージェント起因の成果物の割合が増えているか、エージェントに与えるコンテキストが意図的に整備されているかです。 最後の「判断」の層では、Margaret-Anne Storeyさんの「3つの負債」フレームワーク(技術的負債・認知的負債・意図負債)が引用されていました。そして、スピードの向上が新たな負債を生む構造が説明されました。 組織の基盤とエージェントへの委任度合いによる4象限。右下が「危険な領域」として示されていました 「組織の基盤(FOUNDATIONS)」と「エージェントへの委任度合い(AGENT DELEGATION)」の2軸でマトリックスを作った説明も印象的でした。基盤が強く委任度合いの低い組織は、安全に委任を広げていける「Headroom」の状態にあります。基盤が弱く委任度合いも低い組織は「Slow but safe」で、まず基盤を直すべき状態にとどまります。一方、基盤が弱いまま委任だけを増やす組織は、問題の検知が追いつかなくなる「Danger」に陥りやすいそうです。基盤とセットで委任を進める組織だけが、出力が回を重ねるごとに良くなる「Healthy growth」の複利効果を得られる、という整理でした。 Kalliamvakouさんは最後に、「個人の加速は間違いなく現実であり、素晴らしい出発点です。しかし、真の価値と競争優位性は、システムをどう構築するかにかかっています」と締めくくりました。 「AIは増幅器であり鏡である」という結論は、このカンファレンスを通して一番印象に残ったメッセージだったように思います。個人の生産性指標だけを追うのではなく、組織の「基盤」に投資する必要があるという指摘を、ぜひ社内でも共有し、今後のチーム運営に活かしていきます。 『動くだけ』のその先へ ― AI駆動開発で品質と速度を両取りする温故知新な新手法 speakerdeck.com 宮澤です。株式会社ログラス 執行役員CTOの伊藤博志さんによるセッション「『動くだけ』のその先へ ― AI駆動開発で品質と速度を両取りする温故知新な新手法」を紹介します。 セッションは、2026年2月の3連休にClaude CodeでOLAPデータベースを試作した話から始まりました。OLAPデータベースは集計や分析に特化したデータベースです(本筋ではないので詳細は割愛します)。この試作したデータベースをPolarsやDuckDBといった既存の高速なエンジンとベンチマーク比較したところ、一定の項目で既存エンジンを上回る性能が出たそうです。さらに、集計軸の組み合わせのほとんどに値が入らないスパースデータというユースケースでは、桁違いに速かったとのことでした。ただし、本人も「ユースケースは限定的」と断っており、既存エンジンに全面的に勝った話ではありません。それでも、週末の実験でここまで動くものができてしまう。この事実がセッションの出発点でした。 この開発プロセスは、ビジョンからロードマップを作り、ADR(Architecture Decision Record)で設計を決めるところから始まります。そこから仕様を作って実装し、検証してベンチマークを回すループとして紹介されていました。 続いては、このループの品質担保の方法です。AIの出力の検証はテストで担保することになりますが、出力は確率的なため、「テストを実装したことにする」といった事象も起こり得ます。そこで、仕様とテストと実装をIDで紐付け、その対応をスクリプトで決定論的に検証するという方法が紹介されました。仕様に番号を振り、「この仕様に対応するテストが書かれている」という事実を機械的にチェックする仕組みをとったそうです。 テストの作り方にも原則がありました。テストは仕様書から導出し、実装と同じコンテキストでAIに書かせない、というものです。実装を見ながらテストを書くと、コードの動きをなぞるだけのテストになってしまうからです(こちらは本カンファレンスの基調講演をはじめ、複数のセッションで同様の言及がありました)。 さらに、複雑な仕様には形式手法を適用し、実装前の段階で仕様そのものの矛盾や曖昧さを検証しているとのことです。学習コストの高さがネックになる手法ですが、検証自体はAIが実行するため、導入のハードルは下がったそうです。 検証の道具はもう1つ、オラクルテストです。OLAPデータベースは仕様が広く知られているため、既存のデータベースに同じ入力を与えて結果を突き合わせられます。形式手法が仕様の整合性を担保し、オラクルテストが実装の出力を守る、という役割分担のようです。 ここまでが、冒頭で紹介したOLAPデータベースを試作した方法とのことでした。 続いて、この手法が実際のプロダクト開発で通用するのかという話です。対象に選ばれたのは、既存プロダクトのレポート機能です。データが大きい場合に性能が限界に来ていることを内部で認識しながらも、簡単に直せる規模ではなかったそうです。新しいエンジンを作り、フィーチャーフラグで切り替えられる形にしたそうです。このとき、既存機能がもともとの仕様を体現しているため、それがオラクルになります。既存の挙動をリバースエンジニアリングして仕様書に書き出し、週末の実験と同じループを回したとのことでした。 結果として、第1弾は8日で動くようになったそうです。ただし、品質を担保しきるまでには約2か月かかったそうです。オラクルテストをやり切る過程では、組み合わせが爆発する部分はドメインに詳しい人の目を入れて現実的な範囲に絞り込みながら、バグを出しては直す期間が必要だったとのことです。また、仕様の前提をそろえる議論だけで1か月を使ったとのことで、開発時間の半分以上が品質保証に費やされたと紹介されていました。 印象的だったのは、「仕様が間違っていたら、間違ったものが高速に作られるだけ」という指摘です。どれだけ実装が速くなっても、その仕様は本当に正しいのか、そもそも必要なのかを考える部分は残り、人間はここに一番時間を使うべきだという主張でした。 そしてこの意思決定をどう行っていくかについて、ログラスが現在試行しているのがトレーサビリティを「前」と「後ろ」に伸ばすことだそうです。開発の前にある「なぜ作るのか」「何を作るのか」の探索と、デリバリー後の成果検証までを仕様とつなぎ、当初の仮説が満たされたかを確認できるようにする構想とのことでした。その探索の場面では、AIにドキュメントを生成させるのではなく、インタビュアーとして人間の思考を助ける役割を担わせているとのことでした。 セッションの締めくくりは、タイトルの回収でした。バイブコーディングで、誰でも「動くもの」は作れるようになりました。しかし「正しく動くとは何か」を極め続けると、「そもそも価値があるものを、どうやって作るのか」という問いに行き着きます。AIによって「どのように作るか」のハードルが下がった今こそ、人間は「なぜ作るのか」「何を作るのか」に価値を置くべきだとのことでした。 ここからは私の所感です。 仕様やその検証が重要になるという感覚は、昨今のAIエージェントの進化を見る中で、現場で開発する自分自身も持っていました。このセッションを聞いて、その感覚は確信に変わりました。第1弾が動くまでは8日でも、品質の担保には2か月を要し、仕様の前提をそろえる議論だけで1か月を使っていました。この時間配分が、今後、実装よりも仕様に人間の時間が寄っていくという構図を示しているように思いました。 また、Day1のFarleyさんの基調講演が「求める成果を明示的で検証可能な形で定義する」ことを理論として示したのに対して、このセッションはそれを実プロダクトでやり切った実践例でした。異なる立場の登壇者による理論と実践が噛み合っていたことも印象的でした。 自分の業務に引きつけると、まず試せるのは仕様とテストのID突合だと考えています。スクリプトで決定論的にチェックする部分だけなら、既存のテスト資産にも後付けできそうです。その先では、複雑な仕様のレビューに形式手法を取り入れることも検討します。 2026年のソフトウェアエンジニアリングを考える speakerdeck.com 東谷です。最後に、タワーズ・クエスト株式会社の和田卓人さんによる基調講演「2026年のソフトウェアエンジニアリングを考える」を紹介します。2025年に何が起きたかの振り返りから始まり、開発プロセスの再構築、そして「認知負債」という新しい課題まで、この2日間の総括のような講演でした。 2025年の振り返りはこうです。2月にAndrej Karpathyさんが「Vibe Coding」を提唱し、コードをレビューせず自然言語と動作確認だけで開発するスタイルが広まりました。同時期にTim O'Reillyさんが「私たちの知る形のプログラミングは終わる」と発信しており、開発者の間に期待と緊張が走ったのを覚えています。一方で「AIスロップ」(見た目は整っているのに中身の怪しい生成物)がレビュアーの負担を押し上げ、乗り遅れることへの恐怖が開発者の精神を削った時期もありました。そして11月、モデルの能力がもう一段跳ね上がりました(Claude Opus 4.5が出たあたり、という説明でした)。人間が対話しながら進める「伴走」から、自律的に動くエージェントに任せる「委託」へと開発スタイルがシフトしました。「以前は2分に1回プロンプトを打っていたのが、今は1時間に数回になっていませんか」という問いかけには、大きく共感してしまいました。プランモードで議論して承認したら、40分後に戻ってくればいいという状態が増え、人間がPCの前に張り付く必要は、確かになくなりつつあります。 現在のソフトウェアエンジニアリングは過去の積み上げの上にある 委託型の何が問題かというと、人間のコントロールと状況把握が利きにくくなり、レビューが破綻することです。AIが書いた大量の、自分では書いていないコードを、従来のコードレビューという形式で見続けるのは無理があります。そこで和田さんは、コードレビューが果たしてきた役割を分解し、開発プロセス全体へ再配置することを提案していました。上流の仕様定義を厳密にしてAIの矛盾検出力を活かします。テストを先に書かせて、エージェントが「実装が通るテストを書いて自作自演する」のを防ぎ、型・静的解析・linterといった静的検査で、エージェントのコンテキスト外が壊れても気づけるようにしています。そして全PRを均一に見るのではなくリスクベースでレビューしていました。さらにレビューが担っていた教育の機能は、人間同士のペアプログラミングで意識的に取り戻していました。5つとも明日から検討できる具体的な話でした。 後半の主役は「認知負債」です。保守性の低いコードという従来の技術的負債は、モデルの進化でむしろ改善傾向にあります。代わりに深刻化しているのは、人間が理解しないままコードを生成することで生じる、メンタルモデルと実際のコードの乖離だという指摘です。かつては理解がなければコードは書けなかったので、コードを見ればその人の理解度もある程度推測できたのですが、いまは理解がなくても恐ろしい速さでコードが出てくるので、両者が切り離されてしまいました。しかも理解度を測るメトリクスはまだ存在せず、DORAのFour Keysのような生産性指標は理解を置き去りにしても上げられてしまう構造になってしまっています。見かけの生産性を上げたい組織と個人の間で「共犯関係」が成立しうる、という警告は重く受け止めました。1985年にPeter Naurさんは「プログラミングとは理論(メンタルモデル)の構築であり、それを失ったプログラムは動いていても死んでいる」と書いていました。1983年にはLisanne Bainbridgeさんが「自動化が進むほど人間が介入すべき場面は難しくなるのに、介入するスキルは日常的に使われず失われていく」と指摘していました。40年前の警告が、コードだけが先行して人間の理解がついていかないという反転した形で現実になっている、という整理も見事でした。 「では、どうするか」。和田さんの答えは「理解をゲートにする」でした。ご自身の環境では、プランモードの最後にAIから理解度クイズを出させ、答えられなければ先に進めない仕組みを作っているそうです。 実際に問題を出しているCLIの画面 複雑な実装方針はMarkdownの説明だけでは分かった気になりがちなので、図解やアニメーションのような視覚的な形式で説明させる工夫も紹介されていました。 振る舞いも理解しやすいようにアニメーションをAIに出力させた図 そして重要なのが、理解とスピードはトレードオフではないという話です。講演で紹介された研究では、AIに丸投げしたグループは当たれば最速、外れれば最も遅く理解も残らないという結果でした。一方、AIに説明を求めて質問を繰り返したグループは、スピードをほとんど落とさずに高い理解度を保っていたそうです。 理解とスピードはトレードオフではないと説明する和田卓人さん この話は、私たちのチームの次の一手にそのままつながります。問い合わせ調査のSkill化を進めた結果、調査に必要なクエリの出力も、その検証方法の提示もAIが行うようになりました。次にボトルネックになるのは、まさに人間の理解と検証です。そこで、調査結果と一緒にAIから人間へ理解度を確かめる問題を出してもらう、やったことをHTML形式の図に書き起こして出力してもらう、といった仕掛けを入れていこうと考えています。和田さんの言う「理解をゲートにする」を、コーディングだけでなく運用調査にも適用する形です。「委託が進むほど、理解は意識して守らなければ失われる」という言葉は、まさに2026年後半に向けた宿題でした。 さいごに 今回のカンファレンスで最も印象に残ったのは、立場の異なる登壇者が口をそろえて語った「AIは増幅器である」という言葉でした。AIは導入すれば誰もが等しく速くなる魔法ではなく、強い組織の強みも、苦しい組織の機能不全も、そのまま拡大します。個人がAIで速くなった今、問われているのは、仕様や検証、レビュー、組織の基盤といった「AI以前から大切だったもの」の質なのだと感じました。私たちのチームでも、AIによってコードを書く速度は確実に上がりました。次の課題は、その加速を組織の成果へつなげる基盤づくりです。今回持ち帰った学びを、日々の開発とチーム運営に活かしていきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、情報セキュリティ部の 兵藤 です。日々ZOZOの安全を守るためSOC業務に取り組んでいます。 本記事では、開発環境におけるサプライチェーン対策として、Takumi Guardを全社展開した事例について紹介します。 また、情報セキュリティ部ではその他にもZOZOを守るための取り組みを行っています。詳細については以下の「Claude CodeがSOC業務を全自動でやってくれるってさ」をご覧ください。 techblog.zozo.com 目次 はじめに 目次 背景と概要 Takumi Guardとは Takumi Guardの対応エコシステム Takumi Guardの適用範囲 Takumi Guardの全社展開 コンテナでのTakumi Guard利用 MDMでのTakumi Guard配布 シークレットの問題 Takumi Guardの爆速アップデート対応 許可の設定をEntra側に寄せる シークレットの閲覧 デバイススクリプトの更新権限 Push Ruleset 展開後のログ画面 おわりに 背景と概要 昨今ではサプライチェーン攻撃の脅威が増しており、2026年3月には開発環境でよく使われる axios などが侵害されました。 このような事例から開発環境におけるセキュリティ対策の重要性が高まっています。特に、開発者が使用するツールやパッケージの安全性を確保することは、プロダクトセキュリティに直結します。 そこで、私たちは開発環境におけるサプライチェーン対策として、Takumi GuardをMDM経由で全社展開しました。 Takumi Guardとは Takumi Guard は、GMO Flatt Securityが提供する、悪意のあるパッケージの検知・ブロックを行うツールです。個人利用であれば無償ですが、組織として全端末に展開したり、ログを収集したりする場合は有償の契約が必要です。 詳しくは公式ドキュメント 1 をご覧ください。 Takumi Guardの対応エコシステム Takumi Guardはエンジニアが利用する開発言語における悪意のあるパッケージを検知・ブロックできます。2026年7月27日時点では、以下のエコシステムに対応しています。 npm PyPI RubyGems Go Packagist 2026年4月当初はまだ npm や PyPI までしか対応していませんでしたが、現在では RubyGems や Go 、 Packagist にも対応しており、開発速度が凄まじいです。 Takumi Guardの適用範囲 Takumi Guardの適用範囲は、大きく開発端末とCI/CD環境の2つに分けられます。 弊社ではGitHub ActionsをCI/CD環境として利用しています。このCI/CD環境にもTakumi Guardを導入でき、 公式ドキュメント に簡易手順が記載されているため導入は容易です。手順に記載されているBot IDを利用すると、GitHub Organizationに紐づくリポジトリへTakumi Guardを導入し、ログを後から確認できます。 BotはTakumi Guardのポータルから以下の項目で追加できます。 Botの追加 作成されたBotは「設定」の項目から確認可能です。 Botの確認 Takumi Guardの全社展開 Takumi Guardを全社展開するにあたり、弊社では以下の項目に対応しました。 開発ガイドラインへのTakumi Guardの記載 GitHub Actions環境へのTakumi Guardの導入 全社端末へのMDM経由でのTakumi Guardの導入 これらの対応の中で工夫した点について紹介します。 コンテナでのTakumi Guard利用 GitHub Actions上でコンテナを利用する場合、コンテナ内でのレジストリプロキシについては flatt-security/setup-takumi-guard-npm@v1 などのActionsだけでは対応できません。 これらのActionsの outputs で registry-url や token を取得し、コンテナ内部に渡す必要があります。 この点には注意が必要です。ビルド時に渡す場合、 --mount=type=secret を利用してコンテナ内部に token を渡します。そして、 npm install などを実行する際には同じ RUN 内で完結させる必要があります。別の RUN で実行すると、イメージのレイヤーに token が残ってしまうため、セキュリティ上の懸念があります。 DockerfileやGitHub Actionsのワークフローについては以下のようなものを参考として作成し、全社へ共有しました。 FROM node:20 WORKDIR /app COPY package.json package-lock.json ./ # secretマウントし、そのRUNの中だけで .npmrcを作って使い、最後にrm RUN --mount=type=secret,id=takumi_token \ printf 'registry=https://npm.flatt.tech/\n//npm.flatt.tech/:_authToken=%s\n' \ "$(cat /run/secrets/takumi_token)" > /tmp/.npmrc && \ export NPM_CONFIG_USERCONFIG=/tmp/.npmrc && \ npm ci && \ npm install some-extra-package && \ rm -f /tmp/.npmrc - name : Build image env : TAKUMI_TOKEN : ${{ steps.takumi.outputs.token }} run : | docker build \ --secret id=takumi_token,env=TAKUMI_TOKEN \ -t myapp . PyPIの場合は pip install を実行した際、 ~/.cache/pip にレスポンスの情報などが残ってしまいます。トークンがそのまま残るわけではありませんが、 --no-cache-dir を利用してキャッシュを残さないようにするのがベターです。 FROM python:3.13-alpine WORKDIR /app ... # Takumi Guardのトークン入りPIP_INDEX_URLをsecretとしてマウント RUN --mount=type=secret,id=pip_index_url \ export PIP_INDEX_URL="$(cat /run/secrets/pip_index_url)" && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir -r requirements.txt - name : Build image run : docker build --secret id=pip_index_url,env=PIP_INDEX_URL -t myapp . 上記の渡し方は一例のため、開発環境に合わせて適宜変更してください。 MDMでのTakumi Guard配布 ZOZOではMDMとしてMicrosoft Intuneを利用しています。Intuneでは、Macについては シェルスクリプト を利用してTakumi Guardのラッパースクリプトを配布できます。Windowsについては 修復スクリプト を活用できます。検出スクリプトをダミーで作成し、修復スクリプトでTakumi Guardのラッパースクリプトを配布する形です。 これらのスクリプトはIntuneで定期実行しています。何らかの理由でTakumi Guardが外れてしまった場合でも、次回の定期実行時に再度配布されます。 Takumi Guardのラッパースクリプトについては 公式ドキュメント に記載されています。 これらのスクリプトを利用することで、全社端末にTakumi Guardを配布できます。 シークレットの問題 Intuneでシェルスクリプトを配布する場合、シークレットをうまく扱えないという問題があります。この点が今後の課題です。対策としては、SCEP方式の証明書を端末に配布した後、中間APIサーバを立ててAzure Key Vaultからシークレットを取得する方法などが考えられます。 この作業には各部署との調整や、ラッパースクリプトの大幅な改修が必要です。また、少数チームにとっては実装と運用のコストが高くなります。 このため、Takumi Guardの展開遅延の懸念がありました。昨今のサプライチェーン攻撃の脅威を考え、Takumi Guardのトークンを払い出すだけのBotシークレットの権限であれば一旦このリスクは許容し、速度を優先することにしました。 この方法を実施する場合は、定期的にBotのシークレットのローテーションを行うなど、リスク軽減を図る必要があります。 またトークンの異常な発行が行われていないか発行量を適宜確認し、必要に応じて対応することも求められます。この点はKey Vaultなどを利用した場合も同様です。 Takumi Guardの爆速アップデート対応 上記以外にも対応すべきことはありました。それは、爆速で行われるTakumi Guardのアップデートへの追従です。 前述の通りTakumi Guardは日々アップデートされています。展開の仕組みを整えている途中でRubyGems対応の機能が追加された時は、その速度感に驚愕したことを覚えています。 この速度に追従するためには、利用する側でも同様のデプロイの仕組みを整えておく必要がありました。ZOZOではGitHub Actions経由でIntuneのラッパースクリプトを更新する仕組みを整え、Takumi Guardの爆速アップデートへ追従できるようにしました。 以下がmacOSにおけるラッパースクリプトの更新のワークフローです。 name : Deploy Takumi Guard macOS script to Intune on : push : branches : - main paths : - scripts/install_takumiguard.sh permissions : contents : read id-token : write concurrency : group : intune-takumi-guard-mac cancel-in-progress : false jobs : deploy-macos-script : name : Deploy Takumi Guard macOS script runs-on : ubuntu-latest timeout-minutes : 30 steps : - name : Checkout uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with : persist-credentials : false - name : Azure Login uses : azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with : client-id : ${{ vars.AZURE_CLIENT_ID }} tenant-id : ${{ vars.AZURE_TENANT_ID }} allow-no-subscriptions : true - name : Fetch TG Bot API key from Key Vault id : keyvault env : AZURE_KEY_VAULT_NAME : ${{ vars.AZURE_KEY_VAULT_NAME }} run : | set -euo pipefail : "${AZURE_KEY_VAULT_NAME:?Set AZURE_KEY_VAULT_NAME in GitHub Actions variables}" api_key="$(az keyvault secret show \ --vault-name "${AZURE_KEY_VAULT_NAME}" \ --name tg-bot-api-key-mac \ --query value \ -o tsv)" if [ -z "${api_key}" ] ; then echo "Failed to fetch tg-bot-api-key-mac from Key Vault ${AZURE_KEY_VAULT_NAME}" > &2 exit 1 fi echo "::add-mask::${api_key}" echo "api_key=${api_key}" >> "${GITHUB_OUTPUT}" - name : Resolve target Intune macOS script env : INTUNE_TAKUMI_MAC_SCRIPT_ID : ${{ vars.INTUNE_TAKUMI_MAC_SCRIPT_ID }} run : | set -euo pipefail : "${INTUNE_TAKUMI_MAC_SCRIPT_ID:?Set INTUNE_TAKUMI_MAC_SCRIPT_ID in GitHub Actions variables}" echo "TAKUMI_MAC_SCRIPT_ID=${INTUNE_TAKUMI_MAC_SCRIPT_ID}" >> "${GITHUB_ENV}" echo "TAKUMI_MAC_SCRIPT_RESOURCE=deviceManagement/deviceShellScripts" >> "${GITHUB_ENV}" - name : Update Intune macOS script shell : bash env : SCRIPT_PATH : scripts/install_takumiguard.sh TG_BOT_API_KEY_MAC : ${{ steps.keyvault.outputs.api_key }} run : | set -euo pipefail : "${TG_BOT_API_KEY_MAC:?Failed to resolve tg-bot-api-key-mac from Key Vault}" echo "::add-mask::${TG_BOT_API_KEY_MAC}" build_script_content() { local api_key_literal api_key_literal="$(printf '%s' "${TG_BOT_API_KEY_MAC}" | perl -0pe 's/([\\\"\$`])/\\$1/g' )" API_KEY_EXPORT_LINE="export TG_BOT_API_KEY=\"${api_key_literal}\"" awk ' /^export TG_BOT_API_KEY=/ { print ENVIRON["API_KEY_EXPORT_LINE"] replaced += 1 next } { print } END { if (replaced != 1) { printf("Expected one TG_BOT_API_KEY export line, found %d\n", replaced) > "/dev/stderr" exit 1 } } ' "${SCRIPT_PATH}" | base64 -w 0 } script_content="$(build_script_content)" access_token="$(az account get-access-token \ --resource https://graph.microsoft.com \ --query accessToken \ -o tsv)" echo "::add-mask::${access_token}" response="$(printf '%s' "${script_content}" \ | jq -Rnc '{scriptContent: input}' \ | curl -sS -w " \n %{http_code}" \ -X PATCH "https://graph.microsoft.com/beta/${TAKUMI_MAC_SCRIPT_RESOURCE}/${TAKUMI_MAC_SCRIPT_ID}" \ -H "Authorization: Bearer ${access_token}" \ -H "Content-Type: application/json" \ --data-binary @-)" http_status="${response## *$'\n' } " if [ "${http_status}" = "200" ] || [ "${http_status}" = "204" ] ; then echo "Updated Takumi Guard Intune macOS script (${http_status})" else echo "Failed to update Takumi Guard Intune macOS script (${http_status})" echo "Response body omitted to avoid leaking scriptContent." exit 1 fi - name : Verify Intune macOS script update shell : bash env : SCRIPT_PATH : scripts/install_takumiguard.sh TG_BOT_API_KEY_MAC : ${{ steps.keyvault.outputs.api_key }} run : | set -euo pipefail : "${TG_BOT_API_KEY_MAC:?Failed to resolve tg-bot-api-key-mac from Key Vault}" echo "::add-mask::${TG_BOT_API_KEY_MAC}" build_script_content() { local api_key_literal api_key_literal="$(printf '%s' "${TG_BOT_API_KEY_MAC}" | perl -0pe 's/([\\\"\$`])/\\$1/g' )" API_KEY_EXPORT_LINE="export TG_BOT_API_KEY=\"${api_key_literal}\"" awk ' /^export TG_BOT_API_KEY=/ { print ENVIRON["API_KEY_EXPORT_LINE"] replaced += 1 next } { print } END { if (replaced != 1) { printf("Expected one TG_BOT_API_KEY export line, found %d\n", replaced) > "/dev/stderr" exit 1 } } ' "${SCRIPT_PATH}" | base64 -w 0 } access_token="$(az account get-access-token \ --resource https://graph.microsoft.com \ --query accessToken \ -o tsv)" echo "::add-mask::${access_token}" expected_script_content="$(build_script_content)" actual_script_content="$(curl -sS \ "https://graph.microsoft.com/beta/${TAKUMI_MAC_SCRIPT_RESOURCE}/${TAKUMI_MAC_SCRIPT_ID}" \ -H "Authorization: Bearer ${access_token}" \ -H "Accept: application/json" \ | jq -r '.scriptContent // empty' )" if [ "${actual_script_content}" != "${expected_script_content}" ] ; then echo "Takumi Guard macOS scriptContent verification failed" > &2 exit 1 fi echo "Verified Takumi Guard Intune macOS scriptContent" ポイントがいくつかあるため、以下の章で解説します。 許可の設定をEntra側に寄せる GitHub Actionsのワークフローでは Azure Login のOIDCでEntraのサービスアカウントにログインします。固定のシークレットを極力持たせたくないため、この構成を採用しました。 シークレットの閲覧 シークレット情報はGitHub Organizationの権限で管理するのではなく、EntraやARM側に寄せました。GitHub ActionsのワークフローからはEntraのサービスアカウント経由でAzure Key Vaultからシークレットを取得する形です。 取得のために Key Vault Secrets User のRoleを対象のKey Vaultに絞って、このサービスアカウントへ付与する必要があります。 Actions上ではメモリ上でシークレットを扱い、ログに出力されないように ::add-mask:: を利用してマスクしています。 ただ、前述のとおりIntune上ではシークレットが残ってしまいますが、この閲覧権限もIntuneの権限を付与できるEntraで管理できるので、Entraでの権限設定が重要です。 デバイススクリプトの更新権限 この権限もEntra側のサービスアカウントに付与します。 Microsoft Graph APIの deviceManagement/deviceShellScripts のエンドポイントを呼び出す必要があります。これは強い権限のため、上記ワークフローを実施するスクリプトの main ブランチへの変更を厳しく確認する必要がありました。 Push Ruleset そもそも、このリポジトリを閲覧できるアカウントを絞ることが前提です。加えて、そのアカウントによるPushにも制限が必要です。 このActionsは、 main ブランチへのpushかつ scripts/install_takumiguard.sh に変更があった場合のみ実行されるようにしています。 この変更についてはGitHubの Rulesets で管理しています。以下の項目を設定しておくとPRでレビューの通ったものがマージされるようになります。 Restrict deletions Require a pull request before merging Block force pushes 基本的にこのRuleのバイパスは設定せず、一律PRのレビューを通すようにしています。これにより、リポジトリの変更がある場合は必ずレビューが入るようになります。 レビュー者は .github/CODEOWNERS で設定しておくと、PR作成時に自動でレビュー依頼が飛びます。 展開後のログ画面 パッケージのダウンロードログはTakumiのポータルから確認できます。ブロックなどの条件で絞り込むことで、どのパッケージがブロックされたかを簡単に確認できます。 展開後のログ画面 おわりに 本記事では、開発環境におけるサプライチェーン対策として、Takumi Guardを全社展開した事例について紹介しました。 まだまだ課題はありますが、Takumi Guardの導入により、開発環境におけるサプライチェーン攻撃のリスクを低減しました。 ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co Takumi Guard ↩
ZOZO開発組織の2026年6月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2026年6月は、前月のMonthly Tech Reportを含む計16本の記事を公開しました。ここ最近の傾向と変わらず、Claude Code関連の記事は特に反響がありました。ZOZOならではの取り組み事例をぜひご覧ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com 登壇 Google I/O 2026現地参加レポート 6月6日に開催された「 Recap: Google I/O 2026 」において、技術戦略部の堀江( @Horie1024 )が「Google I/O 2026 現地参加レポート」というタイトルで登壇、WEAR開発部の武永が「Android & Ecosystem パネルディスカッション」に登壇しました。 Extended Tokyo - WWDC 2026 6月8日に開催された「 Extended Tokyo - WWDC 2026 」において、技術戦略部の諸星( @ikkou )が「WWDC26 直前!眼鏡型デバイスのイマ!」というタイトルで登壇しました。 2026 年度 人工知能学会 全国大会(第 40 回) 6月8日から12日にかけて開催された「 2026 年度 人工知能学会 全国大会(第 40 回) 」において、データサイエンス部の桐島を第一著者として「画像生成AIによる背景生成を用いた最適なフレグランス商品画像の検討」というタイトルでポスター発表しました。 https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/4Yin-A-24 TSKaigi 2026 本編で話せなかったこと、話し足りなかったこと 6月9日に開催された「 TSKaigi 2026 本編で話せなかったこと、話し足りなかったこと 」において、リプレイス推進部の冨川( @ssssotaro )が「TSKaigi ZOZOブースクイズの裏側」というタイトルで登壇しました。 AI研究最前線:第一人者・気鋭の研究者と語る 6月16日に開催された「 AI研究最前線:第一人者・気鋭の研究者と語る 」において、ZOZO Researchの平川が「AIと創るファッションECの未来」というタイトルで登壇しました。 WWDC26 報告会 at LINEヤフー, ZOZO 6月18日に開催された「 WWDC26 報告会 at LINEヤフー, ZOZO 」において、ZOZOTOWN開発2部の上田( @15531b )が「現地で盛り上がった WWDC26 Keynote」というタイトルでショートトークに、森口( @laprasdrum )がパネルディスカッションに登壇しました。 techblog.zozo.com 生成AI フォーラム / AI エージェント フォーラム 2026 夏 6月26日に開催された「 生成AI フォーラム / AI エージェント フォーラム 2026 夏 」において、執行役員 兼 CTOの瀬尾( @sonots )が「ZOZOのAI活用実践」というタイトルで登壇しました。 以上、2026年6月のZOZOの活動報告でした! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに ZOZOTOWN開発本部のらぷ( @laprasdrum )とイッセー( @15531b )です。WWDC現地参加は、らぷは2016年以来2回目、イッセーは初参加(かつ初渡米)です。本記事ではWWDC26の現地参加レポートとともに、ZOZOのiOSエンジニアによるおすすめセッション、6月18日に開催されたLINEヤフー株式会社との合同報告会イベントについてもご紹介します。 目次 はじめに 目次 WWDC26のSpecial Event Day 0 Day 1 Keynote Download stations Platforms State of the Union 現地視聴をより楽しむために In-person labs Inner ring reception Day 2 Developer session Mixer @Developer Center Theater event 併催コミュニティイベント おすすめセッション集 優れたデザインのための原則 Foundation Modelsフレームワークの新機能 WWDC26 報告会 at LINEヤフー, ZOZO さいごに WWDC26のSpecial Event 今年6月7日〜9日(現地時間)のWWDC26では、昨年と同様に招待制のSpecial Eventが現地開催されました。昨年の様子は「WWDC25現地参加レポート」をご覧ください。 techblog.zozo.com 今年のSpecial EventはWelcome receptionから始まりました。基調講演後には、Appleの方へ新APIを質問したりアプリのフィードバックをもらえたりするIn-person labsが開かれました。その後、Apple ParkのInner ringで交流会がありました。最終日はSteve Jobs Theaterで、スター・ウォーズ作品『The Mandalorian and Grogu』の上映とスペシャルゲストのインタビューを楽しめました。 Date Pacific Time Content Venue Day 0 (June 7) 3 p.m. Welcome reception Infinite Loop Day 1 (June 8) 8 a.m. Check-in Apple Park 10 a.m. Keynote Apple Park After Keynote Download stations open Apple Park 12 p.m. Lunch Apple Park 1 p.m. Platforms State of the Union Apple Park After Platforms State of the Union In-person labs Apple Park 4-6 p.m. Reception in the inner ring Apple Park Day 2 (June 9) 10 a.m. Developer session Steve Jobs Theater or Apple Developer Center Cupertino 11:30 a.m. Mixer Apple Developer Center Cupertino 8 p.m. Pre-show presentation and special screening of The Mandalorian and Grogu Steve Jobs Theater Day 0 私たちはDay 0の午前にSFO(サンフランシスコ国際空港)へ到着し、宿泊先のホテルに荷物を置いてからInfinite Loopへ向かいました。道中は初めてWaymoを利用しました。急カーブでもほとんど揺れずに走行するため、仮眠できるほど静かでした。 Infinite Loopに到着するとチェックイン待ちの長蛇の列がすでにできていました。並んでいる間のおもてなしとして配られたジェラートバーとお水で暑さをしのぎつつ待ちました。 2種類の味から選べたジェラートバー 入口前でセキュリティチェックを済ませると、参加者用のバッジとノベルティを受け取りました。バッジにはNFCタグが入っており、各イベントのチェックイン時にAppleのスタッフのiPhoneにバッジをかざすことになっています。ノベルティ恒例のピンバッジにはLil Finder Guyと創立50周年を象徴するデザインが含まれていました。入場時はAppleのスタッフの方々が歓声とともに出迎えてくださり、Day 0から盛り上がりを肌で感じました。 今年のノベルティバッグ 今年のピンバッジ、ステッカー、ボトル 参加者用バッジ ── お気に入りのピンバッジを添えて その後は終了時刻の夕方7時まで自由に過ごしました。軽食とドリンクをいただきながら各国の参加者との交流、Apple Design Awardsの受賞者とファイナリストとのトーク、Appleのスタッフによるモニュメント前の記念撮影などを楽しみました。 快晴に恵まれたこの日 モニュメント前で記念撮影 また、APACの参加者で集まって記念写真も撮りました。オフラインで参加すると、多くの方がApple Platformの開発者として関わっていることを実感できました。 Day 1 Keynote まずはKeynoteの会場に向かうため、Visitor Center前でチェックインを済ませてApple Parkに入場しました。入場まで横断歩道の信号待ちで列がゆっくり進む中、Caffè Macsのスタッフからいただいたドーナツを食べながら近くの方と「今年のWWDCは何を期待してますか?」と雑談しました。話しかけた方の多くがAIのトピックを気にされていました。 Visitor Center前のチェックインも長蛇の列 Caffè Macsからいただいたオレンジ味のドーナツ 会場に到着したのは8時半頃でした。Keynote開始時間の10時までは自由に散策し、参加者とお話を楽しみました。 KeynoteおよびPlatforms State of the Unionの会場 会場のスクリーンでは、オンラインで公開されているKeynoteの動画が再生されます。現地では動画の再生前にCraig Federighi氏がステージに登場し、挨拶がありました。 developer.apple.com 続いてTim Cook氏も登場し、このときの会場の盛り上がりは強烈でした。CEOとしてのKeynoteでの登壇は今年が最後です。参加者全員が立ち上がってiPhoneを掲げて撮影する光景を前に、Cook氏自身も「これまでこんなにiPhoneに囲まれたことはありません」と語っていました。 壇上に登場したTim Cook氏 Download stations Keynoteが終了すると、高速の有線ネットワークが提供されるDownload stationsに集まりました。ここでXcode 27 BetaやiOS 27 Betaのダウンロードを始めました。この日は晴天で気温も高かったため、パラソルの影に集まって休憩する参加者も多かったです。 有線ネットワークが用意されたDownload stations Platforms State of the Union developer.apple.com 手元にiOS 27 BetaをインストールしたiPhoneを触りながら、先のKeynoteの内容も踏まえてPlatforms State of the Unionを視聴しました。今回のSiri AIやCore AI、Foundation Modelsなどのアップデートには、共通した方向性があります。デバイスへ蓄積された写真やテキストなどのパーソナルなデータをもとに、AI体験を最適化することがAppleの狙いです。Newsroomで公開されたSiri AIの記事には、Apple Intelligenceのアーキテクチャ図が掲載されています。この図では、デバイスとアプリケーション(またはSiri AI)の間にユーザーコンテキストが示されています。Appleプラットフォームのアプリ開発者には、このコンテキストを活かしたAI活用が求められているのでしょう。 www.apple.com 視聴後しばらくすると、スクリーン上にさまざまなアプリのアイコンが表示されました。幸運なことに弊社のアプリが映し出されたタイミングで撮影できました。 どこかにある弊社アプリのアイコン。見つけられましたか? 現地視聴をより楽しむために 現地ならではの視聴の楽しみ方も、二人それぞれ工夫してみました。らぷは Rokid Glasses というスマートグラスのリアルタイム翻訳機能を使って視聴しました。体感1秒未満で翻訳結果が表示されたので和訳文も追いつつ快適に視聴できました。 Rokid Glassesのリアルタイム翻訳結果 イッセーは英語に自信がなかったため、セッションの視聴でAirPods Proのライブ翻訳機能を試しました。実際に使ってみると、登壇者の英語が純正の翻訳アプリ上に日本語テキストとして表示されるだけでなく、AirPodsからも日本語に翻訳された音声がリアルタイムで流れてきます。この機能のおかげで、英語に自信がなくてもセッションの内容を大まかに理解できました。イッセーとしては、画面上のテキスト翻訳はあえて見ず、登壇者の様子を見ながら日本語音声だけを聞くスタイルがとても快適でした。 ライブ翻訳機能の翻訳結果 私たち以外にもスマートグラスやAirPodsを使って視聴している参加者が多くいました。海外カンファレンスへ現地参加する際の言語の壁はこれまでもありましたが、こうした技術によって乗り越えやすくなったと感じました。 In-person labs Platforms State of the Unionが終了するとIn-person labsが始まり、各ラボで新しく発表されたAPIや日々の開発で困っていることを相談しに行きました。特に困ったことがなくても「まだ触れたことのないFrameworkを始めようと思うんだけど何から始めたら良いですか?」といった相談でも問題ありませんでした。気さくな雰囲気の中でいろいろ話せるので、来年現地に行かれる方はぜひ積極的に利用してみてください。 Inner ring reception その後夕方6時まではInner ring内で自由に過ごしました。Xcode 27 Betaで開発したり、初対面や既知の開発者との会話を楽しんだり、Appleの方に質問したりと、あっという間に時間は過ぎていきました。 Xcodeダウンロード中にAppleのWWDRの方にいただいたFoundation Models Frameworkのステッカー 虹のApple Stage前で撮影してもらったイッセー。青いシャツの方は各国のApple Storeのスタッフ Day 2 Developer session Developer sessionに参加するため、Apple Parkへ向かいチェックインを済ませました。今回のセッションはSteve Jobs Theaterで開催されました。ここは過去のWWDCでは立ち入ることができなかった特別な場所です。定員制のため、シアターに入れない場合はApple Developer Centerでの視聴でした。開始1時間前に到着したものの、すでに多くの参加者が列を作っていました。皆がこのシアターでの参加を待ち望んでいる熱気を感じました。 無事に入場でき、ガラス張りの円形の建物と緑豊かな造形美に圧倒されました。会場で軽い朝食をとった後、いよいよセッションがスタートしました。 Developer session前のSteve Jobs Theaterの様子 今回のセッションでは、Xcode 27の「Agentic Coding」やFoundation Modelsなどの新しいAIフレームワークが取り上げられました。実際のアプリ開発にどう組み込むかという実践に焦点を当てた内容です。その中でも、Evaluations Frameworkはどのように役立てられるのかイメージができていませんでした。セッションを通して、LLMの出力の品質を容易にテスト・評価できる点を理解でき、実際に触ってみる良いきっかけになりました。動画でも公開されているので、使い方のイメージが付いていない方の参考になります。ぜひチェックしてみてください。 developer.apple.com Developer sessionの様子 Mixer @Developer Center セッション終了後はDeveloper Centerへ移動し、「Mixer」というイベントに参加しました。会場にはさまざまなフードトラックが用意されており、昼食をとりながらコーディングをする人々の姿も見られました。また、特定のテーマごとにブースが設けられており、参加者同士で交流したり、Appleのエンジニアに直接質問したりできる貴重な場となっていました。さらに、Developer Center内ではGroup Labが開催されており、会場の内外いずれも非常に活気に満ちていました。 テーマごとに交流ができる場所 Mixerの会場図 Theater event 夜は、現地参加者の中で先着申し込みができた「Theater event」に参加しました。日中と同じくSteve Jobs Theaterで開催されるとのことで、再びあの空間に足を踏み入れることができました。 Theater eventでのSteve Jobs Theater 今回上映されたのは、今年公開されたスター・ウォーズ作品『The Mandalorian and Grogu』です。上映前にはスペシャルゲストによる対談が行われていました。その際、通路の透明なディスプレイに会話の文字起こしと手話がリアルタイムで表示されていました。Appleらしいアクセシビリティへの高い配慮に、とても驚かされました。スター・ウォーズにあまり精通していなかったため少し心配でしたが、思った以上に内容が分かりやすく、シーンによっては観客の歓声で盛り上がるなど、非常に楽しむことができました。 インタビュー中に字幕と手話が表示される透明なディスプレイ 併催コミュニティイベント らぷはCommunityKitが主催している2つのイベントに参加してきました。ひとつはPaul Hudson氏が開催した「What's new in iOS 27?」です。 luma.com WWDCでは、Day 1にAppleから全セッション動画とサンプルプロジェクトが公開されます。Hudson氏はSwiftUIやSwiftDataなどのサンプルプロジェクトを実際にビルドし、発生したエラーと修正方法を紹介してくださいました。すべてのサンプルプロジェクトが修正なしで期待どおりに動作するわけではないので、こうした知見を聞けるのは助かります。 会場の様子 もうひとつは「Swift Contributor Social」というイベントです。こちらはSwiftのコントリビューターと、それに興味がある人たちによる交流会です。まだコントリビューターではありませんが、どんな観点で活動しているのか聞いてみたくて参加しました。 luma.com 今回はstdlibのメンテナーでSpanの実装を担当された方にじっくりお話を聞くことができました。 developer.apple.com メモリ安全性への興味や今後の展望、コンパイラーチームが別途持っている構想と突き合わせた上での定期的な議論などを伺いました。Swift Forumsを眺めているだけでは得られない情報が多く、刺激的な時間でした。 ボウリング場のビリヤード台を囲んで自由に交流中 参加中、もしくは興味のあるWorking Groupのバッジ おすすめセッション集 優れたデザインのための原則 ZOZOTOWN iOSアプリの開発をしているつっきー( @tsuzuki817 )です。 今回のWWDCでは、Appleが考える優れたデザインの原則を扱うセッションがありました。デザインを単なる「見た目やふるまい」ではなく、「意図をもってものを作ること」と定義した上で、数々の重要な指針を解説しています。詳細は「 Principles of great design 」のセッションをご覧ください。 私が特に注目したのは、「主導権」「寛容さ」「柔軟性」という3つの原則のつながりです。ユーザーをあらかじめ決められた道に無理に誘導するのではなく、主導権を人々に委ね、自分のペースで探索させることの重要性が語られています。しかし、自由に探索できるようになると、ユーザーは誤って削除してしまったり意図しない操作をしてしまったりすることがあります。 そこで重要になるのが「寛容さ」です。操作を簡単に取り消せるようにし、大惨事を避けられるようにすることで、ユーザーに「いつでも回復できる」という自信を与え、安全な探索を支えることができます。さらに、ユーザーがアプリを使う状況(ランニング中や運転中など)や能力は人それぞれであるため、それらに適応する「柔軟性」を持たせることも強調されていました。 すべての人に合う単一のレイアウトを見つけるのは難しいため、ユーザー自身が好みに合わせて体験をカスタマイズできるようにすることが、最良の柔軟性です。セッションの最後では、これらの原則を正しく実行した自然な結果として、真の感情的なつながりである「喜び」が生まれると語られていました。これは製品に込めた「思いやりの総和」であるとのことです。自分がアプリを開発するうえで大事にしていることでもあり、同時に難しいと感じている点でもあります。 普段ZOZOTOWNの開発や個人開発をするなかで、ユーザーにどのような体験をさせたいのか迷うことが多くあります。このような原則に則って考えることで、よりよいプロダクトが作れそうだと期待しました。また、この考え方はZOZOTOWNのアプリ開発でぜひ意識していきたいです。さらにユーザーだけではなく、普段の仕事で起きるコミュニケーションにも活かせます。相手に選択肢を委ね、ミスを許容し、状況に柔軟に対応していきます。 Foundation Modelsフレームワークの新機能 ZOZOTOWN iOSアプリの開発をしているpe( @shumpei_nagata )です。 今年のWWDCでは、Foundation Modelsフレームワークの進化に興味を惹かれました。詳細は「 What's new in the Foundation Models framework 」で紹介されています。 このフレームワークは昨年登場し、アプリ上でオンデバイスの大規模言語モデルを扱えるようになりました。そして今年は画像入力(Vision)に対応しました。テキストだけだったプロンプトに Attachment(UIImage(...)) のように画像を差し込むと、その画像について回答が得られます。ガイド付き生成( @Generable )やツール呼び出しなどの既存APIもそのまま使えるので、これまで書いたコードを活かして画像対応に広げられます。 また、Foundation Modelsの導入に役立つ道具も増えてきました。生成AIは出力が確率論的に決まるので、生成結果の品質を確かめるのはなかなか骨が折れます。サービスの機能として安心して導入するには、品質を測定できる仕組みが必要です。そういった課題には Evaluations Framework が役立ちます。併せてAppleからオープンソースで公開された foundation-models-utilities も参考になります。コンテキスト超過を防ぐトランスクリプトの圧縮やSkills APIなど、Foundation Modelsをより便利に扱えるツールが揃っています。 このセッションが気になったのは、業務と個人開発のどちらでも視覚的な情報を多く取り扱うアプリを開発しているからです。進化したFoundation Modelsフレームワークを組み込むことで、アプリ上の体験を大幅に拡張できそうです。たとえば説明文の作成、属性の抽出、altテキストによるアクセシビリティの補完などが挙げられます。こうした処理が通信なし・無料で、しかもプライバシーを保ったまま端末の中で完結するのは非常に魅力的です。 とはいえ、既存のサービスへ導入するハードルは低くありません。Foundation Modelsは利用にiOS 26以降が必要で、今回の画像入力はiOS 27以降でないと動きません。その上、Apple Intelligence対応デバイスという条件もつきます。またZOZOTOWNはiOSアプリだけでなく、AndroidアプリやWebサイトなどさまざまなプラットフォームでお客様にご利用いただいています。OSや端末が限られる機能をそのまま主役に据えると、体験に大きな差が生じてしまうので、導入の見極めが難しい技術であるのも事実です。 それでも、アプリに導入するとどのような新しい体験を提供できるかとワクワクしました。技術的な好奇心もくすぐられるセッションでした! WWDC26 報告会 at LINEヤフー, ZOZO WWDC後の6月18日にLINEヤフーと合同で報告会を実施しました。 lycorptech-jp.connpass.com イッセーは「現地で盛り上がったWWDC26 Keynote」というタイトルで登壇しました。 speakerdeck.com 報告会では、実際に現地参加して見えたことを紹介しました。Keynoteで周囲のデベロッパーがどのようなテーマで盛り上がったのか、意外と反応が落ち着いていたテーマはどれか、思いもよらないサプライズは何だったのかを共有しています。一番印象的だったのは、事前に「これが目玉だろう」と考えていたトピックと、会場が実際に沸いたトピックのズレです。この差は公式の発表資料やニュース記事を後から追っても分かりません。世界中のデベロッパーがどのトピックに興味関心があったのかは、その場にいた人にしかわからない情報でした。 らぷはパネルディスカッションに参加し、印象的だったセッションや現地参加に必要な準備などを共有しました。先ほどのスマートグラスの活用や気になったセッションの話も取り上げています。「WWDCに現地参加するなら何をすべきですか?」という質問への回答は、他の参加者やAppleのスタッフの方と積極的にコミュニケーションすることです。これはLINEヤフーのパネラーの方と同じ意見でした。自分にはない考え方に触れたり、同じ考えをもつ同志に出会えたりできる機会です。ぜひ最大限に活用してください。 パネルディスカッションの様子 さいごに 今年のWWDCで感じた現地の雰囲気とおすすめセッション、報告会の様子をお届けしました。この記事を読んで現地での思い出を振り返ったり、新しいAPIの調査を始めて共有したりしていただけると嬉しいです。 ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。WEAR部iOSブロックの坂倉です。 WEARは先日、 SceneDelegate へ移行しました。ライフサイクルそのものに関わる変更のため、画面数の多い歴史あるアプリではなかなか骨が折れました。 この記事では、 WEARが直面した課題 、 課題を解決するために取ったアプローチ 、そして 移行中につまずいた箇所と対処 をご紹介します。 WEARのような大規模アプリが、影響範囲の広い SceneDelegate 対応をどのように進めたのか、その実例として参考になれば幸いです。 目次 はじめに 目次 SceneDelegate対応で直面した課題 iOS 27以降、未対応のアプリは起動できなくなる アプリのライフサイクルに関わる変更のため、すべての画面に影響する ライフサイクルイベントがAppDelegateとSceneDelegateに分かれて呼ばれる すべての画面で問題が出ないか、慎重に調査しなければならない 課題を解決するためのアプローチ アプローチ1. AppDelegateとSceneDelegateの役割を理解する AppDelegateの役割 SceneDelegateの役割 役割と違いのまとめ どの処理をSceneDelegateへ移すか プッシュ通知はAppDelegateに残す アプローチ2. AIを使って影響範囲を特定し、QA観点をまとめる 1. AIを使って影響範囲を把握する 2. AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す 3. AIにQA観点を書き出させる これから対応するならXcode 27の併用も検討を アプローチ3. リスクを抑えるため、最小限の変更にとどめる つまずいた箇所と対処 一番最初のウィンドウを取得するのは危ない 外部ソースからの起動判定はconnectionOptionsで行う さいごに SceneDelegate対応で直面した課題 WEARが直面した課題は大きく2つあります。「対応しないという選択肢がない」ことと、「対応がアプリの全画面に影響する」ことです。 iOS 27以降、未対応のアプリは起動できなくなる Appleは公式ドキュメント Transitioning to the UIKit scene-based life cycle で、 UIScene ライフサイクルの採用が必須になると明記しています。最新SDKでビルドするアプリが対象となり、iOS 27・iPadOS 27・Mac Catalyst 27・tvOS 27・visionOS 27以降、未対応のアプリは起動できなくなります。 つまり、対応しない限り最新SDKでのアップデートを続けられなくなります。 アプリのライフサイクルに関わる変更のため、すべての画面に影響する SceneDelegate に対応すると、これまで AppDelegate が受け取っていたUI関連のライフサイクルイベントは SceneDelegate が受け取るようになります。ライフサイクルはアプリの根幹であり、この変更はWEARが持つすべての画面に影響し得ます。 具体的には次のような難しさがありました。 ライフサイクルイベントがAppDelegateとSceneDelegateに分かれて呼ばれる まず大変だったのが、これまで AppDelegate だけに届いていたライフサイクルイベントが、 AppDelegate と SceneDelegate の2つに分かれて呼ばれるようになる点です。 AppDelegate 中心の頃、起動時のイベントは次の順番で、すべて AppDelegate に届いていました。 application(_:willFinishLaunchingWithOptions:) application(_:didFinishLaunchingWithOptions:) applicationDidBecomeActive(_:) 初期化処理も1つのクラスに集まっているため、実行順序を追うのは難しくありませんでした。 ところが SceneDelegate を採用すると、アプリ起動時の実行順序は次のように2つのクラスをまたぐ形になります。 application(_:willFinishLaunchingWithOptions:) (AppDelegate) application(_:didFinishLaunchingWithOptions:) (AppDelegate) scene(_:willConnectTo:options:) (SceneDelegate) sceneWillEnterForeground(_:) (SceneDelegate) sceneDidBecomeActive(_:) (SceneDelegate) 起動処理が2つのクラスに分散すると、「この初期化はあの処理より先に終わっているはず」という前提が崩れます。たとえば、 SceneDelegate へ移したUIのセットアップが AppDelegate 側の初期化に依存していると破綻します。 さらに、呼ばれるタイミング自体が変わるイベントもあります。 sceneWillEnterForeground(_:) は applicationWillEnterForeground(_:) と違い、アプリ未起動(Not Running)からの起動でも呼ばれます。フォアグラウンド復帰時の処理をそのまま移すと、起動時にも実行されてしまいます。 どの処理を、どちらのクラスの、どのイベントに置くべきか 。アプリ全体の初期化処理を1つずつ見極める必要がありました。 参考: About the app launch sequence すべての画面で問題が出ないか、慎重に調査しなければならない WEARには通常起動のほかに、URLスキーム・ユニバーサルリンク・プッシュ通知など複数の起動導線もあります。特定の条件が揃ったときだけ顕在化するリグレッションもあり得るため、すべての画面について「移行しても問題が出ないか」を1つずつ調査していく必要がありました。 画面数の多いWEARにとって、これは人力だけでやり切れる規模ではありません。 課題を解決するためのアプローチ これらの課題に対して、WEARでは次の3つのアプローチで移行を進めました。 AppDelegateとSceneDelegateの役割を理解する AIを使って影響範囲を特定し、QA観点をまとめる リスクを抑えるため、最小限の変更にとどめる アプローチ1. AppDelegateとSceneDelegateの役割を理解する まず取り組んだのは、Appleの公式ドキュメントを読み込み、 AppDelegate と SceneDelegate がそれぞれ何を担当しているのかを正確に理解することでした。 AppDelegateの役割 UIApplicationDelegate プロトコルに準拠する AppDelegate は、アプリ全体のライフサイクルイベントを管理します。 アプリレベルのイベント管理 :アプリの起動・終了、システムレベルのイベントなど、アプリプロセス全体のライフサイクルを担当する。 application(_:didFinishLaunchingWithOptions:) などを通じて、アプリの初期設定や起動準備をする 初期の環境設定 :アプリ全体のデータやリソースの初期設定、プッシュ通知の処理などを担当する シングルトン :1つのアプリにつき、 UIApplication は1つしか存在しない 注意したいのは、 SceneDelegate に対応すると、UIに関するライフサイクルイベントは SceneDelegate の担当に移り、 AppDelegate には届かなくなるという点です。 SceneDelegateの役割 UIWindowSceneDelegate プロトコルに準拠する SceneDelegate は、「シーン」のライフサイクルイベントに応答します。シーンとは画面に表示されるアプリのUIのひとまとまりのことで、たとえばiPadで同じアプリを2つのウィンドウで並べて開くと、それぞれが独立したシーンになります。 シーンの管理 :アプリのUIを「シーン」として表現し、それぞれのシーンが独自のライフサイクルと状態を持てる。これにより、複数のウィンドウを並べて表示するマルチウィンドウ環境が可能になる シーン固有のライフサイクルイベント :これまで AppDelegate が扱っていたUI関連のライフサイクルイベントを SceneDelegate が担当する 役割と違いのまとめ 特徴 AppDelegate SceneDelegate 責任範囲 アプリ全体のプロセスとライフサイクル(起動・終了・システムレベルイベント)を管理 シーンのライフサイクルを管理 UIの管理 従来はアプリで唯一のUIとウィンドウを管理。iOS 13以降はUI管理の責任が軽減された 各シーンのUIのセットアップ・ウィンドウ管理・UIライフサイクルイベントを処理 マルチウィンドウサポート 単一のウィンドウのみ 複数のウィンドウをサポート 要するに、 AppDelegate はアプリの「プロセス」レベルの管理を担い続けます。一方の SceneDelegate は、シーンごとのUIの管理と、ライフサイクルイベントへの応答を担当します。 どの処理をSceneDelegateへ移すか 役割が分かれば、対応方針は明確です。 AppDelegate で処理していた次の処理を SceneDelegate に移行しました。 説明 移行前(AppDelegate) 移行後(SceneDelegate) 具体例 アプリ起動時のWindow生成処理 application(_:didFinishLaunchingWithOptions:) scene(_:willConnectTo:options:) Windowの生成、起動に必要なデータ取得、GA送信のための設定、URLスキームの受け取り シーンがアクティブになったときの処理 applicationDidBecomeActive(_:) sceneDidBecomeActive(_:) プッシュ通知バッジのリセット シーンが非アクティブになる直前の処理 applicationWillResignActive(_:) sceneWillResignActive(_:) — シーンがフォアグラウンドに入る時の処理 applicationWillEnterForeground(_:) sceneWillEnterForeground(_:) 必要に応じたリロードとAPI実行 シーンがバックグラウンドに入る時の処理 applicationDidEnterBackground(_:) sceneDidEnterBackground(_:) バックグラウンド移行時のUI後処理 ユニバーサルリンク処理 application(_:continue:restorationHandler:) scene(_:continue:) ユニバーサルリンクの受け取り URLスキーム処理 application(_:open:options:) scene(_:openURLContexts:) URLスキームの受け取り プッシュ通知はAppDelegateに残す ここで1つ注意点です。 プッシュ通知はアプリ全体に関わるイベントなので、引き続き AppDelegate で処理します 。 Appleのドキュメント にもある通り、デバイストークンの登録や通知の受信といったプッシュ通知のハンドリングには、 SceneDelegate 側に対応する移行先が用意されていません。 なお、通知への応答がシーンと無関係というわけではありません。通知へのユーザーの応答(タップなどの操作)をシステムがどのシーンに反映するかを示す UNNotificationResponse.targetScene というプロパティが用意されています。通知タップ後の処理でシーンが必要な場合は、このプロパティから取得できます。 アプローチ2. AIを使って影響範囲を特定し、QA観点をまとめる 13年の歴史を持ち、300個ものViewControllerを抱えるWEARにとって、影響範囲の調査は人力だけでやり切れる規模ではありません。そこで、Claude CodeやGitHub CopilotなどのAIをフル活用し、3つのステップを踏んで対応を進めました。 AIを使って影響範囲を把握する AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す AIにQA観点を書き出させる 1. AIを使って影響範囲を把握する 先ほど触れた どの処理をSceneDelegateへ移すか の表をAIに読み込ませ、影響範囲を特定しました。また、具体的にどんな処理が入っているのかも表にさせたことで、必要な対応の解像度を上げました。 2. AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す どの処理をSceneDelegateへ移すか で洗い出した対象メソッドは、もれなくSceneDelegateへ移す必要があります。しかし、一度にやってしまうと対応漏れが出かねません。AIの出力の質やスピードを上げる意味でも、範囲を絞って1個ずつ移すのが良いと判断しました。 3. AIにQA観点を書き出させる ここが一番助かった部分です。今まで、QAに提出する観点は人の手で書いていました。これを機に、その大半をAIが書く形へ変えました。 具体的な手順は次のとおりです。 まず、AIにSceneDelegate対応をさせてプロトタイプを作り、それをベースに全体の影響範囲とQA観点をざっと把握する メソッド移行ごとにブランチを切り、PR作成時、Diffの内容からJiraへQA観点を出力する プロトタイプで書き出したQA観点とブランチごとのQA観点を見比べて不足がないか確認する こうすることで、人間が気づかなかった範囲も調べて出力してくれるので、質の良いQAにつながりました。 具体的には、特定の地域からアクセスしたときだけ表示する利用規約Windowや、メンテナンス画面の表示・非表示のように複数のライフサイクルイベントにまたがって初めて成立する処理などが対象でした。普段意識する機会が少ない処理ほど、人力でQA観点を洗い出す際には抜け落ちがちです。AIに列挙させたことで、こうした処理も取りこぼさずQA観点に反映できました。 これから対応するならXcode 27の併用も検討を WEARのSceneDelegate対応は2025年に行ったため、Xcode 27を使うことはできませんでした。これから対応する場合は、Xcode 27の併用も検討してみてください。 WWDC26のセッション「 Modernize your UIKit app 」では、Xcode 27同梱の「uikit-app-modernization」スキルが紹介されています。このスキルはSceneDelegate対応だけでなく、UIScreen.mainやinterfaceOrientationのモダナイズなどにも対応しています。今進めているAdaptiveレイアウト対応でも役立っています。 また、Xcode 27は、Appleの公式ドキュメントやテックノートはもちろんのこと、Apple Developer Forumsの情報も参照してくれるため、非常に便利です。正式リリースが待ち遠しいですね。 アプローチ3. リスクを抑えるため、最小限の変更にとどめる 影響範囲の広さに対するもう1つの答えが、移行のスコープを必要最低限にとどめることです。WEARでは以下の方針で移行を進めました。 シーンのライフサイクルにのみ対応する 処理の移行を最優先し、処理のリファクタリングは後で行う マルチウィンドウの対応は一旦見送る SceneDelegate に対応すれば、iPadでのマルチウィンドウなど、できることは増えます。しかし、リスクを最小限に抑えるため、まずシーンのライフサイクル対応だけを完結させると決めました。 また、WEARの AppDelegate はObjective-Cで書かれた非常に古いコードだったため、本音を言えばリファクタリングもしたいところでした。今回、 SceneDelegate へ移行した処理に限りSwiftへの置き換えを行いましたが、処理そのものには手を加えず、挙動を変えない必要最小限の修正にとどめています。 つまずいた箇所と対処 最後に、移行中に直面した代表的な不具合と、その対処を紹介します。 一番最初のウィンドウを取得するのは危ない SceneDelegate 対応後は、 UIWindow が各シーン( UIWindowScene )に紐づき、シーンごとの持ち物へと変わります。 本来は良くない実装ですが、WEARはこれまでSceneDelegateに対応していなかったため、以下のコードでWindowを取得しても問題になっていませんでした。 extension UIApplication { var firstKeyWindow : UIWindow? { let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene return windowScene?.windows.first( where : \.isKeyWindow) } } しかし、SceneDelegate移行後はマルチウィンドウが可能になるため、最初に取得したシーンのWindowが、いま操作しているWindowとは限らなくなります。 よって、ViewControllerからWindowを取得する場合は、 view.window を使って「いま表示しているシーンのWindow」を取得する形へ修正しました。ただし view.window は、Viewがウィンドウ階層に追加されるまでは nil のままなので、呼び出すタイミングには注意が必要です。 // Before: アプリ全体のkeyWindowに依存していた let rootVC = UIApplication.shared.firstKeyWindow?.rootViewController as? RootViewController // After: 自分が表示されているシーンのWindowから取得する let rootVC = view.window?.rootViewController as? RootViewController 外部ソースからの起動判定はconnectionOptionsで行う これまでは AppDelegate の launchOptions のキーで「外部から開かれた起動か」を判定し、ダイアログ表示などを制御していました。 SceneDelegate では、この起動情報は scene(_:willConnectTo:options:) に渡される connectionOptions へ集約されています。型は UIScene.ConnectionOptions です。 これを参照することで、URLスキームやユニバーサルリンク、プッシュ通知など、「どこで起動されたのか」を判別できます。 WEARでは次のように判定しています。 userActivities や urlContexts からディープリンクを取り出して種別を判定します。それらがなくリモート通知での起動であれば .remoteNotification を、いずれにも当てはまらない通常起動なら nil を返します。 extension SceneDelegate { enum ExternalSource { case urlScheme case universalLink case remoteNotification } } extension SceneDelegate.ExternalSource { @MainActor init? (_ connectionOptions : UIScene.ConnectionOptions ) { let userActivityURL = connectionOptions.userActivities.first { $0 .activityType == NSUserActivityTypeBrowsingWeb }?.webpageURL let urlSchemeURL = connectionOptions.urlContexts.first?.url if let deepLinkURL = userActivityURL ?? urlSchemeURL { switch deepLinkURL.scheme { case "URLスキーム名" : self = .urlScheme case "https" : self = .universalLink default : return nil } return } if connectionOptions.notificationResponse != nil { self = .remoteNotification return } return nil } } func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { let externalSource = SceneDelegate.ExternalSource(connectionOptions) } さいごに 「技術調査で役割を正確に理解する」「AIで影響範囲を特定する」「最小限の変更にとどめる」という3つのアプローチにより、 SceneDelegate 対応を無事完了できました。 なかなか骨が折れましたが、 SceneDelegate に対応したことでマルチウィンドウに対応する土台ができ、AIエージェントを使って大規模な修正に取り組む経験も得られました。 この記事が、 SceneDelegate 対応を控えている方の一助になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは。Developer Engagementブロックの @wiroha です。2026年7月10日、11日に「 SRE NEXT 2026 」が開催されました。ZOZOはSilverスポンサーとして初協賛し、スポンサーブースを出展しました。 本記事ではZOZOから登壇したセッションと気になったセッションの紹介、協賛ブースの様子についてお伝えします! ZOZOエンジニアの登壇セッション ZOZOTOWNの進化と信頼性を両立する負荷試験 — 年十数回、現場の課題と効率化(LT) speakerdeck.com ZOZOTOWNの会員ID基盤では、年十数回の負荷試験を実施しています。新機能の追加やリプレイスなどプロダクトの進化が続く中で、信頼性をどう担保し続けるか — その要が負荷試験の運用です。しかし実施要否の検討・シナリオ作成・mock準備・想定RPSの見積もりなど、試験前に積み重なる判断と調整が、見えないコミュニケーションコストを生み、運用を試験を回すだけで精一杯の状態にしていました。 今回のLTでは、要否判定の属人化解消・シナリオ資産の使い回し・分析工程の仕組み化・結果のテンプレート化など、現場で有効だった運用改善と、これから取り組みたい効率化の方向性を共有しました。 登壇者からのコメント SRE部 会員ID基盤SREブロックの松石です。今回初めてのカンファレンス登壇となりました。このセッションでは、弊社で頻繁に行っているマイクロサービス負荷試験とその課題、改善について発表しました。 負荷試験は、ECサイトのようにスパイクが頻繁に起こりえるサービスでは重要な試験観点となってきます。一方で、マイクロサービスの依存が増えることやサービス要件により、観点や事前準備が日々複雑になっており試験を行う際の工数は無視できない規模になってきました。今回の発表で、新しい気づきや弊社の取り組みへの興味を持ってくださった方がいたら嬉しく思います。 また、発表を行うことで、ブースや懇親会で負荷試験について興味を持ってくださった方、似た悩みを抱える方と密な意見交換をできました。今回得た知見などは、チーム内でも展開し、サービスの信頼性向上に努めたいと考えています。 良く情報を発信する人間のところに情報は集まってくると言われますが、そのことを体感した2日間で非常に有意義な時間でした。 エンジニアが気になったセッションの紹介 サンプリングは統計学である:数理的根拠に基づき、オブザーバビリティのコストと精度を両立する 山口 能迪 speakerdeck.com SRE部 カート決済SREブロックの北島です。4月に新卒入社して、少しずつ業務に慣れてきました。リプレイスプロジェクトにサブとして参加しながら、Datadogのダッシュボードやトレースを確認したりという日々です。各マイクロサービスのデータがDatadog上で可視化されていますが、「これは本当に正しいものなのか」とどこか引っかかっていました。 このセッションでは、「全てのlogのデータを保持するにはコストがかかるため、サンプリングする。けれど『データを捨てる』という行為なので、捨てていい場合と捨ててはいけない場合がある。その判断基準が統計学にある」という内容が語られました。 具体的には、以下の4点が軸になっていました。 Datadogに表示されるメトリクスが推定値になりうること 推定値として読むためには許容誤差を先に決めてサンプリング率を逆算する必要があること 多段構成だと保持率が積み重なって補正が難しくなること メトリクスとトレースはそもそも責務が違うので分けて考えるべきこと この日のセッションで、自分の引っかかりの正体が少しわかった気がしました。 「Datadogに表示される数字は事実ではなく推定値かもしれない。この数値は本当に信頼できるのか?」を問えるSREになりたいと思いました。 数字を見るたびに「なぜ」を繰り返して、何か起きたときに根拠を持って動けるよう意識していきたいと改めて思いました。 CSに"SLO"は要らない、経営層に"99.9%"は伝わらない - SREを全社に"翻訳"する3原則 川崎 雄太 speakerdeck.com SRE部 会員ID基盤SREブロックの田中です。私が所属するチームでは現在SLOの策定を進めており、その目的・対象・運用方法について議論を重ねています。 その中で、SLOの設定や運用改善はSREだけで完結するものではなく、開発チームやプロダクトマネージャーをはじめとするさまざまな部署と連携し、共通認識を形成する必要があるため、その難しさを実感していました。 本セッションでは、組織にSREを浸透させる際に直面する課題を、1. 専門用語の壁、2. 温度差の壁、3. 完璧主義の壁の3つに整理し、それぞれを「翻訳レイヤー設計」「ビジネスインパクトへの変換」「役割別Include設計」というアプローチで乗り越えた事例が紹介されました。 特に印象に残ったのは、専門用語や指標を部署ごとの視点に合わせて再定義したり、それぞれの立場で必要となる情報に合わせてSREやSLOの価値を伝えたりする工夫です。技術的な正しさだけでなく、相手に伝わる形へ翻訳することが、組織全体でサービス信頼性を向上させるために重要であることを改めて認識しました。 SLOの策定に取り組んでいる現在の私たちのチームにとっても、多様なステークホルダーとの認識合わせはまさに直面している課題です。本セッションを通じて、SLOの運用に限らず、サービス信頼性向上を組織全体で推進するための具体的な考え方やアプローチを学ぶことができ、非常に参考になる内容でした。今後、より多くの部署と連携しながら活動を進めていく上で、ぜひ実践していきたいと感じました。 分散システム、なんですぐ死んでしまうん?耐障害性を高めたいあなたのためのレジリエンスパターン入門 ZOZOMO部SREブロックの蔭山です。私からは1日目の『分散システム、なんですぐ死んでしまうん?耐障害性を高めたいあなたのためのレジリエンスパターン入門』を紹介します。 speakerdeck.com 本セッションでは分散システムにおけるレジリエンスパターンについて、実際の取組事例を交えながら紹介されました。 レジリエンスパターンの手法についてTimeoutやRetry、Request Hedging、Circuit Breaker、Load Shedding、Graceful Degradationが解説され、それを実現するService Meshとライブラリそれぞれでの実装方法、ケーススタディとしてメルカリでのレジリエンス実践の実例も紹介されました。 私たちが担当しているシステムでは分散システムを扱うことが多く、TimeoutやRetryなどは普段の実装から意識しているものもありました。それでもTimeout PropagationやRetry Budgetなどは初めて知った概念でした。特にRetry Budgetは、Retryの回数を制御することで、システム全体の負荷を抑えつつ、サービスの可用性を高めることができるという点で非常に興味深かったです。 また導入を諦めていたService Meshの導入やCircuit Breakerの導入も、今回の実際にサービスを守れた事例を聞いて改めて検討する価値があると感じました。私が担当しているシステムでは、まだまだレジリエンスパターンの導入が十分ではないため、今回のセッションで学んだことを活かして、システムの耐障害性を高める取り組みを進めていきたいと思います。 ポストモーテム!DDoSからサイトは守れた。でもビジネスは守れなかった。 speakerdeck.com ZOZOMO部SREブロックの𠮷富です。本セッションでは、弁護士ドットコムが実際に直面したDDoS攻撃の事例が紹介されました。 同社は継続的なDDoS攻撃を受けましたが、WAFによる防御や関係部署の連携によってサービスのダウンタイムは発生せず、「可用性」を守り切ることができました。しかしその一方で、攻撃による大量のリクエストによりインフラコストが平常時の約40倍まで膨れ上がり、「事業」という観点では大きな損失が発生していました。 この事例で興味深かったのは、コスト急増への初動が遅れた原因です。日次のコスト急増アラートが、偶然にも毎月送られてくる予算アラートと似た金額だったため、担当者は「いつもの月末アラートだろう」と思い込み、異常に気が付きませんでした。また、毎月鳴る予算超過アラートが常態化し、形骸化していたことも見逃しにつながっていました。これは個人の不注意ではなく、人が見逃してしまうことを前提としたアラート設計になっていなかったことが根本原因でした。 また、エンジニアが「可用性」を優先しがちであるという課題も挙げられていました。システムを止めないことを重視する一方で、クラウドでは従量課金そのものが攻撃対象になり得るため、サービスを維持できていても事業としては大きな損失につながる可能性があります。そのため、「システムが動いているから大丈夫」とは言えず、コストも含めてサービス全体の健全性を捉える視点が必要であることを学びました。 このセッションを通して、SREは技術的な課題を解決するだけではなく、事業を守るためのコミュニケーションを担う存在でもあることを改めて認識しました。「どの時点でサイトを停止するのか」「誰が判断するのか」といった基準を、平時から関係者間で共有しておく必要があります。また、技術的な事象をコストやリスクといった経営の言葉に翻訳することが、迅速で適切な意思決定につながります。技術だけでなく、事業全体を見据えた視点を持つことの重要性を強く感じたセッションでした。 なぜ私たちのSREプラクティスはなかなか機能しないのか 〜システムより先に組織を見る〜 speakerdeck.com ZOZOMO部 SREブロックの中村です。私の所属するチームはSREとして立ち上がって約2年ほどです。 SREとは何か、SREとしてのベストプラクティスは何かについて輪読会を行いながら、チームとして日々正解を探し続けています。 オンコール体制整備、SLOの設定、オブザーバビリティの強化改善、SREとしてどのように価値や存在感を出していくのかを考え、実行してみるのですが、思った効果が得られなかったりする場合があります。 今までチームでやっていなかったSREとしての役割を果たそうとすると、あまりうまくいかず、とても悩んでいました。 このセッションでは、他組織の事例や書籍の内容通りにSREプラクティスを実践してもうまくいかないと感じている人向けに、なぜシステムより先に組織を見るべきなのか、組織を見るときに何をチェックすればよいのかを、事例とともに紹介されていました。 セッションの冒頭では、SREプラクティスを行ったときに起こる問題について話されていました。ポストモーテムを導入したりモニタリングを強化したりしても、SRE以外がポストモーテムを書かない、アラートやダッシュボードを作成してもメンテナンスをSREだけが行う状況が起こりえます。 その原因としては、SREが整備してもSRE以外に扱い方が浸透していなければ、そのまま誰も意識しないようになりますし、そのためにドキュメントを用意しても誰も読みたがらず、そのまま風化していきます。 このような問題は、SREプラクティスをやりたいという気持ちだけで進めてしまうことで起こると、セッションを通して認識できました。 SREプラクティスを機能させたいのであれば、自組織の文化や価値観がすでに形成されていることを前提に、それをしっかり守るための手段としてSREプラクティスを設計する必要がある、という話にとても納得感を得られました。また、SREチーム内だけで完結する内容はSREプラクティスとして成果が見えやすい一方、うまくいかないと感じるときはSREチーム外の関係者が必要な領域に踏み込んでしまっている、という話がありました。 今後SREとして何か実践するときには、ただシステムを見るだけでなく、「組織の文化はどうなっているのか」「それを守るためには何が足りていないのか」「足りていないものを補うには何を追加すべきか」「追加する際の影響はどの範囲まで及ぶのか」を意識したうえで、SREチームとしてたくさんチャレンジしていきたいと思いました。 誰のためのReliability? SRE部カート決済SREブロックの伊藤です。2日目の基調講演『誰のためのReliability?』を紹介します。 speakerdeck.com この講演はデザインリサーチの専門家であるアンカーデザイン株式会社の木浦幹雄さんが「SREの言うユーザーとは誰なのか」を正面から問うものでした。 デザインの世界では、ユーザーを属性だけでなく「属性・文脈・目的」の組み合わせで捉え、それを「ペルソナ」としてチームの共通理解にするそうです。一方で木浦さんいわく、SREの本を何冊読んでもペルソナという言葉は出てこなかったとのことでした。SREは「ユーザー」という漠然とした言葉のまま、誰がどんな状況で何のためにサービスを使っているのかを具体化できていないのではないか、という問いかけが印象に残りました。 私が担当しているZOZOTOWNのカート決済機能は、止まればユーザーの購入がそのまま止まってしまう、ECサイトの根幹となる機能です。セールやクーポン期限前の駆け込みなど、注文が集中するタイミングはデータとして把握しています。ただそれを「負荷の波」として見るだけでは足りず、「期限内にクーポンを使い切りたい人」「セール開始と同時に狙った商品を確保したい人」の活動として捉え直すと、見えるものが変わります。同じ稼働率99.9%でも、平日の昼間と、「今買えないと機会を逃す」人が殺到するセール終了間際とでは、下回ったときにユーザーの活動へ与える影響は大きく違うはずです。 こうした重みの違いまで含めて可視化し、SLOの根拠として語れるようになれば、「誰のためのSLOか」に答えられるようになるのではないかと思いました。 もう1つ考えさせられたのは、この問いはサービスの利用者だけでなく、一緒に働く開発者にも向けるべきだ、ということでした。SREが用意する監視の仕組みや運用ルールは、開発者にとってのプロダクトでもあります。その提案は開発者が望むものと本当に合っているのか。どんな状況で何のために使われるのかを確かめずに、良かれと思って作っていないか。「ユーザー像は、妄想で決めない」というデザインの教訓は、社内の開発者に対しても当てはまるはずです。「誰のためか」を問い続けることが、SREの仕事をより確かなものにするのだと気づかされた講演でした。 End-to-Endで考える信頼性 — LINEアプリにおけるクライアント開発×SRE連携の実践 speakerdeck.com SRE部 会員ID基盤SREブロックの中根です。私の所属ブロックでは、ZOZOTOWNの会員情報やログインに関する機能のSRE業務を担当しています。機能単体の信頼性向上には深く取り組めている一方で、End-to-Endでの信頼性は見えづらいという課題感があります。 こちらのセッションは、障害発生時にユーザー操作へ影響を出さないための設計と運用について、End-to-Endでの取り組みを解説する内容でした。ユーザーが操作した際に「画面が真っ白な状態」を絶対に避けるべき状態として定義し、オフラインファーストで吸収する取り組みが印象的でした。 クライアントでのキャッシュの取り扱いとして指数バックオフによるリトライ制御や部分障害時の選択的リトライ、負荷過多時の優先制御、クライアント視点のログの継続的な分析など、過負荷を避けつつ体験を守る工夫が具体的に紹介されていて学びが多いセッションでした。 また、そういった対応を実践するためには各チームが横断的に取り組むことが何より重要である点や、その文化を醸成するには時間を要する点が紹介されていました。 アプリケーションとバックエンドは組織も要素技術も異なる場合が多く、関連ブロックが増えるほど横断的な連携は難しくなりがちです。 その中で、早い段階からサーバー・クライアント・SRE・QAが一丸となり、仕様やエラーケースを議論する取り組み方は大変参考になりました。私も組織や機能にとらわれず、お客様のより良い体験のために機能や部門の垣根を越えた改善が行えるように取り組んでいきたいです。 ZOZOブースの紹介 LTでの発表内容にあわせて、スポンサーブースでは負荷試験の構成図を中心に展示しました。 「負荷試験に取り組みたいがまだ実践できていないため参考にしたい」といった参加者の方々にも多くお立ち寄りいただき、設計や運用に関する質問など活発な意見交換が行われました。また、初めての協賛ということもあり、ZOZOについて知っていただけるようZOZOTOWNのシステム構成図もご用意しました。こちらも注目を集めていました。 また、ノベルティとして「シューズ用クリーナー消しゴム」を配布しました。スニーカー等のソールの汚れをこすって落とせる便利アイテムです。 「こんなグッズがあるんだ!」「便利!」と好評いただき、ファッションを軸に事業を展開するZOZOらしさも感じていただけたのではないかと思います。 おわりに SRE NEXTへの初協賛を通して、ZOZOのことが少しでも来場者のみなさまに伝わっていれば嬉しいです。ありがとうございました! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、SRE部 フロントSREブロックの秋田と池田です。普段はリプレイスプロジェクトをはじめ、BFF・WebアプリケーションやオンプレのWebサーバーなど、ユーザー体験に関わるシステムの運用保守に携わっています。 ZOZOTOWNでは長年、オンプレミスのDNSアプライアンスとして Infoblox を利用してきました。しかし、7年間運用してきたInfobloxがEOL(End of Life)を迎えることとなり、リプレイスを検討することになりました。 本記事では、Infobloxのリプレイス検討から廃止に至るまでの課題をご紹介します。あわせて、Akamai Edge DNSのPrimary昇格・Akamai SIA導入・TerraformによるIaC化という3つのアプローチで課題を解決した取り組みについても触れます。 目次 はじめに 目次 既存のDNS構成と課題 既存構成 課題1:コスト面 - ライセンス費用の高騰 課題2:運用面 - ライセンス更新・脆弱性対応・バージョンアップ 課題3:管理面 - ゾーン管理の非コード化 課題4:チーム面 - 運用の属人化 課題解決のアプローチ 既存構成とAkamai Edge DNS Primary昇格の比較 施策1:Akamai Edge DNS - Secondary → Primary昇格 施策2:Akamai SIA導入 - キャッシュDNSの置き換え PoC(概念実証)の実施 DNSレベルの脅威検知によるセキュリティ強化 施策3:Terraform化 - ゾーン管理のIaC移行 稼働中ゾーンのTerraform import GitHub ActionsによるCI/CDパイプラインの整備 手順書レビューからPRベースレビューへの転換 Infoblox廃止に向けたクローズ作業 まとめ 最後に 既存のDNS構成と課題 既存構成 リプレイス検討前のDNS構成は以下の通りです。 権威DNS(Primary) : Infoblox(オンプレミス) 権威DNS(Secondary) : Akamai Edge DNS キャッシュDNS : Infoblox(オンプレミス) ZOZOTOWNではInfobloxをHidden Primary(NSレコードに公開しない権威DNS)として長年利用していました。AkamaiはInfobloxからゾーン転送を受けるSecondaryの役割を担いながら、インターネット側に公開されているためユーザーからの名前解決にも応答していました。オンプレのActive Directoryで使われるキャッシュDNSはInfobloxを利用していました。 この構成はInfobloxを導入した当初は問題なく運用していましたが、7年間の運用をする中で複数の課題が浮き彫りになってきました。 課題1:コスト面 - ライセンス費用の高騰 昨今の急激な円安の影響もあり、ライセンス更新のたびに費用が大きく膨らんでいました。さらにEOLに伴う機器リプレイスとなれば、ハードウェアの調達・入替コストも新たに発生します。 同等のInfoblox機器へリプレイスするという選択肢では、こうしたコスト面の課題がそのまま継続するため、根本的な解決にはなりませんでした。 課題2:運用面 - ライセンス更新・脆弱性対応・バージョンアップ Infobloxの運用では、以下のような定期作業が繰り返し発生していました。 ライセンス更新対応 : 年次でのライセンス更新作業とライセンス費用管理 脆弱性対応 : InfobloxはBINDベースであり、BINDの脆弱性が発見されるたびに対応が必要 バージョンアップ対応 : セキュリティパッチやバグフィックスのためのアップデート作業 どれも定期的に発生する作業で、特にBINDベースゆえの脆弱性対応は予告なく緊急対応が必要になるケースもあり、計画外の工数が積み重なっていました。 課題3:管理面 - ゾーン管理の非コード化 InfobloxのゾーンはInfobloxの管理コンソールを通じた手作業での運用となっており、設定変更の経緯が社内の申請と管理コンソールのコメントでしか追えない状態でした。具体的には以下のような問題がありました。 変更履歴が社内申請と管理コンソールのコメントのみで、いつ・誰が・なぜ変更したかを追うことが困難 手作業でのレコード追加・変更による作業ミスのリスク 手順書ベースの作業かつ手順書レビューの作業準備コスト ゾーン設定の現状把握がInfobloxにアクセスしないとできない IaC(Infrastructure as Code)が当たり前になった環境において、DNSゾーン管理だけが手作業のまま取り残されている状態でした。 課題4:チーム面 - 運用の属人化 ZOZOTOWNのマイクロサービス化に合わせてチーム体制の最適化が進む中、Infobloxの運用もフロントSREブロックで担当するようになりました。しかし、チーム内でInfobloxの運用経験を持つメンバーは1人だけという状態が続いており、運用の属人化が課題となっていました。 属人化が進むと、不在時に対応できるメンバーを確保できなくなるリスクや、人員変動に伴う引き継ぎコストの増大といった問題を招きます。チームとして持続可能な運用体制を整えるために、属人化の解消が急務となっていました。 課題解決のアプローチ 既存構成とAkamai Edge DNS Primary昇格の比較 リプレイスの選択肢として、まず「同等のInfoblox機器へのリプレイス」と「Akamai Edge DNSをPrimaryに昇格してInfobloxを廃止する」の2案を比較検討しました。 比較の結果、Akamai Edge DNS Primary昇格案は以下の点で優位でした。 観点 Infobloxリプレイス Akamai Edge DNS Primary昇格 ライセンスコスト 継続(高騰リスクあり) 不要(既存Akamaiの活用) ハードウェアコスト 機器調達が必要 不要 脆弱性対応 BINDベースの定期的な対応が継続 Akamaiのマネージドサービスに委譲 ゾーン管理 手作業継続 Terraform化で解消 キャッシュDNS Infobloxのまま Akamai SIAで代替(別途費用が発生) Akamaiは既にSecondaryとして動いており、ゾーンデータはそのまま引き継げました。Akamai SIAの利用には新たに費用が発生するものの、ライセンス・ハードウェア・運用コストの削減や運用品質の向上といった優位点が大きく上回りました。 これらの比較結果をもとに、 AkamaiをPrimaryに昇格してInfobloxを廃止する 方針を決定し、以下3つの施策を実施しました。 施策1:Akamai Edge DNS - Secondary → Primary昇格 まず、Akamai Edge DNSをSecondaryからPrimaryに昇格させ、Infobloxからのゾーン転送をなくしました。 Infoblox(Primary)→ Akamai(Secondary)という構成から、Akamai単体を権威DNSのPrimaryとする構成への移行です。 移行にあたっての主な作業は以下の通りです。 AkamaiのゾーンをSecondaryからPrimaryへ変更 Secondaryとして利用していたため実際にゾーン情報を持っていることもあり、AkamaiのコンソールからSecondaryをPrimaryに切り替える作業のみで済みました。また、切り戻しの際もPrimaryからSecondaryに戻してゾーン転送の設定を再設定するだけで済むため、作業工数をかけずに移行が実現できました。 施策2:Akamai SIA導入 - キャッシュDNSの置き換え InfobloxのCache DNS機能を Akamai SIA(Secure Internet Access) で代替しました。 Akamai SIAはDNSフォワーダーとして機能するだけでなく、 DNSレベルでの脅威検知・ブロック 機能も提供します。従来のInfobloxのキャッシュDNSにはなかった機能で、セキュリティ面での強化も同時に実現しました。 PoC(概念実証)の実施 本番環境への適用前に、STG(ステージング)環境で3週間のPoCを実施しました。 PoCの確認項目は以下の通りです。 Akamai SIAのアクセス制限 名前解決の動作確認(社内ドメイン・外部ドメイン) 既存のDNS依存アプリケーションへの影響確認 手動切り戻し手順の確認 3週間のPoC期間中に問題は発生せず、本番環境への適用に移行しました。 DNSレベルの脅威検知によるセキュリティ強化 Akamai SIAでは、マルウェアのC2(Command and Control)サーバーやフィッシングサイトへのDNSクエリをブロックする機能を持っています。 Infobloxのキャッシュ置き換えに加えて、このセキュリティ機能を獲得できたことは想定外のプラスαでした。 施策3:Terraform化 - ゾーン管理のIaC移行 課題3として挙げたゾーン管理の手作業運用を解消するため、Terraformを用いてAkamaiのDNSゾーンをIaC管理する仕組みを整備しました。 稼働中ゾーンのTerraform import Akamaiには既にゾーンが存在しているため、影響範囲が小さいものから段階的に、既存ゾーンのTerraform importを始めました。 terraform import akamai_dns_zone.example example.com 全ゾーンを一括importするスクリプトを作成し、既存ゾーンをTerraform管理下に置きました。import後はtfstateと実際のゾーン設定の差分を確認し、コードと実態の一致を検証しました。 GitHub ActionsによるCI/CDパイプラインの整備 import完了後、以下のパイプラインをGitHub Actionsで整備しました。 PRを作成 → terraform plan → レビュー → マージ → terraform apply 各ステップの詳細は以下の通りです。 terraform plan(PR時) PRを作成するとGitHub Actionsが自動でterraform planを実行 planの結果はPRコメントへ出力され、レビュアーが変更内容を確認できる状態になる terraform apply(マージ時) mainブランチへのマージをトリガーにterraform applyを実行 Akamaiへの変更が自動で反映される 手順書レビューからPRベースレビューへの転換 Terraform化により、ゾーンの変更オペレーションが以下のように変わりました。 変更前 変更後 Infoblox GUIで手作業変更 tfファイルを編集してPRを作成 手順書をレビュー PRをレビュー 変更履歴が残らない Gitのコミット履歴として残る 変更の影響範囲が手順書に依存 planで変更内容が定量的に把握できる コードレビューを通じて変更の意図と影響範囲を確認できるようになり、ヒューマンエラーの削減と変更の透明性向上を同時に実現しました。 Infoblox廃止に向けたクローズ作業 施策1〜3が完了した後、Infobloxを安全に廃止するためのクローズ作業を実施しました。 Infobloxに依存しているクライアントが残っている状態で廃止してしまうと障害につながります。移行済みかどうかを地道に確認しながら、以下の順序で進めました。 キャッシュDNSの向き先変更 :各クライアントのDNSフォワーダー設定をInfobloxからAkamai SIAへ変更 ゾーン転送の停止確認 :Infoblox→Akamaiへのゾーン転送が停止していることを確認 権威DNS問い合わせ確認 :外部からの問い合わせがInfobloxを経由していないことを確認 監視の切り替え :Infoblox関連のモニタリング・アラートの削除 機器のシャットダウン・撤去 :一定期間の様子見後、機器の電源を落として廃止 クローズ後も一定期間はリバートできる状態を維持し、問題がないことを確認した上で機器を撤去しました。 まとめ 7年間運用してきたInfobloxの廃止とAkamaiへの移行を通じて、コスト・運用・管理・チームの4つの課題を同時に解決できました。 コスト削減 :Infobloxライセンス費用・保守サポート費用・EOLに伴うハードウェア調達費用が不要になりました 運用負荷の低減 :BINDベースの脆弱性対応やライセンス更新といった定期作業がなくなりました 管理品質の向上 :ゾーン変更がPRベースのレビュープロセスに変わり、変更履歴が残るようになりました セキュリティ強化 :Akamai SIAによるDNSレベルの脅威検知も獲得できました 属人化の解消 :SaaS移行によりInfoblox固有の知識がなくても運用できる体制となり、引き継ぎ負担を軽減できました EOLへの対応はどうしても後ろ向きな作業に見えがちですが、今回は「機器交換」で終わらせず、積み上がっていた課題を整理する機会として捉えられチーム状況を考えながら最適化できたことはとてもよかったです。同様の状況を抱えているチームの参考になれば幸いです。 最後に ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、コーポレートエンジニアリング部ITサービスブロックの高塩です。2026年2月に入社し、社内の共通システムやIdP、SaaSの管理、日々のオペレーション自動化などを担当しています。 ZOZOでは入社者のマスタデータをkintoneで管理しています。入社のたびに、Active Directory(以下、AD)を起点にMicrosoft 365・Google Workspace・Boxなどでアカウントを作成し、利用できる状態まで整えています。この作業は長らく手作業で運用してきましたが、工数や属人化、そしてCSVの受け渡しに課題を抱えていました。 オンプレADやLDAPサーバーを長く運用してきた組織では、それが自動化の足枷になっているケースも少なくないと思います。かといって、すべてをクラウドへすぐに寄せられるとも限りません。本記事では、この一連のアカウント作成を、kintoneを起点にGitHub Actions(self-hosted runner)で自動化した取り組みを紹介します。閉域にあるオンプレADの操作までGitHub Actionsに載せ、コードレビュー・実行ログ・再現性といったCI/CDの利点をアカウント運用に持ち込みました。 目次 はじめに 目次 背景:手作業時代のフローと課題 全体アーキテクチャ オンプレADをGitHub Actionsから操作する kintoneから翌月の入社予定者を取得し、業務ロジックで判定してADに作成する 同期待ちを能動的に潰して即時反映する AD → Entra ID:デルタ同期を要求して完了までポーリング Entra ID → SaaS:Graph APIのオンデマンドプロビジョニング 運用:前日にdry-runで予定を流し、翌日に本番で作成する 導入の効果 まとめ 背景:手作業時代のフローと課題 自動化前のアカウント作成は、おおよそ次のような流れでした。 GASでkintoneから入社者のレコードを取得する スプレッドシートに転記し、関数で表示名やグループなどの値を加工する 加工した結果をCSVでエクスポートし、オンプレミスの作業サーバーへ手動で転送する 作業サーバー上で複数のPowerShellスクリプトにCSVを読み込ませ、順次実行してアカウントを作成する 途中、Microsoft Entra Connectのデルタ同期を手動で実行し、各SaaSへの反映を待つ 反映後、各SaaSの個別設定を、設定用スクリプトの実行や手作業で投入する 一見すると回っているように見えますが、運用を続けるうちに次のような課題が積み重なっていました。 業務ロジックが散在している :誰が・どの雇用形態で・どのグループに入るのか、その判断ロジックがスプレッドシートの関数と作業サーバー上のスクリプトに分散していました。そのため全体像の把握が困難でした。 手作業でのCSV転送 :スプレッドシートからCSVをエクスポートし、作業サーバーへ人手で転送していました。 ログの視認性が低い :実行は作業サーバーのコンソールで行うため、いつ・誰が・何を流したのかが後から追いにくい状態でした。 スクリプトのドリフト :Gitリポジトリ上のスクリプトと、作業サーバーに置かれた実際に動くスクリプトが少しずつ乖離していき、「リポジトリのコード=実際に動いているコード」と言い切れなくなっていました。 これらをまとめて解決するために、フロー全体を作り直すことにしました。 全体アーキテクチャ 新しい仕組みでは、データはkintoneから直接取り、全工程をGitHub Actionsから動かすことにしました。 手作業のころから大きく変えたのは、次の3つです。 CSVの受け渡しをやめ、kintone APIを直接呼び出す :人の手によるエクスポートと加工を排除しました。 散在していた業務ロジックをコードに集約する :判断ロジックをすべてPowerShellスクリプトにまとめ、Gitリポジトリで管理するようにしました。 オンプレADの操作にself-hosted runnerを使う :GitHub Actionsの仕組みに乗りながら、オンプレミスのADコマンドレットをそのまま実行できるようにしました。 ワークフローは schedule によるcron実行で動きます。kintoneからはデフォルトで翌月の入社者を取得するので、毎月決まった日に翌月分の作成予定をSlackへ自動投稿し、その翌日にまとめて作成する、という運用にしました。 ここからは、土台となるself-hosted runner、kintoneからのアカウント作成、クラウドへの即時反映を、順に見ていきます。 オンプレADをGitHub Actionsから操作する この仕組みで一番頭を悩ませたのが、オンプレミスのADをどう自動化に組み込むかでした。 ADはオンプレミスの閉じたネットワークの中にあります。操作するには、次の3つが必要です。 ドメインコントローラへ通信できるネットワーク(オンプレ内)にいること リモート サーバー管理ツール( RSAT )の ActiveDirectory モジュールが入った実行環境 対象OUでユーザー作成とグループ追加ができるドメイン権限 最初に検討したのは、この処理を自前でオーケストレーションせず、Entra ID Governanceのようなマネージド機能に寄せる形でした。たとえばオンプレADへのユーザー作成は、 API 駆動型インバウンド プロビジョニング に任せられます。 ただ、今回の要件には合いませんでした。アカウント作成にはオンプレADのグループ操作が含まれますが、属性ベースの動的グループが扱うのはクラウドのグループで、オンプレADのグループメンバーシップまでは管理しません。ここはマネージド機能だけでは実現できません。 さらに、出し分けの業務ロジックは、手作業時代のPowerShellスクリプトへある程度作り込まれていました。これを一から組み直すより、既存の資産をそのまま流用したい事情もありました。そこで今回は、自前でオーケストレーションする方針にしました。 自前で組むなら、ADを操作する方法はいくつかあります。検討した選択肢の長所と短所を、次の表にまとめました。 実行方式 長所 短所・見送り理由 タスクスケジューラ+スクリプト 追加インフラが不要 時刻起動のみ・コードがサーバーに残りドリフト再発・ログやSecrets、レビューが弱い 自前でAPIを建てる オンデマンド実行・自由度が高い 着信ポートの開放が必要・認証や可用性、監視を自前運用 Azure Automation(Hybrid Runbook Worker) Power Automate/Microsoft Formsから起動しやすい・Azure/M365中心の組織に馴染む GitのPR/CIと一体化しにくい・別基盤の学習/運用コスト self-hosted runner(採用) 既存のGitHub・PR・CIにそのまま乗る・チェックアウトでドリフトなし・手動/定期の両対応 runnerの保守が必要・信頼できるコードのみ実行する配慮が要る 自前API以外の3方式は、オンプレからのアウトバウンド通信だけで動作し、外部に着信ポートを開ける必要がありません。 最終的に self-hosted runner を選びました。ADドメインに参加したWindows Server上でrunnerを動かし、PowerShellを実行しています。 コード・トリガー・ログがすべてGitHub側へ移り、サーバー上では実行時にチェックアウトされた最新のスクリプトが動くだけです。背景で挙げたドリフトは、これで自然に消えます。 ほぼ同じ仕組みは、AzureのHybrid Runbook Workerでも実現できます。私たちはGitHubを使った業務フローが根付いていたため、self-hosted runnerを選びました。 ただし、この構成では強い権限を持つrunner自体が攻撃対象になります。runnerを侵害させないことと、万一侵害されても被害を広げないことの両方に注意を払う必要があります。 runnerに任意のコードを実行させる入口はGitHubなので、次のような基本的な制御を入れています。 リポジトリをprivateにし、権限は必要なメンバーだけに限定する ログインはSSOを必須とし、MFAの登録を義務づける mainはbranch protection ruleでCODEOWNERのレビュー承認を必須にする 認証は原則OIDC(Workload Identity連携)を使い、やむを得ずシークレットを使う場合もEnvironmentシークレットに置いてmainのワークフローからのみ参照できるようにする self-hosted runnerのサービスを動かすアカウントに過剰な権限を与えないことも重要です。runnerはオンプレADからクラウドまで触れる強い実行主体なので、万一侵害されたときの影響範囲を抑えるため、アクセスできるOUを絞るなど、必要最低限の権限だけを与えています。 kintoneから翌月の入社予定者を取得し、業務ロジックで判定してADに作成する 土台ができたら、次はアカウントを作る処理です。 まず取得です。kintoneの Cursor API を使い、クエリで対象者を絞り込みます。条件は3つで、入社月・入社区分(中途や新卒など)・雇用形態(社員やアルバイトなど)です。対象月は、定期実行では NEXT_MONTH() で翌月分を自動的に対象にします。 # 入社区分・雇用形態・入社月で対象者を絞り込む $categoryFilter = 'Category in ("中途", "新卒", "社員登用", ...)' $contractFilter = 'Contract in ("社員", "出向", "CSアルバイト", ...)' $query = " $categoryFilter and $contractFilter and Date = NEXT_MONTH() order by Date desc" # カーソルを作成し、レコードを取得し切るまで繰り返す $cursorId = ( Invoke-RestMethod -Uri $cursorEndpoint -Method Post -Headers $headers -Body $utf8Body ).id $nextCursorId = $cursorId while ( $nextCursorId ) { $res = Invoke-RestMethod -Uri " $cursorEndpoint `? id= $nextCursorId " -Method Get -Headers $getHeaders $allRecords += $res .records $nextCursorId = $res .next } CSVのエクスポートと手動転送がなくなり、「どんな条件で対象者を抽出しているか」がクエリを見れば分かるようになりました。 ただ、取得したデータをそのままADに流し込めるわけではありません。雇用形態や所属によって、どのOUに作り、どのグループに入れるかが変わります。以前はこの出し分けがスプレッドシートの関数に埋もれていましたが、今回コード側の分岐に移しました。判定しているのは、たとえば次のような内容です。 雇用形態に応じた、アカウントの配置先OUの決定 所属に応じた、各種グループへの追加 その他のユーザーアカウント属性の設定 判定が終わったら、self-hosted runnerの上で ActiveDirectory モジュールのコマンドレットを実行し、ADにアカウントを作成します。あとは次に述べる同期で、クラウド側へ反映されていきます。 同期待ちを能動的に潰して即時反映する ADにユーザーを作っただけでは、クラウド側のアカウントはすぐには使えません。間に2つの同期が挟まるためです。 1つ目はAD → Entra IDです。オンプレADのユーザーは、Microsoft Entra Connectが同期して初めてEntra ID側に現れます。この同期は通常、30分間隔のサイクルでしか走りません。 2つ目はEntra ID → 各SaaSです。Entra IDからGoogle WorkspaceやBoxへは、エンタープライズアプリのプロビジョニングで連携されます。この自動プロビジョニングのサイクルは最大40分待つことがあります。 手作業のころは、この待ちを人がこなしていました。同期サイクルが回るのを待つか、待ちきれなければデルタ同期を手動で叩き、クラウドにアカウントが現れたのを確認してから次の設定に進む、という具合です。 ところが、これをそのまま自動化に持ち込むと厄介でした。AD作成の先には、Entra IDのライセンスやグループ設定、Google WorkspaceやBoxへのプロビジョニング、その後の各SaaS個別の設定が続きます。どれも前の結果を前提にした処理です。定期サイクル任せだと全部終わるまでに1時間近くかかるうえ、まだ存在しないアカウントを触って空振りするステップも出てきます。 そこで、2つの同期をワークフローから自分でトリガーし、完了を見届けてから次へ進むようにしました。 AD → Entra ID:デルタ同期を要求して完了までポーリング 1つ目の同期は、Microsoft Entra Connectの同期サーバーに対してデルタ同期を要求し、完了するまで待ちます。runnerから Invoke-Command で同期サーバーに入り、 Start-ADSyncSyncCycle でデルタ同期を開始したあと、同期が進行中でなくなるまでポーリングします。 Invoke-Command -ComputerName $syncServer -ScriptBlock { Import-Module ADSync Start-ADSyncSyncCycle -PolicyType Delta | Out-Null # 同期が完了するまで待つ do { Start-Sleep -Seconds 10 $inProgress = ( Get-ADSyncScheduler ).SyncCycleInProgress } while ( $inProgress ) } ポーリングで完了を待つので、後続の処理に進む時点では、新入社員が必ずEntra ID側に存在します。 この Invoke-Command によるPowerShell Remotingは、runnerが侵害されたときに同期サーバーへの横移動の足がかりになりかねません。そこで、同期サーバーへ接続できる元をrunnerに限定し、接続に使うアカウントの権限も同期の実行に必要な分だけに絞っています。 Entra ID → SaaS:Graph APIのオンデマンドプロビジョニング 2つ目の同期は、Entra IDの自動プロビジョニングのサイクルを待たず、Graph APIの provisionOnDemand を使ってユーザー単位で即座にプロビジョニングをトリガーします。 このリクエストには、対象アプリのサービスプリンシパルID( $spId )・同期ジョブID( $jobId )・同期ルールID( $ruleId )の3つが必要です。ただ、こちらで用意するのは1つ目だけです。 $spId はエンタープライズアプリの「オブジェクトID」で、Entra管理センターのアプリのプロパティ画面に表示されます。 残りの $jobId と $ruleId は、実行時に $spId からGraphで取得します。ユーザーの同期ルールはアプリごとに1つしかないので、同期ジョブのスキーマを覗けば、それがそのまま使うルールです。IDを手で管理する必要はありません。 # $spId から同期ジョブを取得(Quarantine以外を優先) $jobs = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/ $spId /synchronization/jobs" $jobId = ( $jobs .value | Where-Object { $_ .status.code -ne "Quarantine" } | Select-Object -First 1 ).id # ジョブのスキーマから、ユーザーが対象の同期ルールを選ぶ $schema = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/ $spId /synchronization/jobs/ $jobId /schema" $ruleId = ( $schema .synchronizationRules | Where-Object { $_ .objectMappings.sourceObjectName -contains "User" } | Select-Object -First 1 ).id あとは、取得した $jobId と $ruleId 、そしてプロビジョニング対象のユーザーを指定してリクエストを投げます。 $body = @{ parameters = @( @{ ruleId = $ruleId subjects = @(@{ objectId = $userObjectId ; objectTypeName = "User" }) } ) } Invoke-MgGraphRequest -Method POST ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/ $spId /synchronization/jobs/ $jobId /provisionOnDemand" ` -Body ( $body | ConvertTo-Json -Depth 10 ) ` -ContentType "application/json" この2つで同期待ちを潰すと、AD作成・各SaaSへのプロビジョニング・その後のSaaSごとの設定までが一本につながります。プロビジョニングのサイクルを待たずに済むので、後続の設定も同じ実行の中で流し込めます。終わった時点で、結果がSlackとジョブログに残ります。 運用:前日にdry-runで予定を流し、翌日に本番で作成する 毎月の作成は、dry-runと本番の2段階で回しています。 前日に走るのはdry-runです。AD作成やプロビジョニングといった変更は実際には行わず、「何をするか」(翌月入社者の作成予定)をSlackに流します。チームはこの通知で対象者を確認できるので、kintoneのデータに不備があっても、本番の前に気づけます。 翌日に本番が走ります。処理の開始から各ステップ、完了までを1本のSlackスレッドに流し、完了サマリーと、失敗したときのエラーだけはチャンネルにもブロードキャストして気づけるようにしています。作業サーバーのコンソールを覗かなくても、SlackとGitHub Actionsのログだけで実行状況を追えます。手作業時代に困っていた「ログの視認性の低さ」は、これで解消しました。 もう1つ効いているのが冪等性です。アカウント作成は既存ユーザーをスキップし、グループ追加も「すでにメンバー」を無視します。途中のステップが失敗しても、直して同じワークフローを流し直せば、できているところはそのまま、足りないところだけが埋まります。 また、プロビジョニングの後に各SaaSで行う個別の設定は、SaaS側の状態によっては失敗することがあります。これらは失敗しても全体を止めず、結果をSlackに残して先へ進む設定にしています。気づいたら、その部分だけ後で流し直せば済みます。 導入の効果 一番大きいのは、毎月の新入社員アカウント作成がほぼ無人で回るようになったことです。これまで人がCSVを受け渡し、スクリプトを手で実行し、同期を待っていた一連の作業が、1回のワークフロー実行にまとまりました。 業務ロジックもコードに集まり、「どの条件で何が付与されるか」をリポジトリで追えます。リポジトリのコードと実際に動くコードが一致するので、ドリフトも起きません。 まとめ 本記事では、新入社員のアカウント作成を、kintoneを起点にGitHub Actionsで自動化した取り組みを紹介しました。オンプレADの操作はself-hosted runnerで担い、散らばっていた業務ロジックはコードにまとめ、同期待ちはGraph APIのオンデマンドプロビジョニングで潰しました。手作業で属人化していた運用を、コード化して実行を追える形にできました。 オンプレミスとクラウドが混在する環境でアカウント運用を自動化したい方の参考になれば幸いです。今後は、退職時のアカウント無効化など、入社から退職までのライフサイクル全体へ自動化を広げていく予定です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、データシステム部MA推薦ブロックの佐藤( @rayuron )です。私たちは、主にZOZOTOWNのメール配信のパーソナライズなど、マーケティングオートメーションに関するレコメンドシステムを開発・運用しています。 以前、テックブログで工数やAI活用による工数削減を計測する仕組みを、タスク管理ツールの GitHub Projects で構築する方法をご紹介しました。 前回の記事 が計測の仕組みの作り方を扱ったのに対し、本記事では仕組みを1年間運用して得られた結果とその考察、そして今後の展望をご紹介します。 目次 はじめに 目次 背景と課題 計測の仕組みと運用 計測の仕組み 運用の整備 結果と考察 AIによる工数削減とAI活用率の推移 作業内容別の効率 定性的な変化 体感と実測のギャップ 今後の展望 Spec: 仕様の明文化 Eval: 成果物の品質の評価 AIが自律的に動く状態の構築 ソウゾウのナナメウエの創出 おわりに 背景と課題 昨今のAIの進化は目覚ましく、私たちの業務でもAIを活用する機会が増えています。AIを使うことで、業務の工数削減や品質向上など、多くのメリットを享受できます。特に工数削減については、「タスクを捌く時間がなんとなく短くなった」という体感がありました。一方で、実際どれだけ効果があったのかは把握できていませんでした。そこで、私たちはAI活用の効果のうち工数削減を計測することにしました。 AIによる工数削減を測るには、AIを使った時と使わなかった時の工数を比較する必要があります。しかし、現実にはある1つのタスクに対してAIを使う場合と使わない場合を同時に観測できません。AIの利用回数やPull Request(以下PR)のマージ数などの指標は取得できるものの、これらは活動量を示すものであり、工数削減に「どれだけ効果があったか」を直接測るものではありません。また、MLチームの業務にはモデル開発の実験・ドキュメント作成・プロジェクトマネジメント業務・登壇準備など、PRにならない業務が多く、PRベースの計測ツールでは網羅的に計測できませんでした。 AIの効果が測れないままでは、投資対効果を説明できませんし、どのパターンの業務がAIと相性が良いのかも感覚的にしか分からず、改善の方向性も定まりません。そこで私たちは、効果を測る仕組みを作り、得られたフィードバックをもとにAI活用の改善を進めることにしました。 計測の仕組みと運用 まず取り組みを見ていただく前に、私たちの通常業務を紹介します。後述する結果はチームのタスクの内容に大きく依存するため、先に知っていただくと結果を解釈しやすくなると思います。 私たちMA推薦ブロックは、エンジニア3人の小規模チームです。メール・LINE・Pushなどの配信をパーソナライズするレコメンドシステムを開発・運用しており、業務例は以下の通りです。 施策起案/要件定義:施策インパクトの事前分析・要件定義 システム/モデル設計:推薦モデル・システムの設計 システム/モデル実装:推薦モデル・システムの実装、テスト、デプロイ 効果検証:A/Bテストダッシュボード作成、効果検証レポーティング作成 運用保守:オンコール対応、パッケージ更新、問い合わせ対応 その他:プロジェクトマネジメント、チームの仕組み作り、目標設定、勉強会、登壇など 開発にはGitHubを使用しており、ドキュメント管理には Confluence を使っています。AIツールとしては、 Claude Code ・ Codex ・ Devin などを必要に応じて使い分けており、メインではClaude Codeを使っているメンバーが多いです。 計測の仕組み 背景と課題で述べたとおり、同一タスクにおいてAIを使った場合と使わなかった場合を同時に観測できません。そこで、タスクの担当者にAIを使わなかった場合の想定工数を見積もってもらい、実際にかかった工数との差を「AIによる削減工数」として記録することにしました。 具体的には以下の仕組みを作成し運用しています。 全タスクをGitHub Issueで管理しGitHub Projectsに紐付け、カスタムフィールド「AI削減工数」「AI活用の成功・失敗事例」に記録する GitHub ProjectsのデータをBigQueryへ毎日自動エクスポートし、ダッシュボード上で可視化する 全体像は以下の通りです。記録したデータは日次で BigQuery へエクスポートされ、BIツールの Data Studio(旧Looker Studio) のダッシュボードから確認できます。 この計測方法について、詳細は以下のテックブログで紹介しています。 techblog.zozo.com Issue作成とフィールド入力には、Claude Codeの Skill をそれぞれ用意して、運用負荷を下げています。例えば、Issueを標準テンプレートで作成する /create-issue や、AIとのセッションログを解析してAI活用フィールドを自動入力する /fill-ai-usage です。なお、入力内容そのものをAIに判断させるわけではありません。あくまで担当者の判断を補助するために使います。例えば、以下のようなタスクで工数削減があった場合、担当者は次のように記録します。 Issueタイトル:MLパイプラインの実装 AI削減工数:2時間 AI活用の成功・失敗事例:パイプラインのステータスをポーリングするスクリプトを書いて、エラー時にClaude Codeで自動修正させて再実行させるループを作った AI活用の成功・失敗事例の欄には、Issueのタスクそのものではなく、タスクを進める中でAIをどう活用したかを記録します。こうした記述が、工数削減にどうAIが効いたかを後から振り返るナレッジになります。 運用の整備 計測の仕組みを作っただけでは、チーム全員が使いこなせるようにならず、改善のサイクルも回りません。そこで、以下の運用を整備しました。 チーム目標への組み込み:「AIで社員を0.5人増やす」を半期のチーム目標に設定した。削減した時間を1か月あたりの営業日数×8時間と比較することで、チーム全体で「1か月あたり0.5人増えた」と言える状態を目指した 週次振り返り: 毎週ダッシュボードで削減実績を確認し、ナレッジを共有する。削減工数が最も多かったメンバーに社内のピアボーナスを送った AI活用会: AI活用において、活用方法を知っているかどうかの差が大きいため、全員がAIの機能や使い方を「知っている」レベルに揃えることを目的に、毎週1時間開催してきた。その後はチーム外のメンバーにも拡大した。現在は、社内のAI活用レベル指標 AZARS に基づき、既存業務のうちAI活用の効果が大きいものを特定し、改善を進めている デモ会: AI活用で削減できた時間を使い、施策提案のためのプロトタイプ作りと他チームへの提案をしている 以下は、週次振り返りで使っていたダッシュボードの一例です。 結果と考察 このような取り組みの結果、以下のような変化がありました。 AIによる工数削減とAI活用率の推移 FY2025H1とFY2025H2を比較すると、AIによる工数削減とAI活用率の計測結果は以下の通りです。 期間 AIによる工数削減 完了件数 平均AI活用率 FY2025H1(2025/04〜09) 212.6時間 269件 45.0% FY2025H2(2025/10〜2026/03) 439.9時間 260件 71.5% 合計 652.5時間 529件 -- ※ AI活用率は「AI削減工数が記録されたIssue件数 ÷ 完了したIssue全件数」で算出しています。 月別に見ると、削減工数は2025年10月の90.2時間と2026年3月の96.8時間がピークでした。1人月を各月の営業日数×8時間とすると、2025年10月は約0.51人月、2026年3月は約0.58人月に相当します。半期のチーム目標に掲げた月0.5人分は、この2つのピーク月で達成できました。Issue単位のAI活用率は2025年6月の28.0%から、2026年3月には88.6%まで上がりました。また、削減工数の伸びはFY2026に入っても続いています。2026年7月時点の集計では、2026年4〜6月の3か月だけで253.6時間を削減しており、6月単月の114.5時間は計測を開始してから最も大きい月間削減です。 H1からH2で削減工数は全体で見ると212.6時間から439.9時間へ、約2.1倍に伸びました。AI活用件数は121件から186件へ1.54倍、活用1件あたりの平均削減は1.76時間から2.37時間へ1.35倍です。つまり、AIを適用するタスクが増える「広がり」と、1件のタスクの中でAIに任せる工程が増える「深まり」の両方が寄与したと考えています。 ただし、この伸びは12か月の時系列を見た変化であり、活用率の上昇が削減を生んだという因果を示すものではありません。MA推薦ブロックは2025年2月に立ち上がったチームで、FY2025H1はチームの仕組み作りや環境の整備といった、AIの効きにくいタスクの比率が高い時期でした。さらに、この1年で利用しているAIの性能自体も大きく向上しています。チームの工夫と、タスクの変化、AIの性能向上が重なった結果として解釈していただきたいです。 作業内容別の効率 以下のグラフに示す作業内容別では、システム開発の削減工数が42.5時間から103.0時間へ伸びました。伸び率ではテスト・QAが4.2時間から47.0時間へ約11倍、テックブログ執筆・登壇が4.0時間から53.3時間へ約13倍となりました。 総量だけでなく効率も以下のグラフで確認します。1営業日あたりの削減工数は、AI削減工数の合計を実作業日数の合計で割った値です。作業内容ごとに見ると、上位はテックブログ執筆・登壇の0.53時間/営業日、分析・レポーティング作成の0.49、社内活動の0.42でした。下位はプロジェクトマネジメントと要件定義がともに0.14、設計が0.19でした。 ※ なお、この指標は小さいIssueほど高く出やすいですが、作業内容ごとのIssueの粒度に大きな差はないことを確認しています。 この傾向から、成果物が明確で作業に時間のかかるタスクほどAIの効果は出やすく、意思決定や対人調整を中心とするタスクほど効果は出にくいと考えています。 効果が出やすいタスクの中でも、コード生成やドラフト作成は特に速さを実感しやすい作業です。実際にAIによる開発量が増えていることは、外部ツールでも確認できています。開発生産性の計測サービスである Findy Team+ のデータを見ると、AI活用が広がったH2に、PR作成数はH1の約1.5倍、デプロイ頻度も1営業日あたり1.3件から2.1件へ増えました。 定性的な変化 数字には表れない、以下のような変化もありました。 チームのナレッジが蓄積された:AI活用の記録やデモ会での議論を通じて、タスクごとのAIの利用例を後から参照できるようになり、ナレッジの共有が進んだ メンバーのAIの活用レベルが上がった:AI活用の成功・失敗を経て、AIのより効率的な使い方を学び、実践できるようになった チーム横断の交流が生まれた:勉強会をチーム外へ広げた結果、他部署との交流が増え、AI活用のナレッジが社内に広がった プロトタイプ駆動の業務の進め方が生まれた:AIはアイデアをすぐ形にできるので、自分たちやステークホルダーからのフィードバックをすぐ得られる。この相性の良さを活かしたデモ会からは、デモを起点に案件化を判断する実例も生まれた また、失敗の記録からも学びがありました。1つ目は2.0時間、2つ目は1.0時間の工数増として記録されたものです。 Claude Codeに方針を委ねようとして、結局出力がよくわからなくなり最終的に自分が方針決めをした。 GitHub Projectsのフィールド埋めをコマンド経由で自動化しようとしたところ手動でやった方が良いことに気づいた。設計時間分のロスが発生した。 当たり前と言われれば当たり前ですが、これらは方針決めのような曖昧なタスクをAIに委ねると逆に時間を失うこと、自動化にも判断が必要なことを示しています。こうした失敗事例も、チーム内で共有することで、次のタスクで同じ失敗を繰り返さないようにしました。 体感と実測のギャップ メンバーからは、半期振り返りで以下のような声が上がりました。 AIで社員を0.5人増やすことが目標であったが、分析やコーディング業務では既に「俺が3人分になる…」ケースがある 一方、計測した工数削減を見ると、ピーク月でも96.8時間で約0.58人月の削減にとどまっています。エンジニア3人のチームにおける0.58人月の削減を、3人で3.58人分の業務をこなしたとみなすと、1人あたりは約1.2倍となり、体感の3人分とはギャップがあります。 このギャップについては、以下のように解釈しています。 実装をAIが一瞬で終わらせる体験は脳に強く残り、その印象に引っ張られ、業務全体が速くなったように感じる タスクが半日で終わってもその分2倍働くのは難しく、空いた時間は別のタスクに使われる 「理解してから実装する」が「実装されたものを理解する」に逆転し、実装時のフロー状態に入れない 並列でタスクを回せる分、コンテキストスイッチの回数と時間あたりのインプット量が増え、脳が疲れて作業の速度が落ちる 人間のレビューの待ち時間や対人コミュニケーションのコストが変わらない場合、AIで作業を速くこなせるほど人間の対応頻度が増えるので人間がボトルネックになる 今後の展望 FY2026H1の目標は、AIによる工数削減を月1人分に引き上げることです。これまでのように各自のタスクにAIを使って速く終わらせるだけでは、この目標に届かないというのがチームの共通認識です。特に、考察で見えたのは私たちのチームの伸びを止めているのがAIの性能というよりは、人間や運用体制だということでした。人間の集中力や、コンテキストスイッチへの耐性は簡単には変えられません。変えられるのは人間によるAIの効果的な使い方や運用体制だと考え、具体的には以下のように改善を進めています。 Spec: 仕様の明文化 1つ目は、実行前に仕様と完了条件を明文化して、AIの成果物への理解度を上げることです。結果と考察で触れた失敗事例が示すとおり、仕様と完了条件が固まっていないタスクをAIへ投げると、出力の良し悪しを判断できず、かえって時間がかかります。仕様の明文化に加えて、仕様の検討時に過去の類似タスクを参考として提案する仕組みも作り始めています。 Eval: 成果物の品質の評価 2つ目は、成果物の品質評価を自動化しやすい形に整備することです。品質のうち定量化できるものはテストとして実装し、自動的に評価します。一方、定量化が難しいものについては判断基準をガイドラインとして整備し、それに沿って評価できるようにします。これにより、成果物の生成だけでなく評価においても人間の介入を減らせると考えています。 このような動きをレビューの自動化にもつなげるつもりです。レビューの目的は、規範適合・検証の代行・意思決定と合意形成・知識の伝達などと整理できます。規範適合や検証の代行といったタスクはAIと自動的なテストに任せ、人間は意思決定と合意形成を中心に行うことでAIと人間の役割を分担します。 以下の図の左側が現在の状態です。デプロイの前に第三者によるレビューを必須としているため、AIで1人の実装が速くなっても、チーム全体のスピードは上がりにくい構造です。右側のように自動テストとAIレビューを挟むことで、この構造を変えていきます。 成果物の定量化と自動的なテストが進むほど、人間からAIへ委譲できるタスクは増えると考えています。そして、人間の確認を要するタスクが減れば、レビュー待ちは少なくなり、各自は自走して高速にタスクを進められると考えています。 AIが自律的に動く状態の構築 3つ目は、人が毎回指示しなくてもAIが自律的に動き、使うほど賢くなっていく状態を作ることです。SpecとEvalが整備されたあとは、人間のトリガーを待たずにAIが安全に動き、人間の判断やAIの成功・失敗のフィードバックを適用しながら自ら改善していく状態を目指しています。 私たちのチームでは、自律的なAIが活躍でき、効果の大きそうな以下の領域から着手し、次のような状態を作ろうとしています。 アラート対応:アラートを検知するとIssueを自動作成し、AIエージェントが原因調査から修正対応までを行い、アラートを解決する 定型的な改修:トリガーとなるイベントを受けて、仕様が明確な定型的な改修をAIが行い、リリース前の状態を作る データ分析:人間とAIが仮説を作り、データの前処理・分析・可視化までを行い、結果をレポートとしてまとめる モデル開発:人間とAIが実験計画を作り、分析→実装→実験を繰り返し、モデルの精度を改善して、結果をレポートとしてまとめる ソウゾウのナナメウエの創出 最後は、AIで生まれた余白の一部を、意図的に新しい価値づくりへ投資することです。考察で見たとおり、削減した時間をすべて次のタスクの前倒しに使うと脳が疲れるだけになりかねません。実際に、デモ会ではビジネスサイドへの提案を文書から動くデモへ変える取り組みを続けています。こういった活動を広げることで、ZOZOが企業理念でZOZOらしさとして掲げる ソウゾウのナナメウエ なアイデアの実現に挑戦し続けられるチームでありたいと考えています。 おわりに 本記事では、AIによる工数削減を計測して見えた結果と考察、今後の展望をご紹介しました。 現在ZOZOでは一緒にサービスを作り上げてくれる方を募集しています。ご興味がある方は以下のリンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは、新規事業部フロントエンドブロックの 大野純平 です。2025年度に新卒入社し、現在のチームに配属されました。チームでFlutter製モバイルアプリを開発する中で、新規プロジェクトの立ち上げを効率化するための社内テンプレートリポジトリの整備を進めています。 このテンプレートを育てる中で、 Flutter 3.44 へのアップデートに伴うCocoaPodsからSPMへの移行対応が積み残しになっていました。あわせて、このタイミングでdev / stg / prdの3環境対応も新規に追加しました(3 flavor化自体はSPM移行の必須要件ではなく、テンプレート整備の一環で同時に着手したものです)。本記事では、 CocoaPodsからSPMへの移行 と、 3 flavorを mise run setup で一括整備する仕組み を紹介します。あわせて、Flutterにおけるビルド構成ファイルを解説します。 目次 はじめに 目次 Flutterプロジェクトのビルド構成 iOS側のしくみ Android側のしくみ Dart側のしくみ 3層を貫く全体像 背景・課題 3環境の設定整合が壊れやすい 解決策の全体像 実装内容と具体的な解決策 1. CocoaPodsを撤去してSPMに移行する CocoaPodsの統合を解除する Crashlytics dSYMのパスを更新する 2. iOS:xcconfigでBuild Configurationを9パターンに整える Firebase plistをBuild Phaseで差し替える 3. Android:productFlavorsとgoogle-services.jsonをflavorごとに配置する 4. Dart:firebase_optionsをセレクタshim化する main.dartでduplicate-appを処理する 5. mise run setupで全flavorを一括生成する まとめ Flutterプロジェクトのビルド構成 Flutterアプリは iOSネイティブ層・Androidネイティブ層・Dart層 の3つが組み合わさっています。環境(flavor)の切り替えとは「3層すべてに、同じflavor用の設定を選ばせること」です。 iOS側のしくみ Xcodeは以下の単位を組み合わせてビルドを定義します。 単位 役割 Flutter デフォルト Project ( .xcodeproj ) プロジェクト全体の定義。実体は project.pbxproj Runner.xcodeproj Target ビルド成果物の単位 Runner Build Configuration ビルド設定値の集合 Debug / Profile / Release の 3 種 Scheme Run / Test / Profile / Analyze / Archive の各アクションで使う Target と Configuration を定義する起動設定 Runner 1 つ xcconfig はBuild Configurationの値をテキストで宣言するファイルです。別のxcconfigを #include で取り込んだとき、同じ変数があれば 後から読み込んだ値が優先 されます。共通値を書いたxcconfigを先に、flavor固有値を書いたxcconfigを後に重ねれば、最終的な値が組み上がります。 Info.plist の CFBundleIdentifier は $(PRODUCT_BUNDLE_IDENTIFIER) を参照しています。そのため、xcconfig側で PRODUCT_BUNDLE_IDENTIFIER を決めればBundle IDが自動的に解決されます。 iOS・macOSで外部ライブラリを使うときの依存管理ツールには、 CocoaPods と Swift Package Manager(SPM) の2つがあります。Flutterプロジェクトでは長らくCocoaPodsが標準でしたが、Flutter 3.44からiOS・macOSの両方でSPMがデフォルトに切り替わりました。両者の違いは以下のとおりです。 CocoaPods Swift Package Manager (SPM) 提供元 OSS(Ruby 製) Apple 公式 設定ファイル Podfile Package.swift (Xcode では Package Dependencies UI から編集) 成果物 Pods/ + .xcworkspace を別管理 Xcode が直接管理 ロックファイル Podfile.lock Package.resolved Flutter での状況 長年の標準 3.44 でデフォルト有効化 Android側のしくみ AndroidのビルドはGradleが android/app/build.gradle.kts を読んで実行します。 概念 役割 buildTypes ビルド種別( debug / release ) productFlavors 同じアプリのバリアント定義。 applicationId やリソースを分岐できる applicationId / applicationIdSuffix アプリの一意な識別子。iOS の Bundle ID 相当 resValue ビルド時に文字列リソースを生成する Gradle の機能 AndroidManifest.xml アプリのメタデータ宣言。 android:label でアプリ名を指定 Gradleは、flavor別のリソース( AndroidManifest.xml や res/ 配下など)を source set という仕組みでディレクトリ単位に分けます。Gradleがflavorを選んだときに src/main/ と自動マージします。なお google-services.json は厳密にはsource set mergeの対象ではありません。google-servicesプラグインが src/<flavor>/google-services.json などのパスを優先度順に探索し、1ファイルを選びます。 Dart側のしくみ Dartでflavorを識別するにはコンパイル時定数を使います。 API / オプション 役割 --dart-define-from-file=path/to/x.json .json または .env ( KEY=VALUE 形式)で書いた値をまとめて Dart に渡す flutter コマンドのオプション String.fromEnvironment('KEY') 渡された値をコンパイル時定数として参照する API。 switch の case 値にもできる firebase_options.dart flutterfire configure が生成する Firebase 設定ファイル。通常は 1 ファイル = 1 Firebase プロジェクト 3層を貫く全体像 3層を1枚にまとめると、 app_config.yaml → mise run setup → 各層のファイル群 → flutter run --flavor X という流れです。以降、iOS・Android・Dartの順に、各層内で mise run setup と flutter run --flavor X がどのファイルへつながるかを示します。 iOS層では、xcconfigチェーンが Info.plist を解決します。 project.pbxproj のRun Scriptがflavor別の GoogleService-Info.plist をコピーします。 Android層では、 build.gradle.kts がflavorディレクトリの google-services.json と AndroidManifest.xml を結びつけます。 Dart層では、 flavor/{dev,stg,prd}.json の FLAVOR が起点になります。 firebase_options.dart が FLAVOR に応じて firebase_options_{dev,stg,prd}.dart を切り替えて返すコード( セレクタ shim )になります。 flutter run --flavor dev --dart-define-from-file=flavor/dev.json を1回実行したときに、各層で何が起きるかを並べると以下のようになります。 iOS :Scheme dev → Debug-dev.xcconfig で PRODUCT_BUNDLE_IDENTIFIER が .dev 付きに解決される。Build PhaseのRun Scriptが ios/firebase/dev/ のplistをコピーする。 Android :productFlavor dev が選ばれて applicationIdSuffix=".dev" が適用される。 src/dev/google-services.json がsource setにマージされる。 Dart : flavor/dev.json の FLAVOR=dev がコンパイル時定数になる。 firebase_options.dart のセレクタshimが firebase_options_dev.dart を返す。 ここまでがFlutterプロジェクトのビルド構成の概要です。これを踏まえて、本記事で取り組んだ課題と解決策を見ていきます。 背景・課題 3環境の設定整合が壊れやすい dev / stg / prdの3環境を切り替えるには、iOS・Android・Dartの3層を整合させる必要があります。各層の対象は次のとおりです。 iOS:Bundle ID、 GoogleService-Info.plist Android: applicationId 、 google-services.json Dart:Firebase初期化オプション 3層が独立しているため設定漏れによるビルド失敗やFirebase接続不可が繰り返し発生し、原因特定にも時間がかかっていました。 解決策の全体像 以下の5つの取り組みで課題を解決しました。 # 層 取り組み 1 iOS / 基盤 CocoaPods 撤去 → SPM 移行 2 iOS / flavor xcconfig × 9 Build Configuration + flavor 別 Firebase plist 3 Android / flavor productFlavors + flavor 別 google-services.json 4 Dart firebase_options.dart のセレクタ shim 化 5 setup app_config.yaml の per-flavor 化と mise 自動化 1つのflavorを選んで flutter run すると、iOS・Android・Dartの3層でそれぞれ対応する設定が適用されます。 実装内容と具体的な解決策 1. CocoaPodsを撤去してSPMに移行する CocoaPodsはRuby製のAppleプラットフォーム向けパッケージマネージャーで、Swift Package Manager(SPM)はApple公式の代替です。Flutter 3.44では新規プロジェクトでiOS・macOSともにSPMがデフォルトで有効化され、Firebaseをはじめとする主要プラグインもSPM対応を完了しています。 pod install が不要になるため、CIのセットアップも大幅に高速化されます。 CocoaPodsの統合を解除する 既存プロジェクトでは、 Podfile をいきなり削除するのではなく、まず pod deintegrate でXcodeプロジェクトからCocoaPodsの統合を解除します。 cd app/ios pod deintegrate # project.pbxproj から Pods 統合を除去 rm Podfile Podfile.lock # Podfile 本体を削除 rm だけで済ませてはいけません。 Podfile を削除しただけでは project.pbxproj にCocoaPods統合の痕跡が残ります。たとえば [CP] Check Pods Manifest.lock などのBuild Phaseや、 Pods-Runner xcconfigの #include 参照などです。 pod deintegrate はこれらを正しく除去します。 .gitignore も忘れず更新します。 # app/ios/.gitignore -.symlinks/ -Pods/ +# SPM +.build/ SPMが管理する Package.resolved はバージョンを固定するためリポジトリにコミットします。 Crashlytics dSYMのパスを更新する dSYM(Debug Symbols)はクラッシュレポートのスタックトレースを人間が読めるシンボルに復元するためのファイルです。Crashlyticsを使っている場合、そのdSYMをアップロードするBuild PhaseスクリプトをSPM構成のパスに合わせて更新します。このRun Scriptは setup 系スクリプトが自動生成するものではなく、Crashlyticsを利用する場合はプロジェクトごとに手動で追加する手順です。 Firebase公式のApple向けガイド は現在SPM構成を前提としており、Run Scriptに以下を追加するよう記載されています。 # Build Phases > Run Script " ${BUILD_DIR % /Build/* } /SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run " CocoaPods時の ${PODS_ROOT}/FirebaseCrashlytics/run に相当します。Xcode 15以降は User Script Sandboxing がデフォルトで有効になっています。有効な場合はRun ScriptのInput Filesに以下を列挙する必要があります( Firebase公式ドキュメント )。 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME} ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME} ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist $(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH) # Debug Dylib(DEBUG_DYLIB)が有効な場合は追加で必要 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib 2. iOS:xcconfigでBuild Configurationを9パターンに整える iOSのXcodeプロジェクトは「Build Configuration」というビルド設定の単位を持ちます。各Configurationにはxcconfigファイル(テキストでビルド設定を記述する仕組み)を1つ紐付けられます。本テンプレートは、以下の9通りのBuild Configurationを用意しています。 dev stg prd Debug Debug-dev Debug-stg Debug-prd Profile Profile-dev Profile-stg Profile-prd Release Release-dev Release-stg Release-prd これらはあらかじめ project.pbxproj にコミット済みです。あわせて、それらを参照する dev / stg / prd の3 Schemeもコミット済みです。各Schemeは、Run/Test/Analyzeで Debug-<flavor> を参照します。Profileでは Profile-<flavor> 、Archiveでは Release-<flavor> を参照します。3つのSchemeには、Flutter標準の Run Prepare Flutter Framework Script pre-actionも含めて完全な状態で用意されています。このpre-actionは xcode_backend.sh prepare を実行します。これによりSPM統合に必要な FlutterGeneratedPluginSwiftPackage が解決されます。 mise run setup はこのScheme⇔Configurationのマッピングやpre-actionには一切触れません。書き換えるのは、各Configurationに紐づくxcconfigの値(Bundle IDやアプリ名など)だけです。 Flutter/Shared.xcconfig ← 全 Configuration 共通の値 Flutter/flavor-dev.xcconfig ← dev 固有の値(BUNDLE_ID_SUFFIX など) Flutter/flavor-stg.xcconfig Flutter/flavor-prd.xcconfig Flutter/Debug-dev.xcconfig ← #include で連結(9ファイル) Flutter/Debug-stg.xcconfig ... Flutter/Release-prd.xcconfig 後ろの #include が前の値を上書きするため、共通値( Shared )→ flavor固有値( flavor-dev )の順に重ねることで最終的な値が決まります。 Debug-dev.xcconfig は3行の #include のみで構成されます。 #include "Debug.xcconfig" #include "Shared.xcconfig" #include "flavor-dev.xcconfig" これでビルド時に対応するxcconfigが PRODUCT_BUNDLE_IDENTIFIER を解決します。その値が Info.plist 経由でflavor付きBundle ID(例: jp.testapp.dev )として適用されます。 Firebase plistをBuild Phaseで差し替える flavor別の GoogleService-Info.plist を使い分けるため、Build PhaseにRun Scriptを追加します。Run Script内の ${FLAVOR} は実行直前にxcconfigの値( dev / stg / prd )に置き換えられます。これによりコピー元ディレクトリがflavorごとに切り替わり、該当するplistだけが .app バンドルに入ります。 # Build Phases > Run Script SRC = " ${SRCROOT} /firebase/ ${FLAVOR} /GoogleService-Info.plist " DST = " ${BUILT_PRODUCTS_DIR} / ${PRODUCT_NAME} .app/GoogleService-Info.plist " if [ -f " $SRC " ]; then cp -f " $SRC " " $DST " else echo " warning: $SRC not found, Firebase native init will be skipped " fi plistが存在しないflavorでもビルドが失敗しないよう、存在チェックを入れています。 firebase_project_id を空にしてFirebase設定をスキップした場合がこれに該当します。このRun Scriptは読み書き対象のパスをInput Files / Output Filesとしてあらかじめ宣言しています。そのためUser Script Sandboxingが有効な環境でも安全に動作します。 plistは以下のスロットに配置します( setup:firebase が生成)。 app/ios/firebase/ ├── dev/GoogleService-Info.plist ├── stg/GoogleService-Info.plist └── prd/GoogleService-Info.plist 3. Android:productFlavorsとgoogle-services.jsonをflavorごとに配置する Androidでは build.gradle.kts に productFlavors を追加し、flavorごとに applicationIdSuffix とアプリ名を設定します。 // app/android/app/build.gradle.kts flavorDimensions + = "env" productFlavors { create( "dev" ) { dimension = "env" applicationIdSuffix = ".dev" resValue( "string" , "app_name" , "Flutter Template App Dev" ) } create( "stg" ) { dimension = "env" applicationIdSuffix = ".stg" resValue( "string" , "app_name" , "Flutter Template App Stg" ) } create( "prd" ) { dimension = "env" resValue( "string" , "app_name" , "Flutter Template App" ) } } google-services.json はAndroid Gradleのflavorソースセット( src/<flavor>/ )に配置します。flavor用の設定ファイルはgoogle-servicesプラグインが自動で選択します。 android/app/src/ ├── dev/google-services.json # setup:firebase が生成 ├── stg/google-services.json └── prd/google-services.json 注意点が2つあります。1つ目は google-services.json の package_name です。suffix込みの最終applicationIdに合わせます(例: com.example.app.dev )。suffixなしの com.example.app だけFirebaseに登録していると、dev / stgビルド時にプラグインが例外を投げてビルドが通りません。Firebase側のアプリ登録もsuffix込みのapplicationIdで行う必要があります。 AndroidManifest.xmlで android:label="@string/app_name" のように参照します。 productFlavors の resValue がflavorごとに生成した文字列が、そのままアプリ名として使われます。 <!-- app/android/app/src/main/AndroidManifest.xml --> <application android : label = "@string/app_name" ...> 2つ目は、 strings.xml との重複です。 resValue("string", "app_name", ...) はビルド時に文字列リソースを生成します。そのため、 res/values/strings.xml に同名の app_name が定義されていると Duplicate resources エラーになります。 app_name は strings.xml には置かず、各flavorの resValue に一本化してください。 4. Dart:firebase_optionsをセレクタshim化する flutterfire configure が生成する firebase_options.dart は単一の設定しか返しません。そこでファイル名とAPIを保ったまま、 セレクタ shim に置き換えます。 まず app/flavor/dev.json に FLAVOR キーを定義し、 --dart-define-from-file で読み込みます。 { " FLAVOR ": " dev " } FLAVOR はDartコード側で String.fromEnvironment('FLAVOR') を使って、コンパイル時定数として参照できます。 // app/lib/firebase_options.dart import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'firebase_options_dev.dart' as dev; import 'firebase_options_prd.dart' as prd; import 'firebase_options_stg.dart' as stg; const _flavor = String .fromEnvironment( 'FLAVOR' ); class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { switch (_flavor) { case 'dev' : return dev.DefaultFirebaseOptions.currentPlatform; case 'stg' : return stg.DefaultFirebaseOptions.currentPlatform; case 'prd' : return prd.DefaultFirebaseOptions.currentPlatform; default : throw StateError( 'Unknown FLAVOR: "$_flavor". ' 'Run with --dart-define-from-file=flavor/<flavor>.json' , ); } } } 呼び出し側( main.dart )は DefaultFirebaseOptions.currentPlatform をそのまま使うだけで、flavorの切り替えを意識する必要がありません。 setup:firebase はこのファイルを上書きせず、本物の firebase_options_<flavor>.dart だけを再生成します。 注意:flavor指定は必須 _flavor が空( --dart-define-from-file 未指定)だとshimの default が StateError を投げます。 _initializeFirebase はこれをcatchしないため、flavorなしの flutter run は起動時に落ちます。VS Codeのlaunch.jsonや mise run タスクで各flavorを必ず渡す構成にしておくと安全です。 main.dartでduplicate-appを処理する Firebaseには2つの初期化経路があります。ネイティブの自動初期化と、Dart側の Firebase.initializeApp() です。両方が呼ばれると duplicate-app の FirebaseException が発生します。ただし duplicate-app はflavor不整合(ネイティブのplist/jsonが別のFirebaseプロジェクトを指している等)でも発生しうるため、無条件で成功扱いにはできません。ネイティブ初期化済みの options と比較し、一致した場合のみ成功扱いにします。不一致の場合は例外を再throwし、アプリを起動時にクラッシュさせます。例外を握りつぶしてしまうと、不正なAPIキーや設定不整合といった検知したいバグを見逃してしまう可能性があるためです。 // app/lib/main.dart(抜粋) Future< bool > _initializeFirebase() async { try { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); return true ; } on UnimplementedError { // placeholder(setup:firebase 未実行)の場合は Firebase を無効化して続行 return false ; } on FirebaseException catch (e) { if (e.code == 'duplicate-app' ) { final existing = Firebase.app().options; final expected = DefaultFirebaseOptions.currentPlatform; final matchesNativeConfig = existing.appId == expected.appId && existing.projectId == expected.projectId && existing.apiKey == expected.apiKey; if (matchesNativeConfig) return true ; } rethrow ; } } 5. mise run setupで全flavorを一括生成する mise は、タスク実行とツールのバージョン管理を兼ねるCLIツールです。本テンプレートではセットアップスクリプトの実行に利用しています。手作業の設定漏れを防ぐため、セットアップの入力ファイル app_config.yaml にFirebaseのproject_idをflavorごとに設定できる構造を追加しました。これにより、 mise run setup:firebase で全flavorの設定ファイルを一括生成できます。 flavor : dev : app_name_suffix : " Dev" bundle_id_suffix : ".dev" firebase_project_id : "my-project-dev" # 空文字なら Firebase 設定をスキップ stg : app_name_suffix : " Stg" bundle_id_suffix : ".stg" firebase_project_id : "my-project-stg" prd : app_name_suffix : "" bundle_id_suffix : "" firebase_project_id : "my-project-prd" mise run setup:firebase は app_config.yaml の各flavorの firebase_project_id を読み取ります。読み取った値で flutterfire configure をflavorごとに実行します。 iOSは --ios-out でパスを直接指定すると、plistは出力されるもののproject.pbxprojへの参照は Runner/ 配下を前提に追加されます。配置と参照のズレでビルドが落ちる事例もありました。そこで setup:firebase は flutterfire configure のデフォルト出力( Runner/ )を利用し、後処理で配置を整えます。 Build PhaseのRun Scriptがflavorに応じてplistを動的にコピーするので、 project.pbxproj の静的参照は不要です。 setup:firebase は実行のたびに追記される参照を毎回除去し、pbxprojが肥大化しないようにしています。 # クリーンアップ確認(0 であれば OK) grep -c " GoogleService-Info.plist .*PBXBuildFile " \ app/ios/Runner.xcodeproj/project.pbxproj mise run setup:flavor はxcconfigやflavor JSONを app_config.yaml から再生成します。Firebaseの設定ファイルは別途 setup:firebase で更新します。何度実行しても同じ出力になるため、設定変更後に再実行すればxcconfigとflavor JSONが同期されます。 この仕組みにより、新規プロジェクトでは app_config.yaml を編集して mise run setup を実行するだけで済みます。iOS xcconfig・Android productFlavors・Dartセレクタshim・Firebase設定ファイルのすべてが一括でできあがります。 まとめ 本記事では、FlutterプロジェクトにおけるCocoaPodsからSPMへの移行を紹介しました。あわせてdev / stg / prdの3 flavorをiOS・Android・Dartの3層で整合させる仕組みの構築も紹介しました。 CocoaPodsからSPMへの移行自体は pod deintegrate → Podfile削除で思ったよりシンプルです。手間がかかるのはflavorの3層整合で、それぞれが独立した設定体系を持つため一つひとつ繋ぎ込む必要があります。 app_config.yaml → mise run setup という自動化の仕組みを整えました。これにより手動作業による設定漏れのリスクを排除し、チームが本来の開発に集中できる環境を作れました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、全社AWS管理部門の江島です。社内で利用されているAWS環境を全社横断的に管理する役割を担っています。 全社AWS管理部門では、AWSを安全に運用するために日頃からさまざまな取り組みを行っています。例えば、AWS本番環境へのアクセス管理においてIAM Identity Centerを活用し、定期的な棚卸しや申請ベースの権限管理といったセキュリティ対策を継続的に実施してきました。 こうした取り組みを土台としつつ、さらなるセキュリティ強化に向けて「必要なときだけ権限を付与する」最小権限の原則を徹底することを次の目標に掲げました。 本記事では、JITアクセス(ジャストインタイムアクセス)の仕組みを全社に導入した取り組みをご紹介します。AWSがオープンソースで公開する「TEAM(Temporary Elevated Access Management)」を活用しました。 目次 はじめに 目次 従来の運用 / 課題 JITアクセスとは TEAM (Temporary Elevated Access Management) TEAMの導入に必要な検討ポイント JITアクセスの対象とする権限 新しい権限付与フロー IdP管理 TEAMで利用される各Policyの設定 Eligibility Policy Approver Policy 初回導入後の状況 AWSアカウントごとに個別のPermission Set まとめ 従来の運用 / 課題 従来の権限付与フローは以下の通りです。 まず、申請者が社内ツールにて権限を申請します。その後、セキュリティ部門が内容の妥当性を確認し、全社AWS管理部門が権限を付与します。 なお、定期的な棚卸しや利用者からの申請に応じた見直しは実施していたものの、権限自体は常時付与された状態でした。 付与する権限が申請理由と照らし合わせて妥当であることは都度確認されていますが、必要時にだけ利用できれば良いような 強い権限 を常に保持しておくことは、最小権限の原則に照らしてリスクになり得ます。 そこで、これを解決するために「JITアクセス」の仕組みを導入することになりました。 JITアクセスとは JITアクセス(ジャストインタイムアクセス)とは一時的に権限昇格するための仕組みです。IAM Identity Centerの公式ドキュメントでは次のように説明されています。 Temporary elevated access (also known as just-in-time access) is a way to request, approve, and track the use of a permission to perform a specific task during a specified time. 引用元: docs.aws.amazon.com これを活用することで、本記事のタイトルにある「強い権限は使い捨て」を実現できます。 なお、前述したIAM Identity Centerの公式ドキュメントに記載があるように、さまざまなSaaSがIAM Identity Centerと連携してJITアクセスの機能を提供しています。また、AWSとしても独自のOSS(後述するTEAMというアプリケーション)を提供しています。 比較検討を行った結果、コストが安価であり、国内外のさまざまな企業での導入実績もあるTEAMを採用することになりました。 TEAM (Temporary Elevated Access Management) TEAMとはAWSによって提供されている一時的に権限昇格するためのアプリケーションです。以下のリポジトリで公開されています。 github.com AWS LambdaやAmazon DynamoDBのように複数のサーバレスサービスで構成されており、安価で拡張性を持ったアーキテクチャです。 公式ドキュメントからの引用ですが、具体的なアーキテクチャは以下の通りです。詳細は引用元をご参照ください。 TEAMのアーキテクチャ https://aws-samples.github.io/iam-identity-center-team/docs/overview/architecture.html より引用 TEAMを組織へ導入するには運用面でさまざまな点を検討する必要があります。具体的には、以下のような観点があります。 JITアクセスの対象とする権限 新しい権限付与フロー IdP管理 承認可能な範囲の決定(Approver Policy) 申請可能な範囲の決定(Eligibility Policy) 以降、それぞれの検討ポイントについて具体的な内容を紹介します。 TEAMの導入に必要な検討ポイント JITアクセスの対象とする権限 IAM Identity Centerを利用しているため、各ユーザへ付与する権限はPermission Setで管理しています。当社ではユーザの役割に応じて複数のPermission Setを用意しており、「読み取り専用」や「書き込みも可能」な権限があります。 結論としては、常に保有可能な権限は「読み取り専用」のみ、「書き込みも可能」な権限については すべてJITアクセスの対象 としました。これについては運用とセキュリティのトレードオフを考える必要がありますが、システムを安全に運用することを最優先とするために厳しめの方針となっています。なお、読み取り専用といっても、機密性が高い情報についてはさらに細かなアクセス制御を別途行っています。 新しい権限付与フロー JITアクセスによる権限付与フローは以下の通りです。 このフローを実現するために、一連のやり取りで登場する役割を整理しました。 役割 従来の権限付与フロー 新しい権限付与フロー(JITアクセス) 申請者 権限の申請 権限の申請 セキュリティ部門 申請内容を都度確認 AWSアカウント管理者へ判断を委譲 全社AWS管理部門 申請ごとに権限付与 AWSアカウント管理者へ作業権限を委譲 AWSアカウント管理者 - JITアクセス承認者の管理・承認 JITアクセス承認者 - JITアクセスの承認操作 新しいフローを実現するために、「AWSアカウント管理者」および「JITアクセス承認者」の役割を新しく定義しました。なお、セキュリティ部門はAWSアカウント管理者の任命に関する判断は引き続き実施します。また、AWSアカウント管理者は自身もJITアクセス承認者の役割を担います。 当社は多くのAWSアカウントを保有しているため、従来のフローのままですべての承認作業を行うことが困難でした。そこで、最低限のガバナンスを維持した上で権限を委譲する方針としています。 元々、AWSアカウントを新規で発行する際にはAWSアカウントごとに管理者を立ててもらう運用としていました。そこで、AWSアカウント管理者にJITアクセスの承認権限も委譲することにしました。一方で、単純に権限を委譲するだけだとAWSアカウント管理者自身の負担が増大すると想定されたので、JITアクセスの承認者についてはAWSアカウント管理者自身でコントロール可能なルールとしました。 IdP管理 TEAMはIdP(Identity Provider)であるIAM Identity Centerのグループ機能を前提として動きます。具体的には、以下のような役割をグループとして用意する必要があります。 役割 説明 TEAM管理者 TEAM自体の設定を変更する権限を持つ TEAM監査者 TEAM自体の監査機能(監査ログの閲覧)を利用する権限を持つ 申請者 Eligibility Policyに基づいて権限申請を行うことができる 承認者 Approver Policyに基づいて承認操作を行うことができる 申請者と承認者のグループは複数用意でき、当社でも複数グループを利用しています。これについては、次に説明するEligibility PolicyやApprover Policyと合わせて説明します。 TEAMで利用される各Policyの設定 TEAMにはEligibility PolicyとApprover Policyというものが存在します。それぞれ以下の役割です。 Eligibility Policy どのAWSアカウント(もしくはOU)に対して誰が 申請できるか を設定する Approver Policy どのAWSアカウント(もしくはOU)に対して誰が 承認できるか を設定する それぞれ、細かく設定もできますが、運用負担とのトレードオフです。当社では次に記載する方針としました。 Eligibility Policy Eligibility Policyは1つだけ用意し、IAM Identity Centerで管理されている全社員がすべてのAWSアカウントへ申請できる構成としました。 セキュリティの観点では、役割ごとに申請可能なアカウントを絞り込む方が理想的です。しかし、そのためには「権限申請するための事前申請」が別途必要となり、運用負担が大きくなると判断しました。 「申請の入口は広く、承認の出口は厳密に」 という方針のもと、誤った申請や不正な申請は後述するApprover Policyの承認者が拒否できるため、このトレードオフを意図的に選択しています。 Approver Policy Approver PolicyはAWSアカウント単位で用意して、該当するAWSアカウントのJITアクセス承認者となるグループに関連付けます。 これによって、承認可能な人を厳密に制御できるためセキュリティが向上します。 なお、複数のApprover Policyを管理することが大変な場合には、OSSとしてTerraform Providerが公開されています。 registry.terraform.io 初回導入後の状況 ここまでご紹介した方針で、まずは一部の組織へ導入して問題がないことを確認しました。その上で、他の組織に対しても段階的に導入を進めました。 幸いにも大きな混乱が発生することはありませんでしたが、以下のような要望があがりました。 日常的な運用業務において読み取り専用のみでは不十分なケースがある。そのために都度JITアクセス申請するのは運用的に困難。 例えば、日常的な運用作業で必要となる一部の操作がReadOnlyAccessポリシーではカバーされていないケースがありました。 そこで、JITアクセスの例外として 必要最小限のポリシー を持ったAWSアカウントごとに個別のPermission Setを作成できる仕組みを検討しました。 AWSアカウントごとに個別のPermission Set Permission Setの数が増えるということは、全社AWS管理部門の負担も増加します。また、個別の事情に応じてPermission Setを用意できるといっても、JITアクセスのメリットを損なうようでは意味がありません。 そこで以下を要件として仕組みを検討しました。 AWSアカウント毎の個別要件に応じた権限を常時保有できること Permission Setの数が膨大になっても全社AWS管理部門の負担が増えすぎないようにすること 必要最小限の権限になっていることを仕組みで担保すること これらの要件を満たすために検討したアーキテクチャは以下です。 全社AWS管理部門の負担を低減するために、Identity Centerの管理アカウント側では Permission Setの箱だけを用意 します。ポリシーの中身については、 メンバーアカウント側のカスタマー管理ポリシーで設定 してもらう方針としました。これによって、メンバーアカウント側で運用変更が発生しても、全社AWS管理部門側での作業は発生しません。 また、必要最小限の権限となっていることを担保するために、AWS Configを活用したガードレールを用意し、基準を満たさないポリシーを検知できる仕組みとしています。 なお、Permission Set数のクォータは以下の公式ドキュメントで説明されています。上限緩和も可能となっており、当社の規模では不足することはない想定です。 docs.aws.amazon.com まとめ 本記事では、JITアクセスの概念とTEAM選定の理由から、承認フローの設計、大規模組織での段階的展開における調整のポイント、導入後に発生した課題への対応までをご紹介しました。導入から1年以上が経過しましたが大きな問題なく運用が継続しています。セキュリティ向上のためにJITアクセスの導入を検討している方がいれば、ぜひ参考にしてみてください。今後もAWSを安全に運用するためにさまざまな取り組みを行っていこうと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com