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

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1015

はじめに こんにちは。プラットフォーム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
はじめに こんにちは、ZOZOTOWN開発1部iOSブロックの @kitasuke です。 前回の記事「 ZOZOTOWN iOS のアーキテクチャとチームの進化 」では、MVCからMVVM、そしてMVVM + Repositoryへのアーキテクチャ進化を取り上げました。あわせて、レビュー文化をチームに根づかせてきた3年間も振り返っています。 ただ、アーキテクチャを文章で定義しても、書き手によって命名や責務分割はぶれが生じますし、AIに任せると過去の望ましくない実装パターンまで律儀に再現されます。 ドキュメントによる「努力目標」では、アーキテクチャは守りきれません。 そこで発想を逆にしました。アーキテクチャを「守るべきルール」ではなく、 構造化されたスキーマ として定義し、人間とAIの双方がそれに従うしかない形にします。Swiftの型システムがコンパイル時に不正を弾くのと同じ発想を、アーキテクチャのレイヤーにスキーマという形で持ち込みます。それが本記事で紹介する 「スキーマでアーキテクチャを縛る」アプローチ です。副産物として、設計からコードを自動生成するパイプラインも動いています。 目次 はじめに 目次 どんなスキーマを定義したのか architecture-guidelines.md — コンポーネントをスキーマで縛る architecture-templates.md — スキーマから Swift を導出するルール どうやって縛っているのか 画面ごとの設計を YAML で表現する /architectureと/codegen — 実際の運用 /architecture: 仕様書やデザインから YAML を起こす /codegen: YAMLからSwiftのコードを生成する 何が変わったのか AIの書くコードがレビューを通る水準になった レビューで「プロダクト品質」の話ができるようになった まとめ どんなスキーマを定義したのか 全体像はこうなっています。 仕様書 (Confluence) / デザイン (Figma) / 既存コード │ ▼ /architecture ┌─────────────────────┐ │ 設計 YAML │ ←── AI / Codegen 向け │ Human Doc (Markdown) │ ←── 人間向けレビュー資料 └─────────────────────┘ │ ▼(人間がレビュー・編集) │ ▼ /codegen Swift コード一式 ↑ 全工程でガイドラインとテンプレートが参照される 土台となっているのが、チームで整備した 2つのドキュメント です。 architecture-guidelines.md — 各コンポーネントのスキーマ(何が正しいか) architecture-templates.md — スキーマからSwiftを導出するテンプレート(どう書くか) architecture-guidelines.md — コンポーネントをスキーマで縛る 各コンポーネント(ViewModel、Repository、Translatorなど)を、型・依存・命名・必須ルール・禁止パターンなどのフィールドで厳密に定義しています。たとえばViewModelのスキーマは次のとおりです。 ### ViewModel - type : `@MainActor final class` - imports : [ Foundation, Combine ] - imports_forbidden : [ APIModule ] - depends_on : [ RepositoryProtocol, UIModelTranslator, DataModel, UIModel ] - nested_types : [ ViewState, Router ] - naming : { Feature } ViewModel - required : - ViewState enum で画面状態を管理(複数 Bool 禁止) - @Published private(set) で外部からの直接変更を防止 - 1 ユーザーアクション = 1 input メソッド(did{Verb}{Noun}) - forbidden : - キャッシュロジック(Repository の責務) - ログ送信の直接呼び出し(UseCase/別 Repository に分離) 自由に書ける余地を 意図的に潰している のがポイントです。ViewModelがAPIモジュールをimportした時点でアウトです。 @Published を private(set) にしなかった場合もアウトです。自己流のMVVM解釈を許さない設計になっています。 architecture-templates.md — スキーマから Swift を導出するルール スキーマだけではSwiftコードの具体的な書き方までは決まりません。命名規則、ファイルの生成順序、各レイヤーのSwiftコードテンプレートなどを、もう一段別のドキュメントで固めています。 ガイドラインがスキーマで、テンプレートが導出規則です。 この2つが揃うことで、アーキテクチャのスキーマから具体的なSwiftコードが一意で決まる状態になりました。 どうやって縛っているのか 人間・AI・ツールの全員が、同じスキーマで動くようになっています。順に見ていきます。 画面ごとの設計を YAML で表現する コンポーネントのスキーマが決まっても、画面ごとの実装は別物です。そこで、 画面ごとの設計を1枚のYAMLで記述 します。 feature : ProductList domain : Product api : - id : fetchProducts method : GET path : /products response : items : [ Product ] actions : - trigger : didAppear api : fetchProducts - trigger : didTapRetry api : fetchProducts condition : "state == .error" models : data : - name : Product fields : id : String name : String brandName : String price : Int imageURL : URL ui : - name : ProductListUIModel fields : nameText : String brandText : String priceText : String このYAMLは、ガイドラインが定めたスキーマの「値」にあたります。画面のAPI、アクション、データモデルが構造化されて並んでいるだけで、曖昧さの入り込む余地はありません。 /architecture と /codegen — 実際の運用 この縛りを日々の開発で実行しているのが2つのスラッシュコマンドです。 /architecture : 仕様書やデザインから YAML を起こす 重要なのは、このYAMLを人間がゼロから書いているわけではない という点です。Confluenceの仕様書やFigmaのデザインを入力にすると、 /architecture コマンドが設計YAMLと人間向けMarkdownの大部分を自動生成します。 人間の作業は「書く」ではなく「判断する」に寄っています。生成されたYAMLを読み、責務分割やエッジケースの扱いなど 設計判断が必要な箇所だけ に手を入れます。スキーマが縛ってくれているので、AIが起こしたYAMLも標準から外れた形にはなりません。 /codegen : YAMLからSwiftのコードを生成する レビューが終わったYAMLを /codegen に渡すと、Swiftコード一式が出力されます。具体的には、View / ViewModel / Repository / プロトコル / モック / ユニットテストの雛形 / 依存注入のコードです。 たとえば先ほどの ProductList.yaml のうち、以下の部分に注目します。 actions : - trigger : didAppear api : fetchProducts - trigger : didTapRetry api : fetchProducts condition : "state == .error" この部分を /codegen に流すと、ViewModelは次のように生成されます。 @MainActor final class ProductListViewModel : ObservableObject { enum ViewState { case loading case loaded(ProductListUIModel) case error(Error) } @Published private ( set ) var state : ViewState = .loading private let repository : ProductRepositoryProtocol func didAppear () async { await fetchProducts() } func didTapRetry () async { guard case .error = state else { return } await fetchProducts() } private func fetchProducts () async { state = .loading do { let products = try await repository.fetchProducts() state = .loaded(ProductListUIModelTranslator.translate(from : products )) } catch { state = .error(error) } } } ガイドラインで定義した制約が そのまま反映されている のが分かります。たとえば @MainActor final class 、 @Published private(set) 、ViewState enumでの状態管理、 did{Verb}{Noun} 命名規則などです。YAMLの actions はそのままViewModelのメソッドに、 condition はguard文に対応しています。コード生成は仕組みの主役ではなく、スキーマで縛った結果として得られる副産物です。 何が変わったのか AIの書くコードがレビューを通る水準になった スキーマで縛ったことで、実際にAIの出力が目に見えて安定しました。命名・配置・レイヤー構成がプロジェクト標準に揃い、ハルシネーションもほぼ消え、同じYAMLを何度通してもほぼ同じコードが出てきます。 AIの生成するコードは、そのままレビューを通る水準に達しました。 これが縛りの直接的な見返りです。 レビューで「プロダクト品質」の話ができるようになった コード品質(命名、配置、責務分割)はスキーマが自動的に揃えるので、レビューで議論する必要がなくなりました。その分、UXが成立しているか、エッジケースの仕様が妥当か、ビジネスゴールに沿っているか、といった プロダクトとしての品質 に時間を使えるようになっています。コードの良し悪しではなく、 プロダクトの良し悪し を議論できるようになったのは、狙い通りの大きな変化でした。 まとめ アーキテクチャは「努力目標」ではなく「スキーマ」で守ります。Swiftの型システムが不正を弾くのと同じ発想を、設計レイヤーにも持ち込みます。人間とAIを同じスキーマで動かすことで、チームのアーキテクチャを長く保てる状態を目指しています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
Developer Engagementブロックの @ikkou です。2026年4月22日から24日の3日間にわたり北海道は函館市の 函館サーモン・まるなまアリーナ で「 RubyKaigi 2026 」が開催されました。 日本Rubyの会「RubyKaigi 2026」特別ライトアップ 今回の函館開催にあわせ、通常の白色のみの五稜郭タワーのライトアップが、Rubyをイメージした特別色のレッドにライトアップされていました。 ZOZOは今年もプラチナスポンサーとして協賛し、スポンサーブースを出展しました。 technote.zozo.com 本記事では、前半はWEARのバックエンドエンジニアが気になったセッションを紹介します。後半では、ZOZOの協賛ブースの様子と各社のブースにおけるコーディネートを写真中心に報告します。 ZOZOとWEARとRubyKaigi WEARのバックエンドエンジニアが気になったセッション A Faster FFI そもそもFFIとは? 現状のFFIの課題 なぜFFIは遅いのか? 改善1 改善2 FFXの仕組み 最終的なパフォーマンス ruby.wasm can also enable JavaScript to call Ruby libraries. The Less-Told Story of Socket Timeouts なぜopen_timeoutが必要だったのか net/httpのtimeoutライブラリ依存問題 resolv_timeout + connect_timeoutでは代替できない open_timeoutのAPI仕様 試してみる Ruby 3.4: fast_fallback ON Ruby 3.4: fast_fallback OFF Ruby 4.0: open_timeout fast_fallback ON + open_timeout fast_fallback OFF + open_timeout 同時指定はエラー 全体の対比 おわりに Autoresearching Ruby Performance with LLMs Autoresearchとは なぜ「ループ」が重要なのか 4つのループパターン Ralphループ Autoresearchループ Factoryループ Sidekiqでの実証実験 実験の背景 "マージされないコードを生成することの意味" PRレビューとは何か? 4つのレッスン Lesson 1: 「自動調査」であって「自動変更」ではない Lesson 2: 自分がオーナーで Architect でないものに Autoresearch を適用しない Lesson 3: ループはビター・レッスンを実践する Lesson 4: 人間のゲートをソフトウェアゲートに変換する Software Factory とその課題 まとめ おわりに Exploring RuboCop with MCP The Journey of Box Building Ruby Boxとは何か Ruby Boxの仕組み Ruby Boxが生まれた歴史 おわりに ZOZOブースの紹介 協賛企業ブースのコーディネートまとめ RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜を開催します おわりに ZOZOとWEARとRubyKaigi 私たちが運営する ファッションコーディネートアプリ「WEAR by ZOZO」 のバックエンドはRuby on Railsで開発されています。2013年にVBScriptで構築されたシステムでしたが、2020年頃からコードフリーズし、Rubyへのリプレイスを開始しました。現在もリプレイスを進めながら、新規の機能もRubyで開発しています。また、Matzさんを技術顧問としてお迎えし、毎月Matz MTGと称したオンラインミーティングを実施しています。 ZOZOとRubyKaigiの関係は、ZOZOの前身であるVASILY時代の RubyKaigi 2017 に遡ります。コロナ禍を経て再開した RubyKaigi 2022 からはWEARのバックエンド開発を担うチームが中心となって協賛とスポンサーブースの出展を続けています。 RubyKaigi2017参加レポート(全日分)とスライドまとめ RubyKaigi2018参加レポート RubyKaigi 2019参加レポート〜sonots登壇セッション & エンジニア8名による厳選セッション RubyKaigi 2022参加レポート 〜エンジニアによるセッション紹介〜 RubyKaigi 2023参加レポート 〜エンジニアによるセッション紹介〜 RubyKaigi 2024 参加レポート RubyKaigi 2025 協賛&参加レポート WEARのバックエンドエンジニアが気になったセッション 今年はWEARチームから6名のバックエンドエンジニアがRubyKaigiに参加しました。本パートでは各エンジニアが特に気になったセッションを個々の視点で紹介します。 A Faster FFI chika です。私からはAaron Patterson( @tenderlove )氏の「 A Faster FFI 」を紹介します。 このセッションでは、「RubyはC言語より速くなるか?」という問いからスタートし、具体的にはRubyのFFIを高速化し、ネイティブのC言語(C拡張)よりもRubyを速く実行できるか? というのがメインの議題でした。 余談ですが、Aaron氏はRubyKaigi 2026の外国人登壇者の中でおそらく唯一日本語でスピーチする方です。流暢な日本語に加えて時折ジョークも交え、大変ユニークなセッションであり毎年楽しみに拝聴しています(最初の挨拶では「外国人みたいな名前ですが、外国人です」と日本語でジョークを言っていて面白かったです)。 そもそもFFIとは? FFIとはForeign Function Interfaceの略称で、一般的にはRubyのような高水準言語からC言語やRust, Zigなどで書かれた外部の関数を呼び出すための「概念」のことを指します(FFI自体は、あるプログラミング言語から他のプログラミング言語で定義された関数などを利用するための機構)。 Rubyでは主にlibffiというライブラリやfiddle gemを介して利用され、プラットフォームごとの呼び出し規則の違いを吸収してくれます。 例:FFIを使ったhello world #include <stdio.h> void hello ( void ) { printf ( "Hello, World from C! \n " ); } require ' ffi ' module Hello extend FFI :: Library ffi_lib File .expand_path( ' libhello.dylib ' , __dir__ ) attach_function :hello , [], :void end Hello .hello cc -shared -o libhello.dylib hello.c && ruby hello.rb #=> Hello, World from C! FFIはネイティブC拡張に比べ、CRuby, JRuby, TruffleRubyといった異なるRuby実装でも、ほぼそのまま動くというポータビリティの観点でもメリットがあります。 現状のFFIの課題 一見するとRubyからC言語の関数が直接呼び出せるのはパフォーマンスや移植性などの観点から便利そうに見えますが、実際のところFFIは滅多に使われておらず、その理由としてパフォーマンスが悪いという点が挙げられていました。 ベンチマーク比較では、既存のC拡張とFFIを比較すると、ネイティブC拡張の方が約2.4倍も高速に動作しているとのことでした(FF愛(FFI)してない…)。 なぜFFIは遅いのか? FFIが遅い主な原因として、以下の三点を挙げていました。 余分なフレームプッシュ Rubyから目的のC言語の関数を最終的に呼び出すまでに、呼び出し規則の変換などを行う「中間的な関数」がFFIやVM内部で何層も呼び出される。そのたびにコールスタックに余分なフレームが積まれる(プッシュされる)ため、関数呼び出しそのもののオーバーヘッドが大きくなってしまう。 値の渡し方の違い Rubyはスタックを使って値を渡すが、C言語は(x86_64やARM64などの環境において)CPUのレジスタを使用して値を受け取る。そのため、C言語の呼び出し規約(ABI)に合わせて、スタックからレジスタへ値をコピーして詰め直す操作が発生する。 タイプ変換 RubyのオブジェクトをC言語が理解できる型に変換し、Cからの戻り値を再びRubyオブジェクトに変換するオーバーヘッドが発生する。 改善1 この遅い原因を解消するために、JITコンパイラを用いてそれらのコストを削減するアプローチを試みていました。そして、Rubyで書かれたFFIのためのJITコンパイラ「FJIT」が誕生しました。これはfiddle gemなど複数のgemの機能を活用して直接マシンコードを生成することで、既存のFFI実装に比べて約2倍ほど高速化することが可能となりました。 改善2 さらにもう1つの解決案として提案されたのが、CRuby内蔵のJITコンパイラであるZJITと、新しいトランスレータである FFX でした。 FFXの仕組み FFXは、Rubyで書かれたFFI拡張のコードを読み込み、自動的にC拡張のコードに変換します。この時、生成されるCコードの中にはZJIT向けのヒント(具体的には、生成されるCコードの中に「この関数の引数はこういう型である」といったメタデータをZJITが読み取れる形で配置する)が意図的に埋め込まれます。ZJITはこのヒントから関数の引数や戻り値の型情報を認識することで、最適化された高速なマシンコードを動的に生成することが可能となります。 最終的なパフォーマンス これらの改善を導入しベンチマークを測定したところ、なんとC拡張よりも約1.4倍速く動作するという結果になりました。結果的に、「RubyはCよりも速くなる?」という問いに対しては、「(JITとFFXの力を借りれば)イエス」となり、つまり「RubyはCよりも速い」という結論で締め括られていました(笑) FFIの「遅いから使わない」という常識が覆りつつあり、ZJITやFFXの成熟によってRubyからCライブラリを気軽に・高速に呼べる未来が近づいていると感じました。 ruby.wasm can also enable JavaScript to call Ruby libraries. 小島( @KojimaNaoyuki )です。私からはShigeru Nakajima( @ledsun )さんの「 ruby.wasm can also enable JavaScript to call Ruby libraries. 」を紹介します。 www.docswell.com このセッションでは、ruby.wasmのこれまで積み重ねられてきた改善を振り返りつつ、なぜまだ実務での採用事例が少ないのかの分析とそれを解消するために「JSからRubyを呼び出す」機能を強化していく方針が話されていました。 ruby.wasmとは、ブラウザ環境でRuby実行を可能にするものです。JavaScriptと相互で連携しながらRubyをブラウザ環境で実行できます。 ruby.wasmはこれまでRubyからJavaScriptを呼び出す改善を多く実施しています。しかし、現状、本番環境で利用されている例はあまりないそうです。その原因としてこのセッションでは以下の2点について言及されていました。 ruby.wasmバイナリが大きすぎる JSからRubyを呼ぶサポートが弱い ruby.wasmバイナリが大きすぎることについては https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.9.3-2.9.4/dist/ruby+stdlib.wasm を例に出して話されていました。こちらのファイルは非圧縮で31.80MiB、圧縮すると8.84MiBになるそうです。そして、BtoCのブラウザアプリケーションではダウンロードサイズが重要ですが、BtoBの場合はダウンロードサイズは大きな問題ではない可能性があるとセッションでは話されていました。 JSからRubyを呼び出すサポートが弱いことについては、RubyからJSを呼び出す場合とJSからRubyを呼び出す場合を比較して扱いやすさの違いを言及していました。以下がセッションで使用されていた表です。 Feature R → J J → R eval Yes Yes Method Call Yes call Property Access Yes N/A Object Conversion Yes string Load Ruby Scripts browser No 出典: ruby.wasm can also enable JavaScript to call Ruby libraries. | ドクセル (最終閲覧日:2026/05/07) こちらを見ると確かにJSからRubyを呼び出す場合にサポートされていないものが多い印象ですね。現状JSからRubyを呼び出すにはevalメソッドを利用して実行できますが、rubyの実行結果の戻り値を扱う適切なAPIが不足していたりと課題があるようです。 これらの現状を受けて、本番環境で利用されている例が少ないのは「JSからRubyを呼ぶ」機能が不十分であるからではないかと分析されていました。そして、もしJSからRubyを呼ぶことが簡単にできたなら、ロジックの二重実装を防ぐことができるため有用ではないかとおっしゃっていました。ロジックをRubyに統一できれば修正も容易になり保守性も向上しそうですね! 「JSからRubyを呼ぶ」機能を強化していくためにはRubyオブジェクトをラップするRbValueを拡張する必要があり、このプロジェクトを「Ruby’s Blanket」と呼ぶと発表がありました。ちなみに、「Ruby’s Blanket」という命名は函館出身の人気バンドGLAYの楽曲に由来しているともおっしゃっていました。 セッションでは、目指していきたい理想の姿も紹介されており、以下のような使用感を想定しているようです。 vm . evalFile ( "dog.rb" ) ; const dog = vm .eval ( “Dog . new” ) ; dog . vow () ; dog . vow () ; const count = dog . count_of_vows . toInt () ; 出典: ruby.wasm can also enable JavaScript to call Ruby libraries. | ドクセル (最終閲覧日:2026/05/07) JSネイティブなプロパティアクセスやメソッド呼び出しでRubyプログラムを扱っているところを示しています。すごく直感的にJSからRubyコードを呼び出せていますね! セッションの最後にはコーディングエージェントの活用についても語られていて、以前よりも難しい課題に取り組むことができていると語られていました。「Ruby Committers and the World」や「Matz Keynote」でもAI活用が議題に上がっており、多くのコミッターがAIを駆使してRubyの改善に当たっているそうです。 個人的には、言語実装のような「世界的にサンプルが少なそうな低レイヤーの領域」はAIが不得意な分野だと思い込んでいたため、これほど多くのコミッターが活用している事実に驚かされました。 Ruby本体や周辺ライブラリの開発が、AI活用によって今後さらに加速していくのが非常に楽しみです! このセッションを通して「JSからRubyを呼ぶ」機能が充実することで、Rubyの活用の幅が広がる未来を感じました。実際のプロダクトのブラウザ上でRubyが動いているところを目にすることを期待しています! The Less-Told Story of Socket Timeouts WEARのバックエンド開発を担当しているaao4seyです。私からは、 @coe401_ さんの「 The Less-Told Story of Socket Timeouts 」を紹介します。 このセッションでは、Ruby 4.0で Socket.tcp / TCPSocket.new に新しく加わった open_timeout の導入経緯と、その背後にあるsocketライブラリのタイムアウトの歴史が語られていました。本記事では open_timeout が必要になった経緯を発表内容を踏まえて整理し、続いて各Rubyバージョンで実際にタイムアウトの挙動がどう変わるのかを手元で観察した結果を紹介しようと思います。 なぜopen_timeoutが必要だったのか net/httpのtimeoutライブラリ依存問題 発表者のもとに「net/httpのtimeoutライブラリ依存を外したい」という相談が届いたことが、 open_timeout 導入のきっかけでした。スライド内でも紹介されていますが、Ruby 2.7の開発時代にもnet/httpのような標準ライブラリが、標準ではないライブラリに依存しないほうが良いのではという趣旨の提案がなされていました。 加えて、timeoutライブラリは内部で Timeout::State::GLOBAL_STATE という共有状態を持っているため、non-main Ractorからこれにアクセスすると Ractor::IsolationError が発生してしまう状況でした。 resolv_timeout + connect_timeoutでは代替できない Ruby 4.0開発時点で、 Socket.tcp / TCPSocket.new には名前解決のタイムアウトを指定する resolv_timeout と、接続確立のタイムアウトを指定する connect_timeout がすでに用意されていました。これらを組み合わせればtimeoutライブラリの代替になりそうにも思えますが、実際にはメソッド全体の絶対上限時間を制御できません。 ここで前提として1つ補足しておくと、Ruby 3.4で Socket.tcp / TCPSocket.new にはHappy Eyeballs Version 2 (HEv2 / RFC 8305)が fast_fallback という名前でデフォルト有効として導入されました。HEv2は複数候補のIPアドレスに対して名前解決と接続試行を並行実行するアルゴリズムです(HEv2 自体の解説は同僚が「 RubyKaigi 2025 協賛&参加レポート 」で書いているので、本記事ではそちらに委譲します)。なお、Ruby 3.4までは fast_fallback: false 経路の resolv_timeout がAPI onlyで実装されておらず、本記事の対象であるRuby 4.0で両経路で機能するように修正されています。 その上で、たとえばfast_fallback有効で resolv_timeout: 2000ms , connect_timeout: 1000ms を指定した場合、IPv6が先に解決されてIPv4が解決待ちのまま接続試行が始まると、 connect_timeout の1000msを過ぎても resolv_timeout の期限まで待機が続きます。結果、全体タイムアウトは2つの合計でも connect_timeout の値でもなく、 resolv_timeout の値である2000msに支配されてしまいます。 HEv2のConnection Attempt Delay (250ms)が絡むケースでは「 connect_timeout: 1000ms を指定しても全体は1250ms待つ」、fast_fallback無効では「IP数 × connect_timeout 」と、いずれもユーザーが指定した値とは別の値で全体時間が決まってしまいます。 open_timeoutのAPI仕様 これらの背景を受けて、発表者ご自身が open_timeout を Socket.tcp / TCPSocket.new に追加する提案をされました。 open_timeout はtimeoutライブラリと同じく「名前解決〜接続確立の全体」を1つの期限で管理するオプションです。 Socket .tcp( " ruby-lang.org " , 80 , open_timeout : 1 ) 設計上の論点として、 open_timeout を resolv_timeout / connect_timeout と併用された場合にどう振る舞うかを整理する必要がありましたが、複雑になりすぎてしまうという課題があり、最終的に open_timeout は connect_timeout / resolv_timeout との同時指定を禁止し、同時指定時は ArgumentError を投げる仕様となったそうです。 発表内では実装の細かい部分が fast_fallback ありなしごとに説明されていました! スライドのp.224以降に記載されています。 試してみる ここまで見てきた経緯を踏まえて、 open_timeout がRuby 4.0で動作するのかの検証をしてみます。また、私自身はRuby 3.4で導入された fast_fallback 機能もこの発表内で知ったので、こちらの挙動も合わせて確認してみることにしました。 実験のため /etc/hosts に応答しない3つのIPを割り当てた test-multi-local.wear.jp を用意しました(記載のIPはdummyです)。 192.168.xx.1 test-multi-local.wear.jp 192.168.xx.2 test-multi-local.wear.jp 192.168.xx.3 test-multi-local.wear.jp 接続先には応答しないポート(50000)を指定し、SYNを送っても応答が返ってこない状況を作ります。 Ruby 3.4: fast_fallback ON この場合、HEv2のアルゴリズムに従い以下のような挙動になるはずです。 約250ms間隔(Connection Attempt Delay)で次のIPへ並行に試行を発射している 期待結果: (IP数-1) × 250ms + connect_timeout = 1.5 秒 で全体タイムアウト 検証スクリプト require " socket " t = Time .now begin Socket .tcp( " test-multi-local.wear.jp " , 50000 , connect_timeout : 1 , fast_fallback : true ) { |s| s.close } rescue => e puts "#{ e.class } : #{ e.message }" ensure puts " elapsed= #{ Time .now - t } s " end 実行結果(Ruby 3.4.9) Errno::ETIMEDOUT: Operation timed out \- user specified timeout elapsed=1.51281s tcpdumpの結果抜粋 10:41:18.580 SYN → .1 ← IP1 開始 (t=0) 10:41:18.836 SYN → .2 ← IP2 開始 (t≒0.256) 10:41:19.086 SYN → .3 ← IP3 開始 (t≒0.506) 想定どおり、約1.5秒でtimeoutエラーになることが観測できました! また、tcpdumpでSYNパケットをざっくり観測した結果、約250msごとに異なるIPへSYNパケットが飛んでいることも見て取れました。 Ruby 3.4: fast_fallback OFF fast_fallback: false を渡すと、Ruby 3.3以前と同じ直列フォールバックの挙動に戻ります。以下のような挙動になるはずです。 各IPに対して connect_timeout: 1 で1秒ずつ順番に試行 期待結果: IP数 × connect_timeout = 3 秒 で全体タイムアウト 検証スクリプト 前述のスクリプトの Socket.tcp 呼び出し行を以下のように変更します。 - Socket.tcp("test-multi-local.wear.jp", 50000, connect\_timeout: 1, fast\_fallback: true) { |s| s.close } + Socket.tcp("test-multi-local.wear.jp", 50000, connect\_timeout: 1, fast\_fallback: false) { |s| s.close } 実行結果(Ruby 3.4.9) Errno::ETIMEDOUT: Operation timed out \- user specified timeout elapsed=3.013505s tcpdumpの結果抜粋 10:40:20.753 SYN → .1 ← IP1 開始 (t=0) 10:40:21.760 SYN → .2 ← IP2 開始 (t≒1.0) 10:40:22.764 SYN → .3 ← IP3 開始 (t≒2.0) 想定通り、fast_fallbackを無効にするとIP1 → IP2 → IP3と1秒ずつ順番に試行され、合計約3秒で全体タイムアウトすることが観測できました。 Ruby 4.0: open_timeout ここまでの結果から、 connect_timeout だけではメソッド全体の上限時間を制御できないことが見えました。Ruby 4.0で導入された open_timeout を使うと挙動がどう変わるかを見ていきます。 fast_fallback ON + open_timeout fast_fallback ONで open_timeout が機能するか試します。 期待結果:open_timeoutに指定した1秒でタイムアウト 検証スクリプト 前述のスクリプトの Socket.tcp 呼び出し行を以下のように変更します。 \- Socket.tcp("test-multi-local.wear.jp", 50000, connect\_timeout: 1, fast\_fallback: true) { |s| s.close } \+ Socket.tcp("test-multi-local.wear.jp", 50000, fast\_fallback: true, open\_timeout: 1) { |s| s.close } 実行結果(Ruby 4.0.3) IO::TimeoutError: user specified timeout for test-multi-local.wear.jp:50000 elapsed=1.004653s 想定通り、約1.0秒で IO::TimeoutError が発生し、 open_timeout: 1 で指定した時間とほぼ同じ時間でタイムアウトすることが観測できました! fast_fallback OFF + open_timeout fast_fallback OFFで open_timeout が機能するか試します。 期待結果:open_timeoutに指定した1秒でタイムアウト 検証スクリプト 前述のスクリプトの Socket.tcp 呼び出し行を以下のように変更します。 \- Socket.tcp("test-multi-local.wear.jp", 50000, connect\_timeout: 1, fast\_fallback: true) { |s| s.close } \+ Socket.tcp("test-multi-local.wear.jp", 50000, fast\_fallback: false, open\_timeout: 1\) { |s| s.close } 実行結果(Ruby 4.0.3) Errno::ETIMEDOUT: Operation timed out \- user specified timeout for 192.168.xx.1:50000 elapsed=1.007804s こちらも想定通り、約1.0秒で user specified timeout が発生し、 open_timeout: 1 で指定した時間とほぼ同じ時間でタイムアウトすることが観測できました!  fast_fallback の設定有無に関わらず open_timeout が機能していることがわかりました。 同時指定はエラー open_timeout は connect_timeout / resolv_timeout との同時指定が禁止されており、 Socket.tcp の入口で ArgumentError が即座に発火します。 Socket .tcp( " test-multi-local.wear.jp " , 50000 , connect_timeout : 1 , open_timeout : 1 ) { |s| s.close } # => ArgumentError: Cannot specify open_timeout along with connect_timeout or resolv_timeout 全体の対比 ここまで観測した結果を並べると以下のようになります。 Ruby 設定 elapsed 3.4.9 fast_fallback: false , connect_timeout: 1 約 3.01 秒 3.4.9 fast_fallback: true , connect_timeout: 1 約 1.51 秒 4.0.3 fast_fallback: false , open_timeout: 1 約 1.01 秒 4.0.3 fast_fallback: true , open_timeout: 1 約 1.00 秒 connect_timeout だけを指定したケースでは、ユーザーの指定値とは別の値で全体時間が決まっていたのに対し、 open_timeout を指定すれば 指定値ぴったりで打ち切られる ようになりました。 おわりに 本パートではセッションの内容である open_timeout の導入経緯と、実際に fast_fallback や open_timeout の挙動を観測した結果を紹介しました! open_timeout の実装により、接続試行全体のタイムアウトが管理しやすくなったと感じます。実際のセッションではRuby 2.0の開発時代から今に至るまでのsocketライブラリのタイムアウト導入の歴史も詳細に説明されており、各時代ごとにどんな課題がありどう解決してきたのかを知ることができとても興味深かったです。また、このセッションに限らずですが、RubyKaigi全体を通して発表者の方のモチベーションを垣間見ることができたのも自分にとって良い刺激となりました。 Autoresearching Ruby Performance with LLMs 小山 です。私からはNate Berkopec( @nateberkopec )さんの「 Autoresearching Ruby Performance with LLMs 」を紹介します。 Berkopec氏は、Speedshop代表でありPumaメンテナー、Railsパフォーマンスコンサルタントとして広く知られています。本セッションは Autoresearch という先行ツールを参考に、AIエージェントを使って、Rubyのパフォーマンス問題を自動で調査する取り組みとそれにより得られた示唆の発表でした。 発表資料は以下のリポジトリで公開してくださっているためご参照ください。 github.com Autoresearchとは Autoresearchは、2026年3月にAndrej Karpathy( @karpathy )氏が公開したLLM実験自動化ツールです。仕組みはシンプルです。 AIエージェントがコードへの変更を提案・適用する 一定時間ベンチマークを実行する ベースラインより良ければコミット、悪ければリバート 1に戻る これを無限に繰り返します。PyTorchと単一のメインファイル以外に依存関係がなく、1時間あたり約12実験、一晩で約100実験を回せます。Berkopec氏がこのコンセプトをRubyのパフォーマンス改善に応用できるかを探ったのが今回の発表です。 なぜ「ループ」が重要なのか セッションで繰り返し強調されたのが ループ という概念です。 AIエージェントは本質的にはループにすぎないと、Berkopec氏は次のコード例で説明しています。 messages = [user_prompt] loop do reply = llm.call(messages, tools : TOOLS ) break puts(reply.text) unless reply.tool_call? result = run_tool(reply.tool_name, reply.arguments) messages << reply messages << tool_result(result) end このループに どのようなゲート(通過条件)を置くか が、ループの性格を決めます。それがセッション全体の一貫したテーマでした。 4つのループパターン Berkopec氏は、ゲートの種類によってループを4種類に分類しました。 ループ ゲート シグナル 成果物 Agents LLM 自己停止 離散 最終的な返答 Ralph ビルド+テスト 離散 グリーンなコミット Autoresearch ベンチマーク差分 連続 改善した diff Factory 多数のチェック 多変数 マージ可能な PR Ralphループ while :; do cat PROMPT.md | claude-code ./build \_ and \_ test || continue git add \- A git commit \- m " ralph: passing build " git push done ビルドとテストをゲートにしたループです。テストが通った変更だけをコミットし続けます。バグへの対処に向いています。 Autoresearchループ best \= benchmark loop do change \= agent.propose\_optimization apply(change) score \= benchmark if score \> best git\_commit(change.summary) best \= score else git\_revert end end ゲートがブール値(pass/fail)ではなく 連続値(ベンチマークスコアの改善量) である点がRalphとの違いです。 Factoryループ backlog.each do |spec| loop do code \= agent.implement(spec) gates \= scenarios.map { |s| s.run(code) } break if gates.all?(& :pass? ) end ship(code) end スペックからコードを生成し、複数のゲートを全て通過したら出荷するパターンです。StrongDM社の「 Software Factory 」が参照されていました。 Sidekiqでの実証実験 セッション中に紹介された実験の1つが、 pi-autoresearch を用いた、Sidekiqを対象にした自動最適化です。 実験の背景 Sidekiqの Processor::Counter はアトミックなカウンターで、16行程度のシンプルな実装です。Autoresearchエージェントはここにストライプドロック(各スレッドが自前の状態を持ち、書き込みを安くする仕組み)を適用する変更を提案しました。 また、別のPRでは Time.now.to_i を Process.clock_gettime に置き換えることも提案されました。 Comparison: Process.clock_gettime: 17294486.8 i/s Time.now.to_i: 12698329.6 i/s - 1.36x slower Timeオブジェクトを生成せずに整数を直接返すことで1.36倍高速化できます。 "マージされないコードを生成することの意味" これらの変更についてBerkopec氏は「OSSメンテナーは実際にはこれをマージしないだろう」と正直に述べていました。その上で、なぜマージできないコードを生成するのか、という問いを投げかけました。 "Why generate code that you can't merge?" この問いに対して、セッションは PR レビューの目的を再定義するパート へと展開しました。 PRレビューとは何か? 変更を却下する理由として、Berkopec氏は以下を挙げました。 バグがある → LLMはある程度得意 トレードオフがある →「十分に速い」とはどのレベルか? 複雑すぎる → Flog/Flayスコア、ABCスコア、LOC リスクが高すぎる → キャッシュ無効化など(Autoresearchはキャッシュが大好きで、しかもバグりやすい) テストを通るが... → Autoresearchは良いテストがある前提で動く GPL 違反など → GitHub外の法的・コンプライアンスチェック 要するに 「マージできるか」を判断する能力こそがArchitectである という主張です。 4つのレッスン セッションを通じて提示された4つのレッスンをまとめます。 Lesson 1: 「自動調査」であって「自動変更」ではない Stan Lo氏(Shopify)の取り組みが紹介されました。Ruby-lspのCI時間を33%削減、rubydexのインデックス速度を10%〜50%改善するといった成果を、AIの提案を自身がレビューした上で実現しています。AIの出力を人間が検証・理解して取り込む姿勢が重要だという例です。 Lesson 2: 自分がオーナーで Architect でないものに Autoresearch を適用しない 理解・検証・修正できるコードにのみ適用すべきです。 Lesson 3: ループはビター・レッスンを実践する Richard Suttonさんの「苦い教訓(Bitter Lesson)」(人間の知識を組み込むより、計算のスケールによって探索・学習する方が長期的に優れるという教訓)をループは体現しています。ループは人間のゲートを担保するバージョンに過ぎず、ゲートをソフトウェアに置き換えることで探索をスケールさせられます。 Lesson 4: 人間のゲートをソフトウェアゲートに変換する 「遅い」という感覚を赤/緑の状態・バグチケットに変換するプロセスがパフォーマンス改善の本質です。たとえAutoresearchが失敗しても、ゲートを定義する過程でソフトウェアの品質自体が上がります。 Software Factory とその課題 セッション後半では、 Software Factory (StrongDM社の提唱するコンセプト)が紹介されました。 "Code must not be written by humans. Code must not be reviewed by humans." — StrongDM's "Software Factory" これは人間のレビューを完全に排除し、多数の自動ゲートを通過したコードのみを出荷する考え方です。セッションではこれを「dark factory」と表現し、Berkopec氏は多くの未解決の問いを並べて紹介しました。 形式手法・プロパティベーステストは現実的にスケールするか? 既存の大規模コードベース(ブラウンフィールド)に適用できるか? LOCの肥大化をどう防ぐか? LLMがトレーニングデータの著名な最適化を「再発明」するだけにならないか? 決定論的なゲートとLLMジャッジをどう組み合わせるか? Berkopec氏はこれらに対して現時点で答えはないとしながらも、「ゲート設計こそが重要になる」という方向性を示しました。 まとめ Berkopec氏のセッションを一言でまとめると、 「AI に自律的なループを回させることはできるが、それを意味のある成果に変えるにはゲート設計という人間のスキルが必要」 というメッセージでした。 発表で推奨されていた実践ステップは以下の通りです。 自分が十分に理解・レビューできるコードにのみ適用する(Stan Lo氏のアプローチ) 成熟した小さなモジュールから始める ゲートが最小で済む場所からスタートする ゲートを増やせば人間のレビューを減らせる Ralph(離散)・Autoresearch(連続)・Factory(多ゲート)のいずれかのループを試してみる おわりに 今年のRubyKaigiはAIに関するセッションの多さが印象的でした。普段の業務で自身もAIを活用しています。本セッションで紹介されていたゲートの設計・導入に注力して、パフォーマンス改善はもちろんのこと、それ以外のあらゆる業務課題の自動化をより推進していきたいと思いました! Exploring RuboCop with MCP 伊藤です。今年もRubyKaigiに参加してきました! 私からは @koic さんの「 Exploring RuboCop with MCP 」をご紹介します。 speakerdeck.com 本セッションは「I. MCP Ruby SDK」「II. RuboCop x MCP」の2部構成で、前半では @koic さんがコミッターを務める公式の MCP Ruby SDK ( mcp gemとして公開)の設計や、Streamable HTTPでのセッション管理・Sampling・Elicitationまでが解説されました。後半は、RuboCop 1.85.0で実験的に追加された組み込みMCPサーバーの紹介でした。 ここでは特に後半「RuboCop x MCP」を題材に、WEARのバックエンドエンジニアとして実際に手元で動かしてみた感想を中心にご紹介します。 rubocop --mcp は、RuboCop 1.85.0から導入されたサブコマンドで、AIエージェント(Claude CodeなどのMCPクライアント)からRuboCopを呼び出せるようにする組み込みMCPサーバーを起動します。stdio transportで通信し、以下の2つのツールを公開します。 rubocop_inspection :オフェンス検査(読み取り専用) rubocop_autocorrection :オートコレクト適用(破壊的) なお、RuboCopではコーディング規約違反のことを「オフェンス(offense)」と呼びます。以降本記事でもこの用語を使います。 rubocop --lsp の内部で使われている診断機構( RuboCop::LSP::Runtime )をそのまま再利用しているのが特徴で、出力されるオフェンスはLSPの Diagnostic 形式(JSON)です。Clang形式のテキストよりも構造化されているため、AIエージェントがそのままパースして扱いやすい形になっています。 ここからは実際に手元で動かしてみます。 rubocop --mcp を使うには、RuboCop本体に加えて mcp gemが必要です(RuboCopは mcp をruntime dependencyとして持っていないため)。既存のGemfileのdevelopmentグループに以下を追加します。 group :development do gem ' mcp ' end 次にClaude Codeに .mcp.json でRuboCopのMCPサーバーを登録します(Claude Codeが設定を読み、起動時に bundle exec rubocop --mcp をサブプロセスとして立ち上げてくれます)。 { " mcpServers ": { " rubocop ": { " command ": " bundle ", " args ": [ " exec ", " rubocop ", " --mcp " ] } } } Claude Code上で /mcp を叩くと、登録した rubocop サーバーから2つのツールが認識されているのが確認できます。 ❯ /mcp ──────────────────────────────────────────────────── Tools for rubocop 2 tools ❯ 1\. RuboCop's inspection read-only 2\. RuboCop's autocorrection destructive それでは実際に rubocop_inspection を呼んでみます。オフェンスを意図的に仕込んだ以下のサンプルファイル sample.rb を用意して、検査を呼んでもらいます。 # frozen_string_literal: true def greet ( name ) message = " Hello, " +name+ " ! " puts message end greet( " WEAR " ) 「 sample.rb をRuboCopで検査して」とお願いすると、Claude Codeが rubocop_inspection ツールを呼び、結果を以下のように整形して提示してくれました。 ❯ sample.rb をRuboCopで検査して Called rubocop (ctrl+o to expand) ⏺ sample.rb の検査結果: 9 件の offense が検出されました(すべて自動修正可能)。 ┌─────┬─────────────────────────────┬─────────────────────────────┐ │ 行 │ Cop │ 内容 │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 2 │ Layout/SpaceInsideParens │ 括弧内の余計なスペース(2 │ │ │ │ 箇所) │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 3 │ Style/StringConcatenation │ \+ 連結より文字列補間を推奨 │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 3 │ Style/StringLiterals │ "Hello, " → │ │ │ │ シングルクォート推奨 │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 3 │ Style/StringLiterals │ "\!" → シングルクォート推奨 │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 3 │ Layout/SpaceAroundOperators │ \+ 演算子の前後にスペース無 │ │ │ │ し(2 箇所) │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 4 │ Layout/IndentationConsisten │ インデントが不揃い │ │ │ cy │ │ ├─────┼─────────────────────────────┼─────────────────────────────┤ │ 7 │ Style/StringLiterals │ "WEAR" → │ │ │ │ シングルクォート推奨 │ └─────┴─────────────────────────────┴─────────────────────────────┘ すべて rubocop \-A で自動修正できます。自動修正を実行しますか? 「自動修正を実行しますか?」というフォローアップ付きで、続けて rubocop_autocorrection ツールへの切り替えまで自然に提案してくれているのが分かります(行番号がLSPの0-indexed値のまま表示されているのは、RuboCopが返すLSP Diagnosticの値をClaude Code側で1-indexedに変換せずに表示しているためです)。 このとき裏で rubocop_inspection から返ってきている生のJSONは、紙面の都合で code_actions を省略し、検出された9件のうち代表2件のみ抜粋すると以下のようになっています。 { " files ": [ { " path ": " sample.rb ", " offenses ": [ { " range ": { " start ": { " line ": 2 , " character ": 10 } , " end ": { " line ": 2 , " character ": 11 } } , " severity ": 3 , " code ": " Layout/SpaceInsideParens ", " codeDescription ": { " href ": " https://docs.rubocop.org/rubocop/cops_layout.html#layoutspaceinsideparens " } , " source ": " RuboCop ", " message ": " Layout/SpaceInsideParens: Space inside parentheses detected. ", " data ": { " correctable ": true , " code_actions ": [ /* 省略 */ ] } } , { " range ": { " start ": { " line ": 3 , " character ": 12 } , " end ": { " line ": 3 , " character ": 30 } } , " severity ": 3 , " code ": " Style/StringConcatenation ", " codeDescription ": { " href ": " https://docs.rubocop.org/rubocop/cops_style.html#stylestringconcatenation " } , " source ": " RuboCop ", " message ": " Style/StringConcatenation: Prefer string interpolation to string concatenation. ", " data ": { " correctable ": true , " code_actions ": [ /* 省略 */ ] } } ] } ] , " summary ": { " target_file_count ": 1 , " offense_count ": 9 } } LSPの診断と同じ形式なので、各オフェンスにcop名・該当範囲・cop個別のドキュメントURL・自動修正可否( correctable )まで構造化された形で含まれます。さらに省略した code_actions には実際のautocorrectで挿入する文字列(例: Style/StringConcatenation なら "Hello, #{name}!" への置換)や、 # rubocop:disable を入れて該当行で無効化する案もLSPの WorkspaceEdit 形式で含まれており、エージェントから機械的に扱えるようになっています。 rubocop_autocorrection ツールに切り替えれば、そのまま破壊的に修正をかけることもできます。 WEARのバックエンドではすでにRuboCop+各種pluginを大規模に運用しているので、生成AIによる開発を取り入れる際にも「RuboCopを通っている」という決定的なゲートはそのまま使い続けたい、と日頃から思っていました。本セッションは、まさにその「決定的なツール(RuboCop)」と「確率的なLLM」を rubocop --mcp というかたちで素直に橋渡ししてくれる発表で、聞きながら自分の開発フローに組み込む絵が具体的に浮かびました。 セッション後半で @koic さんが投げかけられていた "What happens when combined?"(決定的なツールと確率的なLLMを組み合わせると何が起きるか)という問いも印象的でした。例えば、現状の .rubocop.yml で無効化しているcopをLLMに一時的に有効化させ、レビュー観点として「いま無効になっているcopをあえて見たらどう見えるか」を提示してもらう、といった使い方は、ツール単体だと出てこない発想です。Streamable HTTPでのSamplingやElicitationまで含めて、まだ「探索が始まったばかり」という締めくくり通り、これからRuboCop×MCPでどんな試みが出てくるかとても楽しみです。 The Journey of Box Building 坂元( @sakam0cchan )です。私からはSatoshi Tagomori( @tagomoris )さんの「 The Journey of Box Building 」というセッションを紹介します。 speakerdeck.com このセッションは、Ruby 4.0で実験的に導入される新機能「Ruby Box」をテーマにしたKeynoteです。Boxの基本概念から、実行中のRubyプログラムの中でBoxを特定する際の難しさ、そしてその実装に至るまでの背景などが語られていました。 私自身が初めてRubyKaigiに参加し、そして初めて聞いたKeynoteがこの内容だったこともあり、これからの3日間がどんなものになるかとてもわくわくしました。そんな思いも込めて、内容を共有します。 Ruby Boxとは何か Ruby Box(RubyKaigi 2025までは「Namespace」と呼ばれていた機能)は、Ruby 4.0で導入された実験的な機能です。 Ruby Boxは、これまでRubyのプロセス全体で共有されていたクラスやモジュール、モンキーパッチなどを、別の空間(Box)として分離し読み込み・実行できる革新的な機能となっています。たとえば、 something.rb に書かれたクラスを、メイン環境とは独立したBoxの中に読み込むイメージです。 # something.rb class Something def hello = " hello from Something " end # main.rb box = Ruby :: Box .new box.require( ' something ' ) s = box:: Something .new p s.hello # => "hello from Something" Boxの中で読み込んだ Something は box::Something としてだけアクセスでき、Boxの外からは見えません。これにより、ライブラリやモンキーパッチをプロセス全体へ漏らさずに使えるのがBoxのコアアイデアです。セッションの中では、以下のような用途がBoxの利用シーンとして挙げられていました。 一部のコードにて有効なモンキーパッチの適用 テスト内でのモック処理 異なるバージョンのライブラリに依存する複数のアプリケーションを、同じプロセス内に同居させる どちらにも同じリクエストを送ることで、異なるバージョンのライブラリでの挙動を比較する このように、Boxは「同じプロセス内での隔離された環境」を提供することで、これまでRubyで難しかったことを可能にする機能として期待されています。 Ruby Boxの仕組み 本セッションでは、Ruby Boxを実現する上で必要不可欠となる「現在、実行中のコードがどのBoxに属しているか」を判定する仕組みが詳細に解説されていました。 Ruby Boxでは、「1つのファイルを、別のBoxで同時に読み込む」といったことが可能です。つまり、ファイル自体は特定のBoxに縛られていないため、プログラムが実行されるたびに、Ruby自身が「自分は今、どのBoxの中で動いているのか?」を動的に特定し続けなければなりません。 Boxの特定・決定のプロセスは、以下のようなステップで行われているとのことでした。 現在実行中のフレームを特定する まず、自分がいま実行しているControl Frameを見つける LOCALな変数スコープを探す そこから、ブロックや例外処理といった非ローカルなものを無視し、ファイル全体、クラス、メソッドといった「LOCAL(局所的)」なスコープを持つフレームまでさかのぼる Boxの情報を引っ張り出す そこで、見つけたLOCALなフレームの中に保存されている情報をたどり、そこに紐付いている「Box情報」を取得する ここで一番興味深かったのは、Control Frameとローカル変数などを管理しているENVとの関係性です。それぞれはRubyを実行する上で、現時点でどのコードが動いているのかを把握するためのものです。Rubyのメモリ上では、Control FrameとENVの2つが両端から内側に向かって伸びていく特殊な構造となっているそうです。 この二つが衝突すると、Stack Overflowが発生するという話をこのセッションで初めて知り、今まであまり意識していなかったRubyの内部構造について、もっとより知って勉強したいと改めて感じました。 Ruby Boxが生まれた歴史 さらに、印象に残っているもう1つのトピックとして、Ruby Boxが生まれた歴史についての話があります。開発者のTagomoriさんがRuby Boxの開発を始めたきっかけは、RubyKaigi 2023で発表された 「Multiverse Ruby」のセッション でした。その内容が、過去に自身が抱えていた課題と重なると感じたことで、一気にモチベーションが高まったそうです。 さらに、その会期中に発表者の方と直接話す機会があり、当初は「Hako」という命名にして函館のRubyKaigiで発表しよう、という話もあったそうで…。そして実際に今、こうして発表に至っている、なんと胸が熱くなる展開でしょうか。 こんなエピソードを通して、「みなさんにも、ふとモチベーションが湧き上がる瞬間があるかもしれない。あなたにとっての『Multiverse Ruby』があるかもしれない。そんなプロセスも含めてRubyKaigiを楽しんでほしい」という言葉が印象的でした。最初のセッションでその一言を聞いたとき、これから始まる3日間のRubyKaigiにとてもわくわくしたあの瞬間を、今でも覚えています。そんな開発の裏エピソードも含め、とても印象的なセッションでした。 おわりに 本セッションは「新機能の紹介」というよりも、どんなふうに実現されているのか、さらにその機能が生まれた瞬間を聴ける発表で、聴いた後の余韻が長く残るキーノートでした。 WEARのバックエンドはRuby on Railsで開発・リプレイスを進めており、monkey patchが絡む箇所のスコープ管理に苦労する場面が今後出てくるかもしれません。Boxのように「実行時の境界」を表現できるようになれば、たとえばライブラリの差し込みやテスト環境固有の上書きを、より見通しよく扱えるようになる可能性を感じました。Ruby Boxはまだ実験的な機能ではありますが、今後も引き続き注目していきたいです! ZOZOブースの紹介 こんにちは。Developer Engagementブロックの wiroha です。ここからはZOZOや各社の協賛ブースの様子を紹介します。 今回は出発当日の朝、管制トラブルの影響で飛行機の運航に乱れが出ている状況からのスタートとなりました。搭乗予定の便も遅延か欠航かの見通しが立たなかったため、早々にキャンセルして新幹線へ切り替えたことが功を奏し、4〜5時間かかったものの比較的早めの時間に函館入りできました。また直前の地震の影響で北海道では配送の遅延が起きていたようですが、幸いブースの荷物は無事に届いており、予定どおり運営できそうでほっとしました。 今回のブースはファッション×AIの2つの新しい体験をご用意しました。ひとつは「 Apps in ChatGPT 」、もうひとつはWEARアプリ内の「 着回し提案 」機能です。 ファッション×AIの新機能を体験しよう! 「 Apps in ChatGPT 」の体験では、技術カンファレンスに合うコーディネートを相談する方や、自分の写真を撮って「もっとおしゃれにするにはどうしたらいい?」と質問する方など、それぞれ自由な対話を楽しんでいる様子が見られました。 「Apps in ChatGPT」を体験中の様子 「 着回し提案 」は特定のアイテムを選択すると、ファッションに特化したAIがユーザーの好みに合わせた着回しを提案します。「知らなかった、便利そう!」という感想を多くいただきました。 「着回し提案」を体験中の様子 いずれかの体験をしていただいた方に、ZOZOらしいノベルティの「 洗濯ネット 」をプレゼントしました。「ちょうど今日洗濯しようと思ってました」「洗濯ネットが欲しくて体験しに来ました」といった声があり、日常的に使えるアイテムとして多くの方に喜んでいただけました! ZOZOらしいノベルティの洗濯ネット 協賛企業ブースのコーディネートまとめ assu_ です。函館へはすんなり辿り着く──はずでした。伊丹空港で突如届く欠航の知らせ、満席の函館便、迫られる決断。滑り込んだ新千歳行き、やけくそで高い寿司を買い、特急北斗で函館へ。家を出てから実に13時間、極寒の函館に到着──虚無の心を救ったのは、函館駅前のハセガワストア。波乱の移動を経て辿り着いた会場には、同じように大変な思いをした人も少なくありませんでした。 そんな会場から、前回の RubyKaigi 2025 同様、素敵なコーディネートの協賛企業の皆様を撮影させていただきました! デザインスポンサーのGaji-Laboさん Committer限定のMA-1は、希少性と刺繍の2026デザインがとてもクール。 北海道らしいイラストをあしらったフーディーの note さん。 パンダをモノクロにすると北海道らしくなったという GMO Flatt Security さん。 CM でおなじみのキャラクターにより個性的なデザインの ファインディ さん。 シンプル胸元とカタカナ背中の対比が良い、ネットプロテクションズさん。 襟シャツに絞り付きの裾、男性も女性も着こなし上手だったギフティさん。 ロゴのラインが今っぽい、視認性が高くセンスの良いWEDさん。 誇り高きデザインにより、長い Ruby歴 を表しているKOMOJUさん。 桜が満開の函館に一番ぴったりなカラーと、法被で華やかなwithさん。 Wellness Sponsorらしいブラックで “ W ”orkoutしやすそうなhacomonoさん。 北海道・函館らしいデザインや、変化する気温に対応しやすそうな形が目立った回でしたね。お忙しい中ご協力いただいたブースの皆さん、本当にありがとうございました! RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜を開催します RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜 5月13日(水)にRubyKaigi 2026スポンサー企業の株式会社ZOZO、株式会社リブセンス、株式会社TOKIUM、株式会社マイベストでRubyKaigi 2026のアフターイベント「 RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜 」を開催します。 RubyKaigi 2026に参加した方も、参加できなかった方も、ぜひお気軽にご参加ください! mybest.connpass.com おわりに 世界中のRubyistが函館の地に集まりました! ZOZOは毎年RubyKaigiに協賛し、ブースを出展しており、今年も多くの方々との交流を通じて有意義な時間を過ごすことができました。実行委員会の皆様、そして温かく迎えてくださった函館市の皆様に感謝申し上げます。来年も再び素晴らしい時間を共有できることを楽しみにしております! 技術カンファレンスでは恒例のスポンサーパネルへのサイン! RubyKaigi 2027 #rubykaigi 🗓️ 14..16 Apr 2027 🥭 Miyazaki, Japan — RubyKaigi (@rubykaigi) 2026年4月24日 そして、次回の開催地は私たちZOZOの宮崎オフィスがある宮崎県です。宮崎在住エンジニアも数名在籍しているので、地元を生かしたイベントなどができると良いですね。それではまた来年、RubyKaigi 2027でお会いしましょう。現場からは以上です! ZOZOでは、来年のRubyKaigi 2027を一緒に盛り上げるエンジニアを募集しています。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com
はじめに こんにちは、ZOZOTOWN開発本部でiOSエンジニアをしている續橋( @tsuzuki817 )です。 2026年4月13日〜14日に開催された try! Swift Tokyo 2026 にて、「GeoJSON×SwiftUI:地図を“美しく”描くための技術」というタイトルで20分のトークをしました。 speakerdeck.com www.youtube.com 本記事では、プロポーザルの準備から採択、トーク作成、社内での練習とフィードバック、そして登壇当日までの道のりをお伝えします。これからカンファレンスへの登壇を考えている方の参考になれば幸いです。 目次 はじめに 目次 try! Swift Tokyo とは ZOZOのiOSエンジニア全体でプロポーザルに挑む プロポーザルを考える会の開催 GPTを活用した「try! Swift プロポーザル コーチ」 採択 — LT枠から20分トークへ トーク作成 チーム内トーク練習とフィードバック スライドの改善 — 英語化とビジュアライズ ビジュアライズの強化 英語化 登壇当日 組織横断で登壇を支える意義 まとめ try! Swift Tokyo とは try! Swift Tokyo は、世界中からSwift開発者が集まる国際カンファレンスです。2026年は4月12日〜14日の3日間にわたって立川ステージガーデンで開催されました。4月12日がワークショップ、4月13日〜14日がカンファレンス本編という構成です。 トークの言語は自由で、AI翻訳による同時通訳が提供されています。多くのスピーカーが英語で発表する中、私はスライドを英語で作成し、発表自体はAI翻訳に任せて日本語で行いました。 ZOZOのiOSエンジニア全体でプロポーザルに挑む プロポーザルを考える会の開催 try! Swift Tokyo 2026のプロポーザル募集が告知されたタイミングで、ZOZOのiOSエンジニア全体に向けて 「プロポーザルを考える会」 を企画しました。 企画の背景には、いくつかの想いがありました。正直なところ、私自身もtry! Swiftへのプロポーザル提出は初挑戦で不安がありました。一人で世界的なカンファレンスに挑むのはハードルが高いと感じていました。だからこそ仲間を募り、みんなで一緒に挑戦したいと考えました。一人だと尻込みしてしまうことでも、みんなで「お祭り」のように取り組めば乗り越えられると思いました。そして、結果(採択)以上に、みんなで知見を出し合う「プロセス」そのものを大事にしたいと考えました。この活動を通じて、プロダクトの枠を超えた横のつながりを深めたいという想いもありました。 ZOZOTOWN、WEAR、FAANSなど複数のプロダクトチームから横断的に参加を募り、2026年1月7日に開催したところ、8名が参加しました。 会のアジェンダは以下のとおりです。 イントロ — 会の目的説明、try! Swift Tokyo 2026のスケジュール共有(締切1月末、採択2月中旬) 採択傾向の共有 — 過去セッションの分析 プロポーザル例の紹介 — 私のプロポーザル2案を紹介し、フィードバックを募集 ネタ出しブレスト — 各自のアイデアを出し合う まとめ & Next Action — 興味のあるネタの深掘り担当決め、次回開催について 「ネタがなくてもとりあえず参加して、みんなのネタを見てプロポーザルが閃くかもしれない」というスタンスで、飛び入り参加も歓迎としました。結果的に、この会をきっかけに、ZOZOTOWN・WEAR・FAANSの 3プロダクト横断で5名 がプロポーザルを提出できました。 GPTを活用した「try! Swift プロポーザル コーチ」 プロポーザルの質を高めるために、ChatGPTのカスタムGPT機能で 「try! Swiftプロポーザルコーチ」 を作成しました。 このコーチは、雑にネタを投げるとtry! Swiftに提出できる形へプロポーザルを整形してくれるものです。過去のtry! Swiftで採択されたプロポーザルの傾向や、効果的なプロポーザルの書き方の知見を組み込んでいます。 作成したコーチは、プロポーザルを考える会の場でも参加者に共有しました。自分のプロポーザルを磨くだけでなく、他のプロダクトチームのメンバーにも活用してもらうことで、全体のプロポーザルの質を底上げできました。ツールとして共有することで、個人の経験や知見に依存せず、誰でも一定水準のフィードバックを得られる仕組みになったのは大きな成果でした。 採択 — LT枠から20分トークへ プロポーザルは「GeoJSON×SwiftUI:地図を"美しく"描くためのサイズ設計と描画テク」というタイトルでLT枠として提出しました。 2月中旬、try! Swift運営チームから採択のメールが届きました。驚いたのは、 LT枠として提出していたにもかかわらず、20分のトーク枠での登壇を打診された ことです。運営からは「可能であれば20分のトークでお願いしたい」とのことで、二つ返事で承諾しました。 ZOZOTOWN、WEAR、FAANSの各チームから合わせて5名がプロポーザルを提出しましたが、採択されたのは私のみでした。それでも、組織横断の取り組みから「次こそは」という機運が生まれたことは大きな収穫でした。 トーク作成 採択が決まってからは、20分のトーク内容を作り込んでいきました。 テーマは、個人開発アプリ「旅行思い出マップ」で直面した、GeoJSONを使った地図描画の技術的な挑戦です。市町村レベルの地形に沿って写真を切り抜き、地図上にパズルのように配置する機能を実装しました。本トークでは、図法(投影法)の違いによる地図の歪みの解決方法を解説する内容としました。 まず、Claude Codeを使ってプロポーザルと実際のプロジェクトのコードから発表の流れを組み立てました。しかし、最初に生成された構成は同じ内容の重複や、話の順番の繋がりの悪さが目立ちました。細かく指示を出して何度も修正を重ね、ようやく納得のいく台本に仕上がりました。 台本が完成した後は、Claude Codeでベースのスライドを生成しました。ただ、生成されたスライドは好みに合わなかったため、Keynoteで自分好みにスライドを分割したり、画像を差し込んだりして手作業で仕上げていきました。 トークの構成は以下のとおりです。 イントロ & アプリ紹介 MapKitの限界と画像ベースの苦労(Before) GeoJSONとの出会い(転換点) 描画パイプライン全体像 GeoJSONパース 座標変換とMercator投影 SwiftUIでの描画 地図を"美しく"する工夫 まとめ チーム内トーク練習とフィードバック スライドの初版が完成した段階で、3月27日にチーム内でトーク練習会を実施しました。実際に20分の通し発表をし、メンバーからリアルタイムでフィードバックをもらいました。 フィードバックは以下のようなものでした。 GeoJSONの説明タイミング —「GeoJSONに触れてから1分くらい経っていたので、もう少し早めに説明へ入ったほうがいい」 技術的な説明の速度 —「GeoDataProviderやMKGeoJSONDecoder、MKGeoJSONFeatureのくだりが速くてあまり入ってこない。全体的な流れのイメージがあるといい」 ビジュアルの追加 —「地図の説明はビジュアルで見たい」「もうちょっと実際の地図との関連性がわかるといい」 Before-Afterの対比 —「プログラムと図(Before-After)をセットで見られるともっと良さそう」「プログラムのページをわかりやすくするだけでもっと良くなりそう」 コードの細かな指摘 — インデントやスラッシュの数など、スライド上のコード品質についても指摘をもらいました チームメンバーの率直なフィードバックのおかげで、説明の流れや視覚的なわかりやすさについて多くの改善点を発見できました。一人では気づけない「聴衆視点」の指摘は非常に貴重でした。 スライドの改善 — 英語化とビジュアライズ 練習会でのフィードバックを受けて、以下の改善をしました。 ビジュアライズの強化 フィードバックで特に多かった「視覚的にわかりにくい」という指摘に対応するため、以下を追加・改善しました。 Before-Afterの図をコードとセットで表示 描画パイプラインの全体像を図解 離島を含む地図描画のビジュアル例を追加 実際の地図との対比がわかるスクリーンショットを追加 ビジュアライズにはClaude Codeを活用しました。実際のGeoJSONデータを渡してSwiftUIのコードを生成してもらい、そこから画像を出力する形で対応しました。実データを使って図を描画できるため、スライド上の説明と実際の動作が一致した正確なビジュアルを効率よく用意できました。 英語化 try! Swift Tokyoは国際カンファレンスのため、スライドは英語で作成しました。発表自体はAI翻訳による同時通訳を活用し、日本語で行う方針としたため、日本語で作成したスライドを英語に翻訳する作業が必要でした。 英語化にはClaude Codeを活用し、作業を効率的に進めました。ただし、AIによる翻訳だけでは伝えたい感情やニュアンスを正確に表現しきれない部分がありました。そこで、チーム内で英語に強いメンバーの、らぷ( @laprasdrum )と小松( @tosh_3 )にレビューを依頼しました。2人には「ここはこういう意図で伝えたい」という部分の表現をかなり細かく指摘してもらい、機械的な翻訳では得られない自然で伝わる英語に仕上げられました。AIと人間のレビューを組み合わせることで、スライドの完成度を大きく引き上げられたと感じています。 登壇当日 4月11日のスライド提出締め切りを経て、4月12日のワークショップ、そして4月13日〜14日のカンファレンス本番を迎えました。 会場に着いて受付でスピーカーのプレートをもらいました! 会場は想像していたよりもかなり広く、とても迫力がありました。 歴代のtry! Swiftのポスターが飾られており、10周年のタイミングで参加できてとても光栄に思いました。 登壇終了後にステージから撮影した写真です。本番中は緊張してしまい、会場の方をほとんど見られていなかったのが反省点です。 登壇とAsk the Speakerを何とか終え、開放感からすぐにスポンサーブースを回りました。 可愛いお菓子もデプロイされていました! 組織横断で登壇を支える意義 今回の登壇を振り返ると、個人の努力だけでなく ZOZOのiOSエンジニア全体の支えがあったからこそ実現できた と強く感じます。 プロポーザル段階 : ZOZOTOWN・WEAR・FAANSの各チームから横断的にプロポーザルを考える会を開催し、互いのアイデアにフィードバックを送り合いました ツールの共有 : GPTを活用したプロポーザルコーチを作り、全員が活用できるようにしました トーク練習 : 社内でリハーサルを行い、聴衆視点からの具体的なフィードバックをもらえました 英語レビュー : 英語に強いメンバーがスライドの英語をレビューしてくれました カンファレンス登壇は個人の活動と思われがちですが、組織横断で取り組むことで、プロポーザルの質・トークの質ともに大きく向上します。また、たとえ今回採択されなかったメンバーも、この過程で得た経験は次のチャレンジに活きるはずです。 まとめ try! Swift Tokyo 2026での登壇は、プロポーザルの準備からトーク完成まで約3か月の道のりでした。この経験を通じて、組織横断で挑戦することの価値を改めて実感しました。 カンファレンスへの登壇を検討されている方は、ぜひチームを巻き込んでみてください。一人では出てこないアイデアやフィードバックが、必ず登壇の質を高めてくれるはずです。 ZOZOのiOSエンジニア一同、今後もカンファレンスへの登壇を組織横断で積極的に推進していきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
ZOZO開発組織の2026年3月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2026年3月は、前月のMonthly Tech Reportを含む計19本の記事を公開しました。特に次の3記事は反響も大きく、とても多くの方に読まれています。ぜひご一読ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com 登壇 【Flutter推し活】Flutter好きが集うLT会 Studyplus x Linc'well 3月13日に開催された「 【Flutter推し活】Flutter好きが集うLT会 Studyplus x Linc'well 」に、新規事業部の大野( @junjun_1345 )が登壇しました。 ZOZO フロントエンドMeetup 3月18日にZOZOで主催した「 ZOZO フロントエンドMeetup 」に、ZOZOTOWN開発3部の揚原、WEAR開発部の岩崎、ZOZOTOWN開発1部の佐藤、そしてZOZOTOWN企画開発部の片岡が登壇しました。 掲載 Apps in ChatGPT対応 OpenAIの対話型AI「ChatGPT」の新機能「Apps in ChatGPT」にファッション領域でいち早く対応し、アプリ連携を開始しました。このことが複数のメディアで取り上げられました。まだ試したことがない方はぜひ一度お試しください。 corp.zozo.com www.fashionsnap.com netkeizai.com ZOZOEDUCATION つくっちゃお! ZOZO初となる、子どもが“つくって売る”に挑戦できる教育プロジェクト「ZOZOEDUCATION つくっちゃお!」を始動しました。本プロジェクトの一環として、Tシャツのデザインから販売までを、親子が自宅で気軽に体験できるTシャツづくりキットを3月25日より販売開始しています。このことが複数のメディアで取り上げられました。親子で体験できる楽しいキットですので、ぜひお試しください。 corp.zozo.com www.nikkei.com kosodate.mynavi.jp 以上、2026年3月のZOZOの活動報告でした! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、SREブロックの岩切です。普段はZOZOTOWN Yahoo!店の連携基盤のリプレイスを担当しています。 ZOZOTOWN Yahoo!店では、FTPによるデータ連携の遅延をSplunkアラートで検知し、PagerDutyにインシデントを作成して運用しています。しかし、遅延が解消してもインシデントは自動でResolveされず、手動で対応する必要がありました。 Splunk × PagerDutyの運用では、「アラートは自動だがResolveは手動」という課題に悩まされがちです。本記事では、 追加のミドルウェアなしでインシデントを自動Resolveする実装パターン を紹介します。 目次 はじめに 目次 この記事で得られる知見 背景・課題 自動Resolveの要件整理 解決策の検討 アプローチ1:Splunk Add-onのparam.dedup_keyを使う アプローチ2:Events API v2統合を使う アプローチ3:Event Transformer統合 + Service Event Orchestration(採用) 全体のアーキテクチャ インシデント作成(遅延発生時) インシデント自動Resolve(遅延解消時) 設定の詳細 Splunk側の設定 トリガーアラート(既存設定の変更) 解消アラート(新規作成) PagerDuty側の設定 Event Transformer統合(新規作成) Service Event Orchestration 動作シナリオ 正常時(遅延なし) 遅延発生から解消までの流れ まとめ この記事で得られる知見 Splunk Add-on for PagerDutyの制約と、その回避方法 PagerDutyの dedup_key を活用したインシデントのライフサイクル管理 「検索結果が0件のときだけアラートを発火する」SPLテクニック 背景・課題 ZOZOTOWN Yahoo!店では、商品情報などのデータをFTPで連携しています。FTPによるデータ反映が一定時間遅れると、Splunkのアラートが発火し、PagerDutyのインシデントが作成されます。 しかし、遅延が解消してもインシデントは自動Resolveされず、 毎回オンコール担当者が手動でResolve していました。この手動対応が繰り返され、運用上の負担になっていました。 手動Resolveの課題は以下のとおりです。 対応コスト :遅延解消を確認し、PagerDutyを開いてResolveする作業が都度発生する Resolve忘れのリスク :インシデントが残り続けると、新たなアラートとの区別がつきにくくなる オンコール負荷 :深夜・休日に遅延が解消しても、Resolveのためだけに対応が必要になる場合がある 自動Resolveの要件整理 PagerDutyのインシデントを自動Resolveするには、以下の2つを満たす必要があります。 インシデント作成時と 同じ dedup_key で resolve イベントを送ること イベントの event_action が resolve であること dedup_key とは、PagerDutyがイベントをインシデントに紐づけるための一意キーです。同じ dedup_key を持つイベントは同一インシデントとして扱われ、重複排除やResolveの対象になります。 要件自体はシンプルに見えますが、Splunk Add-on for PagerDutyには resolve イベントを直接送信する機能がありません。そのため、Splunk側とPagerDuty側の両方に工夫が必要でした。 解決策の検討 最終的な設計に至るまで、いくつかのアプローチを検討・検証しました。 各アプローチの説明へ入る前に、本記事で登場するPagerDutyの主要な概念を整理します。 Event Transformer統合 :Splunkなどの外部ツールからイベントを受け取り、PagerDuty形式に変換する統合タイプ。 incident_key の設定により、受信したイベントのどのフィールドを dedup_key として使うかを決定する Service Event Orchestration :サービスに届いたイベントをルールベースで加工する機能。条件に応じて event_action の変更、優先度の設定などが可能 アプローチ1:Splunk Add-onの param.dedup_key を使う Splunk Add-onの param.dedup_key パラメータで dedup_key を明示的に指定する方法です。しかし、Event Transformer統合は incident_key 設定に基づいて dedup_key を自動生成するため、 Splunk側の param.dedup_key は無視されます 。この方法では意図した dedup_key を指定できず、採用を見送りました。 アプローチ2:Events API v2統合を使う Events API v2統合であれば、ペイロードの dedup_key をそのまま使えます。しかし、Splunk Add-onはSplunk固有のペイロード形式で送信するため、 Events API v2統合ではペイロードを解釈できず、インシデントが作成されません でした。 アプローチ3:Event Transformer統合 + Service Event Orchestration(採用) 新しいEvent Transformer統合を作成し、 incident_key=source に設定します。SPLに eval source="yshp-ftp-delay-warning" を追加することで、トリガーと解消で同じ dedup_key を生成します。そのうえで、Service Event Orchestrationで resolve に変換します。 検討した3つのアプローチの比較 全体のアーキテクチャ 自動Resolveの仕組みは、 インシデント作成 と インシデント自動Resolve の2つのフローで構成されます。設計のポイントは以下の2点です。 dedup_keyをSPL側で強制的に統一する : eval source="yshp-ftp-delay-warning" でトリガーと解消に同じ値を付与 resolveはPagerDuty側で変換する :Splunkからは trigger として送り、Orchestrationで resolve に変換 自動Resolveの全体アーキテクチャ インシデント作成(遅延発生時) Splunk :「Yahoo!FTPデータ反映遅延警告」アラートが遅延ファイルを検出して発火 SPL :末尾の eval source="yshp-ftp-delay-warning" により結果にsourceフィールドを付与 Event Transformer : incident_key=source の設定により dedup_key="yshp-ftp-delay-warning" を生成 Service Event Orchestration : event.summary に「解消」を含まないため、そのまま通過(trigger)。なお、Splunkの search_name (アラート名)はPagerDutyでは event.summary として受信される 結果 :インシデント作成(同一 dedup_key なら重複排除) インシデント自動Resolve(遅延解消時) Splunk :「Yahoo!FTP遅延解消チェック」アラートが遅延ファイル0件を検出して発火 SPL : | stats count | where count = 0 | eval source="yshp-ftp-delay-warning" で遅延ファイルが0件のときだけ結果を返す Event Transformer : dedup_key="yshp-ftp-delay-warning" (トリガーと同一キー) Service Event Orchestration : event.summary に「解消」を含むため、 event_action を resolve に変換 結果 :同一 dedup_key のインシデントを自動Resolve 以上の仕組みで解決した技術的課題をまとめます。 課題 解決方法 Splunk Add-onは resolve を送れない Service Event Orchestrationで trigger → resolve に変換 trigger/resolveのインシデント紐づけ SPLに eval source="yshp-ftp-delay-warning" を追加し、Event Transformerの incident_key=source で同一 dedup_key を生成 解消イベントの識別 event.summary に「解消」を含めてOrchestrationルールで判別 既存統合の incident_key=search_name では dedup_key 不一致 FTP遅延専用のEvent Transformer統合を新規作成し incident_key=source に設定 設定の詳細 Splunk側の設定 トリガーアラート(既存設定の変更) 「Yahoo!FTPデータ反映遅延警告」アラートに以下の変更を加えました。 integration_key / url を新しいEvent Transformer統合に変更 SPL末尾に | eval source="yshp-ftp-delay-warning" を追加 発火条件・スケジュールは変更なし 解消アラート(新規作成) 「Yahoo!FTP遅延解消チェック」アラートを新規作成しました。 解消アラートの設定画面 設定項目 値 備考 SPLクエリ トリガーと同一ベース + | stats count | where count = 0 | eval source="yshp-ftp-delay-warning" 遅延ファイル0件のときのみ結果を返す counttype number of events SPL結果の行数で判定 quantity / relation 0 / greater than result_count > 0で発火 cron_schedule 02-59/10 * * * * 10分毎(トリガーと同一間隔) ここでのポイントは、SPLに追加した | stats count | where count = 0 の組み合わせです。 通常、Splunkアラートは「検索結果が存在するとき」に発火します。しかし今回実現したいのは「遅延ファイルが0件のとき(=遅延が解消したとき)」の発火です。遅延ファイルが0件だと検索結果も0件になり、アラートが発火しません。 そこで、ベースとなるSPLの後に | stats count | where count = 0 を追加します。 stats count は検索結果の件数を常に1行で返すため、遅延ファイルが0件なら count=0 の1行が出力され、 where count = 0 を通過します。逆に遅延ファイルが存在する場合は count > 0 となり、 where count = 0 で除外されて結果が0行になります。 これにより、「結果が0件のときだけ発火する」という逆転の発火条件をSPLだけで実現しています。 PagerDuty側の設定 Event Transformer統合(新規作成) FTP遅延のトリガー・解消アラート専用に、新しいEvent Transformer統合を作成しました。 項目 値 統合名 Splunk (自動Resolve用) 対象サービス zozo-yshp-alert incident_key source Event Transformer統合の設定画面。incident_keyをsourceに設定 既存のEvent Transformer統合は incident_key=search_name に設定されており、アラート名がそのまま dedup_key になります。トリガーと解消でアラート名が異なるため、既存統合では dedup_key が一致せずResolveできません。そこで incident_key=source に設定した専用統合を新規作成し、SPLで付与した source フィールドを共通の dedup_key として使用します。 Service Event Orchestration 項目 値 ルール event.summary matches part '解消' → event_action = resolve catch_all そのまま通過(変換なし) Service Event Orchestrationのルール設定 Service Event Orchestrationは対象サービスのイベントのみに適用されます。他のアラートは従来のEvent Transformer統合を使用しており、影響はありません。 動作シナリオ 正常時(遅延なし) 解消アラートが10分毎に発火し、PagerDutyに resolve イベントが送信されます。これは設計上意図した動作です。PagerDutyは、対応する dedup_key のインシデントが存在しない場合、 resolve イベントを無視します。新規インシデントが作成されることはないため、副作用はありません。 遅延発生から解消までの流れ 実際の動作を時系列で示します。遅延が発生するとインシデントが作成され、解消後に最初の解消チェックが走ったタイミングで自動Resolveされます。 遅延発生から自動Resolveまでの時系列 まとめ Splunk Add-on for PagerDutyには resolve を直接送れない制約があります。今回はこの制約を、 Event Transformer統合の incident_key 設定 と Service Event Orchestration の組み合わせで解決しました。 この仕組みの導入により、以下の改善が得られました。 Slackに通知された自動Resolveの実績 オンコール担当者の手動Resolve作業がなくなった インシデントのライフサイクルが実際の障害状況と一致するようになった Splunk Add-onの制約内で、追加のミドルウェアなしに実現できた Splunk × PagerDutyの運用では、同様の「アラートは自動だがResolveは手動」という課題を抱えているケースがあるかもしれません。本記事の設計パターンが参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくださる方を募集中です。ご興味のある方は、ぜひ採用ページをご覧ください。 corp.zozo.com
はじめに こんにちは、ZOZOTOWN企画開発部 企画フロントエンド2ブロックのパクサンイです。普段はZOZOTOWNにあるCMSベースのLPページのメンテナンスや機能追加、企画LPページ環境のメンテナンスを担当しています。 ZOZOTOWNの複数のWebアプリケーション間で、プロモーション用ランディングページコンポーネントを共有するために、 Lit ベースのWeb Componentsを導入しました。本記事ではその事例を紹介します。 ZOZOTOWNでは多数のLPページが開設・更新されており、従来はiframeを使った埋め込み方式でUIを共有していました。しかし、この方式にはさまざまな課題が存在し、レガシー環境からNext.jsベースの新環境へのリプレイスを進める中で、フレームワークに依存しないUI共有アーキテクチャが必要となりました。 本記事では、iframeベースの共有方式が抱える具体的な課題と、LitベースのWeb Componentsを採用した理由と選定プロセスを解説します。さらに、フレームワーク非依存なコンポーネント共有基盤を設計・実装する中で得た経験を共有します。 対象読者 マルチWebアプリケーション環境でUI共有に課題を感じているフロントエンドエンジニア iframeを使ったUI共有方式の代替手段を探している方 Web Componentsの導入を検討している方 目次 はじめに 対象読者 目次 背景・課題 ZOZOTOWNフロントエンドのマルチWebアプリケーション構成 LPコンポーネントの共有仕様 従来のiframeベース共有方式とその課題 1. レイアウト制御の煩雑さ 2. UI制御の複雑化 3. SEOの制約 アプローチ:Web Componentsの導入 要件整理 技術選定:Lit基盤Web Components Litを選択した理由 npmパッケージ方式を除外した理由 設計・実装 全体アーキテクチャ 1. 利用側アプリケーションによるデータ取得・加工 2. Lit ContextによるProps Drilling防止 3. Scriptローディングによる独立したUI更新 4. Shadow DOMからLight DOMへの切り替え ビルド・配信 全体フロー LPコンポーネント開発側(コンテンツ共有専用リポジトリ) 利用側Webアプリケーション 効果 学んだこと 今後の課題 今後の展望 まとめ 最後に 参考資料 背景・課題 ZOZOTOWNフロントエンドのマルチWebアプリケーション構成 現在、ZOZOTOWNのフロントエンドは3つのマルチWebアプリケーションで運用されています。 リポジトリ 説明 主な役割 リポジトリA(レガシー環境) 統合リポジトリ 既存の全ページを管理 リポジトリB(リプレイス環境) コアメインページ ホーム、カート、検索結果、商品詳細ページなど リポジトリC(リプレイス環境) 企画ページ フルスクラッチLP、CMS活用LP レガシー環境では複数のサービスが単一リポジトリで管理されていたため、共通UI共有に関する課題はありませんでした。しかし、リプレイス後にマルチWebアプリケーションが増えたことで、従来の方式ではUIを再利用できなくなりました。 LPコンポーネントの共有仕様 ZOZOTOWNでは特定のLPコンポーネントを複数のページで表示しています。一部のページでは以下の2つの形態で表示されます。 単独ランディングページ — header/footerを含むフルページ モーダル表示 — 特定ページのバナークリック時に、header/footerなしでコンテンツセクションのみをモーダルで表示 つまり、ほぼ同一のUIでありながら、header/footerの有無、SEOメタタグ、計測用トラッキングスクリプトの有無などで差異がある仕様でした。 従来のiframeベース共有方式とその課題 リプレイス後は以下の方式でUIを共有していました。 環境 運用方式 リポジトリA(レガシー) LPページ配信 + iframe用LPページ(header/footerなし)配信 リポジトリB・C(リプレイス) 特定ページにバナー表示 → クリック時にモーダル内でiframeとしてリポジトリAのLPを埋め込み このiframe方式には以下の課題が存在していました。 1. レイアウト制御の煩雑さ iframeは独立したドキュメントを読み込むため、フレームサイズの調整や使用箇所ごとの非表示領域の処理は対応していたものの、煩雑な部分がありました。 2. UI制御の複雑化 各バリエーションに応じて非表示にすべき子コンポーネントもあり、クエリパラメータや postMessage で解決できるものの、ケースが増えるほど複雑化しました。 3. SEOの制約 検索エンジンはiframe内のコンテンツを src 側の所有として認識するため、SEO上の制約がありました。 アプローチ:Web Componentsの導入 要件整理 上記の課題を解決するために、以下の4つの要件を整理しました。 要件 説明 各アプリのデプロイなしにUI更新 iframe方式の利点であった各マルチWebアプリケーションのデプロイなしにUI変更が反映されることを維持 iframe脱却 各アプリケーションでネイティブにUIをレンダリング フレームワーク非依存 React、Vueなど、どのフレームワークでも使用可能であること 軽量バンドルサイズ 利用側に負担のない最小限のサイズを維持 技術選定:Lit基盤Web Components Web Componentsはブラウザのネイティブコンポーネントモデルであり、特定のフレームワーク(React、Vueなど)に依存せず、ブラウザが直接理解する標準技術です。主に以下の3つの中核技術で構成されています。 Custom Elements :開発者が独自のHTMLタグを定義できる。タグ名にはハイフン( - )を含む規約がある。 Shadow DOM :コンポーネントのスタイルとマークアップを外部ページから隔離(Encapsulation)する。 HTML Templates : <template> と <slot> 要素により、再利用可能なマークアップ構造を定義する。 このWeb Componentsをより効率的に開発するため、 Lit ライブラリを採用しました。 Litを選択した理由 選定基準 Litの特徴 バンドルサイズ 約5KB(minified + compressed)で非常に軽量 リアクティブプロパティ Reactive Propertiesにより状態変更時に自動再レンダリング テンプレート Tagged Template Literalsベースで別途コンパイル不要 パフォーマンス Virtual DOM diffingなしに動的部分のみを直接更新 相互運用性 すべてのLitコンポーネントはネイティブWeb Componentであり、HTMLを使うあらゆる場所で動作 npmパッケージ方式を除外した理由 LPページはテキスト更新の頻度が高く、UIも不定期に変更されます。npmパッケージで運用すると、変更のたびに各環境でパッケージ更新+デプロイが必要となり、運用負荷が大きいため除外しました。 設計・実装 全体アーキテクチャ コンテンツ共有専用リポジトリを新たに構築し、以下の設計原則を適用しました。 1. 利用側アプリケーションによるデータ取得・加工 ZOZOTOWNにはページアクセス時に初期設定すべき値やAPIフェッチのためのロジックが各アプリケーションに存在します。これらのロジックをコンテンツ共有専用リポジトリにも含めると管理が二重になりメンテナンス負荷も大きくなるため、このリポジトリでは UIレンダリングのみ を責任範囲としました。 利用側の親アプリケーションでデータを取得・加工してpropsで渡す形式を採用しています。 2. Lit ContextによるProps Drilling防止 UI内部で必須的に共有すべき情報(デバイス種別、性別など)は、 Lit Context を活用したカスタム要素を設けて処理しました。 Lit ContextはReactのContext APIと同様の概念で、Props Drillingなしに上位から下位コンポーネントへデータを渡すことができます。 3. Scriptローディングによる独立したUI更新 各Webアプリケーションで別途デプロイなしにUI変更が可能なよう、 Scriptローディング を採用しました。各アプリケーションでは <script> タグで必要なコンポーネントのJSファイルを読み込み、クライアントでWeb Componentがレンダリングされます。 4. Shadow DOMからLight DOMへの切り替え Web Componentsの代表的な特徴であるShadow DOMは、スタイルを完全に隔離し、コンポーネント内部のCSSが外部に影響せず、外部CSSも内部に影響しません。 しかし、今回のケースでは、Shadow DOMで隔離して管理するUIではなく、利用側から自由にスタイルだけでなく要素にもアクセスできることが重要でした。そのため、Shadow DOMの代わりに Light DOM を採用しました。 ビルド・配信 Viteを使用してLit基盤Web Componentをビルドし、S3にデプロイしてCDN経由で配信します。 全体フロー LPコンポーネント開発側(コンテンツ共有専用リポジトリ) Lit + Vite dev serverでローカル開発 各テスト環境にてHTML + JSで動作確認 問題なければ各環境(S3)にデプロイして確認 利用側Webアプリケーション SSR時にCMS APIでデータ取得(スケジュールに応じて変更されるテキストなどはCMSで管理) クライアントで <script> タグによるJSファイルローディング、Web Componentのレンダリング カスタムタグへCMS API仕様に合わせたデータをpropsで渡す 効果 この仕組みの導入により、以下の効果が得られました。 マルチWebアプリケーション間でiframeを使わずにUIコンポーネントを共有できるようになった 各アプリケーション側のリリース(デプロイ)なしでコンテンツ更新が可能になった 利用側からスタイルだけでなく要素へのアクセスも自由に可能になった(Light DOM採用) CMS連携により、エンジニア以外でも直接スケジュールベースのデータ管理が可能に 学んだこと Litを通じて開発する中で、Web Componentsのベースとなるウェブ標準技術をより深く理解し、関心を持つようになりました。また、CSS変数などを活用してJavaScriptなしにCSSだけでスタイルを制御する方法も知ることができました。 今後の課題 Web Components公式のSSR対応はまだ限定的ですが、Lit SSRなど複数の解決策がライブラリやコミュニティで共有されています。現在、このプロジェクトで管理しているLPページの仕様ではWeb ComponentのSSRは不要ですが、将来に備えた準備は必要だと考えています。 また、現在の運用方式では、Scriptローディング+CMSデータ連携という構造上、テストが非常に重要であり補強が必要です。 今後の展望 移行すべきLPページが多数残っており、段階的にマイグレーションを進めていく予定です。より小さな単位の共用コンポーネントもこの基盤で管理できるよう拡張を検討しています。また、可能であれば、ネイティブアプリケーションでの活用も検討したいと考えています。 まとめ 本記事では、ZOZOTOWNのマルチWebアプリケーション環境におけるiframeベースUI共有方式の課題を解説しました。また、LitベースのWeb Componentsを活用したフレームワーク非依存のコンテンツ共有基盤の構築事例を紹介しました。 ReactベースであればReactでもUIを共有する方法はあります。しかし、今後どのフレームワークでも問題なく移植できるWeb Componentsを選択し、メインスタックと共存しながら運用するのもよいのではないでしょうか。同様の課題をお持ちの方の参考になれば幸いです。 最後に ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 参考資料 MDN - Web Components Lit 公式ドキュメント Lit Context MDN - Using Shadow DOM MDN - iframe Vite - Building for Production
はじめに こんにちは、FAANS部フロントエンドブロックの 中島 です。普段はFAANSのiOSアプリ開発を担当しています。FAANS iOSチームではSwift 6移行の取り組みをしています。以前、 Strict Concurrency CheckingをTargeted に変更した過程で得た知見を紹介しました。今回TargetedからCompleteに変更するとXcodeで約1400個の新たな警告が出ました。機械的に対応できる警告もありますが、曖昧な知識だと修正が難しいケースもありました。本記事では、Swift 6移行時の警告やエラー解決を通じて得た知見を共有します。実際に遭遇した警告への対処法など、移行作業を始める前に押さえておきたかったポイントを中心に解説します。 移行当初はXcode 16.4だったので、最新のXcodeでは警告がエラーとなる可能性もありますが、本記事では警告で統一します。また、Swift 6でビルドをするとそれまで警告だったものがエラーになりビルド自体が通らなくなるため、まずはSwift 5の段階ですべての警告を解消しました。その後、Swift 6へ切り替えてビルドが通ることを確認し、新たに不具合が発生していないかを検証しました。 目次 はじめに 目次 Completeに変更後、新しく発生した警告の分類 Sendable非準拠の警告原因 解決方法 コンテキストの不一致について 1. nonisolatedのコンテキストでMainActorを利用するケース 解決方法 2. クロージャ内がnonisolatedになるケース Sendableクロージャにおけるケースの実例 解決方法1. Task { @MainActor in } の利用 解決方法2. async関数の利用 3. nonisolatedなプロトコルをMainActorで実装するケース 自作プロトコルのケース ライブラリのプロトコルのケース 実行時にクラッシュするケースの紹介 クラッシュの原因 Combineでコンテキスト不一致によるクラッシュのケース sendingキーワードについて Region Based Isolationについて Task-isolatedな値について Combineのsinkする値について その他の警告 1. passing closure as a 'sending' parameter risks causing data races 2. static property '...' is not concurrency-safe because it is nonisolated global shared mutable state まとめ さいごに Completeに変更後、新しく発生した警告の分類 Strict Concurrency CheckingをTargetedからCompleteへ変更後、新たに発生した警告を分類して集計しました。Xcode 16.4でMinimum Deployment TargetをiOS 16、Swift Language VersionをSwift 5に設定してビルドした際に発生した警告です。 警告概要 警告例 分類 割合 Sendable非準拠 type '...' does not conform to the 'Sendable' protocol Sendable 約46% nonisolatedで MainActorを利用 call to main actor-isolated initializer '...' in a synchronous nonisolated context コンテキスト不一致 約44% SendableクロージャでMainActor を利用 main actor-isolated property '...' can not be referenced from a Sendable closure コンテキスト不一致 約2% SendableクロージャでNonSendable を利用 capture of '...' with non-sendable type '...' in a @Sendable closure Sendable / コンテキスト不一致 約2% nonisolatedプロトコル要件の不一致 main actor-isolated instance method '...' cannot be used to satisfy nonisolated protocol requirement コンテキスト不一致 約2% sending引数の データ競合 sending '...' risks causing data races Sendable / コンテキスト不一致 約2% static/class変数のデータ競合 static property '...' is not concurrency-safe because it is nonisolated global shared mutable state Sendable / コンテキスト不一致 約2% 上の表を見てわかるように次の2つに関連する警告がほとんどでした。 Sendableに非準拠 コンテキストの不一致 Sendableに準拠できていないことによる警告は、Non-SendableのクラスなどをActor境界を超えて利用したり、Sendable指定の引数に渡したりしたときに発生します。コンテキストの不一致は、あるActorから別のActorのプロパティやメソッドを利用していると出る警告です。すべての警告を本記事で解説するのは難しいですが、警告内容が異なっても同じような直し方や考え方が非常に多いです。 Sendable非準拠の警告原因 FAANSでSendable非準拠の警告が最も発生したのはAPIレスポンスモデルでした。API通信でOpenAPI(Swagger)を用いており、自動生成されたコードを利用しています。レスポンスの型はpublicなstructやenumでしたが、publicな型は暗黙のSendable準拠が行われないため、明示的にSendableを付与する必要があります。そのため、次のコードのようにUICollectionViewで指定するアイテムの型がSendableに準拠する必要があるので、警告が出ていました。 解決方法 移行当初、Swift 6用の自動生成テンプレートがBeta版だったため利用を見送りました。テンプレートのコードにSendableが付与されていることを確認し、暫定対応としてimport文に@preconcurrencyを付与して警告を抑制しました。現在はすでにStable版がリリースされているため、今後対応する場合はSwift 6用のテンプレートを利用することで解決可能です。FAANSアプリにおいても近いうちに対応を予定しています。 コンテキストの不一致について 他の警告も簡単に解決できるとよいのですが、これから登場するコンテキストの不一致にはさまざまなパターンがあり、一筋縄ではいかないケースもありました。しかし、このコンテキストの不一致を理解すれば、Swift 6対応において発生する警告のほとんどを解決できます。次の3つについて実例を交えて説明します。 nonisolatedのコンテキストでMainActorを利用するケース クロージャ内がnonisolatedになるケース nonisolatedなプロトコルをMainActorで実装するケース 1. nonisolatedのコンテキストでMainActorを利用するケース nonisolatedのコンテキストでMainActorのメソッドやプロパティを利用したことが原因で発生する警告を解説します。FAANSでは一覧表示のためにUICollectionViewを多くの画面で利用しており、セクションごとにレイアウトを切り替える実装をしています。セクション名をenumで管理してその中でレイアウトを生成するメソッドを定義しています。しかしenum自体はnonisolatedなコンテキストであるため、MainActorのUICollectionViewLayoutなどを扱うと警告が発生しました。 また、UINavigationControllerを設定するためのヘルパー関数があります。UINavigationControllerはMainActorに隔離されているため、そのヘルパー関数をnonisolatedなstructに定義すると警告が出ました。 解決方法 enumやstructに@MainActorを付与してMainActorと同じコンテキストにそろえました。このように単にMainActorにできていなかったというケースは非常に多く、機械的に修正可能です。 2. クロージャ内がnonisolatedになるケース 次に、クロージャ内がnonisolatedになるケースを紹介します。クロージャ内のコンテキストは利用側と同じ場合もあれば、異なる場合もあります。コード例を用いて説明します。 通常のクロージャは利用側のクラスのコンテキストを引き継ぎます。上記のコード例はMainActorのクラスなのでクロージャ内もMainActorです。一方、Sendableを付与したクロージャはnonisolatedと判断されます。nonisolatedのコンテキストでMainActorのプロパティを変更しているので警告が出ます。この警告を解決するにあたって、実務で遭遇したケースを紹介します。 Sendableクロージャにおけるケースの実例 FAANSアプリではKingfisherという画像ダウンロードライブラリを利用しています。downloadImageメソッドで画像をダウンロードし、後続処理をクロージャ内で実装しています。発生した警告についてコード例とともに説明します。 クロージャ内で Capture of 'self' with non-Sendable type 'Downloader?' in a '@Sendable' closure の警告が出ました。はじめに示した例ではMainActorのクラスでSendableクロージャを利用しましたが、このケースではクラスにMainActorが付与されていません。Sendableクロージャで扱う値はSendableである必要があるので、Non-Sendableのselfをキャプチャしたことで警告が出ました。 ViewModelクラスはダウンロードした値を格納し、状態を持つのでSendableにするのは難しい状況です。実際のコードではUIViewControllerがこのクラスを保持しています。UIViewControllerはMainActorなのでViewModelクラスもMainActorにしました。 しかし、Sendableのクロージャはnonisolatedと判断されるため、nonisolatedのコンテキストでMainActorのプロパティを変更できないという警告が新たに出ました。 解決方法1. Task { @MainActor in } の利用 nonisolatedのクロージャ内でTask { @MainActor in }を利用してMainActorのコンテキストで値をセットするように変更しました。 @MainActor class ViewModel { var coverImage : UIImage? func setImage (url : String ) { guard let imageURL = URL(string : url ) else { return } KingfisherManager.shared.downloader.downloadImage( with : imageURL ) { [ weak self ] result in // クロージャ内はnonisolatedとして推論 switch result { case .success( let value ) : // MainActorのコンテキストに切り替える Task { @MainActor in self ?.coverImage = value.image } case .failure : break } } } } 解決方法2. async関数の利用 ライブラリによっては同じ内容のメソッドのasync版が提供されている場合があります。KingfisherのdownloadImageメソッドにもasync版があります。ネストを減らせて可読性が向上し、Sendableクロージャを考慮する必要がなくなります。 @MainActor class ViewModel { var coverImage : UIImage? func setImage (url : String ) { guard let imageURL = URL(string : url ) else { return } // Taskはコンテキストを引き継ぐのでTask { @MainActor in } としなくてもよい Task { [ weak self ] in do { let result = try await KingfisherManager.shared.downloader .downloadImage(with : imageURL ) self ?.coverImage = result.image } catch { // エラー処理 } } } } FAANSアプリはasync対応を進めているので、この解決方法2を採用しました。一方、解決方法1を採用しているケースも存在します。例えば、KVOのobserveで値を監視し、そのクロージャ内でViewの更新処理を行っている箇所がその一例です。observeのchangeHandlerはSendableクロージャなので、クロージャ内でTask { @MainActor in }を使ってMainActorのコンテキストに切り替えました。 private var observation : NSKeyValueObservation? private func setupCollectionViewContentSizeObserver () { // UICollectionViewのcontentSizeをobserveで監視 observation = collectionView.observe( \.contentSize, options : [ .new ] ) { _, change in // クロージャ内はnonisolatedなのでMainActorのコンテキストに切り替える Task { @MainActor [ weak self ] in guard let contentSize = change.newValue else { return } self ?.onCollectionViewContentHeightDidChange?(contentSize.height) } } } 3. nonisolatedなプロトコルをMainActorで実装するケース 次は、nonisolatedなプロトコルを利用するケースです。プロトコルがnonisolatedとして定義されていますが、実装側がViewControllerなどでMainActorになっている場合に出る警告です。具体例を見ていきましょう。 QRコードの読み取りのためにQRScannerというライブラリを利用しています。QRScannerViewDelegateをMainActorのQRCodeScannerViewControllerで実装すると2つの警告が出ました。 1つ目はQRScannerViewDelegateプロトコルへの適合に関する警告です。MainActorに隔離されたコードを跨いでおり、データ競合を引き起こす可能性があると指摘されています。 2つ目はMainActorに隔離されたインスタンスメソッドがnonisolatedの要求を満たせていない、という警告です。 つまり、プロトコルはnonisolatedであるため実装側のMainActorにコンテキストをそろえられません。その解決方法を自作プロトコルと、ライブラリのプロトコルの2つのケースで説明します。 自作プロトコルのケース QRScannerViewDelegateは変更できませんが、自分で定義したプロトコルだと比較的簡単に解決できるケースが多いです。キーボードの表示/非表示に合わせて処理を実行するKeyboardShowableプロトコルの例をコードとともに説明します。キーボードに関する操作なのでMainActorのViewControllerで利用しています。しかしprotocol側はMainActorではなくてnonisolatedなので同じように警告が出ました。 解決方法として、protocol KeyboardShowableにMainActorを付与します。 // @MainActorを付与してKeyboardShowableをMainActorのコンテキストにする @MainActor protocol KeyboardShowable {     func keyboardWillShow (_ notification : Notification )     func keyboardWillHide (_ notification : Notification ) } extension UploadCodeViewController : KeyboardShowable { func keyboardWillShow (_ notification : Notification ) { buttonsBackgroundView.isHidden = true } func keyboardWillHide (_ notification : Notification ) { buttonsBackgroundView.isHidden = false } } KeyboardShowableプロトコルはUIに関することなのでMainActorで利用されると考えてよく、MainActorを付与することで利用側とコンテキストを一致させました。このケースのように自作プロトコルにおいてはプロトコル側の修正をすることで対応が可能です。 ライブラリのプロトコルのケース QRScannerViewDelegate等、ライブラリ側の修正ができないケースについて説明します。MainActorのコンテキストに合わせられないので、実装側のメソッドにnonisolatedを付与してライブラリ側のコンテキストに合わせました。しかしnonisolatedのコンテキストでMainActorのメソッドを呼び出している箇所があり、コンテキスト不一致の警告が新たに発生しました。 解決方法として、Sendableクロージャのケースで紹介した対応と同じように、Task { @MainActor in } を利用してMainActorで実行すると良いでしょう。一方で、Taskを使う処理は非同期に実行されるため、呼び出し元が同期的な完了を前提としている場合は注意が必要です。 extension QRCodeScannerViewController : QRScannerViewDelegate { // nonisolatedを付与 nonisolated func qrScannerView ( _ qrScannerView : QRScannerView , didSuccess code : String ) { // Task { @MainActor in ... }を使ってMainActorのコンテキストで非同期実行 // delegateメソッドの呼び出し元が同期的な完了を前提としていないかは確認が必要 Task { @MainActor in ... qrScannerView.stopRunning() ... } } } 実行時にクラッシュするケースの紹介 nonisolatedを付与して問題を解消する方法に加えて、@preconcurrencyによって警告を抑える選択肢もあります。ライブラリ側のconcurrency対応待ちや既存実装の動作確認が済んでいて挙動を変えたくない場合に、Swift 6移行を進めるための暫定策として利用できます。Swift 5のビルドでは問題なく動作していましたが、Swift 6のビルドで実行時にクラッシュするケースがあったので紹介します。 FAANSアプリでは文字を認識する機能があります。AVCaptureSessionを利用して、AVCaptureVideoDataOutputSampleBufferDelegateのcaptureOutput(...)で出力処理の実装をしています。nonisolatedを付与してTask { @MainActor in … }で必要に応じてMainActorのコンテキストに切り替えました。しかしcaptureOutputの引数がNon-SendableでMainActorに渡せない問題がありました。そのため、@preconcurrencyをつけて対応しました。 // @preconcurrencyをつけて警告を消す extension CameraViewController : @preconcurrency AVCaptureVideoDataOutputSampleBufferDelegate { // nonisolatedをつける必要がなくなる func captureOutput ( _ output : AVCaptureOutput , didOutput sampleBuffer : CMSampleBuffer , from connection : AVCaptureConnection ) { guard let cvBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } // MainActorのメソッドの処理 detectText(buffer : cvBuffer ) } } Swift 6のビルドで動作確認したところ、クラッシュが発生しました。 クラッシュの原因 クラッシュの原因を説明します。AVCaptureVideoDataOutputSampleBufferDelegateはMainActorのViewControllerで利用されています。一方でcaptureOutput(...)はAVCaptureVideoDataOutputに設定した出力キュー上で呼び出されるため、このメソッドはバックグラウンドスレッドで実行されます。 @preconcurrencyを付与するとnonisolatedをつけなくても警告を抑制できるため、captureOutput(...)をMainActorに隔離されたまま実装できてしまいます。しかし@preconcurrencyはコンパイル時のチェックの緩和であり、実行コンテキストまで変えるわけではありません。 Swift 6でビルドすると、どのスレッドで実行されているかのチェックが強化されています。MainActorに隔離されたメソッドが実際にはバックグラウンドスレッドから呼ばれると、実行コンテキストの不一致としてクラッシュしてしまいました。実際のクラッシュログは次の通りです。 // frame#3, frame#4, frame#5がSwift Concurrencyの実行時チェックに関するフレーム // 現在の実行コンテキストが、そのメソッドに要求されるexecutorと一致しているかを検証 (lldb) bt * thread # 35 , queue = 'cameraQueue', stop reason = EXC_BREAKPOINT (code = 1 , subcode = 0x1052ff8e4 ) * frame # 0 : 0x00000001052ff8e4 libdispatch.dylib`_dispatch_assert_queue_fail + 120 frame # 1 : 0x000000010533601c libdispatch.dylib`dispatch_assert_queue $V2 .cold. 1 + 116 frame # 2 : 0x00000001052ff868 libdispatch.dylib`dispatch_assert_queue + 108 frame # 3 : 0x0000000186fc903c libswift_Concurrency.dylib`_swift_task_checkIsolatedSwift + 48 frame # 4 : 0x0000000187028744 libswift_Concurrency.dylib`swift_task_isCurrentExecutorWithFlagsImpl(swift :: SerialExecutorRef , swift :: swift_task_is_current_executor_flag ) + 356 frame # 5 : 0x0000000186fc8d88 libswift_Concurrency.dylib`_checkExpectedExecutor(_filenameStart : _filenameLength : _filenameIsASCII : _line : _executor : ) + 60 frame # 7 : 0x00000001af2249c4 AVFCapture` - [AVCaptureVideoDataOutput _processSampleBuffer : ] + 300 frame # 8 : 0x00000001af2246f8 AVFCapture`__47 - [AVCaptureVideoDataOutput _updateRemoteQueue : ]_block_invoke + 88 frame # 9 : 0x00000001b2b1c1d8 CMCapture`__FigRemoteOperationReceiverCreateMessageReceiver_block_invoke + 104 frame # 10 : 0x00000001b2fb7424 CMCapture`__rqReceiverSetSource_block_invoke + 260 frame # 11 : 0x00000001053162e0 libdispatch.dylib`_dispatch_client_callout + 16 frame # 12 : 0x00000001053000d8 libdispatch.dylib`_dispatch_continuation_pop + 672 frame # 13 : 0x000000010531618c libdispatch.dylib`_dispatch_source_latch_and_call + 448 frame # 14 : 0x0000000105314cd4 libdispatch.dylib`_dispatch_source_invoke + 872 frame # 15 : 0x0000000105304988 libdispatch.dylib`_dispatch_lane_serial_drain + 344 frame # 16 : 0x00000001053057d4 libdispatch.dylib`_dispatch_lane_invoke + 432 frame # 17 : 0x0000000105311b20 libdispatch.dylib`_dispatch_root_queue_drain_deferred_wlh + 344 frame # 18 : 0x00000001053111c4 libdispatch.dylib`_dispatch_workloop_worker_thread + 752 frame # 19 : 0x00000001e56b13b8 libsystem_pthread.dylib`_pthread_wqthread + 292 安易に@preconcurrencyをつけて警告を無視すると、実行時にクラッシュするリスクがあるので注意が必要です。 Combineでコンテキスト不一致によるクラッシュのケース 別のクラッシュ事例として、Combineの使用箇所で発生したケースを紹介します。API通信は基本的にasyncメソッドへ移行していますが、一部ではまだCombineを使っています。バックグラウンドで発火したPublisherの結果をMainActor隔離のViewModelでsinkしている箇所がありました。このsinkのクロージャ実行直前でクラッシュが発生しました。 URLSession.shared.dataTaskPublisherでバックグラウンドから取得した値を、MainActor隔離クラスのsinkで受け取るコード例を紹介します。 struct ImageDownloader { // バックグラウンドスレッドでダウンロード後にPublisherを返す func dataTaskPublisher ( for url : URL ) -> AnyPublisher < Data , Error > { return URLSession.shared.dataTaskPublisher( for : URLRequest (url : url )) .tryMap { (output) -> Data in guard let urlResponse = output.response as? HTTPURLResponse else { throw ImageDownloaderError.unknown } switch urlResponse.statusCode { case 200 ..< 300 : return output.data default : throw ImageDownloaderError.responseError } } .eraseToAnyPublisher() } } @MainActor class MainActorViewModel { private var cancellables : Set < AnyCancellable > = [] func download () { // dataTaskPublisherを実行するだけではクラッシュしない let publisher = ImageDownloader().dataTaskPublisher( for : URL (string : "..." ) ! ) // sinkのタイミングでクラッシュする publisher .sink( receiveCompletion : { _ in }, receiveValue : { data in // .. ダウンロード後の処理 } ) .store( in : & cancellables) } } クラッシュの原因は、メインスレッドに戻すための .receive(on: DispatchQueue.main) をsinkの前に挟み忘れていたことでした。Swift 6でビルドすると、Combineにおいても実行時のコンテキストのチェックが強化されています。しかし、sinkのクロージャの実行コンテキストはコンパイラが静的に追跡できないため、警告が出ないケースもあります。 .receive(on: DispatchQueue.main) をsinkの直前に挿入することで、sinkクロージャの実行コンテキストをメインスレッドに切り替え、クラッシュを回避できます。 @MainActor class MainActorViewModel { private var cancellables : Set < AnyCancellable > = [] func download () { let publisher = ImageDownloader().dataTaskPublisher( for : URL (string : "..." ) ! ) // .receive(on: DispatchQueue.main)でメインスレッドに切り替える publisher .receive(on : DispatchQueue.main ) .sink( receiveCompletion : { _ in }, receiveValue : { data in // .. ダウンロード後の処理 } ) .store( in : & cancellables) } } sendingキーワードについて 次に、sendingキーワードを説明します。警告の数自体はそれほど多くないのですが、理解がやや難しい警告であるため、ぜひ触れておきたい内容です。FAANSではCombineを用いたAPI通信のasync対応でwithCheckedThrowingContinuationを利用しています。クロージャでcontinuationを受け取り、resumeメソッドを実行する部分があります。リクエストした結果の値(value)をresumeメソッドに渡している箇所で警告が出ました。 Task-isolatedとsending parameterの意味がわかりにくいかもしれません。まずはsendingキーワードをコード例と一緒に説明します。 sendingキーワードは関数の引数に付与できます。sendingを付与した場合、Non-Sendableな値でもActor境界を超えられます。しかし引数として渡した値を呼び出し元で利用できなくなります。実際にコード例として、sendingを付与したreceiveWithSendingメソッドにNon-Sendableのクラスを渡すケースを紹介します。 useCounterAfterSendingメソッドで、counterがsendingパラメータとして渡された後に使用されており、後続の使用によるデータ競合の可能性を示す警告が出ました。一方、sendingメソッドでは呼び出し元でcounterを利用していないので警告が出ていないことを確認できます。 Region Based Isolationについて sendingに関連する話題として、Region Based Isolationについて説明します。sendingを付与しているとNon-Sendableの値を送れると説明しましたが、実はsendingキーワードをつけなくてもコンパイラが同じように判断します。次のコードのようにsendingを付与していないreceiveWithoutSendingメソッドにNon-Sendableのクラスを渡しても警告が出ません。また、useCounterAfterSendingメソッドでは呼び出し元でcounterを利用しているので先ほどと同じ警告が出ます。 コンパイラが判断するならばsendingキーワードが不要に見えますが、必要になるケースを紹介します。上のコード例ではNonSendableCounterクラスをその場で初期化して渡しているため、安全に受け渡せるとコンパイラが判断します。しかし、少しコードを複雑にするとコンパイラが安全性を判断できなくなるケースがあります。 例えばNonSendableCounterを戻り値の型とするメソッドで値を取得してからその値を渡すと警告が発生しました。そこで、sendingをメソッドの戻り値の型に付与すると警告が解消されることを確認できました。戻り値にsendingをつけることで、その値が所有権ごと安全に受け渡されることを明示できます。 Task-isolatedな値について 次に、Task-isolatedを見ていきましょう。もう一度、はじめに紹介したwithCheckedThrowingContinuationのコード例を紹介します。 continuation.resumeメソッドの引数にsendingキーワードが付与されているので、sending parameterに関する警告が出ています。 しかし、コードを見る限りvalueを受け取りresumeに渡した後は後続で使っていないように見えます。ここで、Task-isolatedなvalueの意味が重要なので説明します。Task-isolatedの警告の別のケースを見てみましょう。 Task-isolatedの警告が出ました。Taskの外から中に送ると、Task-isolatedになるとわかります。Uses in callee may race ... の警告は、Task-isolatedの利用と呼び出し先の利用でデータ競合が起きるかもしれないという意味です。つまり、呼び出し元でどのように使われるかをコンパイラが判断できないので警告が出ます。一方で、Taskの中で変数を定義した場合はスコープが明確なので警告が出ません。 Combineのsinkする値について Combineのsinkで利用する値はsinkの外で初期化されているため同じくTask-isolatedと判断されます。次のCombineのコード例のように、FutureをsinkするとTask-isolatedの警告を確認できます。 すでに紹介したwithCheckedThrowingContinuationのコード例でも同様にCombineのsinkを利用していたため、Task-isolatedの警告が出ました。警告の解決方法として、送る値をSendableにするか、利用するクラスをMainActorにします。sink内外で同じコンテキストにそろえることで警告が消えます。 その他の警告 最後に、これまでに説明したことを踏まえて解決できる警告を2つ紹介します。 1. passing closure as a 'sending' parameter risks causing data races sendingパラメータの警告で警告内容がわかりにくいケースを紹介します。 クロージャ自体の原因ではなくキャプチャしている値が原因です。TaskのクロージャにNon-Sendableなselfを渡していますが、selfの利用をTaskの中のみに限定できないので警告が出ています。ClassをMainActorにするか、Sendableに準拠すれば解決します。 2. static property '...' is not concurrency-safe because it is nonisolated global shared mutable state シングルトンクラスなど、static変数/class変数をnonisolatedのコンテキストで利用しているケースです。 定数のみ利用しているならばstructにするといったリファクタリングで対応可能です。状態を持つケースではMainActorを付与して利用側とコンテキストをそろえることを検討してください。 まとめ 本記事ではSwift 6対応を始める前に知っておくと役立つポイントを紹介しました。警告の大半はSendable非準拠とコンテキストの不一致の2種類でした。特にコンテキストの不一致がSwift 6移行における最重要ポイントです。また、Swift 6でビルドが成功しても、実行時のクラッシュが発生するリスクは残っているので動作確認も大事です。本記事が移行作業の一助になれば幸いです。 さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。コーポレートエンジニアリング部ITサービスブロックの高橋です。 当社はJira / Confluence Data Center版からAtlassian Cloudに移行しました。今回は、実務で直面した課題を交えてその経験をお伝えします。 目次 はじめに 目次 なぜ移行したのか 移行の進め方 アプリ移行を重視した理由 事前準備 組織統合の整理 SSO/SCIMと権限設計 グループ同期の注意ポイント UAT(ユーザ受け入れテスト)の設計と運用 ユーザへの案内内容 アプリ確認を支えるガイド 問い合わせの集約方法 本番移行 移行後の対応 権限の付け替えとAPI活用 実際にハマったポイント 外部サービスに残る旧URLへの対応 移行を終えて おわりに なぜ移行したのか 私たちが移行を検討した理由は大きく2つあります。1つはユーザ目線、もう1つは管理者目線です。 ユーザ目線として、Data Center版では最新機能が利用できず、ユーザ体験を損なっていました。 UIの違い:Confluenceのライブ編集エディタなど、Cloud版のUI改善がData Center版には反映されない SaaS間連携の制約:Slackとの連携などCloud前提の機能が多く、Data Center版では手段が限られる 自動化機能の差:Jira AutomationのトリガーやテンプレートがCloud版の方が充実している Cloud限定の新機能:Atlassian IntelligenceやJiraのチーム管理対象プロジェクトが利用できない 管理者目線では、管理・運用負荷やセキュリティ対応の負担が増していました。Cloud版ではこれらが以下のように改善されます。 定期メンテナンスによる停止 → 自動アップグレードによりダウンタイムが不要に 都度必要になるストレージ増設 → Atlassianが管理するため不要に 定期アップグレードや脆弱性対応 → 自動適用されるため運用負荷が軽減 ユーザ管理や権限管理の運用負荷の増大 → SCIMによる自動プロビジョニングが利用可能に セキュリティ対策の個別対応 → Atlassian Guardによるデータ保護や脅威検知の一元管理が可能に Data Center版のサポート終了(2029年3月28日にEOL予定) 以上を踏まえ、最終的にEOLの明確化が決定打となり、ユーザ利便性の向上と管理コストの削減を目的にAtlassian Cloudへの移行を決めました。 移行の進め方 移行は「調査→検証(UAT)→本番」の段階で進め、2024年に情報収集と方針決定、2025年上期に移行準備とUAT、下期に本番移行するスケジュールで実施しました。 移行作業の大部分はSIに委託しました。具体的には以下のような作業です。 Atlassian純正移行ツールによるデータ変換・投入 Jira / ConfluenceのデータをCloud Migration Assistantで変換・投入し、ボードやページの移行可否を検証するためにツールの最新版で再移行テストを実施。 添付ファイル移行(Jira / Confluence)の個別実行と複数回の実施 添付ファイルは別工程で移行し、Jira添付→Confluence添付の順で実行。完了しない箇所は再移行・調整を繰り返した。 本番移行の手順実行(停止→再移行→確認) 旧環境の停止(夜間)→Jiraデータ移行→Confluenceデータ移行→データ確認、という順序で本番移行を実施(移行日程・停止時間の調整を含む)。 アプリ/マクロの個別移行・チューニング ユーザーマクロやアプリ固有の変換不具合に対し、対象スペースでの変換・調整・チューニングを実施。必要箇所は再移行や個別チューニングを依頼。 リダイレクト/旧環境の読み取り専用化等の運用設定 移行後のリダイレクト設定や旧環境を読み取り専用に切り替える作業を実施。 また、移行後のリスクに備え、旧Data Center版の環境は保険的に半年間維持する方針としました。 アプリ移行を重視した理由 (※以下、旧来の呼称である「アドオン」を含め、本記事では「アプリ」と表記します) 当社の環境は、長年の運用で蓄積されたデータ量が膨大であり、細部まで逐一確認できる状況ではありませんでした。そのため、一般的な表示や日常機能の微細な差分をすべて確認するのではなく、データ変換で致命的な問題が発生しないことを前提としました。移行時の最重点は、データ変換だけでは対応できないアプリの検証に置きました。 アプリ移行が厄介なのは、「変換してみないと変換精度や変換後の実行エラーが判明しない」点です。 Data Center版のマクロやカスタムスクリプトは、Atlassian Cloudの標準機能に置き換えられる場合もあれば、別のアプリで代替するしかない場合もあります。代替先に移す場合でも挙動や仕様が異なるため、変換ツールがあっても正常に動作するか、どの程度エラーが発生するかは試してみないと分かりません。 そのため実務では、検証環境で変換テストを行い、UATで例外を拾い、検出した問題をSIに返して対応・再変換するという反復作業を行いました。 すべてを完璧に移行するのは現実的でないため、アプリの使用件数など影響度で優先順位を付けました。軽微・特殊な例外はユーザ側で手作業により補完する方針です。 事前準備 組織統合の整理 移行検証で意外に問題となったのが、グループ内の別組織がすでにAtlassian Cloudを契約していた点です。 このまま別々の組織として運用すると、SSOやプロビジョニングの設定も分断され、ユーザ管理や権限管理の運用コストが大きくなるため、組織を統合する方針としました。 具体的には、既存の組織を当社側で管理する形に移管し、その組織内でサイトを分けて新規サイトに移行する構成としました。既存環境を壊すことなく統合でき、複数サイトのアクセス権を組織グループで一元管理できるようになりました。 このような対応は、組織間の調整やAtlassianとの事務的な手続きを伴います。技術的な作業とは別に時間がかかるため、移行の早い段階で整理しておくことで、後工程での手戻りを防ぐことができます。 SSO/SCIMと権限設計 Atlassian Cloudでは、Data Center版と比べて認証・ユーザ管理の考え方が変わります。Data Center版でもSSO自体は利用できますが、Atlassian Cloudでは組織単位でのユーザ管理やサイト単位でのアクセス制御、SCIMによる自動プロビジョニングが前提です。ユーザやグループの管理がよりIdP寄り(当社ではEntra ID)になります。 この違いにより、単純に既存の権限設計をそのまま移すのではなく、ユーザ管理と権限管理をあわせて見直す必要があると判断しました。 Entra IDを中心にSSOを構成し、ログインを一本化(以前と同様) Entra IDの組織グループを活用した権限管理に移行(Data Center版では静的グループを中心に、一部スクリプトで擬似的な同期を行っていた) SCIMによる自動プロビジョニングを導入し、手動・スクリプトベースのユーザ/グループ管理を削減 移行時はユーザ重複やドメインを事前に整理し、UATで実際のアクセスを確認 Data Center版では実現できていなかった「IdP側のグループと権限の連動」を前提に設計し直したことで、移行後のユーザ管理の手間を大きく減らすことができました。 グループ同期の注意ポイント SCIMによるグループ同期を行う際、同期したいグループと同名のグループが既に存在する場合、同期前に変更内容のレビューがUI上で求められる点にも注意が必要です。この場合、どのユーザが追加/削除されるかを事前に確認し、承認したうえで同期を実行する必要があります。 既存グループとの衝突が発生すると、そのままでは同期できないケースもあるため、グループ設計や命名の整理を事前に行っておくことが重要です。 support.atlassian.com UAT(ユーザ受け入れテスト)の設計と運用 UATは約1か月間設けました。検証用のAtlassian Cloud環境を構築し、そこにData Center版のデータを変換して投入し、ユーザが自由に触れる状態で検証を進めました。検証環境のデータは本番に引き継がれません。したがって検証で「再設定が必要だ」と判明した箇所は、検証中に手順を確認してもらいました。本番の利用開始後に改めて同じ手順で再設定していただく前提です。検証環境は「変換後の動作確認と再設定手順の確認の場」と位置づけました。再変換で潰しきれないものを本番で再設定していただく運用にしました。 ユーザへの案内内容 基本動作: ログインや基本機能に問題がないか。 業務の継続性: 普段の業務がこれまで通りスムーズに行えるか。 アプリの重点確認: マクロや自動化を多用しているユーザには、アプリの動作を入念に確認するよう依頼。 ライトユーザへの負荷設計: 複雑な機能を使っていないライトユーザには、単純に「いつもの業務ができるか」だけを確認してもらい、全体の負担を抑える。 アプリ確認を支えるガイド ユーザが確認しやすいように、ガイドとなる資料を配布しました。資料は対象アプリごとに「どこで使用されているか」「移行されないもの」「正常に移行される想定のもの」「各自で再設定や運用変更が必要になりそうなもの」をざっくりリストアップしたものにしました。 問い合わせの集約方法 問い合わせや不具合報告は専用のSlackチャンネルに集約し、Slackワークフローでフォーム化して問い合わせを集計できるようにしました。報告された事項は順次SIに確認・修正を依頼しました。 本番移行 本番移行のタイムスケジュールは以下のとおりです。 10/03(金):検証環境の再構築 10/06(月)〜 10/10(金):添付ファイル移行(複数回実施) 10/31(金)22:00:Data Center版の本番環境を停止・読み取り専用に設定 11/01(土)深夜〜11/02(日):本番の移行作業(Jiraの移行対応→Confluenceの移行対応) 11/02(日):移行後データ確認、リンク修正、設定修正 11/03(月):当社側でのデータ確認・結果判定 11/04(火):Atlassian Cloud運用開始 予備日:移行の結果判定がNGの場合や追加対応が必要な場合に備え、11/22(土)〜11/24(月)を予備日として確保(連休をターゲットに) 事前にUATで十分に検証していたため、本番移行で大きな問題は発生しませんでした。また、UAT期間中にアプリ周りのデータ変換を複数回繰り返していたことにより、変換精度はある程度向上しており、本番時のリスクを抑えることができました。 移行後の対応 権限の付け替えとAPI活用 本番移行の完了後、各スペースの権限を既存の静的グループからEntra IDと同期した組織グループに付け替えるにあたり、Atlassian CloudのAPIを利用して一括付け替えを実施しました。 あらゆるグループを差し替えたわけではなく、トップレベルや本部単位の主要グループを対象に差し替えています。これは入退社によるアクセス権管理を自動化するためです。 権限の付け替えは対象が増えると手作業では現実的ではないため、スペース単位で整理した内容をもとにAPIを使って一括で処理する形にしました。 基本的には以下の流れで進めました。 スペース一覧を取得 既存の権限(グループ)を取得 新しいグループ構成との差分を整理 APIで権限を追加/削除する また、権限の付け替えは一度にすべて入れ替えるのではなく、まず新しいグループの付与が正しく行われていることを確認したうえで、既存の権限を削除するようにしました。 実際にハマったポイント Atlassian CloudのAPIは、トークンの種類によってリクエスト方法が変わります。 スコープなし(従来のAPIトークン) スコープ付きトークン(推奨) それぞれリクエストURLの形式が異なります。 Confluence(スコープなし): https://<site>.atlassian.net/wiki/rest/api/space Confluence(スコープ付き): https://api.atlassian.com/ex/confluence/<cloudId>/wiki/rest/api/space Jira(スコープなし): https://<site>.atlassian.net/rest/api/3/project Jira(スコープ付き): https://api.atlassian.com/ex/jira/<cloudId>/rest/api/3/project スコープ付きトークンではapi.atlassian.comベースのURLにCloud IDを含める必要があります。Cloud IDはサイトURLと異なる識別子であり、これを含めてリクエストを構築しないとAPIは動作しません。 Cloud IDは以下の方法で確認できます。 admin.atlassian.comのURL /s/{cloudId} から確認 https://<site>.atlassian.net/_edge/tenant_info にアクセスして取得 詳細は公式ドキュメントを参照してください。 support.atlassian.com support.atlassian.com また、Atlassian CloudのAPIはバージョン(v2 / v3)によって仕様が異なります。基本的な操作は共通しているものの、レスポンス形式や扱える項目に差があるため、実装時にはドキュメントを確認しながら利用する必要があります。 外部サービスに残る旧URLへの対応 旧Data Center版の環境は保険的に半年間維持する方針としましたが、廃止後は完全にアクセスできなくなります。Jira / Confluence内のリンクは移行時にある程度変換されます。一方、SlackなどAtlassian外のサービスに記載されたURLは自動で置き換わらないため、そのままではリンク切れが発生します。 そのため、旧環境と新環境のページURLの対照表の作成をSIに依頼し、必要に応じて参照できるようにしました。これにより、移行後も外部に残っているリンクから該当ページを特定できるようにしています。(ページの一覧はData Center版ではDBから取得、Atlassian Cloud環境ではREST APIで取得可能) 移行を終えて 社内からの問い合わせ件数は全体で約100件でした。多くはアプリのデータ変換や表示差分、操作方法の違いに関するもので、致命的な障害は発生していません。 ただし、移行ツールやSI側の対応だけでは解決できない細かなケースもあり、一部ユーザ側で手動修正が必要になりました。主な例は次のとおりです。 マクロの差し替え:旧環境で使用されていたマクロの一部がAtlassian Cloudで正常に表示されず、代替マクロへの差し替えや再作成が必要になった Plans/ガントの再設定:表示やフィルタ設定の一部が引き継がれず、再設定が必要になった JQLの書き換え:旧環境で使えていたJQL関数がAtlassian Cloudで使えず、代替クエリへの書き換えが必要になった Automationの再構築:トリガーや参照フィールドの仕様差で動作しないルールがあり、修正・再作成が必要になった この大規模な移行プロジェクトは何が起きるか分からず、計画初期から漠然とした不安がありました。移行ツールの質も上がっているからか、特に大きなトラブルもなく移行でき、ほっとしました。 アプリ周りの移行が最も大変でしたが、見込み通りだったのは幸いでした。変換方法についてSI側と協議しながら再変換を繰り返すなど、手探りな部分はどうしても発生します。手動での修正は、対象が多すぎると現実的でないため、できる限り変換で潰していくことが重要です。変換処理は繰り返す前提で、早いうちから検証を進めることをおすすめします。 おわりに Data Center版からAtlassian Cloudへの移行は技術的なデータ移行作業だけではありません。機能面・運用面・契約・組織間調整と、多面的に検討する必要があります。UATを中心に据えて現場で検証し、APIを活用することで作業負荷を抑えられます。同様の移行を検討している現場の参考になれば幸いです。 ZOZOでは、テクノロジーを活用して社内の課題を解決する仲間を募集中です。SaaSの運用管理からシステム間連携・自動化まで、グループ全体の業務環境をよりよくするための取り組みにチャレンジしていきます。カジュアル面談も実施していますので、社内ITについて語りましょう! ご興味のある方は、下記のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは、ZOZOTOWN開発2部iOSブロックのらぷ( @laprasdrum )です。普段はZOZOTOWN iOSアプリを開発するチームで各メンバーの開発における設計や技術課題のフォローアップを担当しています。また、iOS領域におけるテックリードとして社内の技術共有会や ZOZO.swift などを運営しており、各プロダクトのiOSチーム全体をつなげる横断活動に従事しています。 ZOZOTOWN iOSアプリは2010年11月にリリースされ、15年以上にわたって開発が続くプロダクトです。長い歴史の中でチームと技術が変遷し続け、Fat ViewControllerやObjective-Cコードの残存といった技術的負債を抱えていました。これに対してチームは2023年からアーキテクチャの刷新に本格的に取り組んできました。 本記事では、その3年間の変遷を振り返り、アーキテクチャがどのように進化し、設計をレビューする力がチーム全体にどう広がったかをお伝えします。 なお、チーム運営の全体像は ZOZOTOWNのiOSチームを支えるチーム運用 で紹介しています。Fat ViewController解消の具体的な手法は ZOZOTOWN iOSアプリでのFat ViewController解消への取り組み を参照してください。 目次 はじめに 目次 アーキテクチャ変遷の全体像 最初の一歩 — 2023年の型作り MVVMの採用と方針ドキュメントの策定 アーキテクチャレビューの始動 意思決定を支えた二層構造 アーキテクチャの判断と見直し レイヤー設計の選定と見直し Translatorレイヤーの導入(2024年5月) MVVM + UseCaseの採用(2024年9月) ドメインレイヤーの廃止とRepositoryパターンへの集約(2026年3月) 開発プロセスの進化 定量データで見るチームの変化 アーキテクチャ移行の規模 設計レビューの担い手の逆転 AIによる設計知識のチーム展開 1. Geminiによるレビューコメントの要約 2. Copilotレビュー指示の導入・強化 3. チーム共有コマンドへの昇華 4. アーキテクチャ学習サイトと理解度クイズ まとめ アーキテクチャ変遷の全体像 各トピックの詳細へ入る前に、3年間の変遷と技術スタックの移行を俯瞰します。 まず、主要な取り組みの時系列です。 2023年 — MVVM化の型作り Fat ViewControllerの解消を目指し、対象画面を決めてリファクタリングを実施した。 2024年前半 — Translatorレイヤーの導入・アーキテクチャレビューの再設計 API通信モジュールとUIレイヤーの依存を断ち切るTranslatorを導入した。PRレビュー課題の体系的分析から、関心の分離をレビューする運用に刷新している。 2024年後半 — MVVM化の複数画面への展開・Objective-CからSwiftへの移行 商品詳細を中心にMVVM + UseCaseを適用した(詳細は 前述の記事 を参照)。 2025年 — 大規模画面の並行リファクタリング 大規模画面にMVVM + UseCaseを適用し、Storyboardの一部削除やBaseViewControllerの廃止を進めた。 2025年後半 — ガイドラインと設計制約の明文化 MVVM + UseCase統一ガイドやアーキテクチャガイドラインを策定した。 2026年〜 — Repositoryパターン本格導入 実装経験を経てUseCaseが不要なケースを特定し、Repositoryパターンを本格適用している。 技術スタックも同時期に大きく変わっています。 UIアーキテクチャ(2023〜2026) MVC(Fat ViewController)からMVVM + Repositoryへ移行した。 UIフレームワーク(2023〜2026) UIKit + XIB/StoryboardからUIKit(コードベース)+ SwiftUIへ移行した。 非同期・リアクティブ(2024〜2026) ReactiveSwiftからCombineへ移行し、非同期処理にSwift Concurrencyを導入した。 言語(2023〜2025) Objective-CとSwiftの混在状態からSwift中心へ移行した。Swift比率は94.7%(2023年)から99.5%(2026年)へ向上している。数値上は約5ポイントの変化だが、2025年に削除した12クラスはのべ64箇所から参照されており、1クラスの削除に最大17ファイルの改修を伴った。通常の案件開発と並行しながら段階的に解消した。 テスト(2025〜2026) XCTest + NimbleからSwift Testing + 振る舞い駆動のテスト記述へ段階的に移行している。 これらの移行は一括で実施したのではなく、チームの習熟度に合わせて段階的にスコープを拡大していきました。その意思決定の裏側を以降のセクションで掘り下げます。 最初の一歩 — 2023年の型作り MVVMの採用と方針ドキュメントの策定 Fat ViewControllerの解消にあたり、チームはアーキテクチャとしてMVVMを採用しました。実装に先立ち、2022年末からMVVMの実装方針をチームで統一するためのドキュメント作成が始まり、2023年初頭にWIPを外してチームに公開されました。これにより、MVVM化の共通認識となる土台が整いました。このドキュメントは以降も継続的に更新されています。 アーキテクチャレビューの始動 方針が共有された直後の2023年3月、最初のMVVM化が始まりました。この取り組みで特に意識されたのは、単なる1画面の改修ではなく「今後他の人がMVVMで画面を実装する際の参考になるか」という点です。 実際にこのレビューには25件のアーキテクチャレベルのコメントが付きました。レビュアがデータフロー図を描いてViewModelの状態更新フローを可視化するなど、コードではなく設計を先にレビューする文化の萌芽となっています。 続く2画面目(2023年4月)では、レビュー依頼の本文に設計図を含む「アーキテクチャレビュー」セクションが初めて明示され、テンプレートとして定着しました。 こうしてMVVM化の最初の型は作れたものの、これを複数画面に展開し、チーム全体で設計判断の質を維持していくには、個々のPRレビューだけでは限界がありました。チームとして設計を議論し、方針を決め、知識を共有する仕組みが必要だったのです。 意思決定を支えた二層構造 チーム運用の記事 で紹介した「開発生産性MTG」と「Rethink!」について改めて簡単に説明します。 開発生産性MTG :シニアエンジニアとマネージャーが主導し、コードベースの技術的課題を除くチームの課題を議論する場である(2023年10月〜、約30回開催)。各案件の開発マネジメント、PRレビュー、メンバー間の情報格差など、チームがより成熟するために必要な課題を整理し、アクションを設計していた。 Rethink! :技術的な課題をチームメンバーで定期的に見直す週次勉強会である(2024年2月〜通算90回以上)。技術的負債と感じていること、Appleが提示している技術への所感や適応方針、自分だけしか知らないかもしれないことなど、お題は多岐にわたる。話した内容はアーカイブとして残し、過去の意思決定の資料として参照している。 この2つは、案件開発の流れの中だけで方針を決めず、立ち止まって振り返り分析するためのイベントです。 なお、現在は開発生産性MTGという場を設けなくても、メンバー各々がときに立ち止まり、チームに課題を投げかけるコミュニケーションが日常的に行われるようになりました。立ち止まって考える姿勢が特定の会議体に閉じず、チーム全体に浸透した結果、開発生産性MTGは役割を終えています。 この二層構造から3つの重要な方針が生まれました。コード課題の根本原因の特定、リファクタリングのゴール定義、そしてレビュー工程の分離です。 まず、コード品質の課題を時間をかけて分析し、関心の分離・疎結合がコード課題の原因の8割という結論に到達しました。暗黙知が多く「良いコードとは何か」の定義が揃っていないことが本質的な問題であり、「きれいなコードの定義」を目指すのではなく「アンチパターンの提示」で十分という方針が生まれました。 この分析を受けて、リファクタリングのゴールを「きれいなコードをメンバー全員が理解すること」と定義しました。リリースすることはMUSTではなく、認識を揃えることが目的です。この方針のもと、Rethink!をチーム全体での実践場として活用することが決まりました。 同時に、レビューが特定のメンバーに集中しマージ待ちが常態化していた問題も分析しました。その結果、外部品質(仕様通りに動くか)と内部品質(設計・保守性)の確認がひとつのレビューに混在していることが負荷の原因と判明しました。これを受けて、基本設計レビュー(仕様の漏れと実現可能性)、アーキテクチャレビュー(レイヤー間の関心分離)、コードレビュー(実装)の3段階にレビュー工程を分離しました。 これらの方針は、Rethink!を通じてチーム全体の実践に落とし込まれました。全員が同じ題材で設計を提出して観点を揃えたり、実際のPRを題材にレビューの進め方を学んだりしました。レイヤー設計やレビュー手法といったアーキテクチャの具体的な意思決定もRethink!から生まれています。 開発生産性MTGが方向性を定め、Rethink!がチーム全体で実践し、ドキュメントとして蓄積していくことで、暗黙知が特定の個人に閉じることなく、チーム全体の形式知として更新される構造を実現しました。PRレビューのコメントでも日常の雑談でも、「Rethink!で話してみますか」という言葉が自然に出てくるようになりました。 アーキテクチャの判断と見直し 二層構造の仕組みが実際にアーキテクチャの判断をどう動かしたのか、レイヤー設計と開発プロセスの2つの観点から見ていきます。 レイヤー設計の選定と見直し チームはレイヤー構成を3回にわたって選定・見直しました。 Translatorレイヤーの導入(2024年5月) 「関心の分離が課題の8割」という方針を受けて、最初に着手したのはデータレイヤーの整備でした。ZOZOTOWN iOSでは、API通信の処理をアプリ本体とは独立したモジュールとして切り出しています。このモジュールがAPIリクエストの発行とレスポンスのデコードを担い、アプリ側はモジュールが提供するクライアントを呼び出す構成です。 しかし当時は、このAPI通信モジュールが返すレスポンス型をViewModelがそのまま扱っており、モデル変換のロジックがUIレイヤーに漏れ出していました。UIレイヤーがAPI通信モジュールの型に直接依存する状態です。 この課題に対して導入されたのがTranslatorレイヤーです。API通信モジュールのレスポンスをアプリ内のモデルに変換する責務を一手に担い、UIレイヤーとAPI通信モジュールの依存を断ち切りました。2024年5月に最初の実装がマージされ、同時期にMVVM方針ドキュメントにもTranslatorの項目が追記されています。 この分離は設計方針の浸透とテスタビリティの両面で効果がありました。ViewModelはアプリ内のモデルだけを扱う前提になるため、「ViewModelはアプリ内のモデルを扱う層である」という方針をチームに伝えやすくなりました。また、ViewModelのテストからAPI通信モジュールへの依存がなくなり、テストビルド時に不要なモジュール依存を削除できるようになりました。 Translatorの導入により、データレイヤーの一部が確立されました。次の課題は、ViewModelとこのTranslatorの間、つまりドメインレイヤーをどう設計するかです。 MVVM + UseCaseの採用(2024年9月) データレイヤーやドメインレイヤーの責務分割にはチーム方針がなく、メンバーによるコード品質の差が大きくなるポイントでした。一方で、馴染みのないアーキテクチャを導入してメンバーの認知負荷を上げるわけにもいかず、最低限のレイヤー定義が求められました。開発生産性MTGでUseCaseの責務を議題に絞り込み、Rethink!で3つの選択肢を比較しました。 Androidの推奨アプリアーキテクチャ方式 :UseCaseはオプショナルで、複雑なビジネスロジックのカプセル化や、複数のViewModelから再利用されるロジックの共通化を担う。ただしデータレイヤーのRepositoryがある前提 Clean Architecture + DDD方式 :UseCaseがドメインロジックを担い、ドメインモデルとペアで設計する。画面に紐づかずドメイン単位で定義するため、境界設計が難しくなる MVVM + UseCase方式 :UseCaseがドメインレイヤー(本来のUseCase)とデータレイヤー(Repository)の両方をカバーする。データソースが単一ならRepositoryを別途作るコストをスキップできる チームが選んだのはMVVM + UseCaseでした。ZOZOTOWN iOSではデータソースが単一のケースが多く、Repositoryを別途設ける必要性が低かったためです。「レイヤーの責務が肥大化しすぎた場合はClean Architecture方向に進化させる」という留保付きの判断でした。 ドメインレイヤーの廃止とRepositoryパターンへの集約(2026年3月) この判断のもと、チームは複数画面でMVVM + UseCaseを実装していきました。しかし約1年半の運用を経て、Rethink!で「ドメインレイヤー(UseCase)を廃止し、データレイヤー(Repository)に責務を集約すべきではないか」という議論が持ち上がりました。実装を重ねる中で見えてきたのは以下の点です。 ZOZOTOWN iOSではドメインモデルやそれを用いるビジネスロジックが稀であること APIClientとTranslatorの組み合わせが実質的にRepositoryの役割を果たしており、UseCaseとの責務が重複していたこと UseCaseを設けても責務の理解が揃わず、チーム内に混乱を招いていたこと 重要だったのは「各レイヤーの責務の理解が揃うこと」であり、UseCaseという層を設けること自体が目的ではなかったという気づきです。結果、MVVM + Repository(UseCaseなし)をチームの方針としました。 これは単なる揺り戻しではなく、「レイヤーの責務が肥大化しすぎた場合はClean Architecture方向に進化させる」という最初の留保に対する回答です。実際に使ってみた結果「肥大化ではなく責務の重複が問題であり、むしろ層を減らすほうが適切だった」という結論に至りました。 開発プロセスの進化 同時期に、設計の進め方そのものにも変化がありました。 アーキテクチャレビューでは、当初は重厚なPlantUML図を用いて設計を可視化していました。しかし「作図コストが高い割に手戻りは防げない」ことがわかり、Protocol定義をPRにして設計をレビューする軽量な手法に転換しました(2024年8月)。XIB/Storyboardからの移行方針(2025年4月)など、UIフレームワークの選定もRethink!で議論しています。 こうした議論と意思決定の積み重ねは、チームの活動にどのような変化をもたらしたのでしょうか。定量データで確認します。 定量データで見るチームの変化 アーキテクチャ移行の規模 ViewModel / UseCase / Repository / Translatorを含むコミットメッセージを年別に集計しました。 年 コミット数 前年比 2023 217 — 2024 302 +39% 2025 608 +101% 2026(〜2月) 194 — コミット数の増加と並行して、レガシーコードの削減も進みました。以下は2025年のレガシーコード削減実績です。 指標 2025年1月 2025年12月 削減率 Objective-C .mファイル 19 3 84% Objective-Cコード行数 2,768 788 72% XIB/Storyboard 88 58 34% 特筆すべきは、2024年に積み重ねた依存解消が2025年の大規模削除を可能にした点です。一度に大きく変えるのではなく、依存を個別に剥がし続けたことで、翌年のレガシーコード削減につながりました。 設計レビューの担い手の逆転 アーキテクチャ移行の規模が拡大する中で、その設計レビューを誰が担うかにも大きな変化が起きました。 2023年末時点では、GitHubのCODEOWNERSに筆者のみが設定されており、すべてのPRが筆者のApproveなしにマージできない状態でした。この期間にマージされたPRは月平均約53件にのぼります。一部案件のスプリントレビュー時にレビュー待ちチケットが残留し、「マージ待ちになっている時間が結構ある」という声が上がるほどで、まさに「できる人がやる」体制の限界でした。 2024年6月、この問題を受けて筆者ともう1名のシニアエンジニアによる2名体制に移行し、アーキテクチャレビューとコードレビューの役割を分担しました。同時に、それまでレビューに関わる機会のなかった他のメンバーをピアレビュアとして外部品質のレビューに参加してもらう体制を整え、レビュー文化の醸成とレビュア育成がここから始まりました。 PRレビューコメントのうち設計に関するものを定量分析した結果、シニアレビュアの比率は2024年10-12月の89%から2025年1-3月には25%へ低下しました。2026年1-2月時点では9%まで下がり、ピアレビュアが設計コメントの91%を担っています。2026年2月には固定レビュア制度そのものが廃止され、ピアレビュアのランダム選出に移行しています。 この変化の背景には、開発生産性MTGによる制度設計とRethink!での実践という二層構造の仕組みがありました。 数字の裏にある変化を一人のメンバーの軌跡で見ると、より具体的になります。初期(2024年)はレイアウト設計の範囲内にとどまっていたレビューが、UseCase設計の責務境界の理解(2025年1-3月)を経て成長しました。最終的にはディレクトリ構造・命名規則の統一提案・APIレスポンス型の設計方針など、プロジェクト全体を俯瞰するレビューに到達しています。 設計レビューの担い手が広がる一方で、シニアレビュアが持っていたレビュー観点そのものを、人に依存しない形で残す取り組みも並行して進めました。 AIによる設計知識のチーム展開 アーキテクチャの形式知化の最終段階として、設計知識とレビュー観点をAIを活用した4つの手段でチームに展開しました。 1. Geminiによるレビューコメントの要約 レビュア育成とシニアレビュアへのレビュー依存の解消を目指し、週次でPRレビューコメントを収集しGeminiに要約させてSlackに投稿しています。要約結果をチームの週次定例で振り返り、レビュー観点の共有やレビュア同士の知見交流、まだレビュアになっていないメンバーへのレビュー観点インプットに活用しています。 2. Copilotレビュー指示の導入・強化 GitHub CopilotによるPRへのインラインコメントは、開発者が環境構築や新しいツールを導入する必要がなく、チームメンバーがAIの恩恵を素早く実感できる手段でした。 copilot-instructions.md にレビュー観点を定義し、アーキテクチャ準拠・iOSベストプラクティス・テスタビリティ等の知見を追加しました。 Copilotによるアーキテクチャ関連コメントは月間28件から94件に増加しました(2025年8月から2026年2月)。従来はシニアレビュアが暗黙的にチェックしていた観点が自動的にレビューされるようになりました。 3. チーム共有コマンドへの昇華 レビュー観点をClaude Codeのコマンドとして言語化し、チーム共有のセルフレビュースキルとして整備しました。マージ先ブランチの確認、開発チケットや設計ドキュメントからのコンテキスト取得、アーキテクチャ準拠チェック、動作確認シナリオの生成まで、PR作成前に開発者自身でセルフレビューできるようにしました。 4. アーキテクチャ学習サイトと理解度クイズ アーキテクチャガイドラインやテンプレートの内容をもとに、インタラクティブな学習サイトの生成とCLI上でのクイズによる理解度チェックをClaude Codeのコマンドとして整備しました。レイヤー構成やデータフロー、アンチパターンなどをブラウザ上で視覚的に学べるほか、クイズではドキュメントに明記されたルールから出題し、回答ごとに根拠となるドキュメント箇所を提示します。 この4つは、暗黙知の移転経路がそれぞれ異なります。 Gemini要約 :人のレビューコメントをAIが集約して全員に届ける仕組み Copilot指示 :人からAIへの知識の埋め込みによる自動レビュー セルフレビューコマンド :人がAIを介して自分自身に観点を返す仕組み 学習サイトとクイズ :ドキュメントをAIが対話的な教材に変換する仕組み 経路が異なるからこそ、どれかひとつが欠けても他で補完できる構造になっています。 まとめ ZOZOTOWN iOSチームのアーキテクチャは、2023年の1画面でのMVVM化から始まりました。2024年のTranslator導入と複数画面への展開、2025年の大規模並行リファクタリング、2026年のRepositoryパターンへの集約と段階的に進化しています。チームの習熟度に合わせてスコープを拡大し、実装経験をもとにレイヤー構成そのものを見直す判断もできるようになりました。 この過程を支えたのが、開発生産性MTGで方向性を定め、Rethink!でチーム全体が実践し、ドキュメントとして蓄積していく二層構造の知識形成フローです。暗黙知が特定の個人に閉じることなく、チーム全体の形式知として更新される仕組みを作りました。 さらに、Geminiによるレビューコメント要約、Copilotレビュー指示、Claude Codeのセルフレビューコマンド、アーキテクチャ学習サイトの4つを整備しました。これらを通じて、設計知識とレビュー観点をAIを介してチームに展開しています。 設計レビューの担い手もシニアレビュア89%からピアレビュア91%へ完全に逆転しています。 この変化はチームに具体的な効果をもたらしています。責務やデータフローの定義がチーム全体で揃ったことで、PRレビューの場で合意形成がスムーズに進むようになりました。設計の相談先が特定の個人に閉じなくなり、意思決定の属人化も解消されています。かつては口伝に頼っていた知識が体系化されたことで、新メンバーが自律的に学べる環境も整いました。 「できる人がやる」から「全員で設計をレビューできる」への転換が、アーキテクチャの一貫性とチームのスケーラビリティの両立を実現しています。 ZOZOでは一緒に働くエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ZOZOTOWN企画開発部 企画フロントエンド1ブロックの片岡優斗です。ZOZOTOWNでは、セール訴求や新作アイテム訴求、未出店ブランドの期間限定ポップアップ、著名人とのコラボなどの企画イベントが日々展開されています。その集客や回遊の起点となるランディングページを「企画LP」と呼んでおり、私はこの企画LPを主に実装するチームに在籍しています。 本記事では、LP制作における属人的なデザイン管理の課題解決に向けて「LP向けデザインガイドライン」を構築した取り組みをご紹介します。 目次 はじめに 目次 背景と課題 プロジェクトの進め方 プロジェクトの始動と進行の中で見えてきた課題 進め方の改善 ガイドラインの設計 ファイル構成 ガイドライン構成 変更可能な要素 コンポーネント まとめ さいごに 背景と課題 企画LP制作では速く作ることだけではなく、「案件ごとの表現を最大化すること」が求められています。 一方で制作を支えるデザインに関する仕様や参照基準は十分に体系化されていませんでした。施策の種別ごとに担当者をある程度固定する体制を取っていたこともあり、それぞれの経験やナレッジをもとに制作を進める状態が続いていました。 これは月10〜20本という高頻度のリリースを支えるためにスピードを優先してきた結果でもあります。しかし約30名(2026年3月時点)のデザイナーが関わる体制へと成長する中で、この暗黙知に依存した運用では徐々に限界が見え始めていました。 施策数の増加や案件内容の多様化に伴い、従来担当していなかった種類の施策を担当するケースが増え、参照すべき基準や過去事例が見えづらくなってきました。その結果、同じ役割を持つUI要素であっても担当や案件ごとに微細な差分が生じるようになり、それらが積み重なることで開発側の調整コストが増大していきました。 組織規模と施策量の拡大によって従来の運用モデルでは対応しきれなくなったことで、本来注力すべき施策の世界観設計や企画ごとの表現づくりに十分な時間を割くことが難しくなっていました。 プロジェクトの進め方 この課題を解決するために立ち上げたのがデザインガイドラインプロジェクトです。当初はデザインルールを整備することをゴールとしていました。リモート勤務が中心になって以降、職種間のコミュニケーション量も減っていました。そこで各デザイナーチームから担当者を1人選出して定例MTGや専用Slackで連携を取りながら継続的に議論できる体制を整えました。 プロジェクトの始動と進行の中で見えてきた課題 当初はエンジニア側がLPでよく使うコンポーネントをリストアップしてデザイナー側に共有し、デザイナー側がその内容をもとにガイドラインを作成する方針で進めていました。しかし実際に進めてみるとガイドライン作成は想像以上に停滞しました。 進行が停滞した背景には、ガイドライン整備という作業の性質と企画デザイナーの業務特性との間に構造的な問題があったと考えています。 ガイドライン整備は要素の網羅的な洗い出し、揺れの排除、再利用可能な形への収束が必要な作業です。さらに内容の正確さだけではなく公開可能なドキュメントとしての見た目の整合性や網羅性も求められるため、1つのコンポーネントを整えるだけでも想定以上の時間がかかります。 一方で企画デザイナーの日常業務は、案件ごとの世界観や表現を設計する発散型の思考が中心です。この性質の違いから、通常業務と並行してガイドラインをゼロから整備することは構造的にも負荷の高い進め方でした。デザイナーの業務は案件の波に影響されやすく、繁忙期には計画通りに工数を確保することが難しいという企画LP組織の特性もありました。 進め方の改善 こうした構造的な課題を踏まえ、ガイドライン整備という作業の性質に着目して進め方を改善しました。要素の網羅的な洗い出しや仕様の収束といった作業は、実装構造やコンポーネント設計を把握しているエンジニアの視点と相性が良いと判断しました。 そこでエンジニアがコードベースから過去の仕様やユースケースを確認して叩き台を作成し、デザイナーがその内容を精査・調整してガイドラインを発展させていく作業を担う体制へと切り替えました。この体制によりデザイナーは「ゼロから整える作業」ではなく「品質を高める判断」に集中でき、収束型の意思決定が加速しました。結果として進行は大きく改善し役割分担も明確になりました。 またこの進め方に切り替えたことで、エンジニアが普段の開発業務で扱っているコンポーネントや変数に近い概念をFigma上でも扱いやすくなり、コンポーネントやバリアブルなどの機能活用も進みました。その結果ガイドラインは単なるデザインルール集にとどまらず、デザインシステムに近い形へと発展していきました。 ガイドラインの設計 ここからは、実際のガイドライン構成をご紹介します。 ファイル構成 本デザインガイドラインでは、デザイナーが管理しやすいように、ガイドラインとコンポーネントセットを同じFigmaファイル内で管理しています。そのためFigma内の情報量が多く、今後もコンポーネントが継続的に増えていく可能性も高いことから、拡張性を考慮してコンポーネント単位でページを分けて作成しています。またファイル名は「デザイン名 / 実装名」で統一し、デザイナー・エンジニアどちらの視点からも目的の項目にたどり着きやすくしています。 また運用マニュアルとして「デザインガイドライン運用マニュアル」と「LP運用マニュアル」を用意し、ガイドラインの利用ルールやLP制作時のFigma活用方法を明文化しました。 ガイドライン構成 ガイドラインは「コンポーネントの利用方法を伝える」ことを意識した構成になっています。コンポーネント名・構造・変更可能な要素・スタイル・デザインパターンをひとまとまりで参照できるようにしています。 コンポーネント名 基本構造 構造詳細(デザイン変更可能な要素) スタイル デザインパターン 変更可能な要素 企画LPは施策ごとにトーンや世界観が大きく変わるため、表現の自由度を残す必要がありました。一方で1つのコンポーネントの中でも自由に変更可能な箇所やデザインパターンが多数存在するため、FigmaのComponent propertiesだけでは十分に制御しきれませんでした。そのためパターンとして定義できるものはComponent propertiesで管理し、それ以外の自由に変更できる部分についてはガイドライン上で変更可能な要素を明示する形にしました。 コンポーネント利用箇所から変更可能な要素を確認できるプラグインを作成 ガイドライン上で明示するだけではコンポーネントを使うたびにガイドラインを都度見に行く手間がありました。そこでコンポーネントの利用箇所からでも変更可能な要素を確認できるように専用のプラグインも作成しました。 変更可能な要素は画面上で点線のレイヤーを付けて視覚的に示し、右側のアノテーションでも項目を明示してどこが調整可能かひと目で把握できるようにしています。 コンポーネント 各コンポーネントのページには、デザインガイドラインとFigmaコンポーネントを1つのファイル内にまとめています。左側にはコンポーネントの基本構造やSP(スマートフォン)/PCごとのスタイル、基本的な種類といったガイドラインを記載し、右側にはFigmaコンポーネントを配置しています。これにより、デザイナーはガイドラインを確認しながらそのままコンポーネントを利用でき、仕様の確認と実作業を同じファイル内で完結できる構成としました。 ガイドライン整備の結果、利用を開始した2025年7月下旬以降のコンポーネント利用数は継続的に増加していきました。制作現場でもコンポーネント利用が徐々に定着してきており、ガイドラインを整備するだけでなく実際に使いやすい構成や運用にまで落とし込めたことがこの定着につながったと捉えています。 まとめ 本記事では、ZOZOTOWNのLP制作における属人的なデザイン管理の課題に対し「LP向けデザインガイドライン」を構築した取り組みをご紹介しました。 デザインガイドラインを作成することで開発側のコンポーネントとデザインの差分が抑えられ、企画LP開発時の出戻り削減につながりました。 またエンジニアが叩き台を作りデザイナーが精査・発展させるという進め方は、業務特性に合わせた協業モデルの再設計でもあり、暗黙知を言語化しチームで活用できる知識に変えるための知見も得られました。 企画LPのように表現の多様性が求められる領域でも「固定する要素」と「自由に変更できる要素」を明確に区分することで、効率化と表現の最大化を両立しやすい形にできました。この結果、制作の起点が「それぞれの経験やナレッジで作る」から「既存のコンポーネントを活用して表現を作り込む」へと移行できました。デザインガイドライン作成を通して個人最適ではなく組織全体として効率的に制作を進められる土台ができたと考えています。 さいごに 今回の取り組みを通して、企画LP向けのデザインガイドラインを整備できました。一方でガイドラインは作成して終わりではなく、実際の制作現場で継続的に活用され改善されていく状態を作ることが重要だと思います。今後は継続的に改善できる仕組みづくり、さらなるガイドラインの浸透、Figma Makeなどを活用した拡張を進め、制作現場の運用基盤としてさらに発展させていくことが目標です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。Developer Engagementブロックの @wiroha です。3月23日(月)に、ZOZOにて中高生女子を対象とした体験イベント「 ZOZOTOWN・WEARを支える技術と働き方を知ろう! 」を開催しました。 これは 公益財団法人山田進太郎D&I財団 が実施する「 Girls Meet STEM 」プログラムの一環です。中高生女子がSTEM(科学・技術・工学・数学)分野で働く人やSTEM分野で学ぶ学生、実際の現場に触れることで、将来の可能性を広げる機会を提供することを目的としています。ZOZOではこの活動の意義に共感し2024年より参画しており、今回は3度目の開催です。 今回は18名の参加者が集まり、オフィスツアー、サービス体験&技術紹介、女性エンジニアとの交流を通じて、ファッションと技術の面白さを体感しました。本記事では、当日の様子をご紹介します。 イベント概要 日時:2026年3月23日(月)13:00~15:30 会場:ZOZO西千葉本社 対象:中学1年生~高校3年生までの戸籍上または性自認が女性の方 定員:20名 www.shinfdn.org オープニング まずは会社紹介や事業紹介により、ZOZOのことを知ってもらう時間を設けました。ZOZOTOWNやWEAR by ZOZO(以下、WEAR)のサービス、計測事業などについて解説することで、この後のサービス体験&技術紹介の内容をより深く理解してもらうことを目指しました。 サービス体験&技術紹介 2つのグループにわかれ、「サービス体験&技術紹介」と「オフィスツアー」を交代で実施しました。「サービス体験&技術紹介」では、ZOZOTOWNのARメイク、フェイスカラー計測ツール「ZOZOGLASS」、WEAR by ZOZOのファッションジャンル診断を体験してもらいました。 AR技術でメイクが施された画面上の自分の顔に驚き、カラフルで見慣れないZOZOGLASSを手に取り笑顔が出るなど、ZOZOの技術を楽しんでいる様子でした。体験した後は各サービスに使用されている技術を紹介し、技術によってファッションが楽しくなることを感じてもらいました。 オフィスツアー こだわりの社屋である、西千葉本社のオフィスツアーを実施しました。メッセージが込められたアートや遊び心のある会議室、絨毯の模様や色使いの工夫など、ZOZOらしいデザインが施されたオフィス内を案内しました。クイズを交えながらの紹介で、参加者の皆さんも考えながら楽しんでいました。 昨年竣工したばかりの会議棟「ZOZOTENT(ゾゾテント)」も案内し、最新のオフィス環境を体験してもらいました。最初は緊張していた参加者も、オフィス内を歩くうちにリラックスできた様子でした。 パネルトーク 次にパネルトークを開催し、新卒1〜2年目の若手女性エンジニアから話を聞きました。学生時代の経験やエンジニアになろうと思ったきっかけ、中学・高校時代の進路選択などについて語ってもらいました。 年齢の近いエンジニアからの話は身近に感じられたようで、熱心に聞き入っていました。転学科した話もあり、タイミングに合わせて進路やキャリアを考えながら、自らアクションすることの大切さを感じてもらえたのではないでしょうか。 質問会 その後は少人数のグループに分かれて参加者からの質問に答える時間を設け、ZOZOの女性エンジニア4名が一緒にお話ししました。 Slido を活用したところ、非常にたくさんの質問が寄せられました。「ZOZOにはどんな職種がありますか?」「エンジニアに文系の人はいますか?」「就活で一番必要だと思ったスキルは何ですか?」など、学習や進路に関する質問に対してエンジニアたちが自身の経験を交えながら丁寧に答えました。 お土産 参加者の皆さんに、ZOZOオリジナルグッズなどをお土産としてお渡ししました。イベントの思い出として楽しんでもらえたら嬉しいです。今回の体験時間に入りきらなかったZOZOMATもお渡ししており、自宅で足の3Dサイズ計測を体験してもらえればと思います。 最後に 参加者の皆さんからは、次のような感想をいただきました。 文理選択のみならず、学部や職業決めの体験談を聞くことができたので、とても参考になりました。 実際に働いている方々が感じていることや、大切にしている考え方などを教えていただき、自分の視野が広がったように感じました。 施設もとても綺麗でとても楽しそうに仕事していて、私もこんなところで働きたいなと思いました。 将来の職についてたくさん不安があったのですが、悩みを沢山聞いていただけて本当に参加してよかったと思いました。 ZOZOはこれまでもさまざまな女性活躍推進のための活動に取り組んできており、今後もこうした機会を提供していきたいと考えています。本イベントにより中高生女子の皆さんがファッションと技術の面白さを感じ、将来の可能性を広げるきっかけになれば幸いです。