TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1006

はじめに こんにちは、情報セキュリティ部の 兵藤 です。日々ZOZOの安全を守るためSOC業務に取り組んでいます。 本記事では、SOCでの業務効率化のためにClaude Codeを活用して、自動アラートトリアージエージェント(以降SOC Agent)を構築した事例を紹介します。 また、情報セキュリティ部ではその他にもZOZOを守るための取り組みを行っています。詳細については以下の「OpenCTIをSplunkに食わせてみた」をご覧ください。 techblog.zozo.com 目次 はじめに 目次 背景と概要 SOC Agentの設計 Agentの全体像 Splunk MCPの活用 OpenCTI MCPの活用 SOC AgentのSubAgent設計 opencti-agent log-search-agent memoryの活用 SOC Agentの運用 SOC Agent Skillsのコマンド notable-response threat-hunting slack-alert-triage SOARの活用 おわりに 背景と概要 ZOZOのSOCメンバーは3人体制で、日々大量のセキュリティアラートを処理しています。これらのアラートは、ZOZOTOWNやWEARなどのプロダクトや社内システムのログから生成され、3人で分担して対応するにしては量が多く、負荷がとても高い状況でした。 そこでClaude Codeを活用して、アラートの内容を分析し、優先的に対応すべきアラートを自動で選別するエージェントを構築することにしました。これにより、SOCメンバーは重要なアラートへ集中できるようになり、効率的な対応が可能になると考えました。 また、このSOC AgentのSkillsによってある程度網羅的な調査が可能になるため、SOCメンバーの負荷軽減だけでなく、アラート対応における質の平準化にも寄与しています。 SOC Agentの設計 このAgentが担うのは、初動対応の切り分けです。一般的にはTier1の初動対応に相当します。具体的なレスポンスや即時対応はSOARで行う想定のため、SOC AgentはRead権限で完結する設計にしています。 Agentの全体像 SOC Agentは以下のようなフロー設計で構築しました。 SOC Agentの全体像 SOCアナリストの指示、またはSlackのアラート通知のもと、Claude Codeが動作 Splunk MCPを通じ、アラートデータや詳細なログデータを取得 ZOZOで活用している脅威インテリジェンスプラットフォーム(TIP)であるOpenCTIから、脅威インテリジェンスを取得 取得した情報をもとに、SOC Agentがアラートの優先度を評価し、対応案を検討 レポートを生成し、Slackに対応サマリを投稿 Slackに対する起動は定期実行も可能で、未処理アラートを自動検知して対応サマリを投稿 Splunk MCPの活用 SOC AgentはSplunk MCPを活用して、アラートデータや詳細なログデータを取得しています。Splunk MCPを活用する際、Splunk Cloudにはレガシーなエンドポイントも存在します。ただし、オンプレミスのSplunkと同様に Splunk MCP Server の公式Splunk Appを利用することが推奨されています 1 。 このAppを利用すると、通常のSplunk APIを利用する場合と異なり、MCP専用のTokenをUserごとに払い出すことが可能です。 Splunk MCP Server このMCP Serverに接続するためには IP allow lists を設定する必要があります。見落としがちな点のため、注意してください。 また、このMCPを利用するためのRoleはこのSOC Agentにおいて基本的に以下の4つで、SPLを実行するためのRead権限があれば十分でした。 search get_metadata indexes_list_all mcp_tool_execute クライアント側の設定は基本的に .mcp.json などのファイルに記載します。以下は .mcp.json の例です。 { " mcpServers ": { " splunk-mcp-server ": { " command ": " op ", " args ": [ " run ", " -- ", " npx ", " -y ", " mcp-remote ", " https://<stack-name>.splunkcloud.com:8089/services/mcp ", " --header ", " Authorization: Bearer ${AUTH_TOKEN} " ] , " env ": { " AUTH_TOKEN ": " op://<vault-name>/<item-name>/<token-field> " } } } } 上記のように1Password CLIを利用してMCP ServerのTokenを取得します。このSOC AgentのコードはチームでGitHub管理しているため、誤ってTokenをコミットしてしまうリスクを避けるために、VaultからTokenを取得しています。 MCPのエンドポイントは443ポートのもの( /en-US/splunkd/__raw/services/mcp )もあります。ただし、内部的には8089ポートにリダイレクトされるのでどちらを使っても問題ありません。 OpenCTI MCPの活用 実際の攻撃手法や攻撃に使われたIOC情報などを取得して危険度を評価するために、OpenCTI MCPも活用しています。OpenCTI MCPはFiligran社から公式に MCP が提供されています。 OpenCTI MCPを利用するにはTokenが必要なため、MCPを使うためのロール、グループ、サービスアカウントの作成が必要です。アカウントの作成に大きな手間はかかりませんが、ロールに関しては実際のAgentにどこまで作業をさせるかで必要な権限が変わります。今回のSOC AgentではRead権限のみで完結するように設計しているため、以下のようなロールを作成しました。 OpenCTI MCP ロール設定 基本的にこの Access knowledge のRoleがあればこのAgentの機能は十分に動かすことができました。caseまで記載させたい場合は別の権限が必要でしょう。 また、グループの作成の際にそのグループがアクセスできるインテリジェンスの範囲を定義できます。これは組織によってAIのオプトアウトの設定やインテリジェンスの発行元、TLPなどを考慮して設定する必要があります。個々の組織にあったポリシーを設定してください。 以下のような画面でグループのアクセス範囲を設定できます。「TLP:RED」のインテリジェンスは定義上見せない設定が基本でしょう。「TLP:AMBER」に関しては状況によるでしょう。 OpenCTI MCP グループ設定 クライアント側の設定は以下の記載で接続できます。 { " mcpServers ": { " opencti-mcp-server ": { " command ": " op ", " args ": [ " run ", " -- ", " <path-to-python-venv>/.venv/bin/python3 ", " -m ", " opencti_mcp.server " ] , " env ": { " OPENCTI_URL ": " op://<vault-name>/<item-name>/<url-field> ", " OPENCTI_TOKEN ": " op://<vault-name>/<item-name>/<token-field> ", " PYTHONPATH ": " <path-to-xtm-mcp> " } } } } このサーバは from opencti_mcp.graphql_queries などの記述でPythonモジュールをimportしているため、 PYTHONPATH の環境設定が必要です。 これらのMCPの設定のように、AIはある程度予期せぬ挙動を取る可能性も考え、与えるTokenの権限を絞ることをまずお勧めします。AIには自由にさせた方が柔軟に対応してくれます。変更されたくない接続先の権限を絞ることで、万が一の暴走リスクを減らせます。 SOC AgentのSubAgent設計 このSOC Agentは、特定のSkillを呼び出すAgentを並列起動できるようSubAgentを設計しています。具体的には、OpenCTIから脅威インテリジェンスを取得するSubAgentと、Splunkからログを取得するSubAgentの2つです。 Agent名 説明 opencti-agent OpenCTI MCPを通じて脅威インテリジェンスを取得するSubAgent log-search-agent Splunk MCPを通じてログデータを取得するSubAgent 全体像は以下のとおりです。 SubAgentの構成 この2つのSubAgentが調査によって数十体並列で呼び出されます。IOCの種類や調査するログの種類によって呼び出すSubAgentを分けることで、効率的に必要な情報を取得できるようにしています。 opencti-agent このAgentはOpenCTI MCPを通じて脅威インテリジェンスを取得するSubAgentです。OpenCTIから攻撃手法や攻撃に使われたIOC情報などを取得して、SOC AgentのメインのAgentがアラートの優先度を評価する際に活用します。基本的には別途設計したOpenCTIへGraphQLを投げる opencti-lookup Skillを参考にしています。大量の調査結果やインテリジェンスを収集する目的で利用するため、分析機能を持たせず、ひたすらクエリを投げる設計にしています。高度な分析を必要としないため、 sonnet のモデルで動かしています。 上記Skillのreferenceに各種GraphQLのテンプレートを記載することで、必要な情報を取得する際のクエリを定義しています。例えば、「Reportの基本情報」を取得する際には以下のようなクエリを定義しています。 query ReportById { report(id: "<REPORT_ID>") { id standard_id entity_type name description published report_types confidence created_at updated_at objectLabel { value color } createdBy { ... on Identity { id name entity_type } } } } 念のため、 Hooks にて create や delete 、リレーションを変更する stixCoreRelationshipEdit などRead権限以外の操作するクエリは禁止しています。 " hooks ": { " PreToolUse ": [ { " matcher ": " ^mcp__opencti-mcp-server__(execute_graphql_query|validate_graphql_query)$ ", " hooks ": [ { " type ": " command ", " command ": " python3 \" $CLAUDE_PROJECT_DIR \" /.claude/hooks/validate-opencti-graphql.py ", " timeout ": 5 , " statusMessage ": " Validating OpenCTI GraphQL safety policy " } ] } ] } この validate-opencti-graphql.py には、禁止するGraphQLのパターンを tool_input から正規表現でマッチさせるPythonコードが記載されています。 SOCの対応でVirusTotalなどの判定を利用するフローも一般的に存在します。一方ZOZOでは脅威情報をOpenCTIに集約しているため、このエージェント単体で完結させています。 また、OpenCTIにはZOZO独自で調査している脅威情報も蓄積しているため、外部の脅威情報と社内の知見を組み合わせてアラートの優先度評価ができるようになっています。例えば以下のようなMalware解析のレポートなどを参照します。 qiita.com log-search-agent このAgentはSplunk MCPを通じてログデータを取得するSubAgentです。NotableのReference IDなどをもとに、アラートの概要を取得したり、より詳細なログを取得するSPLを投げたりするために利用します。こちらも分析機能は持たせず、別途設計した splunk-search Skillをもとに、ひたすらクエリを投げる設計にしています。高度な分析を必要としないため、 sonnet のモデルで動かしています。 上記Skillのreferenceに各種SPLのテンプレートを記載することで、必要な情報を取得する際のクエリを定義しています。例えば、「Notableの概要」を取得する際には以下のようなSPLを定義しています。 index=notable source_event_id="<SOURCE_EVENT_ID>" | sort -_time | head 5 | table _time source_event_id event_id notable_event_id orig_event_id search_name rule_title notable_title signature security_domain urgency severity status status_label owner src dest user host risk_object risk_object_type risk_score info_min_time info_max_time SplunkのMCP側で詳細なSPL制御ができません。そこで、OpenCTI同様に Hooks を利用して outputlookup や outputcsv などの作成・変更を伴うSPLは以下の設定で利用禁止にしています。 " hooks ": { " PreToolUse ": [ { " matcher ": " ^mcp__splunk-mcp-server__splunk_run_query$ ", " hooks ": [ { " type ": " command ", " command ": " python3 \" $CLAUDE_PROJECT_DIR \" /.claude/hooks/validate-splunk-spl.py ", " timeout ": 5 , " statusMessage ": " Validating Splunk SPL safety policy " } ] } ] } このMCPの利用に際して、JSON形式で入れ子になっているログはSubAgentの判断で最初に spath などのSPLを打つことがあります。簡易なログだと問題ありませんが、Endpoint系のログだと大量のkeyに対して展開するため、各種SPLのreferenceを詳細に定義しておく方が安全です。また、サーチマクロを定義しておけば、Agentの調査の平準化やトークン消費の最適化が可能です。 memoryの活用 このSOC Agentでは、過去の調査履歴も活用しています。「このインシデントは過去に過検知フィードバックがあったものだ」や「引き続きこの証跡と同じアクターが関与している可能性が高い」といった情報を基に、調査の効率化や精度向上を図っています。 SOC Agentの運用 SOC Agent Skillsのコマンド このAgentには様々なSkillsを実装しています。その中でもZOZOのSOCアナリストが利用するコマンドに絞ってここでは紹介します。 コマンド 説明 /notable-response NotableのReference IDからインシデントを取得し、自動調査を実行するコマンド /threat-hunting OpenCTIのレポートから脅威ハンティングを実行するコマンド /slack-alert-triage Slackのアラートを自動でトリアージし、調査結果をスレッドに投稿するコマンド notable-response このコマンドでのワークフローは以下のイメージです。 SOCアナリストが /notable-response <Reference ID etc> を入力 SOC AgentがSplunk MCPを通じてNotableの概要を取得 SOC AgentがNotableの内容をもとに、opencti-agentとlog-search-agentを呼び出して、関連する脅威インテリジェンスやログデータを取得 SOC Agentが取得した情報をもとに、Notableの相関分析や脅威判定し、対応案を生成 深掘りが必要な場合は、さらに追加の情報を取得するために3からのステップを繰り返す レポート作成、メモリに事象の内容や調査結果を保存 引数には調査観点の概要を含めて渡すことでカスタムした調査が可能です。 threat-hunting このコマンドでのワークフローは以下のイメージです。 SOCアナリストがSlackで /threat-hunting <Report ID> を入力 SOC AgentがOpenCTI MCPを通じてレポートの内容を取得 SOC Agentがレポートの内容をもとに、関連するIOCや攻撃手法を抽出 SOC Agentが抽出したIOCや攻撃手法をもとに、log-search-agentを呼び出して、関連するログデータを取得 SOC Agentが取得した情報をもとに、脅威ハンティングの結果を分析し、対応案を生成 深掘りが必要な場合は、さらに追加の情報を取得するために4からのステップを繰り返す レポート作成、メモリに事象の内容や調査結果を保存 Report IDは別途ZOZOで活用しているMalware解析Agentの結果を渡すこともできます。 slack-alert-triage このコマンドでのワークフローは以下のイメージです。 SOCアナリストがSlackで /slack-alert-triage <time> を入力 SOC Agentが指定された時間範囲のアラートをSlack MCPを通じて取得 SOC Agentが取得したアラートをもとに、NotableのReference IDを抽出 SOC Agentが抽出したReference IDをもとに、notable-responseと同様にNotableの内容を取得 Notableの内容をもとに関連事象をグルーピング、Slackのスレッドに調査開始の投稿 各グループごとにopencti-agentとlog-search-agentを呼び出して、関連する脅威インテリジェンスやログデータを取得 SOC Agentが取得した情報をもとに、Notableの相関分析や脅威判定し、対応案を生成 深掘りが必要な場合は、さらに追加の情報を取得するために6からのステップを繰り返す レポート作成、Slackスレッドに対応サマリを投稿 脅威度がCritical判定の場合は、SOCアナリストにメンション通知 このコマンドを /loop 1h /slack-alert-triage 1h のように定期実行することで、未処理のアラートを自動的に検知して対応サマリを投稿しています。24時間365日対応が難しい場合でも、こういった自動化を活用することでSOCメンバーの負荷を軽減できます。 SOARの活用 /slack-alert-triage コマンドのポイントはSlackのアラートにReference IDが含まれていることです。通常のSplunk ESの「Edit event-based detection」だと「Adaptive response」はNotableの作成とは別のアクションを実行するため、Reference IDがSlackのアラートに含まれません。そこでSplunk SOARを用いて、NotableのReference IDをSlackのアラートに含めています。こうすることで、SOC AgentがSlackのアラートからNotableのReference IDを抽出し、調査を開始できます。 SOARは以下のformatとSlackへのsend messageの Splunk App の設定だけで済みます。 Splunk SOAR 設定 formatはSlackのblocks項目に以下の記述をすれば、ビジュアライズされたアラートをSlackに送れます。 [ {{ " type ": " header ", " text ": {{ " type ": " plain_text ", " text ": " 🟢 Splunk Finding Detected ", " emoji ": true }} }} , {{ " type ": " section ", " fields ": [ {{ " type ": " mrkdwn ", " text ": " *🔎 Name* \n {0} " }} , {{ " type ": " mrkdwn ", " text ": " *🆔 Reference ID* \n `{1}` " }} ] }} ] {0} にはアラート名、 {1} にはReference IDを入れるように設定しています。これでSOC AgentがSlackのアラートからReference IDを抽出し、調査を開始できます。 SOC Agentの本命のSkillはこのコマンドです。ループ処理することでTier1相当の対応をAIで完全自動化することに成功しており、SOCメンバーの負荷軽減に大きく寄与しています。 以下はSOC Agentの対応例です。 SOC Agentの対応例 おわりに 本記事ではClaude Codeを用いたSOC Agentを紹介しました。SOC Agentの導入によって少人数のSOC業務を改善し、アラート対応の効率化、平準化、SOCメンバーのさらなる高度業務へのアサインを図れました。 ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co About MCP Server for Splunk platform ↩
はじめに こんにちは、データ・AIシステム本部 検索基盤部 検索基盤ブロックの吉永です。 ZOZOは2026年5月30日(土)に東京・新宿のベルサール新宿グランドで開催された「 JJUG CCC 2026 Spring 」にブーススポンサーとして協賛しました。JJUGは、日本におけるJava技術の向上・発展と一層の普及・活性化を目指して設立された、ボランティアメンバーで運営される日本のJavaユーザーコミュニティです。CCCは、JJUGが主催する「Javaに閉じず」「技術に閉じず」オープンソースを中心とする様々なコミュニティから参加者を募って、コミュニティを横断した「クロスコミュニティなカンファレンス」です。 ZOZOTOWNでは、検索、カート決済、ショップ直送、基幹システムなど、多くの領域でJavaを使用しています。私たちが日々使っている技術を支えてくれているコミュニティへ貢献したいという思いから、昨年秋に引き続き今回も協賛しました。 本記事では、ZOZOのエンジニアが気になったセッションの紹介と、ZOZOブースの様子をお伝えします。 目次 はじめに 目次 Javaエンジニアが気になったセッションの紹介 普通のFeature Flag実践入門 AI時代のソフトウェア設計の学び方 不変条件と整合性境界ービジネスが決める設計判断と実現パターン アンカンファレンス(2)「テーマ:AI×レビュー」 Javaコミュニティの関心の変遷を可視化する:JJUG CCC発表データから見る変化と不変 ZOZOブース 「ソウゾウのナナメウエ × Java」 集まった体験談 バージョン・言語仕様 関連 AI 関連 ”沼”系の苦労話 まとめ Javaエンジニアが気になったセッションの紹介 ZOZOのJavaエンジニアが気になったセッションをいくつか紹介します。 普通のFeature Flag実践入門 ZOZOMO部OMOブロックの木目沢です。irofさん( @irof )の『普通のFeature Flag実践入門』を紹介します。 speakerdeck.com Feature Flagについて体系的にまとまった情報があまりなく、非常に役に立つセッションでした。自チームの現状を振り返ると、Feature Flagの利用は散発的でした。導入後の撤去漏れや、AWS AppConfigにおける「インフラ側の管理とコード側の管理を同時に意識しなければならない」という運用コストが課題となっていました。ここ最近は後方互換を保てるリリースだったり、先行リリースしても問題ない状況だったのでFeature Flagをそれほど使わずに済んでいましたが、常にそうした条件が揃うとは限りません。そういう意味でも、今後は積極的に活用していきたいと考えていたので、体系的に説明いただいた今回のセッションは大変貴重でした。 「デプロイとリリースを分離することで、誰が嬉しいのか」という問いが良かったです。その恩恵を受けるのはエンジニアではなくビジネスサイドであるという視点は、改めて重要な示唆を与えてくれるものでした。生成AIの活用によって開発速度が向上しても、リリースプロセスが重ければ効果はあまりありません。自チームはスクラムを採用していますが、スクラムにおいてもスプリントを通じてリリース可能なインクリメントを作り上げることが前提であり、そのリリース自体が重いというのはいい傾向ではありません。 実装面では、Feature Flagの設計におけるON/OFFの制御方針、if/elseの構造、else節の要否といった意思決定が大事であること、撤去が極めて重要であることも改めて確認しました。ログ・監視・マイグレーションとの整合性など、運用上の注意点も網羅されており、実践に直結するセッションでした。 AI時代のソフトウェア設計の学び方 引き続き木目沢が、増田さん( @masuda220 )の『AI時代のソフトウェア設計の学び方』を紹介します。 speakerdeck.com 正解のあるなしではなく、増田さんの考えという前提ではありましたが、自分の仕事の範囲における解釈でもやはり「小さな設計の反復×人の活動支援」が今のところ合っていると感じています。コンテキストの制限やLost in the Middleの現象も避けられない現状を見ると、小さな設計の反復をやっていくほうが良いというのが現時点での自分の理解です。 同様に、「ソフトウェア開発」は「事業開発」と「組織開発」と強く結びついているということもすんなり入ってきました。むしろ生成AIで実装は速くできるようになったので、開発者も事業開発や組織開発といった領域に足を踏み入れる余裕が持てるようになったのではないでしょうか?OMOブロックでも去年からビジネス部門と開発部門が一緒になって仕事をすることを始めています。ビジネス部門と一緒にソフトウェア開発をするのはとても良い体験です。 ソフトウェア開発の根底原則は「事業目的の適合性」と「変更容易性」です。特に「ソフトウェア設計のあらゆる原則とパターンは、この変更容易性の原則の特殊化」という言葉には納得しかありません。 気になるのは、生成AIでこのあたりが変わっていくのか、つまり生成AIが読めればなんでもいいのではないでしょうかという論調も囁かれたりします。少なくとも生成AIは「人の活動支援」というスタンスでいるうちは「変更容易性」の原則は変わらないでしょう。私のケースですが、生成AIが変更容易性の高いコードを書くように色々な設計原則をSkillで伝えているのもそういうことかと思います。 「事業目的の適合性」「変更容易性」を学ぶための初級編として「区分」に注目するのは面白いと思いました。思い返せば、売上に直結するような変更をするのに大体「◯◯区分」に手が入っていました。新しいプラン、プランの内容の変更、ルールの追加や変更などですね。 最後に印象的だったのが、内容はあくまで増田さん自身の考えであると繰り返しおっしゃっていたことです。他にも多くの考え方があると承知した上での内容でした(そのとおりの言葉ではないですが、ニュアンスは合っているかと思います)。そもそも生成AIの時代はかつてなく不確実性が高く、1つの答えが存在するはずもありません。そういう意味でも、今の時代は「人と人との相互作用」がより大事なフェーズではないでしょうか。生成AIがこんなに発展しているのに人が大事とはまた面白い時代ですよね。 不変条件と整合性境界ービジネスが決める設計判断と実現パターン 商品基盤部1ブロックの井草です。nrsさん( @nrslib )の『不変条件と整合性境界ービジネスが決める設計判断と実現パターン』を紹介します。 speakerdeck.com スピーカーのnrsさんも仰っていましたが、朝一から頭をフル回転させるセッションでした。今回のテーマは「不変条件(Invariant)」と「整合性境界(Consistency Boundary)」です。私自身、オブジェクト指向設計に慣れていることもあり、設計を考えるときは「どのオブジェクトをまとめるか」「どこで集約を切るか」から考え始めることが少なくありません。しかし、このセッションで特に印象的だった考え方があります。 「不変条件が先、集約が後」 です。まず考えるべきなのは、システムが守るべきビジネス上のルールです。そして、そのルールが「一瞬たりとも破ってはいけない真の不変条件」なのか、「最終的に整合していればよい遅延可能な不変条件」なのかを見極めることが重要だと説明されていました。 例えば、二重引き落としや二重予約のように、一度破られると後から補償できないルールは即時整合性が必要です。一方で、後から整合性を回復できるものは、プロセスによる結果整合性という選択肢もあります。 なぜこの見極めが重要なのか。それは、すべてを即時整合性で守ろうとすると、トランザクションや集約が肥大化し、システムの変更や分割が難しくなるからです。マイクロサービス化や分散システムを多く扱う今においては、次の問いを持つことが大切です。 「そのルールは本当に遅延を許容できないのか?」 設計者として、この問いを意識することの重要性を感じました。集約から考えがちな自分にとっても、「まず守るべきルールを見つける」という視点は非常に学びの大きいセッションでした。 アンカンファレンス(2)「テーマ:AI×レビュー」 ECプラットフォーム部マイクロサービス戦略ブロックの半澤です。アンカンファレンスの第二部に参加しました。テーマは「AIが生成したコードを全部見るのか」「レビューする手間は増えていないか」で、Javaチャンピオンの谷本さんの進行のもと、ディスカッションが行われました。生成AIを活用した開発が身近になりつつある今、多くのエンジニアにとって関心の高いテーマではないでしょうか。 今回のアンカンファレンスでは、さまざまな観点から意見が交わされました。AIが生成したコードの確認はもちろん、レビューの認知負荷やコードレビューと品質保証の関係、テストや設計で何を担保するのかといった幅広い話題に及びました。 話題の中で興味深かったのは、AIによってコードを書く時間とレビューする時間のバランスが変わってきているという観点です。AIがコード生成を担う場面が増えると、コードを書く時間は短くなります。一方で生成されたコードを確認する作業が連続しやすくなり、レビューの負荷が大きく感じられるのではないか、という話がありました。その負荷を下げる工夫として、コードだけを見るのではなく確認しやすい形を整える方法が挙がりました。AIや自動化を活用して変更内容の可視化・スクリーンショット・テストコード・補助的なドキュメントを用意するのがその例です。 また、コードレビューだけで品質を保証できるのか、という話題も印象に残りました。レビューでソースコードをすべて確認することだけが品質保証ではなく、テストや設計段階での確認など、複数の仕組みを組み合わせて品質を支える必要があります。一方で、レビューによって見つけられる問題があることも確かです。 レビューで何を見るかも、考え方はさまざまです。細かな実装の書き方すべてを見るというより、リスクが高そうな箇所や、自分が実装するとしても迷いそうな箇所を重点的に見るという考え方があります。たとえば次のような観点は、人間が注意して見る価値があります。データの処理方式、アーキテクチャによって影響が大きく変わる部分、テストでは見つけにくい同時処理や状態管理の問題、後から変更しやすい構造になっているかなどです。AIが生成したコードであっても、人間が書いたコードであっても、こうした観点自体は大きく変わらないのだと感じました。 一方で、AIの生成するコードには、生成させた本人も意図を説明しづらい処理が混入することもあります。なぜその実装になっているのか、不要な処理ではないのか、後から変更しやすい構造になっているのか。そうした点を確認するには、単にコードを眺めるだけでなく、テストケースや設計の意図、変更の影響範囲も含めて見ていく必要があります。 品質に対する考え方は、システムの特性によっても変わります。利用者が限られたシステムで、非常に低い確率でしか発生しない不具合であれば、完全に防ぐために大きなコストをかけるよりも、発生時の対応も含めて合理的に判断する方がよいケースもあります。求められる品質水準は、システムの用途や利用規模、影響範囲によって変わるという点も、アンカンファレンスの中で印象に残った話題でした。 私が普段携わっているZOZOTOWNは、多くのユーザーや取引先に関わるサービスです。そのため、一見すると小さな不具合であっても、実際にはユーザー体験や業務に少なからず影響を与えることがあります。 もちろん、あらゆるケースを事前に防ぎきることはできません。しかし、開発工程が後半に進むほど修正コストが大きくなりやすいため、レビューやテストで気づける問題については、できるだけ早い段階で見つけ、未然に防ぎたいと感じています。そして、そのエラーの先には、実際にサービスを利用するユーザーがいます。また、不具合が発生した際には、その対応にあたる営業やカスタマーサポートなどの仲間もいます。小さく見える不具合であっても、そうした対応が積み重なることで、サービスやチームへの信頼に影響するかもしれません。 だからこそ、合理的な判断は大切にしながらも、ユーザーに届けるものに対してできる限り誠実に向き合いたいと思います。今回のアンカンファレンスを通じて、コードレビューを品質保証における重要な工程の1つとして捉える自分の考えを、改めて認識しました。 AIが書いたかどうかにかかわらず、エンジニアとしても、自分たちが届けるものに責任を持つ姿勢は変わりません。AI生成コードとの向き合い方を通じて、自分たちのレビュー観や品質への向き合い方を見直す、有意義なアンカンファレンスでした。 Javaコミュニティの関心の変遷を可視化する:JJUG CCC発表データから見る変化と不変 引き続き半澤が、Ayana Murakamiさんの『Javaコミュニティの関心の変遷を可視化する:JJUG CCC発表データから見る変化と不変』について紹介します。 speakerdeck.com Murakamiさんは、大学院で情報可視化を専攻していた「可視化オタク」として、遡れる限りの過去のJJUG CCCの発表データを分析されていました。松尾芭蕉の言葉として知られる「不易流行」を軸に、変わらず語られ続けているテーマと、時代に応じて関心が高まっているテーマを、テキスト分析と可視化によって読み解く内容でした。 まず、「変わらないもの」として挙げられていたのは、DBやテーブル設計に関するトピックです。業務の現場で複雑な要件をどのようにテーブル設計へ落とし込むのか、変化に強く柔軟な設計をどう実現するのかといったテーマは、継続して高い関心を集めていました。また、ファイルやクラスといったトピックからは、業務システムの設計だけでなく、Javaそのものの仕組みや内部構造への関心も根強くあるようです。前者は現場での永遠のテーマであり、後者はJJUG CCCでこそ聞きたいテーマでもあります。もし同時間帯に両テーマのセッションがあった場合、どちらを聴講するか迷ってしまいそうです。 次に、関心の高さに周期性があるトピックとして、Spring Bootのアップデートや周辺技術も紹介されていました。Spring BootのメジャーアップデートやJava LTSのリリースに連動するという分析は、自分の経験からも納得感がありました。 「変わるもの」としては、GraalVM / Native Image、Gradle / Maven、コミュニティといった、過去により強く関心を集めていたトピックが取り上げられていました。これらは廃れたというより、現在も議論され続けているテーマです。特にコミュニティに関する発表では、キャリアに関する話題も多かったとのことで、技術だけでなくエンジニアとしてどう成長していくかも、JJUG CCCで語られてきた大切なテーマなのだと感じました。周囲にも「JJUGに育てられた」と感じている人がいますし、自分自身もその一人です。技術的な学びだけでなく、エンジニアとしての視野を広げ、自分の歩み方を考えるきっかけを得られることも、JJUG CCCの大きな魅力だと感じています。 近年関心が高まっているトピックとして、2023年以降ではAIやLLMが挙げられていました。JJUG CCCでは生産性向上や効率化、エンタープライズ領域でのAI・LLM活用が多く語られています。一方、海外のJavaカンファレンスではAIをどうサービスに組み込むかという観点の議論が多いという比較も印象的でした。 JJUG CCCには、コンピュータサイエンスや設計の基礎のように長く大切にされてきた「不易」と、AIやLLMのように時代とともに関心が高まる「流行」の両方があります。Murakamiさんは、その根底にはJavaを使って安定したサービスを届けたいという思いがあると述べられていました。基礎的な知識や新しい技術の両方に触れられる場として、JJUG CCCの魅力を改めて感じるセッションでした。 ZOZOブース 「 ソウゾウのナナメウエ × Java 」 今回のZOZOブースのテーマは「 ソウゾウのナナメウエ × Java 」。 お題として「あなたのソウゾウのナナメウエなJava体験を教えて」を掲げ、来場者の皆さんに体験談を付箋に書いてパネルに貼っていただく企画を実施しました。”ソウゾウのナナメウエ”はZOZOのカルチャーです。ZOZOらしくJavaコミュニティを盛り上げたいという思いからこのテーマを企画しました。やらかし談、ドハマりしたバグ、救われた話など、ジャンルを問わずJavaにまつわるエピソードを募ったところ、 約70件 の体験談が集まりました。 参加してくださった方には、JJUG CCC限定の「 Duke×箱猫マックスステッカー 」をプレゼントしました。 集まった体験談 集まった付箋を分類すると、以下のような5つのトピックに分かれました。 トピック 件数 バージョン・言語仕様 19件 Spring・フレームワーク 14件 コミュニティ・その他 14件 JVM・ビルド・運用 13件 AI・他言語・開発スタイル 11件 注目のトピックをいくつか紹介します。なお、付箋に書かれた体験談は、参加者の皆さんの表現を活かすため原文のまま掲載しています。 バージョン・言語仕様 関連 新機能への歓迎と現場の苦労がリアルに表れていました。「recordクラス誕生!」「Virtual Threadはいいぞ」という新機能への期待の声がある一方、「Java 8→21移行中!」という苦労話も。「Springバージョン上げたいけどJavaバージョン上げられない…」「JavaのVerupするしないで顧客ともめがち…」という現場の率直な声も並びました。「まだJava 8(担当)」という付箋には、多くの参加者が共感していた様子でした。 AI 関連 今年ならではの傾向が出ていました。「AIが書いてくれるのでJavaを書けない」「AIでGradleの使い方を忘れた」「AIネイティブすぎてJavaが読めない」という声が集まりました。「コードのライセンスの問題でAIが使えなかった」という実務的な課題まで登場し、AIに任せる快適さと任せすぎることへの不安が同居しています。エンジニアコミュニティとして今まさに向き合っているテーマだと感じました。 ”沼”系の苦労話 「Eclipseでの環境構築で6営業日溶かした(泣)」「GCチューニングで全部止めた」「ビルド時間にコーヒーのめる」「カレンダーの月が0始まり…」など、思わず笑いを誘うエピソードが並びました。苦労話もユーモアに包んで共有されているのが、JJUGコミュニティらしい温かい雰囲気でした。 今回のお題を通じて、バージョン移行の苦労やAI時代の新しい戸惑いといった技術的なリアルはもちろん、まさに”ナナメウエ”な体験まで、Javaエンジニアのリアルな声が集まりました。苦労話ややらかし談を楽しく共有できる。そんなオープンで温かい雰囲気こそが、JJUGコミュニティの魅力だと改めて感じました! ご参加いただいた皆さん、ありがとうございました! まとめ ブースやセッションで交流してくださった皆さん、本当にありがとうございました。これからも一緒に楽しみながらコミュニティを盛り上げていけたら嬉しいです。次のJJUG CCCでもぜひお会いしましょう! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
img.hatena-fotolife[src="https://cdn-ak.f.st-hatena.com/images/fotolife/v/vasilyjp/20260616/20260616114500.jpg"] { width: 600px !important; max-width: 100%; height: auto !important; } こんにちは。ZOZO研究所の莫です。ZOZO研究所はZOZOグループが保有するファッションに関する多様な情報資産を活用し、「ファッションを数値化する」ことをミッションとしている研究組織です。 今回は、2026年6月8日(月)から6月12日(金)にかけてGメッセ群馬で開催された 2026年度 人工知能学会全国大会(JSAI2026) に参加しました。本記事では、JSAI2026でのZOZO・ZOZO NEXTメンバーの取り組み、JSAI2026の様子や参加メンバーの気になった発表を報告します。なお、JSAIに参加したZOZO・ZOZO NEXTメンバーの協力を得て本記事を作成しました。 2026年度 人工知能学会全国大会(JSAI2026)会場入口 JSAI2026とは 企業展示 全体の動向 ZOZO Researchメンバーの発表 [4Yin-A-24] 画像生成AIによる背景生成を用いた最適なフレグランス商品画像の検討 [2F6-OS-19b-04] 事業利用に向けたファッション領域の視覚言語モデル評価用ベンチマーク設計と初期検討 [2L4-GS-5c-05] LLMエージェントを用いたファッションECサイト購買シミュレーション ― トレンドの観測と要因分析を可能にするモデルの提案 ― 気になった研究発表 [1Yin-A-14] パレットクエリに基づくファッション画像のマルチモーダル検索 [2L1-GS-10t-02] 商品説明文を対象としたECRTMとLLMによる反復的トピック洗練に関する一考察 [5I1-OS-3-06] PID制御構造を内包する方策を用いたプロセス始動運転の強化学習 [5M2-GS-2c-04] 相互励起を伴う点過程データに対するグレンジャー因果性検定に基づくグラフ構造学習 [2K4-GS-7b-01] 再構成的画像埋め込みによる直交潜在シフトを用いたテキスト-画像拡散モデルにおける色のバイアス緩和 [3Yin-A-44] 用語辞書を用いたLLMによる設計書の表記ゆれ自動修正手法 [4F5-OS-29c-03] 感情推定を用いた長期対話のための大規模言語モデルによる記憶管理 [2Yin-B-02] 次元削減による日本語埋め込み表現の文章長バイアスの検証 おわりに JSAI2026とは JSAI2026とは、 人工知能学会(JSAI) が主催する2026年の日本最大級のAI学術イベントです。毎年、AIに関する学術分野について広いテーマの発表や、それに関する議論が活発に行われています。2026年の今回は、群馬県・高崎市のGメッセ群馬(群馬コンベンションセンター)を現地会場としてオンラインを併用したハイブリッド形式にて開催されました。今年は人工知能学会全国大会の設立40周年でもあり、40周年記念イベントも併催されました。JSAIの参加者数は年々増加しており、今年は最終日の13時時点で過去最多となる5,246名が参加し、会場は大きな賑わいを見せました。発表件数も大きく増え続け、同じく過去最多となる1,397件の発表がありました。今年、ZOZO NEXTはJSAI2026にプラチナスポンサーとして協賛しました。 企業展示 企業展示ブースでは、ZOZO NEXTの取り組みをポスター形式で紹介しました。ZOZOの多角的なファッションサービスと多様なデータ資産、機械学習やHCIに関する研究・応用事例、そしてZOZO研究所が近年発表した論文について紹介しました。今年も多くの方々にご関心を持っていただき、お話しできたことをとても嬉しく思います。ブースにお越しいただいた皆さま、ありがとうございました。展示していたポスターはこちらです。 ZOZO研究所のミッションとZOZOTOWN、WEAR、ZOZOGLASSなどの情報資産を紹介するポスター ZOZO研究所の研究成果としてファッションAI、集合データAI、ファッションHCI、推薦・検索とML一般の論文を紹介するポスター また、ブースでは触り心地が変化する布( Pinching Tactile Display ) のデモ展示を行いました。ファッションECへの展開だけでなく、車載シートやアクセサリー、衣服の製造支援への展開など、豊かなディスカッションをさせていただきました。誠にありがとうございました。 Pinching Tactile Display 全体の動向 発表の傾向を読むことで、人工知能分野における最新の研究トレンドを横断的に把握できるのもJSAIの良さのひとつです。JSAI2026では、生成AI、基盤モデル、大規模言語モデル(LLM)を中心とした研究が引き続き大きな存在感を示していました。特に、RAG、AIエージェント、安全性評価など、モデルそのものの性能向上だけでなく、実際のシステムとしてどのように活用し、評価し、制御するかに関する研究が多く見られました。 また、マルチモーダル化も大きな流れのひとつでした。言語情報に加えて、画像や音声、実世界の状況を扱う研究が広がっており、AIがより多様な情報を統合的に理解し、活用する方向へ進んでいることが感じられました。こうした流れは、医療、教育、製造、社会課題など幅広い応用にもつながっており、AI研究の対象がより実世界に近い場面へ広がっている印象を受けました。 一方で、AIと社会の接点を扱う研究も目立ちました。AI倫理、バイアス、説明可能性、人間とAIの協調など、AIを「作る」だけでなく、AIをどのように信頼し、説明し、社会の中で受け入れていくかを問う研究が増えているように見受けられました。JSAI2026は、生成AIブーム後の研究が、単なるモデル利用から、社会・産業・実世界の中で使えるAIへと移りつつあることを強く感じさせる大会でした。 ZOZO Researchメンバーの発表 ZOZO・ZOZO NEXTから3件の研究をポスター形式で発表しました。各研究の要約は以下の通りです。 [4Yin-A-24] 画像生成AIによる背景生成を用いた最適なフレグランス商品画像の検討 桐島雅也、荒木諒介、椎橋怜史 https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/4Yin-A-24 pub.confit.atlas.jp ECサイトにおいて商品画像はクリック率(CTR)や購買行動に大きく影響する重要な要素です。特にフレグランス商品のような「香り」という視覚的ではない属性を持つカテゴリでは、商品画像を通じて世界観や使用シーンを補完することが求められます。一方、近年の画像生成AIの発展により、商品コンセプトに応じた背景を自動生成するなど、商品画像の制作手法が多様化しています。しかし、「どのような商品画像が実際にユーザ行動に有効か」については十分に検証されていませんでした。また、商品画像に背景を付与すること自体の有効性や、背景の情報量・複雑さが与える影響についても明らかではありませんでした。 そこで本研究では、商品説明文や商品タグからLLMを用いて背景生成用のプロンプトを作成し、画像生成モデルによって商品画像の背景のみを生成するパイプラインを構築しました。さらに、ZOZOTOWN上で実際にA/Bテストを実施し、「背景なし画像」と「背景を生成した画像」のCTRを比較しました。また、オンライン実験だけでは把握しきれない背景デザインの影響を詳細に分析するため、1,502件の商品画像と約50万人分のログデータを用いたオフライン実験を実施しました。この際、マルチモーダルLLMを用いて商品画像を「背景なし」「シンプルな背景」「派手な背景」の3クラスに自動分類し、背景の複雑さとCTRの関係を分析しました。 実験の結果、オンライン実験では、背景を生成した画像のCTRが、背景のない画像と比べて相対的に10.90%向上する傾向が確認されました。さらに商品ごとに分析したところ、余白の多いシンプルな背景ではCTRが相対的に38.24%向上しました。一方、情報量の多い派手な背景では54.40%低下するケースも確認されました。オフライン実験においても、同様に「シンプルな背景」が最も高いCTRを示し、逆に「派手な背景」は最も低いCTRとなることが確認されました。これらの結果から、フレグランス商品のような視覚的ではない属性を持つ商品では、背景による世界観の補完は有効であると考えられます。しかし、背景の情報量が過剰になると、ユーザの注意が分散し、認知負荷の増加によって逆効果となる可能性も示唆されました。 本論文では、フレグランス商品のような視覚的ではない属性を持つ商品画像のデザインにおいて、「背景を付与すること」だけでなく、「背景の見た目の複雑さを制御すること」も重要であるという知見が得られました。また、実サービスでのA/Bテストと大規模ログ分析を組み合わせることで、商品画像のクリエイティブ設計を定量的に評価するアプローチや、LLMを用いた画像分類・評価手法についても示しています。 ポスター発表中の桐島さん [2F6-OS-19b-04] 事業利用に向けたファッション領域の視覚言語モデル評価用ベンチマーク設計と初期検討 サイ タウンカン、清水 悠揮、桜井 詩音、冨田 勇人、佐々木 北都、戸塚 将、久保利 彩、森本 陽菜、川田 心、宮園 太貴、清水 良太郎 https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/2F6-OS-19b-04 pub.confit.atlas.jp 視覚言語モデル(VLM)をファッションEC業務へ適用する際には、既存の汎用ベンチマーク(MMMU等)が一般物体やシーン理解に偏っているという課題があります。そのため、色・素材・スタイルといったファッション固有の要素や、EC運用に直結する情報抽出タスクを十分に評価できていません。また、汎用スコアの高いモデルであっても、業務品質を満たすとは限らず、タスクごとのモデル選定材料が不足していました。 そこで本研究では、評価ベンチマークを提案します。このベンチマークは、タグ抽出・色分け・品質判定・シーズン判定・素材判定という5種類のタスクで構成されます。これらのタスクは、入力画像の種類によって整理できます。ユーザー投稿などの「全身コーディネート画像」にはタグ抽出と色分けを割り当てました。EC商品の「アイテム単体画像」には品質判定・シーズン判定・素材判定を割り当てました。プロンプトについては、まず人手で設計した標準プロンプト(Canonical Prompt)を用意しました。さらに、これだけではモデルごとのプロンプト相性の差を捉えきれません。そのため、各モデル自身に最適なプロンプトを提案させました(Model-Proposed Prompts)。これらを全モデルに適用して評価するMulti-Prompt Evaluation Frameworkを構築しました。最後に、商用モデル6種類とOSSモデル2種類を同一条件で比較しました。 初期検証の結果、次の4点を確認しました。なお、各タスクの精度はWeighted-F1(クラスごとのF1をサンプル数で重み付けした平均)で評価しています。第1に、タスクごとに最適なモデルが異なり、軽量モデルが高性能なフラッグシップモデルを上回るケースも存在しました。たとえば品質判定タスクでは軽量なgpt-5-miniが、シーズン判定タスクでは軽量なgemini-2.5-flash-liteが最高性能を示しました。いずれも自系列のフラッグシップモデル(gpt-5.2、gemini-3-pro-preview)を上回っています。第2に、色分けタスクのWeighted-F1は商用モデルで0.94以上に達した一方、素材判定タスクは最高でも0.28程度にとどまりました。タスクごとの認識難易度に大きな差が存在します。第3に、プロンプトを変えてもエラーの傾向はモデルごとに一貫しており、どのプロンプトを使うかよりも、どのモデルを選ぶかの影響が支配的でした。第4に、提供モデルのバージョン更新であっても、タスクによって結果が異なりました。実際に、gemini-2.5-flash-liteからgemini-3-pro-previewへの更新を例に挙げます。この更新では、Weighted-F1が13.9ポイント改善するタスクがある一方、15.7ポイント低下するタスクもありました。 本記事からは、ファッションVLMを事業へ適用する際に、「タスク別のモデル選定」「プロンプトに対する頑健性の検証」「継続的なモニタリング」が必要であることを示しました。さらに、その定量的根拠も得ることができました。 [2L4-GS-5c-05] LLMエージェントを用いたファッションECサイト購買シミュレーション ― トレンドの観測と要因分析を可能にするモデルの提案 ― 柴田 悠生、清水 良太郎、山下 遥 https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/2L4-GS-5c-05 pub.confit.atlas.jp 推薦システムは、ユーザーの嗜好に基づいて情報を提示し、意思決定を支援します。一方で、提示される選択肢に偏りが生じ、市場における購買集中やトレンド形成に影響を与える可能性があります。しかし、現実の市場では、口コミや広告、季節性など、多様な要因が同時に作用します。そのため、推薦システム単独がトレンド形成にどのように関与しているのかを分離して検証することは困難です。また、トレンドが生じた場合でも、形成に関与する消費者属性や意思決定の構造を分析することは容易ではありません。 そこで本研究では、LLMを用いた仮想顧客エージェントによるマルチエージェントシミュレーションを提案します。ZOZOのアンケートデータに基づき、21属性を持つペルソナを生成しました。さらに、推薦システムによって商品が提示される場合と、推薦へ依存せずに商品を探索する場合を比較できる仕組みを導入しました。これにより、推薦の有無が与える影響を検証可能にしました。また、カテゴリ選択・商品評価・購買理由の生成を段階的に行うプロンプトを設計しました。その結果、エージェント属性と購買理由の対応関係を事後分析できる構造を実現しました。さらに、購買集中を成長率と購買規模に基づいて定量化するトレンドスコアも定義しました。 100エージェント×50イテレーションの実験では、推薦システムによって商品が提示される条件でのみ、顕著なトレンド創発が観察されました。この結果から、推薦が購買集中の形成へ関与する可能性が示されました。さらに、ブランド意識度や価格感度といった属性に応じて、購買理由の分布が体系的に変化することも確認されました。そのため、消費者の意思決定の構造が購買集中と結びついていることが示唆されました。本記事からは、推薦がトレンド形成へどのように関与するのかを分析する視点を得ることができます。加えて、消費者属性に基づく意思決定の構造と購買集中の関係を分析する視点も得られます。さらに、トレンド創発を観測し、要因分析を可能にするシミュレーション設計の考え方も得ることができます。 気になった研究発表 [1Yin-A-14] パレットクエリに基づくファッション画像のマルチモーダル検索 雨宮佳音、八島大地、勝又圭、杉浦孔明(慶應義塾大学) https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/1Yin-A-14 pub.confit.atlas.jp ファッションECの商品検索では、「青みがかったグレー」や「くすんだピンク」のような微妙な色のニュアンスを言葉だけで伝えるのが難しいという課題があります。本研究は、自然言語の説明文に加えて、カラーピッカーで指定した色(パレットクエリ)を組み合わせてファッション画像を検索するタスクを提案しています。ポイントは、パレットクエリを単純なハードフィルタとして扱えない点です。ユーザが複数の色を指定した場合、それらは「ピンクの生地にゴールドのボタン」のように衣類の異なる部位に対応しうるため、すべての色を同列に一致させるのではなく、どの色をどの程度重視するかという優先順位付けが必要になります。また、同じ色の衣類でも素材や撮影条件によって画像上の見え方は変わるため、指定色と完全一致する商品だけに絞り込むと、本来適合するはずの商品まで除外されてしまいます。指定色に近い色まで許容する柔軟な扱いが求められるのはこのためです。 提案手法は2つのモジュールから構成されます。1つ目のIntent-Palette Fusion Moduleでは、LLMを用いて説明文を抽象度ごとに分解・構造化したうえで、RGBをCIELAB色空間へ変換したパレット特徴量から言語特徴量へのクロスアテンションを計算し、説明文中の色に関する表現を選択的に強調します。2つ目のConfidence-Based Relaxed Alignment Moduleは、対照学習で問題となる「ラベルは付いていないが実際にはクエリに適合してしまう画像(unlabeled positive)」への対処です。MLLMでテキスト・画像ペアごとに信頼度を推定し、類似度が信頼度を下回る場合にのみペナルティを課す緩和版の対照損失を導入することで、通常のInfoNCEで生じる「本来似ているペアの類似度まで抑制されてしまう」問題を回避しています。 実験はMarqo Fashion200Kにパレットクエリを付与して行われています。学習データにはセグメンテーションとスーパーピクセルのCIELABクラスタリングによる自動付与、テストデータにはECの検索UIを模したインタフェースでの手動付与と、データセット構築まで含めて丁寧な設計です。結果として、提案手法はR@1で74.6%を達成し、最良のベースラインを9.4ポイント上回りました。パレットクエリなしの構成でも73.8%と高く、ユーザが色を指定しない場合でも性能が落ちない点も実用上は重要です。定性例では、「red dress」を含む説明文とパレットの組に対して提案手法が目標画像を1位に返した一方、SigLIPでは指定した色よりピンク寄り・濃い赤の商品が上位を占め、目標画像は37位に沈んでいました。 色は購買判断を大きく左右する一方で、言葉で正確に表現するのが最も難しい属性の1つです。カラーピッカーという誰にとっても直感的なUIで検索意図を補えるこのアプローチは、1枚のRTX 4090で学習約20分、1600枚に対する類似度計算が約0.8秒という軽さも含めて実運用のイメージが湧きやすく、ファッションECの検索体験にそのまま繋がりそうな研究だと感じました。 [2L1-GS-10t-02] 商品説明文を対象としたECRTMとLLMによる反復的トピック洗練に関する一考察 鷹羽慧、王嘉翊(早稲田大学)、楊添翔(慶應義塾大学)、邵騰飛、後藤正幸(早稲田大学) https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/2L1-GS-10t-02 pub.confit.atlas.jp ECサイトでは「家電」「食器」「スポーツ用品」といったカテゴリ体系が一般的に用いられる一方で、「初心者向け」「携帯性」といった、ユーザの利用シーンや機能的なニーズに基づく商品間の関係を十分に捉えきれません。本研究では、このような意味的な商品関係を商品説明文から抽出し、「朝食・ティータイム」「衝撃吸収」「装飾的」といった解釈しやすい分類軸として整理することを目指しています。また、「一人暮らし向け」と「省スペース性」のように、事前には体系化されていなかった商品間の潜在関係を発見することにも繋がります。 このような分類軸を人手で設計するには大きなコストがかかるため、商品説明文にトピックモデルを適用して自動抽出するアプローチが考えられます。しかし、商品説明文は一件あたりの記述量が少なく、語彙のばらつきも大きいため、従来のトピックモデルでは意味が曖昧で解釈しづらいトピックが生成されやすいという課題があります。また、LLMによるトピック洗練を全てのトピックに適用すると計算コストが大きく、ノイズを含むトピックに対して不適切な意味付けが行われる可能性もあります。 そこで提案手法では、Embedding Clustering Regularization Topic Model(ECRTM)で商品説明文からトピックを抽出したのち、その中からトピックの意味的な一貫性のスコアで絞り込んだうえで、LLMによる意味的評価も加味して有用なトピックのみを選別したうえで、トピックの文脈に合わない単語を特定し、意味的一貫性が高まるように置き換えます。さらに、洗練後のトピック情報をECRTMの学習に反映することで、トピック生成とLLMによる洗練を反復的に行います。これにより、ノイズを含むトピックの影響を抑えつつ、既存カテゴリを横断する解釈しやすい商品分類軸の抽出を実現します。 日用品、キッチン用品、スポーツ用品やアウトドア用品といった商品データを用いた評価実験を実施し、提案手法はトピックの意味的一貫性を表すCoherence Valueをベースラインの0.2750から0.4163まで向上させつつ、トピックの多様性を表すTopic Diversityも高く維持しました。また、定性的にも「本格カフェ・朝食スタイル」「衝撃吸収・振動制御ギア」など、既存カテゴリをまたいだ解釈しやすい商品群が得られており、ECにおける商品整理や企画立案への応用可能性が示されています。 [5I1-OS-3-06] PID制御構造を内包する方策を用いたプロセス始動運転の強化学習 村松航祐、池本隼也、橋本和宗(大阪大学) https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/5I1-OS-3-06 pub.confit.atlas.jp 非線形な挙動をみせる非定常運転に対して、PID制御や線形モデル予測制御などに基づく従来のプロセス制御方式は適用が難しく、現場では熟練運転員の経験に基づく手動操作に依存していることが多々あります。そこで非線形かつ高次元の状態をもつシステムに対する制御器設計法として期待されている深層強化学習(Deep Reinforcement Learning: DRL)が候補にあがりますが、DRLは学習の不安定性や効率の悪さが指摘されており、プロセス制御への応用障壁の1つとなっています。これに対するアプローチとして、ニューラルネットワークとPID制御の構造を強化学習の方策に組み込むControl-Informed Reinforcement Learning(CIRL) が提案されており、学習効率、制御性能、および外乱に対するロバスト性の向上が期待されます。 しかしCIRLは初期状態と目標状態が大きく乖離している状況下では、CIRLによって目標状態へ到達可能な方策の学習が困難な場合があります。そこで、方策に組み込まれたPID制御部分に最終的な目標状態を与えるのではなく、最終的な目標値と現在値の間に中間の目標値を与え、段階的に最終目標状態に近付ける方策モデルをこの研究では提案しています。 数値実験では既存の手法より提案手法の方が、応答速度と目標値への追従性能に関して向上したと示されており、強化学習を導入したことによる課題をシンプルな発想で解決した点が面白いと感じました。 [5M2-GS-2c-04] 相互励起を伴う点過程データに対するグレンジャー因果性検定に基づくグラフ構造学習 竹中 敦史(大阪大学大学院情報科学研究科)、福井 健一(関西大学ビジネスデータサイエンス学部) https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/5M2-GS-2c-04 pub.confit.atlas.jp 本発表では、多次元イベント発生系列データから次元間の因果構造を表現する有向グラフを推定する方法を提案しています。Hawkes過程は点過程と呼ばれる確率モデルの一種で、自己励起のあるイベント発生のモデリングに用いられます。地震発生の統計モデリングの例が典型的で、大きな地震(=イベント)が発生してしばらくは余震などの大きな地震が再び起きやすい、といったような形でイベント発生時系列をモデル化します。多次元Hawkes過程ではさらに複数種のイベントの発生とそれらの間の相互励起の構造を取り入れることができます。すなわち東京で起きる地震と大阪で起きる地震を、相互作用を考慮しながらそれぞれモデリングできるというイメージです。 本研究では多次元Hawkes過程において、どの次元で発生したイベントがどの次元に影響を与えるかを、グレンジャー因果性検定を用いて推定する方法を開発しています。なおグレンジャー因果は、単に「大阪で発生した地震のデータを使うことで東京の地震を予測しやすくなるかどうか」というような時系列データの関係性を示す概念で、因果推論などで考えられる因果とは似て非なるものです。 提案法ではある次元dに対するグレンジャー因果性をもつ次元の組を調べるために、その次元を考慮することによって最も対数尤度を最大化されるような次元d’を同定し、d’からdへの作用の有無をグレンジャー因果性検定によって調べます。帰無仮説が棄却されればさらに{d, d’}の組に対して最も対数尤度を最大化するd’’を追加して再びグレンジャー因果性検定を適用します。この一連の流れを、検定が棄却されなくなるまで多重検定の補正をしながら繰り返します。 実験結果では適切にグレンジャー因果性検定の有意水準を定めることにより、既存法よりも偽陽性のケースが抑えられ、F1スコアの意味でも良い結果が得られたとのことでした。アルゴリズムのシンプルさなどから実用上の使いやすさの面でも秀でており、一致性などに関する理論的な議論や組合せ最適化との関連など、様々な広がりが予想される面白い発表だと思いました。 [2K4-GS-7b-01] 再構成的画像埋め込みによる直交潜在シフトを用いたテキスト-画像拡散モデルにおける色のバイアス緩和 藤谷 恒輝、邵 之昊、田邉 克晃、渡辺 健太、山崎 俊彦(東京大学) https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/2K4-GS-7b-01 pub.confit.atlas.jp 画像生成AIを使っていて、指示した色と異なる色のオブジェクトが生成されてしまう、といった経験をしたことはないでしょうか。text-to-imageモデルでは、「黄色いサンタクロース」といった典型的な色と異なる指示を与えても、学習データ中で頻繁に観測される色が優先されてしまうことがあります。本発表はこのような色バイアスを緩和するために、テキスト埋め込みへ介入するアプローチを提案しています。 本手法は介入するためのベクトルを得るステージと、バイアスを緩和しつつ画像を生成する2ステージに分かれます。本研究がユニークなのは、介入するためのベクトルをfine-tuningしたCLIPの画像エンコーダから得ている点です。このfine-tuningでは画像の埋め込みベクトルをStableDiffusionの入力とし、与えられた画像を再構成する形で行われます(ステージ1)。ステージ2ではテキストからの画像生成の際に、テキスト条件とは無関係な画像の埋め込みベクトルにスカラーαを乗算した上で、テキスト埋め込みに足し合わせて色のバイアスを緩和した生成を実現します。ただし現時点ではスカラーαは、色の反映が難しいテキストプロンプトごとに、-0.15から0.15の範囲で手動にて探索する必要があるそうです。 一見ステージ2で与えられるランダムな画像の構成に影響されそうですが、αの絶対値を十分小さくすることで、入力画像の構成に影響を受けずにバイアスを緩和することに成功しています。論文では足し合わせるベクトルとしてガウシアンノイズを採用した場合と比較されており、本手法で得られたベクトルの有効性を示しています。また興味深いことに、本手法で得られた画像埋め込みベクトルは、テキスト埋め込みベクトルとほぼ直交することを報告しています。 色のバイアスがガウシアンノイズに対し頑健であること自体も知見であり、直交するベクトルが色のバイアスの緩和に有効であった結果は重要かと思います。解釈には議論の余地がありますが、意味の成分を持たないベクトルが「オブジェクト」と「よくある色」の結びつきを緩和するという考察も興味深いと感じました。 [3Yin-A-44] 用語辞書を用いたLLMによる設計書の表記ゆれ自動修正手法 西川 和寿1、是枝 祐太1、森 靖英1、大井田 駿1、福井 大輔1(1. 日立製作所) https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/3Yin-A-44 pub.confit.atlas.jp 生成AIを設計書レビューに活用する取り組みは増えていますが、実際の開発現場ではプロジェクト固有の略語や業界用語が多く、単純にLLMへ「表記ゆれを直して」と指示するだけでは十分な精度が得られません。本研究では、設計書から固有用語を抽出して意味付きの「用語辞書」を自動生成し、その辞書をLLMに与えながら表記ゆれを検出・修正する手法を提案しています。さらに、表記ゆれを「漢字/ひらがな」「略称/正式名称」「半角/全角」など6種類に分解して個別に推論させることで、検出漏れを減らす工夫も行われています。 評価では、金融システムの基本設計書30件を対象に、単一プロンプト方式と比較しました。その結果、表記ゆれ種別ごとの反復実行によって指摘率が向上し、さらに用語辞書を追加することで平均修正成功率は0.325から0.432へ改善しました。特に小規模モデルでも改善効果が見られ、モデル性能だけに頼らず、タスク分解とドメイン知識の付与が有効であることを示しています。 ZOZOのようなECサービス開発でも、設計書や仕様書には社内固有の用語や略称が数多く登場します。また、商品管理やレコメンド、物流など複数ドメインが混在するため、用語の不統一は認識齟齬やレビューコスト増加につながります。本研究は「LLMを賢くする」のではなく、「社内知識を辞書として与え、チェック観点を分割する」という実践的なアプローチであり、設計書レビュー支援だけでなく、要件定義書や運用ドキュメントの品質向上にも応用できそうだと感じました。 [4F5-OS-29c-03] 感情推定を用いた長期対話のための大規模言語モデルによる記憶管理 南谷優里、長野雅俊、谷口忠大(京都大学、電気通信大学、立命館大学) https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/4F5-OS-29c-03 pub.confit.atlas.jp LLMを用いた対話システムでは、ユーザーと長期的にやり取りする中で、「何を記憶し、何を忘れるべきか」が重要な課題になります。すべての会話を保存すれば情報の取りこぼしは少なくなりますが、データベースが肥大化し、検索効率や応答品質の低下につながる可能性があります。一方で、単純に古い情報を忘却してしまうと、ユーザーにとって重要な経験や好み、人間関係に関する情報まで失われてしまいます。 本研究では、「強い感情を伴う出来事は長期記憶に残りやすい」という心理学的知見に基づき、LLMを用いた記憶管理手法を提案しています。具体的には、ユーザー発話に含まれる感情の強さを推定し、それを記憶の重要度計算に反映します。その際、感情強度に加えて、記憶が過去に参照された頻度や現在の対話との関連度を数値化し、それらを組み合わせて各記憶の重要度スコアを算出します。 これにより、ユーザーにとって意味のある対話内容を優先的に保持し、相対的に重要度の低い情報を忘却することが可能になります。 実験結果では、提案手法はすべての発話を保存するRAGほど高い正答率ではないものの、忘却機能を持つ既存手法の中では最も高い正答率を示しました。また、長期的に保存する記憶数を抑えながら、ユーザー理解に必要な情報を保持できる点も確認されています。本研究は、長期対話エージェントにおいて、単に多くを記憶するのではなく、ユーザーにとって本当に重要な情報を選択的に記憶することの重要性を示した研究だと感じました。 特に、パーソナルAIや対話型アシスタントのように、ユーザーとの継続的な関係性が求められる場面では、このような記憶管理は重要になると考えられます。今後、感情だけでなく、ユーザーの目的や状況、対話の文脈なども組み合わせることで、より自然で信頼できる長期対話システムにつながる可能性があります。 [2Yin-B-02] 次元削減による日本語埋め込み表現の文章長バイアスの検証 鈴木 彰人、田代 雄介(三菱UFJトラスト投資工学研究所) https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/2Yin-B-02 pub.confit.atlas.jp 本研究は、入力文の長さによってテキスト埋め込みモデルの出力がどう変化するか(文章長バイアスが存在するか)を、次元削減の手法などを用いて調べた論文です。文章長以外の条件(意味など)をできるだけ揃えるため、モデルの入力にはウェブ上の日本語の記事と要約文がペアになったデータセットを用い、埋め込みモデルの出力が入力の長さによってどう変化するかを調べています。 実験では、複数の埋め込みモデルの出力を様々な観点で分析しています。最初に、埋め込みのL1ノルムの大きさを比較すると、いずれのモデルでも要約文の方が要約する前の文章よりもノルムが大きくなることが明らかとなりました。また、埋め込み表現に対して主成分分析(PCA)を行い各主成分の値の差分を比較すると、上位主成分の値が要約前と要約後で大きな差が出るモデルと、そうではないモデルに分かれる結果となりました。一方、独立成分分析(ICA)を行って同様の分析をした場合、モデルによらず文章長の違いを表す成分が得られる結果となりました。これらの分析により、いずれのモデルも入力の長さや情報密度の違いを情報として(大なり小なり)保持することが示されました。文の意味や言語の違いではなく、文の長さに注目してテキスト埋め込みを分析する研究はあまり見たことがなかったので、面白いと感じました。 おわりに 本記事では、JSAI2026の参加レポートをお伝えしました。今年もJSAIに参加し、業界の最新動向を俯瞰するとともに、多くの新たな知見を得ることができました。また、発表を通じてフィードバックをいただけたこと、スポンサーブースで弊社の取り組みをご紹介できたことは、大変貴重な経験となりました。今回得た知見を今後の研究開発へ活かし、さらなる成果の創出と業界の発展への貢献を目指して、引き続き邁進してまいります。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com zozonext.com hrmos.co
はじめに 2025年新卒のブランドソリューション開発本部ZOZOMO部OMOブロックの東谷です! 私は筋トレが趣味なのですが、増量期(筋肉をつけるために体重を増やす時期)が終わろうとしています。早く痩せなきゃと思いつつ、つい揚げ物や甘いものを食べ、現実から逃げている今日この頃です。 早いもので入社からもう1年が経ちました。この1年を振り返って一番強く感じているのは、 スクラムは「アジャイル開発の手法」であると同時に、新卒にとっての最高の学習環境だった ということです。 配属直後の自分は、リファインメントの議論についていけず、実装中もどこから手をつけてよいか分からない状態でした。それでも1年後、チームの中でスクラムを一通り回せるようになりました。この変化は、研修や独学だけではなく、スクラムの各イベントそのものに大きく支えられました。 スクラムがよく語られるのは「ビジネス価値を最大化する仕組み」としての側面です。しかし新卒視点から見直してみると、これらはすべて先輩たちの思考プロセスを短時間で観察し、その場で質問できる場でもありました。書籍やドキュメントでは身につきにくい「思考の型」、つまり先輩がどんな問いを立て、何をよりどころに判断しているかという基準があります。これを学べる環境として、スクラムは新卒にとって非常に整った構造を持っていると感じています。 この記事では、「思考の型」を自分が新卒1年目に具体的にどう身につけたかを振り返ってみます。題材として取り上げるのは、プロダクトバックログリファインメント(以下、リファインメント)、スプリントプランニング(以下、プランニング)、モブプログラミングの3つです。前者2つはスクラムイベントで、モブプログラミングはチームで採用している開発プラクティスです。新卒エンジニアには「スクラムは新卒の最高の学習環境になりうる」という視点を、新卒受け入れを担う開発組織には育成設計のヒントを提供できればと思っています。 目次 はじめに 目次 配属直後の自分とチームの環境 スクラム開発で身についた3つの学び 1. リファインメント:機能を「課題解決の手段」として見る視点が身についた 先輩が立てる「問い」が、自分の視野を順に広げていった 議論を経て、機能の質と学びが見えた 2. プランニング:Acceptance Criteriaで「実装の自由度」を制御することを学んだ Acceptance Criteriaで「実装の自由度」を制御する 見積もり精度はAC定義の解像度の問題に集約される 3. モブプログラミング:「事実ベースで実装する」という姿勢が身についた 先輩は事実から方針を決めていた 「事実から始める」という型を学んだ AI時代に、思考の型はどう活きるか おわりに 配属直後の自分とチームの環境 配属前の自分にとって「開発」とは、仕様が決まったタスクを実装することとほぼ同じ意味でした。 学生時代に長期インターンとして参加していたのは、ITベンチャー企業のBtoBプロダクト開発チームでした。そのチームのバックログには仕様まで書かれたタスクが並んでおり、エンジニアはその中から自分でタスクを取って作業する開発サイクルでした。タスクはリーダーが起票していき、起票時点で何を作るかは決まっています。自分の仕事は、それをコードに落とす精度と速度を上げることだと思ってました。 そんな自分が配属先の今のチームに入ったとき、まず驚いたのは開発サイクルの「広さ」でした。 リファインメントでは、プロダクトバックログアイテム(PBI)の目的や受け入れ条件を整理し、チームで認識を揃えながら実装可能な粒度まで要件を具体化します。プランニングでは、スプリントゴールを踏まえてチーム全員で作業内容を確認し、相対見積もりをしながらスプリント内で達成する内容を決定します。開発中はモブプログラミングを通じてリアルタイムに知識共有と意思決定を行います。スプリントレビューでは完成したインクリメントをステークホルダーとともに確認し、次の方向性を議論します。 この1週間のスプリントを、新卒の自分も初日からチームの一員として回すことになります。それまでの開発経験と決定的に違ったのは、バックログにタスクが並ぶ前の段階から、自分も議論に参加するという点でした。 もう1つの驚きは、議論についていくために要求される知識の量です。リファインメントやプランニングでは、複数の前提知識を踏まえた議論が当たり前のように展開されます。例えば、自分の担当しているサービスが他のサービスとどう連携しているか、CQRSで構築されたデータの流れはどうか、認証基盤との関係はどうなっているのかなどです。配属直後の自分は、議論の中で出てくる用語の半分も追えていない状態でした。 「自分が貢献できるだろうか」。最初の数週間、率直に言ってそう感じていたのを覚えています。 ところが、振り返ってみるとこの環境が、自分にとってこれ以上ない学習機会になっていました。スクラムの各イベントが、まさに知識、経験が足りていない新卒にとって最適な学習装置になっていたからです。 スクラム開発で身についた3つの学び ここからは、新卒1年目で身についた3つの学びを、具体的なエピソードとともに紹介します。 1. リファインメント:機能を「課題解決の手段」として見る視点が身についた 私が所属しているチームでは、ZOZOTOWN上でブランド様の店舗在庫を確認し、商品の取り置きができるサービス「 ZOZOMO店舗在庫取り置き 」を開発しています。複数のマイクロサービスが連携し、CQRSを採用しているため、イベントを通じたデータの流れを理解することが開発の前提になるプロダクトです。 配属されてしばらく経った頃、ブランド様の店舗情報をシステム上で更新できる機能を実装しました。それまでは開発チームが手動で対応しており、完了までに3営業日ほどかかっていました。ブランド様を待たせることになり、運用者の認知負荷や作業時間も大きいという課題がありました。 議論に参加する前、私の頭にあったイメージは簡単でした。「入力フォームを作って、POSTで更新するAPIを叩けば完了」。配属されたばかりの私は、機能を「実装するもの」として捉え、実装イメージが浮かんだ時点で完成像が見えたつもりになっていました。 ところが、実際のリファインメントの議論はまったく別の地点から始まりました。 先輩が立てる「問い」が、自分の視野を順に広げていった 最初に出てきたのは、システム構造に対する問いでした。認証基盤との関係、マイクロサービスごとの責務、そしてデータがどのサービス間をどのように流れるのか、といった内容です。私が「POSTを1つ作ればいい」と思っていた機能は、実際には複数のサービスにまたがってイベントを発行し、各サービスがそれぞれの責務でデータを更新していくものでした。CQRSやイベント駆動の設計は独学で吸収するには時間のかかる領域ですが、議論の場で出てくる用語や設計判断についてその場で質問できることで、認知負荷の高い情報を一気に吸収できました。 次に、システム設計が具体化してくると、想定していなかった論点が次々と出てきます。「店舗情報が更新されたことをどう確認するのか」「確認するためにはどのデータが必要か」。考えるべきことが膨らんでいくなかで、先輩から自然に出てきたのが「 この機能はそもそも何を解決するものだったっけ? 」という問いでした。 そこから議論は、機能を実装する話からユースケースを実演してみる話に切り替わります。実際の運用者の動きを想像し、ときには運用者に直接ヒアリングしながら、ユーザー体験ベースで仕様が具体化されていきました。 例えば、「店舗情報として更新できるデータは何があるのか」を全部洗い出すところから始まりました。さらに「運用者が更新ボタンを押す前に、入力ミスがないと安心できる状態は何か」「更新した後、本当に意図通りに反映されたかをどう確認するか」など、ユーザー体験の細部にまで問いが続いていきます。これらに応える形で、機能の中身が具体化されていきました。 さらに出てきたのが、「この機能を実装した後の恒久的な運用フローはどうなるか?」という問いです。「今回のケース以外にも対応できる汎用性って必要だっけ?」「逆に今回のユースケースに限定すれば不要となる実装ってないっけ?」のように汎用化を考える問いと、削ぎ落とすための問いが、同じ場で同時に飛び交っていたことが印象的でした。 象徴的だったのが、ブランド情報の鮮度をめぐる議論です。ZOZOMOのシステム構成上、店舗が所属するブランド様の情報が変わったとき、システム側は正常に更新されますが、その変更がリアルタイムで運用ツールに表示されない仕組みでした。そのため運用上、「正確なデータを変更しているかどうか確認したい」と運用者から開発側へ問い合わせを受けるケースの発生も予測されました。今回の店舗情報の更新機能を考えるうえで、この運用上の課題へ手を入れる余地があると判断し、ブランド情報を最新状態として取得し直す機能を同じ画面の中で組み込むことになりました。この機能を実装した結果、関連する問い合わせは発生していません。目の前の機能要求だけでなく、運用される将来の状態まで含めて考えることで、機能の質が変わっていく瞬間でした。 議論を経て、機能の質と学びが見えた リファインメントの議論を経て、最終的な機能は、私が当初抱いていたイメージとは大きく違うものになりました。最終的にできたのは、店舗情報の更新作業だけでなく、その前後で必要になる作業まで1画面で完結できる機能でした。 1画面に統合したのは、更新前の確認(店舗IDから店舗名・ブランド名を表示)、更新後の確認(変更後データの表示)、そしてブランド情報の鮮度を保つための再取得機能の3つです。これにより、運用者は別ページに遷移したり、別件で開発側に問い合わせをしたりする必要がなくなりました。効率よく作業できる機能に仕上がっています。 この一件で体感したのは、機能の「核となる実装」は全体のごく一部にすぎないということでした。POSTのAPIとUIという核は確かにイメージできていましたが、それが実際のユーザーへ届きビジネス価値へとつながるまで、想像をはるかに超える議論と設計判断が積み重なっていました。 リファインメントに新卒のうちから参加できたことで、私は機能を「実装するもの」ではなく「課題を解決するもの」として見る視点を、議論の中で自然に身につけることができたと感じています。これは、先輩から受け取った最初の思考の型のひとつでした。 2. プランニング:Acceptance Criteriaで「実装の自由度」を制御することを学んだ リファインメントで十分実装が可能だと判断されると、次はプランニングでスプリントの計画を立て、タスクの洗い出しやタスクの規模を見積もります。配属直後の私にとって、この時間はリファインメント以上に難しいものでした。 規模の見積もりには、チームごとに蓄積されるベロシティ(過去のスプリントでどれくらいの規模を消化できたかという感覚値)があります。「このくらいの規模の変更ならだいたい何ポイント」という相場が、過去の実装経験から自然と形成されていきます。配属されたばかりの自分にはそれがなく、対象機能の核となる変更部分の理解もまだ浅い状態で、規模を出すのは正直難しい作業でした。 では、先輩はどう見積もっているのかなと気になりました。観察して印象的だったのは、実装を頭の中で先取りして見積もるやり方でした。ZOZOMOではDDDやCQRSを採用しているため、これはコードベースを頭の中で走らせる形になります。例えば「Query側のStore集約のUsecaseでデータを整形して、Infra層に店舗集約をUpsertするクエリを書いて、UnitTestとE2Eを書いて…」といったイメージです。見積もりは「数字の当てっこ」ではなく、この工程をどれだけ正確に頭の中で走らせられるかなのだと理解しました。そして、その想像力を支えているのは、過去の実装を積み重ねてきた経験にほかなりませんでした。 Acceptance Criteriaで「実装の自由度」を制御する 想像力と経験がある程度身についた状態で、より規模を正確に見積もり、要件を満たすために考えるべき重要な指標があることに気づいたのは、1年経ってからでした。それが Acceptance Criteria(受け入れ基準、以下AC)の解像度 です。 ACとは、その機能が「完成した」と判断するための条件を具体的に言語化したものです。重要なのは、ACの粒度が実装の自由度をコントロールするということでした。 象徴的だったのが、ブランド様と店舗の紐付けを除外する設定を確認する機能の実装です。これはリファインメントの結果、ブランド様ごとに検索ができ、絞り込んだ結果をExcelとして出力する機能も追加するスコープに広がりました。Excelはメールで該当ブランド様に送り、店舗とブランド様の紐付きが正しいかを確認してもらうためです。 このときのACの一例は、「検索後、Excel出力ボタンを実行したタイミングでポップアップが表示され、絞り込みした店舗の属するブランド一覧がポップアップに表示されること」と定義しました。 このACは、自由度の制限の仕方が絶妙でした。検索のクエリや検索ロジック、Excel生成の方法そのものには制限がかかっていません。より良い実装方法があれば実装時に改善できる余地が残されています。一方で、出力時にポップアップで対象データを一覧表示するという最終的な体験の部分は明確に固定されています。これは複数ブランドを指定して検索できる仕様上、運用者が「どのブランド様のExcelを送ろうとしているんだっけ?」を実行直前に視覚的に確認できる状態を保つことで、ヒューマンエラーを抑えるためです。 ACが「実装の細部までガチガチに固定する文書」になっていると、開発者は工夫の余地を失います。逆にACが曖昧すぎると、実装中に判断を迫られる回数が増え、規模が大きくブレます。ACは、考えてよい部分と考えなくてよい部分を明確に切り分け、実装の自由度を意図的にコントロールするための装置なのだと、この実装を通じて理解しました。 見積もり精度はAC定義の解像度の問題に集約される ACの粒度をこの形でコントロールできていたから、実装中に時間をかけて考える部分と、考えずに型通り進めてよい部分の切り分けが明確でした。結果として、見積もった規模と要件の両方を、ある程度の確度で同時に守れる構造になります。 規模そのものを正確に当てに行くのではなく、ACの粒度をコントロールして実装中の判断回数を制御します。プランニングを1年続けた末に言語化できたのは、「見積もり精度の問題は、その手前のAC定義の抽象度に集約される」という知見でした。 数字を出すこと自体に意識を向けていた1年前の自分から、今は「ACで実装の自由度を制御することで、規模と要件の両立を狙う」ことに意識を向けるようになりました。プランニングの場の捉え方がこのように変わったことが、この1年で起きた最も大きな変化の1つです。ACの粒度を意図的に調整するという考え方も、私のなかに定着した思考の型のひとつです。 3. モブプログラミング:「事実ベースで実装する」という姿勢が身についた リファインメントとプランニングを経て、いよいよ実装フェーズです。私のチームでは実装の多くをモブプログラミングで進めます。複数人が同じ画面を見ながら、ナビゲーター役とドライバー役を交代しつつコードを書いていく形式です。 モブプロから学んだことは数多くありますが、今の自分に最も大きく影響しているのは「 事実ベースで実装する 」という姿勢です。 先輩は事実から方針を決めていた リファインメントやプランニングで要件をどれだけ詰めても、実装段階で「考慮しきれていなかったこと」は必ず出てきます。とくにマイクロサービス間でデータを連携している箇所では、外部要因も絡んで挙動が読みにくくなります。 ZOZOMOでも、他のマイクロサービスとイベントを通じたデータ連携をしています。しかし、さまざまな要因によりイベントの連携順序が入れ替わり、本来連携されるべきデータを正しく届けられないケースもありました。この問題に対処する仕組みを実装した際、モブプロで先輩のアプローチを間近で見たのが印象的でした。 先輩はまず、入れ替わりが起きたデータを全件出してくるところから始めました。一部ではなく全体を並べて共通項を探すと、どの経路の、どのタイミングで入れ替わりが起きているのかという事実が浮かび上がってきます。 そこから先輩が見出したのは、ZOZOMO側の開発基盤にデータが連携される箇所で、入れ替わりそのものを直すという根本的な解決策でした。全件のデータという事実から出発したからこそ見つけられた解決策です。 「事実から始める」という型を学んだ このやり方の何がすごいかというと、 実装の前に「何を解決すべきなのか」が事実として明らかになっている ことです。事実から始めれば「この具体的なケースを直すには何が必要か」という明確な目的が手元にあります。結果として、不要な処理が混ざらず、シンプルでビジネス価値の高い実装にたどり着きやすくなります。 このとき自分が何より学んだのは、「とりあえず動かしてみる」のではなく、手元の事実をしっかり集めてから設計に入るという姿勢でした。新卒1年目で身につけた中でも、実装に向かう前の心構えとして特に役立っている型です。 事実ベースで判断するという姿勢も、モブプロを通じて自分のなかに残った思考の型のひとつです。 AI時代に、思考の型はどう活きるか この1年は生成AIの普及によって「情報を知っていること」自体の価値が一気に下がった年でもありました。ドキュメントを読み込む、仕様を覚える、エラー文を検索する。こうした若手エンジニアの仕事の一部だった作業の多くを、AIがあっという間に肩代わりする時代です。 だからこそ、ここまで紹介してきた「思考の型」の価値が、以前より一段はっきり見えてきた気がしています。ACをAIに書かせることも、実装方針をAIに提案させることもできます。しかし出てきた出力が正しい方向に向かっているかを評価し、軌道修正の指示を出すには、自分の中に判断の物差しが必要です。 思考の型を自分の言葉で持っていれば、それはそのままAIへの的確な指示に変わります。たとえば、3つの学びはそれぞれ次のような指示につながります。 リファインメントで身につけた「これは何を解決する機能か」という問いは、「この機能の目的はこうだから、それに沿った実装案を出して」という指示になる プランニングで身につけた「ACで自由度を制御する」思考は、「実装手段は任せるが、最終的な体験はこう固定したい」という指示になる モブプロで身につけた「事実ベースで判断する」姿勢は、「推測で進めず、まず該当データを全件出してから方針を決めて」という指示になる AIに何を問いかけ、その答えをどう評価し、どこで意思決定するか。その一連の判断は、思考の型を自分の足場として持っている人間にしかできないと思います。 おわりに 1年を振り返って、スクラムは新卒にとって 先輩の思考の型を最速で学べる装置 として機能しました。リファインメントで問いの立て方を学び、プランニングでACの解像度を上げる感覚を掴み、モブプログラミングで事実ベースの実装の進め方を身につけました。一つひとつは技術書を読んでも身につかず、リアルタイムの観察と質問なしには手に入らないものでした。 スクラムは「アジャイルに開発するためのフレームワーク」として語られることが多いです。しかし新卒として配属された自分の視点からは、先輩たちの思考にアクセスする頻度を最大化する仕組みであり、かつAI時代に価値を持ち続ける能力を集中的に育てる仕組みでもありました。 これから配属を迎える新卒エンジニアの方には、配属先のチームがスクラムで動いているなら、それを「ただの開発手法」とは思わずに思考を盗む1年にしてほしいと伝えたいです。技術知識を吸収する場としてだけでなく、思考の型をコピーする場として向き合うと、得られるものの密度が大きく変わります。 新卒受け入れを担う開発組織の方には、スクラムを生産性の文脈だけで評価せず、新規メンバーの学習装置としての側面にも目を向けてもらえると嬉しいです。先輩の思考に高頻度で触れられる場が組織にあるかどうかは、人材の立ち上がりスピードに大きな差を生むはずです。 自分自身は、まだスクラムから学べることの入口に立ったところだと思っています。2年目以降は、観察する側だけでなく、自分の思考の型を他のメンバーに見せていく側にも回っていきたいです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、SRE部 検索基盤SREブロックの 富田 です。2026年5月4日〜5日の2日間、New Yorkで開催された「 AI Agent Conference 2026 」に参加しました。 本記事では、現地の様子と印象に残ったセッションをご紹介します。 目次 はじめに 目次 AI Agent Conferenceとは 特徴 現地の様子 セッションレポート (1) Architecting for the Agentic Customer: Systems Design for Non-Human Actors (2) What Agents Want: Beyond One-Size-Fits-All Retrieval Systems (3) Measuring & Evaluating Agentic AI (4) Workflow Democratization & Operating in an Accelerated Development Environment おわりに AI Agent Conferenceとは AI Agent Conference は、 エンタープライズ領域における自律型AI(Autonomous AI/Agentic AI) をテーマとする国際カンファレンスです。研究系のAIカンファレンスとは異なり、AIエージェントのエンタープライズ実装に焦点を当て、技術・運用・戦略にわたる深い知見が交わされます。 特徴 本カンファレンスの特徴は、 「1つのカンファレンス、3つのAgenticテーマ」 というコンセプトでセッションが構成されている点です。2026年は以下の3テーマで議論が展開されました。 Agentic Enterprises:AIエージェントによるビジネスオペレーションの変革 Agentic Engineering:AIエージェントシステムを支えるインフラと設計 Agentic Industries:金融・医療・法務・ロジスティクスなど業界別のユースケース 現地の様子 本カンファレンスは、New Yorkにあるヒルトン・ミッドタウンホテルで開催されました。来場者数は3,000人を超える規模でした。 Agentic Engineeringトラックは来場者数が多く、入場制限のかかるセッションも複数あるほどの人気でした。AIエージェントへの注目度が、想像していた以上に高かったというのが率直な印象です。 会場の様子 会場には70を超える企業ブースが並び、来場者がその場でプロダクトを体験できるブースも多く、終日盛況でした。 ブースのマップ 体験型のブース セッションレポート 検索機能を担当するSREとして、特に印象に残ったセッションを4つ紹介します。 (1) Architecting for the Agentic Customer: Systems Design for Non-Human Actors 本セッションでは、AIエージェントが消費者に代わって買い物をする「Agentic Commerce」に向け、EC事業者やプラットフォームが取るべきアーキテクチャ設計について紹介されました。 GoogleのHeiko Hotz氏によるセッション AIエージェントによる自律的な購買行動を支えるプロトコルは、ここ1年で急速に整備されています。代表例として、A2A(Agent-to-Agent Protocol)、AP2(Agent Payments Protocol)が挙げられます。さらに2026年1月にGoogleがUCP(Universal Commerce Protocol)を発表しました。 発表後の数週間でAmazon・Meta・Microsoft・Shopify・SalesforceがUCPに参画しました。業界全体で、エージェント間通信の標準化が一気に進みつつある状況です。 こうして「外側」の仕組みが急速に整う一方で、LLMベースのエージェント本体には本質的な弱点が残っています。ハルシネーション、予算を超えた購入、悪意のあるサイトに操作される脆弱性です。研究では、エージェントが偽の認証情報や虚偽の主張に騙されるケースが確認されています。セッションでは、これを「人間と同じ感覚で野に放つのは危険」と表現されていました。 セッションで提示されたのは、 サンドイッチアーキテクチャ と呼ばれる設計パターンです。LLMの強みと、入力が同じなら必ず同じ結果を返す決定論的レイヤー(ルールベースの処理層)を組み合わせ、エージェントの暴走を構造的に抑え込みます。 サンドイッチアーキテクチャの説明スライド 第1層(LLM):自然言語の意図を構造化属性に変換する。「スコットランドのハイランドをハイキングするための防水ジャケットがほしい」という曖昧な要求を、防水等級・サイズ・想定気温などの具体的な属性に落とし込む 第2層(決定論的レイヤー):UCPなどのプロトコルに準拠したAPI経由でEC事業者のデータを取得し、属性で確定的にフィルタする。ここでLLMは介入させず、ルールベースで候補を絞り込む 第3層(LLM):最終決定する。必要なら第1〜2層に戻ってループする このアーキテクチャでは、決定論的なフィルタリングが「LLMが暴走しても結果が破綻しない安全弁」として機能します。 EC事業者側の責務として、 AEO (Agentic Engine Optimization) という概念も紹介されました *1 。エージェントは画像や装飾ではなく、構造化された属性データしか見ません。商品ページがいくら美しくても、属性データに「急速充電に対応」という項目が抜けていれば、エージェントはその商品を選びません。実験では、ある属性が欠落しているだけで、エージェントは25ドル以上の価格差を付けないと購入対象に含めないという結果も示されました。 最後に、取引の全過程を後から追跡できる記録(監査証跡)の設計が、エージェント時代の信頼性の土台になる、という話で締められました。 AI時代の安全性は、LLMを賢くするだけでなく、その周りに置く決定論的レイヤーをどう機能させるかも重要だと感じました。また、エージェント駆動の購買が広がれば、商品検索の相手は「人間のユーザ」と「AIエージェント」両方になります。商品データの属性を漏れなく構造化し、AEOの観点で「エージェントから評価される」ことも、ECプラットフォームの検索機能の要件として加わってくると思いました。 (2) What Agents Want: Beyond One-Size-Fits-All Retrieval Systems 本セッションでは、エージェントの記憶や作業メモリの役割を果たすコンテキストレイヤーに求められる要件と、それを支えるベクトルデータベース(LanceDB)の設計思想について紹介されました。 LanceDB CEOのChang She氏によるセッション これまでのRAG (Retrieval-Augmented Generation)は、ユーザのクエリ1つに対して数件のテキストを返すだけで成立する世界でした。しかし、エージェントが本番環境で動き出すと、扱うデータとクエリパターンは一変します。 エージェントは計画を立て、並列で検索を投げます。各種ツールを実行して得られた中間結果を随時メモリに記録し、バラバラの情報を要約・集約しつつ、もし行き詰まったら一歩手前のステップに逆戻りして別のルートから調べ直す、といった自律的な試行錯誤を行います。 その結果、コンテキストレイヤーが管理すべきデータはテキストだけでなく、PDF・スクリーンショット・テーブル・動画フレーム・オーディオクリップ・イベントログ・JSONログまで膨らみます。こうしたデータの広がりに合わせて、クエリの種類も多様化します。意味的検索だけでなく、キーワード検索や、特定の顧客IDかつ過去7日間といった構造化フィルタへの対応も必要です。さらにデータの来歴(どの埋め込みモデルを使ったか、いつ書き込まれたか)も管理対象になります。 これらを別々のシステム(ベクトルDB・データレイク・OLAP・メタデータストア)に分散させると問題が生じます。エージェント自身が「どのツールを使うべきか」「返ってきた結果をどう統合するか」の判断にトークンと推論能力を浪費してしまいます。 LanceDBはこの課題に対し、以下のアプローチを取っています。 コンテキスト・来歴・特徴量・埋め込みベクトルの単一テーブル管理:エージェントはSQL、セマンティック検索、フルテキスト検索を同じテーブルに対して投げられる。ツール選択や結果統合の手間が消える データタイプ別ストレージ戦略の抽象化:インラインカラム、ページ単位管理、外部URL参照といったサイズに応じた格納戦略をエージェントから隠蔽する エージェント時代のスケール対応:100億行規模・10K QPSの書き込みは、従来のベクトルDBには想定外の負荷。p99レイテンシを悪化させず安定動作するよう、インデックス・シャーディング・量子化を再設計 バージョニングと再現性:エージェントが探索した分岐の特定時点に戻れる、デバッグ時のリプレイができる仕組み ベンチマーク結果では、エージェントフレームワーク標準のメモリ機能(インメモリの単純な検索)の取得精度が約52%なのに対し、LanceDBに置き換えると約76%まで上がります。取得時間も80秒台から数秒に短縮されると報告されていました。精度を上げると遅くなる、ではなく、正しい構造を選ぶと精度と速度が同時に上がるという結果は、素直に面白いと思いました。 また、AIエージェントが普及していくとデータ量やトラフィック量が大きく変化するため、今後の検索に求められる要件はさらに拡張されていくと感じました。 (3) Measuring & Evaluating Agentic AI 本セッションは、本番環境で動くエージェントの評価とモニタリングをテーマにしたパネルディスカッションでした。 エージェントのトレースは長く、深く、複雑です。1つのインタラクションの中で多段のChain-of-Thought(段階的に推論を重ねる思考過程)が走り、複数のツール呼び出しが連鎖します。そのため、「何が起きたか」を後追いで評価するには専用の仕組みが必要になります。 加えて、LLM-as-a-judge(LLM自身を評価者として使う手法)は、シンプルなユースケースなら安価ですが、エージェントが複雑化すると評価コストが急騰します。判定にも高価なモデルを使うため、本番規模ですべての出力をjudgeで評価する運用はコスト面で困難です。 さらに、オフライン評価(事前に用意した正解集に対する評価)と本番モニタリング(実行時の観測)が分断されており、両者をどう接続するかが課題になります。組織によって重視する軸もコスト・速度・リスクと異なるため、画一的な指標も置きにくい状況です。 パネリストからは、以下のような実践的なアプローチが共有されました。 指標はユースケース起点で設計する:「営業支援エージェントなら何を測るか」をユースケース単位でアカウンタビリティ・フレームワークとして定義する 「Cost per Successful Outcome」を中心指標に:単なる精度やレイテンシではなく、成功した結果1件あたりのコストで全体を見る LLM-as-a-judgeのコスト最適化:評価ごとに高価なLLMを呼ぶとコストが膨れ上がるため、本番規模ではSLM(小規模言語モデル)などの軽量モデルに切り替え、品質を保ちながらスケールさせる オフライン評価と本番モニタリングの継続的フィードバックループ:本番の挙動をオフライン評価に取り込み、評価セットを継続的に更新する 既存の観測スタックに乗せる:OpenTelemetryなど既存の観測基盤にエージェントの計装を統合し、モデル選択(LLMとSLMの使い分け)まで1つのダッシュボードで管理する また、Microsoftの agent-governance-toolkit も紹介されていました。ポリシー強制・サンドボックス・OWASP Agentic Top 10対応など、エージェント運用全体を標準化しようとするツールキットです。 個人的に印象に残ったのは「Cost per Successful Outcome」という指標の話でした。精度やレイテンシを個別に追うのではなく、成功した結果1件あたりのコストで全体を見るという発想です。この観点で見ると、高価なLLMを使い続けるよりも、品質を保ちつつ徐々に安価なモデルに切り替えていくのが自然な方向だと感じました。実際、パネルディスカッション内でも同様のアプローチが議論されていました。 「エージェントが本番で失敗したとき、誰が責任を取るのか(エンジニア・プロダクトオーナー・エージェント自身)」という問いも面白かったです。「もはやみんなが互いの役割を担うようになっているので、全体を1つのシステムとして見るしかない」というのもAI時代の形だと思いました。 (4) Workflow Democratization & Operating in an Accelerated Development Environment 本セッションでは、NVIDIA Applied AI Labが10週間で25個のCLIツールを4人で構築した実例を交えて、AIエージェントのみで完結する開発パイプラインの設計思想が紹介されました。 NVIDIA Applied AI LabのJulie Yaunches氏によるセッション 冒頭で「AIが生成できるコード量と、コードレビュー・CI・自動テストといった既存の開発プロセスが吸収できる量の間に大きなギャップが生まれている」と問題提起がありました。このギャップを放置すれば、品質劣化・技術的負債の蓄積・アーキテクチャドリフトが避けられません。セッションでは、10月以降は一行も自分でコードを書いていない、それでも品質を担保する仕組みが必要だった、というエピソードも紹介されました。 NVIDIA Applied AI Labが採用しているのは、 Research・Gates・Sweeps という3つの実践です。 Research:何かを作り始める前に、開発対象のコードベースを理解するためのエージェント。コードベースの構造をダイアグラムとして生成させ、それに対して「この部分をもう少し詳しく」と対話的に深堀りすることで、設計の理解を深め、どこを変更すべきかを見極められるようにする Gates:PRがマージされる前のチェックを行うエージェント。PRレビューはもちろんのこと、変更内容に応じてどのテスト群(CI・自動テスト・ハードウェア上のE2Eテスト)を走らせるべきか判断し実行する Sweeps:PRがマージされた後の「掃除」を行うエージェント。アーキテクチャドリフトを定期的に検出し、技術的負債を刈り取る ポイントは、これらを独立に運用するのではなく、 Sweepで見つかった問題を新しいGateに昇格させる という継続的なキャリブレーションです。この仕組みにより、人が介在することなくAIエージェント間のみで品質を高めるサイクルが出来ています。 「4人のエンジニアで10週間に25個のCLIツールを構築し、いまも1日30〜40 PRをマージし続けている」という規模感に驚きました。 コーディングそのものをほぼAIに任せ、エンジニアはAIが自律的に動くためのワークフローを設計・チューニングする側に回っています。AIエージェントとの協働が進めば、エンジニアの仕事の重心もこちら側に寄っていくのだろうと感じました。 おわりに 本記事では、「AI Agent Conference 2026」の参加レポートをお届けしました。 全セッションを通して一番気になったのは、「これまで人間の活動リズムに沿って変動していたトラフィックが、AIエージェントの普及によって24時間絶え間なく発生するようになる」という話でした。複数のセッションでこのテーマが繰り返し言及され、このトラフィックの変化により膨れ上がるデータ量も従来の比ではないと語られていました。 またAIエージェントの普及を前提に、その負荷に耐えるインフラを考えるセッションも多くありました。AIエージェント普及後のトラフィックに向き合い方は、数年で大きく変わっていくのだろうと感じました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからご応募ください。 corp.zozo.com *1 : GEO(Generative Engine Optimization)と呼ばれる場合もあります
はじめに こんにちは、MA部SREブロックの片桐です。MA部ではメルマガやLINE、アプリプッシュ通知を配信するためのマーケティングオートメーションシステムを開発・運用しています。 MA部ではDBとして主にCloud SQL for MySQLを利用しており、調査や不具合対応のために開発メンバーがDBにログインして各種SQLを実行する場面があります。 このとき、共用の特権DBユーザーとパスワード認証を利用していました。しかし、この方式ではパスワード管理が必要になるほか、DB上のログイン主体も個人に紐づけにくい状態でした。 これらの課題を解決するために、人間によるDBへのログイン方式を、共用の特権DBユーザーとパスワード認証から個人のGoogle Cloudアカウントを使ったIAM認証へ移行しました。 あわせて、IAM認証でログインする各ユーザーには通常時は参照権限のみを付与し、書込系権限が必要な場合だけGitHub Actionsの承認付きワークフローから一時付与する運用にしました。 本記事では、共用DBユーザーによる運用から個人のIAM認証を使った運用へ移行した背景と、MySQLロールの一時付与を実現するための構成例を紹介します。 目次 はじめに 目次 従来の運用の課題 強い権限が常時使える 個人単位で追跡できない 目指した状態 全体構成 IAM認証で個人ログインにする IAM認証の有効化 IAMデータベースユーザーの作成 Cloud SQLへのログイン権限の付与 MySQLロールでDB内権限を分ける 通常時と一時付与用のロール ロールの作成 参照用ロールの付与 GitHub Actionsを承認ゲートにして書込系ロールを一時付与する GitHub Environmentで申請者以外の承認を必須にする 権限操作用サービスアカウントの作成 ワークフロー設定例 一時付与した書込系ロールを剥奪する 手動でロールを剥奪する 定期実行でロールを剥奪する 運用上の注意点と今後の改善 一時付与したロールを使うときの注意 ワークフロー入力値の検証 SQL本文のレビューと監査 一時付与した権限の失効タイミングの厳密化 MySQLロールの粒度 おわりに 従来の運用の課題 従来の構成ではデータベース操作用の共用特権DBユーザーを作成し、パスワード認証でCloud SQL for MySQLへログインしていました。 構成としては次のとおりです。 ここで利用している Cloud SQL Studio は、Google Cloudコンソール上からCloud SQLへ接続してSQLを実行できるWebベースの画面です。 この運用では、複数人が同じDBユーザーを使ってログインします。そのため、主に次のような課題がありました。 強い権限が常時使える 日常運用におけるデータ調査であれば、多くの場合は SELECT を実行できれば十分です。 しかし、共用の特権DBユーザーを使うと、参照だけで済む作業時でも特権によりデータ変更やDDL操作まで実行できてしまいます。 強い権限を持った状態でSQLを実行すると、誤操作時に本来不要だったデータ更新やスキーマ変更まで起きてしまう可能性があります。そのため、通常時は参照のみを許可し、必要なときだけ書込系権限を一時的に付与する運用にしたいと考えました。 個人単位で追跡できない 共用ユーザーでログインするため、データベースから見ると誰が操作しても同じユーザーに見えます。 そのため、DB上のログイン主体を開発メンバー個人のGoogle Cloudアカウントと結びつけにくい状態でした。 人間のDBログインを個人のIAM認証に寄せることで、少なくともDBへのログイン主体は個人単位で扱えるようになります。 目指した状態 共用特権DBユーザーの課題を踏まえ、今回の移行では次の状態を目指しました。 人間によるDBへのログインを、共用ユーザーではなく個人のGoogle Cloudアカウントに紐づける 通常時は参照権限のみを付与する 書込系権限は常時付与せず、必要なときだけ一時的に付与する 書込系権限の付与申請を簡単に行えるようにする 書込系権限の付与には、申請者以外の承認を必須にする 付与した書込系権限は、作業後または定期実行で剥奪する 今回の構成では、Cloud SQLへのログインにはIAM認証を利用します。一方で、ログイン後にどのSQLを実行できるかはMySQL側の権限で制御します。 さらに、書込系権限は常時付与せず、必要なときだけ承認付きで一時付与して、作業後または定期実行で権限を剥奪します。 そのため、今回の構成ではログイン可否、DB内権限、権限の一時付与、付与後の剥奪を次のように分けて考えました。 項目 役割 利用する仕組み 認証 誰がCloud SQLへログインできるかを制御する Cloud SQL IAM認証 認可 ログイン後に何を実行できるかを制御する MySQLロール 一時付与 必要時だけ書込系権限を付与する GitHub Actionsの承認付きワークフロー 剥奪 一時付与した権限を戻す 手動または定期実行のREVOKEワークフロー この構成により、通常時は参照権限のみを使い、書込系権限が必要な場合だけ承認付きで一時的に付与する運用にしました。 全体構成 今回構築した仕組みは、通常時のログイン経路、必要時における書込系権限の一時付与フロー、一時付与した権限の剥奪フローに分かれます。 通常時は、開発メンバーがCloud SQL Studioから自分のGoogle CloudアカウントでCloud SQL for MySQLへログインします。 本記事ではCloud SQL Studioから接続する例で説明しますが、接続元はこれに限りません。IAM認証に対応した接続方式であれば、同じ考え方を適用できます。 Cloud SQLへのログイン可否はIAMで制御し、ログイン後に実行できるSQLはMySQLロールで制御します。通常時は、開発メンバーに参照用のMySQLロールのみを付与します。 IAM認証へ移行した後の通常時の構成は次のとおりです。 この状態では、開発メンバーはCloud SQL Studioから SELECT を実行できます。一方で、書込系権限は通常時には付与しません。 データ修正などで書込系権限が必要な場合は、GitHub Actionsの手動ワークフローを実行します。ワークフローはGitHub Environmentの承認待ちになり、申請者以外のメンバーが承認すると、対象ユーザーに書込系のMySQLロールを一時的に付与します。 書込系権限を一時付与する流れは次のとおりです。 一時付与した書込系ロールは、作業後の手動実行または定期実行で剥奪します。剥奪の流れは次のとおりです。 剥奪は権限を戻す操作であるため、今回の例では付与時のような承認ゲートは設けていません。手動実行または定期実行でGitHub ActionsからSQL実行基盤を起動し、MySQLロールの REVOKE を実行します。 以降のコード例では、次のプレースホルダーを使います。 プレースホルダー 意味 YOUR_PROJECT_ID Google CloudプロジェクトID YOUR_PROJECT_NUMBER Google Cloudプロジェクト番号 YOUR_MEMBER_NAME 開発メンバーのメールアドレスの @ より前の部分 YOUR_MEMBER_DOMAIN 開発メンバーのメールアドレスのドメイン YOUR_SA_NAME 権限操作用サービスアカウント名 YOUR_DB_NAME 対象のデータベース名 YOUR_TABLE_NAME 対象のテーブル名 YOUR_COLUMN_NAME 対象のカラム名 YOUR_WIF_POOL Workload Identity Pool名 YOUR_WIF_PROVIDER Workload Identity Provider名 YOUR_GITHUB_ENVIRONMENT_NAME 承認ゲートとして利用するGitHubのEnvironment名 IAM認証で個人ログインにする まず、個人のGoogle CloudアカウントでCloud SQL for MySQLへログインできる状態を作ります。 Cloud SQL for MySQLでIAM認証を利用するため、主に次の項目を設定しました。 Cloud SQLインスタンスでIAM認証を有効化する 開発メンバーごとのIAMデータベースユーザーを作成する Cloud SQLへログインするためのIAMロールを付与する Cloud SQL Studioを利用するためのIAMロールを付与する これらの設定は、Google Cloudコンソール、gcloud CLI、Terraformなどで行えます。MA部ではインフラ設定をTerraformで管理しているため、以降ではTerraformでの設定例を示します。 IAM認証の有効化 Cloud SQL for MySQLで IAM認証 を有効化するには、インスタンスのデータベースフラグ cloudsql_iam_authentication を有効にします。 resource "google_sql_database_instance" "main" { # name, database_version, region などは省略しています settings { database_flags { name = "cloudsql_iam_authentication" value = "on" } } } IAMデータベースユーザーの作成 次に、開発メンバーをIAMデータベースユーザーとして作成します。 人間のGoogle CloudアカウントをIAMデータベースユーザーとして作成する場合は、 type に CLOUD_IAM_USER を指定します。 resource "google_sql_user" "member" { project = "YOUR_PROJECT_ID" name = "YOUR_MEMBER_NAME@YOUR_MEMBER_DOMAIN" instance = google_sql_database_instance.main.name type = "CLOUD_IAM_USER" } この例では、IAMデータベースユーザーを YOUR_MEMBER_NAME@YOUR_MEMBER_DOMAIN として作成しています。 Cloud SQL for MySQLでは、IAMデータベースユーザーのメールアドレスの @ より前の部分をMySQL上のユーザー名として扱います。そのため、後続の GRANT では YOUR_MEMBER_NAME を指定します。 Cloud SQLへのログイン権限の付与 IAM認証でCloud SQLへログインするには、 cloudsql.instances.login 権限が必要です。この権限は、事前定義ロールの roles/cloudsql.instanceUser に含まれています。 resource "google_project_iam_member" "cloudsql_login" { project = "YOUR_PROJECT_ID" role = "roles/cloudsql.instanceUser" member = "user:YOUR_MEMBER_NAME@YOUR_MEMBER_DOMAIN" } また、本記事では開発メンバーがCloud SQL Studioから接続する前提のため、Cloud SQL Studioを利用するためのIAMロールも付与します。 resource "google_project_iam_member" "cloudsql_studio" { project = "YOUR_PROJECT_ID" role = "roles/cloudsql.studioUser" member = "user:YOUR_MEMBER_NAME@YOUR_MEMBER_DOMAIN" } ここまでで、開発メンバーが自分のGoogle Cloudアカウントを使ってCloud SQLへログインするための準備が整います。 ただし、IAM認証はCloud SQLへログインする主体を制御する仕組みです。ログイン後にどのSQLを実行できるかはMySQL側の権限で制御します。 MySQLロールでDB内権限を分ける Cloud SQLへのログイン可否はIAMで制御しますが、ログイン後にどのデータベースやテーブルに対して、どのSQLを実行できるかはMySQL側の権限で制御します。 MySQLにおけるロールは、複数の権限をまとめて管理してユーザーへ付与するための仕組みです。本記事では、Cloud SQL for MySQLのMySQL 8.0系でロールを利用する前提で説明します。 通常時と一時付与用のロール 今回は例として、次の2種類のMySQLロールを用意します。 ロール 用途 権限 viewer 通常時の参照用ロール SELECT editor 承認後に一時付与する書込系ロール SELECT , INSERT , UPDATE , DELETE 通常時は、開発メンバーに viewer のみを付与します。これにより、Cloud SQL Studioへログインした直後は参照のみ実行できる状態です。 一方、 editor は通常時には付与しません。データ修正などで書込系権限が必要になった場合だけ、後述するGitHub Actionsの承認付きワークフローから一時的に付与します。 ロールの分け方、付与する権限、対象範囲は、実際の運用や対象データによって調整が必要です。本記事では通常時の参照権限と、承認後に一時付与する書込系権限とで2つに分ける例として説明します。 ロールの作成 MySQL上にロールを作成して、参照や更新に必要な権限を付与します。 この例では、 YOUR_DB_NAME.* に対して権限を付与しています。実際の運用では必要以上に広い範囲へ権限を付与しないよう、対象データや作業内容に応じて、データベース単位、テーブル単位、権限種別を調整してください。 CREATE ROLE ' viewer ' , ' editor ' ; GRANT SELECT ON YOUR_DB_NAME.* TO ' viewer ' ; GRANT SELECT, INSERT, UPDATE, DELETE ON YOUR_DB_NAME.* TO ' editor ' ; 参照用ロールの付与 開発メンバーには、通常時の権限として viewer ロールを付与します。 また、データベースへのIAMログイン直後から標準で有効になるように、 viewer をデフォルトロールとして設定します。 MySQLユーザーは 'user'@'host' の形式で扱われます。本記事のサンプルでは 'YOUR_MEMBER_NAME'@'%' としており、 % は任意の接続元を表すhost部です。 実際の運用では、Cloud SQLへの到達経路やネットワーク制御に応じてhost部を調整してください。 GRANT ' viewer ' TO ' YOUR_MEMBER_NAME ' @ ' % ' ; SET DEFAULT ROLE ' viewer ' TO ' YOUR_MEMBER_NAME ' @ ' % ' ; GitHub Actionsを承認ゲートにして書込系ロールを一時付与する editor ロールが必要な場合は、GitHub Actionsの手動ワークフローから申請するようにしました。 この仕組みにおけるGitHub Actionsの役割は、Cloud SQLへの接続経路そのものではなく、書込系ロールを一時付与するための承認ゲートです。実際に GRANT を実行する処理はCloud SQLへ接続できる実行環境で行います。本記事ではCloud Buildを利用した例として説明します。 GitHub Environmentで申請者以外の承認を必須にする 今回利用するGitHub Actionsのワークフローの中では、 GitHub Environment を承認ゲートとして利用します。 Environmentには Required reviewers を設定し、 Prevent self-review を有効にします。ワークフローのjobで対象Environmentを指定すると、そのEnvironment上での実行前に承認を要求できます。 これにより、申請者以外のメンバーによる承認を挟んでから後続の処理を実行できるようになります。今回の例では、この承認ゲートを使って承認後に editor ロールを一時付与するようにしました。 権限操作用サービスアカウントの作成 承認後に GRANT を実行するため、権限操作用サービスアカウントを用意します。 権限操作用サービスアカウントは、GitHub ActionsからWorkload Identity Federation経由で利用します。承認後は、SQL実行基盤がこのサービスアカウントでMySQLへ接続し、対象ユーザーへ editor ロールを付与します。 このサービスアカウントもCloud SQLへIAM認証でログインできるようにするため、IAMデータベースユーザーとして作成しておきます。 resource "google_sql_user" "grant_sa" { project = "YOUR_PROJECT_ID" name = "YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com" instance = google_sql_database_instance.main.name type = "CLOUD_IAM_SERVICE_ACCOUNT" } この例では、権限操作用サービスアカウントを YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com として作成しています。 サービスアカウントの場合も、MySQL上では @YOUR_PROJECT_ID.iam.gserviceaccount.com を除いた部分をユーザー名として扱います。そのため、後続の GRANT では YOUR_SA_NAME を指定します。 権限操作用サービスアカウントがMySQL上で editor ロールを他ユーザーへ付与できるように、MySQL側では WITH ADMIN OPTION 付きで editor ロールを付与しておきます。 GRANT ' editor ' TO ' YOUR_SA_NAME ' @ ' % ' WITH ADMIN OPTION ; WITH ADMIN OPTION を付与されたユーザーは、そのロールを他のユーザーへ付与できます。つまり、この権限操作用サービスアカウントは書込系ロールの付与経路です。 そのため、Workload Identity Federationの条件やサービスアカウントの利用権限を絞る必要があります。想定したGitHubリポジトリ、ブランチ、Environment以外から利用されないようにしておきます。 承認後に実行するSQLは、最終的には次のような形です。 GRANT ' editor ' TO ' YOUR_MEMBER_NAME ' @ ' % ' ; ワークフロー設定例 GitHub ActionsからGoogle Cloudへの認証には、 Workload Identity Federation を利用します。 サービスアカウントキーをGitHub Secretsに保存せず、GitHub ActionsのOIDCトークンを使ってGoogle Cloudのサービスアカウントを利用できます。 GitHub Actionsの ワークフロー構文 を使った設定ファイルの例は次のとおりです。 name : grant-cloudsql-db-role on : workflow_dispatch : inputs : target_user : description : 対象ユーザーのIAMアカウントメールアドレス type : string required : true role : description : 付与するMySQLロール type : choice options : - editor required : true jobs : grant : runs-on : ubuntu-latest # このEnvironmentにRequired reviewersとPrevent self-reviewを設定する environment : YOUR_GITHUB_ENVIRONMENT_NAME permissions : contents : read # OIDCトークンを発行するために必要 id-token : write steps : - uses : actions/checkout@v4 # Workload Identity FederationでGoogle Cloudへ認証する - uses : google-github-actions/auth@v2 with : workload_identity_provider : projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_WIF_POOL/providers/YOUR_WIF_PROVIDER service_account : YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com - name : Grant DB role run : gcloud builds submit --config=grant.yaml --substitutions="_TARGET_USER=${{ inputs.target_user }},_ROLE=${{ inputs.role }} " ここでは、Cloud SQLがPublic IPを持たず、プライベート経路でのみ到達できる構成として、Cloud Build上で権限操作SQLを実行します。 今回の構成では、Cloud SQLへ到達できるVPC内に中継用Compute Engineインスタンスを配置しました。Cloud Buildからは、そのインスタンス上の Cloud SQL Auth Proxy を経由してCloud SQLへ接続します。 なお、SQLの実行経路は環境に依存します。Cloud SQLへの到達方式や既存の実行基盤によっては、GitHub Actionsのself-hosted runnerやCloud Run jobsなども選択肢になります。 一時付与した書込系ロールを剥奪する editor ロールは一時的な付与を前提としているため、作業後には剥奪できるようにします。 editor ロールの付与は権限を強める操作のため、GitHub Environmentによる承認を必須にしています。一方、 editor ロールの剥奪は一時付与した権限を戻す操作のため、今回の例では承認ゲートを設けていません。 ロール剥奪の方法には、手動実行と定期実行の2つを用意しました。 作業後に任意のタイミングで剥奪するための手動ワークフロー 戻し忘れを抑止するための定期実行ワークフロー いずれの場合も、GitHub ActionsからCloud Buildへ処理を渡し、権限操作用サービスアカウントでMySQLへ接続して REVOKE を実行します。 手動でロールを剥奪する 手動剥奪では、対象ユーザーを入力として受け取り、次のようなSQLを実行します。 REVOKE ' editor ' FROM ' YOUR_MEMBER_NAME ' @ ' % ' ; GitHub Actionsのワークフロー設定ファイルの例は次のとおりです。 name : revoke-cloudsql-db-role on : workflow_dispatch : inputs : target_user : description : 対象ユーザーのIAMアカウントメールアドレス type : string required : true jobs : revoke : runs-on : ubuntu-latest permissions : contents : read id-token : write steps : - uses : actions/checkout@v4 - uses : google-github-actions/auth@v2 with : workload_identity_provider : projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_WIF_POOL/providers/YOUR_WIF_PROVIDER service_account : YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com - name : Revoke DB role run : gcloud builds submit --config=revoke.yaml --substitutions="_TARGET_USER=${{ inputs.target_user }} " 定期実行でロールを剥奪する 手動での剥奪漏れを防ぐため、 editor ロールを定期的にも剥奪するように設定しておきます。 定期剥奪では、MySQL上の現在のロール付与状態を確認し、 editor ロールを保持しているユーザーを対象に REVOKE を実行します。 GitHub Actionsのワークフロー設定ファイルの例は次のとおりです。 name : revoke-cloudsql-db-role-scheduled on : workflow_dispatch : schedule : # 毎日 00:00 JST に実行する # GitHub Actions の cron は UTC 基準のため、15:00 UTC を指定する - cron : "0 15 * * *" jobs : revoke : runs-on : ubuntu-latest permissions : contents : read id-token : write steps : - uses : actions/checkout@v4 - uses : google-github-actions/auth@v2 with : workload_identity_provider : projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/YOUR_WIF_POOL/providers/YOUR_WIF_PROVIDER service_account : YOUR_SA_NAME@YOUR_PROJECT_ID.iam.gserviceaccount.com - name : Revoke all temporary DB roles run : gcloud builds submit --config=revoke-all.yaml この例でCloud Buildに渡している revoke-all.yaml では、MySQL上で editor ロールを保持しているユーザーを取得します。そのうえで、権限操作用サービスアカウントのDBユーザーを剥奪対象から除外し、残ったユーザーに対して順に REVOKE を実行します。 権限操作用サービスアカウントから editor ロールを剥奪すると、以降の GRANT や REVOKE を実行できなくなるためです。 運用上の注意点と今後の改善 今回の構成により、人間によるCloud SQL for MySQLへのログインを個人のIAM認証に寄せ、通常時に書込系権限を持たない運用にできました。 一方で、この仕組みをより安全に、かつ便利に運用するには、権限付与後のロールの使い方、ワークフロー入力値の扱い、ロール粒度などを継続的に見直す必要があります。 ここでは、今回の構成における運用上の注意点と、今後の改善余地を整理します。 一時付与したロールを使うときの注意 今回の例では、 editor ロールはMySQLユーザーに対するデフォルトロールとして設定していません。そのため、Cloud SQL Studioなどから一時付与された権限を使う場合は、実行するSQLと同じセッション内で SET ROLE を実行します。 特に、実行単位によってセッションが変わりうる接続方式では、 SET ROLE と対象SQLを同じ実行単位にまとめます。 SET ROLE ' editor ' ; UPDATE YOUR_TABLE_NAME SET YOUR_COLUMN_NAME = ' XXXXX ' WHERE id = XXX ; なお、Cloud SQL for MySQLでは activate_all_roles_on_login フラグを有効にすると、ログイン時に付与済みのロールを自動的に有効化できます。ただし、その場合は一時付与した書込系ロールもログイン時に自動で有効化されるため、通常時に有効化したいロールと一時付与するロールの扱いを踏まえて設計する必要があります。 ワークフロー入力値の検証 本記事では、GitHub ActionsからCloud Buildへ target_user や role を渡し、SQL実行基盤側で GRANT や REVOKE を実行する構成を例にしています。 ワークフロー入力値をもとにSQLを組み立てる場合、想定外のユーザー名やロール名がSQLに含まれる可能性があります。 そのため、GitHub Actions側で入力形式を絞るだけでなく、SQL実行基盤側でも許可したユーザー名やロール名だけを扱うように制御する必要があります。 SQL本文のレビューと監査 今回の構成では、GitHub Actionsから対象ユーザーへの editor ロールの一時付与を申請し、承認を挟むようにしています。 ただし、承認対象は editor ロールの一時付与です。承認後に実行されるSQL本文そのものは、今回の仕組みではレビュー対象にしていません。 SQL本文まで事前に確認する場合は、申請時に実行予定のSQLや作業内容を添付し、承認者による確認を挟む運用も考えられます。 実行後の追跡性を高めるには監査ログやDB監査機能を組み合わせ、誰がいつ、どの操作をしたかを確認できる状態にしておくことも重要です。 一時付与した権限の失効タイミングの厳密化 今回の例では、手動剥奪と毎日0時の定期剥奪で editor ロールを戻す構成にしています。 より厳密に制御したい場合は、付与時刻や申請IDを記録してユーザー単位で失効時刻を管理する設計が必要になります。 MySQLロールの粒度 本記事でのMySQLロールの例としては、書込系権限を editor ロールにまとめました。 ただし、 INSERT 、 UPDATE 、 DELETE 、各種DDLでは影響範囲が異なります。特に DELETE 、 DROP 、 ALTER のような操作は対象データや運用ルールによっては別ロールに分けるほうが安全です。 例えば次のように、MySQLのロールを細分化する余地があります。 ロール 権限 用途 viewer SELECT 通常調査 data_writer INSERT , UPDATE など 手動データ補正 data_deleter DELETE 削除が必要な例外対応 schema_editor CREATE , ALTER など スキーマ変更 schema_dropper DROP 破壊的DDL ただし、ロールを細かく分けるほど、申請フローや承認基準も複雑になります。そのため、対象データ、作業頻度、レビュー体制に応じてロール粒度を調整していく必要があります。 おわりに 本記事では、人間によるCloud SQL for MySQLへのアクセスを、共用の特権DBユーザーとパスワード認証から、IAM認証とMySQLロールを使った運用へ移行した例を紹介しました。 今回の構成では、Cloud SQLへのログインは個人のGoogle Cloudアカウントに寄せ、DB内の権限はMySQLロールで制御しています。通常時は参照用の viewer ロールのみを利用し、書込系権限が必要な場合だけGitHub Actionsの承認付きワークフローから editor ロールを一時付与する形にしました。 これにより、共用特権DBユーザーに依存した人間によるアクセスをやめ、強い権限が常時使える状態を避けられるようになりました。 SQL本文のレビューや監査、ロール粒度の細分化、失効タイミングの厳密化などは、引き続き改善の余地があります。 今回の対応を足がかりとして、運用負荷と安全性のバランスを見ながら改善に取り組んでいきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。基幹システム本部 基幹開発部 商品管理ブロックの田中秀明です。 Claude CodeやCodexの利用が広がるほど、各人の使い方、プロンプト、レビュー観点、AIへ任せる範囲がばらつき始めました。AIを高度に使いこなせる人は開発の進め方そのものを変えられる一方で、これから使い始める人にとっては「どの工程で、どこまでAIに任せればよいのか」が分かりにくい状態になっています。 ZOZOでは2025年7月に、1人あたり月額200ドルを基準として、Claude Codeをはじめとする開発AIエージェントを全エンジニアに導入することを発表しました。 corp.zozo.com 利用可能なツールはClaude Code、Codex、Devin、Cursorなど多岐にわたっており、Claude Codeは数百名規模で利用されています。選択肢が増えること自体は前進ですが、組織として見ると、使い方が個人に閉じるほど開発プロセスの再現性は個人差に依存しやすくなります。 そこで基幹システム本部では、Claude CodeとCodexを組み合わせ、設計から実装、レビュー、必要に応じた画面確認までを支援する仕組みを作りました。この仕組みを /dev-init と /dev-resume という2つの標準コマンドに集約しています。本記事では、この2コマンドの設計思想を紹介します。あわせて、内部でのClaude CodeとCodexの連携方法や、標準化によって変えようとしていることも説明します。 本記事で扱う内容は次の3点です。 AIの本質から逆算した「AIに任せる工程」と「人間が判断する工程」の線引き /dev-init と /dev-resume の2コマンドに集約した理由 Claude Code × Codexの批判的対話による設計・実装レビューの仕組み 目次 はじめに 目次 問題は「AIを使っていないこと」ではなく「使い方が個人に閉じていること」 AIに任せる工程を、AIの本質から決める なぜ2コマンドなのか なぜ自前の標準コマンドなのか なぜClaude Code × Codexでレビューするのか 全体アーキテクチャ /dev-init:AIが迷わない初期状態を作る /dev-resume:AIが文脈を失わずに作業を続ける Codexレビューの中身 Playwright MCPで画面確認まで接続する セットアップ 標準化によって何が変わるか 残課題と今後 まとめ 問題は「AIを使っていないこと」ではなく「使い方が個人に閉じていること」 開発AIツールの導入初期は、まず個人が試して便利な使い方を見つけていく段階があります。これは自然な流れであり、実際にAIを使いこなすメンバーは、調査、設計、実装、レビュー観点の整理など多くの工程で生産性を高めています。 一方で、個人の工夫が増えるほど、組織としては別の問題が見えてきました。 ある人はJiraの内容を貼り付けて設計書を生成させる。別の人は差分だけを見せてレビューさせる。さらに別の人は、仕様の曖昧さをAIに洗い出させる。どれも有効な使い方です。しかし、プロンプト、判断基準、AIへ渡す情報、レビュー時に見る観点が人によって異なると、設計書、進捗管理、検証観点がバラバラの形式で蓄積されていきます。 この状態では、AI活用が進んでいるように見えても、組織全体の最低水準は上がりにくくなります。先端ユーザーの成果は伸びますが、その知見が再利用可能な形で残らないためです。 ZOZOでは、AI活用の状態を個人と組織の両面から捉えるために、All ZOZO AI Readiness Score、通称AZARSという指標も定義しています。AZARSでは、個人がAIをどの程度業務に組み込めているかだけでなく、組織として「AIを前提とした業務プロセスを仕組みに落とし込めているか」も見ます。求められるのは、AI活用が得意な人の存在に加えて、誰でも同じ入口から一定水準のAI駆動開発を始められる状態です。 この標準化で避けたかったのは、AIに「任せすぎる」ことと「任せなさすぎる」ことです。 任せすぎると、AIの出力を検証しないまま設計や実装が進み、要件の読み違いや品質低下につながります。一方で任せなさすぎると、調査、設計書化、タスク分解、レビュー観点の抽出のような、AIで短縮できる工程を人が手作業で続けることになります。この2つの間に、組織として再現可能な線を引く必要がありました。 ここから先は、その線を「2コマンドの形」にどう落とし込んだのかを、設計判断の順に紹介します。 AIに任せる工程を、AIの本質から決める AI駆動開発を標準化するうえで、最初に決めるべきことは「何をAIに任せるか」です。 現在のモデル性能だけを基準にすると、判断はすぐに古くなります。モデルやツールは短い周期で変わるため、「今このモデルならできる」「今このツールでは難しい」という視点だけで線を引いても、標準プロセスとして長く使い続けるのは難しくなります。 そこで、AIの仕組みからAIの本質的性質を「入力に対して、学習データと文脈からもっとも確からしい出力を返す変換機」であると捉えました。AIの出力品質は、入力の具体性、正解の一意性、学習データ上のパターンの豊富さに強く依存します。逆に言えば、入力と出力の対応関係が明確で、似たパターンが学習データに多くあるタスクほど、AIは安定して高い品質を出しやすくなります。 この性質は、情報の変換方向で整理できます。 AIに任せやすい変換パターンと人間が判断する領域 具体から抽象への変換は、複数の事例から共通項やパターンを抽出する作業です。たとえば障害報告から課題パターンを見つける、コード差分からレビュー観点を抽出する、仕様書から不足や矛盾を検出する作業はここに含まれます。AIの仕組みが力を発揮しやすい領域です。 同一レベルの変換は、ある形式の情報を別の形式に整形する作業です。Jiraチケットから設計書を作る、設計書から実装タスクを作る、Git diffからレビューコメントを作る作業がこれにあたります。入力と出力の対応が比較的明確であれば、AIに寄せやすい領域です。 一方で、抽象から具体への変換は注意が必要です。要求を整理して企画を立てる、複数の実現案からどれを選ぶか決める、限られた期間で何を優先するか判断する、といった作業がこれにあたります。ここでは入力にない情報を補い、組織事情、事業インパクト、リスク、顧客価値を踏まえて決める必要があります。AIは選択肢の列挙や論点整理を支援できますが、最終判断を標準コマンドに閉じ込めるにはリスクが高い領域です。 この整理から、次の方針にしました。 同一レベル変換はAIに任せる。Jiraから設計書、設計書から進捗管理表、仕様から実装、差分からレビューを作る 具体から抽象はAIに任せる。矛盾検出、レビュー観点抽出、影響範囲の整理に使う 抽象から具体は人間が主導する。企画、要求整理、優先順位、最終責任は人間が持つ つまり今回の2コマンドは、事業や要求の最終判断をAIに置き換えるものではありません。事業側から提供された企画や仕様の判断は人間が行うことを前提に、確定した仕様を正確に設計・実装・検証へつなげる領域を標準化するものです。 なぜ2コマンドなのか 標準化するとき、最初に迷うのはコマンドの粒度です。 工程ごとに細かく分ければ、調査、設計、タスク分解、実装、レビュー、テストといった各機能の責務は明確になります。しかし、入口が増えるほど利用者は「今どのコマンドを使うべきか」を判断しなければなりません。組織の最低水準として全員に使ってもらうには、最初の一歩が重くなります。 逆に、完全に1コマンドにする案もあります。利用者の認知コストは最小になりますが、開発初回だけは問題があります。設計書や進捗管理表がまだ存在しないため、AIが現在位置を特定する材料を持てません。初回は、以後のAI作業が参照する状態そのものを作る必要があります。 このため、初回作成と再開の2つに分けました。 /dev-init :Jira情報から、設計書、詳細設計兼・進捗管理表、必要に応じてテストパターンを作る /dev-resume :進捗管理表を読み、Gitの状態も見ながら、現在位置を特定して作業を再開する 選択肢 内容 長所 短所 判断 工程別に細かく分ける 調査・設計・実装・レビューを別コマンドにする 機能ごとの責務が明確 入口が増え、初心者のファーストステップが重い 不採用 完全に1コマンド化する 初回も再開もすべて1コマンド 利用者の認知コストは最小 初回は設計書・進捗管理表がなく、AIが現在位置を特定しにくい 不採用 初回作成と再開に分ける 初回 /dev-init 、以後 /dev-resume 初回だけ不足情報を補い、以後は状態から再開できる コマンドが2つになる 採用 ここで重要なのは、2コマンドが「工程ごとに分ける」設計思想ではないことです。むしろ入口はできるだけ少なくしたいと考えています。 /dev-init が存在する理由は、Git管理できる設計書・進捗管理表がまだ出力されていない初回だけ、AIが現在位置を特定できないからです。 進捗管理表が一度生成された後は、 /dev-resume がそのファイルを読み、現在のタスク、未着手タスク、ブロッカー、関連ファイル、Git diffを照合して次のアクションを提示できます。技術的に必要な初回だけを /dev-init として分離し、それ以降は /dev-resume に寄せます。このバランスが、利用者の学習コストとAIの技術的制約の接点でした。 なぜ自前の標準コマンドなのか AI開発支援のツールやフレームワークは次々に登場しています。汎用的な開発オーケストレーションツールを使う選択肢もあります。 それでも、基幹システム本部ではZOZOの開発工程に合わせた標準コマンドを用意しました。理由は、汎用性と最適化がトレードオフだからです。 汎用ツールは幅広い用途に対応できます。一方で、ZOZOのJira、Confluence、既存コード調査、設計書の粒度、進捗管理、レビュー観点、フロントエンド確認までを、日々の開発導線に深く埋め込むには限界があります。 各チームが自由にワークフローを作る方法もあります。現場ごとの自由度は高くなりますが、属人化が進み、組織全体の最低水準は上がりにくくなります。 そこで、利用者に見えるUIは /dev-init と /dev-resume の2つに固定し、内部のプロンプトやSkills、連携ツールを継続的に更新する形にしました。世の中のAIツールやモデルが変わっても、利用者は毎回新しい操作体系を覚える必要がありません。標準コマンドの中身を改善すれば、組織全体のAI駆動開発をまとめてアップデートできます。この標準コマンドは、先端的な使い方を止めるためのものではありません。できる人は標準以上の使い方ができます。一方で、標準コマンドがあることで、組織としての最低水準を引き上げられます。先端ユーザーの知見を標準コマンドへフィードバックし、中身を改善し続けることが重要です。 なぜClaude Code × Codexでレビューするのか /dev-init と /dev-resume ではClaude Codeでベースラインを作成した後、Codexでレビューする仕組みを採用しています。 Claude Codeだけでも、設計書生成、実装、レビューはできます。それで十分な場面もあります。 ただし、品質を重視する場面で単一モデルに依存しすぎると、リスクがあります。モデルには得意不得意があり、提供側の性能調整の影響も受けます。また、同じモデルにセルフレビューさせる方法は実装しやすいものの、観点の独立性は強くありません。 そこでClaude CodeとCodexを別の役割で組み合わせました。 方式 長所 短所 採用判断 Claude Codeのみ 構成が単純で実行コストが低い 単一モデルの見落としや性能調整の影響を受けやすい 通常モードとして残す Claude Code × Codex 異なるモデルの観点を突き合わせられる コストと時間が増える 品質重視モードとして採用 Claude Codeは、全体のオーケストレーション、設計書生成、採否判断、修正実行を担当します。Codexは、リポジトリ内のコードを独自に調査したうえで、設計や実装を批判的にレビューします。 狙いは、片方のAIが出した答えを別のAIが独立した観点で疑うことです。 AIによるレビューの批判的対話は、次の3ラウンドを最小単位にしています。 Codexがリポジトリを独自調査し、設計や実装を批判的にレビューする Claude CodeがCodexの指摘を精査し、採用、却下、保留を判断する CodexがClaude Codeの却下判断や残存リスクを再批判する Claude CodeとCodexによる批判的対話レビュー レビュー回数を固定しているわけではありません。難しい変更では追加のサイクルが必要になり、軽い変更では早く収束します。自動実行では各レビューポイントにつき最大3サイクルまで実行し、同一の指摘内容が2サイクル連続で解消しない場合は対話型に切り替える設計です。これにより、難易度に応じてレビューの深さを変えつつ、無限に回り続けることを避けています。 全体アーキテクチャ 全体の流れは、Jira起票から始まります。 開発者はまず /dev-init を実行し、Jiraチケットの内容を渡します。Claude Codeはチケットの背景、受入条件、関連情報を解析し、サブエージェントを使ってコードベースやConfluenceを並列調査します。その結果をもとに、Confluence設計書、ローカルMarkdownの詳細設計を兼ねた進捗管理表、必要に応じてテストパターンを生成します。 その後の開発では /dev-resume を使います。進捗管理表を読み、現在の進捗、未着手タスク、進行中タスク、関連ファイル、 git status 、 git diff をもとに再開ポイントを提示します。実装後はCodexでレビューし、フロントエンド関連の変更でPlaywright MCPが利用できる場合は画面確認まで接続します。 Jiraからdev-init、dev-resume、レビュー、画面確認までの全体フロー 役割分担は次のように整理できます。 Claude Code:オーケストレーター、設計書生成、進捗管理表の生成、コード実装、採否判断、修正実行 Codex:独自調査に基づく批判的レビュー、再批判、残存リスクの指摘 サブエージェント:コードベース調査、関連ファイル検索、既存実装パターン調査、タスク分解、テストパターン生成 Playwright MCP:フロントエンド変更時のDOM、コンソール、ネットワーク確認 /dev-init :AIが迷わない初期状態を作る /dev-init は、開発開始時に使うコマンドです。 /dev-init < JiraチケットまたはURLをコピー&ペースト > 最初にJira情報を解析し、チケットID、タイトル、優先度、説明、受入条件、関連チケットを整理します。その後、作成対象を選びます。Confluence設計書と進捗管理表の両方を作る、Confluence設計書のみ作る、進捗管理表のみ作る、という選択ができます。 重要なのは、設計書を書く前にコードベースを調査する点です。Jiraの内容だけから設計書を作ると、文章としては整っていても、既存コードと整合しない可能性があります。そこで /dev-init では、複数のサブエージェントを使って次の調査を並列に実行します。 code.claude.com コードベース構造分析 関連ファイル検索 既存実装パターン調査 Confluence関連ドキュメント検索 調査結果は、設計書と進捗管理表に反映されます。使用フレームワーク、レイヤー構成、主要コンポーネント、類似実装、関連ファイル、テスト方針、実装時の注意点を、以後の作業で参照できる形にします。 進捗管理表は、単なるタスク一覧ではありません。作業を中断しても、そのファイルだけを読めば再開できる詳細さを目指しています。チケットの背景、受入条件、タスク一覧、対象ファイル、実装詳細、テスト観点、未解決課題、作業ログを持たせます。 実際に使っている進捗管理表テンプレートは次のとおりです。 # {チケットID}: {タイトル} ## 基本情報 | 項目 | 内容 | |------|------| | Jiraチケット | {JiraチケットURL} | | 設計書 | {Confluence設計書URL} | | テストパターン | {テストパターンファイルパス} | | ブランチ | feature/{チケットID} | | 担当者 | {担当者名} | | 開始日 | {今日の日付} | | 目標完了日 | {目標日} | | 最終更新 | {最終更新日時} | --- ## 要件サマリー ### 背景・目的 {Jiraの説明から抽出した背景・目的を簡潔に記載} ### 受入条件 - [ ] {受入条件1} - [ ] {受入条件2} - [ ] {受入条件3} ### スコープ - **対象**: {実装対象} - **対象外**: {対象外範囲} --- ## コードベース調査結果 ### 関連ファイル一覧 #### 直接修正対象ファイル | ファイルパス | 役割 | 修正内容 | |-------------|------|----------| | `{パス1}` | {役割1} | {修正概要1} | #### 参照・影響範囲ファイル | ファイルパス | 役割 | 影響内容 | |-------------|------|----------| | `{パス3}` | {役割3} | {影響概要3} | ### 既存コード構造 #### クラス・インターフェース構成 ``` {パッケージ構成} ├── {クラス名1}.java // {概要} └── {インターフェース名}.java // {概要} ``` ### 類似実装の参考箇所 | 参考ファイル | 行番号 | 参考内容 | |-------------|--------|----------| | `{ファイルパス}` | L{開始}-L{終了} | {参考になるポイント} | --- ## 詳細タスク一覧 ### フェーズ1: 準備・設計確認 | # | タスク | 状態 | 対象ファイル | 実装詳細 | |---|--------|------|-------------|----------| | 1.1 | {タスク名} | ⬜ | `{ファイルパス}` | {具体的な実装内容} | ### フェーズ2: 実装 | # | タスク | 状態 | 対象ファイル | 実装詳細 | |---|--------|------|-------------|----------| | 2.1 | {タスク名} | ⬜ | `{ファイルパス}` | {具体的な実装内容} | ### フェーズ3: テスト | # | タスク | 状態 | 対象ファイル | テスト観点 | |---|--------|------|-------------|-----------| | 3.1 | 単体テスト作成 | ⬜ | `{テストファイルパス}` | {テスト観点} | ### フェーズ4: レビュー・完了 | # | タスク | 状態 | 備考 | |---|--------|------|------| | 4.1 | セルフレビュー | ⬜ | チェックリスト確認 | | 4.2 | PR作成 | ⬜ | | | 4.3 | コードレビュー対応 | ⬜ | | | 4.4 | マージ・完了 | ⬜ | | ### ステータス凡例 - ⬜ 未着手 - 🔄 進行中 - ✅ 完了 - ⏸️ 保留 - ❌ キャンセル --- ## テストパターン進捗サマリー > ※ テストパターンファイル: {テストパターンファイルパス} ### テスト実施状況 | 分類 | 総数 | Pass | Fail | 未実施 | 実施率 | |------|------|------|------|--------|--------| | 正常系 | {n} | 0 | 0 | {n} | 0% | | 異常系 | {n} | 0 | 0 | {n} | 0% | | 境界値 | {n} | 0 | 0 | {n} | 0% | | 回帰 | {n} | 0 | 0 | {n} | 0% | | **合計** | **{N}** | **0** | **0** | **{N}** | **0%** | --- ## 総合進捗 | 項目 | 完了 | 総数 | 進捗率 | |------|------|------|--------| | 実装タスク | 0 | {タスク総数} | 0% | | テストパターン | 0 | {テスト総数} | 0% | | **総合** | **0** | **{合計}** | **0%** | --- ## 作業ログ ### {今日の日付} #### 実施内容 - [ ] 詳細設計兼進捗管理表作成 - [ ] 設計書作成完了 #### 進捗サマリー - **完了タスク**: 0/{総タスク数} - **進行中タスク**: 0 - **ブロッカー**: なし --- ## 関連リンク ### プロジェクト関連 - **設計書**: {Confluence設計書URL} - **Jira**: {JiraチケットURL} - **テストパターン**: {テストパターンファイルパス} - **PR**: (作成後に追記) --- ## メモ・課題 ### 未解決課題 | # | 課題 | 優先度 | 期限 | 担当 | |---|------|--------|------|------| | 1 | {課題内容} | {高/中/低} | {期限} | {担当} | ### 決定事項 | 日付 | 決定事項 | 決定者 | |------|----------|--------| | {日付} | {決定内容} | {決定者} | --- ## 作業再開ガイド ### 現在の状態 - **最終作業タスク**: {タスク番号と名前} - **作業中断理由**: {理由} - **次のアクション**: {具体的なアクション} ### 再開時の確認事項 1. {確認事項1} 2. {確認事項2} 3. {確認事項3} ### コンテキスト復元用コマンド ```bash # ブランチ切り替え git checkout feature/{チケットID} # 最新化 git pull origin feature/{チケットID} ``` Codexレビューモードを選択した場合は、設計フェーズで3つのレビューポイントを通します。 設計書レビュー: 設計品質、要件カバレッジ、コードベースとの整合性 タスク分解レビュー: タスク粒度、依存関係、受入条件カバレッジ 実装方針レビュー: 実装方針、既存コードとの整合性、リスクと実行順序 実際の流れは次のようになります。 Step 0: Codex CLI セットアップ・認証確認 Step 1: Jira情報の解析 Step 2: 作成対象の選択 Step 3: 並列コードベース・Confluence調査 Step 4: 設計書プレビュー Step 5: Codex設計書レビュー Step 6: 並列タスク分解による進捗管理表の生成 Step 7: Codexタスク分解レビュー Step 8: Codex実装方針レビュー Step 9: 並列テストパターン生成 Step 10: 完了 この時点でレビューを挟む理由は、修正コストを下げるためです。実装後に問題を見つけるより、設計とタスク分解の時点でズレを潰すほうが低コストです。特にAIが生成したタスク分解は、一見もっともらしくても、既存コードの実態からズレることがあります。Codexに独自調査させることで、Claude Codeが作った計画を別視点から疑わせます。 /dev-resume :AIが文脈を失わずに作業を続ける /dev-resume は、開発中や中断後に作業を再開するためのコマンドです。 /dev-resume ./docs/progress/PROGRESS-TICKET-123.md # 引数なしの場合は進捗管理表を自動検索 /dev-resume 引数で進捗管理表を指定した場合はそのファイルを読みます。指定がない場合は、カレントディレクトリ、 .claude/progress/ 、 docs/ などから PROGRESS-*.md や進捗管理表らしいMarkdownを探します。 進捗管理表からは、基本情報、要件サマリー、タスク一覧、関連ファイル、作業再開ガイド、未解決課題を抽出します。そのうえで、完了タスク数、進行中タスク、未着手タスク、ブロッカーの有無を計算します。 さらに、サブエージェントを使って現在の作業状態を自動検出します。 git status で未コミット変更を確認し、 git diff で変更内容を見て、進捗管理表のタスクと照合します。これにより、前回の会話が消えていても、AIが現在位置を取り戻せます。 進捗管理表とGit差分から再開アクションを提示する流れ /dev-resume のアクションメニューでは、次の操作を選べます。 1. 並列開発モードを開始する 2. 次のタスクを開始する 3. 特定のタスクを選択して開始する 4. 進行中のタスクを完了にする 5. タスクのステータスを変更する 6. 作業ログを追記する 7. 課題・ブロッカーを記録する 8. 終了する 中でも重要なのが、並列開発モードです。未着手タスクの依存関係を分析し、同じファイルを編集しないタスク、依存関係のないタスク、実装とテストを同時進行できるタスクを見つけます。そのうえで、複数のサブエージェントに分けて同時実装します。 並列実行後は結果を統合し、ファイル競合、import整合性、型定義の整合性を確認します。Codexレビューモードを選んだ場合は、その差分をCodexとの批判的対話にかけます。 /dev-resume の本質は、AIに作業を継続させるための文脈を、会話ではなくファイルに持たせることです。会話履歴に依存すると、中断や翌日再開、別メンバーへの引き継ぎで文脈が失われます。進捗管理表をGit管理できる形式で残すことで、AIと人間が同じ状態を参照できます。 Codexレビューの中身 Codexレビューでは、単に git diff を渡して「レビューして」と依頼するだけではありません。進捗管理表から、対象タスクの背景、目的、受入条件、実装詳細、設計上の制約を抽出し、差分と一緒に渡します。 レビュー観点は大きく2つです。 1つ目はコード品質です。バグ、回帰、設計不備、セキュリティ、テスト不足を確認します。 2つ目は仕様準拠です。実装が受入条件を満たしているか、要件サマリーの目的と整合しているか、過剰実装になっていないかを確認します。 目的: git差分に対して、コード品質と仕様準拠の2つの観点からレビューする コード品質: - バグ - 回帰 - 設計不備 - セキュリティ - テスト不足 仕様準拠: - 受入条件を満たしているか - 要件サマリーの目的と整合しているか - 実装詳細の方針に沿っているか - 過剰実装がないか Codexの指摘に対して、Claude Codeは必ず採否を判断します。採用、却下、保留のいずれかを決め、理由を記録します。Codexの指摘が正しければ修正します。誤指摘や過剰な指摘であれば、なぜ採用しないのかを説明します。 その後、Codexに再批判させます。前回の主要指摘、Claude Codeの採否判断、修正後の差分を渡し、指摘が解消されたか、却下判断が妥当か、新しい問題がないかを確認します。 Codexレビューの最大3サイクルと対話型切り替え また、Codexへ送るdiffやプロンプトには、APIキー、パスワード、トークンなどの秘密情報が含まれていないか確認する制約を入れています。AI連携を標準化するほど、品質だけでなくセキュリティ上の運用ルールもコマンドに組み込む必要があります。 この設計で重要なのは、Claude CodeとCodexが「最終判断者」ではないことです。Codexは批判者として独自の観点を出し、Claude Codeは実装者として採否を判断して修正します。最終的に自動モードで収束しない場合は、人間の判断に戻します。 Playwright MCPで画面確認まで接続する フロントエンド変更では、コードレビューだけでは不十分です。実際の画面でDOMが崩れていないか、コンソールエラーが出ていないか、ネットワークエラーが起きていないかを見る必要があります。 /dev-resume では、Playwright MCPが設定されており、修正対象にフロントエンド関連ファイルが含まれる場合、確認対象URLの手がかりを探します。手がかりがある場合に、画面を自動確認します。 確認内容は次のとおりです。 DOM構造の確認 コンソールエラーの検出 ネットワークエラーの検出 問題検出時の自動修正 問題を検出した場合は、自動修正を最大2回まで試行します。Playwright MCPが未設定の場合は、そのステップを自動でスキップし、フローを止めません。 標準コマンドとして使うには、「環境が整っている場合は自動で確認し、整っていない場合はスキップして作業を継続する」というフォールバックが必要です。 セットアップ 実装はGitHubのリポジトリで管理して、以下の方法で利用できるようにしています。 既にリポジトリを利用している場合は git pull で /dev-init と /dev-resume が利用可能になる 利用していない場合はClaude Codeの /plugin (marketplace) からmarketplace URLを追加してインストールする 標準化によって何が変わるか /dev-init と /dev-resume の標準化によって変わることは大きく4つあります。 1つ目は、初動コストの低下です。利用者は、開発開始時は /dev-init 、再開時は /dev-resume だけ覚えればよくなります。工程ごとの細かいコマンドを覚える必要はありません。 2つ目は、設計書と進捗管理表の品質の均一化です。コードベース調査、関連ファイル、既存実装パターン、受入条件、テスト観点が一定のフォーマットで残ります。これにより、AIへの依頼も、人間同士のレビューも、同じ材料をもとに進められます。 3つ目は、AI活用判断の可視化です。人間が選択する箇所、AIに調査させる箇所、Codexレビューを挟む箇所、画面確認する箇所が、コマンドの流れとして現れます。これは教育装置としても機能します。 4つ目は、標準コマンドの中身を更新し続けられることです。AIツールやモデルは変化が速く、今日の最適解が数か月後も最適とは限りません。利用者のUIを2コマンドに保ったまま、内部のプロンプト、Skills、レビュー観点、MCP連携を更新すれば、組織全体を新しいAI駆動開発へ追従させやすくなります。 2コマンドの安定した入口と内部更新の関係 標準コマンドは、効率化ツールであると同時に、教育装置でもあります。AI活用に慣れていない人でも、 /dev-init を使うと、Jira情報の整理、受入条件の確認、コードベース調査、設計書作成、タスク分解、テストパターン作成という流れを体験します。 /dev-resume を使うと、進捗管理表の読み込み、Git差分との照合、次のアクション選択、レビュー、画面確認という流れを体験します。 つまり、コマンドを使うこと自体が「開発時に何を確認すべきか」を可視化します。最初はコマンドに沿って進めるだけでも、繰り返すうちに、AIに任せやすい工程、人間が判断すべき工程、レビューで見るべき観点が分かるようになります。 Step 1: 2コマンドを使える │ ツールが判断ポイントを可視化する ▼ Step 2: コマンドが問いかける項目から、開発時に必要な判断観点を理解する │ 繰り返し使ううちに、AIに任せるべき箇所と人が判断すべき箇所の感覚が育つ ▼ Step 3: フレームワークなしでも適切なAI利用判断ができる 効果が出やすいのは、Jiraに要件や受入条件がある程度まとまっており、既存コードの調査範囲も推測できるタスクです。たとえば、既存機能の改修、入力チェックの追加、APIや画面の一部変更、既存パターンに沿った実装などは、AIの得意な同一レベル変換に寄せやすい領域です。 逆に、効果が出にくいのは、要求そのものが曖昧な段階です。誰の何の課題を解くのか、何を優先するのか、どのリスクを受け入れるのか。このような点が未定の状態では、AIへ渡す前に人間側で論点を整理する必要があります。 残課題と今後 標準化によって多くの課題を解決できましたが、引き続き取り組むべき課題も残っています。 まず、Codex連携にはコストと時間がかかります。Claude Code単独で進めるよりも、Codexによる独自調査、批判、Claude Codeによる採否判断、再批判まで行うためです。重要な機能やリスクの高い変更では有効ですが、すべてのタスクで常に使うべきとは限りません。 次に、自動実行の限界があります。最大3サイクルまで回しても同一の指摘内容が残る場合、人間の判断に戻す必要があります。これは失敗ではなく、標準コマンドが越えてはいけない境界です。AIが収束できない論点を人間に戻すことも、品質担保の一部です。 また、Playwright MCPによる画面確認は、環境整備に依存します。確認対象URLの手がかり、認証、テストデータ、安定した環境がなければ、自動確認は十分に機能しません。AI駆動開発の前提として、開発環境や検証環境を整える必要があります。 効果測定も今後の課題です。標準コマンドを作るだけでは、組織としての改善は証明できません。設計書の作成時間、実装着手までのリードタイム、レビュー指摘の質、利用回数、チームごとの利用状況、手戻り件数などを継続的に見ていく必要があります。 最後に、AIに任せない領域は固定ではありません。私は、現時点では、企画、要求整理、優先順位、顧客価値の判断は人間が主導すべきだと考えています。ただし、AIの性能向上や学習技術の変化により、境界は動きます。標準コマンドの設計も、その変化に合わせて見直し続ける必要があります。 AIに任せる境界を継続的に見直す考え方 今後は、 /dev-init と /dev-resume を使った実績を蓄積し、効果測定と改善サイクルを回していきます。 また、開発工程だけでなく、要求定義や企画整理を補助するコマンドへの応用も検討しています。ただし、上流工程は抽象から具体への変換が増えるため、開発工程と同じようにAIへ任せるわけにはいきません。AIが得意なフォーマット統一、網羅性チェック、矛盾検出と、人間が判断すべき優先順位や顧客価値を切り分ける必要があります。 AI駆動開発は、特定のツールを導入すれば完了するものではありません。ツール、プロンプト、レビュー、環境、効果測定、教育を含めて、開発プロセスとして運用し続ける必要があります。 まとめ AI活用が広がるほど、個人ごとのプロンプトやワークフローは増えていきます。そのままでは、高度に使いこなす人と、どこに使えばよいか分からない人の差が開きます。 基幹システム本部では、この属人化に対する答えを、開発開始時の /dev-init と開発再開時の /dev-resume という2つの入口に集約しました。 /dev-init はAIが迷わない初期状態を作り、 /dev-resume は文脈を失わずに作業を続けるためのコマンドです。 設計思想としては、AIの本質を確率的な予測変換機と捉え、同一レベル変換と具体から抽象のタスクを中心にAIへ任せました。一方で、企画、要求整理、優先順位、最終責任のような抽象から具体への判断は、人間が主導する領域として残しています。 品質を重視する場面では、Claude CodeとCodexの批判的対話を組み込みました。Claude Codeが設計・実装を進め、Codexが独自調査に基づいて批判し、Claude Codeが採否を判断し、Codexが再批判します。これにより、単一モデルに依存しないレビュー観点を取り入れています。 2コマンドという安定したUIを維持しながら、中身のプロンプト、Skills、レビュー観点、MCP連携を更新し続けます。これが、AI駆動開発を個人技から組織標準へ移すための現実的なアプローチだと考えています。 AIの進化に合わせて、人間とAIの境界は変わります。だからこそ、固定された手順ではなく、更新し続けられる標準として運用していくことが重要です。今後も、AIを前提にした開発プロセスを磨き、設計からテストまでの品質と速度を組織として引き上げていきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
はじめに こんにちは、Developer Engagementブロックの @wiroha です。5月13日に「 RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜 」を開催しました。 株式会社ZOZO、株式会社リブセンス、株式会社TOKIUM、株式会社マイベストの4社共催で、 RubyKaigi 2026 を振り返るアフターイベントです。初参加エンジニアによるLTと、公募によるLT、各企業によるブース運営に関するパネルディスカッション、そして懇親会を行いました。 当日の雰囲気を含めてレポートします! 登壇内容まとめ 発表タイトル 登壇者 ESP32 IoTを動かしながらメモリ使用量を観測してみた話 株式会社ZOZO もっちゃん Rubyはただの言語に非ず 株式会社リブセンス こりん Rubyの内側を意識し始めた日 株式会社マイベスト koki515 RubyKaigi Mapを作って出そうとした話 株式会社TOKIUM ikeda 公募LT - パネルディスカッション 各社スポンサー担当 ESP32 IoTを動かしながらメモリ使用量を観測してみた話 speakerdeck.com 株式会社ZOZOのもっちゃんからは、ESP32とPicoRubyを使ってIoTシステムを構築した話がありました。メモリ消費量の節約への努力が感じられました。 Rubyはただの言語に非ず speakerdeck.com 株式会社リブセンスのこりんさんは、Rubyはただの言語ではなく文化であるとお話していました。RubyKaigi初参加ながら、RubyKaraokeといった関連イベントにも積極的に参加していたことが印象的でした。 Rubyの内側を意識し始めた日 speakerdeck.com 株式会社マイベストのkoki515さんは、Rubyコミッターの話を聞くことで内部構造をもっと理解したいと思うようになったそうです。RubyKaigiの会場には本屋さんがありCRubyの本を購入して読み始めたとのことで、良い学びの流れができているなと感じました。 RubyKaigi Mapを作って出そうとした話 speakerdeck.com 株式会社TOKIUMのikedaさんは、RubyKaigiの開催地を地図上にマッピングした「RubyKaigi Map」について発表しました。地震により当日披露が叶わなかったシステムを見ることができました。 ここまで、25卒の4名の若手エンジニアによる発表を紹介しました。「発表に慣れていない、緊張する」と言っていた方々もいましたが、堂々と意欲あふれる発表をされていました。 Spinelに貢献した話 speakerdeck.com 公募によるLT枠では、note株式会社のsacckeyさんよりRubyのAOTコンパイラであるSpinelにコントリビュートしたという発表がされました。「Spinelでは失敗するがCRubyでは成功する5行のRubyコード」という指標がわかりやすく、挑戦してみたくなる内容でした。 飛び入りLT 公募枠が1枠余っていたため、マイベストのKoyaさんが飛び入りでLTをしてくださいました。「カンマは演算子ではない」をテーマに、Rubyの文法を深掘りした内容でした。急遽対応いただきありがとうございました! パネルディスカッション 4社のスポンサー担当者による、ブース運営についてのパネルディスカッションを行いました。どんなブースを出して(出す予定で)いたか、その決め方や苦労などをお聞きできました。 当日見られなかったコンテンツを知ることができたり、SNSで話題になっていた投稿の裏側を知ることができたりと、興味深い内容が盛りだくさんでした。 最後に 発表の終了後には懇親会も行い、活発に交流する様子が見られました。ローカルオーガナイザーの方も参加してくださっていたため、参加者・運営・スポンサー企業といったさまざまな立場の方とのつながりが生まれていたように感じました。ご参加くださったみなさま、ありがとうございました! 来年のRubyKaigi 2027は宮崎での開催です。ZOZOは宮崎にオフィスがあるため、何か企画ができないものかと話し合っています。また来年もたくさんのRubyistたちとお会いできることを楽しみにしています! corp.zozo.com
.images-row {width: 100% !important;} Developer Engagementブロックの @ikkou です。2026年5月22・23日の2日間にわたりベルサール羽田空港で「TSKaigi 2026」が開催されました。 ZOZOはGold Sponsorとして協賛し、スポンサーブースを出展しました。ZOZOがTSKaigiに協賛するのは今回が初めてです。 technote.zozo.com 本記事では、前半はZOZOのWebフロントエンドエンジニアが気になったセッションを紹介します。後半では、ZOZOのスポンサーブースの様子と各社のブースにおけるコーディネートを写真中心に報告します。 ZOZOのWebフロントエンドエンジニアが気になったセッション 開発体験を左右するライブラリの API 設計 ― GraphQL スキーマ構築ライブラリから考える 「関数型プログラミング」を分解する.ts 純粋性について 型でエフェクトを表す いつテストを書くか?―ソフトウェア開発における安心と不安について考える LLM時代のリファクタリング戦略:AIエージェントによる段階的・安全なTS移行方法 TypeScript の型で副作用の実行順序を制御する ZOZOのスポンサーブースの紹介 協賛企業ブースのコーディネートまとめ おわりに ZOZOのWebフロントエンドエンジニアが気になったセッション 開発体験を左右するライブラリの API 設計 ― GraphQL スキーマ構築ライブラリから考える ssssota です。izumin5210さんの「 開発体験を左右するライブラリの API 設計 ― GraphQL スキーマ構築ライブラリから考える 」を紹介します。 speakerdeck.com このセッションでは、スキーマや型情報をいかにTypeScriptの実装に接続するかという観点で、既存ライブラリのアプローチやその長短を深ぼる内容でした。弊社ではOpenAPIを使っているケースが非常に多く、いかにOpenAPIスキーマを実装に接続するかは往々にして発生する問題の1つです。 セッションではGraphQLに焦点が当てられていましたが、スキーマから実装を生成するスキーマファースト、コードからスキーマを生成するコードファースト、コードファーストのうちDecoratorsを使うパターン、DSL的な独自のbuilderパターン、計3パターンについて評価していました。比較・評価軸として、1.スキーマと実装の分離、2.型整合性、3.DBモデルとの接続、の3軸を用いています。 スキーマと実装の分離については、スキーマファーストが優れているのは言うまでもありませんが分離する強いモチベーションがなければ優先度は低くなります。型整合性は採用するライブラリのtype ergonomicに依りますが、コードファーストなDSL builderパターンが強い傾向にあります。DBモデルとの接続においてはGraphQL特有と見ることができますが、コードファーストなDSL builderパターンで型整合問題と合わせて解決できることを示唆しています。 セッションの最後には、自作のライブラリでこのギャップを埋める取り組みとAIを用いた評価結果を紹介していました。気になる方はスライドも合わせて確認してみてはいかがでしょうか。 私自身、OpenAPIスキーマと実装の接続に関して関心があり、ライブラリ( openapi-ts-hono )を作った経験から非常に共感できるところがありました。もちろんGraphQLとはギャップがありますが、スキーマと実装の分離、型整合性などは感覚としてもっていながらも、改めて言語化されることで気付きのあるセッションでした。 「関数型プログラミング」を分解する.ts www_REM_zzz です。おーみーさんの『 「関数型プログラミング」を分解する.ts 』を紹介します。 tsk-2026-aumy.vercel.app 自分の話ですが、TypeScriptに入門する前はScalaを書いていた経験があります。当時はコップ本と呼ばれる本とHaskellの公式ドキュメントが日本語で関数型プログラミングに入門する入口でした。Object指向プログラミングとは全く別の世界からやってきたような考え方で、面白くもあり、苦労もした過去があります。 このセッションでは、そもそも関数型プログラミングとは何なのかの考え方に触れながら、TypeScriptで真の関数型はできないのかに触れられています。僕もTypeScriptで真の関数型が書けたらいいのにと思った一人です(OCaml書けよというのは一旦置いといて)。スライドの中で語られた関数型プログラミングは「いい感じのソフトウェアを作るため」というのは本質的だなと思いました。ついつい手段に引っ張られてしまうところがあるのですが、心に留めておきたいです。 純粋性について 特に純粋性についてのところはReactでも他のライブラリでも語られる部分であり、意味の純粋性の部分は悩ましいと感じたことがあるので共感しました。 // 「副作用を表す値」を返すだけ(純粋関数) function pureAlert ( msg : string ) { return [ "alert" , msg ] as const ; } // 副作用の実行は別の関数に委ねる function executeAction ( action : readonly [ "alert" | "confirm" , string ] ) { switch (action[ 0 ]) { case "alert" : alert (action[ 1 ]); break ; case "confirm" : confirm (action[ 1 ]); break ; } } const actions = [ pureAlert( "hey" ), pureAlert( "bye" ) ] ; actions. forEach (( a ) => executeAction(a)); 引用: https://tsk-2026-aumy.vercel.app/29 このような「何をするかの宣言」と「実行」が分離されている書き方は普段からできるし、メンテナンスを考えると普段から実践していきたいと思いました。 return は「この関数の呼び出し元(= 継続)に値を渡して戻る」という考え方はTSを書いていてなんとなく感じていたものがはっきりと言語化されてスッキリした気持ちになりました。 型でエフェクトを表す () => T // 特に何も起きない純粋な処理 () => Option< T > // 失敗しうる処理 () => Promise < T > // 非同期処理 これを徹底すると 関数の型を見るだけで「何が起きるか・何が起きないか」がわかる 純粋な部分と副作用のある部分が型レベルで分離される 「支払い処理を起こしうる部分」だけを特定して二重実行を防げる これはTypeScriptを堅牢に書くうえで実践したいと思います。ちょうど業務でも似たシチュエーションがあることを思い出して、まず「この関数は副作用を持つか?」を命名( execute , get , ! 記法)で示すのが現実的な入口かなと思いました。 いつテストを書くか?―ソフトウェア開発における安心と不安について考える ジン( @Jin_pro_01 )です。自分の気になったセッションとして、 lacolacoさん の「 いつテストを書くか?―ソフトウェア開発における安心と不安について考える 」を紹介します。 docs.google.com このセッションでは、テストをどのような時に書くべきなのかを「開発者の安心と不安」を起点に問い直したlacolacoさんの気づきの共有、問いの提示、視点の提案をするというセッションでした。 セッションの中ではソフトウェアの保守性の本質は「変更容易性」であり、それは予期的変更容易性(変更する前に感じる不安)と経験的変更容易性(変更をする中で実際に感じる手応え)の二層モデルとして見ることができるとしていました。その上でテストはその両方にフィードバックを返すセンサーであるとし、変更前に感じる不安があるならそれを取り除く安心のために書き、変更のしやすさを試したり構造に問題が見つかったりするなら設計を見直すために書くという体系的な整理がされており、とても興味深いセッションでした。 自分が従事しているZOZOTOWNでは、新規機能の実装や既存機能の改修と並行で、フロントエンドリプレイスも各チームで進行しています。ZOZOTOWNの発展を止めずに開発を進める体制である一方、考慮すべきことが多く、自分にとっては比較的「予期的変更容易性」が低い状態だと表現できることに気づきました。そして、まさにこの「予期的変更容易性」を高めるためのテストへの投資価値が高いと感じました。 さらにAIを使ってコーディングをしていく時代に入り、開発の生産量が増える一方で、自分が直接書いていないコードや構造との距離は広がっていきます。その距離は新たな不安、つまり予期的変更容易性の低下にもつながると感じています。だからこそ変更の前後で「振る舞いが変わっていないこと」を担保し、その不安を取り除くセンサーとしてのテストの価値は、AI時代にこそますます高まっていくのだと考えました。 最大の収穫は、テストを書く目的を「ソフトウェアがソフトであり続けるための、変更容易性のセンサー」と説明できるようになったことです。テストはあくまで手段の1つと捉えつつ、ZOZOTOWNがソフトであり続けるために、他に何ができるかも考えていきたいと思いました。 LLM時代のリファクタリング戦略:AIエージェントによる段階的・安全なTS移行方法 いもけん( @iimokeenpi )です。「 LLM時代のリファクタリング戦略:AIエージェントによる段階的・安全なTS移行方法 」について紹介します。 speakerdeck.com このセッションは、JSのコードをAIエージェントを使い安全にTSに移行するというものでした。しかし、JSからTSへの移行のみならず日常的なリファクタリングにおいても活用できそうなノウハウが詰まっていました。 特に自分が興味を持った部分としては”test-firstフロー”と”役割ごとにサブエージェントを切り出す”の2つがあります。AIエージェントの使用有無にかかわらずリファクタリングの際にデグレには細心の注意を払って行っていきたいところです。そこで”test-firstフロー”というのは、デグレの防止策としても効果が高くAIエージェントとの相性もかなり良いなと感じました。 そして“役割ごとにサブエージェントを切り出す”という点に関してです。自分は基本的に全てOpusで乗り切ろうとしていたのですが、消費トークンの効率や時間的な効率の面でも損をすることが多々あります。なので役割ごとにサブエージェントを切り出し、モデルを使い分けることはすぐにでも実践したいと感じました。 TypeScript の型で副作用の実行順序を制御する 佐藤です。私が印象に残ったセッションは「 TypeScript の型で副作用の実行順序を制御する 」です。 speakerdeck.com Branded Typeは「 UserId と ProductId を区別するためのタグ付け」くらいにしか使えないと思っていましたが、Type-State Patternを使えばそれが実行順序の制御に転用できます。TypeScriptの型システムでここまで表現できるのかと、型に対する認識が更新されました。 加えて魅力的なのが、ライブラリ依存ゼロで既存コードに薄く入れられる点です。Effect-TSやXStateは強力ですが導入コストは高いです。Type-Stateパターンなら守りたい箇所だけにピンポイントで適用できます。 実際、 getServerSideProps 内に「バリデーション→取得→加工」のような実行順序を守らなければならない処理があり、これまではAIのルールや運用上の規約に頼らざるを得ませんでした。型で制御できるようになれば、コードレビューや属人的な注意に依存せず、エディタ上でミスを即座に検出できます。自分のチームに導入できないか実践したいと思えるトークでした。 サンプルコードは GitHubで公開されています 。既存ライブラリとの比較実装も含まれているので、ぜひ手元で動かしてみてください。 ZOZOのスポンサーブースの紹介 ZOZOのスポンサーブースとWebフロントエンドエンジニアたち ZOZOのスポンサーブースでは「 Google I/O 2026から帰国したばかりのZOZOフロントエンドエンジニア テックリード ssssota に挑戦! 」と題したTypeScript & JavaScript Quizをメインコンテンツとして提供しました。日替わりで全10問、ブースにはその日のクイズから1問だけ掲示しました。 TypeScript & JavaScript Quiz Day 1 & Day 2 ZOZOブースでは #GoogleIO から帰国したばかりの Web フロントエンド テックリード @ssssotaro が考えた JavaScript & TypeScript Quiz を実施中です!難易度は高め!ぜひ挑戦してください! #TSKaigi pic.twitter.com/7K9ZTt22Qq — ZOZO Developers (@zozotech) 2026年5月22日 \TSKaigi 2026 最終日/ 今日もクイズ企画を開催しています!昨日とは異なる問題で、今日は特典をゲットしやすくなっています! オリジナル洗濯ネットをご用意していますので、ぜひご参加ください! #TSKaigi pic.twitter.com/QwR6v2F96t — ZOZO Developers (@zozotech) 2026年5月23日 難しい! ということが話題になり、とても多くの方に挑戦してもらいました。難しいのは作問者の意図通りですが、この「難しい」ということが反響を呼び、楽しんでもらえたのではないでしょうか。 No Bugs, Just Clean. というメッセージの込められた特製ノベルティの洗濯ネット クイズに挑戦し、7問以上正解した方には特製ノベルティの「洗濯ネット」をお渡ししました(Day 2は3問以上正解した方に変更)。 Day 1、Day 2の7問以上正解者 また、上位正解者の皆さんにはリーダーボードにもハンドルネームなどを書いてもらいました。2日間を通しての全問正解者は、Day 1が @uhyo_ さんと @vaaaaanquish さんの2名、Day 2が @U3Qc9 さんの1名だけでした。改めて全問正解おめでとうございます! このTypeScript & JavaScript Quizに関する解説記事を別記事として公開しています。あのクイズの答えが気になるという方はもちろん、もう一度あのクイズに挑戦したい、当日できなかったので挑戦したい! という方もぜひご覧ください。 techblog.zozo.com 10分セッションに登壇中のテックリード ssssota この難問揃いのクイズを作問したテックリードのssssotaはDay 2に「 ReactとSvelteのその先、Ripple-TS 」というタイトルで10分セッションにも登壇しています。こちらもあわせてご覧ください。 speakerdeck.com 協賛企業ブースのコーディネートまとめ ジン( @Jin_pro_01 )です。セッションを見たり、自社ブースに立ったりしている合間にTSKaigi 2026の全協賛企業ブースを回ってきました。当日の会場の様子を思い出しながら、各社の個性や雰囲気の出るデザイン・着こなしをぜひご覧ください。 ウェルスナビさん。 / @WealthNavi_Tech AVITAさん。 Dress Codeさん。 / @dresscode_com Hacobuさん。 / @MHacobu sattoさん。 / @satto_ai_agent アサインさん。 / @ASSIGN_dev レバレジーズさん。 PLAINERさん。 / @plainer_inc ビットキーさん。 / @bitkey_dev UPSIDERさん。 / @upsider_inc ニーリーさん。 / @nealle_pr LayerXさん。 / @LayerX_tech エブリーさん。 / @every_engineer スリーシェイクさん。 / @3shake_Inc ミツモアさん。 / @meetsmore Ubieさん。 / @UbieCorp_JP Nstockさん。 / @Nstock_jp プレイドさん。 / @PLAID_Tech ギークプラスさん。 / @GeekJapan1 ウォンテッドリーさん。 / @wantedly_dev サイボウズさん。 / @cybozuinsideout ドワンゴさん。 / @dwango_tech CodeRabbitさん。 / @Coderabbitaija シェルパ・アンド・カンパニーさん。 ファインディさん。 / @findy_code ディップさん。 / @dip_developers RightTouchさん。 / @righttouch_dev Gaji-Laboさん。 / @gaji_labo スタメンさん。 / @stmn_eng TOKIUMさん。 / @TOKIUM_Dev カオナビさん。 / @kaonavi_jp テイラーさん。 / @TailorERP_JP KINTOテクノロジーズさん。 / @KintoTech_Dev MOSHさん。 / @MOSHinc_jp 皆さん照れていたりウキウキしていたりしてよかったです! ご協力いただいた皆さん本当にありがとうございました! おわりに TSKaigi 2026 協賛企業一覧 TSKaigiへの初協賛を通して、ZOZOのことが少しでも来場者の皆さまに伝わっていれば嬉しいです。みなさま、ありがとうございました! TSKaigi 2026をきっかけとしてZOZOのWebフロントエンドエンジニアに興味を持たれた方は、技術スタックなどがまとまったページをぜひご覧ください。 techblog.zozo.com ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、基幹システム本部リプレイス推進部の ssssota です。本記事では、TSKaigi 2026、ZOZOのスポンサーブースで実施したクイズを紹介・解説します。 はじめに TSKaigi 2026は、2026年5月に実施されたTypeScriptに関するカンファレンスです。ZOZOはゴールドスポンサーとして参加し、スポンサーブースでTypeScriptやJavaScriptに関するクイズを実施しました。TSKaigi 2026のレポートは以下の記事にまとめていますので、あわせてご覧ください。 techblog.zozo.com 来場者の皆さんに体験してもらったクイズアプリはGitHubリポジトリで公開しています。RippleというUIフレームワークを用いてAIと共に実装しました。興味のある方はぜひリポジトリもご覧ください。 github.com Rippleに興味のある方は、私がTSKaigi 2026で発表した登壇資料もあわせてご覧ください。 speakerdeck.com 目次 はじめに 目次 Day 1 TypeScript Q1. 次のコードはエラーになる? (tsconfig strict:true) Q2. X の型は? Q3. TypeScript 7 はなんの言語で開発されている? Q4. enum を TypeScript コンパイラに渡すとどのような JavaScript コードが出力される? Q5. erasableSyntaxOnly でエラーになるのは? JavaScript Q6. 次のコードの出力は? Q7. 次の式の結果は? ランタイム Q8. 次の JavaScript ファイルを実行するとエラーになるのは? Q9. 次の JavaScript ファイルを実行するとエラーになるのは? Q10. URL クラスはどの組織・仕様グループで標準化されている? Day 2 TypeScript Q1. X の型は? Q2. satisfies の正しい挙動は? Q3. TypeScript 7 (tsgo) の開発コードネームは? Q4. 返り値の型が void で推論されるのは? JavaScript Q5. 次のコードの結果は? Q6. 次のうち JavaScript (ECMAScript) の予約語は? Q7. 次の式の結果は? ランタイム Q8. 次の TypeScript ファイルを実行するとエラーになるのは? Q9. globalThis.navigator.share() メソッドが使えるのは? Q10. fetch API はどの組織・仕様グループで標準化されている? おわりに Day 1 TypeScript Q1. 次のコードはエラーになる? (tsconfig strict:true) const a = 1 + '1' ; 実行時エラー コンパイルエラー ならない 答えと解説 正解: 3. ならない JavaScriptではnumberとstringの + 演算はstringへの暗黙変換で評価され、TypeScriptもこのケースは許容するためコンパイル/実行どちらもエラーになりません。禁止したい場合はESLint (typescript-eslint) の restrict-plus-operands やOxlintの typescript/restrict-plus-operands ルールを使用する必要があります。 Q2. X の型は? type X = unknown extends number ? true : false ; true false boolean 答えと解説 正解: 2. false unknown は最上位型で number に代入可能ではないため、Conditional Typeは false 側に分岐します(参考: TypeScript Playground ) Q3. TypeScript 7 はなんの言語で開発されている? TypeScript Rust Go 答えと解説 正解: 3. Go TypeScript 7 (tsgo) はネイティブ実装としてGoで書き直されています。 Q4. enum を TypeScript コンパイラに渡すとどのような JavaScript コードが出力される? enum Hoge { a, b } const a: Hoge = Hoge.a; IIFEでHogeオブジェクトを構築する形に展開される const enumと同等にインライン定数へ展開される 答えと解説 正解: 1. IIFE で Hoge オブジェクトを構築する形に展開される 通常のenumはランタイムオブジェクトとして残り、IIFEで双方向マップを構築する形に展開されます。const enumはインライン化されます(参考: TypeScript Playground ) Q5. erasableSyntaxOnly でエラーになるのは? class Hoge { private a ?: number // A private b () {} // B constructor ( private c : number ) {} // C } A B C 答えと解説 正解: 3. C parameter properties ( constructor の private c ) は、コード除去するだけでは等価にできずエラーになります。 JavaScript Q6. 次のコードの出力は? console . log ( typeof null ) ; "null" "undefined" "object" 答えと解説 正解: 3. "object" 歴史的経緯により typeof null は "object" を返します(参考: typeof - JavaScript | MDN ) Q7. 次の式の結果は? JSON . stringify ({ nan : NaN }) {"nan":NaN} {"nan":null} Error 答えと解説 正解: 2. {"nan":null} JSONではNaNを表現できないため、 JSON.stringify はNaNを null にシリアライズします(参考: JSON.stringify() - JavaScript | MDN ) ランタイム Q8. 次の JavaScript ファイルを実行するとエラーになるのは? globalThis . alert ( "Hello, TSKaigi!" ) ; // node ./index.mjs // deno run ./index.mjs // bun run ./index.mjs Node.js Deno Bun 答えと解説 正解: 1. Node.js alert はWeb互換APIとして Deno と Bun ではサポートされていますが、Node.jsには存在しません。 Q9. 次の JavaScript ファイルを実行するとエラーになるのは? const obj = {} ; obj . __proto__ . a = 1 ; console . log ( obj . a ) ; // node ./index.mjs // deno run ./index.mjs // bun run ./index.mjs Node.js Deno Bun 答えと解説 正解: 2. Deno Denoはセキュリティ上の理由から、 Object.prototype.__proto__ をサポートしていません。使用する場合は --unstable-unsafe-proto フラグを付けて実行する必要があります。Node.jsとBunはサポートしています。Node.jsでも --disable-proto フラグで無効化できます。 Q10. URL クラスはどの組織・仕様グループで標準化されている? WHATWG ECMA-262 W3C 答えと解説 正解: 1. WHATWG URLはWHATWGの URL Standard で標準化されています。 Day 2 TypeScript Q1. X の型は? type X = 1 extends number ? true : false ; true false boolean 答えと解説 正解: 1. true リテラル型 1 は number のサブタイプなので、Conditional Typeの真側 true が選ばれます(参考: TypeScript Playground ) Q2. satisfies の正しい挙動は? const x = { a : 1 , b : 2 } satisfies { a: number; b: unknown } ; x の型は { a: number; b: number } x の型は { a: number; b: unknown } コンパイルエラー 答えと解説 正解: 1. x の型は { a: number; b: number } satisfies は制約に適合することを検証しつつ、変数自身の推論結果(ここでは { a: number; b: number } )を保持します(参考: TypeScript Playground ) Q3. TypeScript 7 (tsgo) の開発コードネームは? Breeze Corsa Strada 答えと解説 正解: 2. Corsa tsgoの開発コードネームはCorsaです。Stradaは既存のJS実装、Breezeは社内PJの名称です(参考: A 10x Faster TypeScript ) Q4. 返り値の型が void で推論されるのは? const A = () => { throw 'Oops' ; } ; function B () { throw 'Oops' ; } const C = function () { throw 'Oops' ; } ; A B C 答えと解説 正解: 2. B 関数宣言は return 文がない場合、返り値の型が void として推論されます。アロー関数や関数式は、 throw により返り値なしであることが推論され、返り値の型は never になります(参考: TypeScript Playground ) この挙動に関する詳細はTypeScriptのIssue #16608 、Pull Request #8767 、さらにTypeScriptのLead開発者が回答しているStackOverflow Inconsistent never type inference も参考になります。 JavaScript Q5. 次のコードの結果は? "use strict" ; let str = "zozo" ; str [ 0 ] = "s" ; console . log ( str ) ; "sozo" "zozo" Error 答えと解説 正解: 3. Error 文字列はプリミティブで不変です。strictモードでは str[0] はread only propertyとして代入不可、TypeErrorになります。ちなみに非strictモードでは代入は無視され、エラーにならず "zozo" が出力されます。 Q6. 次のうち JavaScript (ECMAScript) の予約語は? string with using 答えと解説 正解: 2. with with はECMAScriptの予約語です。 string はTypeScriptにおける型名、 using は文脈依存キーワードで予約語ではありません。 Q7. 次の式の結果は? ( NaN == NaN ) === ( NaN === NaN ) true false Error 答えと解説 正解: 1. true NaNは自分自身とも等しくないため、 == / === ともに false を返し、結果は false === false で true です。 == と === の違いは型変換の有無ですが、どちらもNaNには適用されないため、結果は同じになります。 ランタイム Q8. 次の TypeScript ファイルを実行するとエラーになるのは? const enum Hoge { a, b } console . log (Hoge.a); // node ./index.mts // deno run ./index.mts // bun run ./index.mts Node.js Deno Bun 答えと解説 正解: 1. Node.js Node.jsのtype strippingはerasable syntaxのみを対象としておりconst enumを扱えません。DenoとBunはサポートしています。 Q9. globalThis.navigator.share() メソッドが使えるのは? iOS WebView Android WebView Deno 答えと解説 正解: 1. iOS WebView Web Share API ( navigator.share ) はiOS WebViewではサポートされますが、Denoではサポートされていません。Android WebViewでは現在バグとして利用できない状態にあります(参考: Navigator: share() メソッド - ブラウザーの互換性 、 Web Share API and Media Session API don't work in Android WebView 40540400 - Chromium ) Q10. fetch API はどの組織・仕様グループで標準化されている? WHATWG ECMA-262 W3C 答えと解説 正解: 1. WHATWG fetchはWHATWGの Fetch Standard で標準化されています。 おわりに 以上、TSKaigi 2026スポンサーブースクイズの紹介でした。現地でクイズに挑戦してくださった皆様、改めてありがとうございました。かなりマニアックな内容が多く難しかったと思いますが、楽しんでいただけていれば幸いです。 また、TSKaigi 2026のスポンサーブースや、本記事で解説したクイズを通してZOZOのWebフロントエンドエンジニアに興味を持たれた方は、技術スタックなどがまとまったページをぜひご覧ください。 techblog.zozo.com ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。WEARバックエンド部SREブロックの 春日 です。普段は WEAR というサービスのSREとして開発・運用に携わっています。 本記事では、WEARのハイブリッド検索のリリースに伴い刷新した検索インデクシングシステム(以下、インデクサー)について、 OpenSearch Ingestion を採用しようとした際にハマったポイントや、ベクトル検索のためのインデクサーを設計する上で工夫した点を中心に紹介します。 目次 はじめに 目次 背景 既存のインデクサーと刷新の動機 ベクトルデータの保持方法の検討 インデクサーの構成方針 BigQuery → S3 のデータ連携 日次更新の設計 差分更新の設計 初期設計:OpenSearch Ingestion+Lambdaプロセッサでのベクトル化とインデクシング 1万件の差分更新で表面化した問題 再設計:ベクトル化Lambdaを前段に出す 最終設計:S3+SQS+Lambdaで非同期にベクトル化とインデクシング ベクトル化Lambdaでの工夫 Bedrockのリージョン分散 1ファイル単位の処理量を制御する 出力形式と後処理 OpenSearch投入Lambdaでの工夫 external versionで古いデータで新しいデータを上書きすることを防止 処理完了後のファイル削除 Lambdaエラー時のファイル退避 非同期処理の完了待機 既存データに対する初回ベクトル化 結果 まとめ 背景 WEARでは、検索基盤として Amazon OpenSearch Service(以下、OpenSearch) を利用しています 1 。これまでフリーワード検索ではタグマッチングを主軸としていましたが、タグが付与されていない検索ワードに対する検索結果の質と量に課題がありました。 これを改善するため、ベクトル検索と全文検索を組み合わせたハイブリッド検索(WEARではあいまい検索と呼んでいるため、以下「あいまい検索」と表記)をリリースすることになりました 2 。 あいまい検索のためにベクトル検索を導入するには、検索対象の各documentに対して、タイトル・説明文・タグなどを連結したテキストをベクトル化したフィールドを持たせる必要があります。しかしながら、既存のインデクサーでこのフローを実現するのは難しく、インデクサー自体を刷新することになりました。本記事ではその刷新の過程と、設計時に行った工夫を紹介します。 既存のインデクサーと刷新の動機 WEARではOpenSearchへのインデクシングを Embulk を用いて行っていました。 embulk-input-bigquery と embulk-output-elasticsearch などを組み合わせ、 BigQuery からOpenSearchへデータを連携する構成です。Embulkのジョブは Digdag のworkflowで管理し、 Amazon EKS(以下、EKS) 上のJobとして実行していました 3 。インデクサーには差分更新と日次更新の2種類があり、それぞれ次の役割を持っていました。 差分更新:10分間隔で実行。直近で新規投稿・更新documentをインデクシングし、削除された投稿をindexから削除 日次更新:1日1回、新しいindexを作成して全件をインデクシングし、Blue/Greenでエイリアスを切り替える形で全件更新する。統計データなどの日次で更新すべき値はこのタイミングで反映 しかし、ベクトル検索の導入を検討するにあたり、この構成にはいくつかの課題がありました。 WEARで一番大きいコーディネートのindexは大量のdocumentを持っており、これらを毎日ベクトル化するのはコストと処理時間の両面で非現実的 BigQueryからOpenSearchへの連携中にベクトル化の処理を挟むのが困難 本対応の検討時点でEmbulkはすでにメンテナンスがされていない状態であり、長期的な保守性に不安 これらを踏まえ、ベクトル検索対応に必要な機能と、長期的な保守性の両方を満たす構成へとインデクサーを刷新する方針を決めました。 ベクトルデータの保持方法の検討 最初に取り組んだのが、ベクトルデータをどこに、どのタイミングで持たせるかという検討です。 既存の日次更新では新しいindexを毎日作成して全件インデクシングしていましたが、大量のデータを毎日全件ベクトル化するのは非現実的なため、ベクトルデータを別ストレージに保存しておく案を検討しました。しかし、ベクトル取得時のパフォーマンスやコスト面で見合わないと判断し、最終的には日次での全件更新そのものを廃止する方針を取りました。 毎日indexを全件更新するメリットの1つとして、indexの不整合が発生した場合に、日次での全件更新によって整合性を保つことができるという点がありました。これは例として、差分更新の失敗時のリトライで、古いデータで新しいデータが上書きされてしまうといった状況が挙げられます。全件更新を廃止するにあたり、この点をどう担保するのかが課題でしたが、後述する方法でdocumentのバージョニングを行うことで、不整合が発生しないようにしました。 新しい設計では、ベクトル化は差分更新のみで行い、日次更新では同じindexに対して日次で更新すべき値のみを上書きするように責務を分けました。これにより、ベクトル化を投稿の追加・更新時のみに限定でき、ベクトル化コストと処理時間の問題を回避できるようになりました。 インデクサーの構成方針 インデクサー刷新にあたって、ベクトル化を含む新しい構成として複数の選択肢を検討しましたが、OpenSearch Ingestionを軸とする構成を採用しました。判断のポイントは以下の通りです。 自前で運用する外部ツールは最小限にしたい(Embulkのように追加でメンテナンスが必要なツールを増やしたくない) データ抽出のSQLはバックエンドエンジニア、インデクサーのインフラ構築・運用はSREという責務分離を維持し、両者を疎結合にしたい AWS公式の Lambdaプロセッサでベクトル化するパターン を参考にすれば、ベクトル化部分をLambdaへ切り出して柔軟に構成できそう これらを総合的に考慮し、 Amazon S3(以下、S3) を起点とした AWS Lambda(以下、Lambda) の構成を方針として進めることになりました。 BigQuery → S3 のデータ連携 WEARでは Microsoft SQL Server からBigQueryへリアルタイム連携をしており、インデクサー側もBigQueryからデータを取得しています。前述の通り、インデクサーはS3を起点としてデータを処理する設計を取っているため、BigQueryから取得したデータをS3に連携する必要があります。BigQueryから直接S3へ出力する機能はないため、いったん Cloud Storage(以下、GCS) へ出力してからS3へ転送する形を取りました。 GCSへの出力にはBigQueryの EXPORT DATA 文 を利用しています。差分更新・日次更新いずれもJSON Lines形式でGCSへ出力するように記述しており、以下に差分更新を例にしたものを記載します。 EXPORT DATA OPTIONS( uri= ' gs://GCS_BUCKET/coordinates/diff/raw-data/YYYY/MM/DD/HH/mm/data_*.jsonl ' , format= ' JSON ' , overwrite= true ) AS -- 対象データを取得するクエリ ... uri にワイルドカード( * )を含めることで、BigQueryが出力サイズに応じて自動的に複数ファイルへ分割します。出力フォーマットは JSON を指定するとJSON Lines形式になります。 GCSからS3への転送方法は、差分更新と日次更新で異なるツールを使い分けています。 差分更新: rclone 日次更新: AWS DataSync(以下、DataSync) DataSyncは大量データの高速転送に適していますが、タスクの起動・実行に約5分かかります。差分更新は10分間隔で実行する上、ベクトル化のような時間のかかる処理も挟まるため、起動に時間のかかるDataSyncは許容できませんでした。差分更新ではデータ量がそこまで多くないこともあり、rcloneを採用しています。 日次更新の設計 日次更新では、もともとEmbulkで実装されていた全件更新を廃止し、差分更新と同じindexに対して統計データなどの日次で更新すべき値のみを上書きする方式に変更しました。日次更新の構成は以下の通りです。 日次更新はOpenSearch Ingestionを採用しており、S3に格納された全件データに対して S3 scan でOpenSearchへbulkでupsertしています。OpenSearch Ingestionのパイプライン定義の例は以下のとおりです。 version : 2 coordinates-daily-indexer : source : s3 : acknowledgments : true delete_s3_objects_on_read : true scan : buckets : - bucket : name : ${BUCKET_NAME} filter : include_prefix : [ "coordinates/daily/raw-data/" ] aws : region : ap-northeast-1 sts_role_arn : ${STS_ROLE_ARN} codec : ndjson : {} processor : - delete_entries : with_keys : - s3 sink : - opensearch : hosts : - https://${OPENSEARCH_HOST} aws : region : ap-northeast-1 sts_role_arn : ${STS_ROLE_ARN} index_type : custom index : coordinates document_id : ${/id} action : upsert max_retries : 10 bulk_size : 5 dlq : s3 : bucket : ${BUCKET_NAME} key_path_prefix : "dlq/coordinates/daily/" region : ap-northeast-1 sts_role_arn : ${STS_ROLE_ARN} source.s3.scan でS3バケット内の対象プレフィックスをスキャンします。 processor.delete_entries でS3イベントメタデータを落とし、 sink.opensearch でOpenSearchへbulk upsertしています。失敗したdocumentは dlq.s3 で指定したS3パスへ退避されます。 刷新前は4〜6時間ほど動き続けていた日次更新のジョブが、刷新後は1時間以内で完了するようになりました。これは更新フィールドを必要なものに絞れたことも要因の1つですが、OpenSearch Ingestionの処理が速いことも大きな要因です。 差分更新の設計 日次更新のような、すでにS3に格納されているデータをスキャンしてまとめて投入するユースケースでは、OpenSearch Ingestionは安定して動作することが分かっていました。ただし、S3 scanでS3データを処理するのはOpenSearch Ingestion起動のタイミングのみで、起動後にS3へ投入されたデータは処理されません。 日次更新の場合は実行のたびにOpenSearch Ingestionを起動し、完了したら終了させることで意図した動作が行えます。しかし、OpenSearch Ingestionの起動・終了にはそれぞれ5分ほどかかるため、10分ごとに実行される差分更新ではその実行時間は許容できません。 そのため差分更新では起動済みのOpenSearch Ingestionに Amazon Simple Queue Service(以下、SQS) 経由でデータを連携することにしました。これは、OpenSearch Ingestionのパイプラインを起動したまま、 SQS経由でリアルタイムに少量ずつデータを取り込む構成 です。 初期設計:OpenSearch Ingestion+Lambdaプロセッサでのベクトル化とインデクシング 差分更新の初期構成は以下の通りです。しかし、この構成ではいくつかの課題が発生し、最終的には断念しました。 OpenSearch Ingestionには Lambdaプロセッサ があり、パイプラインの途中でLambdaを呼び出して任意の処理を実行できます。これを使って Amazon Bedrock(以下、Bedrock) でのベクトル化を行う想定でした。 1万件の差分更新で表面化した問題 この構成で約1万件規模の差分更新を試したところ、いくつかの問題に直面しました。 OpenSearch IngestionのLambdaプロセッサは同期実行のみで、OpenSearch IngestionからLambdaへのread timeoutも10秒固定で調整できない 短時間に大量のベクトル化を行うとBedrockのリクエスト数クォータ超過で ThrottlingException が発生し、Lambda内でリトライしてもread timeoutする LambdaプロセッサがエラーになってもOpenSearch Ingestion側からリトライを設定する手段がない Lambdaプロセッサで処理が失敗したメッセージは OpenSearch IngestionのDead Letter Queue(以下、DLQ) には送られない。さらにS3の元ファイルも削除される仕様のため、失敗データが完全に消えてしまう。原因追跡やリカバリーができず、運用に耐えない パイプライン側からLambdaへの流量制限ができず、Bedrockを呼ぶLambdaへ過剰なリクエストが流れてしまう スロットリングを抑える目的でOCUと source.s3.sqs.maximum_messages を1にしても、Lambdaへの接続時にコネクションプール枯渇エラーが発生。S3に投入した1万件のうちOpenSearchに格納されたのは約6,900件で、残り3,100件ほどが毎回OpenSearch IngestionのDLQに溜まってしまう ここまでの検証から、OpenSearch IngestionのLambdaプロセッサは今回のユースケースには適さないと判断しました。 Lambdaプロセッサを諦めてベクトル化処理をOpenSearch Ingestionの外で行うことにします。後段のOpenSearch IngestionはS3に置かれたベクトル化済みデータを読んでSQS経由でリアルタイムに投入する構成とすれば、ここまでに挙げた問題は回避できそうだと考えました。 再設計:ベクトル化Lambdaを前段に出す OpenSearch IngestionからLambdaプロセッサを外し、ベクトル化を前段のLambdaに切り出した再設計を検証しました。しかし、この構成でも問題が発生しました。 ベクトル化部分をLambdaに切り出したことで、リトライ・流量制御をLambda側で完結できるようになり、初期設計で発生していた問題は解消されました。1万件規模の差分更新ではこの構成で安定して動作することを確認できました。 しかし、データ量を増やして検証を進めると、後段の SQS → OpenSearch Ingestion → OpenSearch の経路で別の問題が表面化しました。 3万件〜10万件規模になると、投入自体は完了しているにもかかわらず、OpenSearch IngestionがSQSメッセージを掴んだまま処理を継続し続け、可視性タイムアウトが切れて再処理が走る OpenSearch側でcircuit breakerが頻発し、 upsert 自体は成功しているにもかかわらずSQSメッセージが消費されず、同様に可視性タイムアウト切れで再処理が走る SQS → OpenSearch Ingestion の経路で流量制限ができないため、上記の挙動を緩和する手段がない 普段の差分更新で扱うデータ量が常に1万件以下に収まるなら見送りもできましたが、障害やメンテナンス明けに溜まったデータを一度に流すケースを考えると、大量データで挙動が崩れる構成は採用できません。 今回のユースケースだとOpenSearch Ingestionでは運用に耐えないと判断し、OpenSearch投入部分もLambdaに置き換える方針としました。 最終設計:S3+SQS+Lambdaで非同期にベクトル化とインデクシング 再設計で残っていたOpenSearch Ingestion部分も自作のLambdaに置き換え、S3とSQSを挟んで非同期に処理を進める構成にしました。最終的に、この構成で安定して処理できるようになりました。 ベクトル化を担うLambdaと、OpenSearchへ投入するLambdaを分け、それぞれS3のオブジェクト作成イベントをSQS経由で受け取って動作させます。この構成にすることで、次のような恩恵が得られました。 ベクトル化でBedrockのクォータを超過した場合、SQSが自動的にリトライする Lambdaの同時実行数を設定することで、BedrockとOpenSearchへの流量を独立に制御できる ベクトル化処理が重くなっても、後段のOpenSearch投入には影響が及ばない この構成に切り替えた後、10万件規模の差分更新でも想定時間内に推論からupsertまで完了することを確認でき、再設計で発生していたSQS再処理やcircuit breakerの問題も解消されました。実運用の定常時は1万件以下、最大でも数万件規模ですが、障害やメンテナンス明けに大量データを一度に流すケースでも問題なく捌けるようになっています。 ベクトル化Lambdaでの工夫 ベクトル化LambdaはS3に格納されたファイルをSQS経由で受け取り、1ファイルずつ処理します。次のような点を工夫しました。 Bedrockのリージョン分散 ベクトル化に利用している amazon.titan-embed-text-v2:0 モデルは、リージョンごとに1分あたり6,000回のクォータがあり、これは調整不可でした 4 。WEARでは OpenSearchのコネクタ 経由でも検索時の検索ワードのベクトル化を行っており、こちらは ap-northeast-1 リージョンを利用しています。インデクサーのベクトル化が ap-northeast-1 のクォータを使い切ってしまうと、検索時のベクトル化に影響が出てしまいます。 そのため、ベクトル化Lambdaでは検索とは別のリージョンを使うようにしました。具体的には us-east-1 と us-west-2 の2つを使い、リクエストごとに順序をシャッフルして順に試行する形にしています。これにより、検索側のクォータに影響を与えず、かつLambda側で利用できるクォータも実質的に2倍に増やせています。 クォータ超過( ThrottlingException )の場合は別リージョンへフォールバックし、すべて埋まっていたら指数バックオフでリトライするようにしました。 1ファイル単位の処理量を制御する ベクトル化Lambdaは1ファイル1実行で動作するため、1ファイル内のデータ量が多すぎるとBedrock呼び出しに時間がかかり、Lambdaのタイムアウトに引っかかる問題が発生しました。Lambdaのタイムアウトはリトライまでの時間とBedrockクォータエラー時の挙動を考慮して60秒に設定しています。 最初はBigQueryのEXPORT DATA時点でファイルサイズを制限する方法を検討しました。公式ドキュメントの エクスポートファイルのサイズを制限する に従い、対象データをパーティション分割した上でEXPORT DATAをループ処理する方式です。しかし、この方式はBigQuery側の並列ワーカーで一括出力する場合と比べて出力時間が大幅に伸びてしまい、差分更新の実行間隔である10分には到底収まらないため断念しました。 最終的には、GCSからS3へ転送する段階で1ファイルあたり500KBを上限として分割し、1ファイル内のデータ量を一定以下に抑えました。シェルスクリプトとして以下のような処理を組み込んでいます。 # GCS から S3 へのストリーミング転送例 # 第1引数: SRC(例: gcs:bucket/prefix/), 第2引数: DST(例: s3:bucket/prefix/) SRC = " $1 " ; DST = " $2 " CHUNK_BYTES = " 500K " JOBS = 4 # 1) しきい値以下のファイルはそのまま move rclone move " $SRC " " $DST " \ --max-size " $CHUNK_BYTES " --min-size 1B --include " *.jsonl " # 2) しきい値を超えるファイルは rclone cat → split で分割しつつ、 # 分割ファイルを rclone rcat でそのまま S3 へ PUT(中間ファイルを作らない) rclone lsf -R --files-only --min-size " $CHUNK_BYTES " " $SRC " \ | tr ' \n ' ' \0 ' \ | xargs -0 -P " $JOBS " -I{} bash -euo pipefail -c ' REL="$1" STEM="${REL##*/}"; STEM="${STEM%.*}" rclone cat "${SRC}${REL}" \ | split -C "${CHUNK_BYTES}" - chunk- \ --numeric-suffixes=1 --suffix-length=5 \ --additional-suffix=.jsonl \ --filter "rclone rcat \"${DST}${STEM}-\$(basename \$FILE)\"" rclone deletefile "${SRC}${REL}" ' bash " {} " split の --filter オプションを使うことで分割ファイルをディスクに書き出さず、 rclone rcat で直接S3へPUTできます。これによりPodの一時ストレージを使い切らずに済み、 xargs -P によって並列実行することで転送時間も抑えています。 ファイル分割により、1ファイルあたりの処理時間がLambdaのタイムアウト以内に収まり、ベクトル化Lambdaが安定して動作するようになりました。 出力形式と後処理 ベクトル化Lambdaは、JSONから対象テキストを取り出してベクトル化し、元のフィールドの代わりにベクトル用フィールドを追加した上で、JSON Lines形式でS3の別パスに出力します。OpenSearch投入用Lambda側のSQSがそのパスのS3作成イベントを受け取るようにし、後続の処理を行います。出力完了後は元ファイルを削除します。 OpenSearch投入Lambdaでの工夫 OpenSearch投入Lambdaは、ベクトル化済みデータが格納されたS3パスをSQS経由で受け取り、適切なバッチサイズにまとめてOpenSearchへ投入します。 external versionで古いデータで新しいデータを上書きすることを防止 差分更新の全体はDigdag workflowで管理していますが、WEAR全体のメンテナンスやインデクサーのエラーなどでジョブを再実行する可能性があります。その際、後の時間帯のデータを取得したジョブの後に、前の時間帯のデータを取得したジョブを実行する可能性があり、古いデータで新しいデータを上書きしてしまう懸念がありました。 これを防ぐため、OpenSearchの Bulk API で version / version_type を渡し、external versionによって更新可否を判定できるようにしています。データ取得範囲の終了時刻をunixtimeに変換した値をversionとして持たせ、現在のversion以上の値の場合のみ更新が成立するようにしました。これにより、ジョブの実行順序が前後しても古いデータで上書きされることはなくなります。 unixtimeはOpenSearch投入Lambdaに渡されるS3キーのパスから算出しています。ベクトル化Lambdaが出力するS3キーはデータ取得範囲の終了時間を含んだ階層構造であり、それをパースしてversionとしています。 from opensearchpy import helpers # OpenSearchへのbulk投入例(簡略化) # 各アクションのメタデータに version / version_type を含めることで、 # external_versionより小さいversionの更新を拒否させる # 更新処理にバグ等があった場合に再度同じ時間帯のデータで更新できるように、 # version_typeはexternal_gte(現在のversion以上のときのみ更新)を指定している actions = [ { "_op_type" : "index" , "_index" : index_name, "_id" : record[ "id" ], "_source" : record, "_version" : external_version, "_version_type" : "external_gte" , } for record in records ] helpers.bulk(client, actions) なお、external versionはupsertに対応していないため 5 、差分更新ではdocument全体をreplaceする形で更新しています。 日次更新のOpenSearch Ingestionではexternal versionを利用できませんが、upsertすることでversionが+1される形で更新されます。 差分更新と日次更新でデータ投入のタイミングが前後する可能性はありますが、日次で更新する値は仮に古い値で更新されても致命的な問題にはならない内容のため許容しています。 処理完了後のファイル削除 OpenSearchへの投入が完了したファイルはS3から削除します。これは後述する非同期処理の完了待機の仕組みのためでもあります。 Lambdaエラー時のファイル退避 差分更新のLambdaでエラーが発生し、SQSの最大リトライ回数を超えた場合は error/ パスへファイルを移動させています。これにより後述する完了待機が止まらないようにしつつ、後でエラーになったファイルを追跡できるようにしています。 ただし、Lambdaがタイムアウトで終了した場合は error/ パスへの移動前に終了してしまいます。その結果、SQSのDLQにメッセージが残ったまま、元ファイルがS3に残ったままとなり、Digdag workflowの完了待機が永遠に終わらなくなります。 これを補完するため、定期的にSQSのDLQを監視し、 error/ パスへ移動できていないファイルを検知して移動する補助Lambdaを別途用意しています。 非同期処理の完了待機 インデクサーには非同期処理が含まれており、Digdag workflow側からは処理がいつ終わったかを直接知る術がありません。そこで、Digdag workflow側では処理対象のS3パスを監視し、ファイルが残っていない状態になったら処理完了とみなす方式を取りました。完了待機を入れる理由は次の通りです。 Bedrockのクォータで詰まっている状況で次のジョブが走ると、SQSにファイルがどんどん溜まってしまう すべての投入が完了したタイミングでindex refreshを呼び、検索に反映させる必要がある refresh頻度が高いと日次更新時のCPU使用率に影響するため、自動refreshは無効化している aws s3 ls コマンドでS3の対象パスをリストし、ファイルが存在しなければ処理完了とみなすロジックをシェルスクリプトで実装しています。 既存データに対する初回ベクトル化 新しいインデクサーをリリースする前に、既存の大量のdocumentにベクトルデータを付与する必要がありました。差分更新と日次更新のジョブを稼働させ始めるだけでは、その時点で過去に投稿された大量のdocumentにはベクトルデータが付かないままです。 そこで、初回ベクトル化用に AWS Step Functions を作成し、次の手順で全データのベクトル化を実施しました。 Step Functionsで全データのバッチベクトル化を実行 OpenSearch Ingestionで全データをOpenSearchへ投入 差分更新・日次更新のジョブを稼働開始 1〜3の処理中に取りこぼした時間帯のデータを、差分更新のジョブで埋める external versionの仕組みがあるため、4の時点で過去の時間帯のデータを後から流しても、すでに最新のデータが入っているdocumentが古いデータで上書きされてしまうことはありません。差分更新の設計時点で順序を気にしなくて良いようにしたことが、初回ベクトル化のフローでも生きました。 結果 Embulkを用いた構成から、OpenSearch Ingestionと自作Lambdaを組み合わせた構成へとインデクサーを刷新したことで、以下のような効果が得られました。 日次更新のジョブが4〜6時間から1時間以内に短縮された 10万件規模の大量データを流すケースでも、想定時間内に推論からupsertまで完了できるようになった Embulkが メンテナンスモード になるタイミングと重なり、結果的にリプレイスも同時に行えた まとめ 本記事では、WEARのあいまい検索リリースに伴って検索インデクシングシステムを刷新した話を紹介しました。差分更新ではOpenSearch IngestionのLambdaプロセッサで要件を満たせなかったため、S3とSQSを挟んで自作Lambdaで非同期に処理を進める構成に切り替えました。日次更新ではOpenSearch IngestionのS3スキャンを採用し、毎回起動・終了させるという形で、ユースケースに応じて使い分けました。 ベクトル検索を支えるバッチ処理を設計する上では、以下のような工夫が有効でした。 ベクトル化用のBedrockをアプリケーションとは別リージョンに逃がし、クォータを分離する ファイル単位の処理量をあらかじめ転送段階で制御し、Lambdaの処理時間を短縮させる external versionでジョブ実行順序の前後に対する耐性を持たせる ベクトル化と投入をLambdaとして分割し、流量制御や障害の影響範囲を限定する OpenSearch Ingestionの採用やベクトル検索のためのインデクサーの設計を検討している方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 元々はElasticsearchを使用していましたが、OpenSearchへ移行しました。詳細は WEARの検索基盤をElasticsearch 7.10.2からOpenSearch 2.19.0へ無停止で移行する ── ダブルライトとカナリアリリースによる段階的アプローチ をご参照ください。 ↩ あいまい検索のリリースに関する詳細は別記事で紹介予定です。 ↩ 実際はフォークしてカスタマイズしたプラグインを使用しています。 ↩ クォータの値は本記事の執筆時点のものです。最新の値は Amazon Bedrock サービスクォータ一覧 をご参照ください。 ↩ Bulk - OpenSearch Documentation 。 version_type=external 系を指定した場合、documentの作成または完全置換のみが対象になります。 ↩
はじめに こんにちは。プラットフォームSREブロックの酒部・高塚・亀井です。私たちは2026年5月14日〜15日に名古屋で開催された「 クラウドネイティブ会議 」に参加してきました。本記事では印象に残ったセッションをご紹介します! はじめに クラウドネイティブ会議とは セッションレポート キーノート:老舗IoTクラウドサービス組織の変革 -クラウドネイティブをはじめよう- GameDay:チームで挑むリアルな障害対応 100マイクロサービスのTerraform/Kubernetes管理地獄から抜け出すためのAI活用術 巨大組織の認知負荷をどう下げるか?ソフトバンクが描くCNAP×Backstageによるクラウドネイティブの新時代 生成AI時代に信頼性をどう保ち続けるか - Policy as Codeの実践 おわりに クラウドネイティブ会議とは クラウドネイティブ会議は、CloudNative Days、Platform Engineering Kaigi、SRE Kaigiという3つの大規模テックイベントで合同開催されたカンファレンスです。現地参加とオンライン参加のハイブリッド形式で開催され、会場の名古屋・中日ホール&カンファレンスには約1,000人が集まりました。 当日の様子は公式のXのポストまとめで見ることができます。現地の雰囲気を感じることができるので、ぜひご覧ください。 posfie.com セッションレポート ここからは各メンバーからのセッション紹介をお届けします。 キーノート:老舗IoTクラウドサービス組織の変革 -クラウドネイティブをはじめよう- kaigi.cloudnativedays.jp 高塚です。1日目のキーノートでは、パナソニックさんの事例として、巨大なモノリスだった老舗IoTサービスをマイクロサービス化した熱い話を聞くことができました。 見切り発車でマイクロサービス化を始めたものの、最初は問題が山積みだったとのことです。 老舗IoTクラウドサービス組織の変革 -クラウドネイティブをはじめよう- アーカイブ動画 6:19 より引用 かなり昔からあるAWSアカウントでしか見られない「ap-northeast-1b」アベイラビリティゾーンがあった話では会場もざわざわしていました。 老舗IoTクラウドサービス組織の変革 -クラウドネイティブをはじめよう- アーカイブ動画 7:00 より引用 GitLabの導入は、社内から「Gitを使うなんて危険だ、けしからん」という声も上がるなど「正直ここが一番しんどかった部分」だったそうです。しかし、そういった意見を軽々しく否定せず、既存のシステムがお客様に価値を提供してきたことに敬意を持ちながらクラウドネイティブ化を進めたそうです。 老舗IoTクラウドサービス組織の変革 -クラウドネイティブをはじめよう- アーカイブ動画 8:22 より引用 その結果、無事に本番稼働させることができました。 老舗IoTクラウドサービス組織の変革 -クラウドネイティブをはじめよう- アーカイブ動画 15:26 より引用 後半ではIoT特有のE2Eトレーシングについて紹介がありました。 組み込みデバイスはCPU・メモリが限られており、スパン送信による性能劣化を避けるため、OpenTelemetry SDKは利用せずにトレーシングを自前実装したとのことです。 老舗IoTクラウドサービス組織の変革 -クラウドネイティブをはじめよう- アーカイブ動画 23:45 より引用 また、IoTデバイスはネットワークやOSなどの問題が障害の原因になりやすいため、eBPFでカーネルイベントもスパンにしたそうです。 老舗IoTクラウドサービス組織の変革 -クラウドネイティブをはじめよう- アーカイブ動画 24:45 より引用 約30分の発表はとても濃い内容で、大変勉強になりました。また私もZOZOTOWNのマイクロサービス化を長年担当しているため、発表で赤裸々に語られる苦労話には「わかりみが深い〜」と100回くらい頷いていました。 本記事では主に技術的な話をご紹介しましたが、組織の文化を醸成する話や「これからクラウドネイティブをはじめる方へのメッセージ」などとても学びになる内容が盛りだくさんですので、ぜひアーカイブ動画をご覧ください。 GameDay:チームで挑むリアルな障害対応 酒部です。GameDayはKubernetes環境上で発生するさまざまな障害シナリオに対して、チームで協力して原因を特定し、復旧させるという内容でした。参加者にはKubernetes上で稼働するアプリケーション環境が提供され、その環境にはあらかじめ障害が仕込まれており、クエスト形式で出題される問題を解きながら、システムを正常な状態に復旧させていくという形式でした。 形式は約2時間のチーム対抗戦で、Kubernetes初心者から経験者まで幅広い層が集まっていました。ルールはシンプルで、障害を復旧することでスコアが加算され、最終的にチーム単位で順位が決まる仕組みです。 困ったときに相談できるメンターがサポートしてくれる体制に加え、行き詰まっても段階的なヒントで前に進める仕組みも用意されており、勝負というよりもチームで協力して問題に取り組む過程が重視されているように感じました。 題材として用意されていたのは、OpenTelemetry DemoをベースとしたECサイトのマイクロサービス構成です。各チームにCode Server、Grafana、Jaeger、Argo CD、GitHubリポジトリが一式与えられました。基本フローは障害を発見 → GitHub上で修正 → commit & push → Argo CDが自動同期 → Verifierによって各問題の合否判定する流れです。 問題の内容や解説は下記の公式記事に解説があるため、そちらをご覧ください。 kaigi.cloudnativedays.jp 結果としては、最後の問題だけ時間内に解くことができませんでした。ただ、作問者が解かせるつもりで作っていないと言うほどの難問で、ほとんどのチームが解けていなかったので、終了後残り時間で会場でも簡単な解説をしていただきました。解説後、会場のあちこちから「そういうことだったのか」「やられた」といった声が漏れ、参加者全員が思わず唸ってしまうような巧妙な問題設計だったことが印象に残っています。 観察・操作系のチュートリアル問題も用意されていて、Kubernetesに触り始めたばかりの方でも問題なく楽しめる内容で、自信を持っておすすめできるプログラムでした。 100マイクロサービスのTerraform/Kubernetes管理地獄から抜け出すためのAI活用術 kaigi.cloudnativedays.jp speakerdeck.com 酒部です。このセッションでは膨大なTerraformやKubernetesマニフェストファイルを扱う構成において、日々の運用をAIエージェントでどう解決するかがテーマでした。 セッションは大きく4パートで構成されており、以下のトピックが扱われました。 Linear × Codexで全マイクロサービスのTerraform / K8s manifest更新を効率化 AIによるレビューで200 PR/dayの50%を自動化 問い合わせ・トラブルシューティングをAIに任せる試み Production Readiness Check(PRC)のEvidence確認もAIにやってもらう 特に、2と4は弊チームと似た取り組みも紹介されており大変興味深い内容でした。 まず、PRレビューへのAI活用について、Notionに整理されたガイドラインがあるのに、サービス・人数の増加で存在を知らない人が増え、結果としてガイドラインが守られないという課題がありました。 解決アプローチはガイドラインをAIエージェントが利用できる形でリポジトリに降ろすというものです。Notionは引き続きSoT(Source of Truth)として残しつつ、AIレビュー用のコンテキストはリポジトリに固定する設計としました。 紹介されていた実例では、Codex ReviewがIAM設定の重複を「社内ガイドライン違反」として指摘し、参照したガイドラインのファイルパスまで引用していました。一般論ではなく、社内ルールに照らした具体的な指摘ができている点が印象的でした。AIに指摘されない部分こそが暗黙知であるという気づきから、「AIが指摘してこない部分」を観察することで、ドキュメント化されていない暗黙知が浮かび上がってくる、というメタな視点が学びになりました。 本番リリース前のProduction Readiness Check(PRC)の自動化ですが、開発者はEvidenceを集め、SREはその妥当性をチェックするという、両者にとって骨の折れるプロセスがあるそうです。ここで、いきなりAIに丸投げするのではなく、AIに任せるべき項目を選定したり、Code化できる項目はCIで自動チェックしたりするなど工夫されていました。 また、AIに任せるためフォーマットを整理する過程で、以前はやや解釈が難しいPRC項目もあったが、自動化に向けて解釈がブレないようにしたという副次効果も語られていました。AI活用のためのドキュメント整備が、人間にとっても理解しやすいドキュメント整備につながるという気づきがありました。 セッション全体として、AIに使わせるために何を整えるかが重視されており、共感する内容も多かったです。弊チームでもGitHub Actions上でAIレビューを動かしていますが、Kyverno Policyで機械的に守れるルールはPolicyに、非決定論的な項目はAIレビューに任せるという棲み分けをとっています。また、AIレビューに渡す情報も何を入れるかと同じくらい何を入れないかが重要だと再認識しました。チームでも、AIが解釈しやすい形にドキュメントやタスクを整えるということを意識していきたいと思いました。 巨大組織の認知負荷をどう下げるか?ソフトバンクが描くCNAP×Backstageによるクラウドネイティブの新時代 kaigi.cloudnativedays.jp speakerdeck.com 亀井です。クラウドネイティブ化が進むほど、クラウド、Kubernetes、CI/CD、ドキュメント、申請フローなどの選択肢や情報が増え、開発者の認知負荷は高まっていきます。 ソフトバンクさんでは、共通基盤「CNAP」によって、GitOps、自動化、ローコードパッケージ、マルチクラウド対応を提供し、インフラの作り方を標準化していました。 一方で、GitOpsだけでは誰もが迷わず使える状態にはならず、GUIで直感的に操作できる入口としてBackstageを導入した点が印象的でした。Backstageでは、サービスやドキュメントの「見える化」と、アカウント、権限、リソース申請などの「自動化」を実現していました。特にAzureアカウント申請の例では、申請、承認、権限付与、通知までをBackstage上で標準化し、手動の対応工数を大きく削減していたのが具体的で分かりやすかったです。 ただし、アカウント設計、SSO連携、既存ドキュメントサイトとの連携、PluginとCustom開発の見極めなど、運用して初めて見える課題も共有されていました。「Backstageは銀の弾丸ではない」という点も重要で、作って終わりではなく、情報と業務導線をつなぎ、改善を回し続ける設計が必要だと感じました。 後半では、Agentic DevOps時代に向けて、BackstageがAIエージェントに組織のコンテキストやガードレールを渡す基盤になり得るという話がありました。TechDocs、Software Template、Software Catalog、catalog-info.yamlといった機能は、人間の認知負荷を下げるだけでなく、AIに正しい文脈を渡すうえでも有効そうです。 CNAPが「安全で標準化された実行基盤」、Backstageが「迷わず使える入口」として役割分担し、両者を連携させることで、使われ続けるPlatform Engineeringに近づけるという学びがありました。 生成AI時代に信頼性をどう保ち続けるか - Policy as Codeの実践 kaigi.cloudnativedays.jp speakerdeck.com 亀井です。このセッションでは、サービス成長やマルチプロダクト化、生成AI活用による開発量の増加に対して、信頼性をどうスケールさせるかがテーマでした。キャディさんではProduction Readiness Checklist(PRC)を定義し、信頼性、可用性、セキュリティ、完全性、運用性、オブザーバビリティをGAの必須条件としていました。 しかし、SREがPRCを目視レビューする運用は、プロダクト数やチェック項目が増えるにつれて限界を迎え、人によるレビューがボトルネックになっていました。その解決策として、Manual ChecklistをPolicy as Codeに置き換え、手動で属人的な確認から、自動で安定したガードレールへ移行していました。具体的には、Terraform planの結果をConftestとRegoで検証し、KubernetesマニフェストはKyverno CLIでチェックするなど、CI上でシフトレフトしていました。 さらに、Google Cloud Organization Policyを使い、CIを通らない手動操作もAPIレベルで遮断する多層防御の考え方が紹介されていました。印象的だったのは、Policy自体にもテストCIを組み込み、Positive Test、Negative Test、Context Mockingで、誤検知や過剰検知を防ぐ品質管理をしていた点です。 ポリシー記述は生成AIに任せつつ、品質はテストで支えるという考え方は、生成AI時代らしい現実的なアプローチだと感じました。 また、既存負債に対しては警告通知、全件トリアージ、段階的強制、エラー通知という流れで進め、単にブロックするのではなく、SREが自ら主導してEnableする姿勢が印象的でした。検知結果に修正例や除外方法へのリンクを付ける、除外ポリシーをCentral SREが管理する、変更ファイル単位で最小限にチェックするなど、形骸化させないための泥臭い工夫に学びがありました。 おわりに 普段ZOZOTOWNのプラットフォームを管理するチームとして共感する内容が多く、規模やフェーズが違っても各社が同じような問題意識を持って取り組んでいることが強く印象に残りました。 また、GameDayや公式の懇親会などで、セッション以外の企画でも他社のSREやプラットフォームエンジニアの方々と直接交流できたのは、現地参加ならではの大きな価値でした。 今回のカンファレンスで得た知見と刺激を活かして、自分たちの基盤をより使いやすく、より安全に進化させていきたいと思います。そして、自分たちの取り組みや学びを引き続きたくさんアウトプットしていきたいと思っております。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
はじめに こんにちは、ZOZOTOWN開発本部Webバックエンドブロックのひでです。普段は ZOZOTOWN のバックエンド領域を担当しています。 Webバックエンドブロックでは2026年1月より、カスタマーサポートチーム(以下CSチーム)から技術調査として開発側にエスカレーションされる問い合わせの効率化に取り組んでいます。エスカレーション後の調査では、データ・ログ・過去の会話・コードベースなど複数のツールを横断して情報を集める必要があります。そのため、1件あたりの対応に多くの時間がかかるという課題がありました。本記事ではこの課題に対し、AIを活用して開発側での一次回答までを自動化し、調査のリードタイムを平均70%(※アンケートベース)削減できた取り組みをご紹介します。 (※本記事における「CS問い合わせ」は、CSチームで解決に至らず、技術調査のためにエンジニアへエスカレーションされたものを指します。CS側でクローズする問い合わせは本記事の対象外です) 目次 はじめに 目次 背景・課題 CS問い合わせ対応の背景 抱えていた課題 1. 確認すべき情報源が多い 2. 情報源ごとにツールが異なり、メンバー間の習熟度に差がある 3. リプレイス過渡期で全体像を把握しづらい 解決の取り組み システム全体像 マルチリポジトリ対応のWorkflow設定 調査スキルの設計 Step 1: 問い合わせ内容の取得 Step 2: Webバックエンド担当判定(webbe-judge サブスキル) Step 3: 影響画面・リポジトリの特定(scope-finder サブスキル) Step 4: Slack過去類似問い合わせの調査 Step 5: Splunkログ調査 Step 6: コードベース調査 Step 7: 統合レポートの作成とスキル改善メモ 効果 調査リードタイムの大幅短縮 サポート担当への負荷集中の解消 調査品質の均質化 見えてきた課題 蓄積知識ファイルの更新が手動依存 実DBデータを参照する調査ができない 一次回答の精度ばらつきとハルシネーション 得られた知見 LLMは「オペレーションエンジン」として使える MCPでナレッジを集約せずに横断利用できる Skillが「動くマニュアル」になる マルチリポジトリ横断でリプレイス過渡期でも機能する 今後の展望 蓄積知識ファイルへの反映フローの仕組み化 BigQuery MCPなどによる実DBデータへの調査範囲の拡張 ハルシネーションを抑える評価ループの整備 他チームへの展開と各チーム固有Skillの整備 まとめ 背景・課題 CS問い合わせ対応の背景 CS問い合わせは次のようなフローで対応しています。 ユーザーが問い合わせを送信 CSチームが受け付けて一次対応 CS側で解決できず技術調査が必要と判断されたもののみ、過去の類似問い合わせをもとに関連するシステム担当チームへエスカレーション Webバックエンド内のCS問い合わせ回答メンバーが調査・回答 本記事で取り上げる自動化のスコープは、ステップ4のWebバックエンド内での調査です。このステップ4の調査において、徐々に次のような課題が見えてきました。 抱えていた課題 1. 確認すべき情報源が多い 会員データや注文データといった業務データ、アプリケーションログ、過去の問い合わせ会話、関連コードなど、複数の情報源を横断して確認する必要があります。問い合わせの内容によって1件あたり1〜2時間、内容によっては2時間以上かかることも珍しくありませんでした。 2. 情報源ごとにツールが異なり、メンバー間の習熟度に差がある それぞれの情報源は別々のツールで扱う必要があり、ツールの使い方や検索の勘所にもメンバーごとに差が出ます。新しくジョインしたメンバーや初めて触れる領域を担当する場合は独力での調査が難しく、専任のサポート担当が伴走する体制を取っていました。結果としてサポート担当に負荷が集中しがちで、調査・回答までのリードタイムが長くなる要因にもなっていました。 3. リプレイス過渡期で全体像を把握しづらい Webバックエンドの調査範囲は、下図の紫色で示したとおりリプレイス前環境とリプレイス後環境の両方にまたがります。同じ事象でもまず新旧どちらの環境で発生しているのかを切り分け、そのうえで該当する側の構成・ログ・コードを掘り下げる、という二段構えの調査が必要でした。 さらに画面によっては、新旧環境の構成が混在しているケースもあり、片方だけを見ても原因を特定できないことがあります。この場合は新旧両方の構成・ログを並行して確認する必要があり、調査の複雑さがさらに増します。 加えてリプレイスは日々進行しており、リプレイス全体像の把握自体が難しく切り分けに余計なコストがかかっていました。 解決の取り組み システム全体像 これらの課題に対し、CS問い合わせの一次回答までをAIで自動化する仕組みを構築しました。SlackとGitHubを起点とした以下の構成です。 各コンポーネントの役割と採用理由は次のとおりです。 コンポーネント 役割 採用理由 Devin Slackに投稿されたCS問い合わせを拾い、内容をGitHub Issueとして起票する Slack上での会話体験が良く、CSチームの普段のやりとりをそのまま自動化の入り口にできる Claude Code Actions GitHub Issueをトリガに起動し、調査用Agent Skillsを実行する調査エンジン Agent Skills機能で複数ステップの調査ロジックを定式化できる(検討時点ではDevinにAgent Skills機能がなく、現在は追加されている) Splunk MCP / Slack MCP ログ・過去会話を横断的に検索するインタフェース 既存運用しているSaaSにそのままアクセスできる 役割としては、 Slack ⇄ GitHubのインタフェース層 をDevinに、 調査ロジックの実行エンジン をClaude Code Actionsに分担しています。 マルチリポジトリ対応のWorkflow設定 Claudeが複数リポジトリを横断して調査できるよう、Workflow YAMLを工夫しています。関連リポジトリを順に actions/checkout でチェックアウトしてからClaudeを起動する構成です。 # claude code actionsの設定のymlファイル jobs : inquiry-investigation : runs-on : ubuntu-latest steps : - name : Checkout skill repo uses : actions/checkout@v4 - name : Checkout オンプレミスのリポジトリ uses : actions/checkout@v4 with : repository : org/onpre-repo path : repos/onpre-repo - name : Checkout FEのリポジトリ uses : actions/checkout@v4 with : repository : org/fe-repo path : repos/fe-repo - name : Checkout Akamaiのリポジトリ uses : actions/checkout@v4 with : repository : org/akamai-repo path : repos/akamai-repo - name : Checkout BFFのリポジトリ uses : actions/checkout@v4 with : repository : org/bff-repo path : repos/bff-repo - name : Run Claude Code Action uses : anthropics/claude-code-action@v1 with : mcp-config : .mcp/config.json これによりClaudeが「複数リポジトリにまたがるコードベース」を1つの調査文脈として扱えるようになりました。リプレイス前のオンプレミスからAkamaiのルーティング設定・FE・BFFまでを一気通貫で参照できる構成です。 調査スキルの設計 調査手順は Claude CodeのSkill機能 として実装しました。 これまで属人化しがちだった「CS問い合わせ調査の進め方」をSkillとしてリポジトリ管理することで、 人間が読めるドキュメント でありながら そのまま動くマニュアル として再利用できる形になっています。 調査スキルは、入り口となる親スキル inquiry と、判断ロジックを切り出したサブスキル群から構成されます。 スキル名 役割 inquiry 親スキル。問い合わせ内容の取得から最終レポート出力までを統括 inquiry/webbe-judge Webバックエンド担当か他チーム担当かを判定するサブスキル inquiry/scope-finder 影響画面・調査対象リポジトリを特定するサブスキル code-investigator (Agent) 指定されたリポジトリのコードベースを掘り下げるエージェント Skillをサブスキルに分割しているのは、調査ステップごとに責務を切り分けることで、メンテナンス時に該当箇所だけを更新できるようにするためです。 ここからは、親スキル inquiry が実行する Step 1〜7 の中身を詳しく見ていきます。全体像は次のとおりです。 Step 内容 関連サブスキル/エージェント Step 1 問い合わせ内容の取得・抽出 - Step 2 Webバックエンド担当かどうかの判定 webbe-judge サブスキル Step 3 影響画面・調査対象リポジトリの特定 scope-finder サブスキル Step 4 Slackで過去類似問い合わせを検索 Slack MCP Step 5 Splunkでログを段階的に検索 Splunk MCP Step 6 コードベースを掘り下げる code-investigator エージェント Step 7 統合レポートの作成・スキル改善メモの出力 - Step 1: 問い合わせ内容の取得 渡されたSlackリンクからチャンネルIDとタイムスタンプを抽出し、Slack MCP経由でメッセージ本文・スレッドの内容を取得します。そこから以下の情報を抽出します。 問い合わせの概要(ユーザーが何に困っているか) 会員ID(記載されている場合) 発生日時(記載されている場合) エラーメッセージ・デバイス・画面情報(記載されている場合) Step 2: Webバックエンド担当判定( webbe-judge サブスキル) 本格的な調査を始める前段として、Webバックエンド担当か他チーム担当かを切り分けるステップです。誤って自チームで長時間調査してしまうとリードタイムが大きく伸びるため、最初にここで判定します。 判定は以下のサブステップで進めます。 Step 2-1:蓄積事例を参照 — judgment-knowledge.md から過去の判定知見・誤判定の教訓を参照 Step 2-2:Slackで過去事例を検索 — 問い合わせキーワードでSlackを横断検索し、Webバックエンドでの過去の対応事例を確認 Step 2-3:担当判定 — 上記の参照結果から「Webバックエンド担当 / 他チーム担当 / 要確認」を判定 Step 2-4:判定に応じた処理 — Webバックエンド担当ならStep 3へ、他チーム担当なら team-routing.md を参照してルーティング先を提示し終了 なお、図中の judgment-knowledge.md / team-routing.md は、使うほど内容が増えていく 蓄積知識ファイル です。更新の流れはStep 7で説明します。 Step 3: 影響画面・リポジトリの特定(scope-finder サブスキル) どの画面で問題が起きているか と どのリポジトリを調査すべきか の2つを独立して特定するステップです。 Step 3-1:影響画面(URL)の特定 — screen-url-map.md を参照、Splunkのユーザーログから画面を推定 Step 3-2:調査リポジトリの特定 — URLをAkamaiルーティング設定と照合し、新環境/旧環境を判定して対象リポジトリを決定(両方にまたがる場合は複数指定) ここで特定したリポジトリ情報が、後続のStep 6の調査対象です。 screen-url-map.md も、未登録の画面を見つけるたびに更新されていく蓄積ファイルです。 Step 4: Slack過去類似問い合わせの調査 Step 1〜3で抽出した情報をもとに、Slackから過去の類似問い合わせを検索します。 検索は最低3パターンのキーワード(機能名 + 症状 / エラーメッセージ / 機能名のみ)で実行します。ヒットしたスレッドのうち類似度が高いものは、スレッド内の全メッセージを取得します。 Step 5: Splunkログ調査 会員ID・発生日時を起点に、Splunkで段階的にログを引きます。最初から広い範囲を一気に引くと結果が膨大になりすぎるため、時間範囲を段階的に広げる構成にしています。 Step 5-1:UID/IPの特定 — 会員IDから内部UID・IPを特定(±3h→±6h→±9h→±12hと範囲を拡大) Step 5-2:アプリケーションログの3段階検索 — Stage 1(±30分)/ Stage 2(±6時間)/ Stage 3(±1日)で直接エラー→前兆→全体傾向を確認 Step 5-3:Akamaiログ調査 — Step 5-2で特定できなかった場合のみ、5xx・WAFブロックを確認 Step 5-4:新環境のログ調査 — 新環境のAPI層が対象に含まれる場合のみ実施 Step 6: コードベース調査 Step 3で特定したリポジトリのコードを code-investigator エージェントが掘り下げます。 Workflow YAMLで関連リポジトリは事前にチェックアウト済みのため、エージェントは複数リポジトリを横断して該当機能のコードを読み解けます。問い合わせ内容と症状から「どの実装が関係していそうか」当たりをつけ、関連コードを抽出します。 Step 7: 統合レポートの作成とスキル改善メモ Step 1〜6の調査結果を統合し、Markdown形式の統合レポートとしてGitHub Issueにコメント投稿します。レポートに含まれる内容は以下のとおりです。 問い合わせ概要 / 担当判定 / 影響画面・リポジトリ Slack過去調査の結果 / Splunkログ調査の結果 / コードベース調査の結果 総合分析と仮回答案(CSチームへ伝えるための文面) 推奨アクション・他チームへのハンドリング先(該当する場合) スキル改善メモ ← Skill自体を継続的に育てるための仕組み 最後の「スキル改善メモ」が、本仕組みを継続的に育てていくための仕掛けです。調査の中で次のようなトリガー条件に該当した場合、Skillは「どのファイルに何を追記すべきか」をレポートに出力します。 トリガー条件 更新対象ファイル screen-url-map.md 未登録の画面・URLに遭遇した screen-url-map.md team-routing.md 未登録のチーム・サービスが判明した team-routing.md Webバックエンド担当判定で「要確認」になった judgment-knowledge.md Akamaiルーティング判定で「要確認」になった akamai-routing-rules.md これらのファイルは、Step 2・Step 3の図中、黄色で示していた 蓄積知識ファイル そのものです。 使われれば使われるほど内容が増え、次回以降の判定精度が上がっていく 設計になっています。 現状、蓄積知識ファイルへの反映はメンバーが手作業で行う運用です。スキル改善メモをもとに、本当に反映が必要なものを担当者がコミットします。 効果 仕組みを導入した結果、CS問い合わせ対応に次のような効果が出ています。 調査リードタイムの大幅短縮 これまで人手では1件あたり1〜2時間(複雑なものは2時間以上)かかっていた調査が、Claude Code Actionsの起動から一次回答の出力までおおよそ10分程度で完了するようになりました。担当メンバーは「ゼロから調査を始める」のではなく「AIが出した一次回答をレビューする」立場に変わり、回答までのリードタイムを平均70%(※アンケートベース)削減できました。 サポート担当への負荷集中の解消 初回対応者でも独力で調査できる範囲が広がり、専任サポート担当への質問集中も解消されました。新しくジョインしたメンバーでも、Skillが定義している調査手順をClaudeが代わりに実行するため、まずは一次回答が出てくる状態からレビューを始められます。 調査品質の均質化 メンバーごとの知識差やツール習熟度の差で出ていた調査品質のばらつきが、Skillに沿った定型フローによって均質化されました。確認漏れが起こりやすかった「過去の類似問い合わせ」や「Akamai層のログ」といったステップも、Skill側で必ず実行されるため抜けが起きにくくなっています。 見えてきた課題 一方で、運用していく中でいくつか限界も見えてきました。 蓄積知識ファイルの更新が手動依存 スキル改善メモはSkillが自動で出力するものの、その内容を確認して蓄積知識ファイルへ反映する作業は担当者の手作業に依存しています。改善メモは次々と出てくる一方で反映作業が追いつかず、知識ファイルの育成スピードが運用者のリソースに依存してしまっています。 実DBデータを参照する調査ができない 現状の調査対象はログ・コード・Slackまでで、データそのものを直接クエリする手段は持っていません。そのため、在庫不整合やデータ起因の表示崩れなど「データの中身を見ないと特定できないケース」はSkillの守備範囲外となり、人手調査に戻る必要があります。 一次回答の精度ばらつきとハルシネーション AIが出す一次回答は、典型的なケースでは十分実用的ですが、複雑なケースでは結論を誤ったり、もっともらしい根拠を伴って間違った回答(ハルシネーション)を出したりすることもあります。最終回答前のメンバーレビューを必須にしているため致命的な事故には至っていませんが、自信ありげな誤回答に引きずられるリスクは残っています。 得られた知見 今回の取り組みを通して、生成AIをチームの調査オペレーションに組み込むうえで、いくつか再利用できそうな知見が得られました。 LLMは「オペレーションエンジン」として使える 生成AIをチャット用途で使うイメージは広く浸透しています。今回得た最大の知見は、 LLMは「ログ確認」「履歴検索」「コード調査」といった複数の調査ステップを横断的に実行する「オペレーションの実行エンジン」として使える という点です。 人間が個別ツールを行き来して調査する代わりに、Skillとして定義した手順を上から下まで自律的に実行する役割をLLMが担うことで、これまでの「対話するAI」とは別軸の使い方が見えてきました。 MCPでナレッジを集約せずに横断利用できる 調査に必要な情報源(Splunk・Slack・コード)はそれぞれ専用のツール・SaaSに分散しており、これまで「ナレッジを別の場所に集約しないと使いづらい」のが定番でした。 MCP経由で既存のツールを直接叩けるようにしたことで、ナレッジを別のドキュメント基盤やデータレイクへ集約せずとも、AIから横断的に扱えるようになりました。データの最新性が保たれる点も大きなメリットです。 さらに、各ツールから得た調査結果はそのままClaudeのコンテキストに取り込まれるため、 複数ツールの結果を突き合わせたうえで思考・推論できる 点も重要なメリットでした。例えば「Splunkで検出したエラーパターン」と「Slackで見つかった過去対応の経緯」を関連付けて原因の仮説を立てる、といったツール横断の推論が、人手の調査と比べて格段にやりやすくなりました。 Skillが「動くマニュアル」になる これまでチームの調査手順は、メンバーの頭の中にあるか、社内ドキュメントに散らばったメモとして存在しがちで、整備のモチベーションが上がりにくいものでした。 Skillとして実装することで、 人間が読むためのマニュアル であり、かつ そのままClaudeが実行する手順書 でもある形にできました。一度書けば人間とAIの両方にとって有効な資産になるため、整備に向き合う動機が生まれます。 マルチリポジトリ横断でリプレイス過渡期でも機能する Workflow YAMLで複数リポジトリを順にチェックアウトする構成により、ClaudeはあたかもシングルリポジトリのようにフロントエンドからBFF・APIまでを参照できます。 リプレイス過渡期のような「新旧コードが別リポジトリに分散している」状況でも、AIが両方のコードを同じ文脈で扱えるため、環境判断の難しさという課題に直接効きました。新しいリポジトリが増えたときも、Workflowにチェックアウトステップを追加するだけで対応できる点も運用上の利点です。 今後の展望 「見えてきた課題」で挙げた限界に対し、次のような取り組みを進めていきたいと考えています。 蓄積知識ファイルへの反映フローの仕組み化 スキル改善メモから蓄積知識ファイルへの反映を、人手を介さず自動で行う仕組みを整備していきます。これにより知識ファイルの育成スピードを担当者の手番から切り離し、定常的に育てられる状態を目指します。 BigQuery MCPなどによる実DBデータへの調査範囲の拡張 BigQuery MCPなどを接続し、ログ・コード・Slackに加えて実DBデータも調査対象として含める拡張を検討しています。この拡張により、データ起因の問い合わせも自動調査の範囲でカバーできるようになります。 ハルシネーションを抑える評価ループの整備 ハルシネーションの課題に対しては、Skillの一次回答を継続的に評価し、Skillへフィードバックするループの整備を進めていきます。過去の問い合わせをテストケースとして、Skill変更前後の回答品質をA/B比較する仕組みは既に整備しているため、これをSkill更新時の品質ゲートとして組み込みます。評価で検出された誤答パターンをSkillの手順や蓄積知識ファイルへ反映していくことで、「使うほど誤答が減る」状態を目指します。 他チームへの展開と各チーム固有Skillの整備 現状この仕組みはWebバックエンドの調査文脈に最適化されていますが、調査手順をSkillと蓄積知識ファイルに切り出す構造はチーム非依存です。今後は他チームへの展開と、各チーム固有のSkill整備を進めていきます。 まとめ 本記事では、Devin・Claude Code Actions・各種MCPを組み合わせたCS問い合わせ調査の自動化を紹介しました。Skillに調査手順を集約し、SlackからGitHub Issue経由でAIに自律的に調査させる仕組みです。これによって1件あたり1〜2時間かかっていた一次調査を10分程度まで短縮し、属人化していた調査ノウハウを「動くマニュアル」として再利用可能な形にできました。同じようにCS問い合わせや運用業務の属人化に課題を抱えるチームを持つ方にとって、何かしらの参考になれば嬉しいです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
ZOZO開発組織の2026年4月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2026年4月は、前月のMonthly Tech Reportを含む計11本の記事を公開しました。中でもWEARバックエンドの無停止移行に関する記事は詳細に記されています。ぜひご覧ください。 techblog.zozo.com 登壇 JAWS-UG山梨 【第11回】勉強会 4月4日に開催された「 JAWS-UG山梨 【第11回】勉強会 」に、SRE部の姫野が登壇しました。 AWS Data & AI イノベーションフォーラム:顧客成功事例から学ぶデータ活用の最前線 DAY1 4月9日に開催された「 AWS Data & AI イノベーションフォーラム:顧客成功事例から学ぶデータ活用の最前線 DAY1 」に、SRE部の伊藤が登壇しました。 Claude Code Skills実践! - 業務を効率化する活用事例 4月9日に開催された「 Claude Code Skills実践! - 業務を効率化する活用事例 」に、ZOZOTOWN開発3部の平林( @bayacollector )が登壇しました。 try! Swift Tokyo 2026 4月12-13日に開催された「 try! Swift Tokyo 2026 」に、ZOZOTOWN開発2部の續橋( @tsuzuki817 )が登壇しました。 【ZOZO x Mercari x LayerX】企業R&D勉強会 〜 研究と実用化のリアル〜 4月24日に開催された「 【ZOZO x Mercari x LayerX】企業R&D勉強会 〜 研究と実用化のリアル〜 」に、ZOZO研究所の清水が登壇しました。 協賛 RubyKaigi 2026 2026年4月22日から24日の3日間にわたり函館で開催された「 RubyKaigi 2026 」にプラチナスポンサーとして協賛しました。 technote.zozo.com techblog.zozo.com 掲載 ZOZO独自のAI活用指標「All ZOZO AI Readiness Score(AZARS)」 4月8日にZOZO独自のAI活用指標である「All ZOZO AI Readiness Score(AZARS)」の導入を発表しました。 AZARS(アザース)は「組織AI活用レベル」と「個人AI活用レベル」の2つで構成され、生成AIを含むAI活用において、業務上期待される能力と状態をそれぞれ4段階で定義した指標です。本指標の導入により、主観的になりがちなAI活用度を全社統一の基準で可視化・評価することが可能になります。またAZARSは、エンジニアなどの開発者と、事業・コーポレート部門といった非開発者の双方に共通する指標を定めている点に特徴があります。これにより、職種にかかわらず、同一の基準でAI活用を推進することが可能になります。 corp.zozo.com この「AZARS」導入に関連した記事が複数のメディアに掲載されました。 www.itmedia.co.jp www.nikkei.com AI活用はどう可視化して推進する? ZOZOが導入した全職種共通のAI活用指標「All ZOZO AI Readiness Score(AZARS)」とは | ネットショップ担当者フォーラム ZOZOの似合うコーデAI ラボくん 4月27日に対話で日常の服選びをサポートするLINE公式アカウント「ZOZOの似合うコーデAI ラボくん」の開設を発表しました。 「ZOZOの似合うコーデAI ラボくん」は、ユーザーの言語化しにくいファッションの好みやニーズを「ラボくん」との会話によって引き出し、要望を具体化することで、ユーザーの嗜好や利用シーンに応じたコーディネートを提案します。多くの方が利用するLINEという日常的な接点をチャネルとすることで、ファッションの情報探索や相談をより身近で手軽なものにし、日常の服選びをサポートします。 corp.zozo.com この「ZOZOの似合うコーデAI ラボくん」開設に関連した記事が複数のメディアに掲載されました。 netkeizai.com www.ryutsuu.biz ZOZOがLINEでコーデ相談AI ファッションECの「検索から対話」の試金石 - WWDJAPAN 日経BOOKプラス 日経BOOKプラスのゼロから創らない戦略に、ZOZOのビジネスモデルに関する記事が掲載されました。 bookplus.nikkei.com その他 香りの総合プラットフォーム「カラリア」を運営する株式会社High Linkを完全子会社化 4月30日に株式会社High Linkの全株式を取得し完全子会社化 したことを発表しました。 当社は、今後の戦略の一つとして「Near Fashion領域」での成長を掲げ、ファッションの周辺領域における事業推進と利益創出を目指しています。ファッションと親和性が高い香水を中心にした事業を手掛けるHigh LinkをZOZOグループに迎えることで、当社グループはフレグランス市場への領域拡大を図るとともに、サブスクリプションサービスをはじめとする販売手法を取り入れ、ファッション周辺領域における事業展開を加速します。今後は当社の顧客基盤を活用したHigh Linkの各サービスへの送客や、当社のEC運営ノウハウおよびデータを活用し、香水との新たな出会いを促すディスカバリー体験の提供などにも取り組む予定です。 corp.zozo.com 2026年3月期 通期決算発表 4月30日に2026年3月期 通期決算発表を開示しました。詳細は以下のリンクにある開示資料をご確認ください。 corp.zozo.com 以上、2026年4月のZOZOの活動報告でした! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ブランドソリューション開発本部ZOZOMO部FBZブロックの 池上 寛登 です。2026年3月にZOZOへ入社し、 Fulfillment by ZOZO (以下、FBZ)のバックエンド開発を担当しています。 FBZに参画してまず直面したのは、ドメイン知識の壁でした。中でも強く実感したのが、コードレビューの場面です。Pull Request(以下、PR)のレビューには、判断の根拠がドキュメントに載っていない「暗黙知の壁」がありました。既存メンバーの指摘は的確ですが、新規参画者の自分には同じ品質でレビューする難しさがありました。 この課題を解決するために、暗黙知を形式知としてガイドライン化し、Claude Code SkillsとAmazon Bedrockに組み込んだPR自動レビュー基盤を作成しました。本記事では、その仕組みと設計判断を紹介します。 目次 はじめに 目次 背景:FBZにおけるPRレビューの課題 アプローチの全体像 ガイドラインの設計 暗黙知の収集 レイヤー別ガイドラインの作成 変更パスに応じた参照ルールの選択 実行基盤の構成 実行環境 PRサイズに応じたモデル選択 2段レビューによる誤検知抑制 Confidence(確信度)閾値と Severity(重要度)ラベル ai-reviewedラベルによる再レビュー ガイドライン更新の自動提案 効果 ドメイン固有のアンチパターン検出 実装品質の向上 新規参画者にとってのドメインリファレンス まとめ 背景:FBZにおけるPRレビューの課題 FBZはZOZOTOWNの倉庫リソースを活用し、外部のブランドが運営する自社ECへ物流・決済・返金などの機能を提供するフルフィルメントサービスです。在庫同期・注文管理など、扱うドメインは多岐にわたります。 さらにFBZは複数のリポジトリで構成されており、リポジトリごとに採用言語やアーキテクチャが異なります。PRレビューで見るべき観点もリポジトリごとに大きく変わるため、レビュアーには横断的な知識が求められます。 実装やレビューの参考になるようなガイドラインは存在したものの、リポジトリごとに保存場所が異なり、内容の鮮度や粒度も統一されていませんでした。結果として、判断はレビュアー個人の経験知に大きく依存し、次の3つの課題が顕在化していました。 レビュアーごとに観点が異なり、レビュー品質にばらつきが出る 新規参画者がドメイン知識をキャッチアップするまでに時間を要する 同じアンチパターンの指摘が、異なるPRで繰り返し発生する アプローチの全体像 これらの課題に対し、ガイドラインを中心に据えたPR自動レビュー基盤を構築しました。 取り組みは大きく次の3層に分かれます。 ガイドラインの設計 :暗黙知を形式知へ落とし込み、レイヤー別ファイルとNG/OKペアで定義する 実行基盤の構成 :Claude Code Actionを実行し、関連するガイドラインを読み込んでPRをレビューする ガイドライン更新の自動提案 :過去のレビューコメントからガイドラインの更新提案を自動生成し、ルールの陳腐化を防ぐ それぞれを順に説明します。 ガイドラインの設計 暗黙知の収集 ガイドラインに落とし込む暗黙知は、PRのレビューコメントから収集しました。当初はSlack(コミュニケーションツール)の議論やConfluence(社内Wiki、ADR)の設計メモからも抽出を試みました。しかしFBZの場合は設計判断や指摘の根拠の多くがPRのレビュー会話に蓄積されていたため、最終的にPRを主な情報源とすることにしました。 過去のPRレビューを横断的に分析し、次の観点に該当する指摘をルール化しました。 同種の指摘が繰り返し発生している 既存のドキュメントではカバーされていない チーム全体で共有すべき設計判断が含まれている レイヤー別ガイドラインの作成 収集した暗黙知をルール化するため、ドメインで頻出する論点を抽出し、アーキテクチャレイヤーごとのファイルへ分割しました。具体的には、レイヤー責務・データ整合性・エラー設計・テスト/コーディング規約・インフラ/セキュリティといった観点で章を分け、全章で前提となる共通ファイルを別途用意しています。 各ルールはNG/OKペアの形式で記述しています。形式を統一することで、LLMがルールの境界を判定しやすくなります。 # NG: 呼び出し元で税率や端数処理をハードコードしている total = round (price * 1.1 ) # OK: ドメイン共通の計算ロジックを通し、税率や端数ルールを一元化できている total = PriceCalculator.with_tax(price) 変更パスに応じた参照ルールの選択 レビュー時にガイドラインを全て読み込むと、コンテキストが肥大化して精度も落ちます。そこで、変更ファイルのパスから参照すべきガイドラインを対応付ける表を定義しました。 この対応表は SKILL.md に記述しており、LLMが変更ファイル一覧と対応表を照らし合わせて、該当する章だけを動的にロードすることを実現しています。 'app/service/**' : layer-rules/layer-responsibility.md 'app/dataaccess/**' : layer-rules/data-integrity.md 'tests/**' : layer-rules/test-and-coding.md 実行基盤の構成 実行基盤の全体像は以下のとおりです。図中の各要素については、次節以降で順に解説していきます。 実行環境 基盤としてはGitHub Actions上で動作するClaude Code Actionを採用し、モデル呼び出し先にはAmazon Bedrockを選びました。 ガバナンス要件として、社外のサービスへソースコードを送信せず、社内AWSアカウントに閉じた状態でモデルを利用する必要があったためです。GitHub ActionsからはOIDCでAWSにAssumeRoleし、Bedrockの推論プロファイル経由でClaudeモデルを呼び出します。これによりIAM・ログ・モデル呼出のすべてを社内環境に閉じた構成で運用できます。 PRサイズに応じたモデル選択 PRのサイズに応じて、レビューに使うモデルを動的に切り替えています。PRサイズは、差分の行数と変更ファイル数の組み合わせで判定しています。Opusは検証段階で精度向上の幅がコストに見合わなかったため、採用していません。SonnetとHaikuの使い分けで、精度・コスト・レイテンシをバランス良く確保できました。 PRサイズ モデル 設計判断 小規模・単一ファイル Claude Haiku 軽量な判定で十分な品質を確保できるため、コストとレイテンシを優先する 中〜大規模 Claude Sonnet レビュー本処理の標準モデル。大規模PRでは影響範囲の調査で往復回数が増えるため、ターン上限(LLMがツール呼び出しを連続で行える回数の上限)を引き上げて対応する 2段レビューによる誤検知抑制 LLMによる自動レビューで課題となるのが、誤検知(False Positive)と見逃し(False Negative)です。誤検知が多ければBotの指摘は信頼されなくなり、見逃しが多ければ自動レビュー自体の意義が失われます。 この課題に対し、単一セッション内でペルソナを切り替えながら2段でレビューする仕組みを取り入れました。 Reviewer Passは「網羅的に拾うペルソナ」として候補を出し切り、Validator Passは「批判的に再検証するペルソナ」として根拠を再確認します。役割を意識的に分離することで、互いに独立した判断が成立します。検証段階で複数のレビュー方式を比較したところ、この方式が最も誤検知を抑えられました。 Confidence(確信度)閾値と Severity(重要度)ラベル Validator Passでは、各指摘に対してチェック項目を採点し、合計値をその指摘のConfidenceとして算出します。チェック項目は「根拠コードを再読み込みしたか」「PR説明文を踏まえているか」「影響範囲を確認したか」などです。 指摘にはImportant・Pre-existing・Nitの3種類のSeverityを付与し、それぞれに投稿するためのConfidence閾値を設けています。Importantは厳しめ、Nitは緩めの閾値です。閾値を満たさない候補は、たとえReviewer Passで挙がっていても投稿しません。 Severityはコメント先頭に [Important] のようなテキストラベルとして付与します。PR作成者はラベルを見て対応の優先度を判断できます。 ai-reviewedラベルによる再レビュー 不要な再実行を避けてコストを抑えるため、レビュー完了時にPRへ ai-reviewed ラベルを付与しています。PR作成者は修正後にラベルを外すだけで、必要なときだけ再レビューを起動できます。 また再レビュー時はノイズ抑制のため、ブロッキング相当の指摘であるImportantとPre-existingのコメントのみを投稿するよう制御しています。 ガイドライン更新の自動提案 ガイドラインは、コードベースや設計判断の変化に追従できないため、時間の経過とともに陳腐化します。これを避けるため、直近のレビューコメントと既存ガイドラインを照らし合わせ、追加・修正・削除を含む更新提案を起票するワークフローを毎月実行するようにしました。 また、ガイドライン更新時の判断基準を統一するため、追記場所・文体・重複チェック手順をまとめたメタガイドラインを別ファイルで用意しています。これにより、ガイドラインの一貫性を保つことができます。 効果 運用開始からまだ日が浅いですが、現時点で見えている効果は次のとおりです。 ドメイン固有のアンチパターン検出 ガイドラインに沿ったレビューが行われるため、レイヤー責務・データ整合性・エラー設計といったドメイン色の強い論点も、自動レビューで捕捉できるようになりました。 実装品質の向上 今回の対応でプロジェクトにおける暗黙知を形式知化できました。例えばこのガイドラインを .claude/rules/ に配置することで、開発時のClaude Codeも同じルールを参照する効果を期待できます。 新規参画者にとってのドメインリファレンス 副次効果として、ガイドラインは新規参画者の学習リファレンスとしても機能しています。私自身もこのガイドラインで「FBZのService層はどう書くのが正解か」を確認しながら実装を進めてきました。 まとめ 本記事では、Claude Code SkillsとAmazon Bedrockを組み合わせてFBZドメインに特化したPR自動レビュー基盤を構築した取り組みを紹介しました。 ドメイン知識を抱えるチームでPRレビューの自動化を検討している方は、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、WEAR開発部SREブロックの木内です。普段は WEAR のSREとして開発、運用に携わっています。 WEARは2013年にサービスを開始し長年オンプレミスで運用されてきましたが、過去にクラウド(AWS)へのシステムリプレイスを実施しています。その際にWebアプリのCDNとして Fastly を採用し、オンプレミスからクラウドへの段階的な移行を実現しました。 採用の決め手は主に以下の点です。 パスベースのルーティングが可能(パスごとにオンプレミスとクラウドのオリジンを切り替えられる) ネイキッドドメインへの対応 設定変更の即時反映による迅速なロールバック リプレイスの詳細については、以下の記事をご参照ください。 techblog.zozo.com Fastlyを用いた構成はサービスを止めずに安全なリプレイスを実現するうえで大きく貢献しました。一方で、リプレイスが完了しFastlyを導入した当初の目的を達成した後も残り続けたことで、運用負荷やコストといった課題が顕在化してきました。 本記事では、過渡期の構成を整理し、CDNをFastlyからAmazon CloudFront(以下、CloudFront)へ移行してAWSに統一した取り組みを紹介します。 目次 はじめに 目次 移行の背景・課題 運用負荷 コスト 移行後の構成 移行方法 1. LP・アセットの切り替え 2. 動的コンテンツ・認証処理の切り替え 3. VCLの処理をCloudFront Functionsへ移行 4. DNS切り替え WAFの移行タイミング 設計のポイント 移行期間中のリクエスト CloudFront Functionsを用いたリダイレクト・リライト カスタムエラーレスポンス 得られた成果 運用のシンプル化 インフラコストの削減 まとめ 移行の背景・課題 移行前の構成は以下の通りです。 Fastlyをフロントに置き、バックエンドにALBとS3が2つずつ、計4つのオリジンを持つ構成です。 ALBは動的コンテンツの配信や認証処理を担っており、S3はLPやアセットなどの静的コンテンツを配信しています。S3の前段にはそれぞれ個別のCloudFrontを使用しています。 また、FastlyではVCL 1 を用いてCDN以外の多くの処理も担っていました。 パスごとのオリジン振り分け 各種ブロック(IP / ASN / リファラ など) Basic認証 メンテナンスモード リダイレクト / リライト 前述の通り、リプレイス時にFastlyを採用したことで安全にリプレイスを進めることができましたが、リプレイス完了後に以下のような課題が残りました。 運用負荷 AWSとFastlyの二重管理が運用負荷に繋がっていました。 WEARの大部分はすでにCloudFrontを使用しており、2つのCDNそれぞれでキャッチアップが必要だった AWSとFastlyそれぞれでユーザーやリソースの管理も必要だった 設定変更をWebチームとSREチームで担っていたため、Fastly専用のインフラリポジトリを別途設けており、CIの整備やレビューフローの維持など、リポジトリが増えることに伴う管理コストが発生していた コスト FastlyをAWSの前段に置く構成であるため、大きく分けて2種類のコストが発生していました。 Fastlyがユーザーへ配信するコスト AWSがFastlyへ配信するコスト また、LPやアセットはすでにCloudFrontを使用していたため、FastlyとCloudFront両方のCDNコストが発生していた点も見過ごせません。 これらの課題を解消するために、CDNをCloudFrontへ移行し、配信基盤をAWSへ統一する方針を取りました。 移行後の構成 移行後の構成は以下の通りです。 移行後は、ALB・S3など全てのバックエンドの前段に1つのCloudFrontを配置しています。 また、FastlyのVCLで実施していた処理はそれぞれ対応するAWSサービスへ移行しました。 処理 Fastly AWS CDN・配信 Fastly CDN CloudFront パスごとのオリジン振り分け VCL CloudFront(Cache Behavior) 各種ブロック(IP / ASN / リファラ など) VCL AWS WAF Basic認証 VCL AWS WAF メンテナンスモード VCL AWS WAF リダイレクト / リライト VCL CloudFront Functions Basic認証・メンテナンスモードはAWS WAF(以下、WAF)の カスタムレスポンス機能 を利用して実装しています。WAFのルールにマッチしたリクエストに対して、任意のHTTPステータスコード・レスポンスヘッダー・レスポンスボディを返せる機能です。 セキュリティ関連の処理をWAFに集約することで一元管理できることに加え、CloudFront Functionsのコードサイズや実行時間の制限 2 を避けられる点も採用の理由です。 リダイレクト・リライトの実装にあたり、Lambda@EdgeとCloudFront Functionsを比較検討しました。Lambda@Edgeは複雑な処理や長時間実行に向いている一方、今回のような軽量なリダイレクト・リライト処理にはCloudFront Functionsが適しています 3 。加えてスケール量・リクエスト料金の面でも有利なため採用しました。 さらに、リダイレクト・リライトはルール数が多いため、jsをビルドしminify化しながらCloudFront Functionsのコードサイズ制限を超過しないよう工夫しています。 CloudFront KeyValueStore を使用したサイズ圧縮も検討しましたが、フロントとインフラ間の依存関係の簡素化や認知負荷の低減を優先したかったからです。 これらの設計を経て、FastlyのVCLに分散していたエッジ処理をAWS WAFとCloudFront Functionsに集約し、CDNレイヤーの機能をすべてAWS上で完結させる構成になりました。 移行方法 今回のCDN切り替えはフェーズを分けて段階的に実施しました。このCDNはPC・SP全体のトラフィックを受けているため、問題が発生すれば多くのユーザーへ影響が出てしまいます。そのため、いかにユーザー影響を出さず安全に移行するかが本プロジェクトの鍵でした。 具体的なフェーズは以下の通りです。Fastlyの後段にCloudFrontを配置し、影響範囲が小さいものから順にCloudFront経由へ切り替えていきました。 1. LP・アセットの切り替え 最初はS3で配信しているLPとアセットの切り替えです。静的コンテンツは影響範囲が限定的で切り戻しもしやすいため、最初の移行対象としました。 切り替えにあたっては全パスを網羅するURLリストを用意してスクリプトで動作確認を行い、移行前後の挙動に差異がないことを確認しながら実施しました。 2. 動的コンテンツ・認証処理の切り替え 次に動的コンテンツの配信や認証処理を担うALBの切り替えです。動的コンテンツの配信や認証処理を担っているため、CDN切り替えによるヘッダーやキャッシュの挙動の変化が配信や認証に影響を与えるリスクがありました。 影響範囲を抑えるためにALBは1台ずつ切り替え、バックエンドチームにも協力してもらい機能リグレッションがないことを確認しながら進めました。 加えて、ダークカナリアリリース(ユーザーには見えない形で一部のトラフィックを新しい構成に流し、本番環境で検証する手法)でCloudFront経由に流し、環境差異による不具合がないかも検証しました。 3. VCLの処理をCloudFront Functionsへ移行 続いてVCLのリダイレクト・リライト処理を移行しました。VCLの処理の中にはリクエスト元の国別判定をした上でリダイレクトをする処理もあり、障害につながりやすい部分であったため、切り戻しやすさを考慮して2段階に分けて対応しました。 動作確認はFastlyで実施していたリダイレクト・リライト処理を網羅的に検証できるスクリプトをWebチームが作成してくれており、そちらを活用し移行前後の挙動に差異がないことを確認しながら進めました。 スクリプトはDenoのテストフレームワークで書かれており、各パスへのリクエストに対してステータスコード・リダイレクト先・レスポンスヘッダーを検証します。さらにHTMLレスポンス内のJS・CSSアセットも正常に取得できるかまで確認しており、移行後の挙動を一通りカバーしています。 また、VCLの移行はオリジンの切り替えと異なり、パスごとに挙動が細かく異なります。そのため追加のスクリプトも作成し、リダイレクト・リライト対象外のパスへの影響がないか、Serverヘッダーでオリジンが意図した経路を通っているかを、1パスずつ地道に確認していきました。 4. DNS切り替え 最後にDNSを切り替えてFastlyからCloudFrontへ完全移行しました。 WEARはDNSに Akamai を使用しています。今回、Akamaiの Change List を活用し、ダウンタイムなしで切り替えを実現しました。 CloudFrontのIPアドレスは固定されていないため、FastlyのIPを登録していたAレコードからCloudFrontのドメインを指定するCNAMEへの変更が必要でした。しかしAレコードとCNAMEは共存できないため、削除と追加を別々に適用すると瞬断のリスクがありました。Change ListはDNSレコードの変更をまとめてアトミックに適用できる機能で、これを活用することでダウンタイムなしでの切り替えを可能にしています。切り替えは有事の際に素早く切り戻せるよう、事前にTTLを60秒に下げた上で実施しました。 また、次のセクションで詳しく説明しますが、ブロックやBasic認証等のWAF切り替えもこのタイミングで同時に実施しています。 WAFの移行タイミング WAFはIP・国・ASNなどの判定を、送信元IPまたはX-Forwarded-For(XFF)ヘッダーのIPアドレスを基に行います。 DNS切り替え前はFastlyがリクエストを受け取るため、AWS WAFから見るとアクセス元のIPがFastlyのIPになります。この状態でWAFを先に切り替えると、ブロックルールがFastlyのIPに対して適用され、意図しないブロックや、本来ブロックすべきリクエストが通過してしまうリスクがありました。 XFFを参照することでクライアントIPに基づく判定も可能ですが、以下の理由から採用しませんでした。 Fastlyの判定ロジックが送信元IPを利用しており、仕様を変えたくなかった DNS切り替え前後でXFFの内容が変わるため、切り替えのタイミングで挙動が変わることを避けたかった そのため、DNS切り替えまでの間はFastly側のブロック設定を残し、WAFの切り替えはDNS切り替えと同時に行いました。 なお、事前の動作確認はCloudFrontのFQDNに直接curlでアクセスして行いました。DNS切り替え前はFastlyがエンドポイントのため、CloudFrontのFQDNに対してアクセスする必要があります。CloudFrontはHTTPS接続時にHostヘッダーの値でSSL証明書を選択するため、Hostヘッダーにドメインを指定することで正しく証明書検証が行えます。 CloudFront FunctionsについてもDNS切り替え前はリクエストがFastlyを経由するため、リクエスト元の国別判定で同様の問題がありました。 こちらはWebチームが解決策を検討・実装してくれました。DNS切り替え前の移行期間中に限り、Fastly側でCloudFrontの仕様に準ずるヘッダーを付与しました。CloudFront Functions側でもそのヘッダーを参照することで、正しく地域判定が行える構成にしています。 設計のポイント ここまで移行の手順を紹介しました。次に、移行にあたって設計上特に考慮が必要だったCloudFrontのビヘイビア設計について紹介します。 CloudFrontでは、リクエストのURIパターンに応じて処理方法を定義する「 Cache Behavior 」という仕組みがあります。今回はオリジンの振り分けをビヘイビアで制御しており、その設計がFunctions実装やエラーハンドリングに直結するため、いくつか考慮する必要がありました。 移行期間中のリクエスト Cache BehaviorはURIパターンで 優先度順に評価 され、先にマッチしたものが適用されます。パターンにはワイルドカード(*)が使えますが、ワイルドカードなしの場合は完全一致での評価になります。 この仕様を踏まえてビヘイビアを設計しました。また、移行期間中はFastly側の既存のロジックでリダイレクトされたパスがCloudFrontに届くケースもあるため、リダイレクト後のパスに対応するBehaviorも明示的に用意しました。 CloudFront Functionsを用いたリダイレクト・リライト CloudFront Functionsを用いたリダイレクト・リライトの実装にあたり、CloudFrontの処理順序を正しく理解することが重要でした。 CloudFrontはリクエストを受信した時点のURIをもとにCache Behaviorを選択します。その後CloudFront Functions内で request.uri を書き換えても 選択済みのBehaviorは変わりません 。そのため、リライト後のパスが意図したオリジンに届くよう、元のURIの段階で適切なBehaviorを選択できる設計にしました。 カスタムエラーレスポンス CloudFrontの カスタムエラーレスポンス は、オリジンが特定のHTTPステータスコードを返した際に指定のパスへフォールバックできる機能です。今回はすべてのオリジンの404をALBオリジンのエラーページにフォールバックするよう設定しています。 カスタムエラーレスポンスは、CloudFront Functionsの挙動と違い フォールバック先に指定したパスでビヘイビアが再評価されます 。そのため、フォールバック先に指定したパスがALBオリジンのビヘイビアに正しくルーティングされるよう設計しました。 また、S3オリジンはデフォルトでオブジェクトが存在しない場合に404ではなく403を返すため、このままではカスタムエラーレスポンスが正しく動作しません。これを解消するために、 OAC(Origin Access Control) のバケットポリシーに s3:ListBucket を追加し、S3がオブジェクトの有無を判断して404を返せるようにしました。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Effect ": " Allow ", " Principal ": { " Service ": " cloudfront.amazonaws.com " } , " Action ": [ " s3:GetObject ", " s3:ListBucket " ] , " Resource ": [ " arn:aws:s3:::amzn-s3-demo-bucket/* ", " arn:aws:s3:::amzn-s3-demo-bucket " ] , " Condition ": { " StringEquals ": { " AWS:SourceArn ": " arn:aws:cloudfront::111122223333:distribution/<CloudFront distribution ID> " } } } ] } 得られた成果 月間数億リクエストを処理するCDNの切り替えは、ユーザーへの影響が出れば大きな障害につながりかねないものでした。段階的なトラフィック移行や入念な動作確認を重ね、一切のダウンタイムなしで移行を完了できたことは、本プロジェクト最大の成果です。 加えて、今回の移行は単なるCDNの切り替えにとどまらず、長年の過渡期構成を整理し、運用・コスト・セキュリティの三面で改善を実現できた取り組みでした。具体的には次のとおりです。 運用のシンプル化 VCLに集約されていた処理がAWS WAFとCloudFront Functionsへ役割ごと分離されたことで、各機能の責任範囲が明確化しました。変更が必要な際も影響箇所を絞り込みやすく、必要な箇所だけ修正できるようになりました。 また、Fastlyのアカウント管理やVCL設定の維持が不要になり、AWSに運用を集約できるようになりました。 さらに、AWSに統一したことで、追加機能の検証や導入の敷居も下がりました。 実際に、移行完了後1か月以内にAWSが提供する WAFマネージドルール の追加導入が完了しています。WAFのCountモードで事前に検証し、誤検知がないこと・不審なリクエストのブロック効果があることを確認した上で導入を判断しました。 今後も既存のルールを拡充し、さらなるセキュリティ強化を進めていく予定です。 インフラコストの削減 コスト削減は今回の移行における主要な目的の1つであり、結果としてCDN関連のインフラコストを 約40%削減 できました。 主な内訳は以下です。 CloudFrontをCDNとすることにより、ALBからのトラフィックコストが大幅に緩和されたこと LPとアセットで発生していた二重の配信コストが解消されたこと この削減は構成変更による恒久的なものであり、継続的なコスト改善として今後も効果が持続します。 まとめ 本記事では、FastlyからCloudFrontへの移行を通じて、CDN構成をAWSに統一した取り組みを紹介しました。 今回の移行は単なるCDNの乗り換えにとどまらず、長年の過渡期構成を整理し、運用・コスト・セキュリティの三面で改善を実現できた取り組みでした。 「当時は最善だった構成が、今となって見直しどきを迎えている」という状況は多くのサービスで共通する課題だと思います。CDNの移行や構成整理を検討している方にとって、本記事が参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com VCL(Varnish Configuration Language)はVarnishというキャッシュサーバー向けの設定言語です。 ↩ CloudFront Functionsはコードサイズが10KB以下、実行時間が1ms以下という制限があります(参考: CloudFront Functions のクォータ ) ↩ CloudFront Functions と Lambda@Edge の違い ↩
はじめに こんにちは、ZOZOTOWN開発1部iOSブロックの荻野です( @juginon )。 みなさんに日々使っていただいているZOZOTOWN iOSアプリのホーム画面ですが、実は2024年秋から2026年の年初まで約1年半、水面下でリアーキテクチャを行っていました。 リアーキテクチャに着手する前の当時の私はアーキテクチャ設計への理解がまだ浅く、「実際に手を動かしながら身につけたい」という動機でこのリアーキテクチャを主導しました。自分にとってはチャレンジングな取り組みで、アーキテクチャ設計やテスト設計への理解が実践を通して大きく深まったプロジェクトになりました。 本記事では、そのリアーキテクチャのすべての軌跡と、そこで得た学びをお伝えします。 なお、本記事で紹介するホーム画面リファクタリングは、iOSチーム全体で取り組んでいるアーキテクチャ刷新の具体的な事例の1つでもあります。チームとしての取り組みや知識共有の仕組みについては ZOZOTOWNのiOSアーキテクチャとチーム進化の軌跡 にもまとめています。本記事と合わせて読むと、個々の取り組みとチーム全体の文脈をより立体的に理解できます。 目次 はじめに 目次 ホーム画面について タブ モジュール ホーム画面が抱えていた課題 当初の設計と、その後の運用実態の乖離 継承構造が不要になった ログ管理の複雑化 MVCによるViewControllerへの責務集中 高い改修頻度 リファクタリングの設計方針 方針1: 影響範囲を最小化しながら段階的に進める 方針2: 段階的に責務を分離する Step1: Objective-Cレガシー型への依存を剥がす Step2: 最も独立性の高いAPIを対象に、ViewModel/UseCaseを部分導入する 小さく始めることの重要性 Step3: MallHomeViewController全体にViewModel/UseCaseを導入する 問題1. Swift Concurrencyへの移行 問題2. モジュール構築メソッドの整理 Step3完了後にログに関するバグが発覚 バグを引き起こした原因 Step3-ex: 命名整理とユニットテストの追加 命名の整理 ユニットテストの追加 不確かさに気づいた時点でテストを書く Step4: HomeViewControllerにViewModel/UseCaseを導入する TDDによる設計の共有 オンボーディング周りの状態管理 リファクタリング前の課題 ステートマシンによる再設計 長期リファクタリングを進める上でのポイント おわりに ホーム画面について ZOZOTOWN iOSアプリのホーム画面は以下のように、主にタブとモジュールによって構成されています。 タブ 画面上部に表示されている「すべて」「コスメ」部分を指します。タブは切り替えが可能で、すべてタブではアパレル・シューズ・コスメ等すべての商品が、コスメタブではコスメ商品特化の画面表示になります。 実装上は以下の2種類のViewControllerで構成されています。 HomeViewController : ホームタブのルート画面となる画面全体を管理するViewController ヘッダーや検索窓など、両方のタブで共通して表示する部分、ホーム画面全体の管理を担う MallHomeViewController : すべてタブ/コスメタブのコンテンツを管理するViewController それぞれのタブで表示が変わる部分の管理を担う モジュール 各タブのコンテンツは、複数の「モジュール」と呼ばれるブロックが縦に並んだ構成です。モジュールとは、性別選択・バナー・チェックしたアイテムといった、個々のコンテンツ単位のことです。 ユーザーがホーム画面をスクロールすると、これらのモジュールが順番に表示されます。 ホーム画面が抱えていた課題 当初の設計と、その後の運用実態の乖離 ホーム画面の複雑さを理解するには、2021年のフルリニューアル時の背景を知る必要があります。 2021年3月のZOZOTOWNフルリニューアルで初めてタブ構成が導入されました。当時は3つのタブがあり、 MallHomeViewController を基底クラスとした3つのサブクラスによる継承構成を採用しました。各タブで固有の処理が発生することを見越した設計です。 当時の取り組みについては ZOZOTOWNアプリ Home画面のリニューアルにおけるアーキテクチャ再設計 でも詳しく紹介されています。 しかし、フルリニューアルから3年以上が経過し、運用を重ねる中で当初の設計前提が変わっていきました。 継承構造が不要になった 従来では MallHomeViewController を継承する各タブのクラスを作成していましたが、各タブで固有の処理は実際にはほとんど発生しませんでした。 タブの種類を保持するだけで十分な状態で、各タブで専用のクラスを作成する構造はかえって全体像の把握を難しくしていました。 ログ管理の複雑化 リニューアル当初はGA(Google Analytics)のみだったログ送信を専用のLoggerクラスが管理していました。しかしその後、社内分析用ログなど複数種別のログが追加されていく中で、Logger自身が複雑な状態管理を担うようになっていきました。 複数のフラグがLoggerの内部に積み重なり、 MallHomeViewController が持つ状態と常に同期させる必要が生じました。また、ログに関する責務分離が適切に行われていない部分もあり、こういった構造がコードを読む際のコストを高める要因の1つになっていきました。 MVCによるViewControllerへの責務集中 2021年当時はMVCアーキテクチャを採用していたため、API呼び出し・UI状態管理・ビジネスロジックの調整が MallHomeViewController に集中していました。前述のLoggerクラスとの状態同期もVCが直接担っており、改修を加えるたびにVC・Logger双方への影響を考慮しなければなりませんでした。こうした積み重ねで行数は再び1000行を超えるまでに膨らんでしまっていました。 特に問題だったのは、UICollectionViewへのデータ構築と商品押下時のログデータ作成が混在する500行弱の巨大なメソッドです。どこを触れば何が変わるのか把握するだけで大きなコストがかかる状態でした。 高い改修頻度 ZOZOTOWNのホーム画面は平均月1ペースで改修案件が入り、多い時期には3案件が同時並行で走ることもあります。 このリアーキテクチャが開始してから現在まででも、ホーム画面のモジュールを無限スクロールできる機能や、モジュール内のアイテムで動画を表示する機能など、規模の大きな案件がリリースされています。 影響範囲の把握が困難なFat ViewControllerは、改修のたびにリスクを伴い、チームの開発速度を下げる原因になっていました。 リファクタリングの設計方針 課題は明確でしたが、1000行超のVCを一気に書き換えるのはリスクが高すぎます。そこで以下の方針を立てました。 なお、このリファクタリングは通常の機能開発と並行して進めており、稼働の約2割をこの取り組みに充てながら進めていました。1年半という期間はそのためです。 方針1: 影響範囲を最小化しながら段階的に進める 各ステップの影響範囲を小さく保つことで、問題発生時の修正コストを抑えられ、PRの変更量も少なくなりレビューの負担を減らせます。また各ステップを独立してリリース可能な単位とすることで、他案件の進行をブロックしません。 以上のメリットを意識しながら以下のステップで進める計画を立てました(当初は4ステップ、結果として5ステップになりました)。 ステップ 内容 Step1 Objective-Cレガシー型への依存を剥がす Step2 最も独立性の高いAPIを対象に、ViewModel/UseCaseを部分導入する Step3 MallHomeViewController全体にViewModel/UseCaseを導入する Step3-ex Step3完了後にバグが発覚し、命名整理とユニットテストを追加 Step4 HomeViewControllerにViewModel/UseCaseを導入する  ステップを設計する上でのポイントを3点紹介します。 Step1を最初に行った理由 MallHomeViewController にはObjective-Cのレガシーな型への依存がありました。MVVM化を先に進めると、ViewModel/UseCaseはObjCの型を扱う設計になります。その後ObjC依存を除去すると、ViewModel/UseCaseの設計変更も必要になり手戻りが発生します。そのため、MVVM化の前段階として依存の除去を最初のステップとしました。 MallHomeViewControllerから先に着手した理由 タブの中身を管理している MallHomeViewController は、着手開始から間もなく後続案件の改修が入る予定でした。そのため、それより前にMVVM化を完遂させることを優先しました。 Step2とStep3を分けた理由 ホーム画面では複数のAPIを呼び出しており、最初から全APIを対象とするとMVVM化の影響範囲が大きくなりすぎます。まず独立性の高い一部のAPIに絞ってViewModel/UseCaseを導入することで、アーキテクチャの全体像を小さな変更で確認でき、問題が発生した際の修正コストも抑えられます。 方針2: 段階的に責務を分離する UseCase → ViewModel → ViewControllerの順で責務を分離していき、最終的に以下の構成を目指しました。当時のアーキテクチャガイドラインではUseCaseの採用が定められていました。またAPIリクエスト・ログ送信・ビジネスロジックが複合的に絡むホーム画面の規模感においても、ViewModelの肥大化を防ぐうえで適切な設計判断でした。 ここで紹介している大まかな全体方針は、以前チームメンバーの なんしー さんが行った 商品詳細画面のリアーキテクチャにおける進め方 を参考にしています。 Step1: Objective-Cレガシー型への依存を剥がす MallHomeViewController では、商品情報を表示する部分がObjective-Cで書かれたレガシーな型に依存しており、APIレスポンスからレガシーな型へ変換する不要な依存がありました。そのため、最初のステップはMVVM化でなく 不要な依存の除去 から始めました。 以下の3段階で依存を剥がしました。 商品の情報表示において必要な情報を持つUIModelを作成 APIレスポンスをそのUIModelに変換するTranslatorを作成 Translatorは外部APIのレスポンス型をUIModelの型に変換する責務を持つ 外部APIの型定義が変更されてもViewModelやVCへ直接影響しない構造になる レガシーな型を使わない新しいセルを実装し、移行 最終的に MallHomeViewController からObjective-Cレガシー型への依存を完全に除去しました。 Step2: 最も独立性の高いAPIを対象に、ViewModel/UseCaseを部分導入する Step1でクリーンな基盤ができたため、いよいよMVVM化に着手します。設計計画で「最も独立性が高い」と判断した 世代別ランキングモジュール から始めました。 世代別ランキングモジュールとは、ユーザーが世代(~10代、20代など)を選択すると、その世代の人気アイテムがランキング形式で表示されるモジュールです。 ヘッダーの世代選択ボタンをタップして切り替えると、対応するランキングが再取得・再表示されます。 以下の特徴があったため、ホーム画面のMVVM化における最初のステップとして工数がかからず、アーキテクチャの全体像を実装しながら理解できる最適な題材と判断し、着手しました。 世代別ランキング専用の独立したAPIを持つ ユーザーが世代を選択したときだけ更新される 他のモジュールの更新と独立して動作する 小さく始めることの重要性 Step2は全部で7つのPRを作成しました。UseCase作成→UIModel作成→ViewModel作成→ViewControllerからUseCase/ViewModelへ処理を移動する流れで修正を加えていきました。 巨大なViewControllerを一気に書き換えようとすると、変更が大きくなりすぎてレビューが困難になり、バグ混入リスクも高まります。Step2でOpenした7つのPRのほとんどが100行未満のコード追加に収まっており、レビューでの指摘もほとんどなくスムーズにマージできました。 また、Step2を通して PRの分割方法 や 変更を加えるレイヤーの順番 が明確になり、次のステップであるモジュール更新全体のリアーキテクチャへの自信がつきました。大規模なリファクタリングに着手する際は、最も独立性の高い部分から始めることで、レビューでの問題検知やバグ混入の防止に直結します。最初の小さなステップを通じてPRの分割方法や変更を加えるレイヤーの順番を把握しておくと、後続の大きなステップをより自信を持って進められます。 Step3: MallHomeViewController全体にViewModel/UseCaseを導入する ホーム画面では、世代別ランキングモジュールの取得API以外に合計4つのAPIを並行して呼び出しています。Step3ではそれらの主要APIを呼び出している部分すべてにViewModel/UseCaseを導入しました。Step3はStep2のようにスムーズには行かず、いくつかの問題に直面しました。代表的な問題を紹介します。 問題1. Swift Concurrencyへの移行 当時の MallHomeViewController では、一部分のAPI呼び出しに BrightFutures を使っていました。このライブラリは2022年にEOLとなっており、チーム内でも新規実装では非推奨としていたため、このタイミングでSwift Concurrencyへ移行しました。 Swift Concurrency対応に関してもこのときが初めての経験で、その中で色々と学びがありました。 並行処理によるビュー表示時の表示順担保 クロージャベースのコードでは、複数のモジュール取得APIを直列で呼び出しており、すべてのレスポンスが揃ってから一括で描画していました。Swift Concurrencyへ移行して並行呼び出しにしたことで、どのAPIレスポンスが先に返ってくるかが不定になります。レスポンスを受け取った順にUIModelを積んでいく実装のままでは表示順が変わってしまいますが、実装当初はこの問題に気づいていませんでした。 UIModelの配列に常に決まった順序で格納する実装に修正することで解決しました。すべてのAPIレスポンスが揃ってから正しい順序でまとめて描画するという基本的な流れは変わらず、並行取得による速度改善と表示順の保証を両立しています。 withCheckedThrowingContinuation にキャンセルが伝播しなかった 特定のAPI呼び出しにはタイムアウト処理が必要でした。 withThrowingTaskGroup を使い、 データ取得タスク と 一定時間後にタイムアウトエラーを投げるタスク を並走させました。どちらかが完了したら group.cancelAll() でもう一方をキャンセルする実装を採用していました。 しかし実際にはキャンセルが正しく機能していませんでした。通信が切断された状態でリロードを繰り返すと、タイムアウトが発生して group.cancelAll() が呼び出されているにもかかわらず、ローディングが永遠に続く不具合が発生していました。 原因は、コールバック型のサードパーティSDKを withCheckedThrowingContinuation でブリッジしていた部分にありました。このSDKは通信切断時にコールバックを呼び出さない場合があります。タスクグループのキャンセルは withCheckedThrowingContinuation 内には自動で伝播しません。コールバックが呼ばれない限り、continuationは解決されないままとなります。 // 修正前: キャンセルが continuation に伝播しない func fetchData () async throws -> Response { try await withCheckedThrowingContinuation { continuation in legacySDK.fetch { result in // 通信切断時はここが呼ばれない場合がある // group.cancelAll() されても continuation は resolve されないまま continuation.resume(with : result ) } } } // 修正後: withTaskCancellationHandler を追加し、キャンセル時に continuation を resolve する func fetchData () async throws -> Response { let holder = ContinuationHolder() return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in holder.continuation = continuation legacySDK.fetch { result in holder.continuation?.resume(with : result ) } } } onCancel : { // タスクがキャンセルされたとき、onCancel でエラーを投げて continuation を解決する holder.continuation?.resume(throwing : APIError.cancelled ) } } 対応方法は withTaskCancellationHandler を追加することでした。タスクがキャンセルされると onCancel クロージャが呼ばれ、そこでcontinuationにエラーを投げることで、コールバックが返ってこない状態でもタスクを終了できます。continuationへの参照を class で保持しているのは、 onCancel クロージャが別コンテキストで実行されるためです。 var ではSwift Concurrencyの警告が出ます。 withCheckedThrowingContinuation はコールバック型APIを async/await に変換する手段として有効ですが、タスクキャンセルは自動では伝播しません。キャンセルに対応させるには withTaskCancellationHandler と組み合わせて、 onCancel 時に明示的にcontinuationを解決する必要があります。 問題2. モジュール構築メソッドの整理 Step3の終盤では、ViewControllerに置かれていた500行弱の巨大なモジュール構築メソッドを整理しました。 このメソッドには2つの責務が混在していました。 UICollectionViewに表示するデータソースの作成(VC側の責務) 商品押下時のログ送信に必要なモジュール内位置情報の計算(VM側の責務) 後者をViewModelへ移動し、各モジュールの同一性比較を可能な構造とすることで、位置情報を適切に取得できるようにしました。 やること自体は一文で書けるようにとてもシンプルなものです。実装当時の自分の認識も同様で、この整理に関してはスムーズに進み、そのままStep3をリリースしました。 しかし、ここで今回のリアーキテクチャにおける最大の壁にぶつかってしまいます。 Step3完了後にログに関するバグが発覚 Step3のリリース後、モジュールを管理しているチームから「カルーセルバナーのタップログで、バナーの位置が正常に送られていない」という問い合わせが届きました。 調査の結果、カルーセルバナーのタップ時のログに含まれる「バナーの位置」として、 ホーム画面全体におけるセクションの表示位置 を誤って送信していたことが判明しました。本来送るべき値は カルーセル内のバナーの位置(何枚目のバナーか) でした。 バグを引き起こした原因 モジュール/セクション/インデックスなどの位置に関する命名の曖昧さ ホーム画面は複数のコンテンツを縦に並べた構成です。「画面上の表示順(セクション位置)」と「各コンテンツ内の位置(インデックス)」という2種類の"位置"が存在しますが、コード上でこれらを区別する命名が不明確でした。 APIから取得したレスポンス名/ログ送信用パラメーター名/内部で使用している変数名のそれぞれの使い分けが曖昧なまま実装を積み重ねており、コードを読む際に混同しやすい状態でした。 ログの値の正しさをテストで検証できていなかった 「ログが送信されること」は手動確認で検証していましたが、「送信されたログの値の正しさ」まで検証できていませんでした。 当時はユニットテストが整備されていなかったため、コードレビューだけでは防ぎきれませんでした。ユニットテストがあれば、このバグはリリース前に検知できたはずです。 これらを検知できなかった背景として、Swift Concurrency対応での想定外の工数による焦りと、ログの重要度を甘く見積もっていたことが挙げられます。 Step3の終盤のPRはStep2とは打って変わって500行を超える大きなPRになってしまい、レビュアにも大きな負担をかけてしまいました。「小さく分割して進める」という当初の方針を貫けなかった点も反省の1つです。 Step3-ex: 命名整理とユニットテストの追加 バグを迅速に修正した後、Step3の延長として命名の整理とユニットテストを追加しました。 命名の整理 Step3でのバグ原因の1つが「ログ送信コードの読みにくさ」にあったため、まず命名を整理してからテストを書くという順序を選びました。 モジュール・セクション命名の統一 UICollectionView上の概念の呼び方と変数の型を整理し、「モジュール」と「セクション」の使い分けルールを明確にしました。 ログ送信の位置情報に関する命名統一 セクションの表示位置とセクション内の商品位置を表す変数名を、それぞれ明確に区別できる名前に統一しました。 ユニットテストの追加 バグを引き起こしてしまったログ送信時のセクション位置に関するテストをはじめとして、モジュールの取得、性別変更、画面遷移、ライフサイクルイベントなど多数のシナリオをカバーしました。Step2, Step3でUseCaseをプロトコルでDIできる構造になっていたため、Mockを使ったテストが書けるようになっています。 ユニットテストを新規で書いていくのも初めての経験だったため、テストに関する知識が豊富なチームメンバーにモブレビューを行ってもらいました。 命名整理とテスト追加を終えた時点で、MallHomeViewModelのテストカバレッジは38%から99%に向上しました。 不確かさに気づいた時点でテストを書く Step3では「アーキテクチャを整備してからテストを書けばいい」という考えからバグを引き起こしてしまい、その考えの危うさを実感しました。バグや不確かさに気づいたタイミングでテストを書くことで、結果的に次のステップを安心して進める力になります。 Step4: HomeViewControllerにViewModel/UseCaseを導入する 最終ステップのStep4ではホーム画面全体を管理している HomeViewController のリアーキテクチャを行いました。このステップでは、Step3までの失敗と学びを活かして TDD(テスト駆動開発) を採用しました。また、Step3でのPR分割の粒度ミスを踏まえ、レビューしやすい粒度でPRを作成しレビュアへの負担も考慮したPR戦略を取りました。 TDDによる設計の共有 Step4で特筆すべきは、 UseCase/ViewModelのテストケースをProtocol/実装より先に作成した ことです。UseCase/ViewModelのテスト雛形作成 → テストケースの作成 → UseCase/ViewModelとProtocolの作成 → 実装、という順番で進めました。 このTDDアプローチが特に威力を発揮したのが、 ログ送信周りの仕様整理 でした。 HomeViewController のログ送信ロジックは複雑で、起動経路(通常起動・プッシュ通知・Deeplink)やタブ切り替えに応じてどのログをどのタイミングで送るかが変わります。また、同じ画面遷移でも複数のライフサイクルイベントが連続して発火するため、ログの二重送信を防止する制御も必要です。このような仕様では実装者ごとに解釈が分かれやすく、Step3と同じ轍を踏む可能性もありました。 そこで実装に先立ち、起動経路ごとのログ送信フローをドキュメントとして整理し、 チームで仕様を合意した上でテストケースを設計する というプロセスを踏みました。ドキュメントには どの動線でどのログが何回送られるべきか を網羅的に記述し、それをそのままテストの仕様として共有しました。 テスト設計において重要な方針として、 内部のフラグ状態ではなくユーザーの動線単位でテストを記述する ことを採用しました。例えば以下のようなシナリオをそのままテスト名として記述しています。 通常のアプリ起動でホーム画面を表示したとき、ログが1度だけ送信されること プッシュ通知でアプリを起動したとき、特定のログは送信しないこと Deeplinkでホーム画面に遷移したとき、viewWillAppearでのログ送信はスキップすること 「どの動線で何が起きるべきか」という形でテストを書くことで、テストが仕様書として機能するようになります。内部実装がリファクタリングで変わっても、動線ベースのテストはそのまま維持できるため、保守性も高まりました。 テストを先に書くことで、「このUseCaseは何をすべきか」をチームで議論しながら設計を進めることができました。Step3でロジックの漏れがバグにつながったという反省が、ここで活きています。 オンボーディング周りの状態管理 HomeViewController は オンボーディング (初回起動時の案内フロー)周りの状態管理も複雑です。 リファクタリング前の課題 初めてZOZOTOWNアプリをインストールしたユーザーは、ホーム画面が表示されるまでに複数の案内画面を経由します。 問題は、この一連のフローを管理するために 5つ以上のBoolフラグ が複数のファイルにまたがって散在していたことでした。例えば「ログイン画面の表示が完了したか」「プッシュ通知許諾を表示したか」「訴求バナーの表示が必要か」といったフラグが各所に分散していました。それらを組み合わせた条件分岐によって次の表示内容が決まる構造になっていました。これにより、「今どのフラグがどの状態のとき何が起きるのか」を把握するだけでもかなりのコストがかかっていました。 このような複雑さが原因の1つとなり、オンボーディングに関する不具合が発生したこともありました。 ステートマシンによる再設計 Step4ではこのオンボーディングフローをステートマシンとして再設計しました。 オンボーディングは以下の4つの状態(State)と、それぞれを遷移させるイベント(Event)によってモデル化されます。 ViewModelはこの状態を購読し、状態に応じてどの画面を表示するかを宣言的に記述します。 この設計により、「現在のフローのどこにいるか」が状態として一点に集約され、遷移のトリガーとなるイベントも明示的になりました。それまでのフラグの組み合わせによる暗黙的な状態管理から脱却し、コードを読むだけでオンボーディングフローの全体像が把握できるようになりました。 また、「どのイベントでどの状態に遷移するか」をテストで直接検証できるようになりました。将来的にオンボーディングのステップが追加・変更されても、状態遷移の定義を修正するだけで対応できます。 こうして、約1年5か月にわたるホーム画面リアーキテクチャが完了しました。Step4に関しては、ホーム画面に起因する障害や問い合わせは発生しませんでした。 Step3で体験したバグと、その後段階的に整備したテストが、実際の品質保証として機能している結果だと感じています。 ホーム画面リアーキテクチャ完了後、後続案件でホーム画面を触った他のメンバーから「実装が楽になった」というフィードバックをもらいました。これは、責務が適切に分割されたことで改修の影響範囲が把握しやすくなったことを示しています。 また、ログ周りの修正が入ったときも「テストで挙動が担保できるようになった」という声がありました。Step3で体験したバグに対して、Step3-ex以降で構築したテストが実際に機能している瞬間でした。 長期リファクタリングを進める上でのポイント 今回のリアーキテクチャを通しての学びやポイントは各ステップで紹介しましたが、全体を通じて特に重要だと感じた点として、 設計ドキュメントの継続的な整備 を挙げます。 設計計画(段階的なステップ計画、インタフェース設計)を文書化しておくことは、長期にわたるプロジェクトをチームで共有する土台になります。「なぜこの設計にしたか」が残っていることで、後続のステップでも一貫した判断ができます。また、AIを活用したコーディングが一般的になった現在では、設計方針が文書化されていることはより一層重要です。AIへの指示の精度が上がるだけでなく、生成されたコードがプロジェクトの設計意図と一致しているかの検証にも役立ちます。 おわりに このリアーキテクチャを振り返ると、最初は「アーキテクチャについての理解を深めたい」という動機から始まりました。しかし実際には、「テストの重要性」「段階的な変更の価値」「失敗を次に活かすこと」という、より本質的なことを学んだプロジェクトになりました。 特に、Step3後のバグ発覚→Step3-exのテスト追加→Step4でのTDD採用でバグ0を達成できたことは、自分の成長を強く実感できたポイントでした。 ZOZOTOWN iOSアプリのリアーキテクチャはまだ続いています。このホーム画面での経験をチームの資産として積み上げながら、より良いアプリを作り続けていきたいと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、データ基盤ブロックの平本( @cisetn )です。 本記事では、ZOZOTOWNのリアルタイムデータ連携基盤の中核である ETL層 を作り直した事例を紹介します。対象はオンプレミスのSQL ServerからBigQueryへリアルタイムにデータを連携する基盤です。そのETL層を Goで実装したプラグイン (実行基盤はFluent Bit)で再設計しました。 ZOZOのリアルタイム連携基盤は2020年に 一度紹介記事を公開しています が、それ以降、段階的にアーキテクチャを見直してきました。本記事はその中でもETL層の再設計にフォーカスします。 想定読者は、リアルタイム連携基盤やストリーミング処理基盤の設計・運用に関わる方です。 本記事で扱うこと、扱わないことは次のとおりです。 扱う :ZOZOのリアルタイム連携の全体像、今回リプレイスした基盤の背景・設計・実装 扱わない :BigQuery側のテーブル設計、SQL Server側のChange Tracking設定、利用側(BI・分析クエリ等) 目次 はじめに 目次 ZOZOのリアルタイムデータ連携の全体像 これまでの変遷 リプレイスに至った背景 顕在化してきた課題 新基盤アーキテクチャ 設計の軸 技術選定:Fluent Bit + Goプラグイン 全体構成 大量のデータをリアルタイムで捌くために考えたこと 新基盤の構成 INPUT内部:取得とエンコードを分けた OUTPUT内部:送信とACK確認を分けた 結果 今後の展望:Change Data Captureへの移行 まとめ ZOZOのリアルタイムデータ連携の全体像 本題の前に、ZOZOにおけるリアルタイム連携の全体像を軽く俯瞰しておきます。本記事のテーマがあくまで「その中のひとつ」であることを共有するためです。 ZOZOではデータソースが多岐にわたります。オンプレミスのものもあれば、クラウド上のものもあり、MySQL、SQL Server、DynamoDBなどさまざまです。当然、差分を検知する手段もソースに応じて変わりますし、連携の実現方式も1つではありません。 マネージド / SaaSで済むケース :例えばMySQL → BigQueryであれば Datastream を利用する 専用のパイプラインを組む必要があるケース :例えばDynamoDB → BigQueryのように、対応するマネージドサービスがない場合は、別途データ連携のパイプラインを構築する必要がある 結果として、ZOZOのリアルタイム連携基盤は 複数系統に分かれて共存 しています。本記事で扱うのは、そのうち オンプレ SQL Server → BigQuery の系統です。本番環境(prd)で 約400のテーブル を連携対象としており、新規の連携依頼も日々発生するため、データ基盤の運用において比重の大きな系統となっています。SQL ServerのChange Tracking機能で変更を検知し、プラグインで取得したレコードをPub/Sub経由でBigQueryに流しています。 これまでの変遷 実は、本記事で扱う系統は今回が初めてのリプレイスではありません。以下の変遷を経ています。 時期 アーキテクチャ 主目的 2020 Qlik Replicate → fluentd + Dataflow → BigQuery 安定性向上 + コスト削減 2024 fluentd + BigQuery Subscription (Dataflow を廃止) コスト削減 2025 プラグインによる ETL 層の再設計 + BigQuery Subscription 効率改善(メモリ・スループット・コスト) 2024年には、ストリーム処理層のDataflowを廃止し、Pub/SubのBigQuery Subscriptionに置き換えるリプレイスが行われました。このフェーズの主目的はコスト削減です。 そして今回、ETL層をプラグインで再設計したのが本記事のテーマです。詳細な背景と目標は次章で述べますが、結果として、コスト削減・メモリ効率の改善・スループット向上・運用課題の解消といった効果につながりました(数値は末尾)。 リプレイスに至った背景 誤解のないよう先に述べておくと、旧基盤の設計が「悪かった」わけではありません。2020年当時、ZOZOのデータ基盤はまさに拡大していくフェーズにあり、リアルタイム連携の需要も増え始めたばかりでした。そうした状況では、プラグインが豊富なfluentdとDataflowのように既存のツールを組み合わせて素早く構築できる構成は合理的な選択だったかと思います。実際、信頼性(データ欠損が起きないこと)は チェックポイント機構 などによって担保できており、長く運用されてきました。チェックポイント機構は、処理済みのChange TrackingバージョンをBigQueryに保持する仕組みです。Pod再起動時はそこから再開できます。 顕在化してきた課題 一方で、運用を続け、データ量や利用要件が増えていく中で、 効率の側面 でいくつかの課題が徐々に顕在化してきました。 メモリ効率 :結果セットを一括でメモリに載せる実装のため、メモリ使用量がデータ量に比例して増加する構造でした。大量更新時のOOMを避けるためには「ピーク時のデータ量」を見越した大きなメモリを常時確保しておく必要があり、データ量が増えるにつれてリソース見積もりの難しさが目立つようになってきました。 コスト :上記のメモリ確保がそのままコストに直結します。メモリがトランザクション単位のデータ量に比例する構造であるかぎり、「ピーク時のデータ量」の見積もりを下回るとOOM直行となります。そのため運用上の工夫(時間帯別のスケーリング等)では本質的な改善が難しく、リソースの常時確保によるコスト増を抱え続けるしかありませんでした。 性能 :逐次処理ベースの実装のため、1トランザクションあたりの規模が大きいテーブルでは、リアルタイム性を保ちにくい場面もありました。 運用 :依存していたコンテナイメージがEOLを迎えており、継続利用にリスクがありました。加えて、内部状態の可視性が低く、障害発生時の原因特定にも時間がかかる状況でした。 一言でまとめると、各所でガタが出始めており、信頼性を維持したまま効率(メモリ・スループット・コスト)の側面を改善するため、リプレイスを検討するタイミングに来ていた、ということです。 新基盤アーキテクチャ 設計の軸 新基盤の設計指針はシンプルで、 キャパシティプランニングの軸を「ピーク時のデータ量」から「単位時間あたりの処理量」に変える ことに尽きます。信頼性(データ欠損が起きないこと)は旧基盤からチェックポイント機構によって担保されており、新基盤でもそのまま引き継いでいます。そのため本記事のテーマは 信頼性を維持したまま、効率(メモリ・スループット・コスト)をどう改善したか です。 技術選定:Fluent Bit + Goプラグイン 今回のリプレイスは、前フェーズ(2024年のDataflow撤廃 + BigQuery Subscriptionへの切り替え)の延長線上にあります。前フェーズで Dataflow関連の費用がまるごと不要になり大きなコスト削減は既に達成済み で、下流(Pub/Sub HubとBigQuery Subscription)も整理されている状態でした。一方でETL層はfluentdベースのまま残っており、メモリ効率とスループットの面で課題が顕在化していたため、今回はその続きとして ETL 層の中身を作り直す ことにしました。下流はそのまま踏襲し、ソース側(Change Tracking設定)にも手を加えません。 このスコープと、既存のPub/Sub Hub構成・BigQueryテーブル設計を維持する制約のもとで、マネージドCDCサービスやOSSのCDCミドルウェアの活用も検討しました。ただし我々のケースでは、既存テーブル設計とPub/Sub Hubへの直接出力をそのまま組み合わせ続けられる選択肢を見つけられず、プラグインとして実装する形に決めました。 採用したのは Fluent Bit + Goプラグイン です。決め手は次のとおりでした。 既存基盤がfluentdベースで運用されていたため、Fluent Bitへの移行が素直 :プラグインモデル・設定構造・デプロイ手順といった運用ノウハウがそのまま活きる INPUT(Change Tracking取得)とOUTPUT(Pub/Sub送信)の挙動を 自分たちで細かく調整できる 。後述の非同期ACK並列確認のような最適化も、プラグインとして自前で書いているからこそ仕込める Fluent BitのBuffer・バックプレッシャー機構をそのまま活用できる Goプラグイン公式サポートにより、後述する並列処理をgoroutineとchannelで素直に書ける 全体構成 以下の図は主要コンポーネントのみを示した簡略図です。 ETL層(Fluent Bit + Goプラグイン)はGKE上で動作します。プラグインは データ取得(INPUT) と Pub/Subへの送信(OUTPUT) の2つで構成されており、それぞれの実装の詳細は次章で扱います。 大量のデータをリアルタイムで捌くために考えたこと 新基盤の設計で常に意識していたのは、「 大量のデータをいかにリアルタイムで捌くか 」という問いでした。データ量が増えてもパイプラインが詰まらず、メモリ消費がデータ量に比例しない構造をどう実装するかを検討しました。前章で述べた「単位時間あたりの処理量を軸にする」方針を、Fluent Bitのパイプライン上に乗せて具体化していった話を、本章で紹介します。 なお、Fluent Bitのパイプライン構造の全体像については、 公式ドキュメント もあわせてご覧ください。 新基盤の構成 Fluent Bitのパイプライン構造はINPUT → Filter → Buffer → Router → OUTPUTという形です。新基盤ではこのうち INPUTとOUTPUTをGoプラグインで実装 しました。チャンク単位の処理やバックプレッシャーといったBuffer周りの機構はFluent Bit Engineが標準で備えています。そのためプラグイン側は INPUTとOUTPUTの"箱の中"の設計に集中できました 。 設計の出発点として、データ取得から送信までの各処理を「どこがボトルネックになるか」で整理し、並列化方針を決めました。 処理 特性 並列化方針 CT取得(クエリ → カーソル) I/O bound(DB側) 単一スレッド(DBがボトルネック) エンコード CPU bound Worker数で並列化 Pub/Sub Publish I/O bound(NW) 非同期APIで並列化 ACK確認 I/O bound(NW待ち) 別Workerプールで並列化 CPU boundとI/O boundを別レーンに分け、それぞれを独立した並列度で動かす設計です。以下、INPUT内部・OUTPUT内部の順で紹介します。 INPUT内部:取得とエンコードを分けた INPUT内部の設計では、メモリとCPUを独立した軸として扱えるようにしました。 メモリの設計 :結果セット全体を展開せず、 カーソルで小分けに読み進める方式 を採用。1回のクエリで読むレコード数 RecordsPerChunk をプラグインの設定で指定でき、本番では 10,000件/チャンク CPUの設計 :取得処理とエンコード処理を 別レーンに分け 、エンコードは複数のWorkerで並列実行 取得とエンコードの間に 中間キュー(jobs queue) を挟むことで、取得側はエンコードの完了を待たずに次のチャンクを先行投入できます。キュー容量がゼロだと直列に戻ってしまうため、本実装では jobs queue の容量をWorker数の5倍 に設定しています。 この構造のもとで、同時にレコード形式でメモリに乗るチャンク数は NumWorkers × 6 個で頭打ちになります。内訳は「jobs queue上の最大 NumWorkers × 5 個 + 各Workerが処理中の1個」です。 同時メモリ上のレコード数 = RecordsPerChunk × (jobs queue + 処理中 Worker) = RecordsPerChunk × (NumWorkers × 5 + NumWorkers) = RecordsPerChunk × NumWorkers × 6 = 10,000 × NumWorkers × 6 例えばNumWorkers = 2なら、データ量に関わらず常に約12万レコード分のメモリしか確保しなくて済みます。100万件規模のトランザクションが流れてきても、結果セット全体を一括ロードしてしまう旧基盤と違ってOOMにはなりません。 なお、Fluent Bit上でカーソル方式を実装するときには工夫が必要でした。Fluent BitはINPUTに対して定期的に「データをちょうだい」と呼び出してくる構造になっており、素朴に書くと毎回新規にクエリを発行してしまいます。それでは結果セットが毎回頭から読み直されてしまうため、 カーソル状態をプラグイン側に持ち越し、呼び出しごとに「続きから」読み進める ようにしました。 OUTPUT内部:送信とACK確認を分けた OUTPUT内部では、 送信処理とACK確認処理を別レーンに分離 しました。Pub/SubのPublishは同期的に書くと「送信 → ACK待ち → 次へ」と直列化してしまい、ACK待ちのネットワークI/Oが支配的になります。これだとスループットがACKレイテンシに律速されてしまうため、両者を分離して並列化する方針を取りました。 送信側 :非同期APIを呼んで即座にFuture相当の結果を受け取り、次へ進む。送信そのものは止まらない 確認側 :受け取ったFutureのACK確認専用のWorkerプールを設け、複数並列で確認する 各メッセージが独立したACKタイムアウトを持つようになり、1件の遅延が後続全体を巻き込む連鎖タイムアウトを構造的に防げるようになりました。 このパターンはPub/Subに限らず、Future / Promiseを返す非同期メッセージングSDKで同様に当てはまる考え方です。 送信そのものではなく、ACK確認の方をスケールさせる という発想を、我々のケースでは設計時に組み込みました。 なお、下流の詰まりに対する保護(バックプレッシャー)はFluent Bit標準の機構が動いており、OUTPUT側で詰まったときにINPUTを自動で止める仕組みが標準で得られています。これがあるおかげで、プラグイン側は「並列にどんどん投げて確認する」シンプルな構造に保てました。 結果 前章で述べたカーソル方式により、メモリ消費はデータ量に依存しなくなりました。prd環境では、ETL Podを載せているGKEクラスタのTotal Memoryが 約240GiBから約40GiBへ、約1/6にまで縮小 し、ETLのGKEコストは約 -66% 下がりました。 環境 リプレイス前 リプレイス後 削減率 prd $2,800 $940 -66% stg $3,200 $1,100 -67% 合計 $6,000 $2,000 -67% (2025年11月実績、ETLのGKEコストのみ・定価ベース) 注:stgはprdよりテーブル数が多く(stgは約500、prdは約400)、絶対額も大きくなっています。 性能面では、逐次処理からWorkerプールによる並列処理へ切り替えました。Worker数を変えるだけでスループットの線形拡張が可能な構造になりました。旧基盤では一部の大規模テーブルで遅延が長くなりやすく、監視の閾値を最大40分まで緩めて運用していました。新基盤では、全テーブル一律10分以内の閾値で安定処理しています。 運用面では、Fluent Bit標準のメトリクスにより内部状態が可視化されました。 fluentbit_input_records_total や fluentbit_output_retries_total などの指標を、GKEのMetrics Explorerから確認できます。実際、リプレイス後に予期せぬ問題が起きた際も、 fluentbit_output_retries_total の急増から原因を切り分けてデバッグできました。また、プラグインを自前で実装しているため、コアな部分まで踏み込んだ調査・修正も可能です。依存していたコンテナイメージのEOLリスクから解放された点も、得られた効果です。 今後の展望:Change Data Captureへの移行 現在はSQL Serverの Change Tracking (CT) を使っていますが、CTは「その行が変わった」ことは検知できても、変更前後の値や中間の変更履歴までは取得できません。 一方、SQL Serverには Change Data Capture (CDC) という、変更の全履歴を捕捉する機能もあります。今後はこのCTからCDCへの移行を視野に入れています。履歴を全て取得できれば、変更前後の差分分析や任意時点の状態再現など、分析側のユースケースを広げられます。 まとめ 本記事では、ZOZOTOWNのリアルタイムデータ連携基盤のETL層を、Fluent Bit + Goプラグインで作り直した事例を紹介しました。リアルタイムデータ連携基盤の設計や運用に取り組む方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
2026年4月22日〜24日に開催された Google Cloud Next '26 へ参加してきました。昨年に引き続きアメリカ・ラスベガスで開催され、弊社からはMA部の平井・林・木野、AI事業戦略部の川田・桜井の5名が参加しました。なお、昨年参加した様子は以下の記事で紹介しています。 techblog.zozo.com 今年はAIエージェントを『実戦』に投入し、いかに賢く、安全に使うのかに焦点を当てたセッションが多い印象でした。本記事では、現地での様子と特に興味深かったセッションをピックアップして紹介します。 また、Recapのオンラインイベント「 Google Cloud Next 2026 Recap in ZOZO 」を2026年5月18日に開催しました。このイベントでは、Google Cloud Next '26について、今回のテックブログで紹介できなかった内容など、より詳細に共有しております。 現地の様子 私たちは会期の前々日にラスベガスの空港に到着したのですが、空港内にはさっそくGoogle Cloud Nextの広告が流れており、イベントに向けた熱意が一気に高まりました。 Google Cloud Nextの広告を見かけたハリー・リード国際空港の様子 昨年に引き続き会場は、ラスベガスのマンダレイ・ベイホテル コンベンションセンター。非常に盛り上がっており、特に各セッションや展示ブースでのデモでは参加者から活発な質問が飛び交っていたのがとても印象的でした。 熱気に包まれる会場内の様子 以降では、現地に参加したメンバーが気になったセッションを紹介します。 セッション紹介 What's new in Cloud Run こんにちは、MA部配信基盤ブロックの木野です。私は通知系(LINE/Mail/アプリ)の開発をしています。 公開資料「 What's new in Cloud Run 」のP.1より引用 このセッションでは、Cloud Runが単なるWebアプリのデプロイ先ではなく、より幅広いワークロードを受ける汎用実行基盤へ広がっていることが紹介されていました。セッション全体のメッセージは、Cloud Runが「on-demand compute for everyone」であるという点に集約されており、Vibe Coded Apps、AI Agents、AI Models、Large Scale Appsという4つの観点から新機能が説明されていました。 冒頭では、AI Studioで生成したマルチプレイヤーゲームをそのままCloud Runに公開するデモが紹介されており、Cloud Runが「作ったものをすぐにクラウドへ出す」ための基盤として強く打ち出されていました。また、Cloud Run公式のFully managed MCP Serverも発表されており、人間が操作する実行基盤というだけでなく、AIエージェントから直接デプロイや管理の対象になる基盤へ寄ってきていることも印象的でした。 GA対応したCloud Run Worker Pools 私が特に興味を持ったのは、Cloud Run worker poolsのGAです。Worker poolsは、HTTPリクエストを受けることが本質ではない常駐workerやpull consumer、runnerのような処理に対して、Cloud Run上のより自然な置き場を与える機能だと感じました。 Cloud RunにはこれまでもServiceやJobがありましたが、Serviceはrequest-driven、Jobはrun-to-completionであり、そのどちらにもきれいに当てはまらない処理を表現しづらい場面がありました。セッションでも、Temporalのworkerのようなlong polling前提の処理がworker poolsに適している例が紹介されていました。 この点は、私たちの配信基盤にもそのままつながります。例えばPub/Subのpull consumerや、ループし続ける常駐worker、定期的に状態を見て後続処理を進めるfinalizerのような処理は、実態としてはHTTPエンドポイントを持つことが本質ではありません。それにもかかわらず、これまではCloud Run Serviceの形に寄せるためにヘルスチェックや待受用のコードを持たせていました。worker poolsが一般提供されたことで、こうした処理をより素直な形で実装でき、配信基盤の見通しや運用性を改善できる可能性があります。 Cloud Run Instancesとbuilt-in dev loop 公開資料「 What's new in Cloud Run 」のP.30より引用 もう1つ興味深かったのが、Cloud Run Instancesとbuilt-in dev loopの流れです。セッションでは「ローカルでクラウドをエミュレートしようと頑張るのではなく、Cloud Run上でそのまま開発する」というメッセージが明確に打ち出されていました。ローカルの変更をCloud Run instanceに同期し、そのままdev scriptをクラウド側で実行することで、pushしてデプロイを待つ前に即時検証できる世界観が示されていました。さらに、SSH supportも合わせて紹介されており、Cloud Runを本番の実行基盤として使うだけでなく、開発や調査の場としても扱う方向性が見えてきたと感じました。 これは、複数サービスをまたぐ検証が多い配信基盤の開発体験にとって特に大きい変化だと思います。現在でもローカルでの統合テストやcontainer_testのような仕組みは有効ですが、実サービス依存に近い確認をしたい場合は、どうしてもdev環境への反映待ちや、共有環境ゆえの状態差分が問題になります。もしbuilt-in dev loopが成熟すれば、各開発者が自分の変更をCloud Run側へすぐに反映し、実サービス依存に近い状態で軽く検証を回せるようになります。さらに、人間が行う確認フローとPR後のE2EやCIの構成も近づけられる可能性があり、複数サービスをまたぐ開発・検証体験を大きく変えるアップデートだと感じました。 加えて、このセッションはCloud Runの新機能を個別に列挙するだけでなく、「Cloud Runはどこまで守備範囲を広げようとしているのか」という観点で見ると、とても示唆が多い内容でした。これまではHTTPサービスをスケールさせるためのプロダクトという見方が中心だったと思いますが、今回の発表では、AIエージェントの実行基盤、長時間動くworkerの置き場、さらにはCloud Run上での開発ループまで含めて整理されていました。配信基盤のように非同期処理、複数サービス連携、運用時の可観測性が重要なシステムにとっては、単なる機能の追加以上に、Cloud Runをどう使うかの前提そのものが変わり始めていると感じています。 セッションを通しての感想 Cloud Runは長らく「HTTPサービスを手軽に動かす場所」という印象が強かったのですが、今回のセッションを通して、AIエージェント、常駐worker、開発ループまで含めたより広い実行基盤へ進化していることがよく分かりました。特に私たちのように、非同期処理や複数サービス連携を多く持つシステムにとっては、今後の設計や検証フローを見直すきっかけになるセッションでした。 What's new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds こんにちは、MA部MAシステム開発ブロックの平井です。私は自社マーケティングシステム「ZMP」の開発をしています。ZMPではユーザー毎に最適化された情報を配信するパーソナライズ配信機能があり、そのデータベースとしてGoogle CloudのAlloyDBを利用しています。そこで、私はAlloyDBに関するセッションを聴講しました。 「What's new in AlloyDB」セッション会場の様子 このセッションでは、AlloyDBのアップデートをエンタープライズ・分析機能の観点、AI関連機能の観点から説明していました。 エンタープライズ・分析機能に関するアップデート Hot Standby 公開資料「 What’s new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds 」のP.8より引用 Hot Standbyは、スタンバイ中のノードがWALを受け続けながらアクティブなインスタンスとして動く機能です。この機能によって、起動時間の短縮とプライマリー昇格の加速によるRTOの改善、メモリーキャッシュの暖気とフェイルオーバー後のパフォーマンス低下の抑制により一貫したパフォーマンスの維持が可能になります。 Read Pool Autoscaling 公開資料「 What’s new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds 」のP.9より引用 Read Pool Autoscalingは、読み取りインスタンスがワークロードに応じて自動でスケーリングする機能です。また、事前に決められたスケジュールでスケーリングすることも可能です。例えばサイバーマンデーやブラックフライデーなどあらかじめ高負荷が予想される場合にとても有効です。私たちのパーソナライズ配信システムでも読み取りインスタンスを利用していて、負荷がスパイクする傾向があるため、Read Pool Autoscalingが一般提供された際は、その効果を速やかに検証したいと考えています。 Transparent Query Forwarding 公開資料「 What’s new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds 」のP.11より引用 Transparent Query Forwardingは、プライマリーノードで受け付けた読み取りクエリを読み取りノードにフォワードする機能です。読み取りノードにクエリをフォワードすることでプライマリーノードの負荷を軽減し、クラスター全体のリソースを有効活用するために設計されました。アプリケーション側で必要だったライブラリを利用したプライマリノードと読み取りノードのコネクションの作成/クエリフォワード設定が不要になります。また、書き込みと読み込みの一貫性を担保しているため、アプリケーション側で古い情報を参照する心配がありません。 LakeHouse Federation for AlloyDB 公開資料「 What’s new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds 」のP.16より引用 Lakehouse Federation for AlloyDBは、AlloyDBからBigQueryやIcebergにあるデータを簡単にクエリできる機能です。AlloyDB上のトランザクションデータとBigQuery上の履歴データ、集計データを結合することで統合的な分析が可能になります。 私たちのパーソナライズ配信システムでは、BigQuery上の集計データをAlloyDBにロードして、配信処理に利用しています。Lakehouse Federation for AlloyDBを経由したBigQueryのクエリ時には、コンピューティングの料金が発生するためリアルタイムでの利用は難しいですが、BigQueryを利用した集計データをAlloyDBにロードする処理をより簡素化が可能です。セッションではAlloyDB上のリアルタイムなデータとLakehouse上の履歴データを利用して、実績を比較する例が紹介されていました。 以下の画像は、AlloyDBとLakehouseがシームレスに連携することで、運用と分析の統合的なプラットフォームとして活用できることを表現した図です。AlloyDBからLakehouseへはDatastream機能が提供されていて、LakehouseからAlloyDBへもReverse ETL機能が提供されています。相互のデータ連携機能が提供される一方で、データ連携せずに統合的なデータアクセスを実現する手法として、Lakehouse Federation for AlloyDBが紹介されていました。 公開資料「 What’s new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds 」のP.15より引用 AI関連機能のアップデート AI関連機能で紹介されていた内容は以下になります。 ベクトル検索の改善 公開資料「 What’s new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds 」のP.30より引用 AlloyDB開発チームはベクトル検索を今後の検索機能における中核と位置付け、パフォーマンス向上に注力してきました。Google researchが開発したScaNNでは数百億のベクトルまで拡張でき、高速なクエリパフォーマンスとインデックス構築を実現しています。また、業界標準のHNSWのパフォーマンス向上にも取り組んでいて、オープンソースのpgvectorと比較して最大4倍高速な検索を実現できるそうです。 ハイブリッド検索 公開資料「 What’s new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds 」のP.31より引用 ハイブリッド検索は完全一致を行うためのキーワード検索とセマンティックな検索を行うためのベクトル検索を統合した高度な検索機能です。この検索により固有名詞や型番などは確実にヒットさせつつ、曖昧な表現を含んだ単語でも関連性の高い結果を取得できます。既存のキーワード検索においても、RUM拡張のサポートによるパフォーマンス改善に加え、BM25アルゴリズムの活用によって検索精度の向上を実現しています。キーワード検索自体の機能改善による、それをベースとしたハイブリット検索の精度とパフォーマンスの向上にも取り組んでいるそうです。 Geminiを利用したAI関数の拡張 公開資料「 What’s new in AlloyDB: Scale PostgreSQL for agentic AI and hybrid clouds 」のP.32より引用 ai.if、ai.rank、ai.generateといった関数を活用することで、LLM(大規模言語モデル)の強力な機能をクエリインタフェース上で直接享受できます。例えば、ai.rank関数では、上記画像にあるような「サントリーニ島での夏休暇用のシャツ」を検索する場合、クエリ結果がGeminiに送信され、Geminiが現実世界の情報を加味して最適にソートした結果を返してくれます。 セッション全体に対する感想 AlloyDBがAIエージェント時代のデータベースとして選択されるために単なるデータ蓄積を超えた様々な新機能を開発していることが印象的でした。システムの可用性と信頼性を担保するためのエンタープライス機能、データ分析の基盤として活用するための分析機能、AIのパワーを活用したAI機能と今回紹介しただけでも様々な領域における機能が紹介されていて、データベースに求められている要件が非常に多岐に渡っていることを感じました。 また、私たちのパーソナライズ配信システムではAlloyDBでのベクトル検索機能を利用できておらず、AlloyDB開発チームがベクトル検索に投資している点からも、利用できるユースケースがないか探してみようと思います。 Generative UI for any agent, anywhere: A2UI, AG-UI, MCP Apps, and more こんにちは、MA部MAシステム開発ブロックの林です。私は自社マーケティングシステム「ZMP」の管理画面をフロントエンド・バックエンドを横断して開発しています。 現在のAIとの対話はテキストベースが主流ですが、テキストだけではユーザー体験として不十分なケースが多くあります。Generative UIとは、AIエージェントがユーザーに合わせたインタフェース(UI)を動的に構成するための手法です。ここでは、Generative UI関連のセッションで紹介された技術と、現地で体験したデモについてレポートします。 公開資料「 Generative UI for any agent, anywhere: A2UI, AG-UI, MCP Apps, and more 」のP.1より引用 MCP Apps まず紹介されたのが、Anthropic社が提唱するModel Context Protocol(MCP)の公式拡張「MCP Apps」です。従来のMCPがテキストやデータを返すのに対し、MCP AppsではエージェントがインタラクティブなUIを返します。UIは会話の中に直接埋め込まれ、ユーザーはチャットの流れから離れることなくアプリを利用できます。ChatGPT・Claude・Geminiなど主要なホストがすでに対応しています。 MCP Appsのスピーカーは、UIの形式を大きく3種類に分類して説明していました。 Predefined UI (事前定義):決まったフォーマットのUIを返す Declarative UI (宣言的):コンポーネント構造をJSONで指定してUIを組み立てる Generative UI (生成的):ゼロからUIをその場で生成する MCP Appsはどの形式にも依存しない(agnostic)設計のため、後述するA2UIやAG-UIとも連携できます。A2UIやAG-UIが生成したUIをMCP Appの中でレンダリングしたり、逆にMCP Apps自体をA2UIやAG-UIでレンダリングしたりするコンポーネントとして扱うことも可能です。 A2UI 次に、Google社が提唱するエージェント駆動型インタフェース向けの「宣言型UIプロトコル」であるA2UIが紹介されました。 A2UIでは、あらかじめコンポーネントのカタログを定義しておき、エージェントはそこから選ぶ形でUIを組み立てます。エージェントが送信するのはJSONによるコンポーネント構造とデータのみで、実際の描画は自前のデザインシステム(ReactやFlutterなど)が行います。A2UI標準のBasic Catalogもデザインシステムとして利用できます。 コンポーネントを自前で管理する構造はセキュリティ上の利点もあります。たとえば「クリックターゲット内に隠しフォームを埋め込むUI生成」といった攻撃も、定義済みコンポーネントのみを使う構成であれば防御できます。 また、トランスポート不問のため、AG-UIやMCPと組み合わせることも可能です。 A2UIはすでにGemini Enterpriseにてプレビューで提供が開始されています。 AG-UI そして、CopilotKit社が提唱する「エージェントとフロントエンドの接続を標準化するプロトコル」として、AG-UIが紹介されました。MCPがツールとの接続、A2Aが他のエージェントとの接続を担うのに対し、AG-UIはユーザー向けフロントエンドとの接続を担う位置づけです。 AG-UIのスピーカーは、Generative UIの手法は制御の度合いによってグラデーションがあると説明していました。 Controlled :アプリ側が厳密に制御するUI Declarative :JSONなどで宣言的に構成するUI A2UIはここに位置づけられる Open-ended :AIが自由に生成するUI MCP Appsはここに含まれる手法の1つ AG-UIはこのグラデーション全体をカバーし、ユースケースに応じて各技術と連携・使い分けができる設計になっています。 AKQA社の事例紹介 本パートでは、ブランド企業がGenerative UIに取り組むべき理由について説明がありました。従来のWebサイトはナビゲーション中心のユーザー体験に最適化されています。しかし現在では、多くのユーザーが事前にChatGPTやGeminiで情報収集してサイトを訪れるため、必要な情報が見つからなければ再びAIに戻ってしまいます。そのため、ユーザーの意図に直接応答できる仕組みの重要性が高まっています。 デモでは、ユーザーの意図を機能的・感情的・社会的という3つの側面に分解するアプローチが紹介されました。たとえば「コンバージョンを落とさずに不正検知を改善したい」といった入力から、ユーザーの緊急度や心理的背景を推定し、それに応じたページを動的に生成します。従来は人手でPDF資料を作成しており1件あたり6時間以上かかっていた作業が、この仕組みによってWeb上では約10秒で完了するようになったと説明されていました。 GenLatte Generative UIが組み込まれたラテアート注文アプリを「GenLatte」ブースにて実際に体験できました。ラテアートのデザインをリテイクする指示を出したところ、AIから私専用の追加質問がいくつか投げられました。質問は単一選択のもの、スライダーで微調整するもの、テキスト入力のものなど複数パターンがありましたが、どれも私のラテアートの内容に応じた質問で、人間が答えやすい形式のUIとして提示されました。生成されたラテは実際に飲むことができ、本当にお店でラテを注文しているようでした。Generative UIの可能性を実感できるブースでした。 公開資料「 Personalize the user experience with generative UI 」のP.19より引用 Generative UIが組み込まれたラテアート注文アプリを体験できる「GenLatte」ブース(左)と生成されたラテ(右) 自社での活用の可能性 今回のセッション・デモを通じて、Generative UIは自社でも応用可能だと感じました。 現在の自社マーケティングシステムは、マーケターがSQLを直接書いてセグメントを作成することを前提に設計されています。しかし、すべてのマーケターがSQLを書けるわけではないこと、「すでにあるSQLを元に年齢で配信を出し分けたい」といった軽微な修正であっても毎回SQLを書く必要があることに対して改善を求める声がありました。 一方で、エンジニア側も対応に十分な工数を割けていない状況です。過去にはUI上で条件を絞り込める機能を開発したこともありましたが、後から追加になった絞り込み条件などマーケターの要望に追従できず、利用されづらい状態になっていました。 こうした課題に対して、AIによるSQL生成とGenerative UIを組み合わせるアプローチが有効だと考えています。具体的には、以下のような流れです。 マーケターが自然言語でセグメントの条件を入力する AIがSQLを生成する 生成されたSQLの実行結果(セグメント数)や、既存SQLとの差分などの情報を、Generative UIで動的に構成されたダッシュボードとしてマーケターに提示する このような仕組みが実現すれば、セグメント作成をマーケター完結で迅速かつ柔軟に行えるようになります。結果として、マーケターの作業工数だけでなく、エンジニアへの問い合わせ・対応コストの削減にもつながると考えています。 What's new in Google Cloud's agent platform こんにちは、AI事業戦略部AIソリューション開発ブロックの桜井です。私は社内の業務・事業へAIをどのように組み込み、継続的に価値を出せる形へ育てていくかに関心を持って開発・検証に取り組んでいます。 公開資料「 What's new in Google Cloud's agent platform 」のP.1より引用 このセッションでは、Google Cloud上でAIエージェントを構築し、本番業務に展開していくためのAgent Platformのアップデートが紹介されました。これまでVertex AIとして提供されてきた機能が「Gemini Enterprise Agent Platform」に統合され、Agent時代における新しいインフラ環境として生まれ変わりました。 セッションの冒頭で印象的だったのは、AIエージェントの位置づけが「チャットで質問に答えるもの」から「現実の業務タスクを実行するもの」へ移りつつある、という整理です。 公開資料「 What's new in Google Cloud's agent platform 」のP.5より引用 これまでのエージェントは、ユーザーがチャットで問いかけ、その場で応答を返すInteractive Chat Agentsが中心でした。一方で、これからはバックグラウンドでデータやシステムを監視し、必要に応じて判断・処理・通知するBackground Processing Agentsや、音声・映像を使って人と自然にやり取りするReal-time Audio/Video streaming Agentsも重要になると紹介されていました。 この考え方は、社内業務にAIを組み込むときにも非常に重要だと感じました。例えば、問い合わせ内容から注文情報や配送状況を確認して一次調査をまとめる、商品説明文の改善候補を商品マスタやレビュー傾向から整理する、BigQuery上の販促結果を定期的に確認して異常値を通知する、といった業務は単発のチャット応答ではなく「一定時間、業務を預ける」タイプのタスクです。 Agent Platform全体は、Build、Scale、Govern、Optimizeという4つの領域で整理されていました。 公開資料「 What's new in Google Cloud's agent platform 」のP.6より引用 Buildでは、ADKやAgent Studio、Agent Garden、MCP、A2Aなどを使ってエージェントを作るための機能が提供されます。Scaleでは、Agent RuntimeやAgent Sandbox、Agent Memory Bank、Agent Sessionsなどを使って、エージェントを本番ワークロードとして動かすための仕組みが用意されています。Governでは、Agent Identity、Agent Gateway、Agent Registry、Agent Policy、Model Armorなどにより、権限や通信、セキュリティポリシーを統制します。Optimizeでは、Agent EvaluationやAgent Observability、Agent Simulationなどを使って、運用中の品質を継続的に確認していきます。 特に興味を持ったのは、Agent Runtimeのアップデートです。 公開資料「 What's new in Google Cloud's agent platform 」のP.13より引用 Runtime enhancementsでは、1秒未満の高速なコールドスタート、数秒でのプロビジョニング、最大7日間のLong-running Operation、双方向ストリーミング、リソースレベルのIAM binding、Python、Java、TypeScript、Goへの対応、プロジェクトあたり3,000エージェントまでのスケールなどが紹介されていました。これにより、エージェントを長時間・多段階の業務を担う実行単位として扱うための基盤が提供されます。 さらに、Agent Runtimeは実行環境だけでなく、セッションやメモリ、サンドボックス、評価、Observabilityとつながる形で説明されていました。社内でエージェントを使う場合、単にモデルへプロンプトを送るだけでは足りません。どのユーザーの依頼で、どのセッションの文脈を持ち、どのデータを参照し、どのツールを呼び出し、どのような結果を返したのかを追える必要があります。商品情報、FAQ、問い合わせ履歴、販売実績などを横断する業務ほど、セッション管理やメモリ、実行ログが重要になります。 Governの領域では、Agent IdentityとAgent Registryの考え方が実運用に直結すると感じました。 公開資料「 What's new in Google Cloud's agent platform 」のP.16より引用 Agent Identityでは、エージェントごとにIDを持たせ、最小権限の考え方で権限を付与し、エージェントの操作を監査できるようにすることが説明されていました。これは、業務システムに接続するエージェントを「誰かの権限を使って動くスクリプト」としないために重要です。商品情報を読むだけのエージェント、売上や在庫を集計するエージェント、問い合わせ対応を支援するエージェント、外部SaaSへチケットを作成するエージェントでは、必要な権限がまったく異なります。 また、組織内のエージェント、MCPサーバー、エンドポイントを管理するための中央システムとしてAgent Registryが紹介されていました。エージェントが部署やチームごとに増えていくと、似たようなエージェントが乱立したり、古いバージョンが使われ続けたり、オーナーがわからなくなったりする可能性があります。AIソリューション開発ブロックとしても、各業務領域の知識を持つチームと一緒にエージェントを育てるためには、どこに何があり、誰が管理し、どのデータやツールにつながっているのかを可視化する仕組みが必要になると感じました。 Agent Gatewayやセキュリティのアップデートも、業務利用にあたって重要なポイントです。Agent Gatewayは、エージェントの通信やツールアクセスに対して、中央でガバナンスとセキュリティポリシーを適用するための仕組みとして紹介されていました。IAM連携、プロトコル解析、ログ、Trace IDなどを通じて、エージェントがどの通信を行い、どの操作を実行したのかを追えるようになります。エージェントの数が増えるほど、個別実装ごとに認可やログを作り込むのではなく、共通の制御点を持つことが重要になると感じました。 運用面では、Agent ObservabilityとEvaluationのライフサイクルが印象的でした。 公開資料「 What's new in Google Cloud's agent platform 」のP.19より引用 通常のWebアプリケーションであれば、HTTPステータスやエラー率、レイテンシを見ることで、ある程度の健全性を把握できます。しかしエージェントの場合、レスポンスが返っていても、根拠が不十分だったり、不適切な判断をしていたりする可能性があります。そのため、トレースやダッシュボード、Multi-Agent topology graph、オンライン・オフライン評価、シミュレーション、継続的な改善までを一連のライフサイクルとして見る必要があります。 このセッションを通じて、AIエージェントを業務で活用するうえで必要なインフラが、Agentに最適化された形で整備されつつあることを強く感じました。これまでも、実務でAIを活用する際はプロンプトだけで完結するわけではなく、実行環境、権限管理、ログ、評価、監査などをどう組み合わせるかを試行錯誤する必要がありました。だからこそ、Runtime、Identity、Gateway、Registry、Observabilityのような機能がプラットフォームとして提供されるのは非常にありがたい流れだと感じます。ZOZOでも業務・事業におけるエージェント活用において、権限とログを整えながら、Background Processing Agentsのように、ユーザーが意識しなくても裏側で業務を支える形での活用も推進していきたいと思います。 Gemini Enterprise appとGoogle Workspaceのアップデート こんにちは、AI・アナリティクス本部 AI事業戦略部 生成AI推進ブロックの川田です。ZOZOでは生成AI活用を推進するチームのマネージャーとして、業務活用とプロダクト活用の両面で企画・推進を担当しています。 今回、Google Cloud Next '26に参加して、生成AIが「質問に答えるもの」から「業務を理解し、タスクを実行するもの」へ進化していることを強く感じました。特に、ビジネス部門の業務効率化に直結しそうなGemini Enterprise appとGoogle Workspaceのアップデートを中心に紹介します。 Google Workspace Blog「 10 more announcements from Google Workspace at Cloud Next ‘26 」のカバー画像より引用 Knowledge Catalog まず印象的だったのが、Gemini Enterprise appとも関係の深いKnowledge Catalogです。Knowledge Catalogは、一言でいうと仕事に関わる情報の集約場所です。 AIエージェントが自律的に動くためには、仕事のこと、会社のこと、プロダクトのことなど、業務に必要な文脈を理解している必要があります。しかし実際の業務情報は、メール、Slack、Googleドライブ、Box、Salesforce、BigQueryなど、さまざまな場所に分散しています。これまでは人間がそれらのツールを行き来しながら、必要な情報を探し、つなぎ合わせていました。 Knowledge Catalogは、外部ソースも含めて100以上のコネクタで情報をつなぎ、ユニバーサルなコンテキストエンジンとして機能します。例えば、エージェントがBigQueryから過去のキャンペーン結果を取得し、スプレッドシートから現在の在庫状況を確認します。そして、Boxにあるデザインガイドラインも参照したうえで、新しい施策案を作るといった使い方が考えられます。 これまでも、AIに社内情報を参照させるためにベクトルデータベースを構築したり、RAGの精度向上に試行錯誤したりする必要がありました。Knowledge Catalogによって、普段使っている業務ツールをコネクタでつなぐだけでセマンティック検索の基盤を活用できるようになると、AIのための準備に使っていた時間を、本来考えるべき業務設計に向けやすくなると感じました。 また、Gemini Enterprise appのDeep Research機能においても、Knowledge Catalogは重要な役割を持ちます。プロンプトだけでは伝えきれない社内の前提や文脈を補完できるため、調査結果の質を高められる可能性があります。 Agent Designer v3 次に、Gemini Enterprise appのアップデートとして、ノーコードでエージェントを作成できるAgent Designer v3が紹介されました。 Agent Designer v3の特徴は、大きく3つあります。1つ目は、自然言語でエージェントやワークフローを作成できることです。2つ目は、実行順序や条件分岐を設定し、柔軟にタスクを実行できることです。3つ目は、MCPサーバーをコネクタとして利用し、企業データを扱えることです。 つまり、企業データを理解したエージェントを、開発者だけでなくビジネス部門のメンバーも視覚的に設計できるようになります。日々の定型業務や複数アプリをまたぐ作業を自動化したい場合や、現場のニーズに合ったAIツールを各部門で作って活用したい場合に、大きな効果が期待できます。 ZOZOでの活用例としては、「商品レビューの自動分析・構造化」ワークフローが考えられます。 1. MCPサーバー経由で、BigQueryに蓄積された新着の商品レビューデータを取得する 2. AIがレビュー内容から「フィット感」「サイズ感」「使用感」などの特徴量を抽出し、ポジティブ・ネガティブの感情を判定してJSON形式で出力する 3-1. ネガティブな感情が強い場合や不良品に関する言及があった場合は、カスタマーサポート部門へGmailやチャットで通知する 3-2. 通常のレビューであれば、構造化したデータをAlloyDBやBigQueryに書き込み、レコメンドやパーソナライズ配信のデータとして蓄積する このように、「自社データの取得」「AIによる分析・構造化」「条件に応じた通知やシステム操作」までを一連の流れとして構築できる点が、Agent Designer v3の大きな魅力だと感じました。 Workspace Intelligence Google Workspace Blog「 Workspace Intelligence の発表 」の本文中で使用されている画像より引用 Google Workspaceのアップデートとしては、Workspace Intelligenceも印象に残りました。Workspace Intelligenceは、Google Workspaceの各アプリとGeminiを連携させ、ユーザーの業務背景や文脈をAIに理解させるための統合的な仕組みです。 イメージとしては、Knowledge CatalogのGoogle Workspace版に近いと感じました。Gmail、Googleドライブ、Google Chatなどに分散している情報を横断的に理解し、ユーザーが今必要としている情報やアクションを提示してくれます。 例えば、Ask Gemini in Chatでは優先タスクのリストアップやファイル検索を支援できます。また、GmailではAI InboxやAI Overviewといった機能が紹介されていました。AI Inboxは、受信トレイの中から今日中に返信すべき承認依頼や、プロジェクトの遅延リスクにつながるメールを判断し、ToDoとして提示してくれます。AI Overviewを使えば、メール、ドキュメント、スライドを横断して「あの件の最新ステータスはどうなっているか」「あの仕様は最終的にどのような落とし所になったか」といった質問にも答えられるようになります。 数日間の出張や休暇から戻ったときに、メールやチャットを何百件も遡って状況を把握するのは、多くの人が経験している負荷だと思います。Workspace Intelligenceによって、その不在期間の文脈を要約できれば、人間はキャッチアップ作業に時間を使うのではなく、最初から判断や意思決定できるようになります。 Sheets Canvas 最後に、Sheets Canvasについても紹介します。これはエージェント的な機能というより、データの理解や業務の進め方を大きく変えそうなアップデートです。 Sheets Canvasは、スプレッドシート上のデータをもとに、ノーコードでミニアプリを構築できる機能です。例えば、KPIダッシュボードやプロジェクト進捗を管理するカンバンボードのようなものを、数クリックで作成できます。作成したCanvasはシートのデータとリアルタイムに同期されるため、シートを更新するとアプリ側も最新の状態になります。また、通常のスプレッドシートと同じように他のユーザーへ共有できます。 スプレッドシートは柔軟で便利な一方、データ量が増えると全体像をつかむのに時間がかかることもあります。Sheets Canvasによって、データを入力・管理する場所と、それを理解・活用するための画面が近づくことで、情報を把握するスピードや意思決定の質を高められると感じました。 まとめ 今回紹介したKnowledge Catalog、Agent Designer v3、Workspace Intelligence、Sheets Canvasに共通しているのは、AIが業務の文脈を理解し、必要な情報を探し、タスクの実行まで支援する方向に進んでいる点です。 AIに参照させたいコンテキストを集約し、そのデータを活用するエージェントを作成できるようになることで、これまで人が担っていた情報探索や整理、定型的な判断の一部を任せやすくなります。また、普段利用しているGoogle Workspaceの各アプリにもAI機能が組み込まれることで、日常業務の中で自然にAIを活用できる場面が増えていくと感じました。 一方で、これらの機能は導入するだけで価値が出るものではなく、どの業務に適用し、どのように運用へ組み込むかが重要です。ZOZOでも、業務活用とプロダクト活用の両面でユースケースを見極めながら、研修や検証を通じて社内での生成AI活用を推進していきたいと思います。 おわりに 今回のGoogle Cloud Next '26では、各サービスのアップデートを個別に知るだけでなく、それらが実際の開発・運用体験をどのように変えていくのかを考える機会になりました。Cloud Run、AlloyDB、Generative UI、Agent Platform、Gemini Enterprise app、Google Workspaceといったテーマはそれぞれ異なりますが、どのセッションも、私たちが日々向き合っているシステム設計、データ活用、業務改善、開発体験に直接つながる内容でした。 また、現地ではセッションに加えて、企業ブースやデモ展示を通じて、発表された技術がどのようなユーザー体験として提供されるのかを具体的に知ることができました。資料や動画だけでは分かりにくい細かな操作感や、参加者の反応、会場全体の熱量を肌で感じられたことも、現地参加ならではの大きな収穫でした。 今回得た知見を、ZOZOのプロダクト開発や社内でのAI活用にも活かしていきたいと思います。Google Cloud Next '26では数多くのセッションが公開されていますので、気になる方はぜひ 公式サイトのSession and Activity library もご覧ください。 最後に、弊社ではカンファレンス参加に伴う渡航費や宿泊費は福利厚生のひとつであるセミナー・カンファレンス参加支援制度によって、カンファレンス参加にかかる費用は全て会社負担です。 ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは、カート決済部カート決済サービスAブロックの 道場 です。ZOZOTOWN内のカート機能や決済機能の開発、保守運用を担当しています。 現在、ZOZOTOWNのカート決済画面はリプレイスが進行中です。既存システムとリプレイス後のシステムが並行して開発される中、既存システムへのさまざまな機能改修を、リプレイス側にも取り込む必要があります。その際、条件の組み合わせが膨大になるテストを手動で網羅的に実施することが現実的でなく、特に注文金額の計算結果の正確性を人間が1件ずつ確認するには大きなコストがかかっていました。 本記事では、Claude CodeとPlaywright CLIを組み合わせて、自然言語によるE2Eテストを自動化した仕組みをご紹介します。Confluence(Atlassian社が提供するナレッジ共有ツール)に自然言語でテスト手順を記述することでAIが自律的にブラウザを操作し、計算検証も含めてE2Eテストを完結させています。コードを書かずにテストを作成・実行できるため、テスト自動化の属人化解消にもつながりました。 目次 はじめに 目次 背景・課題 リプレイスに伴う二重開発とテストの課題 なぜ従来のE2E自動化では足りなかったのか AIエージェント駆動のE2Eテストシステム 全体アーキテクチャ Playwright CLIによるブラウザ操作 Agent Skillsによる操作手順の定義 テストケースの設計と期待値の保証 Confluenceベースのテストケース管理 計算が必要なテストの期待値保証 テスト実行の6つのStep テスト支援ツールの構築 atlassian-cli:Confluence操作のCLI zozo-sql-server-cli:SQL Serverクエリ実行CLI AIエージェントが必要なツールを自ら作る 従来のテスト自動化との比較 実践から得られた知見 テストケースの実績 実践を通じた気づき まとめ 背景・課題 リプレイスに伴う二重開発とテストの課題 冒頭の通り、ZOZOTOWNのカート決済画面ではリプレイスが進行中です。既存システムとリプレイス後のシステムが並行して動作する期間中、既存システムに対するさまざまな機能改修をリプレイス側へ取り込む必要があります。 これらの改修をすべて取り込み、条件の組み合わせが爆発的に増加するテストケースを検証する工数が大きな課題となりました。 たとえば、ある案件の機能を取り込む場合、以下のような因子が絡み合います。 ユーザーの属性(性別・年齢 等) 購入商品の種類・金額 割引・クーポンの有無 ポイント利用の有無 キャンペーン期間の内外 これらを組み合わせると、1つの案件だけで 100件以上のテストケース が発生することもありました。さらに、各テストケースでは 注文フローの複数画面 (配送・支払い選択、注文の確認 等)で表示値の確認が必要です。そして、 PC用の画面とスマートフォン(以下、SPと表記します)用の画面がそれぞれ存在 するため、検証量は実質的にさらに倍になります。 カート決済画面では、注文金額の計算ロジックにさまざまな要素が関わっており、前述の通り案件ごとに条件の組み合わせが大きくなりがちでした。さらに、期待値は複雑な計算式で決まるため、人間が1件ずつ手計算したうえで画面の表示と照合するには多くの時間がかかっていました。 なぜ従来のE2E自動化では足りなかったのか ZOZOTOWNでは、手動テストに加えて品質管理部によるコードベースのE2E自動テストも活用しています。しかし、そのような従来のコード記述型の自動テストを使ったアプローチでは以下の課題がありました。 プログラミングスキルへの依存 :CSSセレクタやロールを使った要素特定のコードを書く必要があるため、開発者でなければ作成・保守が難しい UI変更への追従コスト :UIの変更に応じて、要素特定の方法やテスト内容のメンテナンスが必要になる テストコードの属人化 :記述・保守できる人が限られるため、特定の開発者への依存が生じる 実現したかったのは、 テスト手順を自然言語で書くだけで、AIが要素を自動で見つけて操作し、計算検証まで完結する仕組み です。そのためのアプローチとして、Claude CodeのAgent SkillsとPlaywright CLIを組み合わせた自動化システムを構築しました。 AIエージェント駆動のE2Eテストシステム 全体アーキテクチャ 構築したシステムの全体像は以下の通りです。 各コンポーネントの役割は次の通りです。 コンポーネント 役割 Confluenceページ テストデータ・手順・期待値を自然言語で記載したテストケース管理の場 エージェント ( zozotown-qa-tester ) テストの実行フローを定義するClaude Codeエージェント Agent Skills ZOZOTOWNの操作手順やCLIの使い方をMarkdownで定義した再利用可能なリファレンス 計算サービス(TypeScript) 期待値を算出するための計算ロジック実装 Playwright CLI コマンドでブラウザを操作するCLIツール atlassian-cli Confluenceの読み取りと、エビデンスを含めた結果の記載を行う自作CLI zozo-sql-server-cli SQL Serverへのクエリ実行と結果の画像化を行う自作CLI Claude CodeのエージェントがConfluenceからテストケースを読み取ります。Agent Skillsを参照しながらPlaywright CLIでブラウザを操作し、結果をConfluenceに書き戻します。 Playwright CLIによるブラウザ操作 Playwright CLI は、ブラウザ操作をコマンドで実行できるCLIツールです。テストコードを書く代わりに、コマンド1つでブラウザを操作できます。Playwright MCPもありますが、CLIの方がトークン使用量を節約できるため選択しています。 特徴的なのは スナップショット機能 です。ページを開くと、Playwright CLIはページの構造をYAML形式で取得します。このとき各要素には ref 番号が付与されています。AIはこのスナップショットを読んで要素を特定し、 ref 番号を使って操作します。 # ref番号を使って要素をクリック playwright-cli click e42 --session = pc # テキストを入力 playwright-cli fill e15 " test@example.com " --session = pc # スクリーンショットを取得 playwright-cli screenshot --output screenshots/cart-top.png --session = pc CSSセレクタやロールを明示的に指定しなくても、AIがスナップショットを解釈して要素を特定できます。そのため、セレクタベースの実装に比べると、軽微なUI変更には追従しやすくなります。 PCとSPの切り替えは設定ファイルで行います。 // playwright-cli.json(PC用) { " browser ": { " launchOptions ": { " headless ": false } , " isolated ": false , " contextOptions ": { " viewport ": { " width ": 1400 , " height ": 1080 } } } } // playwright-cli-sp.json(SP用) { " browser ": { " launchOptions ": { " headless ": false } , " isolated ": false , " contextOptions ": { " viewport ": { " width ": 430 , " height ": 932 } , " userAgent ": " Mozilla/5.0 (iPhone; ...) Safari/604.1 ", " isMobile ": true , " hasTouch ": true } } } PCテストとSPテストは 別セッションで同時に実行できる ため、テスト時間の短縮にも貢献します。 Agent Skillsによる操作手順の定義 Agent Skillsでは、Claude CodeのSkill機能を活用してZOZOTOWN固有の操作手順を定義しています。コードベースのPlaywrightにおけるPage Object Modelに相当する役割を、Markdownによる自然言語の手順書で担うイメージです。 操作手順は次のように自然言語で記述します。 # ログイン手順リファレンス ## 手順 1. 以下のページを開く - PC: ` /_member/login.html ` - SP: ` /sp/_member/login.html ` 2. ` メールアドレス ` 入力欄にメールアドレスを入力する。 3. ` パスワード ` 入力欄にパスワードを入力する。 4. ` ログイン ` ボタンをクリックする。 テストケースに「テストユーザーAのアカウントでログインする」と書けば、エージェントがこのリファレンスを参照して手順を実行します。操作をリファレンスとして標準化しておくことで、 誰が書いたテストケースでも同じ操作が再現できます 。 今回定義した主要なリファレンスは次の通りです。 login-flow.md :ログイン手順(PC / SP対応) add-to-cart-flow.md :商品をカートへ投入する手順 order-flow.md :注文フロー(カートTOP → 配送・支払い選択 → 注文確認 → 注文完了) sql-execution-flow.md :SQL Serverへのクエリ実行手順 テストケースの設計と期待値の保証 Confluenceベースのテストケース管理 テストケースはConfluenceページで管理しています。ページの構成は次の通りです。 セクション 内容 要件 テスト対象の機能仕様 因子と水準 テストに関わる条件の洗い出し(ホワイトボックス観点) デシジョンテーブル 条件の組み合わせパターン テストデータ 環境URL、ユーザー情報、商品情報 テストケース 手順、パラメータ、期待値、実行結果、エビデンス テスト実行後は、Claude Codeがこのページに結果(OK / NG)とスクリーンショットを自動で書き込みます。 実際に実施したテストケースの例を紹介します。 注文金額に関わる計算ロジックの検証テスト :注文の確認画面に表示される金額が、計算サービスの算出結果と一致することを検証します。前述の因子を組み合わせた数十件のパターンを定義しています。 テストの手順は、Confluenceページに次のように自然言語で記述されています。 1. カートを空にする 2. パラメータ(商品)に記載されている商品をカートに入れる 3. 注文へ進み、パラメータ(支払い方法)の支払い方法を選択して注文確認画面を表示する 4. 表示されている計算結果の値が OrderAmountCalculationService.getの値と 一致していることを確認する 5. viewportのスクリーンショットを取得する 6. パラメータ(ポイント利用)に記載のポイントを利用する 7. 表示されている計算結果の値が上記計算サービスの値と一致していることを確認する ... この手順をClaude Codeが読み取り、Agent Skillsを参照しながらブラウザを操作します。 計算が必要なテストの期待値保証 計算結果の検証は、今回の取り組みで最も重要なポイントです。 課題 :注文金額に関わる複雑な計算結果を、人間が手計算して期待値と照合するには大きな工数が必要です。特に、割引・クーポン・ポイント利用・税率が絡み合う計算は、ミスが発生しやすく時間もかかっていました。 解決策 :Playwrightテスト用リポジトリにTypeScriptで計算サービスを実装し、あらかじめ期待値を算出しておきます。Claude Codeはテスト計画の作成時に計算サービスを呼び出し、期待値をプランに出力してから、ブラウザの表示値と照合します。 // ZOZOCARD還元ポイントを計算するクラス export class ZozocardRewardPointCalculationService { private static readonly POINT_RETURN_RATE = 0.05 ; public get ( goodsPriceWithoutTax : number , quantity : number , taxRate : number ): number { // ZOZOCARD 還元ポイントの計算処理... } } この計算処理は、システムと同じ仕様をもとにClaude Codeで生成した 独立した実装 になっています。システム側の実装コードをそのまま流用すると、同じバグを共有してしまいます。仕様を別実装することで、 システム側とテスト側の独立性 を保っています。これにより、期待値とシステムの表示値を照合したときに、単なる一貫性チェックではなく、システム側の実装が仕様どおりかを検証できます。実際に、このテストを通じてシステム側の実装が仕様を正しく考慮できていないケースを検知できた事例もありました。 期待値の検証フローは次の通りです。 Claude Codeはテスト計画を作成する段階で計算サービスを実行し、全テストケースの期待値を事前に算出します。テスト実行時には、ブラウザで取得した表示値と事前に算出した期待値を照合します。 テスト実行の6つのStep エージェント定義ファイル( zozotown-qa-tester.md )では、テスト実行を次の6つのStepで定義しています。 --- name: zozotown-qa-tester description: ZOZOTOWN の QA テストを実行するエージェント skills: - playwright-cli - zozotown-operations - confluence-page-operations - atlassian-cli - zozo-sql-server-cli --- ## テスト実行フロー ### 1. テストケースの確認 Confluenceページからテストケースを取得し、 対象の開発環境・前提条件・手順・期待結果を読み取る。 ### 2. テストケースプランの作成 テストデータ・期待値(計算サービスの実行結果)・実行手順を整理し、 ` test-plans/ ` ディレクトリにMarkdownファイルとして出力する。 **ユーザーの承認を得てからテスト実行に進む。** ### 3. テスト準備 ブラウザを起動し、ログインや初期データのセットアップを行う。 ### 4. テスト実行 各ステップを ` zozotown-operations ` のリファレンスに従って実行する。 手順が定義されていない操作は、実際にブラウザで確認して新しいリファレンスを作成する。 ### 5. 結果の記録 実行結果(OK / NG)を判定し、スクリーンショットを撮影して Confluenceページに結果とエビデンスを書き込む。 ### 6. 結果の報告 ユーザーに実行結果のサマリを報告する。 特に重要なのは Step 2のテストケースプランの作成とユーザー承認 です。AIは非決定的に動作するため、テストケースの解釈が意図と異なる可能性があります。実行前に計画を提示してユーザーに確認することで、 解釈のズレを事前に検出 できます。 また、Step 4の「リファレンスに手順がない操作は自ら作成する」という仕組みにより、エージェントが新しい操作手順を発見するたびにリファレンスファイルが自動的に追加されていきます。使うほどにリファレンスが充実し、テスト作成が楽になっていく仕組みです。 実際のテスト実行では、テスト計画の確認とPC / SPセッションの並列実行をターミナル上で確認できます。 テスト支援ツールの構築 atlassian-cli:Confluence操作のCLI Confluenceのテストケースページを詳細に処理するため、atlassian-cliを作成しました。Atlassian MCPもありますが、スクリーンショットを添付できないため、REST APIをラップしたCLIです。 テスト実行フローでの使用例を示します。 # Confluence のテストケースページを取得 atlassian-cli confluence get-page 348678105 --body-format atlas_doc_format # テスト結果のスクリーンショットをアップロード atlassian-cli confluence upload-attachment 348678105 \ --file ./screenshots/confirm-pc.png # テスト結果をページに追記 atlassian-cli confluence update-page 348678105 \ --body-file ./test-results/result.json \ --page-version 41 zozo-sql-server-cli:SQL Serverクエリ実行CLI 注文完了後のDBデータを検証するため、zozo-sql-server-cliも作成しました。注文データが正しく保存されているかをSQLで確認し、 結果をHTMLテーブルとして描画してPuppeteerでスクリーンショット化 する機能が特徴です。 # SQL クエリを実行してテーブル形式で表示 zozo-sql-server-cli \ " SELECT total_amount, discount_amount FROM orders WHERE order_id = 12345 " # クエリ結果をスクリーンショット(HTMLテーブルとして描画)として保存 zozo-sql-server-cli \ " SELECT total_amount, discount_amount FROM orders WHERE order_id = 12345 " \ --screenshot ./screenshots/order-db.png このスクリーンショットをそのままConfluenceのエビデンスとして添付することで、DB検証の証跡も自動的に記録できます。 AIエージェントが必要なツールを自ら作る atlassian-cliとzozo-sql-server-cliは、いずれもClaude Codeを活用して作成しました。 テスト自動化を進める中で「Confluenceにスクリーンショットを添付したい」「DBの検証結果を画像として保存したい」といったニーズが生まれました。これらをCLIとしてClaude Codeに実装してもらい、短期間で必要な機能を揃えることができました。 AIエージェントに必要なツールをAI自身が作れる という点は、自動化のエコシステムを大幅に加速させます。 従来のテスト自動化との比較 従来のコードベースのE2E自動テストと、今回構築したClaude Code + Playwright CLIのアプローチを比較します。 観点 コードベースのPlaywright Claude Code + Playwright CLI テストケースの形式 TypeScript / JavaScriptコード Confluenceページ(自然言語) 要素の特定方法 CSSセレクタ / ロール スナップショットのref番号(AIが自動特定) 期待値の検証 ハードコードされたアサーション 計算サービス + AIによる照合 UI変更への耐性 低い(セレクタ・ロールの変更対応が必要) 高い(スナップショットベースで柔軟に対応) 作成に必要なスキル プログラミング ドメイン知識 + 自然言語 最も大きな違いは、 テストコードの記述・保守スキルがなくてもE2Eテストを作成・実行できる 点です。 Confluenceでテスト手順を書く際には「テストユーザーAでログインする」「XXXの商品をカートに入れる」といった日常的な言葉で記述できます。Agent Skillsのリファレンスにログインやカート投入の手順が定義されているため、この自然言語の指示だけでAIが正確に操作を再現します。 また、計算検証の自動化により、人手では高コストだった期待値照合をAIが実行できるようになりました。開発者は別の案件の開発を進めながら、Claude Codeにテストを並行して実行させることができます。 実践から得られた知見 テストケースの実績 実際に実施したテストの実績は次の通りです。 テスト対象 テストケース数 対象画面 プラットフォーム 案件A(計算ロジックの検証) およそ20件 注文フローの各画面 PC / SP 案件B(条件の組み合わせ検証) およそ50件 注文フローの各画面 PC / SP 手動でのフローと、今回構築したAIエージェント活用後のフローを比較すると次のようになります。 人が行うのは、テスト計画のレビューのみです。数十件のテストケース × 複数ページ × PC / SPの全テストをClaude Codeに任せられました。案件Aでは詳細な計算結果を、案件Bでは肥大化する条件の組み合わせを検証でき、人手による手計算や確認にかかる工数を大きく減らせました。 実践を通じた気づき Agent Skillsの粒度設計 :ログインやカート投入、注文フローというような1つの手順として指示する粒度がちょうどよく、再利用しやすいです。細かすぎるとリファレンスが増えすぎて管理が難しくなり、粗すぎると他のテストケースで使いにくくなります。 テスト計画承認フローの効果 :「2. テストケースプランの作成」でAIが作成した計画をレビューすることで、テストケースの解釈ミスを事前に検出できた事例がありました。コーディング時もそうですが、私はClaude Codeのプランモードをよく利用します。何をするかを綿密に考えさせたものを自分が確認することで、あとはそれを実行するだけになり、質が高くなると感じています。 自己改善するリファレンス :未定義の操作に遭遇した際、エージェントが実際にブラウザで操作して手順を確認し、新しいリファレンスファイルを自動作成する仕組みは実用的でした。テストを重ねるほどリファレンスが充実し、環境を育てていくことで後のテスト作成が楽になっていきます。 まとめ 本記事では、Claude CodeとPlaywright CLIを組み合わせた自然言語E2Eテストの構築と実践をご紹介しました。Confluenceに自然言語でテスト手順を記述するだけでAIが自律的にブラウザを操作し、計算検証も含めてE2Eテストを完結させることができました。 膨大な組み合わせテストの自動化・計算検証の正確性担保・テスト自動化の属人化解消という課題を同時に解決し、開発者が別の案件を進めながらテストを並行完了できる体制が実現しました。今後は他の画面への展開や、定期的な実行によるリグレッションの検知などを検討していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくださる方を募集中です。ご興味のある方は、ぜひ採用ページをご覧ください。 corp.zozo.com