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

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

なぜC++/JS間のメモリ管理は難しいのか 皆様、はじめまして。グローバルプロダクト開発本部 グローバルシステム部 技術推進ブロックの村木と申します。私たちのチームでは、OpenCVによる画像処理やニューラルネットワークによる画像認識を含む大規模なC++資産を、EmscriptenでWebAssemblyにコンパイルし、ブラウザへ移植するプロジェクトに取り組んでいます。 その中で私にとって難所となったのが、メモリ管理の理解でした。Emscriptenを使用する開発では、メモリの考え方が異なる2つの言語、C++とJavaScriptをまたいだ処理を実装することがあります。C++ではオブジェクトの寿命がコンストラクタとデストラクタによるRAIIで制御されるのに対し、JavaScriptではオブジェクトの寿命をGC(ガベージコレクタ)が管理し、通常、開発者が解放を意識することはありません。 このメモリモデルの違いに加え、C++のオブジェクトをJavaScript側へ渡すとき、その寿命や所有権に関する情報は引き継がれないことを、しっかりと理解しておく必要があります。 JavaScript側から見ると、C++のオブジェクトは単なる数値(メモリ上のアドレス)に過ぎず、この「数値」には、「誰が確保したのか」「いつまで有効なのか」「誰が解放すべきか」といった所有権の情報は含まれていません。また、JSのGCはC++側のメモリ領域を認識できません。 そのため、Wasmの境界を越えるオブジェクトについては、所有者と有効期間、解放の責任を明確にしたうえで実装する必要があります。この点を理解しておくことが、Emscriptenにおける開発の重要なポイントでした。 Emscriptenが提供するembindを使えば、C++のクラスや std::vector をJavaScriptのオブジェクトに近い形で扱えます。ただし、その抽象化されたAPIの背後で、オブジェクトの受け渡しと寿命管理がどのように行われているのかを理解しておく必要があります。 本記事では、まずWasmのメモリモデルの基礎を概観し、その上でembindによる実装を詳しく解説します。 目次 なぜC++/JS間のメモリ管理は難しいのか 目次 Wasmのメモリモデル C++オブジェクトの実体 境界を越えると寿命の情報が失われる embindは何を隠し、何を隠さないのか value_objectはコピー class_はハンドル typed_memory_viewはビュー おわりに Wasmのメモリモデル WebAssembly(Wasm)のメモリモデルは非常にシンプルです。Wasmモジュールは、 Linear Memory(線形メモリ) と呼ばれる、単一の連続したバイト列を自身のメモリ空間として持ちます。この線形メモリは、JavaScript側からは WebAssembly.Memory オブジェクトの buffer として見えます(型は ArrayBuffer )。 C++コードがWasmにコンパイルされると、グローバル変数やスタック領域、そして malloc / new によって確保されるヒープ領域は、すべてこの線形メモリの中に配置されます。したがって、C++オブジェクトの「実体」とは、この線形メモリ上の特定のアドレスから始まる領域にほかなりません。JavaScriptの視点で言い換えれば、C++のオブジェクトは、巨大な ArrayBuffer (= WebAssembly.Memory が内部に保持する一本の配列)の中に、バイト列として格納されているのです。 まずはこのことを、簡単なコードで確認していきます。 C++側で「指定されたアドレスから始まるバイト列の合計値を計算する」関数を実装します。 // C++側:ptrは線形メモリ内のオフセット(アドレス) extern "C" int sum_bytes ( const uint8_t * ptr, int len) { int total = 0 ; for ( int i = 0 ; i < len; i++) total += ptr[i]; return total; } JavaScript側では、Emscriptenが提供する _malloc で線形メモリ上の領域を確保し、 HEAPU8 を通じて値を書き込みます。このとき、関数に渡す ptr は単なる数値(アドレス)です。 const bytes = new Uint8Array ([ 10 , 20 , 30 ]) ; // 1. 線形メモリ上に3バイト確保 const ptr = Module . _malloc ( bytes . length ) ; // 2. JSから線形メモリへ書き込む Module . HEAPU8 . set ( bytes , ptr ) ; // 3. C++側で同じメモリ領域を読み取って合計を計算 const total = Module . _sum_bytes ( ptr , bytes . length ) ; // 4. メモリを解放 Module . _free ( ptr ) ; console . log ( total ) ; // 60 ptr はC++から見れば「ポインタ」ですが、JSから見れば HEAPU8 の「添字(number)」に過ぎません。 HEAPU8[ptr] と記述することで、C++側がポインタを通じてアクセスするのと、全く同じメモリ領域をJS側でも参照できます。 C++オブジェクトの実体 EmscriptenでコンパイルされたC++コードにおいて、 new や std::vector などによる動的なメモリ確保は、すべてWasmの線形メモリ内(のヒープ領域)に対して行われます。JS側から呼び出す _malloc も、同じヒープ領域からメモリを切り出します。 C++オブジェクトの「実体」とは、linear memory上の特定のアドレスから始まる、数バイトから数メガバイトの領域のことです。例えば、以下のクラスを考えます。 class ByteBuffer { public : explicit ByteBuffer ( size_t n) : data_ (n, 0 ) {} size_t size () const { return data_. size (); } private : std :: vector < uint8_t > data_; // 中身はヒープ上の別領域を指す }; new ByteBuffer(1024) を実行すると、まず ByteBuffer 本体(vectorのポインタやサイズ情報など)がヒープに確保され、その内部のvectorがさらに1024バイトをヒープに確保します。これらはいずれもlinear memory内に存在します。 C++の中で完結している限り、この寿命管理は言語仕様によって保証されています。スコープを抜ければスタック上のオブジェクトは破棄され、 std::vector が確保したメモリも自動的に解放されます。 delete を呼び出せばデストラクタが連鎖し、メモリは安全に回収されます。つまり、 RAII(Resource Acquisition Is Initialization)とデストラクタの仕組みが、メモリの確保と解放の対応を担保している のです。 しかし、この保証が有効なのは「C++のライフタイム管理が届く範囲にある」間だけです。オブジェクトへの参照がJS側に渡ると、この前提は崩れます。 境界を越えると寿命の情報が失われる JavaScriptでは、どこからも参照されなくなったオブジェクトはGC(ガベージコレクション)によって自動的に回収されます。しかし、GCが寿命を管理できるのは「JSヒープ上にあるJSの値」だけです。C++オブジェクトの実体は、前述のとおり線形メモリ(巨大な ArrayBuffer )の一領域にすぎず、GCから見れば「生存している1つの大きな箱」の内側でしかありません。その箱の内部のどこが有効で、どこが解放済みなのかを、GCは関知しないのです。 そのため、C++オブジェクトのアドレスを保持していたJS変数が使われなくなり、GCに回収されたとしても、線形メモリ上の実体は確保されたまま残り続けます。C++のデストラクタは呼ばれず、メモリはリークします。 逆のケースも同様で、C++側で free を呼んでメモリを解放しても、JS変数が握っているアドレス(ただの数値)はそのまま残ります。すでに無効になった領域を指し続けるその数値を使えば、解放済みメモリを読み書きする「Use-after-free」を引き起こします。 いずれもC++/JS間の境界を越えた際に、「このアドレスはByteBufferを指し、所有権はこちらにある」という型と所有権のメタ情報が単なる数値に変換されて失われます。その結果、「誰がいつ解放するのか」という所有権の所在が型システムからもランタイムからも見えなくなり、問題が発生します。 embindは何を隠し、何を隠さないのか これまで、WebAssemblyとJavaScript間でデータをやり取りするために、以下の低レイヤーな手順が必要であることを確認しました。 _malloc でメモリを確保する HEAPU8.set 等を介して手動でデータをコピーする 確保したメモリのアドレスを数値として管理する embindは、こうした低レイヤーの「配管」処理を宣言的な記述に置き換え、JavaScriptとC++の間のバインディング実装を省力化可能な、Emscriptenの強力な機能です。 embindを有効にするには、ビルド時に -lembind オプションを付与します。利用にあたっては、C++側で EMSCRIPTEN_BINDINGS ブロックを記述し、公開したい関数やクラスを明示します。 #include <emscripten/bind.h> using namespace emscripten; // 構造体:単純な値のセット struct Point { float x; float y; }; Point midpoint ( const Point& a, const Point& b) { return Point{(a.x + b.x) / 2 , (a.y + b.y) / 2 }; } // クラス:状態を持つオブジェクト class ByteBuffer { public : explicit ByteBuffer ( size_t n) : data_ (n, 0 ) {} size_t size () const { return data_. size (); } void fill ( uint8_t v) { std :: fill (data_. begin (), data_. end (), v); } private : std :: vector < uint8_t > data_; }; EMSCRIPTEN_BINDINGS (demo) { // 構造体をJSのプレーンなオブジェクトとしてマッピング value_object<Point>( "Point" ) . field ( "x" , &Point::x) . field ( "y" , &Point::y); function ( "midpoint" , &midpoint); // クラスをJSのクラスとしてマッピング class_<ByteBuffer>( "ByteBuffer" ) .constructor< size_t >() . function ( "size" , &ByteBuffer::size) . function ( "fill" , &ByteBuffer::fill); // 型登録(JSから操作可能にする) register_vector< float >( "VectorFloat" ); } この定義により、JavaScript側からは、あたかもネイティブなオブジェクトを扱うかのように、C++側の機能を利用できます。 // JS側からの利用例 const m = Module . midpoint ({ x : 0 , y : 0 } , { x : 10 , y : 4 }) ; console . log ( m . x , m . y ) ; // 5 2 const buf = new Module . ByteBuffer ( 1024 ) ; buf . fill ( 0xff ) ; console . log ( buf . size ()) ; // 1024 embindを利用することで、 _malloc による手動メモリ確保や HEAPU8 の直接操作といった低レベルな記述は隠蔽され、コードも簡潔になります。 しかし私は、前節で述べた低レイヤーにおける処理の理解は重要だと考えています。私自身について言えば、前節で見た線形メモリの挙動を一度自分の手で確かめることで、ようやくembindの宣言的な記法を使いこなせるようになったからです。また、いつハンドルを delete() すべきか、なぜ確保したはずのビューが壊れることがあるのかといった疑問にも、筋道を立てて考えられるようになりました。 以降では、embindを使ってJS/C++間でデータを受け渡す手法を見ていきます。この観点で整理すると、本記事が扱う代表的なパターンは、 コピー 、 ハンドル 、 ゼロコピーのビュー の3つに分けられます。 value_object はコピー value_object<Point> として公開した構造体は、JS/C++の境界を越えるたびに、JSのプレーンなオブジェクトへ詰め替えられます。逆方向も同じです。そのため、返ってきた m はC++側の実体への参照ではなく、独立した複製です。たとえば m.x = 99 と書き換えてもC++側には影響しません。使い終わったあとは、通常のJSオブジェクトと同じようにGCによって回収されます。 つまり、 value_object は、所有権を意識せずに扱える一方で、JS/C++の境界を越えるたびにコピーが発生する仕組みです。 この仕組みは、座標や計測値のような小さな値のまとまりを扱うのに向いています。一方で、大きな配列を内包する構造体を毎フレーム往復させるような用途には向きません。 また、 value_object は純粋な値の束であることが前提です。フィールドに生ポインタや所有権を持つリソースを含める設計は避けるべきです。コピーされた先で、そのポインタが何を指しているのか、誰が解放するのかという情報が失われ、単なるアドレス値だけが残ってしまうからです。 class_ はハンドル new Module.ByteBuffer(1024) を実行すると、C++のヒープ、すなわちlinear memory内に ByteBuffer の実体が new されます。JS側に返ってくるのは、その実体を指すハンドルを内蔵した薄いラッパーオブジェクトです。 第1部で見たとおり、C++オブジェクトの実体はlinear memory上にあります。JSの変数が保持しているのは、その実体そのものではなく、実体を指すハンドルです。 buf.fill(0xff) を呼ぶたびに、呼び出しはJSからWasm上の同じC++実体に届きます。ここではコピーは起きません。 その代わり、 実体の解放はJS側が引き受け、明示的に delete() を実行する 必要があります。 register_vector で公開した std::vector も、基本的には class_ と同じ系統です。返ってくるのは素のJS配列ではなく、 .size() 、 .get(i) 、 .set(i, x) 、 .push_back(x) などを持つハンドルです。 この仕組みのため、要素アクセスのたびにJSからC++側への呼び出しが発生します。よって、巨大な配列を .get(i) のループで一要素ずつ走査すると、JS/C++間呼び出しのコストがその回数分だけ積み上がります。また、ハンドル自体には明示的な破棄が必要です。 要素そのものはコピーによって安全にやり取りされますが、ハンドルの後始末はJS側が担う必要があります。次に見る typed_memory_view と対比すると、 register_vector は「中身の扱いは安全寄りだが、破棄の責務は残る配列」と位置づけられます。 typed_memory_view はビュー typed_memory_view が提供するのは、ゼロコピーのアクセスです。C++側の連続領域を一切複製せず、JSの TypedArray としてそのまま覗かせます。たとえば、カメラ画像のようなRGBAバッファを返す例を考えてみます。 #include <emscripten/val.h> class FrameBuffer { public : FrameBuffer ( int w, int h) : pixels_ (w * h * 4 , 0 ) {} // 内部バッファを指すビューを返す。コピーは発生しない emscripten::val pixels () { return emscripten:: val ( emscripten:: typed_memory_view (pixels_. size (), pixels_. data ())); } private : std :: vector < uint8_t > pixels_; // RGBA }; const frame = new Module . FrameBuffer ( 640 , 480 ) ; const view: Uint8Array = frame . pixels () ; // linear memoryを直接指す窓 view [ 0 ] = 255 ; // C++側のpixels_[0]が直接書き換わる この view の正体は、第1部で見た HEAPU8 の部分ビューです。linear memory上の pixels_.data() から始まる領域を、JSの Uint8Array として直接見ているだけです。コピーが発生しないため、メガバイト級のバッファでもほぼ一瞬でJS側へ「渡す」ことができます。 ただし、ビューはあくまで「ビュー」なため、その有効期限は、元のバッファの寿命に完全に支配されます。具体的には、次のいずれかが起きた時点でビューは無効になります。 元のオブジェクト、つまり frame が破棄された → viewは解放済み領域を指すことになる 内部の std::vector が再確保された → viewは古いアドレスを指したままになる 線形メモリが成長した 第1部で予告したとおり、 ArrayBuffer が差し替わり、古いビューはdetachされる。成長は別の場所の malloc をきっかけに起こることもあるため、自分のコードがそのバッファに何もしていなくても、ビューが無効になる可能性がある。 したがって typed_memory_view の鉄則は「 取得したら即座に読み書きし、持ち越さない 」です。保持して使用し続けたいデータは、 view.slice() などでその場でJS側へコピーし、切り離しておく必要があります。 3つの渡し方をまとめると、次のように整理できます。 コピー( value_object )は、所有権を考えなくてよい代わりにコピーコストを払う。 ハンドル( class_ / register_vector )は、コピーを避けられる代わりに、JS側が解放の責務を負う。 ビュー( typed_memory_view )は高速だが、元バッファの寿命と完全に結びつく。 おわりに 本記事では、まずWasmの線形メモリという土台を確認し、その上でembindが提供する3つのデータの渡し方を見てきました。 embindの宣言的なAPIは、 _malloc や HEAPU8 といった低レイヤーを隠蔽してくれます。しかし所有権と寿命の責務は、プログラマが十分に認識し、管理する必要があります。本記事では、低レイヤーの挙動から順を追って解説することで、embindを実装に採用した場合も、そのAPIの背後で所有権と寿命がどう扱われているかを見通せるようになることを目指しました。 同じようにEmscriptenと向き合う方にとって、本記事がメモリ管理を筋道立てて考えるための一助となれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ZOZOMO部OMOブロックの宮澤・多田・東谷です。私たちは2026年7月22日(水)・23日(木)の2日間、JPタワーホール&カンファレンスにて開催された「AI DevEx Conference 2026」に参加してきました。本記事では、会場や各ブースの様子に加え、特に印象に残ったセッションをご紹介します。 目次 はじめに 目次 AI DevEx Conference 2026とは 会場の様子 セッション紹介 Day1(7/22) AIと共存する世界のソフトウェアエンジニアリング─不変の本質とプログラミングの未来 AI時代のプロダクト開発変革をどう進めるか Day2(7/23) なぜ、AIが進化しても期待される生産性を上げることができないのか 『動くだけ』のその先へ ― AI駆動開発で品質と速度を両取りする温故知新な新手法 2026年のソフトウェアエンジニアリングを考える さいごに AI DevEx Conference 2026とは AI DevEx Conference 2026 は、ファインディ株式会社が主催する、AI時代の開発生産性と開発者体験(DevEx)をテーマにしたカンファレンスです。前身の「開発生産性Conference」から名称を改め、MetaやGitHub、Uber、LinkedInなど海外テック企業を含む50社以上から56名を超える方々が登壇しました。 会場の様子 会場のJPタワーホール&カンファレンスには、Room A〜Dの4つのセッションルームがありました。セッション以外の企画も充実しており、スポンサーブースエリアやポスター展示「AI DevEx Gallery」が設けられていました。さらに、スポンサーブースを回ってスタンプを集めると豪華賞品が当たる「巨大ガチャ」も用意されていました。国内外50社以上が登壇する規模だけあって、2日間を通して4トラックが同時進行し、会場内を回るだけでも熱気が伝わってきました。 JPタワーホール&カンファレンスの外観 会場入口の「AI DevEx Conference 2026」サイネージ AI DevEx Gallery(ポスターセッション) セッションの合間には、「開発生産性の先・DevEx」に関する各社の取り組みをポスター形式で展示する「AI DevEx Gallery」を自由に見て回ることができました。 KINTOテクノロジーズ 粟田啓介さんのポスター「AI時代に必要な『組織の成果』を作る人材育成」 このほか、Findy Team+によるサイクルタイム短縮事例(360時間→15.7時間)や、楽々明細 植木遼太さんによる「開発チームへの『無駄な依頼』が消えた話」なども展示されていました。各社の現場ならではの実践知が並び、立ち寄るだけでも学びの多いコーナーでした。 スポンサーブース スポンサーブースエリアの様子。各社のブースが並び、多くの参加者が足を止めていました 朝日新聞社は「伝統的新聞社は、AIエージェントで再定義できるのか」というポスター展示を行っていました。創業150周年の100日プロジェクトでCursorのハンズオンに224名が参加し、技術系エージェントの体験率93%、DORA Four Keysのデプロイ頻度+667%という実績が添えられていました。こうした事業会社側のAI活用実績が数多く紹介されていました。スタートアップだけでなく伝統的な大企業でも、AI活用が着実に進んでいることを実感しました。 朝日新聞社の事例ポスター「伝統的新聞社は、AIエージェントで再定義できるのか」 Postman株式会社の「APIテストは何でやっていますか?」「API仕様・ドキュメントはどこに格納していますか?」というシール投票企画も目を引きました。手動やcurl、Postmanでの管理など、テストへの向き合い方は企業によってさまざまでした。 Postman株式会社ブースの様子。「APIテストは何でやっていますか?」等のシール投票企画 また、株式会社カオナビのブースでは「このAIポンコツすぎるな…一体どんな挙動?」というお題で、参加者が実際に困ったAIの挙動を自由に書き込んでいく付箋ボードもありました。付箋には「同じ失敗を繰り返す」「コンテキストが長くなると指示を無視し始める」「気づいたら英語で話している」といった挙動が並んでいました。 「このAIポンコツすぎるな」付箋ボードに寄せられた参加者たちの実体験 真剣な講演の合間にこうした企画があることで、AIとの向き合い方について肩の力を抜いて話せる空気が、会場全体に流れていたのが印象的でした。 セッション紹介 ここからは、特に印象に残ったセッションを紹介します。 Day1(7/22) AIと共存する世界のソフトウェアエンジニアリング─不変の本質とプログラミングの未来 基調講演「AIと共存する世界のソフトウェアエンジニアリング─不変の本質とプログラミングの未来」の登壇の様子 宮澤です。Dave Farleyさんの基調講演「AIと共存する世界のソフトウェアエンジニアリング─不変の本質とプログラミングの未来」を紹介します。 制約理論を引きながら「コードはボトルネックではない(もしそうだったとしても)」と提起するスライド まず、セッションの冒頭で提起されたのが「コーディングはもうボトルネックではない」ということでした。生成AIにより、人間が読み、理解し、検証できる速度をはるかに超えてコードを生成できるようになりました。コードの作成はボトルネックではなく、生成されたものが適切かどうかを素早く検証することが次の最適化ポイントになっているということです。 この前提は、現場のソフトウェア開発に携わる私自身も感じていたものでした。 続いて、セッションの本題に入っていきます。Farleyさんは、プログラミング言語には3つのゴールがあると述べています。 人間が問題についての思考を整理する その理解を他の人と共有する 実行可能な指示としてコンピュータに伝える そして従来のプログラミング言語は、これらのゴールを達成するために3つの技術を採用してきたとしています。 単純で決定論的な形式文法(同じ命令に対して同じ結果が得られる) 意図の曖昧さのない表現 反復可能で決定論的な実行 しかし、AIを活用するようになったことで、これらの技術が機能しなくなり、以下3つの問題が新たに生じたとしています。 意図を正確に指定すること 得られた結果が意図通りかを検証すること 変化し続ける世界に適応するためのインクリメンタリズムを維持すること 例えば、AIに「セキュアなログインページを作れ」とバイブコーディングで指示するだけでは不十分で、具体的にどのような要件を求めているかを開発者が理解して明示する必要があります(1つ目の問題)。さらに、AIが大量にコードを生成できる時代において、すべてのコードを人間が確認するのは不可能です。生成されたコードが意図した要求を満たしているか検証する技術が必要になります(2つ目の問題)。 従来のプログラミングでは、達成しようとしていた実際の成果はコードの中に明示的に入っておらず、開発者の頭の中に暗黙的にあるものでした。これからのAI時代では逆になるとFarleyさんは述べていました。求めている成果は明示的に述べられて検証可能である必要があり、解決策の方が暗黙的になります。プログラミングは解決策を記述することではなく、より精密に成果を定義することになるという主張でした。 なお、Farleyさんはこれらの問題への対処がエンジニアリング思考に依存するとしていました。技術的規律の素養や、問題解決へのシステム思考的なアプローチが引き続き求められます。問題を分解し、要求を検証する能力が必要になるということです。 ここまでの話で、今後必要になるのは「求める成果を、明示的で検証可能な形で定義する」ということでした。その具体的な対応策としてFarleyさんが提唱した方法はBDD(振る舞い駆動開発)でした。達成したい成果を明示的にテスト可能な仕様として指定してAIにこの仕様を満たすコードを生成させるということです。 Farleyさんはこれを「第5世代プログラミング言語」と位置づけていました。パンチカード(第1世代)、アセンブリ言語(第2世代)、高水準言語(第3世代)、4GL(第4世代)に続いて、BDDによる検証可能な仕様が新しい世代の「プログラム」になるとのことです。 このBDDで用いるDSLには、解決策の言語(プログラミング言語)ではなく、問題領域を表す自然言語を使用します。そのため、開発者以外も仕様を読んで理解できるという利点があります。 しかし、コンピュータに指示するには、ユーザー価値以外の観点も必要になります。例えば、システムの応答速度や耐久性、セキュリティなどのアーキテクチャの観点です。Farleyさんは、これらも結局はシステムの振る舞いにすぎないと主張していました。「応答は一定時間内に返る」「送金の過程でお金が消滅しない」のような、非機能要件も同じく仕様として記述できるということです。 そのうえで、アーキテクチャ自体は「進化的アーキテクチャ」のアプローチで扱うことが重要とのことでした。これは、アーキテクチャを前もって固定するのではなく育てていくものとして扱い、決定を「責任を持てる最後の瞬間」まで遅らせ、設計を変更可能な状態に保つアプローチです。この「進化的アーキテクチャ」という用語は本カンファレンスの別セッションでもたびたび耳にするものでした。 この話で印象的だったのが、注意義務(duty of care)という言葉です。ユーザーが求めるものをそのまま叶えるだけでは不十分です。どれだけセキュリティを担保するべきか、どれだけ耐障害性や応答速度を満たすべきかという専門的な判断を持ち込む必要があります。そのうえでAIに対して詳細に指示することが、プロのエンジニアの責務だということです。 セッションの終盤では、冒頭の主張に立ち戻り「コーディングはもはやボトルネックではない。そしておそらく、最初からボトルネックだったことはなかった」と述べていました。これは、コーディングという工程が消えたのではなく、もともと優れたチームと平均的なチームを分けていたのはコーディングの速さではなかったことが、AIによってあらわになったということでした。 大切なのは、問題を深く理解し、ビジネスの文脈とゴールを把握することです。そして、プロダクトの方向性を考え、アーキテクチャの文脈を保つことです。「問題を深く理解し、携わっているビジネスのゴールを本当に理解しない限り、優れたソフトウェアは作れない」。これはAIの有無によらないプログラミングの本質だということでした。 また、「AIは増幅器(amplifier)である」という言葉も強調されていました(この言葉も本カンファレンスの複数のセッションで耳にしました)。AIは高パフォーマンス組織の強みを拡大し、苦しんでいる組織の機能不全も拡大します。AIが到来する前に機能していたやり方は、AI到来後もよりよく機能するということでした。 BDD、継続的デリバリー、進化的アーキテクチャを採用することで、AIの恩恵をより大きく受けられます。Farleyさんはこれを「Back to the future」と表現してセッションを結びました。古くからの実践に立ち返ることが未来への道になるという、温故知新にも通じるメッセージでした。 AI時代のプロダクト開発変革をどう進めるか 多田です。Notion Labs Inc. CTOのFuzzy Khosrowshahiさんの「AI時代のプロダクト開発変革をどう進めるか」を紹介します。 このセッションを聞いて、AIの登場によって開発の焦点が「形にすること」から「何を作るべきか」へと移りつつあるのだと改めて感じました。開発者の業務時間の約47%はコミュニケーションと調整に費やされ、1回のリリースには平均6つ以上のツールが関わっているといいます。この「意思決定の負荷」と「調整コスト」こそが、AIトランスフォーメーションを阻む「組織」「ワークフロー」「システム」という3つの壁の正体だと位置づけられていました。 3つの本質的な問題として挙げられた「個人への依存」「情報の分散」「スケーラビリティ」 エージェントを会社全体に拡張する段階で必ずぶつかるのが、「個人への依存」「情報の分散」「エンジニアリングとスケーラビリティ」という3つの問題だといいます。Notionはこれを理論としてではなく、自分たちが日々どう組織し動いているかという実践の中で解いてきた、というのが本セッションの軸でした。 多くの組織はいまだに年次計画や階層的な承認プロセスで動いています 1つ目、「組織」の壁の答えが JazzMode(ジャズモード) です。Fuzzy Khosrowshahiさんは、あらかじめ計画を立てて演奏するオーケストラと、共通のテーマだけを決めて即興で音を重ねるジャズバンドを対比しました。年次計画や階層的な承認を経てからでないと動けない組織は、オーケストラのように計画通りにしか動けない状態だといいます。JazzModeとは、そこから抜け出し、メンバー一人ひとりに裁量を与えながら信頼をベースに素早く学び動いていく運用リズムのことです。一部の得意な人だけにAIの価値を閉じ込めず、組織全体へ広げる必要があります。そこで、まず動き方そのものを、こうした信頼と裁量にもとづく形へ変えていくべきだという主張でした。AI活用が一部のメンバーに偏りがちな自分たちにとって、身につまされる思いでした。 2つ目、「ワークフロー」の壁の答えが Agent OS です。ツール・コンテキスト・ワークフローが断片化したままでは、AIはその溝を埋めるのではなく同じ断片化を引き継ぐだけだ、という指摘には心当たりがありました。実際、自分たちもSlackの会話とGitHubの記録を別々に探し回ることが多く、AIに聞いてもツールをまたいだ断片的な答えしか返ってこないことがあります。Data Scout、Office Q&Aなどの業務ロールに紐づくエージェントをカタログ化しているそうです。そのうえで「ナレッジを取り込む→答えを見つける→ワークフローを自動化する」流れを1つのAIオペレーションレイヤーとして提供している、と説明されていました。個々のエージェントを点在させず、1つのオペレーションレイヤーとしてつなぎ直すという発想は、ぜひ弊社でも参考にしたいと思いました。 エージェントが、チームと共に動くネイティブ環境を ここで印象に残ったのが、 AIのアクセスの重要性 です。エージェントは支援する相手と同じ「現場レベルの権限」の範囲内で動作します。このアクセス権限管理の正確さがAgent OSの必須要件だと言っていました。権限を与えるのは怖い一方、与えなければ結局人が動かなければならず時間がかかってしまいます。このバランスをどう取るか、自分たちも試行錯誤しているところです。 3つ目、「システム」の壁の答えが Software Factory です。創業者の言葉を引用し、「従来のソフトウェアを作ることは橋を作るようなもの。正しく設計すれば計画通りに作れる。しかしAIでプロダクトを作ることは日本酒の醸造に近い」と説明されていました。橋は設計図さえ正しければ計画通りに完成させられる仕事だが、酒造りは発酵の進み具合を見ながら条件を調整し続ける仕事だ、という説明でした。AIによるプロダクト開発も同じで、最初から完璧な設計図を引くのではなく、条件を整えては様子を見て調整を重ねる進め方に近づいている、ということのようです。醸造そのものも近代化されてきたように、個人の職人技に頼るのではなく、この「調整し続けるプロセス」自体を組織として仕組み化する必要があるという結論を、興味深く感じました。 組織・ワークフロー・システムの3本柱 アメリカの有名テック企業でも、また日本の一部の企業でも、すでにこうした基盤の上でAI時代の運用を築いているといいます。 最終メッセージは「ソフトウェアの次の章は、判断力とコンテキスト、そして新しい形の裁量を組み合わせられる人たちによってつくられる」というものでした。 組織はJazzModeというリズムで、ワークフローはAgent OSという1つのレイヤーで、システムはSoftware Factoryという再現可能な仕組みで支えるという整理でした。この3本柱は、複数チームでAI活用が進む弊社の状況にもそのまま当てはまる示唆だと強く感じています。ぜひ社内でも参考にし、今後のエージェント活用に活かしていきたいと思いました。 Day2(7/23) なぜ、AIが進化しても期待される生産性を上げることができないのか 多田です。2日目の基調講演「なぜ、AIが進化しても期待される生産性を上げることができないのか」を紹介します。登壇者はGitHub, Inc. Business Insights & Research AdvisorのEirini Kalliamvakouさんです。個人レベルではAIが劇的な生産性向上をもたらしている一方、組織全体ではその恩恵が測定できていない、という矛盾がテーマの講演でした。 講演はまず、一見矛盾する「2つの真実」の提示から始まりました。開発者個人のレベルでは、ポジティブな結果が出ていました。GitHub Copilot利用で55%の高速化、開発者の90%以上が毎日AIを使用(DORA 2025)、80%以上が生産性向上を実感といった数字です。一方、組織のレベルでは対照的な結果が示されていました。MIT NANDAレポートでは生成AIパイロットの95%がROIなしで、投資額300〜400億ドルに対し測定可能な成果は限定的でした。400社以上を対象にした別の縦断調査では、AI導入率が65%増えてもスループット向上はわずか8%で、デリバリーの安定性がむしろ低下しているケースもあったそうです。どちらの立場も嘘をついているわけではなく、AIから価値を生むプロセスは個人の加速から組織の成果へ単純には移行しない、というのが講演の出発点でした。 この矛盾を解くカギとして提示されたのが、「システム」「測定」「判断」という3つの複雑さの層です。まず「システム」の層では、ソフトウェア開発が「リレー競技」に例えられていました。一人が55%速く走れても、バトンパスが遅ければチーム全体のタイムは縮まりません。ここでのバトンパスは、レビューやビルド、デプロイ、要件定義といった工程です。開発者の時間のうち、コードを書くことに使われているのはわずか約14%だといいます。 WHAT AMPLIFIES AI – EVERY FACTOR IS TEAM-LEVEL 個人の成果を組織的な成果に変えるには、次の7つの要因が必要になるといいます。いずれも個人の作業環境だけでは完結せず、チームや組織全体で整備すべき条件です。 明確に共有されたAIへのスタンス 健全なデータエコシステム AIがアクセスできる社内データ 強固なバージョン管理の実践 小さいバッチでの作業 ユーザー中心の視点 質の高い社内プラットフォーム 実際、自分たちのチームでもAIによってコーディング自体は明らかに速くなりました。しかし、その分PRの数が増えてしまい、レビュアーの手も回らなくなってきました。しかもAIが書いたコードは一見良さそうに見えるため、かえって読み解くのに時間がかかります。この「バトンの受け渡しが重くなる」現象は、レビューという工程だけでなく、要件のすり合わせのようなより大きな単位でも起きると指摘されていました。講演を聞いて、自分たちの状況にもそのまま当てはまることに気づかされました。 次の「測定」の層では、従来の指標がAI時代のワークフローでは意味を変えてしまっていると指摘されていました。例えばマージ率の低下は「品質の悪化」ではなく、AIで複数の代替案を安価に試す探索的な行動が増えた結果かもしれません。逆にマージ率の上昇も「品質の向上」とは限らず、レビュー不足のまま通してしまっているだけの可能性があります。今後見るべき新しいシグナルとして、3点が挙げられていました。AI活用がワークフロー全体に組み込まれているか、品質を保ったままエージェント起因の成果物の割合が増えているか、エージェントに与えるコンテキストが意図的に整備されているかです。 最後の「判断」の層では、Margaret-Anne Storeyさんの「3つの負債」フレームワーク(技術的負債・認知的負債・意図負債)が引用されていました。そして、スピードの向上が新たな負債を生む構造が説明されました。 組織の基盤とエージェントへの委任度合いによる4象限。右下が「危険な領域」として示されていました 「組織の基盤(FOUNDATIONS)」と「エージェントへの委任度合い(AGENT DELEGATION)」の2軸でマトリックスを作った説明も印象的でした。基盤が強く委任度合いの低い組織は、安全に委任を広げていける「Headroom」の状態にあります。基盤が弱く委任度合いも低い組織は「Slow but safe」で、まず基盤を直すべき状態にとどまります。一方、基盤が弱いまま委任だけを増やす組織は、問題の検知が追いつかなくなる「Danger」に陥りやすいそうです。基盤とセットで委任を進める組織だけが、出力が回を重ねるごとに良くなる「Healthy growth」の複利効果を得られる、という整理でした。 Kalliamvakouさんは最後に、「個人の加速は間違いなく現実であり、素晴らしい出発点です。しかし、真の価値と競争優位性は、システムをどう構築するかにかかっています」と締めくくりました。 「AIは増幅器であり鏡である」という結論は、このカンファレンスを通して一番印象に残ったメッセージだったように思います。個人の生産性指標だけを追うのではなく、組織の「基盤」に投資する必要があるという指摘を、ぜひ社内でも共有し、今後のチーム運営に活かしていきます。 『動くだけ』のその先へ ― AI駆動開発で品質と速度を両取りする温故知新な新手法 speakerdeck.com 宮澤です。株式会社ログラス 執行役員CTOの伊藤博志さんによるセッション「『動くだけ』のその先へ ― AI駆動開発で品質と速度を両取りする温故知新な新手法」を紹介します。 セッションは、2026年2月の3連休にClaude CodeでOLAPデータベースを試作した話から始まりました。OLAPデータベースは集計や分析に特化したデータベースです(本筋ではないので詳細は割愛します)。この試作したデータベースをPolarsやDuckDBといった既存の高速なエンジンとベンチマーク比較したところ、一定の項目で既存エンジンを上回る性能が出たそうです。さらに、集計軸の組み合わせのほとんどに値が入らないスパースデータというユースケースでは、桁違いに速かったとのことでした。ただし、本人も「ユースケースは限定的」と断っており、既存エンジンに全面的に勝った話ではありません。それでも、週末の実験でここまで動くものができてしまう。この事実がセッションの出発点でした。 この開発プロセスは、ビジョンからロードマップを作り、ADR(Architecture Decision Record)で設計を決めるところから始まります。そこから仕様を作って実装し、検証してベンチマークを回すループとして紹介されていました。 続いては、このループの品質担保の方法です。AIの出力の検証はテストで担保することになりますが、出力は確率的なため、「テストを実装したことにする」といった事象も起こり得ます。そこで、仕様とテストと実装をIDで紐付け、その対応をスクリプトで決定論的に検証するという方法が紹介されました。仕様に番号を振り、「この仕様に対応するテストが書かれている」という事実を機械的にチェックする仕組みをとったそうです。 テストの作り方にも原則がありました。テストは仕様書から導出し、実装と同じコンテキストでAIに書かせない、というものです。実装を見ながらテストを書くと、コードの動きをなぞるだけのテストになってしまうからです(こちらは本カンファレンスの基調講演をはじめ、複数のセッションで同様の言及がありました)。 さらに、複雑な仕様には形式手法を適用し、実装前の段階で仕様そのものの矛盾や曖昧さを検証しているとのことです。学習コストの高さがネックになる手法ですが、検証自体はAIが実行するため、導入のハードルは下がったそうです。 検証の道具はもう1つ、オラクルテストです。OLAPデータベースは仕様が広く知られているため、既存のデータベースに同じ入力を与えて結果を突き合わせられます。形式手法が仕様の整合性を担保し、オラクルテストが実装の出力を守る、という役割分担のようです。 ここまでが、冒頭で紹介したOLAPデータベースを試作した方法とのことでした。 続いて、この手法が実際のプロダクト開発で通用するのかという話です。対象に選ばれたのは、既存プロダクトのレポート機能です。データが大きい場合に性能が限界に来ていることを内部で認識しながらも、簡単に直せる規模ではなかったそうです。新しいエンジンを作り、フィーチャーフラグで切り替えられる形にしたそうです。このとき、既存機能がもともとの仕様を体現しているため、それがオラクルになります。既存の挙動をリバースエンジニアリングして仕様書に書き出し、週末の実験と同じループを回したとのことでした。 結果として、第1弾は8日で動くようになったそうです。ただし、品質を担保しきるまでには約2か月かかったそうです。オラクルテストをやり切る過程では、組み合わせが爆発する部分はドメインに詳しい人の目を入れて現実的な範囲に絞り込みながら、バグを出しては直す期間が必要だったとのことです。また、仕様の前提をそろえる議論だけで1か月を使ったとのことで、開発時間の半分以上が品質保証に費やされたと紹介されていました。 印象的だったのは、「仕様が間違っていたら、間違ったものが高速に作られるだけ」という指摘です。どれだけ実装が速くなっても、その仕様は本当に正しいのか、そもそも必要なのかを考える部分は残り、人間はここに一番時間を使うべきだという主張でした。 そしてこの意思決定をどう行っていくかについて、ログラスが現在試行しているのがトレーサビリティを「前」と「後ろ」に伸ばすことだそうです。開発の前にある「なぜ作るのか」「何を作るのか」の探索と、デリバリー後の成果検証までを仕様とつなぎ、当初の仮説が満たされたかを確認できるようにする構想とのことでした。その探索の場面では、AIにドキュメントを生成させるのではなく、インタビュアーとして人間の思考を助ける役割を担わせているとのことでした。 セッションの締めくくりは、タイトルの回収でした。バイブコーディングで、誰でも「動くもの」は作れるようになりました。しかし「正しく動くとは何か」を極め続けると、「そもそも価値があるものを、どうやって作るのか」という問いに行き着きます。AIによって「どのように作るか」のハードルが下がった今こそ、人間は「なぜ作るのか」「何を作るのか」に価値を置くべきだとのことでした。 ここからは私の所感です。 仕様やその検証が重要になるという感覚は、昨今のAIエージェントの進化を見る中で、現場で開発する自分自身も持っていました。このセッションを聞いて、その感覚は確信に変わりました。第1弾が動くまでは8日でも、品質の担保には2か月を要し、仕様の前提をそろえる議論だけで1か月を使っていました。この時間配分が、今後、実装よりも仕様に人間の時間が寄っていくという構図を示しているように思いました。 また、Day1のFarleyさんの基調講演が「求める成果を明示的で検証可能な形で定義する」ことを理論として示したのに対して、このセッションはそれを実プロダクトでやり切った実践例でした。異なる立場の登壇者による理論と実践が噛み合っていたことも印象的でした。 自分の業務に引きつけると、まず試せるのは仕様とテストのID突合だと考えています。スクリプトで決定論的にチェックする部分だけなら、既存のテスト資産にも後付けできそうです。その先では、複雑な仕様のレビューに形式手法を取り入れることも検討します。 2026年のソフトウェアエンジニアリングを考える speakerdeck.com 東谷です。最後に、タワーズ・クエスト株式会社の和田卓人さんによる基調講演「2026年のソフトウェアエンジニアリングを考える」を紹介します。2025年に何が起きたかの振り返りから始まり、開発プロセスの再構築、そして「認知負債」という新しい課題まで、この2日間の総括のような講演でした。 2025年の振り返りはこうです。2月にAndrej Karpathyさんが「Vibe Coding」を提唱し、コードをレビューせず自然言語と動作確認だけで開発するスタイルが広まりました。同時期にTim O'Reillyさんが「私たちの知る形のプログラミングは終わる」と発信しており、開発者の間に期待と緊張が走ったのを覚えています。一方で「AIスロップ」(見た目は整っているのに中身の怪しい生成物)がレビュアーの負担を押し上げ、乗り遅れることへの恐怖が開発者の精神を削った時期もありました。そして11月、モデルの能力がもう一段跳ね上がりました(Claude Opus 4.5が出たあたり、という説明でした)。人間が対話しながら進める「伴走」から、自律的に動くエージェントに任せる「委託」へと開発スタイルがシフトしました。「以前は2分に1回プロンプトを打っていたのが、今は1時間に数回になっていませんか」という問いかけには、大きく共感してしまいました。プランモードで議論して承認したら、40分後に戻ってくればいいという状態が増え、人間がPCの前に張り付く必要は、確かになくなりつつあります。 現在のソフトウェアエンジニアリングは過去の積み上げの上にある 委託型の何が問題かというと、人間のコントロールと状況把握が利きにくくなり、レビューが破綻することです。AIが書いた大量の、自分では書いていないコードを、従来のコードレビューという形式で見続けるのは無理があります。そこで和田さんは、コードレビューが果たしてきた役割を分解し、開発プロセス全体へ再配置することを提案していました。上流の仕様定義を厳密にしてAIの矛盾検出力を活かします。テストを先に書かせて、エージェントが「実装が通るテストを書いて自作自演する」のを防ぎ、型・静的解析・linterといった静的検査で、エージェントのコンテキスト外が壊れても気づけるようにしています。そして全PRを均一に見るのではなくリスクベースでレビューしていました。さらにレビューが担っていた教育の機能は、人間同士のペアプログラミングで意識的に取り戻していました。5つとも明日から検討できる具体的な話でした。 後半の主役は「認知負債」です。保守性の低いコードという従来の技術的負債は、モデルの進化でむしろ改善傾向にあります。代わりに深刻化しているのは、人間が理解しないままコードを生成することで生じる、メンタルモデルと実際のコードの乖離だという指摘です。かつては理解がなければコードは書けなかったので、コードを見ればその人の理解度もある程度推測できたのですが、いまは理解がなくても恐ろしい速さでコードが出てくるので、両者が切り離されてしまいました。しかも理解度を測るメトリクスはまだ存在せず、DORAのFour Keysのような生産性指標は理解を置き去りにしても上げられてしまう構造になってしまっています。見かけの生産性を上げたい組織と個人の間で「共犯関係」が成立しうる、という警告は重く受け止めました。1985年にPeter Naurさんは「プログラミングとは理論(メンタルモデル)の構築であり、それを失ったプログラムは動いていても死んでいる」と書いていました。1983年にはLisanne Bainbridgeさんが「自動化が進むほど人間が介入すべき場面は難しくなるのに、介入するスキルは日常的に使われず失われていく」と指摘していました。40年前の警告が、コードだけが先行して人間の理解がついていかないという反転した形で現実になっている、という整理も見事でした。 「では、どうするか」。和田さんの答えは「理解をゲートにする」でした。ご自身の環境では、プランモードの最後にAIから理解度クイズを出させ、答えられなければ先に進めない仕組みを作っているそうです。 実際に問題を出しているCLIの画面 複雑な実装方針はMarkdownの説明だけでは分かった気になりがちなので、図解やアニメーションのような視覚的な形式で説明させる工夫も紹介されていました。 振る舞いも理解しやすいようにアニメーションをAIに出力させた図 そして重要なのが、理解とスピードはトレードオフではないという話です。講演で紹介された研究では、AIに丸投げしたグループは当たれば最速、外れれば最も遅く理解も残らないという結果でした。一方、AIに説明を求めて質問を繰り返したグループは、スピードをほとんど落とさずに高い理解度を保っていたそうです。 理解とスピードはトレードオフではないと説明する和田卓人さん この話は、私たちのチームの次の一手にそのままつながります。問い合わせ調査のSkill化を進めた結果、調査に必要なクエリの出力も、その検証方法の提示もAIが行うようになりました。次にボトルネックになるのは、まさに人間の理解と検証です。そこで、調査結果と一緒にAIから人間へ理解度を確かめる問題を出してもらう、やったことをHTML形式の図に書き起こして出力してもらう、といった仕掛けを入れていこうと考えています。和田さんの言う「理解をゲートにする」を、コーディングだけでなく運用調査にも適用する形です。「委託が進むほど、理解は意識して守らなければ失われる」という言葉は、まさに2026年後半に向けた宿題でした。 さいごに 今回のカンファレンスで最も印象に残ったのは、立場の異なる登壇者が口をそろえて語った「AIは増幅器である」という言葉でした。AIは導入すれば誰もが等しく速くなる魔法ではなく、強い組織の強みも、苦しい組織の機能不全も、そのまま拡大します。個人がAIで速くなった今、問われているのは、仕様や検証、レビュー、組織の基盤といった「AI以前から大切だったもの」の質なのだと感じました。私たちのチームでも、AIによってコードを書く速度は確実に上がりました。次の課題は、その加速を組織の成果へつなげる基盤づくりです。今回持ち帰った学びを、日々の開発とチーム運営に活かしていきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、情報セキュリティ部の 兵藤 です。日々ZOZOの安全を守るためSOC業務に取り組んでいます。 本記事では、開発環境におけるサプライチェーン対策として、Takumi Guardを全社展開した事例について紹介します。 また、情報セキュリティ部ではその他にもZOZOを守るための取り組みを行っています。詳細については以下の「Claude CodeがSOC業務を全自動でやってくれるってさ」をご覧ください。 techblog.zozo.com 目次 はじめに 目次 背景と概要 Takumi Guardとは Takumi Guardの対応エコシステム Takumi Guardの適用範囲 Takumi Guardの全社展開 コンテナでのTakumi Guard利用 MDMでのTakumi Guard配布 シークレットの問題 Takumi Guardの爆速アップデート対応 許可の設定をEntra側に寄せる シークレットの閲覧 デバイススクリプトの更新権限 Push Ruleset 展開後のログ画面 おわりに 背景と概要 昨今ではサプライチェーン攻撃の脅威が増しており、2026年3月には開発環境でよく使われる axios などが侵害されました。 このような事例から開発環境におけるセキュリティ対策の重要性が高まっています。特に、開発者が使用するツールやパッケージの安全性を確保することは、プロダクトセキュリティに直結します。 そこで、私たちは開発環境におけるサプライチェーン対策として、Takumi GuardをMDM経由で全社展開しました。 Takumi Guardとは Takumi Guard は、GMO Flatt Securityが提供する、悪意のあるパッケージの検知・ブロックを行うツールです。個人利用であれば無償ですが、組織として全端末に展開したり、ログを収集したりする場合は有償の契約が必要です。 詳しくは公式ドキュメント 1 をご覧ください。 Takumi Guardの対応エコシステム Takumi Guardはエンジニアが利用する開発言語における悪意のあるパッケージを検知・ブロックできます。2026年7月27日時点では、以下のエコシステムに対応しています。 npm PyPI RubyGems Go Packagist 2026年4月当初はまだ npm や PyPI までしか対応していませんでしたが、現在では RubyGems や Go 、 Packagist にも対応しており、開発速度が凄まじいです。 Takumi Guardの適用範囲 Takumi Guardの適用範囲は、大きく開発端末とCI/CD環境の2つに分けられます。 弊社ではGitHub ActionsをCI/CD環境として利用しています。このCI/CD環境にもTakumi Guardを導入でき、 公式ドキュメント に簡易手順が記載されているため導入は容易です。手順に記載されているBot IDを利用すると、GitHub Organizationに紐づくリポジトリへTakumi Guardを導入し、ログを後から確認できます。 BotはTakumi Guardのポータルから以下の項目で追加できます。 Botの追加 作成されたBotは「設定」の項目から確認可能です。 Botの確認 Takumi Guardの全社展開 Takumi Guardを全社展開するにあたり、弊社では以下の項目に対応しました。 開発ガイドラインへのTakumi Guardの記載 GitHub Actions環境へのTakumi Guardの導入 全社端末へのMDM経由でのTakumi Guardの導入 これらの対応の中で工夫した点について紹介します。 コンテナでのTakumi Guard利用 GitHub Actions上でコンテナを利用する場合、コンテナ内でのレジストリプロキシについては flatt-security/setup-takumi-guard-npm@v1 などのActionsだけでは対応できません。 これらのActionsの outputs で registry-url や token を取得し、コンテナ内部に渡す必要があります。 この点には注意が必要です。ビルド時に渡す場合、 --mount=type=secret を利用してコンテナ内部に token を渡します。そして、 npm install などを実行する際には同じ RUN 内で完結させる必要があります。別の RUN で実行すると、イメージのレイヤーに token が残ってしまうため、セキュリティ上の懸念があります。 DockerfileやGitHub Actionsのワークフローについては以下のようなものを参考として作成し、全社へ共有しました。 FROM node:20 WORKDIR /app COPY package.json package-lock.json ./ # secretマウントし、そのRUNの中だけで .npmrcを作って使い、最後にrm RUN --mount=type=secret,id=takumi_token \ printf 'registry=https://npm.flatt.tech/\n//npm.flatt.tech/:_authToken=%s\n' \ "$(cat /run/secrets/takumi_token)" > /tmp/.npmrc && \ export NPM_CONFIG_USERCONFIG=/tmp/.npmrc && \ npm ci && \ npm install some-extra-package && \ rm -f /tmp/.npmrc - name : Build image env : TAKUMI_TOKEN : ${{ steps.takumi.outputs.token }} run : | docker build \ --secret id=takumi_token,env=TAKUMI_TOKEN \ -t myapp . PyPIの場合は pip install を実行した際、 ~/.cache/pip にレスポンスの情報などが残ってしまいます。トークンがそのまま残るわけではありませんが、 --no-cache-dir を利用してキャッシュを残さないようにするのがベターです。 FROM python:3.13-alpine WORKDIR /app ... # Takumi Guardのトークン入りPIP_INDEX_URLをsecretとしてマウント RUN --mount=type=secret,id=pip_index_url \ export PIP_INDEX_URL="$(cat /run/secrets/pip_index_url)" && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir -r requirements.txt - name : Build image run : docker build --secret id=pip_index_url,env=PIP_INDEX_URL -t myapp . 上記の渡し方は一例のため、開発環境に合わせて適宜変更してください。 MDMでのTakumi Guard配布 ZOZOではMDMとしてMicrosoft Intuneを利用しています。Intuneでは、Macについては シェルスクリプト を利用してTakumi Guardのラッパースクリプトを配布できます。Windowsについては 修復スクリプト を活用できます。検出スクリプトをダミーで作成し、修復スクリプトでTakumi Guardのラッパースクリプトを配布する形です。 これらのスクリプトはIntuneで定期実行しています。何らかの理由でTakumi Guardが外れてしまった場合でも、次回の定期実行時に再度配布されます。 Takumi Guardのラッパースクリプトについては 公式ドキュメント に記載されています。 これらのスクリプトを利用することで、全社端末にTakumi Guardを配布できます。 シークレットの問題 Intuneでシェルスクリプトを配布する場合、シークレットをうまく扱えないという問題があります。この点が今後の課題です。対策としては、SCEP方式の証明書を端末に配布した後、中間APIサーバを立ててAzure Key Vaultからシークレットを取得する方法などが考えられます。 この作業には各部署との調整や、ラッパースクリプトの大幅な改修が必要です。また、少数チームにとっては実装と運用のコストが高くなります。 このため、Takumi Guardの展開遅延の懸念がありました。昨今のサプライチェーン攻撃の脅威を考え、Takumi Guardのトークンを払い出すだけのBotシークレットの権限であれば一旦このリスクは許容し、速度を優先することにしました。 この方法を実施する場合は、定期的にBotのシークレットのローテーションを行うなど、リスク軽減を図る必要があります。 またトークンの異常な発行が行われていないか発行量を適宜確認し、必要に応じて対応することも求められます。この点はKey Vaultなどを利用した場合も同様です。 Takumi Guardの爆速アップデート対応 上記以外にも対応すべきことはありました。それは、爆速で行われるTakumi Guardのアップデートへの追従です。 前述の通りTakumi Guardは日々アップデートされています。展開の仕組みを整えている途中でRubyGems対応の機能が追加された時は、その速度感に驚愕したことを覚えています。 この速度に追従するためには、利用する側でも同様のデプロイの仕組みを整えておく必要がありました。ZOZOではGitHub Actions経由でIntuneのラッパースクリプトを更新する仕組みを整え、Takumi Guardの爆速アップデートへ追従できるようにしました。 以下がmacOSにおけるラッパースクリプトの更新のワークフローです。 name : Deploy Takumi Guard macOS script to Intune on : push : branches : - main paths : - scripts/install_takumiguard.sh permissions : contents : read id-token : write concurrency : group : intune-takumi-guard-mac cancel-in-progress : false jobs : deploy-macos-script : name : Deploy Takumi Guard macOS script runs-on : ubuntu-latest timeout-minutes : 30 steps : - name : Checkout uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with : persist-credentials : false - name : Azure Login uses : azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with : client-id : ${{ vars.AZURE_CLIENT_ID }} tenant-id : ${{ vars.AZURE_TENANT_ID }} allow-no-subscriptions : true - name : Fetch TG Bot API key from Key Vault id : keyvault env : AZURE_KEY_VAULT_NAME : ${{ vars.AZURE_KEY_VAULT_NAME }} run : | set -euo pipefail : "${AZURE_KEY_VAULT_NAME:?Set AZURE_KEY_VAULT_NAME in GitHub Actions variables}" api_key="$(az keyvault secret show \ --vault-name "${AZURE_KEY_VAULT_NAME}" \ --name tg-bot-api-key-mac \ --query value \ -o tsv)" if [ -z "${api_key}" ] ; then echo "Failed to fetch tg-bot-api-key-mac from Key Vault ${AZURE_KEY_VAULT_NAME}" > &2 exit 1 fi echo "::add-mask::${api_key}" echo "api_key=${api_key}" >> "${GITHUB_OUTPUT}" - name : Resolve target Intune macOS script env : INTUNE_TAKUMI_MAC_SCRIPT_ID : ${{ vars.INTUNE_TAKUMI_MAC_SCRIPT_ID }} run : | set -euo pipefail : "${INTUNE_TAKUMI_MAC_SCRIPT_ID:?Set INTUNE_TAKUMI_MAC_SCRIPT_ID in GitHub Actions variables}" echo "TAKUMI_MAC_SCRIPT_ID=${INTUNE_TAKUMI_MAC_SCRIPT_ID}" >> "${GITHUB_ENV}" echo "TAKUMI_MAC_SCRIPT_RESOURCE=deviceManagement/deviceShellScripts" >> "${GITHUB_ENV}" - name : Update Intune macOS script shell : bash env : SCRIPT_PATH : scripts/install_takumiguard.sh TG_BOT_API_KEY_MAC : ${{ steps.keyvault.outputs.api_key }} run : | set -euo pipefail : "${TG_BOT_API_KEY_MAC:?Failed to resolve tg-bot-api-key-mac from Key Vault}" echo "::add-mask::${TG_BOT_API_KEY_MAC}" build_script_content() { local api_key_literal api_key_literal="$(printf '%s' "${TG_BOT_API_KEY_MAC}" | perl -0pe 's/([\\\"\$`])/\\$1/g' )" API_KEY_EXPORT_LINE="export TG_BOT_API_KEY=\"${api_key_literal}\"" awk ' /^export TG_BOT_API_KEY=/ { print ENVIRON["API_KEY_EXPORT_LINE"] replaced += 1 next } { print } END { if (replaced != 1) { printf("Expected one TG_BOT_API_KEY export line, found %d\n", replaced) > "/dev/stderr" exit 1 } } ' "${SCRIPT_PATH}" | base64 -w 0 } script_content="$(build_script_content)" access_token="$(az account get-access-token \ --resource https://graph.microsoft.com \ --query accessToken \ -o tsv)" echo "::add-mask::${access_token}" response="$(printf '%s' "${script_content}" \ | jq -Rnc '{scriptContent: input}' \ | curl -sS -w " \n %{http_code}" \ -X PATCH "https://graph.microsoft.com/beta/${TAKUMI_MAC_SCRIPT_RESOURCE}/${TAKUMI_MAC_SCRIPT_ID}" \ -H "Authorization: Bearer ${access_token}" \ -H "Content-Type: application/json" \ --data-binary @-)" http_status="${response## *$'\n' } " if [ "${http_status}" = "200" ] || [ "${http_status}" = "204" ] ; then echo "Updated Takumi Guard Intune macOS script (${http_status})" else echo "Failed to update Takumi Guard Intune macOS script (${http_status})" echo "Response body omitted to avoid leaking scriptContent." exit 1 fi - name : Verify Intune macOS script update shell : bash env : SCRIPT_PATH : scripts/install_takumiguard.sh TG_BOT_API_KEY_MAC : ${{ steps.keyvault.outputs.api_key }} run : | set -euo pipefail : "${TG_BOT_API_KEY_MAC:?Failed to resolve tg-bot-api-key-mac from Key Vault}" echo "::add-mask::${TG_BOT_API_KEY_MAC}" build_script_content() { local api_key_literal api_key_literal="$(printf '%s' "${TG_BOT_API_KEY_MAC}" | perl -0pe 's/([\\\"\$`])/\\$1/g' )" API_KEY_EXPORT_LINE="export TG_BOT_API_KEY=\"${api_key_literal}\"" awk ' /^export TG_BOT_API_KEY=/ { print ENVIRON["API_KEY_EXPORT_LINE"] replaced += 1 next } { print } END { if (replaced != 1) { printf("Expected one TG_BOT_API_KEY export line, found %d\n", replaced) > "/dev/stderr" exit 1 } } ' "${SCRIPT_PATH}" | base64 -w 0 } access_token="$(az account get-access-token \ --resource https://graph.microsoft.com \ --query accessToken \ -o tsv)" echo "::add-mask::${access_token}" expected_script_content="$(build_script_content)" actual_script_content="$(curl -sS \ "https://graph.microsoft.com/beta/${TAKUMI_MAC_SCRIPT_RESOURCE}/${TAKUMI_MAC_SCRIPT_ID}" \ -H "Authorization: Bearer ${access_token}" \ -H "Accept: application/json" \ | jq -r '.scriptContent // empty' )" if [ "${actual_script_content}" != "${expected_script_content}" ] ; then echo "Takumi Guard macOS scriptContent verification failed" > &2 exit 1 fi echo "Verified Takumi Guard Intune macOS scriptContent" ポイントがいくつかあるため、以下の章で解説します。 許可の設定をEntra側に寄せる GitHub Actionsのワークフローでは Azure Login のOIDCでEntraのサービスアカウントにログインします。固定のシークレットを極力持たせたくないため、この構成を採用しました。 シークレットの閲覧 シークレット情報はGitHub Organizationの権限で管理するのではなく、EntraやARM側に寄せました。GitHub ActionsのワークフローからはEntraのサービスアカウント経由でAzure Key Vaultからシークレットを取得する形です。 取得のために Key Vault Secrets User のRoleを対象のKey Vaultに絞って、このサービスアカウントへ付与する必要があります。 Actions上ではメモリ上でシークレットを扱い、ログに出力されないように ::add-mask:: を利用してマスクしています。 ただ、前述のとおりIntune上ではシークレットが残ってしまいますが、この閲覧権限もIntuneの権限を付与できるEntraで管理できるので、Entraでの権限設定が重要です。 デバイススクリプトの更新権限 この権限もEntra側のサービスアカウントに付与します。 Microsoft Graph APIの deviceManagement/deviceShellScripts のエンドポイントを呼び出す必要があります。これは強い権限のため、上記ワークフローを実施するスクリプトの main ブランチへの変更を厳しく確認する必要がありました。 Push Ruleset そもそも、このリポジトリを閲覧できるアカウントを絞ることが前提です。加えて、そのアカウントによるPushにも制限が必要です。 このActionsは、 main ブランチへのpushかつ scripts/install_takumiguard.sh に変更があった場合のみ実行されるようにしています。 この変更についてはGitHubの Rulesets で管理しています。以下の項目を設定しておくとPRでレビューの通ったものがマージされるようになります。 Restrict deletions Require a pull request before merging Block force pushes 基本的にこのRuleのバイパスは設定せず、一律PRのレビューを通すようにしています。これにより、リポジトリの変更がある場合は必ずレビューが入るようになります。 レビュー者は .github/CODEOWNERS で設定しておくと、PR作成時に自動でレビュー依頼が飛びます。 展開後のログ画面 パッケージのダウンロードログはTakumiのポータルから確認できます。ブロックなどの条件で絞り込むことで、どのパッケージがブロックされたかを簡単に確認できます。 展開後のログ画面 おわりに 本記事では、開発環境におけるサプライチェーン対策として、Takumi Guardを全社展開した事例について紹介しました。 まだまだ課題はありますが、Takumi Guardの導入により、開発環境におけるサプライチェーン攻撃のリスクを低減しました。 ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co Takumi Guard ↩
ZOZO開発組織の2026年6月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2026年6月は、前月のMonthly Tech Reportを含む計16本の記事を公開しました。ここ最近の傾向と変わらず、Claude Code関連の記事は特に反響がありました。ZOZOならではの取り組み事例をぜひご覧ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com 登壇 Google I/O 2026現地参加レポート 6月6日に開催された「 Recap: Google I/O 2026 」において、技術戦略部の堀江( @Horie1024 )が「Google I/O 2026 現地参加レポート」というタイトルで登壇、WEAR開発部の武永が「Android & Ecosystem パネルディスカッション」に登壇しました。 Extended Tokyo - WWDC 2026 6月8日に開催された「 Extended Tokyo - WWDC 2026 」において、技術戦略部の諸星( @ikkou )が「WWDC26 直前!眼鏡型デバイスのイマ!」というタイトルで登壇しました。 2026 年度 人工知能学会 全国大会(第 40 回) 6月8日から12日にかけて開催された「 2026 年度 人工知能学会 全国大会(第 40 回) 」において、データサイエンス部の桐島を第一著者として「画像生成AIによる背景生成を用いた最適なフレグランス商品画像の検討」というタイトルでポスター発表しました。 https://pub.confit.atlas.jp/ja/event/jsai2026/presentation/4Yin-A-24 TSKaigi 2026 本編で話せなかったこと、話し足りなかったこと 6月9日に開催された「 TSKaigi 2026 本編で話せなかったこと、話し足りなかったこと 」において、リプレイス推進部の冨川( @ssssotaro )が「TSKaigi ZOZOブースクイズの裏側」というタイトルで登壇しました。 AI研究最前線:第一人者・気鋭の研究者と語る 6月16日に開催された「 AI研究最前線:第一人者・気鋭の研究者と語る 」において、ZOZO Researchの平川が「AIと創るファッションECの未来」というタイトルで登壇しました。 WWDC26 報告会 at LINEヤフー, ZOZO 6月18日に開催された「 WWDC26 報告会 at LINEヤフー, ZOZO 」において、ZOZOTOWN開発2部の上田( @15531b )が「現地で盛り上がった WWDC26 Keynote」というタイトルでショートトークに、森口( @laprasdrum )がパネルディスカッションに登壇しました。 techblog.zozo.com 生成AI フォーラム / AI エージェント フォーラム 2026 夏 6月26日に開催された「 生成AI フォーラム / AI エージェント フォーラム 2026 夏 」において、執行役員 兼 CTOの瀬尾( @sonots )が「ZOZOのAI活用実践」というタイトルで登壇しました。 以上、2026年6月のZOZOの活動報告でした! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに ZOZOTOWN開発本部のらぷ( @laprasdrum )とイッセー( @15531b )です。WWDC現地参加は、らぷは2016年以来2回目、イッセーは初参加(かつ初渡米)です。本記事ではWWDC26の現地参加レポートとともに、ZOZOのiOSエンジニアによるおすすめセッション、6月18日に開催されたLINEヤフー株式会社との合同報告会イベントについてもご紹介します。 目次 はじめに 目次 WWDC26のSpecial Event Day 0 Day 1 Keynote Download stations Platforms State of the Union 現地視聴をより楽しむために In-person labs Inner ring reception Day 2 Developer session Mixer @Developer Center Theater event 併催コミュニティイベント おすすめセッション集 優れたデザインのための原則 Foundation Modelsフレームワークの新機能 WWDC26 報告会 at LINEヤフー, ZOZO さいごに WWDC26のSpecial Event 今年6月7日〜9日(現地時間)のWWDC26では、昨年と同様に招待制のSpecial Eventが現地開催されました。昨年の様子は「WWDC25現地参加レポート」をご覧ください。 techblog.zozo.com 今年のSpecial EventはWelcome receptionから始まりました。基調講演後には、Appleの方へ新APIを質問したりアプリのフィードバックをもらえたりするIn-person labsが開かれました。その後、Apple ParkのInner ringで交流会がありました。最終日はSteve Jobs Theaterで、スター・ウォーズ作品『The Mandalorian and Grogu』の上映とスペシャルゲストのインタビューを楽しめました。 Date Pacific Time Content Venue Day 0 (June 7) 3 p.m. Welcome reception Infinite Loop Day 1 (June 8) 8 a.m. Check-in Apple Park 10 a.m. Keynote Apple Park After Keynote Download stations open Apple Park 12 p.m. Lunch Apple Park 1 p.m. Platforms State of the Union Apple Park After Platforms State of the Union In-person labs Apple Park 4-6 p.m. Reception in the inner ring Apple Park Day 2 (June 9) 10 a.m. Developer session Steve Jobs Theater or Apple Developer Center Cupertino 11:30 a.m. Mixer Apple Developer Center Cupertino 8 p.m. Pre-show presentation and special screening of The Mandalorian and Grogu Steve Jobs Theater Day 0 私たちはDay 0の午前にSFO(サンフランシスコ国際空港)へ到着し、宿泊先のホテルに荷物を置いてからInfinite Loopへ向かいました。道中は初めてWaymoを利用しました。急カーブでもほとんど揺れずに走行するため、仮眠できるほど静かでした。 Infinite Loopに到着するとチェックイン待ちの長蛇の列がすでにできていました。並んでいる間のおもてなしとして配られたジェラートバーとお水で暑さをしのぎつつ待ちました。 2種類の味から選べたジェラートバー 入口前でセキュリティチェックを済ませると、参加者用のバッジとノベルティを受け取りました。バッジにはNFCタグが入っており、各イベントのチェックイン時にAppleのスタッフのiPhoneにバッジをかざすことになっています。ノベルティ恒例のピンバッジにはLil Finder Guyと創立50周年を象徴するデザインが含まれていました。入場時はAppleのスタッフの方々が歓声とともに出迎えてくださり、Day 0から盛り上がりを肌で感じました。 今年のノベルティバッグ 今年のピンバッジ、ステッカー、ボトル 参加者用バッジ ── お気に入りのピンバッジを添えて その後は終了時刻の夕方7時まで自由に過ごしました。軽食とドリンクをいただきながら各国の参加者との交流、Apple Design Awardsの受賞者とファイナリストとのトーク、Appleのスタッフによるモニュメント前の記念撮影などを楽しみました。 快晴に恵まれたこの日 モニュメント前で記念撮影 また、APACの参加者で集まって記念写真も撮りました。オフラインで参加すると、多くの方がApple Platformの開発者として関わっていることを実感できました。 Day 1 Keynote まずはKeynoteの会場に向かうため、Visitor Center前でチェックインを済ませてApple Parkに入場しました。入場まで横断歩道の信号待ちで列がゆっくり進む中、Caffè Macsのスタッフからいただいたドーナツを食べながら近くの方と「今年のWWDCは何を期待してますか?」と雑談しました。話しかけた方の多くがAIのトピックを気にされていました。 Visitor Center前のチェックインも長蛇の列 Caffè Macsからいただいたオレンジ味のドーナツ 会場に到着したのは8時半頃でした。Keynote開始時間の10時までは自由に散策し、参加者とお話を楽しみました。 KeynoteおよびPlatforms State of the Unionの会場 会場のスクリーンでは、オンラインで公開されているKeynoteの動画が再生されます。現地では動画の再生前にCraig Federighi氏がステージに登場し、挨拶がありました。 developer.apple.com 続いてTim Cook氏も登場し、このときの会場の盛り上がりは強烈でした。CEOとしてのKeynoteでの登壇は今年が最後です。参加者全員が立ち上がってiPhoneを掲げて撮影する光景を前に、Cook氏自身も「これまでこんなにiPhoneに囲まれたことはありません」と語っていました。 壇上に登場したTim Cook氏 Download stations Keynoteが終了すると、高速の有線ネットワークが提供されるDownload stationsに集まりました。ここでXcode 27 BetaやiOS 27 Betaのダウンロードを始めました。この日は晴天で気温も高かったため、パラソルの影に集まって休憩する参加者も多かったです。 有線ネットワークが用意されたDownload stations Platforms State of the Union developer.apple.com 手元にiOS 27 BetaをインストールしたiPhoneを触りながら、先のKeynoteの内容も踏まえてPlatforms State of the Unionを視聴しました。今回のSiri AIやCore AI、Foundation Modelsなどのアップデートには、共通した方向性があります。デバイスへ蓄積された写真やテキストなどのパーソナルなデータをもとに、AI体験を最適化することがAppleの狙いです。Newsroomで公開されたSiri AIの記事には、Apple Intelligenceのアーキテクチャ図が掲載されています。この図では、デバイスとアプリケーション(またはSiri AI)の間にユーザーコンテキストが示されています。Appleプラットフォームのアプリ開発者には、このコンテキストを活かしたAI活用が求められているのでしょう。 www.apple.com 視聴後しばらくすると、スクリーン上にさまざまなアプリのアイコンが表示されました。幸運なことに弊社のアプリが映し出されたタイミングで撮影できました。 どこかにある弊社アプリのアイコン。見つけられましたか? 現地視聴をより楽しむために 現地ならではの視聴の楽しみ方も、二人それぞれ工夫してみました。らぷは Rokid Glasses というスマートグラスのリアルタイム翻訳機能を使って視聴しました。体感1秒未満で翻訳結果が表示されたので和訳文も追いつつ快適に視聴できました。 Rokid Glassesのリアルタイム翻訳結果 イッセーは英語に自信がなかったため、セッションの視聴でAirPods Proのライブ翻訳機能を試しました。実際に使ってみると、登壇者の英語が純正の翻訳アプリ上に日本語テキストとして表示されるだけでなく、AirPodsからも日本語に翻訳された音声がリアルタイムで流れてきます。この機能のおかげで、英語に自信がなくてもセッションの内容を大まかに理解できました。イッセーとしては、画面上のテキスト翻訳はあえて見ず、登壇者の様子を見ながら日本語音声だけを聞くスタイルがとても快適でした。 ライブ翻訳機能の翻訳結果 私たち以外にもスマートグラスやAirPodsを使って視聴している参加者が多くいました。海外カンファレンスへ現地参加する際の言語の壁はこれまでもありましたが、こうした技術によって乗り越えやすくなったと感じました。 In-person labs Platforms State of the Unionが終了するとIn-person labsが始まり、各ラボで新しく発表されたAPIや日々の開発で困っていることを相談しに行きました。特に困ったことがなくても「まだ触れたことのないFrameworkを始めようと思うんだけど何から始めたら良いですか?」といった相談でも問題ありませんでした。気さくな雰囲気の中でいろいろ話せるので、来年現地に行かれる方はぜひ積極的に利用してみてください。 Inner ring reception その後夕方6時まではInner ring内で自由に過ごしました。Xcode 27 Betaで開発したり、初対面や既知の開発者との会話を楽しんだり、Appleの方に質問したりと、あっという間に時間は過ぎていきました。 Xcodeダウンロード中にAppleのWWDRの方にいただいたFoundation Models Frameworkのステッカー 虹のApple Stage前で撮影してもらったイッセー。青いシャツの方は各国のApple Storeのスタッフ Day 2 Developer session Developer sessionに参加するため、Apple Parkへ向かいチェックインを済ませました。今回のセッションはSteve Jobs Theaterで開催されました。ここは過去のWWDCでは立ち入ることができなかった特別な場所です。定員制のため、シアターに入れない場合はApple Developer Centerでの視聴でした。開始1時間前に到着したものの、すでに多くの参加者が列を作っていました。皆がこのシアターでの参加を待ち望んでいる熱気を感じました。 無事に入場でき、ガラス張りの円形の建物と緑豊かな造形美に圧倒されました。会場で軽い朝食をとった後、いよいよセッションがスタートしました。 Developer session前のSteve Jobs Theaterの様子 今回のセッションでは、Xcode 27の「Agentic Coding」やFoundation Modelsなどの新しいAIフレームワークが取り上げられました。実際のアプリ開発にどう組み込むかという実践に焦点を当てた内容です。その中でも、Evaluations Frameworkはどのように役立てられるのかイメージができていませんでした。セッションを通して、LLMの出力の品質を容易にテスト・評価できる点を理解でき、実際に触ってみる良いきっかけになりました。動画でも公開されているので、使い方のイメージが付いていない方の参考になります。ぜひチェックしてみてください。 developer.apple.com Developer sessionの様子 Mixer @Developer Center セッション終了後はDeveloper Centerへ移動し、「Mixer」というイベントに参加しました。会場にはさまざまなフードトラックが用意されており、昼食をとりながらコーディングをする人々の姿も見られました。また、特定のテーマごとにブースが設けられており、参加者同士で交流したり、Appleのエンジニアに直接質問したりできる貴重な場となっていました。さらに、Developer Center内ではGroup Labが開催されており、会場の内外いずれも非常に活気に満ちていました。 テーマごとに交流ができる場所 Mixerの会場図 Theater event 夜は、現地参加者の中で先着申し込みができた「Theater event」に参加しました。日中と同じくSteve Jobs Theaterで開催されるとのことで、再びあの空間に足を踏み入れることができました。 Theater eventでのSteve Jobs Theater 今回上映されたのは、今年公開されたスター・ウォーズ作品『The Mandalorian and Grogu』です。上映前にはスペシャルゲストによる対談が行われていました。その際、通路の透明なディスプレイに会話の文字起こしと手話がリアルタイムで表示されていました。Appleらしいアクセシビリティへの高い配慮に、とても驚かされました。スター・ウォーズにあまり精通していなかったため少し心配でしたが、思った以上に内容が分かりやすく、シーンによっては観客の歓声で盛り上がるなど、非常に楽しむことができました。 インタビュー中に字幕と手話が表示される透明なディスプレイ 併催コミュニティイベント らぷはCommunityKitが主催している2つのイベントに参加してきました。ひとつはPaul Hudson氏が開催した「What's new in iOS 27?」です。 luma.com WWDCでは、Day 1にAppleから全セッション動画とサンプルプロジェクトが公開されます。Hudson氏はSwiftUIやSwiftDataなどのサンプルプロジェクトを実際にビルドし、発生したエラーと修正方法を紹介してくださいました。すべてのサンプルプロジェクトが修正なしで期待どおりに動作するわけではないので、こうした知見を聞けるのは助かります。 会場の様子 もうひとつは「Swift Contributor Social」というイベントです。こちらはSwiftのコントリビューターと、それに興味がある人たちによる交流会です。まだコントリビューターではありませんが、どんな観点で活動しているのか聞いてみたくて参加しました。 luma.com 今回はstdlibのメンテナーでSpanの実装を担当された方にじっくりお話を聞くことができました。 developer.apple.com メモリ安全性への興味や今後の展望、コンパイラーチームが別途持っている構想と突き合わせた上での定期的な議論などを伺いました。Swift Forumsを眺めているだけでは得られない情報が多く、刺激的な時間でした。 ボウリング場のビリヤード台を囲んで自由に交流中 参加中、もしくは興味のあるWorking Groupのバッジ おすすめセッション集 優れたデザインのための原則 ZOZOTOWN iOSアプリの開発をしているつっきー( @tsuzuki817 )です。 今回のWWDCでは、Appleが考える優れたデザインの原則を扱うセッションがありました。デザインを単なる「見た目やふるまい」ではなく、「意図をもってものを作ること」と定義した上で、数々の重要な指針を解説しています。詳細は「 Principles of great design 」のセッションをご覧ください。 私が特に注目したのは、「主導権」「寛容さ」「柔軟性」という3つの原則のつながりです。ユーザーをあらかじめ決められた道に無理に誘導するのではなく、主導権を人々に委ね、自分のペースで探索させることの重要性が語られています。しかし、自由に探索できるようになると、ユーザーは誤って削除してしまったり意図しない操作をしてしまったりすることがあります。 そこで重要になるのが「寛容さ」です。操作を簡単に取り消せるようにし、大惨事を避けられるようにすることで、ユーザーに「いつでも回復できる」という自信を与え、安全な探索を支えることができます。さらに、ユーザーがアプリを使う状況(ランニング中や運転中など)や能力は人それぞれであるため、それらに適応する「柔軟性」を持たせることも強調されていました。 すべての人に合う単一のレイアウトを見つけるのは難しいため、ユーザー自身が好みに合わせて体験をカスタマイズできるようにすることが、最良の柔軟性です。セッションの最後では、これらの原則を正しく実行した自然な結果として、真の感情的なつながりである「喜び」が生まれると語られていました。これは製品に込めた「思いやりの総和」であるとのことです。自分がアプリを開発するうえで大事にしていることでもあり、同時に難しいと感じている点でもあります。 普段ZOZOTOWNの開発や個人開発をするなかで、ユーザーにどのような体験をさせたいのか迷うことが多くあります。このような原則に則って考えることで、よりよいプロダクトが作れそうだと期待しました。また、この考え方はZOZOTOWNのアプリ開発でぜひ意識していきたいです。さらにユーザーだけではなく、普段の仕事で起きるコミュニケーションにも活かせます。相手に選択肢を委ね、ミスを許容し、状況に柔軟に対応していきます。 Foundation Modelsフレームワークの新機能 ZOZOTOWN iOSアプリの開発をしているpe( @shumpei_nagata )です。 今年のWWDCでは、Foundation Modelsフレームワークの進化に興味を惹かれました。詳細は「 What's new in the Foundation Models framework 」で紹介されています。 このフレームワークは昨年登場し、アプリ上でオンデバイスの大規模言語モデルを扱えるようになりました。そして今年は画像入力(Vision)に対応しました。テキストだけだったプロンプトに Attachment(UIImage(...)) のように画像を差し込むと、その画像について回答が得られます。ガイド付き生成( @Generable )やツール呼び出しなどの既存APIもそのまま使えるので、これまで書いたコードを活かして画像対応に広げられます。 また、Foundation Modelsの導入に役立つ道具も増えてきました。生成AIは出力が確率論的に決まるので、生成結果の品質を確かめるのはなかなか骨が折れます。サービスの機能として安心して導入するには、品質を測定できる仕組みが必要です。そういった課題には Evaluations Framework が役立ちます。併せてAppleからオープンソースで公開された foundation-models-utilities も参考になります。コンテキスト超過を防ぐトランスクリプトの圧縮やSkills APIなど、Foundation Modelsをより便利に扱えるツールが揃っています。 このセッションが気になったのは、業務と個人開発のどちらでも視覚的な情報を多く取り扱うアプリを開発しているからです。進化したFoundation Modelsフレームワークを組み込むことで、アプリ上の体験を大幅に拡張できそうです。たとえば説明文の作成、属性の抽出、altテキストによるアクセシビリティの補完などが挙げられます。こうした処理が通信なし・無料で、しかもプライバシーを保ったまま端末の中で完結するのは非常に魅力的です。 とはいえ、既存のサービスへ導入するハードルは低くありません。Foundation Modelsは利用にiOS 26以降が必要で、今回の画像入力はiOS 27以降でないと動きません。その上、Apple Intelligence対応デバイスという条件もつきます。またZOZOTOWNはiOSアプリだけでなく、AndroidアプリやWebサイトなどさまざまなプラットフォームでお客様にご利用いただいています。OSや端末が限られる機能をそのまま主役に据えると、体験に大きな差が生じてしまうので、導入の見極めが難しい技術であるのも事実です。 それでも、アプリに導入するとどのような新しい体験を提供できるかとワクワクしました。技術的な好奇心もくすぐられるセッションでした! WWDC26 報告会 at LINEヤフー, ZOZO WWDC後の6月18日にLINEヤフーと合同で報告会を実施しました。 lycorptech-jp.connpass.com イッセーは「現地で盛り上がったWWDC26 Keynote」というタイトルで登壇しました。 speakerdeck.com 報告会では、実際に現地参加して見えたことを紹介しました。Keynoteで周囲のデベロッパーがどのようなテーマで盛り上がったのか、意外と反応が落ち着いていたテーマはどれか、思いもよらないサプライズは何だったのかを共有しています。一番印象的だったのは、事前に「これが目玉だろう」と考えていたトピックと、会場が実際に沸いたトピックのズレです。この差は公式の発表資料やニュース記事を後から追っても分かりません。世界中のデベロッパーがどのトピックに興味関心があったのかは、その場にいた人にしかわからない情報でした。 らぷはパネルディスカッションに参加し、印象的だったセッションや現地参加に必要な準備などを共有しました。先ほどのスマートグラスの活用や気になったセッションの話も取り上げています。「WWDCに現地参加するなら何をすべきですか?」という質問への回答は、他の参加者やAppleのスタッフの方と積極的にコミュニケーションすることです。これはLINEヤフーのパネラーの方と同じ意見でした。自分にはない考え方に触れたり、同じ考えをもつ同志に出会えたりできる機会です。ぜひ最大限に活用してください。 パネルディスカッションの様子 さいごに 今年のWWDCで感じた現地の雰囲気とおすすめセッション、報告会の様子をお届けしました。この記事を読んで現地での思い出を振り返ったり、新しいAPIの調査を始めて共有したりしていただけると嬉しいです。 ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。WEAR部iOSブロックの坂倉です。 WEARは先日、 SceneDelegate へ移行しました。ライフサイクルそのものに関わる変更のため、画面数の多い歴史あるアプリではなかなか骨が折れました。 この記事では、 WEARが直面した課題 、 課題を解決するために取ったアプローチ 、そして 移行中につまずいた箇所と対処 をご紹介します。 WEARのような大規模アプリが、影響範囲の広い SceneDelegate 対応をどのように進めたのか、その実例として参考になれば幸いです。 目次 はじめに 目次 SceneDelegate対応で直面した課題 iOS 27以降、未対応のアプリは起動できなくなる アプリのライフサイクルに関わる変更のため、すべての画面に影響する ライフサイクルイベントがAppDelegateとSceneDelegateに分かれて呼ばれる すべての画面で問題が出ないか、慎重に調査しなければならない 課題を解決するためのアプローチ アプローチ1. AppDelegateとSceneDelegateの役割を理解する AppDelegateの役割 SceneDelegateの役割 役割と違いのまとめ どの処理をSceneDelegateへ移すか プッシュ通知はAppDelegateに残す アプローチ2. AIを使って影響範囲を特定し、QA観点をまとめる 1. AIを使って影響範囲を把握する 2. AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す 3. AIにQA観点を書き出させる これから対応するならXcode 27の併用も検討を アプローチ3. リスクを抑えるため、最小限の変更にとどめる つまずいた箇所と対処 一番最初のウィンドウを取得するのは危ない 外部ソースからの起動判定はconnectionOptionsで行う さいごに SceneDelegate対応で直面した課題 WEARが直面した課題は大きく2つあります。「対応しないという選択肢がない」ことと、「対応がアプリの全画面に影響する」ことです。 iOS 27以降、未対応のアプリは起動できなくなる Appleは公式ドキュメント Transitioning to the UIKit scene-based life cycle で、 UIScene ライフサイクルの採用が必須になると明記しています。最新SDKでビルドするアプリが対象となり、iOS 27・iPadOS 27・Mac Catalyst 27・tvOS 27・visionOS 27以降、未対応のアプリは起動できなくなります。 つまり、対応しない限り最新SDKでのアップデートを続けられなくなります。 アプリのライフサイクルに関わる変更のため、すべての画面に影響する SceneDelegate に対応すると、これまで AppDelegate が受け取っていたUI関連のライフサイクルイベントは SceneDelegate が受け取るようになります。ライフサイクルはアプリの根幹であり、この変更はWEARが持つすべての画面に影響し得ます。 具体的には次のような難しさがありました。 ライフサイクルイベントがAppDelegateとSceneDelegateに分かれて呼ばれる まず大変だったのが、これまで AppDelegate だけに届いていたライフサイクルイベントが、 AppDelegate と SceneDelegate の2つに分かれて呼ばれるようになる点です。 AppDelegate 中心の頃、起動時のイベントは次の順番で、すべて AppDelegate に届いていました。 application(_:willFinishLaunchingWithOptions:) application(_:didFinishLaunchingWithOptions:) applicationDidBecomeActive(_:) 初期化処理も1つのクラスに集まっているため、実行順序を追うのは難しくありませんでした。 ところが SceneDelegate を採用すると、アプリ起動時の実行順序は次のように2つのクラスをまたぐ形になります。 application(_:willFinishLaunchingWithOptions:) (AppDelegate) application(_:didFinishLaunchingWithOptions:) (AppDelegate) scene(_:willConnectTo:options:) (SceneDelegate) sceneWillEnterForeground(_:) (SceneDelegate) sceneDidBecomeActive(_:) (SceneDelegate) 起動処理が2つのクラスに分散すると、「この初期化はあの処理より先に終わっているはず」という前提が崩れます。たとえば、 SceneDelegate へ移したUIのセットアップが AppDelegate 側の初期化に依存していると破綻します。 さらに、呼ばれるタイミング自体が変わるイベントもあります。 sceneWillEnterForeground(_:) は applicationWillEnterForeground(_:) と違い、アプリ未起動(Not Running)からの起動でも呼ばれます。フォアグラウンド復帰時の処理をそのまま移すと、起動時にも実行されてしまいます。 どの処理を、どちらのクラスの、どのイベントに置くべきか 。アプリ全体の初期化処理を1つずつ見極める必要がありました。 参考: About the app launch sequence すべての画面で問題が出ないか、慎重に調査しなければならない WEARには通常起動のほかに、URLスキーム・ユニバーサルリンク・プッシュ通知など複数の起動導線もあります。特定の条件が揃ったときだけ顕在化するリグレッションもあり得るため、すべての画面について「移行しても問題が出ないか」を1つずつ調査していく必要がありました。 画面数の多いWEARにとって、これは人力だけでやり切れる規模ではありません。 課題を解決するためのアプローチ これらの課題に対して、WEARでは次の3つのアプローチで移行を進めました。 AppDelegateとSceneDelegateの役割を理解する AIを使って影響範囲を特定し、QA観点をまとめる リスクを抑えるため、最小限の変更にとどめる アプローチ1. AppDelegateとSceneDelegateの役割を理解する まず取り組んだのは、Appleの公式ドキュメントを読み込み、 AppDelegate と SceneDelegate がそれぞれ何を担当しているのかを正確に理解することでした。 AppDelegateの役割 UIApplicationDelegate プロトコルに準拠する AppDelegate は、アプリ全体のライフサイクルイベントを管理します。 アプリレベルのイベント管理 :アプリの起動・終了、システムレベルのイベントなど、アプリプロセス全体のライフサイクルを担当する。 application(_:didFinishLaunchingWithOptions:) などを通じて、アプリの初期設定や起動準備をする 初期の環境設定 :アプリ全体のデータやリソースの初期設定、プッシュ通知の処理などを担当する シングルトン :1つのアプリにつき、 UIApplication は1つしか存在しない 注意したいのは、 SceneDelegate に対応すると、UIに関するライフサイクルイベントは SceneDelegate の担当に移り、 AppDelegate には届かなくなるという点です。 SceneDelegateの役割 UIWindowSceneDelegate プロトコルに準拠する SceneDelegate は、「シーン」のライフサイクルイベントに応答します。シーンとは画面に表示されるアプリのUIのひとまとまりのことで、たとえばiPadで同じアプリを2つのウィンドウで並べて開くと、それぞれが独立したシーンになります。 シーンの管理 :アプリのUIを「シーン」として表現し、それぞれのシーンが独自のライフサイクルと状態を持てる。これにより、複数のウィンドウを並べて表示するマルチウィンドウ環境が可能になる シーン固有のライフサイクルイベント :これまで AppDelegate が扱っていたUI関連のライフサイクルイベントを SceneDelegate が担当する 役割と違いのまとめ 特徴 AppDelegate SceneDelegate 責任範囲 アプリ全体のプロセスとライフサイクル(起動・終了・システムレベルイベント)を管理 シーンのライフサイクルを管理 UIの管理 従来はアプリで唯一のUIとウィンドウを管理。iOS 13以降はUI管理の責任が軽減された 各シーンのUIのセットアップ・ウィンドウ管理・UIライフサイクルイベントを処理 マルチウィンドウサポート 単一のウィンドウのみ 複数のウィンドウをサポート 要するに、 AppDelegate はアプリの「プロセス」レベルの管理を担い続けます。一方の SceneDelegate は、シーンごとのUIの管理と、ライフサイクルイベントへの応答を担当します。 どの処理をSceneDelegateへ移すか 役割が分かれば、対応方針は明確です。 AppDelegate で処理していた次の処理を SceneDelegate に移行しました。 説明 移行前(AppDelegate) 移行後(SceneDelegate) 具体例 アプリ起動時のWindow生成処理 application(_:didFinishLaunchingWithOptions:) scene(_:willConnectTo:options:) Windowの生成、起動に必要なデータ取得、GA送信のための設定、URLスキームの受け取り シーンがアクティブになったときの処理 applicationDidBecomeActive(_:) sceneDidBecomeActive(_:) プッシュ通知バッジのリセット シーンが非アクティブになる直前の処理 applicationWillResignActive(_:) sceneWillResignActive(_:) — シーンがフォアグラウンドに入る時の処理 applicationWillEnterForeground(_:) sceneWillEnterForeground(_:) 必要に応じたリロードとAPI実行 シーンがバックグラウンドに入る時の処理 applicationDidEnterBackground(_:) sceneDidEnterBackground(_:) バックグラウンド移行時のUI後処理 ユニバーサルリンク処理 application(_:continue:restorationHandler:) scene(_:continue:) ユニバーサルリンクの受け取り URLスキーム処理 application(_:open:options:) scene(_:openURLContexts:) URLスキームの受け取り プッシュ通知はAppDelegateに残す ここで1つ注意点です。 プッシュ通知はアプリ全体に関わるイベントなので、引き続き AppDelegate で処理します 。 Appleのドキュメント にもある通り、デバイストークンの登録や通知の受信といったプッシュ通知のハンドリングには、 SceneDelegate 側に対応する移行先が用意されていません。 なお、通知への応答がシーンと無関係というわけではありません。通知へのユーザーの応答(タップなどの操作)をシステムがどのシーンに反映するかを示す UNNotificationResponse.targetScene というプロパティが用意されています。通知タップ後の処理でシーンが必要な場合は、このプロパティから取得できます。 アプローチ2. AIを使って影響範囲を特定し、QA観点をまとめる 13年の歴史を持ち、300個ものViewControllerを抱えるWEARにとって、影響範囲の調査は人力だけでやり切れる規模ではありません。そこで、Claude CodeやGitHub CopilotなどのAIをフル活用し、3つのステップを踏んで対応を進めました。 AIを使って影響範囲を把握する AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す AIにQA観点を書き出させる 1. AIを使って影響範囲を把握する 先ほど触れた どの処理をSceneDelegateへ移すか の表をAIに読み込ませ、影響範囲を特定しました。また、具体的にどんな処理が入っているのかも表にさせたことで、必要な対応の解像度を上げました。 2. AIが高い精度で仕事できるように、デリゲートメソッドを1個ずつSceneDelegateに移す どの処理をSceneDelegateへ移すか で洗い出した対象メソッドは、もれなくSceneDelegateへ移す必要があります。しかし、一度にやってしまうと対応漏れが出かねません。AIの出力の質やスピードを上げる意味でも、範囲を絞って1個ずつ移すのが良いと判断しました。 3. AIにQA観点を書き出させる ここが一番助かった部分です。今まで、QAに提出する観点は人の手で書いていました。これを機に、その大半をAIが書く形へ変えました。 具体的な手順は次のとおりです。 まず、AIにSceneDelegate対応をさせてプロトタイプを作り、それをベースに全体の影響範囲とQA観点をざっと把握する メソッド移行ごとにブランチを切り、PR作成時、Diffの内容からJiraへQA観点を出力する プロトタイプで書き出したQA観点とブランチごとのQA観点を見比べて不足がないか確認する こうすることで、人間が気づかなかった範囲も調べて出力してくれるので、質の良いQAにつながりました。 具体的には、特定の地域からアクセスしたときだけ表示する利用規約Windowや、メンテナンス画面の表示・非表示のように複数のライフサイクルイベントにまたがって初めて成立する処理などが対象でした。普段意識する機会が少ない処理ほど、人力でQA観点を洗い出す際には抜け落ちがちです。AIに列挙させたことで、こうした処理も取りこぼさずQA観点に反映できました。 これから対応するならXcode 27の併用も検討を WEARのSceneDelegate対応は2025年に行ったため、Xcode 27を使うことはできませんでした。これから対応する場合は、Xcode 27の併用も検討してみてください。 WWDC26のセッション「 Modernize your UIKit app 」では、Xcode 27同梱の「uikit-app-modernization」スキルが紹介されています。このスキルはSceneDelegate対応だけでなく、UIScreen.mainやinterfaceOrientationのモダナイズなどにも対応しています。今進めているAdaptiveレイアウト対応でも役立っています。 また、Xcode 27は、Appleの公式ドキュメントやテックノートはもちろんのこと、Apple Developer Forumsの情報も参照してくれるため、非常に便利です。正式リリースが待ち遠しいですね。 アプローチ3. リスクを抑えるため、最小限の変更にとどめる 影響範囲の広さに対するもう1つの答えが、移行のスコープを必要最低限にとどめることです。WEARでは以下の方針で移行を進めました。 シーンのライフサイクルにのみ対応する 処理の移行を最優先し、処理のリファクタリングは後で行う マルチウィンドウの対応は一旦見送る SceneDelegate に対応すれば、iPadでのマルチウィンドウなど、できることは増えます。しかし、リスクを最小限に抑えるため、まずシーンのライフサイクル対応だけを完結させると決めました。 また、WEARの AppDelegate はObjective-Cで書かれた非常に古いコードだったため、本音を言えばリファクタリングもしたいところでした。今回、 SceneDelegate へ移行した処理に限りSwiftへの置き換えを行いましたが、処理そのものには手を加えず、挙動を変えない必要最小限の修正にとどめています。 つまずいた箇所と対処 最後に、移行中に直面した代表的な不具合と、その対処を紹介します。 一番最初のウィンドウを取得するのは危ない SceneDelegate 対応後は、 UIWindow が各シーン( UIWindowScene )に紐づき、シーンごとの持ち物へと変わります。 本来は良くない実装ですが、WEARはこれまでSceneDelegateに対応していなかったため、以下のコードでWindowを取得しても問題になっていませんでした。 extension UIApplication { var firstKeyWindow : UIWindow? { let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene return windowScene?.windows.first( where : \.isKeyWindow) } } しかし、SceneDelegate移行後はマルチウィンドウが可能になるため、最初に取得したシーンのWindowが、いま操作しているWindowとは限らなくなります。 よって、ViewControllerからWindowを取得する場合は、 view.window を使って「いま表示しているシーンのWindow」を取得する形へ修正しました。ただし view.window は、Viewがウィンドウ階層に追加されるまでは nil のままなので、呼び出すタイミングには注意が必要です。 // Before: アプリ全体のkeyWindowに依存していた let rootVC = UIApplication.shared.firstKeyWindow?.rootViewController as? RootViewController // After: 自分が表示されているシーンのWindowから取得する let rootVC = view.window?.rootViewController as? RootViewController 外部ソースからの起動判定はconnectionOptionsで行う これまでは AppDelegate の launchOptions のキーで「外部から開かれた起動か」を判定し、ダイアログ表示などを制御していました。 SceneDelegate では、この起動情報は scene(_:willConnectTo:options:) に渡される connectionOptions へ集約されています。型は UIScene.ConnectionOptions です。 これを参照することで、URLスキームやユニバーサルリンク、プッシュ通知など、「どこで起動されたのか」を判別できます。 WEARでは次のように判定しています。 userActivities や urlContexts からディープリンクを取り出して種別を判定します。それらがなくリモート通知での起動であれば .remoteNotification を、いずれにも当てはまらない通常起動なら nil を返します。 extension SceneDelegate { enum ExternalSource { case urlScheme case universalLink case remoteNotification } } extension SceneDelegate.ExternalSource { @MainActor init? (_ connectionOptions : UIScene.ConnectionOptions ) { let userActivityURL = connectionOptions.userActivities.first { $0 .activityType == NSUserActivityTypeBrowsingWeb }?.webpageURL let urlSchemeURL = connectionOptions.urlContexts.first?.url if let deepLinkURL = userActivityURL ?? urlSchemeURL { switch deepLinkURL.scheme { case "URLスキーム名" : self = .urlScheme case "https" : self = .universalLink default : return nil } return } if connectionOptions.notificationResponse != nil { self = .remoteNotification return } return nil } } func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { let externalSource = SceneDelegate.ExternalSource(connectionOptions) } さいごに 「技術調査で役割を正確に理解する」「AIで影響範囲を特定する」「最小限の変更にとどめる」という3つのアプローチにより、 SceneDelegate 対応を無事完了できました。 なかなか骨が折れましたが、 SceneDelegate に対応したことでマルチウィンドウに対応する土台ができ、AIエージェントを使って大規模な修正に取り組む経験も得られました。 この記事が、 SceneDelegate 対応を控えている方の一助になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは。Developer Engagementブロックの @wiroha です。2026年7月10日、11日に「 SRE NEXT 2026 」が開催されました。ZOZOはSilverスポンサーとして初協賛し、スポンサーブースを出展しました。 本記事ではZOZOから登壇したセッションと気になったセッションの紹介、協賛ブースの様子についてお伝えします! ZOZOエンジニアの登壇セッション ZOZOTOWNの進化と信頼性を両立する負荷試験 — 年十数回、現場の課題と効率化(LT) speakerdeck.com ZOZOTOWNの会員ID基盤では、年十数回の負荷試験を実施しています。新機能の追加やリプレイスなどプロダクトの進化が続く中で、信頼性をどう担保し続けるか — その要が負荷試験の運用です。しかし実施要否の検討・シナリオ作成・mock準備・想定RPSの見積もりなど、試験前に積み重なる判断と調整が、見えないコミュニケーションコストを生み、運用を試験を回すだけで精一杯の状態にしていました。 今回のLTでは、要否判定の属人化解消・シナリオ資産の使い回し・分析工程の仕組み化・結果のテンプレート化など、現場で有効だった運用改善と、これから取り組みたい効率化の方向性を共有しました。 登壇者からのコメント SRE部 会員ID基盤SREブロックの松石です。今回初めてのカンファレンス登壇となりました。このセッションでは、弊社で頻繁に行っているマイクロサービス負荷試験とその課題、改善について発表しました。 負荷試験は、ECサイトのようにスパイクが頻繁に起こりえるサービスでは重要な試験観点となってきます。一方で、マイクロサービスの依存が増えることやサービス要件により、観点や事前準備が日々複雑になっており試験を行う際の工数は無視できない規模になってきました。今回の発表で、新しい気づきや弊社の取り組みへの興味を持ってくださった方がいたら嬉しく思います。 また、発表を行うことで、ブースや懇親会で負荷試験について興味を持ってくださった方、似た悩みを抱える方と密な意見交換をできました。今回得た知見などは、チーム内でも展開し、サービスの信頼性向上に努めたいと考えています。 良く情報を発信する人間のところに情報は集まってくると言われますが、そのことを体感した2日間で非常に有意義な時間でした。 エンジニアが気になったセッションの紹介 サンプリングは統計学である:数理的根拠に基づき、オブザーバビリティのコストと精度を両立する 山口 能迪 speakerdeck.com SRE部 カート決済SREブロックの北島です。4月に新卒入社して、少しずつ業務に慣れてきました。リプレイスプロジェクトにサブとして参加しながら、Datadogのダッシュボードやトレースを確認したりという日々です。各マイクロサービスのデータがDatadog上で可視化されていますが、「これは本当に正しいものなのか」とどこか引っかかっていました。 このセッションでは、「全てのlogのデータを保持するにはコストがかかるため、サンプリングする。けれど『データを捨てる』という行為なので、捨てていい場合と捨ててはいけない場合がある。その判断基準が統計学にある」という内容が語られました。 具体的には、以下の4点が軸になっていました。 Datadogに表示されるメトリクスが推定値になりうること 推定値として読むためには許容誤差を先に決めてサンプリング率を逆算する必要があること 多段構成だと保持率が積み重なって補正が難しくなること メトリクスとトレースはそもそも責務が違うので分けて考えるべきこと この日のセッションで、自分の引っかかりの正体が少しわかった気がしました。 「Datadogに表示される数字は事実ではなく推定値かもしれない。この数値は本当に信頼できるのか?」を問えるSREになりたいと思いました。 数字を見るたびに「なぜ」を繰り返して、何か起きたときに根拠を持って動けるよう意識していきたいと改めて思いました。 CSに"SLO"は要らない、経営層に"99.9%"は伝わらない - SREを全社に"翻訳"する3原則 川崎 雄太 speakerdeck.com SRE部 会員ID基盤SREブロックの田中です。私が所属するチームでは現在SLOの策定を進めており、その目的・対象・運用方法について議論を重ねています。 その中で、SLOの設定や運用改善はSREだけで完結するものではなく、開発チームやプロダクトマネージャーをはじめとするさまざまな部署と連携し、共通認識を形成する必要があるため、その難しさを実感していました。 本セッションでは、組織にSREを浸透させる際に直面する課題を、1. 専門用語の壁、2. 温度差の壁、3. 完璧主義の壁の3つに整理し、それぞれを「翻訳レイヤー設計」「ビジネスインパクトへの変換」「役割別Include設計」というアプローチで乗り越えた事例が紹介されました。 特に印象に残ったのは、専門用語や指標を部署ごとの視点に合わせて再定義したり、それぞれの立場で必要となる情報に合わせてSREやSLOの価値を伝えたりする工夫です。技術的な正しさだけでなく、相手に伝わる形へ翻訳することが、組織全体でサービス信頼性を向上させるために重要であることを改めて認識しました。 SLOの策定に取り組んでいる現在の私たちのチームにとっても、多様なステークホルダーとの認識合わせはまさに直面している課題です。本セッションを通じて、SLOの運用に限らず、サービス信頼性向上を組織全体で推進するための具体的な考え方やアプローチを学ぶことができ、非常に参考になる内容でした。今後、より多くの部署と連携しながら活動を進めていく上で、ぜひ実践していきたいと感じました。 分散システム、なんですぐ死んでしまうん?耐障害性を高めたいあなたのためのレジリエンスパターン入門 ZOZOMO部SREブロックの蔭山です。私からは1日目の『分散システム、なんですぐ死んでしまうん?耐障害性を高めたいあなたのためのレジリエンスパターン入門』を紹介します。 speakerdeck.com 本セッションでは分散システムにおけるレジリエンスパターンについて、実際の取組事例を交えながら紹介されました。 レジリエンスパターンの手法についてTimeoutやRetry、Request Hedging、Circuit Breaker、Load Shedding、Graceful Degradationが解説され、それを実現するService Meshとライブラリそれぞれでの実装方法、ケーススタディとしてメルカリでのレジリエンス実践の実例も紹介されました。 私たちが担当しているシステムでは分散システムを扱うことが多く、TimeoutやRetryなどは普段の実装から意識しているものもありました。それでもTimeout PropagationやRetry Budgetなどは初めて知った概念でした。特にRetry Budgetは、Retryの回数を制御することで、システム全体の負荷を抑えつつ、サービスの可用性を高めることができるという点で非常に興味深かったです。 また導入を諦めていたService Meshの導入やCircuit Breakerの導入も、今回の実際にサービスを守れた事例を聞いて改めて検討する価値があると感じました。私が担当しているシステムでは、まだまだレジリエンスパターンの導入が十分ではないため、今回のセッションで学んだことを活かして、システムの耐障害性を高める取り組みを進めていきたいと思います。 ポストモーテム!DDoSからサイトは守れた。でもビジネスは守れなかった。 speakerdeck.com ZOZOMO部SREブロックの𠮷富です。本セッションでは、弁護士ドットコムが実際に直面したDDoS攻撃の事例が紹介されました。 同社は継続的なDDoS攻撃を受けましたが、WAFによる防御や関係部署の連携によってサービスのダウンタイムは発生せず、「可用性」を守り切ることができました。しかしその一方で、攻撃による大量のリクエストによりインフラコストが平常時の約40倍まで膨れ上がり、「事業」という観点では大きな損失が発生していました。 この事例で興味深かったのは、コスト急増への初動が遅れた原因です。日次のコスト急増アラートが、偶然にも毎月送られてくる予算アラートと似た金額だったため、担当者は「いつもの月末アラートだろう」と思い込み、異常に気が付きませんでした。また、毎月鳴る予算超過アラートが常態化し、形骸化していたことも見逃しにつながっていました。これは個人の不注意ではなく、人が見逃してしまうことを前提としたアラート設計になっていなかったことが根本原因でした。 また、エンジニアが「可用性」を優先しがちであるという課題も挙げられていました。システムを止めないことを重視する一方で、クラウドでは従量課金そのものが攻撃対象になり得るため、サービスを維持できていても事業としては大きな損失につながる可能性があります。そのため、「システムが動いているから大丈夫」とは言えず、コストも含めてサービス全体の健全性を捉える視点が必要であることを学びました。 このセッションを通して、SREは技術的な課題を解決するだけではなく、事業を守るためのコミュニケーションを担う存在でもあることを改めて認識しました。「どの時点でサイトを停止するのか」「誰が判断するのか」といった基準を、平時から関係者間で共有しておく必要があります。また、技術的な事象をコストやリスクといった経営の言葉に翻訳することが、迅速で適切な意思決定につながります。技術だけでなく、事業全体を見据えた視点を持つことの重要性を強く感じたセッションでした。 なぜ私たちのSREプラクティスはなかなか機能しないのか 〜システムより先に組織を見る〜 speakerdeck.com ZOZOMO部 SREブロックの中村です。私の所属するチームはSREとして立ち上がって約2年ほどです。 SREとは何か、SREとしてのベストプラクティスは何かについて輪読会を行いながら、チームとして日々正解を探し続けています。 オンコール体制整備、SLOの設定、オブザーバビリティの強化改善、SREとしてどのように価値や存在感を出していくのかを考え、実行してみるのですが、思った効果が得られなかったりする場合があります。 今までチームでやっていなかったSREとしての役割を果たそうとすると、あまりうまくいかず、とても悩んでいました。 このセッションでは、他組織の事例や書籍の内容通りにSREプラクティスを実践してもうまくいかないと感じている人向けに、なぜシステムより先に組織を見るべきなのか、組織を見るときに何をチェックすればよいのかを、事例とともに紹介されていました。 セッションの冒頭では、SREプラクティスを行ったときに起こる問題について話されていました。ポストモーテムを導入したりモニタリングを強化したりしても、SRE以外がポストモーテムを書かない、アラートやダッシュボードを作成してもメンテナンスをSREだけが行う状況が起こりえます。 その原因としては、SREが整備してもSRE以外に扱い方が浸透していなければ、そのまま誰も意識しないようになりますし、そのためにドキュメントを用意しても誰も読みたがらず、そのまま風化していきます。 このような問題は、SREプラクティスをやりたいという気持ちだけで進めてしまうことで起こると、セッションを通して認識できました。 SREプラクティスを機能させたいのであれば、自組織の文化や価値観がすでに形成されていることを前提に、それをしっかり守るための手段としてSREプラクティスを設計する必要がある、という話にとても納得感を得られました。また、SREチーム内だけで完結する内容はSREプラクティスとして成果が見えやすい一方、うまくいかないと感じるときはSREチーム外の関係者が必要な領域に踏み込んでしまっている、という話がありました。 今後SREとして何か実践するときには、ただシステムを見るだけでなく、「組織の文化はどうなっているのか」「それを守るためには何が足りていないのか」「足りていないものを補うには何を追加すべきか」「追加する際の影響はどの範囲まで及ぶのか」を意識したうえで、SREチームとしてたくさんチャレンジしていきたいと思いました。 誰のためのReliability? SRE部カート決済SREブロックの伊藤です。2日目の基調講演『誰のためのReliability?』を紹介します。 speakerdeck.com この講演はデザインリサーチの専門家であるアンカーデザイン株式会社の木浦幹雄さんが「SREの言うユーザーとは誰なのか」を正面から問うものでした。 デザインの世界では、ユーザーを属性だけでなく「属性・文脈・目的」の組み合わせで捉え、それを「ペルソナ」としてチームの共通理解にするそうです。一方で木浦さんいわく、SREの本を何冊読んでもペルソナという言葉は出てこなかったとのことでした。SREは「ユーザー」という漠然とした言葉のまま、誰がどんな状況で何のためにサービスを使っているのかを具体化できていないのではないか、という問いかけが印象に残りました。 私が担当しているZOZOTOWNのカート決済機能は、止まればユーザーの購入がそのまま止まってしまう、ECサイトの根幹となる機能です。セールやクーポン期限前の駆け込みなど、注文が集中するタイミングはデータとして把握しています。ただそれを「負荷の波」として見るだけでは足りず、「期限内にクーポンを使い切りたい人」「セール開始と同時に狙った商品を確保したい人」の活動として捉え直すと、見えるものが変わります。同じ稼働率99.9%でも、平日の昼間と、「今買えないと機会を逃す」人が殺到するセール終了間際とでは、下回ったときにユーザーの活動へ与える影響は大きく違うはずです。 こうした重みの違いまで含めて可視化し、SLOの根拠として語れるようになれば、「誰のためのSLOか」に答えられるようになるのではないかと思いました。 もう1つ考えさせられたのは、この問いはサービスの利用者だけでなく、一緒に働く開発者にも向けるべきだ、ということでした。SREが用意する監視の仕組みや運用ルールは、開発者にとってのプロダクトでもあります。その提案は開発者が望むものと本当に合っているのか。どんな状況で何のために使われるのかを確かめずに、良かれと思って作っていないか。「ユーザー像は、妄想で決めない」というデザインの教訓は、社内の開発者に対しても当てはまるはずです。「誰のためか」を問い続けることが、SREの仕事をより確かなものにするのだと気づかされた講演でした。 End-to-Endで考える信頼性 — LINEアプリにおけるクライアント開発×SRE連携の実践 speakerdeck.com SRE部 会員ID基盤SREブロックの中根です。私の所属ブロックでは、ZOZOTOWNの会員情報やログインに関する機能のSRE業務を担当しています。機能単体の信頼性向上には深く取り組めている一方で、End-to-Endでの信頼性は見えづらいという課題感があります。 こちらのセッションは、障害発生時にユーザー操作へ影響を出さないための設計と運用について、End-to-Endでの取り組みを解説する内容でした。ユーザーが操作した際に「画面が真っ白な状態」を絶対に避けるべき状態として定義し、オフラインファーストで吸収する取り組みが印象的でした。 クライアントでのキャッシュの取り扱いとして指数バックオフによるリトライ制御や部分障害時の選択的リトライ、負荷過多時の優先制御、クライアント視点のログの継続的な分析など、過負荷を避けつつ体験を守る工夫が具体的に紹介されていて学びが多いセッションでした。 また、そういった対応を実践するためには各チームが横断的に取り組むことが何より重要である点や、その文化を醸成するには時間を要する点が紹介されていました。 アプリケーションとバックエンドは組織も要素技術も異なる場合が多く、関連ブロックが増えるほど横断的な連携は難しくなりがちです。 その中で、早い段階からサーバー・クライアント・SRE・QAが一丸となり、仕様やエラーケースを議論する取り組み方は大変参考になりました。私も組織や機能にとらわれず、お客様のより良い体験のために機能や部門の垣根を越えた改善が行えるように取り組んでいきたいです。 ZOZOブースの紹介 LTでの発表内容にあわせて、スポンサーブースでは負荷試験の構成図を中心に展示しました。 「負荷試験に取り組みたいがまだ実践できていないため参考にしたい」といった参加者の方々にも多くお立ち寄りいただき、設計や運用に関する質問など活発な意見交換が行われました。また、初めての協賛ということもあり、ZOZOについて知っていただけるようZOZOTOWNのシステム構成図もご用意しました。こちらも注目を集めていました。 また、ノベルティとして「シューズ用クリーナー消しゴム」を配布しました。スニーカー等のソールの汚れをこすって落とせる便利アイテムです。 「こんなグッズがあるんだ!」「便利!」と好評いただき、ファッションを軸に事業を展開するZOZOらしさも感じていただけたのではないかと思います。 おわりに SRE NEXTへの初協賛を通して、ZOZOのことが少しでも来場者のみなさまに伝わっていれば嬉しいです。ありがとうございました! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、SRE部 フロントSREブロックの秋田と池田です。普段はリプレイスプロジェクトをはじめ、BFF・WebアプリケーションやオンプレのWebサーバーなど、ユーザー体験に関わるシステムの運用保守に携わっています。 ZOZOTOWNでは長年、オンプレミスのDNSアプライアンスとして Infoblox を利用してきました。しかし、7年間運用してきたInfobloxがEOL(End of Life)を迎えることとなり、リプレイスを検討することになりました。 本記事では、Infobloxのリプレイス検討から廃止に至るまでの課題をご紹介します。あわせて、Akamai Edge DNSのPrimary昇格・Akamai SIA導入・TerraformによるIaC化という3つのアプローチで課題を解決した取り組みについても触れます。 目次 はじめに 目次 既存のDNS構成と課題 既存構成 課題1:コスト面 - ライセンス費用の高騰 課題2:運用面 - ライセンス更新・脆弱性対応・バージョンアップ 課題3:管理面 - ゾーン管理の非コード化 課題4:チーム面 - 運用の属人化 課題解決のアプローチ 既存構成とAkamai Edge DNS Primary昇格の比較 施策1:Akamai Edge DNS - Secondary → Primary昇格 施策2:Akamai SIA導入 - キャッシュDNSの置き換え PoC(概念実証)の実施 DNSレベルの脅威検知によるセキュリティ強化 施策3:Terraform化 - ゾーン管理のIaC移行 稼働中ゾーンのTerraform import GitHub ActionsによるCI/CDパイプラインの整備 手順書レビューからPRベースレビューへの転換 Infoblox廃止に向けたクローズ作業 まとめ 最後に 既存のDNS構成と課題 既存構成 リプレイス検討前のDNS構成は以下の通りです。 権威DNS(Primary) : Infoblox(オンプレミス) 権威DNS(Secondary) : Akamai Edge DNS キャッシュDNS : Infoblox(オンプレミス) ZOZOTOWNではInfobloxをHidden Primary(NSレコードに公開しない権威DNS)として長年利用していました。AkamaiはInfobloxからゾーン転送を受けるSecondaryの役割を担いながら、インターネット側に公開されているためユーザーからの名前解決にも応答していました。オンプレのActive Directoryで使われるキャッシュDNSはInfobloxを利用していました。 この構成はInfobloxを導入した当初は問題なく運用していましたが、7年間の運用をする中で複数の課題が浮き彫りになってきました。 課題1:コスト面 - ライセンス費用の高騰 昨今の急激な円安の影響もあり、ライセンス更新のたびに費用が大きく膨らんでいました。さらにEOLに伴う機器リプレイスとなれば、ハードウェアの調達・入替コストも新たに発生します。 同等のInfoblox機器へリプレイスするという選択肢では、こうしたコスト面の課題がそのまま継続するため、根本的な解決にはなりませんでした。 課題2:運用面 - ライセンス更新・脆弱性対応・バージョンアップ Infobloxの運用では、以下のような定期作業が繰り返し発生していました。 ライセンス更新対応 : 年次でのライセンス更新作業とライセンス費用管理 脆弱性対応 : InfobloxはBINDベースであり、BINDの脆弱性が発見されるたびに対応が必要 バージョンアップ対応 : セキュリティパッチやバグフィックスのためのアップデート作業 どれも定期的に発生する作業で、特にBINDベースゆえの脆弱性対応は予告なく緊急対応が必要になるケースもあり、計画外の工数が積み重なっていました。 課題3:管理面 - ゾーン管理の非コード化 InfobloxのゾーンはInfobloxの管理コンソールを通じた手作業での運用となっており、設定変更の経緯が社内の申請と管理コンソールのコメントでしか追えない状態でした。具体的には以下のような問題がありました。 変更履歴が社内申請と管理コンソールのコメントのみで、いつ・誰が・なぜ変更したかを追うことが困難 手作業でのレコード追加・変更による作業ミスのリスク 手順書ベースの作業かつ手順書レビューの作業準備コスト ゾーン設定の現状把握がInfobloxにアクセスしないとできない IaC(Infrastructure as Code)が当たり前になった環境において、DNSゾーン管理だけが手作業のまま取り残されている状態でした。 課題4:チーム面 - 運用の属人化 ZOZOTOWNのマイクロサービス化に合わせてチーム体制の最適化が進む中、Infobloxの運用もフロントSREブロックで担当するようになりました。しかし、チーム内でInfobloxの運用経験を持つメンバーは1人だけという状態が続いており、運用の属人化が課題となっていました。 属人化が進むと、不在時に対応できるメンバーを確保できなくなるリスクや、人員変動に伴う引き継ぎコストの増大といった問題を招きます。チームとして持続可能な運用体制を整えるために、属人化の解消が急務となっていました。 課題解決のアプローチ 既存構成とAkamai Edge DNS Primary昇格の比較 リプレイスの選択肢として、まず「同等のInfoblox機器へのリプレイス」と「Akamai Edge DNSをPrimaryに昇格してInfobloxを廃止する」の2案を比較検討しました。 比較の結果、Akamai Edge DNS Primary昇格案は以下の点で優位でした。 観点 Infobloxリプレイス Akamai Edge DNS Primary昇格 ライセンスコスト 継続(高騰リスクあり) 不要(既存Akamaiの活用) ハードウェアコスト 機器調達が必要 不要 脆弱性対応 BINDベースの定期的な対応が継続 Akamaiのマネージドサービスに委譲 ゾーン管理 手作業継続 Terraform化で解消 キャッシュDNS Infobloxのまま Akamai SIAで代替(別途費用が発生) Akamaiは既にSecondaryとして動いており、ゾーンデータはそのまま引き継げました。Akamai SIAの利用には新たに費用が発生するものの、ライセンス・ハードウェア・運用コストの削減や運用品質の向上といった優位点が大きく上回りました。 これらの比較結果をもとに、 AkamaiをPrimaryに昇格してInfobloxを廃止する 方針を決定し、以下3つの施策を実施しました。 施策1:Akamai Edge DNS - Secondary → Primary昇格 まず、Akamai Edge DNSをSecondaryからPrimaryに昇格させ、Infobloxからのゾーン転送をなくしました。 Infoblox(Primary)→ Akamai(Secondary)という構成から、Akamai単体を権威DNSのPrimaryとする構成への移行です。 移行にあたっての主な作業は以下の通りです。 AkamaiのゾーンをSecondaryからPrimaryへ変更 Secondaryとして利用していたため実際にゾーン情報を持っていることもあり、AkamaiのコンソールからSecondaryをPrimaryに切り替える作業のみで済みました。また、切り戻しの際もPrimaryからSecondaryに戻してゾーン転送の設定を再設定するだけで済むため、作業工数をかけずに移行が実現できました。 施策2:Akamai SIA導入 - キャッシュDNSの置き換え InfobloxのCache DNS機能を Akamai SIA(Secure Internet Access) で代替しました。 Akamai SIAはDNSフォワーダーとして機能するだけでなく、 DNSレベルでの脅威検知・ブロック 機能も提供します。従来のInfobloxのキャッシュDNSにはなかった機能で、セキュリティ面での強化も同時に実現しました。 PoC(概念実証)の実施 本番環境への適用前に、STG(ステージング)環境で3週間のPoCを実施しました。 PoCの確認項目は以下の通りです。 Akamai SIAのアクセス制限 名前解決の動作確認(社内ドメイン・外部ドメイン) 既存のDNS依存アプリケーションへの影響確認 手動切り戻し手順の確認 3週間のPoC期間中に問題は発生せず、本番環境への適用に移行しました。 DNSレベルの脅威検知によるセキュリティ強化 Akamai SIAでは、マルウェアのC2(Command and Control)サーバーやフィッシングサイトへのDNSクエリをブロックする機能を持っています。 Infobloxのキャッシュ置き換えに加えて、このセキュリティ機能を獲得できたことは想定外のプラスαでした。 施策3:Terraform化 - ゾーン管理のIaC移行 課題3として挙げたゾーン管理の手作業運用を解消するため、Terraformを用いてAkamaiのDNSゾーンをIaC管理する仕組みを整備しました。 稼働中ゾーンのTerraform import Akamaiには既にゾーンが存在しているため、影響範囲が小さいものから段階的に、既存ゾーンのTerraform importを始めました。 terraform import akamai_dns_zone.example example.com 全ゾーンを一括importするスクリプトを作成し、既存ゾーンをTerraform管理下に置きました。import後はtfstateと実際のゾーン設定の差分を確認し、コードと実態の一致を検証しました。 GitHub ActionsによるCI/CDパイプラインの整備 import完了後、以下のパイプラインをGitHub Actionsで整備しました。 PRを作成 → terraform plan → レビュー → マージ → terraform apply 各ステップの詳細は以下の通りです。 terraform plan(PR時) PRを作成するとGitHub Actionsが自動でterraform planを実行 planの結果はPRコメントへ出力され、レビュアーが変更内容を確認できる状態になる terraform apply(マージ時) mainブランチへのマージをトリガーにterraform applyを実行 Akamaiへの変更が自動で反映される 手順書レビューからPRベースレビューへの転換 Terraform化により、ゾーンの変更オペレーションが以下のように変わりました。 変更前 変更後 Infoblox GUIで手作業変更 tfファイルを編集してPRを作成 手順書をレビュー PRをレビュー 変更履歴が残らない Gitのコミット履歴として残る 変更の影響範囲が手順書に依存 planで変更内容が定量的に把握できる コードレビューを通じて変更の意図と影響範囲を確認できるようになり、ヒューマンエラーの削減と変更の透明性向上を同時に実現しました。 Infoblox廃止に向けたクローズ作業 施策1〜3が完了した後、Infobloxを安全に廃止するためのクローズ作業を実施しました。 Infobloxに依存しているクライアントが残っている状態で廃止してしまうと障害につながります。移行済みかどうかを地道に確認しながら、以下の順序で進めました。 キャッシュDNSの向き先変更 :各クライアントのDNSフォワーダー設定をInfobloxからAkamai SIAへ変更 ゾーン転送の停止確認 :Infoblox→Akamaiへのゾーン転送が停止していることを確認 権威DNS問い合わせ確認 :外部からの問い合わせがInfobloxを経由していないことを確認 監視の切り替え :Infoblox関連のモニタリング・アラートの削除 機器のシャットダウン・撤去 :一定期間の様子見後、機器の電源を落として廃止 クローズ後も一定期間はリバートできる状態を維持し、問題がないことを確認した上で機器を撤去しました。 まとめ 7年間運用してきたInfobloxの廃止とAkamaiへの移行を通じて、コスト・運用・管理・チームの4つの課題を同時に解決できました。 コスト削減 :Infobloxライセンス費用・保守サポート費用・EOLに伴うハードウェア調達費用が不要になりました 運用負荷の低減 :BINDベースの脆弱性対応やライセンス更新といった定期作業がなくなりました 管理品質の向上 :ゾーン変更がPRベースのレビュープロセスに変わり、変更履歴が残るようになりました セキュリティ強化 :Akamai SIAによるDNSレベルの脅威検知も獲得できました 属人化の解消 :SaaS移行によりInfoblox固有の知識がなくても運用できる体制となり、引き継ぎ負担を軽減できました EOLへの対応はどうしても後ろ向きな作業に見えがちですが、今回は「機器交換」で終わらせず、積み上がっていた課題を整理する機会として捉えられチーム状況を考えながら最適化できたことはとてもよかったです。同様の状況を抱えているチームの参考になれば幸いです。 最後に ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、コーポレートエンジニアリング部ITサービスブロックの高塩です。2026年2月に入社し、社内の共通システムやIdP、SaaSの管理、日々のオペレーション自動化などを担当しています。 ZOZOでは入社者のマスタデータをkintoneで管理しています。入社のたびに、Active Directory(以下、AD)を起点にMicrosoft 365・Google Workspace・Boxなどでアカウントを作成し、利用できる状態まで整えています。この作業は長らく手作業で運用してきましたが、工数や属人化、そしてCSVの受け渡しに課題を抱えていました。 オンプレADやLDAPサーバーを長く運用してきた組織では、それが自動化の足枷になっているケースも少なくないと思います。かといって、すべてをクラウドへすぐに寄せられるとも限りません。本記事では、この一連のアカウント作成を、kintoneを起点にGitHub Actions(self-hosted runner)で自動化した取り組みを紹介します。閉域にあるオンプレADの操作までGitHub Actionsに載せ、コードレビュー・実行ログ・再現性といったCI/CDの利点をアカウント運用に持ち込みました。 目次 はじめに 目次 背景:手作業時代のフローと課題 全体アーキテクチャ オンプレADをGitHub Actionsから操作する kintoneから翌月の入社予定者を取得し、業務ロジックで判定してADに作成する 同期待ちを能動的に潰して即時反映する AD → Entra ID:デルタ同期を要求して完了までポーリング Entra ID → SaaS:Graph APIのオンデマンドプロビジョニング 運用:前日にdry-runで予定を流し、翌日に本番で作成する 導入の効果 まとめ 背景:手作業時代のフローと課題 自動化前のアカウント作成は、おおよそ次のような流れでした。 GASでkintoneから入社者のレコードを取得する スプレッドシートに転記し、関数で表示名やグループなどの値を加工する 加工した結果をCSVでエクスポートし、オンプレミスの作業サーバーへ手動で転送する 作業サーバー上で複数のPowerShellスクリプトにCSVを読み込ませ、順次実行してアカウントを作成する 途中、Microsoft Entra Connectのデルタ同期を手動で実行し、各SaaSへの反映を待つ 反映後、各SaaSの個別設定を、設定用スクリプトの実行や手作業で投入する 一見すると回っているように見えますが、運用を続けるうちに次のような課題が積み重なっていました。 業務ロジックが散在している :誰が・どの雇用形態で・どのグループに入るのか、その判断ロジックがスプレッドシートの関数と作業サーバー上のスクリプトに分散していました。そのため全体像の把握が困難でした。 手作業でのCSV転送 :スプレッドシートからCSVをエクスポートし、作業サーバーへ人手で転送していました。 ログの視認性が低い :実行は作業サーバーのコンソールで行うため、いつ・誰が・何を流したのかが後から追いにくい状態でした。 スクリプトのドリフト :Gitリポジトリ上のスクリプトと、作業サーバーに置かれた実際に動くスクリプトが少しずつ乖離していき、「リポジトリのコード=実際に動いているコード」と言い切れなくなっていました。 これらをまとめて解決するために、フロー全体を作り直すことにしました。 全体アーキテクチャ 新しい仕組みでは、データはkintoneから直接取り、全工程をGitHub Actionsから動かすことにしました。 手作業のころから大きく変えたのは、次の3つです。 CSVの受け渡しをやめ、kintone APIを直接呼び出す :人の手によるエクスポートと加工を排除しました。 散在していた業務ロジックをコードに集約する :判断ロジックをすべてPowerShellスクリプトにまとめ、Gitリポジトリで管理するようにしました。 オンプレADの操作にself-hosted runnerを使う :GitHub Actionsの仕組みに乗りながら、オンプレミスのADコマンドレットをそのまま実行できるようにしました。 ワークフローは schedule によるcron実行で動きます。kintoneからはデフォルトで翌月の入社者を取得するので、毎月決まった日に翌月分の作成予定をSlackへ自動投稿し、その翌日にまとめて作成する、という運用にしました。 ここからは、土台となるself-hosted runner、kintoneからのアカウント作成、クラウドへの即時反映を、順に見ていきます。 オンプレADをGitHub Actionsから操作する この仕組みで一番頭を悩ませたのが、オンプレミスのADをどう自動化に組み込むかでした。 ADはオンプレミスの閉じたネットワークの中にあります。操作するには、次の3つが必要です。 ドメインコントローラへ通信できるネットワーク(オンプレ内)にいること リモート サーバー管理ツール( RSAT )の ActiveDirectory モジュールが入った実行環境 対象OUでユーザー作成とグループ追加ができるドメイン権限 最初に検討したのは、この処理を自前でオーケストレーションせず、Entra ID Governanceのようなマネージド機能に寄せる形でした。たとえばオンプレADへのユーザー作成は、 API 駆動型インバウンド プロビジョニング に任せられます。 ただ、今回の要件には合いませんでした。アカウント作成にはオンプレADのグループ操作が含まれますが、属性ベースの動的グループが扱うのはクラウドのグループで、オンプレADのグループメンバーシップまでは管理しません。ここはマネージド機能だけでは実現できません。 さらに、出し分けの業務ロジックは、手作業時代のPowerShellスクリプトへある程度作り込まれていました。これを一から組み直すより、既存の資産をそのまま流用したい事情もありました。そこで今回は、自前でオーケストレーションする方針にしました。 自前で組むなら、ADを操作する方法はいくつかあります。検討した選択肢の長所と短所を、次の表にまとめました。 実行方式 長所 短所・見送り理由 タスクスケジューラ+スクリプト 追加インフラが不要 時刻起動のみ・コードがサーバーに残りドリフト再発・ログやSecrets、レビューが弱い 自前でAPIを建てる オンデマンド実行・自由度が高い 着信ポートの開放が必要・認証や可用性、監視を自前運用 Azure Automation(Hybrid Runbook Worker) Power Automate/Microsoft Formsから起動しやすい・Azure/M365中心の組織に馴染む GitのPR/CIと一体化しにくい・別基盤の学習/運用コスト self-hosted runner(採用) 既存のGitHub・PR・CIにそのまま乗る・チェックアウトでドリフトなし・手動/定期の両対応 runnerの保守が必要・信頼できるコードのみ実行する配慮が要る 自前API以外の3方式は、オンプレからのアウトバウンド通信だけで動作し、外部に着信ポートを開ける必要がありません。 最終的に self-hosted runner を選びました。ADドメインに参加したWindows Server上でrunnerを動かし、PowerShellを実行しています。 コード・トリガー・ログがすべてGitHub側へ移り、サーバー上では実行時にチェックアウトされた最新のスクリプトが動くだけです。背景で挙げたドリフトは、これで自然に消えます。 ほぼ同じ仕組みは、AzureのHybrid Runbook Workerでも実現できます。私たちはGitHubを使った業務フローが根付いていたため、self-hosted runnerを選びました。 ただし、この構成では強い権限を持つrunner自体が攻撃対象になります。runnerを侵害させないことと、万一侵害されても被害を広げないことの両方に注意を払う必要があります。 runnerに任意のコードを実行させる入口はGitHubなので、次のような基本的な制御を入れています。 リポジトリをprivateにし、権限は必要なメンバーだけに限定する ログインはSSOを必須とし、MFAの登録を義務づける mainはbranch protection ruleでCODEOWNERのレビュー承認を必須にする 認証は原則OIDC(Workload Identity連携)を使い、やむを得ずシークレットを使う場合もEnvironmentシークレットに置いてmainのワークフローからのみ参照できるようにする self-hosted runnerのサービスを動かすアカウントに過剰な権限を与えないことも重要です。runnerはオンプレADからクラウドまで触れる強い実行主体なので、万一侵害されたときの影響範囲を抑えるため、アクセスできるOUを絞るなど、必要最低限の権限だけを与えています。 kintoneから翌月の入社予定者を取得し、業務ロジックで判定してADに作成する 土台ができたら、次はアカウントを作る処理です。 まず取得です。kintoneの Cursor API を使い、クエリで対象者を絞り込みます。条件は3つで、入社月・入社区分(中途や新卒など)・雇用形態(社員やアルバイトなど)です。対象月は、定期実行では NEXT_MONTH() で翌月分を自動的に対象にします。 # 入社区分・雇用形態・入社月で対象者を絞り込む $categoryFilter = 'Category in ("中途", "新卒", "社員登用", ...)' $contractFilter = 'Contract in ("社員", "出向", "CSアルバイト", ...)' $query = " $categoryFilter and $contractFilter and Date = NEXT_MONTH() order by Date desc" # カーソルを作成し、レコードを取得し切るまで繰り返す $cursorId = ( Invoke-RestMethod -Uri $cursorEndpoint -Method Post -Headers $headers -Body $utf8Body ).id $nextCursorId = $cursorId while ( $nextCursorId ) { $res = Invoke-RestMethod -Uri " $cursorEndpoint `? id= $nextCursorId " -Method Get -Headers $getHeaders $allRecords += $res .records $nextCursorId = $res .next } CSVのエクスポートと手動転送がなくなり、「どんな条件で対象者を抽出しているか」がクエリを見れば分かるようになりました。 ただ、取得したデータをそのままADに流し込めるわけではありません。雇用形態や所属によって、どのOUに作り、どのグループに入れるかが変わります。以前はこの出し分けがスプレッドシートの関数に埋もれていましたが、今回コード側の分岐に移しました。判定しているのは、たとえば次のような内容です。 雇用形態に応じた、アカウントの配置先OUの決定 所属に応じた、各種グループへの追加 その他のユーザーアカウント属性の設定 判定が終わったら、self-hosted runnerの上で ActiveDirectory モジュールのコマンドレットを実行し、ADにアカウントを作成します。あとは次に述べる同期で、クラウド側へ反映されていきます。 同期待ちを能動的に潰して即時反映する ADにユーザーを作っただけでは、クラウド側のアカウントはすぐには使えません。間に2つの同期が挟まるためです。 1つ目はAD → Entra IDです。オンプレADのユーザーは、Microsoft Entra Connectが同期して初めてEntra ID側に現れます。この同期は通常、30分間隔のサイクルでしか走りません。 2つ目はEntra ID → 各SaaSです。Entra IDからGoogle WorkspaceやBoxへは、エンタープライズアプリのプロビジョニングで連携されます。この自動プロビジョニングのサイクルは最大40分待つことがあります。 手作業のころは、この待ちを人がこなしていました。同期サイクルが回るのを待つか、待ちきれなければデルタ同期を手動で叩き、クラウドにアカウントが現れたのを確認してから次の設定に進む、という具合です。 ところが、これをそのまま自動化に持ち込むと厄介でした。AD作成の先には、Entra IDのライセンスやグループ設定、Google WorkspaceやBoxへのプロビジョニング、その後の各SaaS個別の設定が続きます。どれも前の結果を前提にした処理です。定期サイクル任せだと全部終わるまでに1時間近くかかるうえ、まだ存在しないアカウントを触って空振りするステップも出てきます。 そこで、2つの同期をワークフローから自分でトリガーし、完了を見届けてから次へ進むようにしました。 AD → Entra ID:デルタ同期を要求して完了までポーリング 1つ目の同期は、Microsoft Entra Connectの同期サーバーに対してデルタ同期を要求し、完了するまで待ちます。runnerから Invoke-Command で同期サーバーに入り、 Start-ADSyncSyncCycle でデルタ同期を開始したあと、同期が進行中でなくなるまでポーリングします。 Invoke-Command -ComputerName $syncServer -ScriptBlock { Import-Module ADSync Start-ADSyncSyncCycle -PolicyType Delta | Out-Null # 同期が完了するまで待つ do { Start-Sleep -Seconds 10 $inProgress = ( Get-ADSyncScheduler ).SyncCycleInProgress } while ( $inProgress ) } ポーリングで完了を待つので、後続の処理に進む時点では、新入社員が必ずEntra ID側に存在します。 この Invoke-Command によるPowerShell Remotingは、runnerが侵害されたときに同期サーバーへの横移動の足がかりになりかねません。そこで、同期サーバーへ接続できる元をrunnerに限定し、接続に使うアカウントの権限も同期の実行に必要な分だけに絞っています。 Entra ID → SaaS:Graph APIのオンデマンドプロビジョニング 2つ目の同期は、Entra IDの自動プロビジョニングのサイクルを待たず、Graph APIの provisionOnDemand を使ってユーザー単位で即座にプロビジョニングをトリガーします。 このリクエストには、対象アプリのサービスプリンシパルID( $spId )・同期ジョブID( $jobId )・同期ルールID( $ruleId )の3つが必要です。ただ、こちらで用意するのは1つ目だけです。 $spId はエンタープライズアプリの「オブジェクトID」で、Entra管理センターのアプリのプロパティ画面に表示されます。 残りの $jobId と $ruleId は、実行時に $spId からGraphで取得します。ユーザーの同期ルールはアプリごとに1つしかないので、同期ジョブのスキーマを覗けば、それがそのまま使うルールです。IDを手で管理する必要はありません。 # $spId から同期ジョブを取得(Quarantine以外を優先) $jobs = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/ $spId /synchronization/jobs" $jobId = ( $jobs .value | Where-Object { $_ .status.code -ne "Quarantine" } | Select-Object -First 1 ).id # ジョブのスキーマから、ユーザーが対象の同期ルールを選ぶ $schema = Invoke-MgGraphRequest -Method GET ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/ $spId /synchronization/jobs/ $jobId /schema" $ruleId = ( $schema .synchronizationRules | Where-Object { $_ .objectMappings.sourceObjectName -contains "User" } | Select-Object -First 1 ).id あとは、取得した $jobId と $ruleId 、そしてプロビジョニング対象のユーザーを指定してリクエストを投げます。 $body = @{ parameters = @( @{ ruleId = $ruleId subjects = @(@{ objectId = $userObjectId ; objectTypeName = "User" }) } ) } Invoke-MgGraphRequest -Method POST ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/ $spId /synchronization/jobs/ $jobId /provisionOnDemand" ` -Body ( $body | ConvertTo-Json -Depth 10 ) ` -ContentType "application/json" この2つで同期待ちを潰すと、AD作成・各SaaSへのプロビジョニング・その後のSaaSごとの設定までが一本につながります。プロビジョニングのサイクルを待たずに済むので、後続の設定も同じ実行の中で流し込めます。終わった時点で、結果がSlackとジョブログに残ります。 運用:前日にdry-runで予定を流し、翌日に本番で作成する 毎月の作成は、dry-runと本番の2段階で回しています。 前日に走るのはdry-runです。AD作成やプロビジョニングといった変更は実際には行わず、「何をするか」(翌月入社者の作成予定)をSlackに流します。チームはこの通知で対象者を確認できるので、kintoneのデータに不備があっても、本番の前に気づけます。 翌日に本番が走ります。処理の開始から各ステップ、完了までを1本のSlackスレッドに流し、完了サマリーと、失敗したときのエラーだけはチャンネルにもブロードキャストして気づけるようにしています。作業サーバーのコンソールを覗かなくても、SlackとGitHub Actionsのログだけで実行状況を追えます。手作業時代に困っていた「ログの視認性の低さ」は、これで解消しました。 もう1つ効いているのが冪等性です。アカウント作成は既存ユーザーをスキップし、グループ追加も「すでにメンバー」を無視します。途中のステップが失敗しても、直して同じワークフローを流し直せば、できているところはそのまま、足りないところだけが埋まります。 また、プロビジョニングの後に各SaaSで行う個別の設定は、SaaS側の状態によっては失敗することがあります。これらは失敗しても全体を止めず、結果をSlackに残して先へ進む設定にしています。気づいたら、その部分だけ後で流し直せば済みます。 導入の効果 一番大きいのは、毎月の新入社員アカウント作成がほぼ無人で回るようになったことです。これまで人がCSVを受け渡し、スクリプトを手で実行し、同期を待っていた一連の作業が、1回のワークフロー実行にまとまりました。 業務ロジックもコードに集まり、「どの条件で何が付与されるか」をリポジトリで追えます。リポジトリのコードと実際に動くコードが一致するので、ドリフトも起きません。 まとめ 本記事では、新入社員のアカウント作成を、kintoneを起点にGitHub Actionsで自動化した取り組みを紹介しました。オンプレADの操作はself-hosted runnerで担い、散らばっていた業務ロジックはコードにまとめ、同期待ちはGraph APIのオンデマンドプロビジョニングで潰しました。手作業で属人化していた運用を、コード化して実行を追える形にできました。 オンプレミスとクラウドが混在する環境でアカウント運用を自動化したい方の参考になれば幸いです。今後は、退職時のアカウント無効化など、入社から退職までのライフサイクル全体へ自動化を広げていく予定です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、データシステム部MA推薦ブロックの佐藤( @rayuron )です。私たちは、主にZOZOTOWNのメール配信のパーソナライズなど、マーケティングオートメーションに関するレコメンドシステムを開発・運用しています。 以前、テックブログで工数やAI活用による工数削減を計測する仕組みを、タスク管理ツールの GitHub Projects で構築する方法をご紹介しました。 前回の記事 が計測の仕組みの作り方を扱ったのに対し、本記事では仕組みを1年間運用して得られた結果とその考察、そして今後の展望をご紹介します。 目次 はじめに 目次 背景と課題 計測の仕組みと運用 計測の仕組み 運用の整備 結果と考察 AIによる工数削減とAI活用率の推移 作業内容別の効率 定性的な変化 体感と実測のギャップ 今後の展望 Spec: 仕様の明文化 Eval: 成果物の品質の評価 AIが自律的に動く状態の構築 ソウゾウのナナメウエの創出 おわりに 背景と課題 昨今のAIの進化は目覚ましく、私たちの業務でもAIを活用する機会が増えています。AIを使うことで、業務の工数削減や品質向上など、多くのメリットを享受できます。特に工数削減については、「タスクを捌く時間がなんとなく短くなった」という体感がありました。一方で、実際どれだけ効果があったのかは把握できていませんでした。そこで、私たちはAI活用の効果のうち工数削減を計測することにしました。 AIによる工数削減を測るには、AIを使った時と使わなかった時の工数を比較する必要があります。しかし、現実にはある1つのタスクに対してAIを使う場合と使わない場合を同時に観測できません。AIの利用回数やPull Request(以下PR)のマージ数などの指標は取得できるものの、これらは活動量を示すものであり、工数削減に「どれだけ効果があったか」を直接測るものではありません。また、MLチームの業務にはモデル開発の実験・ドキュメント作成・プロジェクトマネジメント業務・登壇準備など、PRにならない業務が多く、PRベースの計測ツールでは網羅的に計測できませんでした。 AIの効果が測れないままでは、投資対効果を説明できませんし、どのパターンの業務がAIと相性が良いのかも感覚的にしか分からず、改善の方向性も定まりません。そこで私たちは、効果を測る仕組みを作り、得られたフィードバックをもとにAI活用の改善を進めることにしました。 計測の仕組みと運用 まず取り組みを見ていただく前に、私たちの通常業務を紹介します。後述する結果はチームのタスクの内容に大きく依存するため、先に知っていただくと結果を解釈しやすくなると思います。 私たちMA推薦ブロックは、エンジニア3人の小規模チームです。メール・LINE・Pushなどの配信をパーソナライズするレコメンドシステムを開発・運用しており、業務例は以下の通りです。 施策起案/要件定義:施策インパクトの事前分析・要件定義 システム/モデル設計:推薦モデル・システムの設計 システム/モデル実装:推薦モデル・システムの実装、テスト、デプロイ 効果検証:A/Bテストダッシュボード作成、効果検証レポーティング作成 運用保守:オンコール対応、パッケージ更新、問い合わせ対応 その他:プロジェクトマネジメント、チームの仕組み作り、目標設定、勉強会、登壇など 開発にはGitHubを使用しており、ドキュメント管理には Confluence を使っています。AIツールとしては、 Claude Code ・ Codex ・ Devin などを必要に応じて使い分けており、メインではClaude Codeを使っているメンバーが多いです。 計測の仕組み 背景と課題で述べたとおり、同一タスクにおいてAIを使った場合と使わなかった場合を同時に観測できません。そこで、タスクの担当者にAIを使わなかった場合の想定工数を見積もってもらい、実際にかかった工数との差を「AIによる削減工数」として記録することにしました。 具体的には以下の仕組みを作成し運用しています。 全タスクをGitHub Issueで管理しGitHub Projectsに紐付け、カスタムフィールド「AI削減工数」「AI活用の成功・失敗事例」に記録する GitHub ProjectsのデータをBigQueryへ毎日自動エクスポートし、ダッシュボード上で可視化する 全体像は以下の通りです。記録したデータは日次で BigQuery へエクスポートされ、BIツールの Data Studio(旧Looker Studio) のダッシュボードから確認できます。 この計測方法について、詳細は以下のテックブログで紹介しています。 techblog.zozo.com Issue作成とフィールド入力には、Claude Codeの Skill をそれぞれ用意して、運用負荷を下げています。例えば、Issueを標準テンプレートで作成する /create-issue や、AIとのセッションログを解析してAI活用フィールドを自動入力する /fill-ai-usage です。なお、入力内容そのものをAIに判断させるわけではありません。あくまで担当者の判断を補助するために使います。例えば、以下のようなタスクで工数削減があった場合、担当者は次のように記録します。 Issueタイトル:MLパイプラインの実装 AI削減工数:2時間 AI活用の成功・失敗事例:パイプラインのステータスをポーリングするスクリプトを書いて、エラー時にClaude Codeで自動修正させて再実行させるループを作った AI活用の成功・失敗事例の欄には、Issueのタスクそのものではなく、タスクを進める中でAIをどう活用したかを記録します。こうした記述が、工数削減にどうAIが効いたかを後から振り返るナレッジになります。 運用の整備 計測の仕組みを作っただけでは、チーム全員が使いこなせるようにならず、改善のサイクルも回りません。そこで、以下の運用を整備しました。 チーム目標への組み込み:「AIで社員を0.5人増やす」を半期のチーム目標に設定した。削減した時間を1か月あたりの営業日数×8時間と比較することで、チーム全体で「1か月あたり0.5人増えた」と言える状態を目指した 週次振り返り: 毎週ダッシュボードで削減実績を確認し、ナレッジを共有する。削減工数が最も多かったメンバーに社内のピアボーナスを送った AI活用会: AI活用において、活用方法を知っているかどうかの差が大きいため、全員がAIの機能や使い方を「知っている」レベルに揃えることを目的に、毎週1時間開催してきた。その後はチーム外のメンバーにも拡大した。現在は、社内のAI活用レベル指標 AZARS に基づき、既存業務のうちAI活用の効果が大きいものを特定し、改善を進めている デモ会: AI活用で削減できた時間を使い、施策提案のためのプロトタイプ作りと他チームへの提案をしている 以下は、週次振り返りで使っていたダッシュボードの一例です。 結果と考察 このような取り組みの結果、以下のような変化がありました。 AIによる工数削減とAI活用率の推移 FY2025H1とFY2025H2を比較すると、AIによる工数削減とAI活用率の計測結果は以下の通りです。 期間 AIによる工数削減 完了件数 平均AI活用率 FY2025H1(2025/04〜09) 212.6時間 269件 45.0% FY2025H2(2025/10〜2026/03) 439.9時間 260件 71.5% 合計 652.5時間 529件 -- ※ AI活用率は「AI削減工数が記録されたIssue件数 ÷ 完了したIssue全件数」で算出しています。 月別に見ると、削減工数は2025年10月の90.2時間と2026年3月の96.8時間がピークでした。1人月を各月の営業日数×8時間とすると、2025年10月は約0.51人月、2026年3月は約0.58人月に相当します。半期のチーム目標に掲げた月0.5人分は、この2つのピーク月で達成できました。Issue単位のAI活用率は2025年6月の28.0%から、2026年3月には88.6%まで上がりました。また、削減工数の伸びはFY2026に入っても続いています。2026年7月時点の集計では、2026年4〜6月の3か月だけで253.6時間を削減しており、6月単月の114.5時間は計測を開始してから最も大きい月間削減です。 H1からH2で削減工数は全体で見ると212.6時間から439.9時間へ、約2.1倍に伸びました。AI活用件数は121件から186件へ1.54倍、活用1件あたりの平均削減は1.76時間から2.37時間へ1.35倍です。つまり、AIを適用するタスクが増える「広がり」と、1件のタスクの中でAIに任せる工程が増える「深まり」の両方が寄与したと考えています。 ただし、この伸びは12か月の時系列を見た変化であり、活用率の上昇が削減を生んだという因果を示すものではありません。MA推薦ブロックは2025年2月に立ち上がったチームで、FY2025H1はチームの仕組み作りや環境の整備といった、AIの効きにくいタスクの比率が高い時期でした。さらに、この1年で利用しているAIの性能自体も大きく向上しています。チームの工夫と、タスクの変化、AIの性能向上が重なった結果として解釈していただきたいです。 作業内容別の効率 以下のグラフに示す作業内容別では、システム開発の削減工数が42.5時間から103.0時間へ伸びました。伸び率ではテスト・QAが4.2時間から47.0時間へ約11倍、テックブログ執筆・登壇が4.0時間から53.3時間へ約13倍となりました。 総量だけでなく効率も以下のグラフで確認します。1営業日あたりの削減工数は、AI削減工数の合計を実作業日数の合計で割った値です。作業内容ごとに見ると、上位はテックブログ執筆・登壇の0.53時間/営業日、分析・レポーティング作成の0.49、社内活動の0.42でした。下位はプロジェクトマネジメントと要件定義がともに0.14、設計が0.19でした。 ※ なお、この指標は小さいIssueほど高く出やすいですが、作業内容ごとのIssueの粒度に大きな差はないことを確認しています。 この傾向から、成果物が明確で作業に時間のかかるタスクほどAIの効果は出やすく、意思決定や対人調整を中心とするタスクほど効果は出にくいと考えています。 効果が出やすいタスクの中でも、コード生成やドラフト作成は特に速さを実感しやすい作業です。実際にAIによる開発量が増えていることは、外部ツールでも確認できています。開発生産性の計測サービスである Findy Team+ のデータを見ると、AI活用が広がったH2に、PR作成数はH1の約1.5倍、デプロイ頻度も1営業日あたり1.3件から2.1件へ増えました。 定性的な変化 数字には表れない、以下のような変化もありました。 チームのナレッジが蓄積された:AI活用の記録やデモ会での議論を通じて、タスクごとのAIの利用例を後から参照できるようになり、ナレッジの共有が進んだ メンバーのAIの活用レベルが上がった:AI活用の成功・失敗を経て、AIのより効率的な使い方を学び、実践できるようになった チーム横断の交流が生まれた:勉強会をチーム外へ広げた結果、他部署との交流が増え、AI活用のナレッジが社内に広がった プロトタイプ駆動の業務の進め方が生まれた:AIはアイデアをすぐ形にできるので、自分たちやステークホルダーからのフィードバックをすぐ得られる。この相性の良さを活かしたデモ会からは、デモを起点に案件化を判断する実例も生まれた また、失敗の記録からも学びがありました。1つ目は2.0時間、2つ目は1.0時間の工数増として記録されたものです。 Claude Codeに方針を委ねようとして、結局出力がよくわからなくなり最終的に自分が方針決めをした。 GitHub Projectsのフィールド埋めをコマンド経由で自動化しようとしたところ手動でやった方が良いことに気づいた。設計時間分のロスが発生した。 当たり前と言われれば当たり前ですが、これらは方針決めのような曖昧なタスクをAIに委ねると逆に時間を失うこと、自動化にも判断が必要なことを示しています。こうした失敗事例も、チーム内で共有することで、次のタスクで同じ失敗を繰り返さないようにしました。 体感と実測のギャップ メンバーからは、半期振り返りで以下のような声が上がりました。 AIで社員を0.5人増やすことが目標であったが、分析やコーディング業務では既に「俺が3人分になる…」ケースがある 一方、計測した工数削減を見ると、ピーク月でも96.8時間で約0.58人月の削減にとどまっています。エンジニア3人のチームにおける0.58人月の削減を、3人で3.58人分の業務をこなしたとみなすと、1人あたりは約1.2倍となり、体感の3人分とはギャップがあります。 このギャップについては、以下のように解釈しています。 実装をAIが一瞬で終わらせる体験は脳に強く残り、その印象に引っ張られ、業務全体が速くなったように感じる タスクが半日で終わってもその分2倍働くのは難しく、空いた時間は別のタスクに使われる 「理解してから実装する」が「実装されたものを理解する」に逆転し、実装時のフロー状態に入れない 並列でタスクを回せる分、コンテキストスイッチの回数と時間あたりのインプット量が増え、脳が疲れて作業の速度が落ちる 人間のレビューの待ち時間や対人コミュニケーションのコストが変わらない場合、AIで作業を速くこなせるほど人間の対応頻度が増えるので人間がボトルネックになる 今後の展望 FY2026H1の目標は、AIによる工数削減を月1人分に引き上げることです。これまでのように各自のタスクにAIを使って速く終わらせるだけでは、この目標に届かないというのがチームの共通認識です。特に、考察で見えたのは私たちのチームの伸びを止めているのがAIの性能というよりは、人間や運用体制だということでした。人間の集中力や、コンテキストスイッチへの耐性は簡単には変えられません。変えられるのは人間によるAIの効果的な使い方や運用体制だと考え、具体的には以下のように改善を進めています。 Spec: 仕様の明文化 1つ目は、実行前に仕様と完了条件を明文化して、AIの成果物への理解度を上げることです。結果と考察で触れた失敗事例が示すとおり、仕様と完了条件が固まっていないタスクをAIへ投げると、出力の良し悪しを判断できず、かえって時間がかかります。仕様の明文化に加えて、仕様の検討時に過去の類似タスクを参考として提案する仕組みも作り始めています。 Eval: 成果物の品質の評価 2つ目は、成果物の品質評価を自動化しやすい形に整備することです。品質のうち定量化できるものはテストとして実装し、自動的に評価します。一方、定量化が難しいものについては判断基準をガイドラインとして整備し、それに沿って評価できるようにします。これにより、成果物の生成だけでなく評価においても人間の介入を減らせると考えています。 このような動きをレビューの自動化にもつなげるつもりです。レビューの目的は、規範適合・検証の代行・意思決定と合意形成・知識の伝達などと整理できます。規範適合や検証の代行といったタスクはAIと自動的なテストに任せ、人間は意思決定と合意形成を中心に行うことでAIと人間の役割を分担します。 以下の図の左側が現在の状態です。デプロイの前に第三者によるレビューを必須としているため、AIで1人の実装が速くなっても、チーム全体のスピードは上がりにくい構造です。右側のように自動テストとAIレビューを挟むことで、この構造を変えていきます。 成果物の定量化と自動的なテストが進むほど、人間からAIへ委譲できるタスクは増えると考えています。そして、人間の確認を要するタスクが減れば、レビュー待ちは少なくなり、各自は自走して高速にタスクを進められると考えています。 AIが自律的に動く状態の構築 3つ目は、人が毎回指示しなくてもAIが自律的に動き、使うほど賢くなっていく状態を作ることです。SpecとEvalが整備されたあとは、人間のトリガーを待たずにAIが安全に動き、人間の判断やAIの成功・失敗のフィードバックを適用しながら自ら改善していく状態を目指しています。 私たちのチームでは、自律的なAIが活躍でき、効果の大きそうな以下の領域から着手し、次のような状態を作ろうとしています。 アラート対応:アラートを検知するとIssueを自動作成し、AIエージェントが原因調査から修正対応までを行い、アラートを解決する 定型的な改修:トリガーとなるイベントを受けて、仕様が明確な定型的な改修をAIが行い、リリース前の状態を作る データ分析:人間とAIが仮説を作り、データの前処理・分析・可視化までを行い、結果をレポートとしてまとめる モデル開発:人間とAIが実験計画を作り、分析→実装→実験を繰り返し、モデルの精度を改善して、結果をレポートとしてまとめる ソウゾウのナナメウエの創出 最後は、AIで生まれた余白の一部を、意図的に新しい価値づくりへ投資することです。考察で見たとおり、削減した時間をすべて次のタスクの前倒しに使うと脳が疲れるだけになりかねません。実際に、デモ会ではビジネスサイドへの提案を文書から動くデモへ変える取り組みを続けています。こういった活動を広げることで、ZOZOが企業理念でZOZOらしさとして掲げる ソウゾウのナナメウエ なアイデアの実現に挑戦し続けられるチームでありたいと考えています。 おわりに 本記事では、AIによる工数削減を計測して見えた結果と考察、今後の展望をご紹介しました。 現在ZOZOでは一緒にサービスを作り上げてくれる方を募集しています。ご興味がある方は以下のリンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは、新規事業部フロントエンドブロックの 大野純平 です。2025年度に新卒入社し、現在のチームに配属されました。チームでFlutter製モバイルアプリを開発する中で、新規プロジェクトの立ち上げを効率化するための社内テンプレートリポジトリの整備を進めています。 このテンプレートを育てる中で、 Flutter 3.44 へのアップデートに伴うCocoaPodsからSPMへの移行対応が積み残しになっていました。あわせて、このタイミングでdev / stg / prdの3環境対応も新規に追加しました(3 flavor化自体はSPM移行の必須要件ではなく、テンプレート整備の一環で同時に着手したものです)。本記事では、 CocoaPodsからSPMへの移行 と、 3 flavorを mise run setup で一括整備する仕組み を紹介します。あわせて、Flutterにおけるビルド構成ファイルを解説します。 目次 はじめに 目次 Flutterプロジェクトのビルド構成 iOS側のしくみ Android側のしくみ Dart側のしくみ 3層を貫く全体像 背景・課題 3環境の設定整合が壊れやすい 解決策の全体像 実装内容と具体的な解決策 1. CocoaPodsを撤去してSPMに移行する CocoaPodsの統合を解除する Crashlytics dSYMのパスを更新する 2. iOS:xcconfigでBuild Configurationを9パターンに整える Firebase plistをBuild Phaseで差し替える 3. Android:productFlavorsとgoogle-services.jsonをflavorごとに配置する 4. Dart:firebase_optionsをセレクタshim化する main.dartでduplicate-appを処理する 5. mise run setupで全flavorを一括生成する まとめ Flutterプロジェクトのビルド構成 Flutterアプリは iOSネイティブ層・Androidネイティブ層・Dart層 の3つが組み合わさっています。環境(flavor)の切り替えとは「3層すべてに、同じflavor用の設定を選ばせること」です。 iOS側のしくみ Xcodeは以下の単位を組み合わせてビルドを定義します。 単位 役割 Flutter デフォルト Project ( .xcodeproj ) プロジェクト全体の定義。実体は project.pbxproj Runner.xcodeproj Target ビルド成果物の単位 Runner Build Configuration ビルド設定値の集合 Debug / Profile / Release の 3 種 Scheme Run / Test / Profile / Analyze / Archive の各アクションで使う Target と Configuration を定義する起動設定 Runner 1 つ xcconfig はBuild Configurationの値をテキストで宣言するファイルです。別のxcconfigを #include で取り込んだとき、同じ変数があれば 後から読み込んだ値が優先 されます。共通値を書いたxcconfigを先に、flavor固有値を書いたxcconfigを後に重ねれば、最終的な値が組み上がります。 Info.plist の CFBundleIdentifier は $(PRODUCT_BUNDLE_IDENTIFIER) を参照しています。そのため、xcconfig側で PRODUCT_BUNDLE_IDENTIFIER を決めればBundle IDが自動的に解決されます。 iOS・macOSで外部ライブラリを使うときの依存管理ツールには、 CocoaPods と Swift Package Manager(SPM) の2つがあります。Flutterプロジェクトでは長らくCocoaPodsが標準でしたが、Flutter 3.44からiOS・macOSの両方でSPMがデフォルトに切り替わりました。両者の違いは以下のとおりです。 CocoaPods Swift Package Manager (SPM) 提供元 OSS(Ruby 製) Apple 公式 設定ファイル Podfile Package.swift (Xcode では Package Dependencies UI から編集) 成果物 Pods/ + .xcworkspace を別管理 Xcode が直接管理 ロックファイル Podfile.lock Package.resolved Flutter での状況 長年の標準 3.44 でデフォルト有効化 Android側のしくみ AndroidのビルドはGradleが android/app/build.gradle.kts を読んで実行します。 概念 役割 buildTypes ビルド種別( debug / release ) productFlavors 同じアプリのバリアント定義。 applicationId やリソースを分岐できる applicationId / applicationIdSuffix アプリの一意な識別子。iOS の Bundle ID 相当 resValue ビルド時に文字列リソースを生成する Gradle の機能 AndroidManifest.xml アプリのメタデータ宣言。 android:label でアプリ名を指定 Gradleは、flavor別のリソース( AndroidManifest.xml や res/ 配下など)を source set という仕組みでディレクトリ単位に分けます。Gradleがflavorを選んだときに src/main/ と自動マージします。なお google-services.json は厳密にはsource set mergeの対象ではありません。google-servicesプラグインが src/<flavor>/google-services.json などのパスを優先度順に探索し、1ファイルを選びます。 Dart側のしくみ Dartでflavorを識別するにはコンパイル時定数を使います。 API / オプション 役割 --dart-define-from-file=path/to/x.json .json または .env ( KEY=VALUE 形式)で書いた値をまとめて Dart に渡す flutter コマンドのオプション String.fromEnvironment('KEY') 渡された値をコンパイル時定数として参照する API。 switch の case 値にもできる firebase_options.dart flutterfire configure が生成する Firebase 設定ファイル。通常は 1 ファイル = 1 Firebase プロジェクト 3層を貫く全体像 3層を1枚にまとめると、 app_config.yaml → mise run setup → 各層のファイル群 → flutter run --flavor X という流れです。以降、iOS・Android・Dartの順に、各層内で mise run setup と flutter run --flavor X がどのファイルへつながるかを示します。 iOS層では、xcconfigチェーンが Info.plist を解決します。 project.pbxproj のRun Scriptがflavor別の GoogleService-Info.plist をコピーします。 Android層では、 build.gradle.kts がflavorディレクトリの google-services.json と AndroidManifest.xml を結びつけます。 Dart層では、 flavor/{dev,stg,prd}.json の FLAVOR が起点になります。 firebase_options.dart が FLAVOR に応じて firebase_options_{dev,stg,prd}.dart を切り替えて返すコード( セレクタ shim )になります。 flutter run --flavor dev --dart-define-from-file=flavor/dev.json を1回実行したときに、各層で何が起きるかを並べると以下のようになります。 iOS :Scheme dev → Debug-dev.xcconfig で PRODUCT_BUNDLE_IDENTIFIER が .dev 付きに解決される。Build PhaseのRun Scriptが ios/firebase/dev/ のplistをコピーする。 Android :productFlavor dev が選ばれて applicationIdSuffix=".dev" が適用される。 src/dev/google-services.json がsource setにマージされる。 Dart : flavor/dev.json の FLAVOR=dev がコンパイル時定数になる。 firebase_options.dart のセレクタshimが firebase_options_dev.dart を返す。 ここまでがFlutterプロジェクトのビルド構成の概要です。これを踏まえて、本記事で取り組んだ課題と解決策を見ていきます。 背景・課題 3環境の設定整合が壊れやすい dev / stg / prdの3環境を切り替えるには、iOS・Android・Dartの3層を整合させる必要があります。各層の対象は次のとおりです。 iOS:Bundle ID、 GoogleService-Info.plist Android: applicationId 、 google-services.json Dart:Firebase初期化オプション 3層が独立しているため設定漏れによるビルド失敗やFirebase接続不可が繰り返し発生し、原因特定にも時間がかかっていました。 解決策の全体像 以下の5つの取り組みで課題を解決しました。 # 層 取り組み 1 iOS / 基盤 CocoaPods 撤去 → SPM 移行 2 iOS / flavor xcconfig × 9 Build Configuration + flavor 別 Firebase plist 3 Android / flavor productFlavors + flavor 別 google-services.json 4 Dart firebase_options.dart のセレクタ shim 化 5 setup app_config.yaml の per-flavor 化と mise 自動化 1つのflavorを選んで flutter run すると、iOS・Android・Dartの3層でそれぞれ対応する設定が適用されます。 実装内容と具体的な解決策 1. CocoaPodsを撤去してSPMに移行する CocoaPodsはRuby製のAppleプラットフォーム向けパッケージマネージャーで、Swift Package Manager(SPM)はApple公式の代替です。Flutter 3.44では新規プロジェクトでiOS・macOSともにSPMがデフォルトで有効化され、Firebaseをはじめとする主要プラグインもSPM対応を完了しています。 pod install が不要になるため、CIのセットアップも大幅に高速化されます。 CocoaPodsの統合を解除する 既存プロジェクトでは、 Podfile をいきなり削除するのではなく、まず pod deintegrate でXcodeプロジェクトからCocoaPodsの統合を解除します。 cd app/ios pod deintegrate # project.pbxproj から Pods 統合を除去 rm Podfile Podfile.lock # Podfile 本体を削除 rm だけで済ませてはいけません。 Podfile を削除しただけでは project.pbxproj にCocoaPods統合の痕跡が残ります。たとえば [CP] Check Pods Manifest.lock などのBuild Phaseや、 Pods-Runner xcconfigの #include 参照などです。 pod deintegrate はこれらを正しく除去します。 .gitignore も忘れず更新します。 # app/ios/.gitignore -.symlinks/ -Pods/ +# SPM +.build/ SPMが管理する Package.resolved はバージョンを固定するためリポジトリにコミットします。 Crashlytics dSYMのパスを更新する dSYM(Debug Symbols)はクラッシュレポートのスタックトレースを人間が読めるシンボルに復元するためのファイルです。Crashlyticsを使っている場合、そのdSYMをアップロードするBuild PhaseスクリプトをSPM構成のパスに合わせて更新します。このRun Scriptは setup 系スクリプトが自動生成するものではなく、Crashlyticsを利用する場合はプロジェクトごとに手動で追加する手順です。 Firebase公式のApple向けガイド は現在SPM構成を前提としており、Run Scriptに以下を追加するよう記載されています。 # Build Phases > Run Script " ${BUILD_DIR % /Build/* } /SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run " CocoaPods時の ${PODS_ROOT}/FirebaseCrashlytics/run に相当します。Xcode 15以降は User Script Sandboxing がデフォルトで有効になっています。有効な場合はRun ScriptのInput Filesに以下を列挙する必要があります( Firebase公式ドキュメント )。 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME} ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME} ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist $(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH) # Debug Dylib(DEBUG_DYLIB)が有効な場合は追加で必要 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib 2. iOS:xcconfigでBuild Configurationを9パターンに整える iOSのXcodeプロジェクトは「Build Configuration」というビルド設定の単位を持ちます。各Configurationにはxcconfigファイル(テキストでビルド設定を記述する仕組み)を1つ紐付けられます。本テンプレートは、以下の9通りのBuild Configurationを用意しています。 dev stg prd Debug Debug-dev Debug-stg Debug-prd Profile Profile-dev Profile-stg Profile-prd Release Release-dev Release-stg Release-prd これらはあらかじめ project.pbxproj にコミット済みです。あわせて、それらを参照する dev / stg / prd の3 Schemeもコミット済みです。各Schemeは、Run/Test/Analyzeで Debug-<flavor> を参照します。Profileでは Profile-<flavor> 、Archiveでは Release-<flavor> を参照します。3つのSchemeには、Flutter標準の Run Prepare Flutter Framework Script pre-actionも含めて完全な状態で用意されています。このpre-actionは xcode_backend.sh prepare を実行します。これによりSPM統合に必要な FlutterGeneratedPluginSwiftPackage が解決されます。 mise run setup はこのScheme⇔Configurationのマッピングやpre-actionには一切触れません。書き換えるのは、各Configurationに紐づくxcconfigの値(Bundle IDやアプリ名など)だけです。 Flutter/Shared.xcconfig ← 全 Configuration 共通の値 Flutter/flavor-dev.xcconfig ← dev 固有の値(BUNDLE_ID_SUFFIX など) Flutter/flavor-stg.xcconfig Flutter/flavor-prd.xcconfig Flutter/Debug-dev.xcconfig ← #include で連結(9ファイル) Flutter/Debug-stg.xcconfig ... Flutter/Release-prd.xcconfig 後ろの #include が前の値を上書きするため、共通値( Shared )→ flavor固有値( flavor-dev )の順に重ねることで最終的な値が決まります。 Debug-dev.xcconfig は3行の #include のみで構成されます。 #include "Debug.xcconfig" #include "Shared.xcconfig" #include "flavor-dev.xcconfig" これでビルド時に対応するxcconfigが PRODUCT_BUNDLE_IDENTIFIER を解決します。その値が Info.plist 経由でflavor付きBundle ID(例: jp.testapp.dev )として適用されます。 Firebase plistをBuild Phaseで差し替える flavor別の GoogleService-Info.plist を使い分けるため、Build PhaseにRun Scriptを追加します。Run Script内の ${FLAVOR} は実行直前にxcconfigの値( dev / stg / prd )に置き換えられます。これによりコピー元ディレクトリがflavorごとに切り替わり、該当するplistだけが .app バンドルに入ります。 # Build Phases > Run Script SRC = " ${SRCROOT} /firebase/ ${FLAVOR} /GoogleService-Info.plist " DST = " ${BUILT_PRODUCTS_DIR} / ${PRODUCT_NAME} .app/GoogleService-Info.plist " if [ -f " $SRC " ]; then cp -f " $SRC " " $DST " else echo " warning: $SRC not found, Firebase native init will be skipped " fi plistが存在しないflavorでもビルドが失敗しないよう、存在チェックを入れています。 firebase_project_id を空にしてFirebase設定をスキップした場合がこれに該当します。このRun Scriptは読み書き対象のパスをInput Files / Output Filesとしてあらかじめ宣言しています。そのためUser Script Sandboxingが有効な環境でも安全に動作します。 plistは以下のスロットに配置します( setup:firebase が生成)。 app/ios/firebase/ ├── dev/GoogleService-Info.plist ├── stg/GoogleService-Info.plist └── prd/GoogleService-Info.plist 3. Android:productFlavorsとgoogle-services.jsonをflavorごとに配置する Androidでは build.gradle.kts に productFlavors を追加し、flavorごとに applicationIdSuffix とアプリ名を設定します。 // app/android/app/build.gradle.kts flavorDimensions + = "env" productFlavors { create( "dev" ) { dimension = "env" applicationIdSuffix = ".dev" resValue( "string" , "app_name" , "Flutter Template App Dev" ) } create( "stg" ) { dimension = "env" applicationIdSuffix = ".stg" resValue( "string" , "app_name" , "Flutter Template App Stg" ) } create( "prd" ) { dimension = "env" resValue( "string" , "app_name" , "Flutter Template App" ) } } google-services.json はAndroid Gradleのflavorソースセット( src/<flavor>/ )に配置します。flavor用の設定ファイルはgoogle-servicesプラグインが自動で選択します。 android/app/src/ ├── dev/google-services.json # setup:firebase が生成 ├── stg/google-services.json └── prd/google-services.json 注意点が2つあります。1つ目は google-services.json の package_name です。suffix込みの最終applicationIdに合わせます(例: com.example.app.dev )。suffixなしの com.example.app だけFirebaseに登録していると、dev / stgビルド時にプラグインが例外を投げてビルドが通りません。Firebase側のアプリ登録もsuffix込みのapplicationIdで行う必要があります。 AndroidManifest.xmlで android:label="@string/app_name" のように参照します。 productFlavors の resValue がflavorごとに生成した文字列が、そのままアプリ名として使われます。 <!-- app/android/app/src/main/AndroidManifest.xml --> <application android : label = "@string/app_name" ...> 2つ目は、 strings.xml との重複です。 resValue("string", "app_name", ...) はビルド時に文字列リソースを生成します。そのため、 res/values/strings.xml に同名の app_name が定義されていると Duplicate resources エラーになります。 app_name は strings.xml には置かず、各flavorの resValue に一本化してください。 4. Dart:firebase_optionsをセレクタshim化する flutterfire configure が生成する firebase_options.dart は単一の設定しか返しません。そこでファイル名とAPIを保ったまま、 セレクタ shim に置き換えます。 まず app/flavor/dev.json に FLAVOR キーを定義し、 --dart-define-from-file で読み込みます。 { " FLAVOR ": " dev " } FLAVOR はDartコード側で String.fromEnvironment('FLAVOR') を使って、コンパイル時定数として参照できます。 // app/lib/firebase_options.dart import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'firebase_options_dev.dart' as dev; import 'firebase_options_prd.dart' as prd; import 'firebase_options_stg.dart' as stg; const _flavor = String .fromEnvironment( 'FLAVOR' ); class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { switch (_flavor) { case 'dev' : return dev.DefaultFirebaseOptions.currentPlatform; case 'stg' : return stg.DefaultFirebaseOptions.currentPlatform; case 'prd' : return prd.DefaultFirebaseOptions.currentPlatform; default : throw StateError( 'Unknown FLAVOR: "$_flavor". ' 'Run with --dart-define-from-file=flavor/<flavor>.json' , ); } } } 呼び出し側( main.dart )は DefaultFirebaseOptions.currentPlatform をそのまま使うだけで、flavorの切り替えを意識する必要がありません。 setup:firebase はこのファイルを上書きせず、本物の firebase_options_<flavor>.dart だけを再生成します。 注意:flavor指定は必須 _flavor が空( --dart-define-from-file 未指定)だとshimの default が StateError を投げます。 _initializeFirebase はこれをcatchしないため、flavorなしの flutter run は起動時に落ちます。VS Codeのlaunch.jsonや mise run タスクで各flavorを必ず渡す構成にしておくと安全です。 main.dartでduplicate-appを処理する Firebaseには2つの初期化経路があります。ネイティブの自動初期化と、Dart側の Firebase.initializeApp() です。両方が呼ばれると duplicate-app の FirebaseException が発生します。ただし duplicate-app はflavor不整合(ネイティブのplist/jsonが別のFirebaseプロジェクトを指している等)でも発生しうるため、無条件で成功扱いにはできません。ネイティブ初期化済みの options と比較し、一致した場合のみ成功扱いにします。不一致の場合は例外を再throwし、アプリを起動時にクラッシュさせます。例外を握りつぶしてしまうと、不正なAPIキーや設定不整合といった検知したいバグを見逃してしまう可能性があるためです。 // app/lib/main.dart(抜粋) Future< bool > _initializeFirebase() async { try { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); return true ; } on UnimplementedError { // placeholder(setup:firebase 未実行)の場合は Firebase を無効化して続行 return false ; } on FirebaseException catch (e) { if (e.code == 'duplicate-app' ) { final existing = Firebase.app().options; final expected = DefaultFirebaseOptions.currentPlatform; final matchesNativeConfig = existing.appId == expected.appId && existing.projectId == expected.projectId && existing.apiKey == expected.apiKey; if (matchesNativeConfig) return true ; } rethrow ; } } 5. mise run setupで全flavorを一括生成する mise は、タスク実行とツールのバージョン管理を兼ねるCLIツールです。本テンプレートではセットアップスクリプトの実行に利用しています。手作業の設定漏れを防ぐため、セットアップの入力ファイル app_config.yaml にFirebaseのproject_idをflavorごとに設定できる構造を追加しました。これにより、 mise run setup:firebase で全flavorの設定ファイルを一括生成できます。 flavor : dev : app_name_suffix : " Dev" bundle_id_suffix : ".dev" firebase_project_id : "my-project-dev" # 空文字なら Firebase 設定をスキップ stg : app_name_suffix : " Stg" bundle_id_suffix : ".stg" firebase_project_id : "my-project-stg" prd : app_name_suffix : "" bundle_id_suffix : "" firebase_project_id : "my-project-prd" mise run setup:firebase は app_config.yaml の各flavorの firebase_project_id を読み取ります。読み取った値で flutterfire configure をflavorごとに実行します。 iOSは --ios-out でパスを直接指定すると、plistは出力されるもののproject.pbxprojへの参照は Runner/ 配下を前提に追加されます。配置と参照のズレでビルドが落ちる事例もありました。そこで setup:firebase は flutterfire configure のデフォルト出力( Runner/ )を利用し、後処理で配置を整えます。 Build PhaseのRun Scriptがflavorに応じてplistを動的にコピーするので、 project.pbxproj の静的参照は不要です。 setup:firebase は実行のたびに追記される参照を毎回除去し、pbxprojが肥大化しないようにしています。 # クリーンアップ確認(0 であれば OK) grep -c " GoogleService-Info.plist .*PBXBuildFile " \ app/ios/Runner.xcodeproj/project.pbxproj mise run setup:flavor はxcconfigやflavor JSONを app_config.yaml から再生成します。Firebaseの設定ファイルは別途 setup:firebase で更新します。何度実行しても同じ出力になるため、設定変更後に再実行すればxcconfigとflavor JSONが同期されます。 この仕組みにより、新規プロジェクトでは app_config.yaml を編集して mise run setup を実行するだけで済みます。iOS xcconfig・Android productFlavors・Dartセレクタshim・Firebase設定ファイルのすべてが一括でできあがります。 まとめ 本記事では、FlutterプロジェクトにおけるCocoaPodsからSPMへの移行を紹介しました。あわせてdev / stg / prdの3 flavorをiOS・Android・Dartの3層で整合させる仕組みの構築も紹介しました。 CocoaPodsからSPMへの移行自体は pod deintegrate → Podfile削除で思ったよりシンプルです。手間がかかるのはflavorの3層整合で、それぞれが独立した設定体系を持つため一つひとつ繋ぎ込む必要があります。 app_config.yaml → mise run setup という自動化の仕組みを整えました。これにより手動作業による設定漏れのリスクを排除し、チームが本来の開発に集中できる環境を作れました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、全社AWS管理部門の江島です。社内で利用されているAWS環境を全社横断的に管理する役割を担っています。 全社AWS管理部門では、AWSを安全に運用するために日頃からさまざまな取り組みを行っています。例えば、AWS本番環境へのアクセス管理においてIAM Identity Centerを活用し、定期的な棚卸しや申請ベースの権限管理といったセキュリティ対策を継続的に実施してきました。 こうした取り組みを土台としつつ、さらなるセキュリティ強化に向けて「必要なときだけ権限を付与する」最小権限の原則を徹底することを次の目標に掲げました。 本記事では、JITアクセス(ジャストインタイムアクセス)の仕組みを全社に導入した取り組みをご紹介します。AWSがオープンソースで公開する「TEAM(Temporary Elevated Access Management)」を活用しました。 目次 はじめに 目次 従来の運用 / 課題 JITアクセスとは TEAM (Temporary Elevated Access Management) TEAMの導入に必要な検討ポイント JITアクセスの対象とする権限 新しい権限付与フロー IdP管理 TEAMで利用される各Policyの設定 Eligibility Policy Approver Policy 初回導入後の状況 AWSアカウントごとに個別のPermission Set まとめ 従来の運用 / 課題 従来の権限付与フローは以下の通りです。 まず、申請者が社内ツールにて権限を申請します。その後、セキュリティ部門が内容の妥当性を確認し、全社AWS管理部門が権限を付与します。 なお、定期的な棚卸しや利用者からの申請に応じた見直しは実施していたものの、権限自体は常時付与された状態でした。 付与する権限が申請理由と照らし合わせて妥当であることは都度確認されていますが、必要時にだけ利用できれば良いような 強い権限 を常に保持しておくことは、最小権限の原則に照らしてリスクになり得ます。 そこで、これを解決するために「JITアクセス」の仕組みを導入することになりました。 JITアクセスとは JITアクセス(ジャストインタイムアクセス)とは一時的に権限昇格するための仕組みです。IAM Identity Centerの公式ドキュメントでは次のように説明されています。 Temporary elevated access (also known as just-in-time access) is a way to request, approve, and track the use of a permission to perform a specific task during a specified time. 引用元: docs.aws.amazon.com これを活用することで、本記事のタイトルにある「強い権限は使い捨て」を実現できます。 なお、前述したIAM Identity Centerの公式ドキュメントに記載があるように、さまざまなSaaSがIAM Identity Centerと連携してJITアクセスの機能を提供しています。また、AWSとしても独自のOSS(後述するTEAMというアプリケーション)を提供しています。 比較検討を行った結果、コストが安価であり、国内外のさまざまな企業での導入実績もあるTEAMを採用することになりました。 TEAM (Temporary Elevated Access Management) TEAMとはAWSによって提供されている一時的に権限昇格するためのアプリケーションです。以下のリポジトリで公開されています。 github.com AWS LambdaやAmazon DynamoDBのように複数のサーバレスサービスで構成されており、安価で拡張性を持ったアーキテクチャです。 公式ドキュメントからの引用ですが、具体的なアーキテクチャは以下の通りです。詳細は引用元をご参照ください。 TEAMのアーキテクチャ https://aws-samples.github.io/iam-identity-center-team/docs/overview/architecture.html より引用 TEAMを組織へ導入するには運用面でさまざまな点を検討する必要があります。具体的には、以下のような観点があります。 JITアクセスの対象とする権限 新しい権限付与フロー IdP管理 承認可能な範囲の決定(Approver Policy) 申請可能な範囲の決定(Eligibility Policy) 以降、それぞれの検討ポイントについて具体的な内容を紹介します。 TEAMの導入に必要な検討ポイント JITアクセスの対象とする権限 IAM Identity Centerを利用しているため、各ユーザへ付与する権限はPermission Setで管理しています。当社ではユーザの役割に応じて複数のPermission Setを用意しており、「読み取り専用」や「書き込みも可能」な権限があります。 結論としては、常に保有可能な権限は「読み取り専用」のみ、「書き込みも可能」な権限については すべてJITアクセスの対象 としました。これについては運用とセキュリティのトレードオフを考える必要がありますが、システムを安全に運用することを最優先とするために厳しめの方針となっています。なお、読み取り専用といっても、機密性が高い情報についてはさらに細かなアクセス制御を別途行っています。 新しい権限付与フロー JITアクセスによる権限付与フローは以下の通りです。 このフローを実現するために、一連のやり取りで登場する役割を整理しました。 役割 従来の権限付与フロー 新しい権限付与フロー(JITアクセス) 申請者 権限の申請 権限の申請 セキュリティ部門 申請内容を都度確認 AWSアカウント管理者へ判断を委譲 全社AWS管理部門 申請ごとに権限付与 AWSアカウント管理者へ作業権限を委譲 AWSアカウント管理者 - JITアクセス承認者の管理・承認 JITアクセス承認者 - JITアクセスの承認操作 新しいフローを実現するために、「AWSアカウント管理者」および「JITアクセス承認者」の役割を新しく定義しました。なお、セキュリティ部門はAWSアカウント管理者の任命に関する判断は引き続き実施します。また、AWSアカウント管理者は自身もJITアクセス承認者の役割を担います。 当社は多くのAWSアカウントを保有しているため、従来のフローのままですべての承認作業を行うことが困難でした。そこで、最低限のガバナンスを維持した上で権限を委譲する方針としています。 元々、AWSアカウントを新規で発行する際にはAWSアカウントごとに管理者を立ててもらう運用としていました。そこで、AWSアカウント管理者にJITアクセスの承認権限も委譲することにしました。一方で、単純に権限を委譲するだけだとAWSアカウント管理者自身の負担が増大すると想定されたので、JITアクセスの承認者についてはAWSアカウント管理者自身でコントロール可能なルールとしました。 IdP管理 TEAMはIdP(Identity Provider)であるIAM Identity Centerのグループ機能を前提として動きます。具体的には、以下のような役割をグループとして用意する必要があります。 役割 説明 TEAM管理者 TEAM自体の設定を変更する権限を持つ TEAM監査者 TEAM自体の監査機能(監査ログの閲覧)を利用する権限を持つ 申請者 Eligibility Policyに基づいて権限申請を行うことができる 承認者 Approver Policyに基づいて承認操作を行うことができる 申請者と承認者のグループは複数用意でき、当社でも複数グループを利用しています。これについては、次に説明するEligibility PolicyやApprover Policyと合わせて説明します。 TEAMで利用される各Policyの設定 TEAMにはEligibility PolicyとApprover Policyというものが存在します。それぞれ以下の役割です。 Eligibility Policy どのAWSアカウント(もしくはOU)に対して誰が 申請できるか を設定する Approver Policy どのAWSアカウント(もしくはOU)に対して誰が 承認できるか を設定する それぞれ、細かく設定もできますが、運用負担とのトレードオフです。当社では次に記載する方針としました。 Eligibility Policy Eligibility Policyは1つだけ用意し、IAM Identity Centerで管理されている全社員がすべてのAWSアカウントへ申請できる構成としました。 セキュリティの観点では、役割ごとに申請可能なアカウントを絞り込む方が理想的です。しかし、そのためには「権限申請するための事前申請」が別途必要となり、運用負担が大きくなると判断しました。 「申請の入口は広く、承認の出口は厳密に」 という方針のもと、誤った申請や不正な申請は後述するApprover Policyの承認者が拒否できるため、このトレードオフを意図的に選択しています。 Approver Policy Approver PolicyはAWSアカウント単位で用意して、該当するAWSアカウントのJITアクセス承認者となるグループに関連付けます。 これによって、承認可能な人を厳密に制御できるためセキュリティが向上します。 なお、複数のApprover Policyを管理することが大変な場合には、OSSとしてTerraform Providerが公開されています。 registry.terraform.io 初回導入後の状況 ここまでご紹介した方針で、まずは一部の組織へ導入して問題がないことを確認しました。その上で、他の組織に対しても段階的に導入を進めました。 幸いにも大きな混乱が発生することはありませんでしたが、以下のような要望があがりました。 日常的な運用業務において読み取り専用のみでは不十分なケースがある。そのために都度JITアクセス申請するのは運用的に困難。 例えば、日常的な運用作業で必要となる一部の操作がReadOnlyAccessポリシーではカバーされていないケースがありました。 そこで、JITアクセスの例外として 必要最小限のポリシー を持ったAWSアカウントごとに個別のPermission Setを作成できる仕組みを検討しました。 AWSアカウントごとに個別のPermission Set Permission Setの数が増えるということは、全社AWS管理部門の負担も増加します。また、個別の事情に応じてPermission Setを用意できるといっても、JITアクセスのメリットを損なうようでは意味がありません。 そこで以下を要件として仕組みを検討しました。 AWSアカウント毎の個別要件に応じた権限を常時保有できること Permission Setの数が膨大になっても全社AWS管理部門の負担が増えすぎないようにすること 必要最小限の権限になっていることを仕組みで担保すること これらの要件を満たすために検討したアーキテクチャは以下です。 全社AWS管理部門の負担を低減するために、Identity Centerの管理アカウント側では Permission Setの箱だけを用意 します。ポリシーの中身については、 メンバーアカウント側のカスタマー管理ポリシーで設定 してもらう方針としました。これによって、メンバーアカウント側で運用変更が発生しても、全社AWS管理部門側での作業は発生しません。 また、必要最小限の権限となっていることを担保するために、AWS Configを活用したガードレールを用意し、基準を満たさないポリシーを検知できる仕組みとしています。 なお、Permission Set数のクォータは以下の公式ドキュメントで説明されています。上限緩和も可能となっており、当社の規模では不足することはない想定です。 docs.aws.amazon.com まとめ 本記事では、JITアクセスの概念とTEAM選定の理由から、承認フローの設計、大規模組織での段階的展開における調整のポイント、導入後に発生した課題への対応までをご紹介しました。導入から1年以上が経過しましたが大きな問題なく運用が継続しています。セキュリティ向上のためにJITアクセスの導入を検討している方がいれば、ぜひ参考にしてみてください。今後もAWSを安全に運用するためにさまざまな取り組みを行っていこうと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
p:has(> img) { width: 100% !important; } はじめに こんにちは、ZOZOMO部FBZブロックの杉田です。普段は Fulfillment by ZOZO が提供するAPIシステムを開発・運用しています。昨年からは、社内における開発者向けAI支援ツールの推進を担う専門チームでも兼務で活動しています。 ZOZOMO部SREブロックの𠮷富です。普段はFulfillment by ZOZOのSREをしています。 2026年6月25日・26日の2日間、幕張メッセにて「 AWS Summit Japan 2026 」が開催されました。本記事では、会場や各ブースの様子に加え、特に印象に残ったセッションについてご紹介します。 目次 はじめに 目次 AWS Summit Japanとは 会場の様子 セッション紹介 AI 駆動開発ライフサイクル (AI-DLC) のご紹介(AIM221) AI生産性のパラドックス 開発手法「AI-DLC」の紹介 実践事例と驚異的な成果 サイバーエージェントにおける AI 推進戦略と変革への取り組み(AIM229) エンジニアの評価制度 全社AI活用を可視化・底上げする「AI番付」の取り組み AIを「楽しむ」文化の醸成 人間がボトルネックにならないための「Human in the Loop」設計 生成 AI ブームのその先へ -AI を使いこなす人材戦略とプラットフォームエンジニアリング-(PRT237-S) 生成AIプロジェクトの成果創出までの「4つの壁」 本番化までの時間と「Governance as code」 生成AI時代の「FinOps」 Amazon S3セキュリティベストプラクティス(STG357) セッションのテーマと背景 S3セキュリティ8つのベストプラクティス 統制の分離とスケール Advanced VPC Networking-知っておきたいAWSネットワークの最新動向:VPC関連サービスのアップデートを総整理-(CDN320) Amazon VPC Latticeのさらなる進化とカスタムDNS対応 AWS Direct Connectとハイブリッド/マルチクラウド接続の強化 ワークフローオーケストレーターにおける複雑性と非決定性のコントロール(CNS454) ワークフローの役割とAWSサービスの紹介 指揮者は歌わない AIエージェントの非決定性を制御するアプローチ まとめ AWS Summit Japanとは AWS Summit Japanは、AWSの最新技術や活用事例を学べる日本最大級のイベントです。2026年も幕張メッセで2日間にわたり開催され、260以上のセッションや展示、ハンズオン、コミュニティ企画など、幅広いコンテンツが用意されていました。AIエージェントやサーバーレス、クラウド活用の最新動向など、今後のシステム設計や運用に関わるテーマが多く取り上げられていました。現地参加だけでなくオンデマンド視聴もできるため、気になるセッションを後から見返すこともできます。 会場の様子 紹介:𠮷富 早い時間から多くの参加者で賑わっていました。基調講演の開始時刻の前後は列が伸び、入場に1時間以上かかることもあったようです。 同僚提供写真 先着5,000名に毎年恒例のクッションとお弁当引換券が配布されました。さらに基調講演の開始前に入場したところ朝食も配布されていました。 いくつかのセッションは入場時に配布されるイヤホンを利用して視聴するサイレントセッションでした。座席に設置されたレシーバーを利用してセッションの音声を視聴できます。 AWS Builders’ FairではAWSサービスを利用したプロジェクトが多数紹介されていました。ゲーム形式のものが多く、楽しく学べました。 「Physical Hands'on Blocks 組んで守れ! AWSアーキテクチャ」を実際に体験しました。ブロックを動かしアーキテクチャを完成させ、攻撃から守るゲームです。 ELBからEC2を切り離す痛恨のミス Physical AI特設エリアではロボットの実機を体験することができました。 セッション紹介 AI 駆動開発ライフサイクル (AI-DLC) のご紹介(AIM221) 紹介:杉田 AI生産性のパラドックス AIを開発に導入しても速度向上は10〜15%にとどまり、場合によっては「AIを使うと19%遅くなる」というデータが紹介されました。たとえコーディングで時間を節約しても、開発ライフサイクルの他の部分で時間が失われているためです。 AIは決して銀の弾丸ではないので、適材適所で効果的な使い方をしないとかえって状況の悪化を招く場合があると、実感としても強く感じています。この研究結果から、AIに丸投げしたり、逆にAIを単なる補助ツールとして狭く使ったりするだけではダメなのだとあらためて感じました。 開発手法「AI-DLC」の紹介 AIのポテンシャルを最大限に発揮したうえで、ビジネスの意図をソフトウェアシステムとして価値提供できるようにするには、従来の方法の延長線上では限界があります。具体的には、以下のような課題が挙げられます。 人間の関与を最小限またはゼロにした開発は信用が難しく、説明責任も果たすことができない AIによるアジリティは開発ライフサイクル全体では限定的 コーディングで節約した時間は他の部分で失われている これらの課題を解決するためにAWSが再構想したのは「AI-DLC」です。AI-DLCは以下の3つのステップに分かれ、人間の意図伝達、AIによる計画・実行、人間によるレビューや承認という形で進むオペレーティングモデルです。 Inception Construction Operation AI利用を前提とした開発が行われる昨今では、開発プロセス自体をAIネイティブに作り直すというアプローチはとても重要だと思いました。 コンテキストの扱いがポイントとなるAI利用を前提とした開発において、各ステップが次のステップのコンテキストを構築していく流れは、とても理にかなっていると感じました。 実践事例と驚異的な成果 実際の実践事例として、AI-DLCの導入により属人性の高い領域でも開発生産性が200%以上も向上したほか、従来の方法では2か月想定の開発をわずか48時間で達成させた事例が紹介されました。 ここまでの成果を出すには、AI単体の力ではなく、ビジネス、開発、QAなどのチームが垣根を越えて協働する「モブエラボレーション」の体制づくりが不可欠になります。 本セッションを通じて、AIを単なる開発支援のツールとして使う段階は終わったとあらためて痛感しました。適用できそうな領域からAI-DLCの思想を取り入れ、人間とAIが強みを最大化し合う新しい開発プロセスへの移行を検討していきたいと思います。 サイバーエージェントにおける AI 推進戦略と変革への取り組み(AIM229) 紹介:杉田 エンジニアの評価制度 サイバーエージェントでは、2028年の開発プロセス完全自動化を見据え、AIを武器に開発領域からビジネス領域へ越境する「ビジネスリードエンジニア」などを新設していました。技術のみならず課題発見から実装・運用までを一気通貫で担える人材を正当に評価する枠組みです。 詳細については、以下のブログに記載があったので、気になる方はご覧ください。 developers.cyberagent.co.jp AIの台頭により、コーディングそのものの価値が相対的に変化している中で、エンジニアのキャリアパスを組織主導で再定義している点が印象的でした。AI時代においてエンジニアが目指すべき姿を考えるうえで、参考になる評価軸だと感じました。 全社AI活用を可視化・底上げする「AI番付」の取り組み 各事業部におけるAI活用の「成果インパクト」や「カルチャー醸成」などを、相撲の番付表(横綱〜幕下)に見立てて評価・可視化している取り組みが紹介されました。 単にツールを導入して終わるのではなく、組織ごとの成熟度を定量・定性的に測る仕組みを作ったうえで、全社的なナレッジ共有と底上げを図る非常に効果的なアプローチだと感じました。特に、結果を分析レポートとしてまとめ、事業ごとの改善案を提案する仕組みは参考になりました。 AIを「楽しむ」文化の醸成 AIをフル活用して実装速度を競う「AI開発RTA」や、2日間でゼロからプロダクトを作る「AI Agent Arena」を開催した様子が紹介されました。これらのイベントを通じて、エンジニアがAIに楽しく触れる場を提供し、ベストプラクティスの創出・共有を促していました。 利用を強制するのではなく、イベントを通じて「AIフレンドリー化」を推進する姿勢は、組織の熱量を高める取り組みとして参考になりました。 人間がボトルネックにならないための「Human in the Loop」設計 自動化が進むと人間の監視が形骸化する「プロセスの萎縮」というリスクがあります。このリスクに対して、タスクのリスクに応じて「全件レビュー」「スコアベース」「エスカレーション」の3パターンで人間の関与を設計している取り組みが紹介されました。 タスクに関係なく一律でHuman in the Loopを採用するのではなく、リスクや複雑さを加味したうえで段階的にHuman in the Loopを設計している点が参考になりました。 サイバーエージェントの取り組みは、ツールの導入にとどまらず、評価制度、企業文化、そして開発プロセスそのものをAI時代に適合させていくといったものでした。AIと協働する新しい開発組織のあり方については、私の所属組織でも重要なテーマなので、今後の活動の参考になるセッションでした。 生成 AI ブームのその先へ -AI を使いこなす人材戦略とプラットフォームエンジニアリング-(PRT237-S) 紹介:杉田 生成AIプロジェクトの成果創出までの「4つの壁」 同社のアンケートから、2024年から2025年にかけて生成AIの活用率は増加している一方で、DXの成果について「期待通り以上」と回答した割合は減少していることが確認できました。 また、PoCの成功はゴールではなく、その先には「本番化までの時間」「LLMの精度不足」「利用が広がらない」「想定外のコスト増」という4つの壁が存在すると指摘されていました。 本番化までの時間と「Governance as code」 セキュリティやコンプライアンスの都度審査により、本番の開発開始までに数か月を要するケースがあると紹介され、これは組織の規模感が大きくなることでより顕著になる傾向があります。対策として、ガバナンス要件を組み込んだ開発テンプレート(Governance as code)を用意し、事前承認済みの型を開発者に利用させるアプローチが紹介されました。 守りのプロセスを自動化・テンプレート化して、開発者の認知負荷を下げる取り組みはまさにプラットフォームエンジニアリングの真骨頂だと思いました。Governance as codeは、社内での車輪の再発明を回避する手段として、OSSで有名な Backstage をはじめとした開発者向けポータルとの相性が良いと感じました。 生成AI時代の「FinOps」 生成AIの利用拡大に伴い請求額が想定の数倍に膨らむリスクに対し、コストの可視化とプロンプトキャッシング等の対策が紹介されました。 プロンプトキャッシングの仕組みとして、キャッシュは先頭から一致した場合のみヒットするため、プロンプトの上部に変数(可変部)が存在すると効果が薄くなる点について解説されました。 そこで、この性質を活かすために可変部をプロンプトの下部に移動させる構造変更をしたことで、1ワークフローあたり400円かかっていたコストが80円(80%削減)へと劇的に改善した事例が紹介されました。 機能としてキャッシングを有効にするだけでなく、内部の仕組みを理解してキャッシュヒット率を最大化している点は学びでした。生成AIを活用するうえで、それらのコストは切っても切り離せない課題だと思うので、機能を使いこなすことによるコストインパクトに驚きました。 Amazon S3セキュリティベストプラクティス(STG357) 紹介:𠮷富 Amazon S3はシステム構成において、データ保存先、ログ出力先、バックアップ、データ連携基盤など、さまざまな場面で利用されています。利用用途が増えるにつれて、アクセス制御や利用者・アプリケーションごとの権限管理、意図しない公開の防止など、セキュリティ面で考慮すべきポイントも増えていきます。 このセッションでは、Amazon S3のセキュリティを高めるためのベストプラクティスを改めて学べました。 セッションのテーマと背景 パブリックアクセスのブロック(BPA)や全新規オブジェクトの暗号化などがデフォルトで有効になっており、データを安全に活用しやすいデフォルト設定が用意されています。 S3セキュリティ8つのベストプラクティス データ保護を実現するためのプラクティスとして以下が提示されました。 パブリックアクセスをブロックする 。 バケットキーを有効にする :SSE-KMS利用時のAWS KMSコストを最大99%削減。 統制を分離してスケールする 。 セキュリティ変更をモデル上でテストする 。 AWS Organizationsを活用する :RCP/SCPによる組織レベルの強力なガードレールを適用。 データ保護をアプリケーションにも広げる :チェックサムの活用など、アプリケーション側でもデータ保護を考慮する。 ログを有効にする :異常検知や自動修復などの対応を実装する。 耐久性とリカバリを事前に計画する 。 統制の分離とスケール ビジネスや組織の成長に合わせて、権限管理における統制を分離する必要があります。単一のバケットポリシーだけですべてを管理しようとすると保守が難しくなりやすいため、以下の機能を活用して権限を適切に委譲・分割する考え方が紹介されていました。 S3 Access Points :ユースケースごとに独自のポリシーを持たせる。 S3 Access Grants :特定のプリンシパルに対して、バケットやプレフィックスへのアクセス権をプログラムで付与する。 ABAC(属性ベースアクセス制御) :リソースやプリンシパル名ではなく、タグに基づいてセキュリティポリシーを定義する。 S3のセキュリティはバケットの設定だけで完結するものではなく、組織全体のガバナンスやアプリケーション側も含めて設計する必要があることを再認識しました。紹介されていた内容の中には、パブリックアクセスブロックや暗号化、ログ設定など、既存のバケットでもすぐに確認できる項目が多くあります。S3を利用している環境がある場合は、ぜひこの機会に設定状況を見直してみてください。 Advanced VPC Networking-知っておきたいAWSネットワークの最新動向:VPC関連サービスのアップデートを総整理-(CDN320) 紹介:𠮷富 このセッションでは、VPCやネットワーク関連サービスにおける2025年のアップデートを網羅的にキャッチアップできました。普段VPCを触っている人ほど、こんなにアップデートがあったのかと驚く内容だったと思います。紹介量が多く見応えのある内容だったので、ネットワーク構成に関わる方はぜひチェックしてみてください。特に気になったアップデートを2つ紹介します。 Amazon VPC Latticeのさらなる進化とカスタムDNS対応 VPC Lattice は、複数のVPCやアカウントをまたぐサービス間通信を論理的にグループ化(サービスネットワーク)してつなぐ機能です。これまではサービスやリソースに自動生成された一意のFQDNが割り当てられていましたが、2025年のアップデートとして、カスタムDNS名や設定可能なIPに関する機能強化が紹介されていました。自分で設定したDNS名によるアクセスが可能になり、既存のアプリケーションからのアクセスルーティングがやりやすくなります。 AWS Direct Connectとハイブリッド/マルチクラウド接続の強化 マルチクラウド接続を容易にする「AWS Interconnect Multicloud/Last Mile」が一般提供(GA)を開始しました。Direct Connectを経由してオンプレミス環境や他クラウド、AWS上のアプリケーションを接続しやすくなり、ハイブリッド/マルチクラウド環境におけるネットワーク設計の選択肢が広がりました。 VPC関連サービスでは、2025年だけでも150以上の新機能が追加されたそうです。このセッションはVPC周辺の最新情報をまとめてキャッチアップする良い機会になりました。 2024年のVPC関連サービス 2025年のVPC関連サービス ワークフローオーケストレーターにおける複雑性と非決定性のコントロール(CNS454) 紹介:𠮷富 ワークフローは事業の成長、システムの改修に伴って複雑になってしまうことがあります。さらに障害の監視・管理やリトライ処理といった例外的な処理の整備によりシステムが複雑化したり、それらの解決のためにあらゆるシステムでAI・MLとの結合が求められたりするようになっています。 このセッションでは、複雑化しがちなワークフローの管理手法と、AIエージェント特有の「非決定的」な挙動をどう扱い、どのようにアーキテクチャに組み込むべきかを学べました。 ワークフローの役割とAWSサービスの紹介 ワークフローは、複雑なシステム連携における例外処理・リトライ・並列処理・Map実行などの課題に対応し、可観測性(オブザーバビリティ)を高めるために用います。セッションでは、要件に応じたAWSの主要なワークフローオーケストレーションサービスの使い分けが紹介されました。 AWS Step Functions :GUI上でワークフローを視覚的に設計できるサービス。JSONataに対応しており、ステートマシン内でデータの変換・抽出・加工を行える。 AWS Lambda durable functions :JavaScript/TypeScriptやPythonなどで、長時間実行されるワークフローをコードとして記述できる仕組み。最大1年の実行に対応し、リトライや状態管理を扱いやすい。 Amazon MWAA :Apache AirflowをAWS上で利用できるマネージドサービス。DAG(有向非巡回グラフ)によってタスクの依存関係や実行順序を管理でき、AWS外のシステム連携にも向いている。 指揮者は歌わない 複雑性保存の法則 が示す通り、システムが持つ複雑さを完全に消し去ることはできません。重要なのは「複雑性をどこに置き、どうコントロールするか」であり、その基本方針がワークフローの分割です。そこであげられていたのは「指揮者は歌わない(関心の分離)」という設計原則です。 例えばStep FunctionsのChoice stateに複雑なメール検証のような検証ロジックを書くと、保守が困難になります。オーケストレーターは進行管理に徹し、ビジネスロジックはAWS Lambdaなどのコード側に切り出すべきです。 AIエージェントの非決定性を制御するアプローチ ワークフローにとってAIは、予期せぬ判断や無限ループを引き起こす可能性のある「非決定的」なシステムです。この揺らぎをシステムに組み込む方法として、決定論的ワークフローで包み込むアプローチが紹介されました。 Outer/Inner Loopパターン:AIの非決定論的な推論を、タイムアウトやリトライを備えたワークフローで制御。 Human in the Loop(HITL)パターン:重要な判断はAIに任せきらず人間の承認フローを挟む。 Workflow as Toolsパターン:既存の決定的なワークフローを、AIから安全なツールとして呼び出す。 また、Amazon BedrockなどのAIサービス利用時は、スロットリング対策としてExponential BackoffやJitterの活用が重要です。 Exponential Backoff リトライ間隔を指数関数的に増加させることで、サーバーの負荷を軽減する。 Jitter リトライ間隔にランダムな揺らぎを加えることで、同時リトライを防ぐ。 AIエージェントを利用する場合でも「処理を適切な単位で分割・委譲する」という設計の基本が大切なことを再認識しました。 まとめ 今回のAWS Summit Japan 2026を通じて、AI活用を前提にした開発・運用のあり方を改めて考える機会になりました。 生成AIは単なる開発支援ツールではなく、開発プロセス、組織文化、評価制度、ガバナンス、コスト管理まで含めて向き合うべきテーマになっていると感じました。一方で、AIエージェントのような非決定的な仕組みを安全に扱うためには、Human in the Loopや決定論的なワークフローによる制御など、従来から大切にされてきた設計原則も引き続き重要です。また、S3やVPCといった基盤領域についても、サービスの進化に合わせて継続的に設計や設定を見直す必要性を再認識しました。 今回得た学びを、日々の開発・運用だけでなく、社内でのAI活用推進にも活かしていきたいと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
.entry-content td:not(:first-child) { text-align: left; } はじめに こんにちは、ブランドソリューション開発本部ZOZOMO部FBZブロックの座間です。2025年4月にZOZOへ新卒入社し、現在は Fulfillment by ZOZO (以下、FBZ)のバックエンド開発を担当しています。最近はチームメンバーの影響でビリヤニにハマっています。 今回は、Claude Codeを活用してFBZのシステム構成図を自動生成・自動更新する仕組みを構築した事例を紹介します。 目次 はじめに 目次 背景 課題 作成に時間がかかる ルールや要件が明文化されていない ソースを修正しても構成図が更新されず、陳腐化することがあった Claude Codeを使った図の作成を試してみる ソースと構成図の間に中間ファイルを挟む 中間ファイルその1: 規約ファイル 中間ファイルその2: 構造ファイル その他のメリット CIによる自動更新 Skillsの活用 実際に作ってみる 得られた効果・知見 作成時間の短縮 ルール・要件の明文化 陳腐化の防止 一発で完璧な図を作れることはほとんどない 自動レイアウトツールとの相性がいい 新規の機能開発の設計に役立つ まとめ 背景 FBZのシステムはAWS上に構築されており、500以上のLambdaをはじめ、SQS、S3、DynamoDBなどのフルマネージドサービスで構成された、イベント駆動型アーキテクチャです。詳しいシステム構成については、以下のブログ記事で紹介されています。 techblog.zozo.com イベント駆動型アーキテクチャでは、システムの処理の流れを追うときや、システムの全体像を把握するうえで、システム構成図(以下、構成図)が非常に重要な役割を果たします。 これは、1つの機能を実現するために複数のリソースが連鎖的に関わっているためです。処理の全体像を頭の中だけで把握するのが難しいからこそ、リソース間の繋がりを俯瞰的に可視化する構成図が重要になります。 具体的には以下のようなシーンで活用されています。 障害発生時 新機能開発の設計 システムのリファクタリング 新人教育 これらのシーンで構成図が役立った経験のある方も多いのではないでしょうか。構成図は、多くの場面でその価値を発揮します。自分自身も1年前にチームへジョインした際、構成図はシステムの理解に大いに役立ちました。 FBZチームでは以下のような構成図が Miro のボード上で運用されていました。 課題 しかし、上記のような構成図は運用する上でいくつかの課題がありました。 作成に時間がかかる 構成図を作成した経験がある方はお分かりかもしれませんが、手動での構成図の作成にはそれなりに時間がかかります。特にシステムの全体像を把握していない状態での作成は骨の折れる作業であり、規模が大きくなるほどこのコストは重くなります。このような理由により、しばしば構成図の作成は後回しにされ、結果として構成図が作成されにくく、更新も滞るという状況が生まれていました。 ルールや要件が明文化されていない 構成図の作成にあたり、どのようなルールで図を作成すべきか明文化されていないことも課題でした。図を眺めているとそこには確かにルールが存在するように感じるものの、明文化されていないため作成者ごとに表現方法が異なり、図の統一感が失われていました。また、新規参入者はルールが不明瞭であるため図の作成に時間を要するという問題も生まれていました。 ソースを修正しても構成図が更新されず、陳腐化することがあった ソースを修正し本番リリースをしたにもかかわらず、変更内容が構成図へ反映されないまま放置され、陳腐化してしまうことがありました。その結果、ミスリードを引き起こし、構成図の信頼性を損なうことがありました。実際、私も入社した当初、更新されていない構成図を鵜呑みにしてしまい、タスクに必要以上の時間がかかってしまったことがありました。 Claude Codeを使った図の作成を試してみる まず、構成図の作成に時間がかかるという課題を解決するために、Claude Codeを使った図の作成を試してみました。 draw.io のような形式であればClaude Codeを使って直接図を生成できるため、手動で作成するよりも短時間で図を作成できました。 しかしこの直接生成する方法では、AIによって作成された図が、どのような指示・要件を根拠に生成されたのかを参照できません。また、システム構造をどのように捉え、図に落とし込んだのかについても明確ではありません。つまり、依然としてルールが明文化されていないという課題は解決されていませんでした。ルールが図の中に情報として埋め込まれたままブラックボックス化してしまい、何が図を生成するうえでの要件だったのかが曖昧になってしまっていたと言えます。 ソースと構成図の間に中間ファイルを挟む 上記の課題を解決するために、構成図に含まれる、人間の与える要件・チームの慣習といった「規約」と、構成図の「構造」を構成図の生成前にファイルとして出力する方法をとりました。このようなファイル群を 中間ファイル と呼ぶことにします。以下で中間ファイルの詳細を説明します。 中間ファイルその1: 規約ファイル まず、中間ファイルの1つとして、要件を明文化したファイルを挟むことにしました。このようなファイルを 規約ファイル と呼ぶことにします。規約ファイルには、構成図の規約・要件などの情報を記述します。例えば以下のようなものです。 アイコンのルール 図全体のフローの向き 矢印の種類と持たせる意味 ラベルの位置 プロンプトをファイルとして保存しておくイメージに近いです。 規約ファイルは全体に適用されるものと個別の構成図にのみ適用されるものとに分離しました。ディレクトリ構成は以下のようになります。 . ├── conventions.md # 全図共通の表現ルール └── views/ ├── <view1>/ │ └── conventions.md # view1固有の表現ルール └── <view2>/ └── conventions.md # view2固有の表現ルール プロジェクトルートに配置される conventions.md は全図共通のルールです。各View(1枚の構成図)のディレクトリ配下に配置されているものは、その図にのみ適用される規約です。 このように中間ファイルとして規約ファイルを生成過程に挟むことで、以下のメリットがあります。 図がどのような要件をもって生成されたのかが明確になる 図を作成するうえでの指示が蓄積される 規約ファイルには、過去に与えられた要件が蓄積されていき、図は蓄積された規約ファイルの内容をもとに生成されます。仮にその図についての要件を把握していない人であっても、過去のルールを踏襲して図の修正が行えるようになり、結果として安定した図を生成できます。 中間ファイルその2: 構造ファイル また、構造についてのファイルも中間へ挟むようにしました。ここでいう構造とはノードとエッジのグラフ構造およびその付随情報のことです。このファイルを 構造ファイル と呼ぶことにします。構造ファイルは例えば以下のような情報を持ちます。 どのリソースが存在するか それぞれのリソースの物理名 何が何に繋がるか どのような関係として繋がるか また、ディレクトリ構成は以下のようになります。 . ├── structure-spec.md # structure.yaml の仕様定義 ├── conventions.md # 全図共通の表現ルール └── views/ ├── <view1>/ │ ├── structure.yaml # view1 の構造情報 │ └── conventions.md # view1 固有の表現ルール └── <view2>/ ├── structure.yaml # view2 の構造情報 └── conventions.md # view2 固有の表現ルール 構造ファイルの本体は structure.yaml であり、各Viewでファイルを持ちます。また、構造ファイル自体の構造を定義したファイルは structure-spec.md であり、プロジェクトルートに配置され、Claude Codeが構造ファイルを生成する際に参照されます。例えば以下のような内容を定義します。 トップレベル構造: repos 、 view 、 description 、 metadata 、 nodes 、 edges の各フィールドの定義 nodesの仕様: id (snake_case、一意)、 type (CloudFormationのType名)、 name (図に表示する名前)、 description (任意、補足メモ) typeに使う値: AWS::Lambda::Function 、 AWS::S3::Bucket 、 AWS::SQS::Queue 等の一覧。AWS外は External edgesの仕様: from 、 to (nodesのidを参照)、 trigger (列挙値)、 label (任意、矢印ラベル) triggerの列挙値: schedule 、 s3_event 、 send 、 poll 、 stream 、 invoke 、 write 、 read 、 api_call 制約・ルール:捏造禁止、idはファイル内で一意、同じ物理リソースはビューをまたいで同じid これはあくまで一例であり、開発チームのニーズや要件に即した構成を定義できます。例えば、FBZのシステムでは複数のリポジトリにまたがる処理が多く、そのような処理の構成図を書くケースが想定されるため、 repos キーの値は配列を受けるように定義しています。 また余談ですが、 .claude/rules 配下に structure-spec.md のsymlinkを配置することで、Claudeにルールとして構造ファイルの定義を与えることができます。 この定義をもって出力されたファイルは例えば以下のようになります。 repos : - my-api-repo view : order_processing description : 注文データを受け取り、在庫確認・決済処理を経てDBに保存するフロー nodes : - id : api_gateway type : AWS::ApiGateway::RestApi name : 注文受付API - id : process_order type : AWS::Lambda::Function name : "{stage}-process-order" - id : sqs_payment type : AWS::SQS::Queue name : "{stage}-payment-queue" - id : execute_payment type : AWS::Lambda::Function name : "{stage}-execute-payment" - id : dynamo_orders type : AWS::DynamoDB::Table name : "{stage}-Orders" - id : payment_service type : External name : 決済サービス edges : - from : api_gateway to : process_order trigger : api_call - from : process_order to : sqs_payment trigger : send - from : sqs_payment to : execute_payment trigger : poll - from : execute_payment to : payment_service trigger : api_call - from : execute_payment to : dynamo_orders trigger : write このように構造をファイルとして分離することで、図がどのような構造をもって生成されたかを明文化できます。さらに、処理の流れをAIにコンテキストとして与えるケースなどで、ノイズを減らせます。 その他のメリット ここまでの中間ファイルの導入を振り返ると、これは構成図が持つ情報を責務ごとに分離した構造と捉えられます。構成図は以下のように表現できます。 システム構成図 = 構造情報 + 規約情報 + その他の情報(レイアウト情報) 構造情報と規約情報は人間が管理すべき情報です。一方、座標やノードのサイズといったその他の情報は人間が管理すべきでなく、レンダラやAIが補完すべき情報です。このように扱うべき情報を分離することは、さまざまなメリットをもたらします。 まず、変更の局所化です。構造の変更は structure.yaml 、規約の変更は conventions.md のみに閉じるため、変更の影響範囲が明確になります。これにより、レビューが容易になり、構成図の作成・更新を促進することが期待されます。 次に、特定のレンダラとの密結合を回避できるという点です。構造情報と規約情報は特定のレンダラに依存しないため、容易にレンダラの差し替えが可能になります。レンダラは種類ごとにメリットとデメリットが異なり、構成図を見る人や目的・シーンに応じて最善のものを選択することで効果を最大化できるため、この利点は大きいと考えています。以下にレンダラの一例と形式・メリット・デメリットについてまとめます。 レンダラ 形式 メリット デメリット draw.io XML GUI編集可能、AWS等のアイコン豊富 座標・サイズの制御が必要、差分レビューしにくい D2 テキストDSL テキストベースで差分が見やすい、自動レイアウト アイコンのカスタマイズが限定的、普及度が低い Mermaid テキストDSL Markdown内に埋め込める、GitHub等で直接レンダリング レイアウト制御が弱い、複雑な図には不向き CIによる自動更新 構成図の陳腐化を防ぐための方法として、リリース時に構成図を自動更新する仕組みをGitHub Actionsで構築しました。 特定のリポジトリでリリースが行われると、GitHub APIのrepository_dispatchを使って構成図管理リポジトリにイベントが送信されます。イベントを受け取った側では、Claude Code Action(Bedrock経由)が起動し、以下の流れで構成図を自動更新します。 対象ビューの絞り込み — 各ビューのstructure.yamlのreposフィールドを見て、リリースされたリポジトリに関係するビューだけを対象にする 差分の分析 — リリースタグ間のGit diffを取得し、structure.yamlのノード(id, name)と突き合わせて構成図への影響を判断する 構成図の更新 — 影響があるビューについて、IaCやハンドラーのコードを読み、structure.yamlを修正したうえで構成図を更新する PRの作成 — 変更内容をブランチにpushし、更新後のPNG画像を埋め込んだPRを自動作成する この自動更新においては、構造ファイルが効果を発揮します。画像やD2ファイルを直接管理していた場合、diffとの突き合わせは困難です。しかし structure.yaml のノード情報があれば、図のファイルを直接参照せずとも「この変更はどのビューに影響するか」を判断できます。 Skillsの活用 構成図の生成・更新のワークフローは、Claude Codeのスキル機能を活用して create-diagram というスキルにまとめました。 /create-diagram と入力するだけで、以下のステップが自動で実行されます(D2使用前提)。 Step やること 1 対象リポジトリのパス・範囲をユーザーに確認する。 2 IaC・アプリコードを読解してノード(リソース)とエッジ(接続・トリガー)を抽出し、構造情報を structure.yaml に書き出してユーザーにレビューをリクエストする。 3 ユーザーが指示した表現の要件(方向、グルーピング等)を conventions.md に記録する。 4 structure.yaml + conventions.md から D2 を生成し、PNGにレンダリングする。生成したPNGをAI自身がチェックし、問題があれば修正して再生成する。 Skillsを使用することで、コード読解から中間ファイル生成、図のレンダリングまでの流れを1つのスキルにワークフロー化できます。これにより、規約ファイル・構造ファイルを経由して構成図が生成されるという手順を自然に誘導できます。 実際に作ってみる FBZの商品連携の構成図をClaude Codeに生成させてみます。Miro上で運用されていた現状の構成図は以下です(ラベルはダミーとしています)。 テキストベースの図表記言語である D2 を用いて構成図を作成させてみます。なお、既存の構成図はClaude Codeに入力として与えません。最終的に生成された図は以下のとおりです(ラベルはダミーとしています)。 既存の構成図と構造自体は大きく変わりませんが、より整理された形で構成図が生成されています。また、規約ファイルに以下のような指示を入れたため、図の矢印が意味をもって表現されています。 ## エッジ(trigger → 色・線種) 矢印は「色」で**役割**を、「線種」で**種別**を表す。 | trigger | 役割 | 色 | 線種 | | ---------------------------------------------------------------------------------------- | -------------------------------- | -------------------- | ---- | | ` schedule ` / ` s3_event ` / ` poll ` / ` invoke ` / ` stream ` / ` webhook_event ` / ` api_request ` | 制御フロー(処理を起動する主動線) | 黒 ` #000000 ` | 実線 | | ` send ` / ` write ` | データの書き込み・送信 | グレー ` #888888 ` | 実線 | | ` read ` | データの読み取り(参照のみ) | 薄いグレー ` #aaaaaa ` | 実線 | | ` api_call ` | 外部 API 呼び出し | 黒 ` #000000 ` | 破線 | 得られた効果・知見 冒頭で挙げた3つの課題に対して、本記事で紹介した仕組みがそれぞれどのように作用したかを振り返ります。 作成時間の短縮 Claude Codeによるコード読解と構造ファイルの自動生成、さらにスキルを活用したワークフロー化により、構成図の作成時間を大幅に短縮できました。以前はシステムの処理の流れを把握し、手動で図に起こすまでに1時間以上要するケースもありましたが、現在は微調整も含め10分程度で対応できるようになり、従来のおよそ6分の1の作業時間の短縮を実現しました。 ルール・要件の明文化 規約ファイル( conventions.md )を中間ファイルとして導入したことで、図の作成ルールが明文化されるようになりました。これにより、作成者ごとに表現方法がばらつくという問題が解消され、誰が作成・更新しても一貫した図を生成できるようになりました。また、生成した図の失敗パターンを規約に追記して育てていくことで、同じミスを繰り返さない仕組みが自然と構築されていきました。 陳腐化の防止 GitHub Actionsによる自動更新の仕組みを導入したことで、リリースのたびに構成図が自動的に更新されるようになりました。構造ファイル( structure.yaml )にリポジトリ情報が紐づいているため、どのビューに影響があるかを自動判定でき、更新漏れを防止できます。これにより、構成図が実態と乖離するリスクが大幅に低減しました。 また、知見として以下が得られました。 一発で完璧な図を作れることはほとんどない 指示の曖昧さやClaude Codeの誤認により、細かい部分が誤った図になることは珍しくありませんでした。対話的に規約ファイルや構造ファイルを育てていく中で、理想とする構成図を完成させるケースが大半でした。また、全図にまたがる規約ファイルにも、要件や頻発する失敗パターンを明記することで、次回以降の生成品質が着実に向上していきました。 自動レイアウトツールとの相性がいい draw.ioのように座標を指定して図を定義するツールでは、どうしても微調整に多くの時間がかかりました。D2のようにレイアウトを自動で行うツールはその煩わしさから解放してくれ、安定した図を生成できることが多かったです。 新規の機能開発の設計に役立つ 従来、構成図は開発後に作成するドキュメントとして扱われることが大半でした。しかし、この仕組みを活用すれば、実装前に構造ファイルを作成し、構成図を設計書として運用することも可能になります。構造ファイルはYAMLで記述されたノードとエッジの定義であり、draw.ioのXMLのようなレイアウト情報を含みません。そのため、別のAIエージェントに設計意図をコンテキストとして与える際にも、ノイズの少ない効率的なインプットとして機能します。 まとめ 本記事ではClaude Codeを活用して構成図を自動更新する仕組みを構築した事例を紹介しました。この取り組みを通じて感じたのは、AIに仕事を任せるには「何を人間が管理し、何をAIに委ねるか」の境界設計が重要だということです。中間ファイルによる責務の分離は、まさにその境界を明確にする手段であり、これにより諸課題を解決できました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。データシステム部・MA推薦ブロックの住安( @kosuke_sumiyasu )です。 私たちのチームは、ZOZOTOWNのメール・LINE・プッシュ通知といったマーケティングオートメーション(MA)の推薦システムを開発・運用しています。目指しているのは、ユーザーひとりひとりに最適な配信を届けることです。 ZOZOTOWNで本番運用されている推薦モデルは、価格・ブランド・カテゴリ・カラーといった テーブル特徴量 のみを学習に用いていました。そのため、商品画像が持つ視覚情報(シルエット・質感・カラー・柄)を活用できていませんでした。「オーバーサイズシルエット」や「光沢感」「チェック柄」といった、人が画像から読み取れる「見た目の好み」を推薦に反映できていなかったのです。 下図は、四角い縁のメガネを好むユーザーを例に、画像から「見た目」を捉えることで目指した推薦の姿を示したものです。従来のモデルではカテゴリは「メガネ」で合っていても、丸縁やサングラスといった「見た目」の異なる商品が混ざってしまいます。一方、画像から「見た目」を捉えられれば、ユーザーが好みそうな四角い縁のメガネを中心に推薦できます。 そこで私たちは、 商品画像から視覚的特徴を捉えた画像特徴量を生成する仕組み を構築し、既存の推薦モデルに特徴量として組み込むことで、「見た目の好み」を捉えるマルチモーダル推薦システムを実現しました。実際に、この推薦モデルをあるメール配信施策に適用しました。A/Bテストの結果、メール経由サイト流入率(CTR)・メール経由購入率(CVR)・経由売上(メール経由で発生した売上)のすべてで有意な改善が得られました。しかもこの画像特徴量は特定の施策にとどまらず、全社のどの推薦・検索モデルからでも利用できる共通の基盤として提供しています。 本記事では、この取り組みの背景にある課題、画像特徴量を生成・提供する仕組み、そして推薦モデルへの特徴量の組み込みで工夫した点を中心に紹介します。マルチモーダルな特徴量を推薦に活かしたい方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 前提となる推薦システム 課題1: 推薦モデルが「見た目」を捉えられていない 課題2: 画像Embeddingを全社で利用できる基盤がない アプローチの全体像 画像Embeddingを安定供給する仕組みの構築 差分更新によるコスト削減 モデル・バージョンを管理し、VIEWで全社へ提供する 推薦モデルのマルチモーダル化によるパーソナライズ精度向上 モデルの選定 事前学習済みモデルを使用した理由 Item Towerへの組み込み Gated Multimodal Unit(GMU)で画像の寄与度を動的に制御する 特徴量単位の Dropout(Feature/Modality Dropout)で特定特徴量への依存を抑える 定量評価(オフライン) 効果 「見た目の好み」の反映による主要指標の改善 全社共通の画像Embedding基盤の整備 まとめ 今後の展望 最後に 背景・課題 前提となる推薦システム ZOZOTOWNのMAにおけるパーソナライズされたアイテム推薦の一部では、 Two-Towerモデル を使用しています。これは、ユーザーを表現するUser Towerと商品を表現するItem Towerの2つのニューラルネットワークからなります。学習済みの各Towerを使うことで、ユーザーと商品の特徴量をそれぞれEmbeddingに変換できます。このEmbeddingは、特徴を捉えた数値ベクトルで、意味の近いものほどベクトルも近くなる性質を持ちます。両Towerの出力を同じ潜在空間上にマッピングするように学習することで、ユーザーとアイテムの近さをコサイン類似度で測れるようになります。推薦時は、任意のユーザーのEmbeddingと各商品のEmbeddingの類似度を計算し、類似度が高い商品から順に推薦します。 ZOZOでは、このEmbeddingを Embedding基盤 として一元管理し、どの部署からでも利用できるようにしています。私たちの 汎用推薦システム も、この基盤を使用して配信する商品を選定しています。 課題1: 推薦モデルが「見た目」を捉えられていない このItem Towerの特徴量は、価格・ブランド・カテゴリ・カラーなどの テーブル特徴量 のみでした。そのため、 ユーザーの視覚的な嗜好を推薦に反映できない という課題が残っていました。同じカテゴリ・ブランドの商品でも、ユーザーが好むシルエットや柄、質感はさまざまです。しかし従来の推薦モデルは見た目の情報を持たないため、「興味のあるカテゴリやブランドは合っているけれど、見た目の趣味は違う」という結果になりがちでした。例えば筆者は、結婚式用に無地のパステルカラーのネクタイを探していたのですが、柄物ばかりが推薦されてしまい、改善の余地を感じていました。 課題2: 画像Embeddingを全社で利用できる基盤がない 商品画像が持つ視覚情報を推薦に活かすには、それを数値ベクトルに変換した 画像Embedding として扱うのが有効です。しかし当時は、商品画像すべてを画像Embedding化する仕組みも、それを全社で共有する基盤も存在していませんでした。そのため、各チームが検索や推薦で画像特徴量を使いたくても、それぞれが独自に実装する必要があり、開発工数の増加や品質のばらつきが生じます。そこで本プロジェクトでは、 画像Embeddingを常に使える状態で組織に提供し続ける基盤 を構築し、それを推薦モデルに組み込むことで「見た目の好み」を捉えられるようにすることを目指しました。 アプローチの全体像 課題を解決するために、大きく2つに取り組みました。 画像Embeddingを安定供給する仕組みの構築 :商品画像から視覚的特徴を表す画像Embeddingを日次バッチで生成し、BigQueryのVIEWで提供する。どの推薦・検索モデルからでも、常に最新の画像特徴量を利用できる 推薦モデルのマルチモーダル化によるパーソナライズ精度向上 :その画像Embeddingを推薦モデルのアイテム特徴量として組み込み、「見た目の好み」を捉えてパーソナライズ精度を高める マルチモーダル推薦は、次の3つのパイプラインで実現しています。 パイプライン 役割 generate-image-embedding 商品画像から画像Embeddingを生成し、BigQueryへ保存する train-product-recommendation 画像Embeddingを特徴量に加えてTwo-Towerモデルを学習する generate-product-embedding 学習済みモデルでユーザー・商品のEmbeddingを生成する このうち、train-product-recommendationとgenerate-product-embeddingは、もともと運用している既存のパイプラインです。今回はそこに、画像Embeddingを生成するgenerate-image-embeddingを新たに追加しました。あわせて、train-product-recommendationのモデルアーキテクチャと入力特徴量を変更しています。 これらのパイプラインで生成したユーザー・商品のEmbeddingを使って、施策ごとに配信商品を選定します。 以降では、本記事の中心である「画像Embeddingを安定供給する仕組みの構築」と「推薦モデルのマルチモーダル化によるパーソナライズ精度向上」を詳しく紹介します。 画像Embeddingを安定供給する仕組みの構築 画像Embeddingの生成パイプラインは、 Agent Platform Pipelines(旧Vertex AI Pipelines) 上に実装し、日次バッチで実行しています。全体像は次のとおりです。 処理は大きく4ステップで構成されます。 Embedding化の対象とするアイテム集合を取得する 商品画像を取得し、Cloud Storage(以下GCS)へ保存する 事前学習済みの画像モデルで画像Embeddingを生成する 生成したEmbeddingをBigQueryへ保存し、VIEWとして提供する 画像Embeddingの生成(ステップ3)には、 Hugging Face で公開されている事前学習済みモデル( SigLIP 2 )をGPU上で利用しています。画像の保存先にはGCS、Embeddingの保存先にはBigQueryを使っています。なお、SigLIP 2を採用した理由は、のちほど「モデルの選定」で説明します。 この中で工夫した「差分更新によるコスト削減」と「全社への提供」を順に紹介します。 差分更新によるコスト削減 ZOZOTOWNで扱う商品画像は、サイト上でアクティブな商品に限っても数千万枚の規模にのぼります。これらをすべてEmbedding化すると計算コストが大きいため、各商品(商品×カラー)につき代表の1枚に絞ってEmbedding化しています。それでも対象は数百万枚あり、さらに新着商品を考えると、毎日およそ数十万枚を新たにEmbedding化する必要があります。 これらを毎日すべて計算し直すと、GCSからマシンへ画像を転送するオペレーション料金や、推論時間の増加に伴うマシン料金がかさみ、個々は小さくても積み重なると無視できないコストになります。そこで、すべての画像を毎日計算し直す 全件更新 ではなく、未処理分のみを計算する 差分更新 を採用しています。具体的には、次の2つのステップで「まだ処理していないものだけ」を対象にします。 画像の保存(ステップ2) :すでにGCSへダウンロード済みの画像は除外し、未取得の商品画像のみを保存する Embedding生成(ステップ3) :すでに計算済みのEmbeddingは除外し、未計算の商品画像のみを対象とする これにより、新着商品だけを処理すればよくなり、ダウンロードコストと計算コストを抑えられます。また、GCSはAgent Platform Pipelinesの実行リージョンと同じRegionalバケットを使うことで、リージョン間レプリケーション費用やエグレス料金も抑えています。 モデル・バージョンを管理し、VIEWで全社へ提供する 画像Embeddingを全社の共通資産として提供するうえで重要になるのが、 モデルとバージョンの管理 です。精度改善のためにモデルを差し替えたり、複数のモデル・バージョンをA/Bテストで並行させたりすることがあります。そのたびに、利用者が「いまどのモデル名・バージョンが最新で有効か」を追いかけてクエリを書き換えるのは負担が大きく、更新への追従漏れも起こる可能性があります。そこで、利用者がそれらを意識しなくても、常に最新の有効なEmbeddingを取得できる仕組みを用意しました。 具体的には、次の3つのテーブル・VIEWでモデルとバージョンを管理しています。 テーブル / VIEW 種別 役割 product_image_embedding_raw テーブル 生成したEmbeddingを、商品ID・モデル名・モデルバージョン・生成日とあわせて追記する。過去分も残すため、複数のモデル・バージョンが共存する model_manifest テーブル 提供対象とするモデル・バージョンにアクティブフラグを立てる product_image_embedding VIEW model_manifestのアクティブなバージョンに絞り、商品ID × モデル名ごとに最新のEmbeddingを返す product_image_embedding_rawテーブルを直接参照する場合は、利用者がクエリのたびにモデル名やバージョンをWHERE句で指定する必要があります。これをVIEWにまとめることで、利用者はproduct_image_embeddingのVIEWを参照するだけで、常にアクティブなモデル・バージョンの最新Embeddingを取得できます。一方でproduct_image_embedding_rawテーブルにはバージョンごとの履歴が残ります。そのため、モデルのA/BテストではTreatment用のVIEWを用意することで、特定バージョンを指定した検証にも対応できます。 この仕組みによって、追跡性と再現性を確保しつつ、A/Bテストにも対応できます。当初の施策にとどまらず、検索や他の推薦面でも安心して利用できる全社共通の資産として提供できるようになりました。 推薦モデルのマルチモーダル化によるパーソナライズ精度向上 画像特徴量を活かしてパーソナライズ精度を高めるために工夫した点を紹介します。工夫したポイントは2つあります。1つ目が「画像Embedding生成モデルの選定」、2つ目が「生成した画像Embeddingを推薦モデルに組み込む方法」です。特に後者が重要で、画像特徴量は単純に足すだけでは効果が薄く、シンプルな2つの工夫を加えることでモデルの精度を大きく改善できました。 モデルの選定 画像Embeddingの生成には、事前学習済みの SigLIP 2 を採用しています。SigLIP 2は、 CLIP から派生したモデルです。CLIP系のモデルは、画像を扱うImage Encoderと、説明テキストを扱うText Encoderの2つから構成されます。学習時は、対応する画像と説明テキストのペアは近づけ、対応しないペアは遠ざけます。こうした対比的な学習をcontrastive学習と呼び、これにより画像と言語が同じ空間で結びつきます。なお、CLIPがsoftmaxベースの損失を用いるのに対し、採用したSigLIP系はこれをsigmoid損失に置き換えている点が特徴です。 画像が言語の意味と対応づけて学習されるため、得られる画像Embeddingは「柄」「シルエット」「質感」といった視覚的特徴を捉えやすいと考えられます。 CLIP系のモデルの中でSigLIP 2を選んだのは、論文記載のとおり、ゼロショットの分類・検索タスクのベンチマークで良い結果が示されているためです。 事前学習済みモデルを使用した理由 ZOZOの商品画像でファインチューニングする選択肢もありましたが、今回は事前学習済みモデルをそのまま使う方針としました。理由は次の3点です。 テキスト側の教師データがない :CLIP系の追加学習に必要な、画像とペアになる説明テキストを大規模に用意できていない まず有効性を検証したい :画像特徴量が推薦に効くかは未検証のため、まずは低コストに効果を確かめたい 基盤モデルの進化が速い :将来、高性能なモデルへ載せ替える余地を残したい Item Towerへの組み込み 画像Embeddingは、まずItem Towerの入力としてそのまま使えるように整えます。下図のように、画像EmbeddingをItem Towerの入力特徴量の1つ(image_embedding)として追加します。User Tower側は変更せず、Item Tower側にのみ画像特徴量を加えています。 使用した画像Embeddingは768次元です。これを価格やカラーといった他のテーブル特徴量とそのまま結合すると、画像だけで次元の大部分を占めてしまい、他の特徴量の影響が埋もれてしまいます。そこで、画像Embeddingを2層の多層パーセプトロン(768 → 256 → 128)で128次元に圧縮してから、他の特徴量と結合します。これにより、画像とテーブル特徴量の次元のバランスを取りつつ、画像から推薦に効く表現を学習できるようにしています。 ただし、この「圧縮してそのまま結合する」方法だけでは、期待したほどの精度改善が得られませんでした。そこで、さらなる精度改善に向けて次の2つの機構を導入しています。 Gated Multimodal Unit(GMU)で画像の寄与度を動的に制御する 次元を揃えて結合するだけでは、画像をどれだけ重視するかが全商品で一律になってしまいます。しかし本来、画像をどれだけ重視すべきかは商品によって異なります。例えば、Tシャツは柄が選択の決め手になるため画像を重視したい一方、靴下はカラーやブランドといったテーブル特徴量で十分なことが多いです。そこで、画像特徴量の寄与度だけをアイテムごとに動的に調整できるよう、 GMU を参考にしたゲート機構を導入しました。 論文の2モダリティ版GMUは2つのモダリティをゲート値で線形補間するため、片方を強調するともう片方が抑制されるトレードオフを持ちます。これに対して本実装は、 他の特徴量はそのままで、画像特徴量にのみsigmoidゲートを掛ける一方向型のゲート を採用しました。これは、2モダリティ版GMUからもう片方を抑制する項を取り除いた独自の変種で、アイテムごとに画像特徴量の重みづけだけを調整できます。これにより、カテゴリやブランドなどのアイテム情報から、その商品で画像特徴量をどれだけ重視するかを動的に決められます。 一方向型にした理由は、既存のテーブル特徴量(カテゴリ・価格など)は複数のA/Bテストで有効性が実証されており、その表現力をそのまま維持した状態で、画像特徴量を追加したかったためです。 特徴量単位の Dropout(Feature/Modality Dropout)で特定特徴量への依存を抑える もう1つの工夫が、学習のたび、入力の一部をランダムにマスクすることで、特定の特徴量への過度な依存を防ぐDropoutです。よく使われるDropoutは個々のニューロン単位でマスクしますが、今回は特徴量単位でマスクする Feature Dropout を行います。なかでも画像Embeddingは、モダリティ全体を1単位としてマスクし、これを特に Modality Dropout と呼びます。実際には、テーブル特徴量(価格・ブランド・カテゴリ・カラーなど)は各フィールドを、画像Embeddingはモダリティをまるごと1つの塊として、それぞれ独立かつランダムにマスクします。 なぜこれが効くのかを、カラーと画像Embeddingを例に説明します。カラーからもユーザーが好む大まかな色味は学習できますが、画像Embeddingを使えば、より詳細なカラーやシルエット、柄まで捉えられる可能性があります。しかし画像Embeddingは複雑で扱いが難しいため、モデルは学習しやすいカラーにばかり頼り、画像Embeddingを十分に活用しないことがあります。そこでカラーをマスクすると、モデルは画像Embeddingからも学ばざるを得なくなり、画像Embeddingの特徴が使われない状態を防げます。逆に、画像Embeddingに偏りすぎる場合も画像Embeddingをマスクすれば、カラーなどのテーブル特徴量から学べます。こうして、どちらか一方に偏らず、画像Embeddingも含めた幅広い手がかりをバランスよく使う、堅牢なモデルになります。 定量評価(オフライン) これらの工夫により、画像特徴量なしのベースラインと比べて、オフラインのRecall@100は段階的に改善しました。 構成 Recall@100(ベースライン比) ベースライン(画像特徴量なし) — + 画像特徴量あり(単純結合のみ) +1.06% + 画像特徴量あり(Feature/Modality Dropout) +11.3% + 画像特徴量あり(Feature/Modality Dropout + GMU) +12.0% 効果 「見た目の好み」の反映による主要指標の改善 構築したマルチモーダル推薦システムを、1配信あたり約700万人を対象とするメール配信施策のアイテム推薦ロジックに適用し、A/Bテストで効果を検証しました。Control(画像Embeddingなし)とTreatment(画像Embeddingあり)を比較し、CTR・CVRはz検定、経由売上はt検定を用いて有意水準5%で評価しました。 その結果、 CTR・CVR・経由売上のすべてで統計的に有意な改善 が確認され、TreatmentがControlを上回りました。以下はTreatmentのControlに対する相対改善率です。 指標 相対改善率 有意差 CTR(メール経由流入数 / 配信数) 約 9.9% あり(勝ち) CVR(メール経由購入数 / 配信数) 約 14.3% あり(勝ち) 経由売上(メール経由の受注金額 / 配信数) 約 10.3% あり(勝ち) ユーザーの「見た目の好み」を捉えた推薦が、実際の流入・購入・売上の改善に結びつくことを確認できました。この結果を受けて本番リリースを決定し、現在は本番環境で稼働しています。 全社共通の画像Embedding基盤の整備 共通基盤の構築により、画像Embeddingを使いたいチームは、生成パイプラインを自前で用意する必要がなく、VIEWを参照するだけで常に最新のEmbeddingを利用できます。これにより、検索や他の推薦面を担当するチームも、開発工数をかけずに効果検証を始められます。さらに、基盤側でモデルを改善すれば、利用側は追加対応なしでその精度向上を受けられます。モデルの差し替えやバージョン管理を基盤の内側に閉じ込めたことで、利用者は中身を意識せずに使い続けることができます。 まとめ 本記事では、商品画像の視覚情報を推薦に活かすマルチモーダル推薦システムの構築を紹介しました。事前学習済みモデルを活用し、少ない工数で画像特徴量を追加して、その有効性まで確かめられました。さらに、生成した画像特徴量を全社で利用できる資産として提供できたことも、大きな成果だと考えています。これにより、画像特徴量を試したい部署は、自分たちで実装しなくてもすぐに効果検証を始められます。そして「画像」という新しい特徴量の軸を手に入れたことで、ここを足がかりに推薦をさらに良くしていけるはずです。 今後の展望 画像Embeddingのさらなる活用と推薦の精度向上に向けて、次のような展開を考えています。 画像Embeddingの活用箇所の拡大 :整備した共通基盤を活かし、検索・他推薦面へも展開する 画像ベースの候補生成への活用 :閲覧・購入した商品と視覚的に似た商品を、推薦候補とする モデルの高度化 :事前学習済みモデルから、ZOZOのデータでファインチューニングしたモデルへ置き換え、ファッションに特化した表現の獲得を目指す 画像の前処理の工夫 :商品領域をバウンディングボックスで検出してクロップ(切り出し)し、周辺の背景ノイズを除いて視覚的特徴をより正確に捉える 最後に ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ZOZOTOWN開発本部ZOZOTOWN開発2部Androidブロックのにしみーです。 本記事では、レガシーなFragmentをKotlin + Jetpack Compose + MVVMベースの構造へリファクタリングした取り組みを紹介します。 対象画面のFragmentはJava製の共通基底クラスを継承しており、ロジック・状態管理・データ取得が基底クラスとFragmentに混在した構造でした。この構造を解体しながらリファクタリングを進めるにあたり、私たちは2つのアプローチを採用しました。1つは、実装前にAIと協働で「仕様調査 → 設計書 → タスク分解」のドキュメントを書き起こす、設計書から書き始める進め方です。もう1つは、ViewModelやRepositoryを後回しにしUIを先にComposeへ移行し、動かしながら段階的に進める工程設計です。 レガシーなAndroid実装をモダンな構造に置き換えている現場、肥大化した基底クラスと向き合っている方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 解体したい対象 辛かった点 ── レガシーの実像 共通基底クラスへの強い依存 Object型のリストに見出しとアイテムが混在 イベントバスとRxJava 2による状態伝播 Fragment内でのDB直接アクセス 解決の取り組み 設計書から書き始める ── 実装より先にドキュメントを書ききる なぜ実装より先に設計書を書ききる必要があったか ドキュメントの中身 AIを雛形作成に活用する UIから先に組み立て、動かしながら段階的に進める ── ハードコードで動く画面を先に作る なぜUIから先に組み立てるのか フェーズ分割と段階的接続 リファクタリング後のアーキテクチャ sealed interfaceで画面状態を表現する sealed interfaceでリストアイテムを型安全に表現する AI活用Tips Tips 1:タスク分解書を「AIへの依頼の共通言語」にする Tips 2:設計ドキュメント自体もGit管理する Tips 3:レビューで得た知見を「AI参照用」のドキュメントに蓄える Tips 4:AIが苦手なところを人が補う まとめ 背景・課題 解体したい対象 今回対象にしたのは、ZOZOTOWN Androidアプリの「公式ショップから探す」画面です。この画面では、キーワード検索やソート条件をもとにショップ一覧を絞り込めます。検索やソートのロジックをアプリ内で保持していたため、実装が複雑になっていました。 また、この画面のFragmentはJava製の抽象クラスを共通基底として継承する形で実装されていました。基底クラス側に多くの責務がまとめられており、たとえば画面表示・状態管理・データ取得・アニメーション制御まで、多くの実装が集約されていました。サブクラスとなる画面側のFragmentは、基底クラスが定義する抽象メソッドを実装することで成り立っていました。 このような「基底クラスありき」の構成は、複数の画面に対する共通化を素早く実現する手段としては有効です。しかし長年運用するうちに、この土台に責務が積み重なり1000行を超える規模に膨らんでいきました。画面ごとの個別事情も吸収しきれず、気づけば画面1つの挙動を読み解くにはまず基底クラスを読み込まなければならない状態になっていたのです。 今回のリファクタリング対象は、その共通基底クラスを継承した画面のうちの1つです。共通基底クラスごと解体するわけではなく、対象画面のFragmentをKotlin + Jetpack Compose + MVVMベースの構造に作り直すのがスコープでした。Fragmentに集中していたロジックは、Composable・ViewModel・Repositoryへ分散させていきます。ただし、対象画面を作り直すには、基底クラスが担っていた責務をどこに引き取らせるかを先に決めなくてはなりません。そのため、土台ごと向き合いながら作り直す必要がありました。 辛かった点 ── レガシーの実像 リファクタリング着手前に、私たちが向き合わなければならなかった課題を共有しておきます。 共通基底クラスへの強い依存 共通基底クラスによってサブクラスでは多数の抽象メソッドの実装が必要になっていました。サブクラスは基底クラスが定めた枠組みに乗るだけで画面が成立するため、共通機能の修正は基底クラス1箇所に閉じるという保守性の利点もありました。 ただし、抽象メソッドの多くは「Viewを返すだけ」のgetterで、protectedな変数を介して基底クラスとサブクラス間で状態を共有していました。構造を抽象化して示すと次のとおりです。 abstract class BaseListFragment extends Fragment { protected List<Object> displayData = new ArrayList<>(); protected String searchText = "" ; abstract protected RecyclerView getRecyclerView(); abstract protected EditText getSearchEditText(); // ...続く } サブクラスから見れば、protected変数はいつ・どこから書き換えられているかが追跡しづらく、画面単体での挙動を読み解くのは困難でした。 Object型のリストに見出しとアイテムが混在 リスト表示にはObject型のリスト( List<Object> )が使われており、A〜Zなどの見出し行とその配下のアイテムが同じリストに混在していました。この構造は1つのRecyclerViewで「見出し + アイテム」を扱えるため、表示順序や挿入・削除を単一のリスト操作で表現できる利点がありました。 ただ、型情報は失われており、RecyclerViewのAdapter側ではキャストして表示を切り替えていました。コンパイル時の型チェックが効かず、リスト操作のたびにキャストが必要でコード補完やリファクタリングツールの支援も受けにくい状態でした。 イベントバスとRxJava 2による状態伝播 性別の変更やアイテム選択などのイベントはOttoによる静的なイベントバスで伝播され、購読側は @Subscribe で受け取っていました。データ取得はRxJava 2の Observable で書かれており、 subscribeOn / observeOn によるスレッド切り替えや map / flatMap での非同期処理を宣言的に書けました。これらは当時のAndroidで広く採用されていた、Fragment間通信と非同期処理の主流パターンでした。 一方で、イベントがコードを横断して飛び交うため、状態の流れを追うのに時間がかかりました。Disposableの解放管理もFragment側で行う必要があり、リソースリークの温床にもなりがちでした。 Fragment内でのDB直接アクセス Repository層は実質的に存在せず、FragmentからRoomのDAOを直接呼び出していました。レイヤーが少ない分、データ取得の実装はシンプルで見通しが良いため過剰な抽象化を避ける観点では合理的にも見えます。 しかし、検索ロジックもFragment内に実装されていたため、ユニットテストで切り出すにはFragment全体を起動する必要がありました。ビジネスロジックがプレゼンテーション層に貼り付いている状態は、テスタビリティを大きく損ねていました。 これらが組み合わさることで、画面1つの挙動を変えるだけでも基底クラスを読み解き、状態の流れを追い、影響範囲を見積もる作業が必要でした。Compose化や機能改修を進めるうえで、これは大きな足かせになっていました。 解決の取り組み 設計書から書き始める ── 実装より先にドキュメントを書ききる 通常、画面のリファクタリングは「コードを読みながら少しずつ手を入れていく」というアプローチで進められることが多いと思います。今回もそうしたかったのですが、共通基底クラスごと向き合う必要があったためそれは難しい判断でした。 なぜ実装より先に設計書を書ききる必要があったか 基底クラスは対象の画面以外にも継承されていました。場当たり的に対象画面側だけを書き換えていくと、他画面への影響や基底クラスから引き剥がした責務の置き場所が曖昧になっていきます。途中まで進めてから「結局この責務はどこに置けばよかったのか」と立ち止まると、それまでの作業がやり直しになるリスクもあります。 加えて、私たちはこのリファクタリングをAIエージェントと分担して進めることを前提にしていました。AIに任せる範囲を明確にするには、私たちとAIが共通言語として参照できるドキュメントが必要です。コードベースだけを見せて「いい感じにリファクタリングして」と頼んでも、設計判断はぶれてしまいます。 そこで私たちは、実装前に「仕様調査」「リファクタリング設計書」「タスク分解」の3点をドキュメントとして書ききることにしました。 ドキュメントの中身 それぞれのドキュメントは次の目的で書きました。 仕様調査は対象画面の現状の挙動を網羅的に書き起こすドキュメントです。UI構成、状態管理、データフロー、依存関係、アナリティクス、現状の問題点までを1つの「現状把握ドキュメント」としてまとめました。 リファクタリング設計書は、移行後のアーキテクチャを書き起こすドキュメントです。Repository/ViewModel/UIの責務分担、データ構造、フェーズ分割、各フェーズでの動作確認ポイントまでを記述しました。基底クラスが担っていた責務の配置先をこの段階で決定しました。 タスク分解は、設計書に沿って実装タスクを「画面表示」「フィルタ切り替え」「ソート切り替え」「検索」など、機能単位の細かい粒度に分解したドキュメントです。各タスク間の依存関係を明示し、並行して作業できる範囲やクリティカルパスも明記しました。 AIを雛形作成に活用する これらのドキュメントは、すべてを人が一から書いたわけではありません。私たちは主にClaude Codeを活用し、既存コードを読ませて雛形を出力させました。その雛形をもとに、人が事実関係をチェックしながら整えていく進め方を採用しました。AIが正しく拾えなかった箇所は人が補い、AIの捉え方が良かった部分は積極的に採用します。 ドキュメントを「AIと人の両方が読めるもの」として整えておくことは、実装フェーズで特に効果的でした。タスク分解書ではレビューしやすい単位を1つのタスクとして切り出しており、おおむね「1タスク = 1PR」の粒度になっています。実装フェーズではこのタスクごとにIssueを作成し、Issueの内容を参照してAIへ実装を進めてもらう運用としました。Issueには対応するタスクの記述を書き、必要に応じて設計書の該当箇所も転記しておくことで、AIは「どこから手をつけるべきか」「どこまでがスコープか」を迷わず進められるようになりました。 設計書を書く工数自体は決して小さくありませんが、実装フェーズに入ってからの迷いを減らすことができました。 UIから先に組み立て、動かしながら段階的に進める ── ハードコードで動く画面を先に作る 設計書を書ききった後、いよいよ実装フェーズに入ります。ここでは動作確認がしやすいように工夫して進めました。 なぜUIから先に組み立てるのか レガシーなFragmentをモダンな構造に置き換えるとき、素直にやろうとすると「UIも、状態管理も、データ取得も、イベント処理も全部一度に入れ替える」ことになります。これは変更差分が大きく、レビューと動作確認の両面で辛い状態を生みます。途中でバグが出ても、UI側のミスかロジック側のミスかが切り分けづらくなります。 そこで私たちは、まずUI部分だけをComposableとして組み立てるところから始めました。ViewModelやRepositoryの繋ぎ込みは後回しにして、ハードコードした仮のデータを渡して動く画面を先に作る進め方です。Composable関数は引数に渡されるデータの型さえ揃っていれば描画できるため、ロジックが未実装でもUIレイアウトやスクロール挙動などはすべて確認できます。 フェーズ分割と段階的接続 タスク分解書では実装フェーズを段階的に分けていました。順序はおおむね次のとおりです。 フェーズ1 :空のFragmentとComposable Screenの土台を作る フェーズ2–4 :UIを仮データで組み立てる フェーズ5 :ViewData/UiState/Repository/ViewModelの骨格を作る フェーズ6 :機能ごとに「ロジック実装 + UI接続 + Unit Test」を1セットで進める フェーズ7 :統合テスト・動作確認 各フェーズで意識したのは常にアプリが動く状態を保つことです。UIを組み立てている段階でも仮データを渡してアプリを起動すれば、実機でUIレイアウトやインタラクションを確認できます。フェーズ6の機能ごとの繋ぎ込みでも、1機能ずつ実データに接続していくため繋ぎ込んだ瞬間に動作確認ができます。 これにより、レビューも1機能単位の小さなPRで進められるようになりました。差分が小さければレビュアーの負担も減ります。次の章で扱うアーキテクチャの整理と合わせて「変更影響を局所化したまま、確実に進める」工程設計が成立しました。 リファクタリング後のアーキテクチャ 2つのアプローチを経て、対象画面はMVVMベースの構造に生まれ変わりました。Fragment内に集中していた検索ロジックやデータ取得処理はRepositoryに移しています。Fragmentフィールドに散在していた状態と、DomainモデルからViewDataへの変換はViewModelが担います。UI(Composable)はViewModelが公開するUiStateをStateFlowとして購読するだけになり、データの流れが一方向に整理されました。Beforeと比べて状態の流れが追いやすく、責務の切り分けも明確になっています。 責務を分散させるにあたって、特に型安全性を取り戻すことを意識した設計判断が2つあります。いずれもBeforeで挙げた「Object型のリストに見出しとアイテムが混在」「状態がFragmentフィールドに散在」という課題への直接的な解です。 sealed interfaceで画面状態を表現する 画面全体の状態は、 sealed interface を使ってContent/TextSearch/Errorの3つに分けました。構造を抽象化して示すと次のとおりです。 sealed interface ScreenUiState { val selectedFilter: FilterViewData data class Content( override val selectedFilter: FilterViewData, val items: List <ListItemViewData>, val sortType: SortType, ) : ScreenUiState data class TextSearch( override val selectedFilter: FilterViewData, val searchText: String , val items: List <ListItemViewData>, ) : ScreenUiState data class Error ( override val selectedFilter: FilterViewData, val errorMessage: String , ) : ScreenUiState } 旧実装では searchText 、 sortType 、 selectedGender のような画面の状態がFragmentフィールドに散在していました。これらを1つのUiStateに集約することで、状態の遷移はStateFlowを流れるUiStateの差し替えだけで表現できます。 具体的には、Content(テキスト絞り込みなしの一覧表示)・TextSearch(テキスト絞り込み中の一覧表示)・Error(エラーダイアログ)の3状態に分けています。 when 式での網羅性チェックも効くため、状態の追加や変更にも強い構造になりました。 sealed interfaceでリストアイテムを型安全に表現する 見出しとアイテムが混在していたリストには sealed interface で型を分けて表現する方法を導入しました。 sealed interface ListItemViewData { data class Section(...) : ListItemViewData data class Item(...) : ListItemViewData } これにより、LazyColumnの items などでリスト要素を扱うコードは when 式で分岐するだけで網羅的に処理できます。キャストもなく、見出しとアイテムの取り違いもコンパイル時に検出されます。Object型のリストでキャストを繰り返していたBeforeと比べて、型システムが安全に守ってくれる範囲がぐっと広がりました。 AI活用Tips 最後に、このリファクタリングを通して得られたAI活用のTipsをいくつか共有します。 Tips 1:タスク分解書を「AIへの依頼の共通言語」にする 設計書とタスク分解書は、実装フェーズで「AIへの依頼の共通言語」として活用しました。私たちはタスク分解書の1タスクごとにIssueを作成し、Issueに対応するタスクの記述を書く運用にしました。必要に応じて設計書の該当箇所も転記しておくことで、コードベースだけでは判断しづらい「どこから手をつけるべきか」が伝わり、AIの出力は私たちの期待に近づきやすくなりました。 Tips 2:設計ドキュメント自体もGit管理する 実装を進めていると、「設計変更が必要になる」場面は必ず出てきます。たとえば今回も、当初はUiStateをContent/Empty/Errorの3状態で設計していました。しかし実装中にEmptyを独立させるよりテキスト絞り込み中の状態として表現する方が適切と判断し、TextSearchへ置き換えました。他にもタスク分解の途中で機能の依存関係を見直したり、Repository層に切り出す責務の境界を再定義したりと、設計書を書き換える場面が複数回ありました。 私たちは設計書やタスク分解書をマークダウンとしてリポジトリに置き、Gitで履歴を追えるようにしています。変更があった際はコミットを切り、なぜ変えたかをメッセージに書き残します。これによりAIが設計書を参照するときも常に最新の状態を渡せますし、レビュアーも「設計と実装の差分」を追いやすくなりました。同じような取り組みをするチームには、設計書を書いた段階から「変更を前提にドキュメントを置く場所と更新ルールを決めておく」ことをおすすめします。 Tips 3:レビューで得た知見を「AI参照用」のドキュメントに蓄える 個別のレビュー指摘はそのPRで修正して終わりにしてしまいがちです。しかし「次に同じ画面を触るとき」「次にAIに似た実装を依頼するとき」に、また同じ指摘を繰り返さないようにしたいところです。私たちはレビューで頻出した観点やコーディング規約に書ききれていない暗黙のルールを整理し、リポジトリ内のドキュメントとして残しています。たとえば、HiltのスコープごとのDIモジュール構成や、Coroutinesでのスレッド切り替えを抽象化するためのプロジェクト独自ルールなどです。Android開発ではフレームワークやプロジェクト固有の制約が多く、コードを読むだけでは伝わりにくい暗黙のルールが意外と多くあります。AIへの実装依頼時にこれを参照させることでPRの品質が安定し、レビューの往復回数も減っていきました。 Tips 4:AIが苦手なところを人が補う AIに任せれば実装まで一気通貫で終わる、というほど単純ではありませんでした。仕様調査や設計書の雛形作成、機能単位のIssueから始まる定型的な実装はAIに頼りやすい一方で、人の判断が必要だった場面もいくつかあります。 たとえば、複数画面にわたる責務の整理、Repository層に切り出す境界の判断、実装方針のトレードオフを取る場面などはAIに丸投げするとブレやすい部分です。これらは対象の画面以外の前提知識やプロジェクトの方向性を踏まえた判断が必要なためです。こうした判断には、人がドラフトを書いて設計書に落とし込み、それをAIに参照させて実装してもらうというハイブリッドな進め方が結果的に効率的でした。 まとめ 本記事では、共通基底クラスを継承したレガシーな画面をKotlin + Jetpack Compose + MVVMへ移行する際に採用した2つのアプローチを紹介しました。1つは実装より先に設計書を書ききること、もう1つはUIを先にComposeへ移行し動かしながら段階的に進めることです。 これらを組み合わせて進めた結果、状態管理とリスト表現の型安全性も取り戻し、Fragmentに貼り付いていたロジックをRepositoryとViewModelへ整理できました。 リファクタリング前は対象画面のロジックがFragment内に閉じており、ユニットテストが書きづらい構造でした。リファクタリング後はViewModelやRepositoryの単位で70件以上のユニットテストを実装できる構造になり、その後の関連機能の追加・修正もユニットテストで検証しながら進められました。 レガシーなAndroidViewのCompose化や肥大化した基底クラスの解体と向き合っているチームの参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
ZOZO開発組織の2026年5月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2026年5月は、前月のMonthly Tech Reportを含む計10本の記事を公開しました。中でもClaude Code関連のE2Eテストに関する記事は多くの反響がありました。ぜひご覧ください。 techblog.zozo.com 登壇 RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜 5月13日に開催された「 RubyKaigi 2026 アフターイベント〜初参加LT・スポンサー4社のパネル〜 」に、WEAR開発部の坂元( @sakam0cchan )がLTに、技術戦略部の諸星がパネルディスカッションに登壇しました。 AI Agentを支えるデータエンジニアの仕事 5月15日にオンラインで開催された「 AI Agentを支えるデータエンジニアの仕事 」に、技術戦略部の塩崎が登壇しました。 【25卒】技術で尊敬される先輩にならNight 5月15日に開催された「 【25卒】技術で尊敬される先輩にならNight 」に、リプレイス推進部の村形が登壇しました。 Google Cloud Next 2026 Recap in ZOZO 5月18日にオンラインで開催された「 Google Cloud Next 2026 Recap in ZOZO 」に、MA部の林、平井、木野、AI事業戦略部の桜井、川田が登壇しました。 JJUG CCC 2026 Spring 5月30日に開催された「 JJUG CCC 2026 Spring 」に、ZOZOMO部の木目沢がスポンサー企業LTに登壇しました。 協賛 TSKaigi 2026 2026年5月22日から23日の2日間にわたりベルサール羽田空港で開催された「 TSKaigi 2026 」にGold Sponsorとして協賛しました。 technote.zozo.com techblog.zozo.com techblog.zozo.com JJUG CCC 2026 Spring 5月30日にベルサール新宿グランドで開催された「 JJUG CCC 2026 Spring 」に、ブーススポンサーとして協賛しました。 technote.zozo.com techblog.zozo.com 掲載 マイナビニュース 3月に西千葉オフィスで開催した中高生女子向けのSTEM体験プログラム『「Girls Meet STEM」2026 春ツアー』のレポート記事が掲載されました。 news.mynavi.jp shinfdn.org なお、昨年に続き、8月には夏ツアーを開催します。 gms.shinfdn.org 以上、2026年5月のZOZOの活動報告でした! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、カート決済部カート決済基盤ブロックの多田とSRE部カート決済SREブロックの伊藤( @_itito_ )です。普段はZOZOTOWN内のカート機能や決済機能の開発、保守運用、リプレイスを担当しています。 以前の記事で、クレジットカード決済処理をSQSで非同期化し、キャパシティコントロールを実現した取り組みをご紹介しました。 techblog.zozo.com 非同期化によって決済処理の安定性は大幅に向上しました。しかし、セールの終了時刻となる日曜から月曜にかけての日跨ぎ時に注文のピークを迎える時間帯では、依然としてユーザーの待ち時間が課題として残っていました。本記事では、SQSメッセージ処理を高速化することで待ち時間を改善した取り組みについてご紹介します。 目次 .entry .entry-content .table-of-contents > li > ul { display: none; } はじめに 目次 クレジットカード決済の非同期処理の仕組み 課題:ピーク時のSQS滞留による注文完了までの待ち時間 問題の本質:エンキュー速度 > デキュー速度 メッセージ処理をどれぐらい短縮すれば解消できるか 改善箇所の特定 改善1:仮注文から本注文作成のストアドプロシージャ改善 仮注文から本注文の作成処理とは ボトルネック:リンクサーバー経由の参照コスト 改善アプローチ:OPENJSONでリンクサーバー経由の参照を不要にする Java側の事前取得とクエリ最適化 改善結果 改善2:本注文の作成後の後処理の並列化 既存の課題:直列実行される後処理 後処理の分類:待つべきか、投げっぱなしでよいか CompletableFutureとは 改善アプローチ:@AsyncとCompletableFutureによる並列化 改善の全体像と効果 改善サマリー 実測値による効果確認 今後の展望 まとめ クレジットカード決済の非同期処理の仕組み 前回の記事でご紹介した非同期処理の仕組みを簡単に振り返ります。 ZOZOTOWNのクレジットカード決済では、注文確定ボタンの押下後、仮注文の作成まで同期処理で行います。クレジットカードの与信確保から本注文作成までは非同期処理で行われます。与信の確保が成功するとCartDBにある仮注文をFrontDBの本注文に変換し、注文を確定させます。フロントエンドはポーリングで本注文の完了を監視し、完了次第ユーザーに結果を表示します。 この非同期化により、注文数のスパイクをキューで吸収し、与信処理のキャパシティコントロールを実現できました。ただし、注文の完了画面が表示されるまでのユーザーの待ち時間は非同期化により短縮されたわけではありません。 課題:ピーク時のSQS滞留による注文完了までの待ち時間 ZOZOTOWNでは、セールの終了時刻となる日曜から月曜にかけての日跨ぎ時に注文のピークを迎えます。このピーク時に、注文確定のリクエスト受信から注文が確定するまでのP99レイテンシを分析しました。その結果、ピーク時間帯では注文確定までのP99レイテンシが大幅に増加し、最大で約26秒まで悪化していました。 問題の本質:エンキュー速度 > デキュー速度 ピーク時の待ち時間が増加する原因を深掘りしたところ、個々の処理速度ではなくエンキューとデキューの速度差に問題がありました。 指標 値 エンキューのスループット 109 req/s ワーカーの並列数 186 メッセージ処理時間(平均) 2.1s デキューのスループット 186 ÷ 2.1s ≒ 89 req/s エンキューのreq/sは注文確定のリクエスト数に相当します。デキューのスループットはワーカーの並列数 ÷ メッセージ処理時間から算出しています。 上記グラフの通り、 エンキュー速度がデキュー速度を上回っている ため、SQSにメッセージが滞留し、待ち時間が増加していました。つまり、メッセージ処理は2.1秒で終わっているが、キューが詰まり26秒も待たされている状態です。 メッセージ処理をどれぐらい短縮すれば解消できるか メッセージ処理時間を改善することで、単位時間あたりの処理数を増やし、エンキュー速度に追いつくことができます。現在の並列数からメッセージ処理時間を改善することでの、デキューのスループットと待ち時間の見込みは以下の通りです。 デキュー (req/s) メッセージ処理時間 (平均) 短縮量 不足分(エンキュー - デキュー) 89(現状) 2.1s 0s +20 req/s 93 2.0s 0.1s +16 req/s 98 1.9s 0.2s +11 req/s 103 1.8s 0.3s +6 req/s 109 1.7s 0.4s 0 平均メッセージ処理時間を2.1秒から1.7秒程度まで0.4秒短縮できれば、エンキューとデキューのスループットが均衡し、キュー滞留による待ち時間を大幅に改善できる ことが分かりました。 改善箇所の特定 目標が定まったところで、与信処理後に実行される注文の作成処理フローを整理し、改善できそうな箇所を探しました。大きく2つの改善ポイントを見つけました。 仮注文から本注文作成のストアドプロシージャ改善:リンクサーバー経由のアクセスが多く、ネットワークオーバーヘッドが大きい。アクセスを削減すれば大幅な改善が見込める。 本注文の作成以降の後処理の並列化:メール送信やクレジットカード登録などが直列実行されており、並列化すれば合計時間を圧縮できる。 以降、それぞれの改善内容を詳しくご紹介します。 改善1:仮注文から本注文作成のストアドプロシージャ改善 仮注文から本注文の作成処理とは 仮注文から本注文を作成するストアドプロシージャでは、CartDB上の仮注文テーブル群に保存されたデータをFrontDBの本注文テーブル群にINSERTします。与信の確保が成功した後に実行され、この処理が完了することで注文データが作成されます。 参照する仮注文側のテーブルは10種類以上に分かれており、ストアド内ではCartDBに対するSELECT・INSERT・UPDATEが合計で約20回実行されます。 ボトルネック:リンクサーバー経由の参照コスト ストアドプロシージャはFrontDB側で動作しますが、参照する仮注文テーブルはCartDB側にあります。FrontDBからCartDBへの参照にはSQL Serverの機能である リンクサーバー が使われており、クエリ実行のたびにDBサーバー間で通信が発生します。 learn.microsoft.com ネットワーク往復のオーバーヘッドにより、本来1ms未満で完了する軽量なクエリでも数msのコストがかかります。 実測では、 IF EXISTS 程度の軽量クエリ1回でも約3.5msかかっていました。同様のリンクサーバー経由クエリがストアド内で約20回繰り返されることで、ストアド全体で大きなオーバーヘッドになっていました。 改善アプローチ:OPENJSONでリンクサーバー経由の参照を不要にする 改善後のストアドプロシージャでは、リンクサーバー経由のクエリを排除するために以下のアプローチを取りました。 CartDBから仮注文データをJava側で事前に取得する 取得したデータをJSON形式に変換し、ストアドプロシージャのパラメーターとして渡す ストアド内では OPENJSON を使ってJSONをテーブルのように扱い、リンクサーバー経由の参照を置き換える OPENJSON はSQL Serverの組み込み関数で、JSON文字列をテーブル形式に変換できます。リンクサーバー経由のクエリを、メモリ上のJSONに対するクエリに置き換えることで、サーバー間通信を不要にしました。 以下は代表的な変換パターンです。仮注文関連テーブルへの存在チェックを例にすると、改修前後で次のように書き換わります。 -- 改修前: リンクサーバー経由でCartDBの仮注文関連テーブルを参照 IF EXISTS ( SELECT * FROM [接続先].[DB名].[スキーマ名].[テーブル名] WHERE ID = @ID ) -- 改修後: パラメーターで受け取ったJSONをOPENJSONで参照 IF EXISTS ( SELECT * FROM OPENJSON(@Json, ' $.table_foo_bar ' ) WITH ( ID INT ' $.table_foo_bar_id ' ) WHERE ID = @ID ) 同じパターンで、リンクサーバー経由で実行していたCartDBへのアクセスのうち、参照系の処理を中心に約20か所を置き換えました。 Java側の事前取得とクエリ最適化 ストアドへ渡すJSONを生成するためには、改修前はストアド内からリンクサーバーを介して取得していた仮注文データを、Java側で事前に取得する必要があります。事前取得は直列でも実行できますが、少しでも処理時間を短縮するため、 CompletableFuture を使って並列実行しました。( CompletableFuture の詳細は改善2で後述します) さらに、改修前は仮注文テーブルごとに個別のSELECTを発行していましたが、関連テーブルをJOINで集約し、実行するクエリ数を削減しました。事前取得そのものを軽量化することで、JSONパラメーター化のオーバーヘッドを最小限に抑えています。 修正後の処理フローは以下のとおりです。 改善結果 STG環境で負荷試験シナリオを実行し、改善効果を ミクロ(個別クエリ)→ミドル(ストアド全体)→マクロ(エンドポイント全体) の3つの粒度で計測しました。クエリはストアドに含まれ、ストアドはエンドポイント処理の一部であるという包含関係になっています。 計測には、ミクロ・ミドルの粒度ではSQL ServerのDMV( dm_exec_query_stats ・ dm_exec_procedure_stats )を用いています。マクロの粒度ではエンドポイント全体のレイテンシ計測を用いています。 クエリ個別での計測(代表例) リンクサーバー経由のクエリは、置き換えにより1クエリあたりの平均実行時間が1〜2桁減少しました。 クエリ種別 改修前 改修後 改善率 仮注文関連テーブルへの存在チェック( IF EXISTS ) 3.764ms 0.049ms 98.7% 仮注文テーブルからの INSERT 6.424ms 0.984ms 84.7% 仮注文テーブルからの SELECT 2.655ms 0.210ms 92.1% ストアド単体での計測 ストアド内で繰り返されていたリンクサーバー経由クエリの実行時間の短縮が積み重なり、ストアド全体としても75%超の改善につながりました。 改修前 改修後 改善率 平均CPU時間 81.7ms 15.6ms 80.9% 平均実行時間 146.3ms 36.1ms 75.3% 本注文作成の処理全体での計測 エンドポイント全体のレイテンシを計測した結果、 平均で約100msの改善 を確認できました。内訳としては、ストアド単体で約110msの短縮が得られた一方、JSON生成のためにJava側で事前取得する処理が追加された分(約10ms)があり、合計で約100msの改善となっています。 改善2:本注文の作成後の後処理の並列化 既存の課題:直列実行される後処理 本注文の作成後には、以下のような後処理が必要です。 注文完了メール送信 クレジットカード登録 お気に入りブランド登録 メールマガジン登録 買い替え割(割引サービス)の適用 不正検知 在庫の更新 従来の実装では、これらの処理が直列に実行されていたため、個々の処理は数十〜数百ミリ秒程度であっても、全体の実行時間が膨らんでいました。 後処理の分類:待つべきか、投げっぱなしでよいか 並列化を進めるにあたり、まず各処理を一覧化し、 完了を待つ必要があるか(join)、投げっぱなしでよいか(fire-and-forget) を判断しました。 判断基準は、 注文の完了画面の表示に直接影響する処理か という点です。失敗時にエラーメッセージを表示する必要がある処理は完了を待ち、画面表示に影響しない処理は投げっぱなしにします。 なお、ここでのfire-and-forgetは、注文の完了画面の表示を待たないという意味です。処理に失敗した場合は、ログやメトリクスで検知できるようにし、必要に応じてリトライや補完処理を行えるようにしています。 この基準で各処理を分類した結果は以下の通りです。 処理 分類 理由 注文完了メール送信 join(待つ) 失敗時に注文の完了画面でエラーメッセージを表示するため クレジットカードの登録 join(待つ) 失敗時に注文の完了画面でエラーメッセージを表示するため お気に入りブランドの登録 join(待つ) 失敗時に注文の完了画面でエラーメッセージを表示するため メールマガジンの登録 join(待つ) 失敗時に注文の完了画面でエラーメッセージを表示するため 買い替え割の適用 fire-and-forget 画面表示に影響しない非同期処理 不正検知 fire-and-forget 画面表示に影響しない非同期処理 在庫の更新 fire-and-forget 画面表示に影響しない非同期処理 この整理により、 joinで待つ処理同士はCompletableFutureで並列実行し、fire-and-forgetの処理は完了を待たず投げっぱなしにする という方針が定まりました。 CompletableFutureとは CompletableFuture は、非同期処理の結果を表すJavaのクラスです。Springの@AsyncやExecutorと組み合わせることで、複数の処理を並列に実行し、すべての完了を待ち合わせることができます。 例えば、メール送信(200ms)とクレジットカード登録(500ms)を直列実行すると合計700msかかりますが、 CompletableFuture で並列実行すれば最も遅い処理の500msで完了します。 // joinパターン:@Asyncメソッドが返すCompletableFutureをallOfで待ち合わせ CompletableFuture<ResultA> futureA = task.processA(); CompletableFuture<ResultB> futureB = task.processB(); CompletableFuture<ResultC> futureC = task.processC(); CompletableFuture<ResultD> futureD = task.processD(); CompletableFuture.allOf(futureA, futureB, futureC, futureD).join(); // fire-and-forgetパターン:@Asyncメソッドの戻り値を受け取らず呼び出すだけ task.processE(); task.processF(); 改善アプローチ: @Async と CompletableFuture による並列化 リプレイス後のJava実装では、Springの @Async アノテーションと CompletableFuture を活用して後処理を並列化しました。 処理の流れは以下の通りです。 仮注文から本注文作成 :仮注文データを取得し、JSON形式に変換しストアドプロシージャを呼び出し本注文を作成 後処理を順次発火 :join対象・fire-and-forget問わず、すべての後処理を @Async メソッドとして順次発火する。各処理は非同期で並列に実行される join対象のみ待ち合わせ : CompletableFuture を返す4つの処理だけを allOf().join() で待ち合わせ、結果を取得する 改善の全体像と効果 これらの改善は、ASPからJavaへのリプレイスに合わせて実施しました。現時点ではPCサイトのリプレイスのみ完了しており、SPサイト(スマートフォンサイト)は今後リプレイスを実施する予定です。 改善サマリー 改善内容 短縮効果 改善1 本注文の作成処理のJSONパラメーター化 (リンクサーバー削減・クエリ最適化・並列データ取得) 約0.1s 改善2 後処理の並列化・fire-and-forget化 (CompletableFutureによる並列実行と不要な待機の排除) 約0.3〜0.4s 合計 約0.4〜0.5s PCリプレイス済み範囲では、目標としていた0.4秒の短縮を確認できました。 実測値による効果確認 PCのリプレイスが完了した時点で、リプレイス済みのPCとリプレイス未完了のSPのレイテンシを比較しました。 対象 ASP(SP) Java(PC) 改善幅 メッセージの処理時間(平均) 1.74s 1.34s 約0.4s改善 なお、前述の2.1秒はピーク時分析に用いた期間の平均値であり、上表の1.74秒はPCリプレイス後の効果確認の時点におけるSP側の実測値です。目標の0.4秒と実測の改善幅0.4秒は、計測期間・比較対象が異なるため直接比較できるものではありませんが、目標と同等の短縮効果が得られていることを確認できました。 グラフからも、リプレイス後はピーク時に限らず全体的にメッセージ処理時間が改善していることが確認できます。当初の分析で目標としていた0.4秒の短縮が、実測値でも確認できました。過去に計測したピーク時のリクエスト量であれば、SQSの滞留が解消される見込みです。なお、現時点ではPCのリクエスト数はSPと比較して大幅に少なく、同等のリクエスト数での比較はできていません。あくまで現時点で確認できた速報値としてご理解ください。 今後の展望 注文数は年々増加しており、現在の改善だけでは将来的に再びSQSの滞留が起こりえます。 また、今回の効果測定はPCとSPのリクエスト数が大きく異なる状況での比較でした。SPのN%リリース中に同等のリクエスト数で比較し、正式な効果測定を実施する予定です。 今後は以下の取り組みを検討しています。 SPリリース時の同等リクエスト数での正式な効果測定 さらなるストアドプロシージャの最適化 注文フロー全体のリプレイス完遂 まとめ 本記事では、クレジットカード決済の非同期処理におけるSQSメッセージ処理の高速化についてご紹介しました。 注文リクエストのピーク時のデータ分析 で、ボトルネックがエンキュー速度とデキュー速度の差にあることを特定 本注文の作成処理のストアドプロシージャのJSONパラメーター化 でリンクサーバー経由アクセスを削減し、CPU時間を80.9%改善 後処理の並列化 でCompletableFutureを活用し、メール送信・クレジットカード登録などの処理を並列実行 これらの組み合わせで 約0.4秒の短縮 を達成し、ピーク時の待ち時間を大幅に改善 ストアドプロシージャのリンクサーバー削減と、Javaの CompletableFuture による並列化を組み合わせることで、0.4秒という目標を達成できました。決済処理のような高信頼性が求められるシステムでも、ms単位の地道な改善の積み重ねが大きな効果を生むことを実感しました。同様の課題を抱えている方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。ZOZOでアプリバックエンドブロックのブロック長をしている湯川です。以前公開した記事では、ZOZOTOWNアプリ用APIのリプレイスの初期の開発・課題・解決方法などについて紹介しました。 techblog.zozo.com 今回はその続編として、商品詳細APIリプレイスをどのように進めたのかを紹介します。今回のリプレイス対象は、約5,000行のコードと約410項目のレスポンスを持つ巨大なAPIでした。長年の機能追加によって複雑化し、いわゆる「秘伝のタレ」と呼ばれる状態になっていました。 しかし本記事でお伝えしたいのは、単なるAPIリプレイスの話ではありません。 限られた期間の中で、以下に着目して大規模リプレイスを進めるための技術とマネジメントの取り組みについて紹介します。 巨大なモノリスをどう理解したのか 並列開発が可能な構造をどう作ったのか なぜ3チームでの開発が可能になったのか 目次 はじめに 目次 迫る期限と見えない全体像 「どんな手段を使ってもいい」 突破口となったフィーチャー分割 依存関係を可視化する 構造を見てから戦略を立てる スケールできた理由 あえて改善しない 結果 まとめ 迫る期限と見えない全体像 商品詳細APIリプレイスの着手時点で、私たちには大きな課題がありました。リプレイス前のAPIサーバーはオンプレで稼働しており、その縮退スケジュールは既に決まっていました。一方で、商品詳細APIは長年の改修によって巨大化しており、全体像を把握できるメンバーは誰も居ない状態でした。 価格、在庫、商品画像、サイズなど、多数の機能が単一APIに集約されていました。変更の影響範囲は広く、機能間の依存関係も複雑です。先述の通りコード量は約5,000行、レスポンス項目は約410項目もありました。 当然ながら、「どれくらい時間がかかるのか」すら正確には分からない状態でした。 「どんな手段を使ってもいい」 そんな状況の中で、上司から言われた言葉があります。 「どんな手段を使ってもいいので、達成方法を考えてほしい」 振り返ると、この一言が大きな転換点でした。 通常であれば、今いるチームで進めることを前提に考えがちです。既存の役割分担を維持し、既存の開発プロセスに合わせるのが普通です。 しかし、この言葉によって私自身がその前提を外せるようになりました。 チーム構成も変えていい。 担当範囲も変えていい。 進め方そのものを変えていい。 ならばまず、「この巨大な塊をどう攻略するか」を考えるべきだと思いました。 突破口となったフィーチャー分割 私は本案件の開発をリードするカイルさんに「どうにかしたいから対応方法を検討してほしい」と依頼しました。 カイルさんは 前編で紹介したフェーズ3からリプレイスにJOIN したメンバーで、このプロジェクトではアーキテクトも担当しています。 様々な切り口で分割を検討した結果、彼が提案したのは フィーチャー単位での分割 でした。この分割方法を掘り下げるため、カイルさんにインタビューしてきました。 湯川:最初に「どうにかしたいから対応方法を検討してほしい」とお願いしましたが、どうやってフィーチャー分割という発想に至りましたか? カイル:最初はUIコンポーネント単位、レスポンスオブジェクト単位、データソース単位なども検討しましたが、レガシーエンドポイントのためコンポーネント間やオブジェクト間の依存関係が複雑です。開発単位として独立できないと思いました。その時、「このAPIをアジャイルでゼロから作った場合、どの粒度でどの順番で作っていくんだろう」と想像してみました。そうすると、ECサイトの商品ページに絶対必要なコアな機能と、一旦MVPができた状態で後から追加する細かい機能という粒度で分けられることに気づきました。 湯川:商品詳細APIは機能が多いですが、具体的にはどう整理しましたか? カイル:そうですね、全部で41フィーチャーになりました。これだと依存関係や開発する順番を整理するのが大変そうだったので、細かい機能をさらに「基本機能」「商品タイプ」「追加機能」などカテゴリに分類しました。それぞれのカテゴリをひとつのフェーズとして順番に開発していくイメージです。 湯川:振り返ってみて、分割の観点で学びはありましたか? カイル:「商品タイプ」のフィーチャーは様々な機能に横断して影響することが多く、分割観点としてあまりよくなかったかもしれません。後のショップ詳細APIでは、商品の属性ではなく機能の軸で分割するようにしました。例えば「◯◯ショップの商品」ではなく「特定ショップの専用UI」や「特定ショップのカテゴリ」というふうに細かく分けています。 このようにして、巨大なAPIを構造で捉えることができるようになりました。 さらに、分割のアウトプットとして、各フィーチャーごとに以下の情報もまとめてありました。 レスポンス項目 既存コードで該当箇所がわかるコメントをうったコミット 利用している外部API・マイクロサービス一覧 これらの対応によって、1チームで進めた場合は 約1年規模 という見積もりが出ましたが、並行開発という選択肢を取れるようになりました。ただし、並行開発を実現するためには、フィーチャー間の依存関係を明確にする必要がありました。 依存関係を可視化する 次に取り組んだのは依存関係の詳細な整理です。 依存度の高いフィーチャー 独立性の高いフィーチャー どちらが先に必要か、どこがボトルネックになるかを構造として把握できるように再度コードを解析し、各フィーチャーごとの依存関係を明確にしました。 NotebookLMを使って、依存関係を動的に可視化できるインフォグラフィックも作りました。 私たちはこの作業を通じて、「どう実装するか」ではなく、 「どこなら並列化できるか」 を考えられるようになったのです。 構造を見てから戦略を立てる ここでようやく組織の話になります。一般的には、先に体制を決めてから仕事を割り振ります。 しかし今回は逆でした。まず構造を理解し、次に依存関係を整理し、最後に組織を設計します。 つまり、組織に合わせてアーキテクチャを作るのではなく、アーキテクチャに合わせて組織を設計しました。 依存度の高い基本機能は特定チームが担当し、独立性の高い機能は別チームに任せます。 スプリントごとに担当フィーチャーを計画しながら、3チーム並列での開発体制を構築しました。 スケールできた理由 もちろん、人を増やしただけでは成功しません。むしろ、「人月の神話」の通り、遅くなることもあります。そこで私たちは、仕様理解のコストを下げることに注力しました。 実は商品詳細リプレイスの数か月前から、別チームがUIとレスポンスを紐付けた仕様書を整備していました。この仕様書が、フィーチャー分割、開発、テストのすべての基準になりました。 さらにレガシーAPIのリプレイスを進めていく中で、タスクテンプレートを整備し、チーム間でタスク粒度をある程度統一していました。 結果として、3チーム並列でもタスクの粒度が揃っているため、開発の相談ごとが発生しても連携がスムーズでした。 あえて改善しない もう1つの成功要因があります。それは、 改善しないことを決めた ことです。商品詳細APIには改善余地が数多くありました。せっかくリプレイスする・作り変えるのであれば、「不要と思われる仕様をなくしたい」「レスポンス構造も整理したい」「より適切なエラーハンドリングをしたい」と思います。 しかし、それを始めると終わりが見えません。これは今までのリプレイスプロジェクトで得た知見でした。 今回の目的は改善ではなくリプレイスです。 そのため、 現行をそのまま置き換える ことに徹底的に集中しました。この意思決定によって議論が減り、スピードが大きく向上しました。 結果 最終的に、以下の様々な良い結果を得ることができました。 3チームによる並列開発を実現 約1年規模だった開発を約3か月で完了 サイクルタイムを50%改善 レイテンシーを50%改善 リプレイス前のAPIサーバーのCPU負荷を70%削減(ピーク時) 不具合による切り戻しは0件 品質とスピードを両立しながら、オンプレサーバー縮退にも大きく貢献できました。 まとめ 今回のリプレイスを振り返ると、成功要因はやはりこのフィーチャー分割です。 このフィーチャー分割がきっかけで、後続のリプレイスも順調に進行しました。分割方法も改善を重ね、より細かくユーザーストーリーにあわせた分割ができるようになりました。そして、何より3チームのみんなで同じ目標に向かって開発を進めていくことができたのも大きな成果でした。 今回のプロジェクトは、まさにZOZOが大切にしている「想像」と「創造」を体現した取り組みだったと感じています。 この経験が、同じように大規模リプレイスへ挑戦する方々の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、情報セキュリティ部の 兵藤 です。日々ZOZOの安全を守るためSOC業務に取り組んでいます。 本記事では、SOCでの業務効率化のためにClaude Codeを活用して、自動アラートトリアージエージェント(以降SOC Agent)を構築した事例を紹介します。 また、情報セキュリティ部ではその他にもZOZOを守るための取り組みを行っています。詳細については以下の「OpenCTIをSplunkに食わせてみた」をご覧ください。 techblog.zozo.com 目次 はじめに 目次 背景と概要 SOC Agentの設計 Agentの全体像 Splunk MCPの活用 OpenCTI MCPの活用 SOC AgentのSubAgent設計 opencti-agent log-search-agent memoryの活用 SOC Agentの運用 SOC Agent Skillsのコマンド notable-response threat-hunting slack-alert-triage SOARの活用 おわりに 背景と概要 ZOZOのSOCメンバーは3人体制で、日々大量のセキュリティアラートを処理しています。これらのアラートは、ZOZOTOWNやWEARなどのプロダクトや社内システムのログから生成され、3人で分担して対応するにしては量が多く、負荷がとても高い状況でした。 そこでClaude Codeを活用して、アラートの内容を分析し、優先的に対応すべきアラートを自動で選別するエージェントを構築することにしました。これにより、SOCメンバーは重要なアラートへ集中できるようになり、効率的な対応が可能になると考えました。 また、このSOC AgentのSkillsによってある程度網羅的な調査が可能になるため、SOCメンバーの負荷軽減だけでなく、アラート対応における質の平準化にも寄与しています。 SOC Agentの設計 このAgentが担うのは、初動対応の切り分けです。一般的にはTier1の初動対応に相当します。具体的なレスポンスや即時対応はSOARで行う想定のため、SOC AgentはRead権限で完結する設計にしています。 Agentの全体像 SOC Agentは以下のようなフロー設計で構築しました。 SOC Agentの全体像 SOCアナリストの指示、またはSlackのアラート通知のもと、Claude Codeが動作 Splunk MCPを通じ、アラートデータや詳細なログデータを取得 ZOZOで活用している脅威インテリジェンスプラットフォーム(TIP)であるOpenCTIから、脅威インテリジェンスを取得 取得した情報をもとに、SOC Agentがアラートの優先度を評価し、対応案を検討 レポートを生成し、Slackに対応サマリを投稿 Slackに対する起動は定期実行も可能で、未処理アラートを自動検知して対応サマリを投稿 Splunk MCPの活用 SOC AgentはSplunk MCPを活用して、アラートデータや詳細なログデータを取得しています。Splunk MCPを活用する際、Splunk Cloudにはレガシーなエンドポイントも存在します。ただし、オンプレミスのSplunkと同様に Splunk MCP Server の公式Splunk Appを利用することが推奨されています 1 。 このAppを利用すると、通常のSplunk APIを利用する場合と異なり、MCP専用のTokenをUserごとに払い出すことが可能です。 Splunk MCP Server このMCP Serverに接続するためには IP allow lists を設定する必要があります。見落としがちな点のため、注意してください。 また、このMCPを利用するためのRoleはこのSOC Agentにおいて基本的に以下の4つで、SPLを実行するためのRead権限があれば十分でした。 search get_metadata indexes_list_all mcp_tool_execute クライアント側の設定は基本的に .mcp.json などのファイルに記載します。以下は .mcp.json の例です。 { " mcpServers ": { " splunk-mcp-server ": { " command ": " op ", " args ": [ " run ", " -- ", " npx ", " -y ", " mcp-remote ", " https://<stack-name>.splunkcloud.com:8089/services/mcp ", " --header ", " Authorization: Bearer ${AUTH_TOKEN} " ] , " env ": { " AUTH_TOKEN ": " op://<vault-name>/<item-name>/<token-field> " } } } } 上記のように1Password CLIを利用してMCP ServerのTokenを取得します。このSOC AgentのコードはチームでGitHub管理しているため、誤ってTokenをコミットしてしまうリスクを避けるために、VaultからTokenを取得しています。 MCPのエンドポイントは443ポートのもの( /en-US/splunkd/__raw/services/mcp )もあります。ただし、内部的には8089ポートにリダイレクトされるのでどちらを使っても問題ありません。 OpenCTI MCPの活用 実際の攻撃手法や攻撃に使われたIOC情報などを取得して危険度を評価するために、OpenCTI MCPも活用しています。OpenCTI MCPはFiligran社から公式に MCP が提供されています。 OpenCTI MCPを利用するにはTokenが必要なため、MCPを使うためのロール、グループ、サービスアカウントの作成が必要です。アカウントの作成に大きな手間はかかりませんが、ロールに関しては実際のAgentにどこまで作業をさせるかで必要な権限が変わります。今回のSOC AgentではRead権限のみで完結するように設計しているため、以下のようなロールを作成しました。 OpenCTI MCP ロール設定 基本的にこの Access knowledge のRoleがあればこのAgentの機能は十分に動かすことができました。caseまで記載させたい場合は別の権限が必要でしょう。 また、グループの作成の際にそのグループがアクセスできるインテリジェンスの範囲を定義できます。これは組織によってAIのオプトアウトの設定やインテリジェンスの発行元、TLPなどを考慮して設定する必要があります。個々の組織にあったポリシーを設定してください。 以下のような画面でグループのアクセス範囲を設定できます。「TLP:RED」のインテリジェンスは定義上見せない設定が基本でしょう。「TLP:AMBER」に関しては状況によるでしょう。 OpenCTI MCP グループ設定 クライアント側の設定は以下の記載で接続できます。 { " mcpServers ": { " opencti-mcp-server ": { " command ": " op ", " args ": [ " run ", " -- ", " <path-to-python-venv>/.venv/bin/python3 ", " -m ", " opencti_mcp.server " ] , " env ": { " OPENCTI_URL ": " op://<vault-name>/<item-name>/<url-field> ", " OPENCTI_TOKEN ": " op://<vault-name>/<item-name>/<token-field> ", " PYTHONPATH ": " <path-to-xtm-mcp> " } } } } このサーバは from opencti_mcp.graphql_queries などの記述でPythonモジュールをimportしているため、 PYTHONPATH の環境設定が必要です。 これらのMCPの設定のように、AIはある程度予期せぬ挙動を取る可能性も考え、与えるTokenの権限を絞ることをまずお勧めします。AIには自由にさせた方が柔軟に対応してくれます。変更されたくない接続先の権限を絞ることで、万が一の暴走リスクを減らせます。 SOC AgentのSubAgent設計 このSOC Agentは、特定のSkillを呼び出すAgentを並列起動できるようSubAgentを設計しています。具体的には、OpenCTIから脅威インテリジェンスを取得するSubAgentと、Splunkからログを取得するSubAgentの2つです。 Agent名 説明 opencti-agent OpenCTI MCPを通じて脅威インテリジェンスを取得するSubAgent log-search-agent Splunk MCPを通じてログデータを取得するSubAgent 全体像は以下のとおりです。 SubAgentの構成 この2つのSubAgentが調査によって数十体並列で呼び出されます。IOCの種類や調査するログの種類によって呼び出すSubAgentを分けることで、効率的に必要な情報を取得できるようにしています。 opencti-agent このAgentはOpenCTI MCPを通じて脅威インテリジェンスを取得するSubAgentです。OpenCTIから攻撃手法や攻撃に使われたIOC情報などを取得して、SOC AgentのメインのAgentがアラートの優先度を評価する際に活用します。基本的には別途設計したOpenCTIへGraphQLを投げる opencti-lookup Skillを参考にしています。大量の調査結果やインテリジェンスを収集する目的で利用するため、分析機能を持たせず、ひたすらクエリを投げる設計にしています。高度な分析を必要としないため、 sonnet のモデルで動かしています。 上記Skillのreferenceに各種GraphQLのテンプレートを記載することで、必要な情報を取得する際のクエリを定義しています。例えば、「Reportの基本情報」を取得する際には以下のようなクエリを定義しています。 query ReportById { report(id: "<REPORT_ID>") { id standard_id entity_type name description published report_types confidence created_at updated_at objectLabel { value color } createdBy { ... on Identity { id name entity_type } } } } 念のため、 Hooks にて create や delete 、リレーションを変更する stixCoreRelationshipEdit などRead権限以外の操作するクエリは禁止しています。 " hooks ": { " PreToolUse ": [ { " matcher ": " ^mcp__opencti-mcp-server__(execute_graphql_query|validate_graphql_query)$ ", " hooks ": [ { " type ": " command ", " command ": " python3 \" $CLAUDE_PROJECT_DIR \" /.claude/hooks/validate-opencti-graphql.py ", " timeout ": 5 , " statusMessage ": " Validating OpenCTI GraphQL safety policy " } ] } ] } この validate-opencti-graphql.py には、禁止するGraphQLのパターンを tool_input から正規表現でマッチさせるPythonコードが記載されています。 SOCの対応でVirusTotalなどの判定を利用するフローも一般的に存在します。一方ZOZOでは脅威情報をOpenCTIに集約しているため、このエージェント単体で完結させています。 また、OpenCTIにはZOZO独自で調査している脅威情報も蓄積しているため、外部の脅威情報と社内の知見を組み合わせてアラートの優先度評価ができるようになっています。例えば以下のようなMalware解析のレポートなどを参照します。 qiita.com log-search-agent このAgentはSplunk MCPを通じてログデータを取得するSubAgentです。NotableのReference IDなどをもとに、アラートの概要を取得したり、より詳細なログを取得するSPLを投げたりするために利用します。こちらも分析機能は持たせず、別途設計した splunk-search Skillをもとに、ひたすらクエリを投げる設計にしています。高度な分析を必要としないため、 sonnet のモデルで動かしています。 上記Skillのreferenceに各種SPLのテンプレートを記載することで、必要な情報を取得する際のクエリを定義しています。例えば、「Notableの概要」を取得する際には以下のようなSPLを定義しています。 index=notable source_event_id="<SOURCE_EVENT_ID>" | sort -_time | head 5 | table _time source_event_id event_id notable_event_id orig_event_id search_name rule_title notable_title signature security_domain urgency severity status status_label owner src dest user host risk_object risk_object_type risk_score info_min_time info_max_time SplunkのMCP側で詳細なSPL制御ができません。そこで、OpenCTI同様に Hooks を利用して outputlookup や outputcsv などの作成・変更を伴うSPLは以下の設定で利用禁止にしています。 " hooks ": { " PreToolUse ": [ { " matcher ": " ^mcp__splunk-mcp-server__splunk_run_query$ ", " hooks ": [ { " type ": " command ", " command ": " python3 \" $CLAUDE_PROJECT_DIR \" /.claude/hooks/validate-splunk-spl.py ", " timeout ": 5 , " statusMessage ": " Validating Splunk SPL safety policy " } ] } ] } このMCPの利用に際して、JSON形式で入れ子になっているログはSubAgentの判断で最初に spath などのSPLを打つことがあります。簡易なログだと問題ありませんが、Endpoint系のログだと大量のkeyに対して展開するため、各種SPLのreferenceを詳細に定義しておく方が安全です。また、サーチマクロを定義しておけば、Agentの調査の平準化やトークン消費の最適化が可能です。 memoryの活用 このSOC Agentでは、過去の調査履歴も活用しています。「このインシデントは過去に過検知フィードバックがあったものだ」や「引き続きこの証跡と同じアクターが関与している可能性が高い」といった情報を基に、調査の効率化や精度向上を図っています。 SOC Agentの運用 SOC Agent Skillsのコマンド このAgentには様々なSkillsを実装しています。その中でもZOZOのSOCアナリストが利用するコマンドに絞ってここでは紹介します。 コマンド 説明 /notable-response NotableのReference IDからインシデントを取得し、自動調査を実行するコマンド /threat-hunting OpenCTIのレポートから脅威ハンティングを実行するコマンド /slack-alert-triage Slackのアラートを自動でトリアージし、調査結果をスレッドに投稿するコマンド notable-response このコマンドでのワークフローは以下のイメージです。 SOCアナリストが /notable-response <Reference ID etc> を入力 SOC AgentがSplunk MCPを通じてNotableの概要を取得 SOC AgentがNotableの内容をもとに、opencti-agentとlog-search-agentを呼び出して、関連する脅威インテリジェンスやログデータを取得 SOC Agentが取得した情報をもとに、Notableの相関分析や脅威判定し、対応案を生成 深掘りが必要な場合は、さらに追加の情報を取得するために3からのステップを繰り返す レポート作成、メモリに事象の内容や調査結果を保存 引数には調査観点の概要を含めて渡すことでカスタムした調査が可能です。 threat-hunting このコマンドでのワークフローは以下のイメージです。 SOCアナリストがSlackで /threat-hunting <Report ID> を入力 SOC AgentがOpenCTI MCPを通じてレポートの内容を取得 SOC Agentがレポートの内容をもとに、関連するIOCや攻撃手法を抽出 SOC Agentが抽出したIOCや攻撃手法をもとに、log-search-agentを呼び出して、関連するログデータを取得 SOC Agentが取得した情報をもとに、脅威ハンティングの結果を分析し、対応案を生成 深掘りが必要な場合は、さらに追加の情報を取得するために4からのステップを繰り返す レポート作成、メモリに事象の内容や調査結果を保存 Report IDは別途ZOZOで活用しているMalware解析Agentの結果を渡すこともできます。 slack-alert-triage このコマンドでのワークフローは以下のイメージです。 SOCアナリストがSlackで /slack-alert-triage <time> を入力 SOC Agentが指定された時間範囲のアラートをSlack MCPを通じて取得 SOC Agentが取得したアラートをもとに、NotableのReference IDを抽出 SOC Agentが抽出したReference IDをもとに、notable-responseと同様にNotableの内容を取得 Notableの内容をもとに関連事象をグルーピング、Slackのスレッドに調査開始の投稿 各グループごとにopencti-agentとlog-search-agentを呼び出して、関連する脅威インテリジェンスやログデータを取得 SOC Agentが取得した情報をもとに、Notableの相関分析や脅威判定し、対応案を生成 深掘りが必要な場合は、さらに追加の情報を取得するために6からのステップを繰り返す レポート作成、Slackスレッドに対応サマリを投稿 脅威度がCritical判定の場合は、SOCアナリストにメンション通知 このコマンドを /loop 1h /slack-alert-triage 1h のように定期実行することで、未処理のアラートを自動的に検知して対応サマリを投稿しています。24時間365日対応が難しい場合でも、こういった自動化を活用することでSOCメンバーの負荷を軽減できます。 SOARの活用 /slack-alert-triage コマンドのポイントはSlackのアラートにReference IDが含まれていることです。通常のSplunk ESの「Edit event-based detection」だと「Adaptive response」はNotableの作成とは別のアクションを実行するため、Reference IDがSlackのアラートに含まれません。そこでSplunk SOARを用いて、NotableのReference IDをSlackのアラートに含めています。こうすることで、SOC AgentがSlackのアラートからNotableのReference IDを抽出し、調査を開始できます。 SOARは以下のformatとSlackへのsend messageの Splunk App の設定だけで済みます。 Splunk SOAR 設定 formatはSlackのblocks項目に以下の記述をすれば、ビジュアライズされたアラートをSlackに送れます。 [ {{ " type ": " header ", " text ": {{ " type ": " plain_text ", " text ": " 🟢 Splunk Finding Detected ", " emoji ": true }} }} , {{ " type ": " section ", " fields ": [ {{ " type ": " mrkdwn ", " text ": " *🔎 Name* \n {0} " }} , {{ " type ": " mrkdwn ", " text ": " *🆔 Reference ID* \n `{1}` " }} ] }} ] {0} にはアラート名、 {1} にはReference IDを入れるように設定しています。これでSOC AgentがSlackのアラートからReference IDを抽出し、調査を開始できます。 SOC Agentの本命のSkillはこのコマンドです。ループ処理することでTier1相当の対応をAIで完全自動化することに成功しており、SOCメンバーの負荷軽減に大きく寄与しています。 以下はSOC Agentの対応例です。 SOC Agentの対応例 おわりに 本記事ではClaude Codeを用いたSOC Agentを紹介しました。SOC Agentの導入によって少人数のSOC業務を改善し、アラート対応の効率化、平準化、SOCメンバーのさらなる高度業務へのアサインを図れました。 ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co About MCP Server for Splunk platform ↩