TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

385

はじめに デリッシュキッチンのiOSアプリを開発している成田です。 デリッシュキッチンではデザイン管理にFigmaを利用し、実装時にはDev Mode MCPサーバーを活用して精度を高めています。しかし、実際にビルドして確認してみると、レイアウト崩れが生じたりで期待するUIになっておらず、手動での「スクショ撮影→Cursorへ添付→指示→確認」という反復作業が発生していました。 この課題を少しでも解決するため、XCUITestによるスクリーンショット自動取得とCursorを組み合わせたUI自己改善ワークフローを構築しました。 概要 今回自動化したのは主に以下の2つです: スクリーンショットの取得 : UI実装後に、自動的にアプリをビルド、画面をナビゲーションしてスクリーンショットを取得 画像の読み込み : スクリーンショット取得後、画像を読み込んで分析し、レイアウト崩れなどを検出して実装を修正する 実現したワークフロー: UI実装依頼 ↓ AIがUIコードを生成 ↓ xcodebuildでビルド・UITestの実行 ↓ UITestが画面をナビゲーションしてスクリーンショット取得 ↓ スクリーンショットパスを一時ファイルに保存 ↓ 画像を読み込んで分析 ↓ 必要に応じてレイアウト崩れなどを自動修正 手順 このワークフローを実現するために必要な設定や実装手順は以下の通りです。 1. XCUITestの実装 XCUITestの標準API( XCUIApplication の screenshot() メソッド)を使用してスクリーンショットを取得します。 実装すべき内容は以下の通りです: スクリーンショット取得テスト : 画面をナビゲーションしてスクリーンショットを取得するテストメソッドを実装します。このテストメソッド内で以下の2つのヘルパーを使用します スクリーンショットヘルパー : XCUIApplication の screenshot() メソッドを使用してスクリーンショットをファイルに保存するヘルパークラスを実装します UI要素待機ヘルパー : UI要素が表示されるまで待機するユーティリティを実装します。XCUITestには自動待機機能がありますが、ネットワーク通信や非同期処理による画面更新の遅延がある場合などには、明示的な待機処理が必要です。これにより、テストの安定性を確保できます これらのファイルをUIテストのターゲットに追加します。 2. スクリーンショット取得スクリプトの作成 スクリーンショット取得は、 xcodebuild コマンドを使ってコマンドラインからビルド・テストを実行することで実現します。 スクリプト( scripts/cursor-screenshot.sh )で実装する処理は以下の通りです: 画面名の受け取りと環境変数の設定 : スクリプトは引数として画面名を受け取り、環境変数として設定します。この環境変数はUITestに渡され、どの画面にナビゲーションするかを決定します アプリのビルド : xcodebuild build でアプリをビルドします UIテストの実行 : xcodebuild test で DynamicScreenshotTests.testTakeScreenshotForScreen を実行します。UITestは環境変数を参照して、画面名に応じて適切な画面にナビゲーションしてからスクリーンショットを取得します スクリーンショットファイルの検索とパスの保存 : テスト実行後、最新のスクリーンショットファイルを検索し、パスを /tmp/cursor_screenshot_info.txt に保存します。このファイルを参照してスクリーンショットを読み込むようにします 3. Cursorのルール設定 - .cursor/rules/my-custom-rule.mdc にUI実装後の自動ワークフローのルールを追加 - UI実装後のワークフロー - UIを新規実装または大幅に変更した後は、必ず以下を実行すること: - 大幅な変更の定義: - 大幅な変更の定義: レイアウト、UIコンポーネント、View構造、スタイルなど、レイアウトや見た目に影響を与える変更 1. 当該の画面に対して`scripts/cursor-screenshot.sh`を実行 2. 取得した画像をもとに元のデザインと比較してレイアウト崩れなどがあれば修正する 3. 修正後、再ビルドして確認 4. 必要に応じて、上記のステップ(1-3)を繰り返す(最大n回まで) これによって期待するワークフローが実現できました。 おわりに UIを実装した後は、手動でビルドして確認し、レイアウト崩れなどを発見したら修正依頼を投げるという作業を繰り返す必要がありました。 XCUITestの仕組みを使ったワークフローによってUI実装から自己改善までを自動化することができるようになったので、プロンプトで修正依頼を投げる量を最小限に留めることができるようになりました。 とはいえ、UITestは壊れやすくワークフローの途中でこけることも度々起こりうるので、その辺はデメリットかなと思います。
アバター
目次 はじめに 注意事項 Echo v5の主な変更点 Echo v4からv5への移行しながら変更点を確認する バージョン更新とecho.Contextの変更 Routerのカスタマイズ (Interface + DefaultRouter) StartConfigを用いたサーバー起動 デフォルトロガーがslog.Loggerに変更 レスポンス情報の取得方法 (UnwrapResponse) URLパラメータの埋め込み方法変更 echo.POSTのような http.MethodXXX のヘルパーが廃止 まとめ はじめに こんにちは、開発本部開発1部トモニテグループのエンジニアの パンダム/rymiyamoto です。 2026/01/18 より Echo v5 がリリースされました 🎉 弊社プロダクトの多くが依存しているフレームワークなだけに、最新バージョンへの移行パスをいち早く探っておきたいところ。 さっそく、主要な変更点や所感をレポートします。 Echoを使って開発している方も多いと思いますので、その手助けになればと思います。 github.com 注意事項 本記事はEcho v4の基本的な知識がある読者を対象としています。 また執筆は2026/01/22時点のもので、Go 1.25.6をベースにしています。 リリース直後のv5には、今後も破壊的な変更が加わる余地が残されています。そのため公式からは、商用利用については 2026/3/31まで待機すること が推奨されている点に注意が必要です。 (ちなみにv4のサポートは2026/12/31までです) 公式からの引用↓ v5.0.0 was release on 2026-01-18. v4 will be supported with security updates and bug fixes until 2026-12-31. Until 2026-03-31, any critical issues requiring breaking API changes will be addressed, even if this violates semantic versioning. If you are using Echo in a production environment, it is recommended to wait until after 2026-03-31 before upgrading. Echo v5の主な変更点 今回の変更点をまとめてみると、主に以下の5点に集約されます。 【破壊的変更】 echo.Context がインターフェースから構造体に変更 将来的な機能追加を容易にする ための大きな設計変更です 【新機能】Router のインターフェース化による拡張性の向上 独自のルーター実装が可能になり、 正規表現ルーティング なども導入しやすくなります 【仕様変更】サーバー起動設定の構造体化 ( StartConfig ) アドレスやタイムアウト設定が構造体に集約され、 設定の可読性が向上 します 【破壊的変更】Go標準ライブラリ log/slog をネイティブサポート サードパーティ製ライブラリなしで、 モダンな構造化ログ が扱えるようになります 【破壊的変更】APIの一貫性向上とメソッド整理 全体的なシグネチャが見直され、より 洗練された開発体験 を提供します 概要からみてこれまでv4の期間が長かったのもあり大きな進歩だなと感じました。 今までは標準で対応しづらい部分はサードパーティを利用していたので、ログ周りが自分としては一番ありがたいです。 他にも設定周りが統一化されているのでよりコードの可読性が向上しそうな予感がしますね。 もちろん他にもいくつかAPIの変更点がありますが、詳細については実際にv4で動いているコードの修正をしながら気づいた部分を紹介していきます! (紹介しきれていない部分は下記のドキュメントを読んでみてください) github.com Echo v4からv5への移行しながら変更点を確認する バージョン更新とecho.Contextの変更 公式に記載されている通りの方法でバージョン更新や最低限の修正を行うことができます。 # v4 から v5 への移行 go get github.com/labstack/ echo /v5 # サードパーティの更新(対応されているサードパーティ系のライブラリは更新) go get github.com/labstack/echo-contrib go get github.com/labstack/echo-jwt/v5 # 一括置換 # echo.Context -> *echo.Context find . -type f -name " *.go " -exec sed -i ' s/ echo.Context/ *echo.Context/g ' {} + # echo/v4 -> echo/v5 find . -type f -name " *.go " -exec sed -i ' s/echo\/v4/echo\/v5/g ' {} + github.com 実際のコードの変更は以下のようになり、大多数の変更箇所になるかと思います。 // v4 func MyHandler(c echo.Context) error { return c.JSON( 200 , map [ string ] string { "hello" : "world" }) } // v5 func MyHandler(c *echo.Context) error { return c.JSON( 200 , map [ string ] string { "hello" : "world" }) } これだけだと型定義が変わっただけか〜となりますが、この変更による恩恵は新機能追加時に破壊的な変更にならないようにするための大事な変更と言えます。 まずecho.Contextがインターフェースだった場合、将来的に新しい機能(メソッド)を追加すると、そのインターフェースを実装しているすべてのコード(自作のContextラッパーやモックなど)がコンパイルエラーになります。 これは「破壊的変更」にあたるため、次のメジャーバージョンアップ(v6~)まで機能追加がしにくくなります。 構造体( *echo.Context )に変更することで、本体に新しいメソッドを追加しても、既存の利用側のコードはそのまま動作します。 これにより、v5の期間中にマイナーアップデートで便利な新機能をどんどん追加できるようになります。 Routerのカスタマイズ (Interface + DefaultRouter) Routerのインターフェース化により、独自のルーター実装が可能になりました。 これにより、正規表現ルーティングや、特定のアプリケーションでは不要な機能(ワイルドカードや 405 Method Not Allowed のハンドリングなど)を削ぎ落とし、より効率的なルーティング処理を実現できるようになるのではないかと思います。 まだこれからだとは思いますが、 RouterConfig が拡張されていくことにより様々な恩恵を受けやすくなりそうです。 // v4 type Router struct { ... } func NewRouter(e *Echo) *Router func (r *Router) Add(method, path string , h HandlerFunc) func (r *Router) Find(method, path string , c Context) func (r *Router) Reverse(name string , params ... interface {}) string func (r *Router) Routes() []*Route // v5 type Router interface { Add(routable Route) (RouteInfo, error ) Remove(method string , path string ) error Routes() Routes Route(c *Context) HandlerFunc } type DefaultRouter struct { ... } func NewRouter(config RouterConfig) *DefaultRouter func NewConcurrentRouter(r Router) Router // NEW type RouterConfig struct { NotFoundHandler HandlerFunc MethodNotAllowedHandler HandlerFunc OptionsMethodHandler HandlerFunc AllowOverwritingRoute bool UnescapePathParamValues bool UseEscapedPathForMatching bool } github.com StartConfigを用いたサーバー起動 新しく追加された echo.StartConfig 構造体を使用する形式でもサーバーを起動することができます。 もちろんこれまで通りの echo.Start を使用することもできますが、アドレス指定が引数ではなく設定から指定できるのは使いやすそうです。 以下は StartConfig を使用した場合の例です。 // v5: echo.StartConfig を使用する func main() { e := echo.New() e.Use(middleware.RequestLogger()) e.GET( "/" , func (c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!" ) }) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() sc := echo.StartConfig{ Address: ":1323" , GracefulTimeout: 10 * time.Second, } if err := sc.Start(ctx, e); err != nil { log.Fatal(err) } } $ go run server.go { " time " : " 2026-01-20T23:28:54.719092+09:00 " , " level " : " INFO " , " msg " : " Echo (v5.0.0). High performance, minimalist Go web framework https://echo.labstack.com " , " version " : " 5.0.0 " } { " time " : " 2026-01-20T23:28:54.719326+09:00 " , " level " : " INFO " , " msg " : " http(s) server started " , " address " : " [::]:1323 " } { " time " : " 2026-01-20T23:29:11.2848+09:00 " , " level " : " INFO " , " msg " : " REQUEST " , " method " : " GET " , " uri " : " / " , " status " :200, " latency " :3000, " host " : " localhost:1323 " , " bytes_in " : "" , " bytes_out " :13, " user_agent " : " curl/8.7.1 " , " remote_ip " : " ::1 " , " request_id " : "" } github.com デフォルトロガーがslog.Loggerに変更 これまではGoの標準のログを少し拡張した シンプルなloggingパッケージ を使っていましたが、slogを使うようになり、ログの設定でそのままslogの設定が利用できるようになりました。 これによりこれまで slog-echo のようなサードパーティを使ってslog利用していたように拡張する必要がなくなり、ロギング設定がより直感的になって個人的には一番嬉しいポイントになりました。 // v4 type Echo struct { Logger Logger // Custom interface with Print, Debug, Info, etc. } // v5 type Echo struct { Logger slog.Logger // slog.Logger interface } 実際にslogを使ってログ設定をカスタマイズする場合は以下のようになります。 skipper := func (c *echo.Context) bool { return c.Request().URL.Path == "/health" } e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ Skipper: skipper, // ヘルスチェックのリクエストはログに含めない LogStatus: true , LogURI: true , LogError: true , HandleError: true , // エラーをグローバルエラーハンドラに転送し、適切なステータスコードを決定できるようにします。 LogValuesFunc: func (c *echo.Context, v middleware.RequestLoggerValues) error { if v.Error == nil { logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST" , slog.String( "uri" , v.URI), slog.Int( "status" , v.Status), ) } else { logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR" , slog.String( "uri" , v.URI), slog.Int( "status" , v.Status), slog.String( "err" , v.Error.Error()), ) } return nil }, })) // start server... echo.labstack.com レスポンス情報の取得方法 (UnwrapResponse) ミドルウェア等でレスポンスの書き込みサイズやステータスコードを参照する場合、これまでは c.Response() のフィールドに直接アクセスしていましたが、v5では echo.UnwrapResponse ヘルパーを使用する必要があるので修正が必要になります。 // v5 resp, err := echo.UnwrapResponse(c.Response()) if err == nil { // resp.Size <-- 書き込まれたバイト数 // resp.Status <-- ステータスコード // resp.Committed <-- クライアントに送信済みかどうか fmt.Printf( "Status: %d, Size: %d \n " , resp.Status, resp.Size) } github.com この変更に伴う修正には正直、かなり手こずりました。いざv5の変更を当ててビルドしてみると、あちこちでヘルパーがエラーを吐き出しレスポンスの中身を見ている処理の見直しがいるのかなと疑問に思い、一体どうすれば…と右往左往する羽目になりました 😇 しかし、APIの変更点を見直したり実際のv5のコードを追ってみるなどの試行錯誤の末にこの UnwrapResponse の存在に気づきました この変更には最初こそ戸惑いましたが、 val, err := func() というGo言語の標準的なエラーハンドリングパターンに統一されたことで、変更の意図が腹落ちしました。 v4までではフィールド直接参照は手軽な反面、内部実装への依存度が高く、将来的な変更に弱い側面があります。 今回ヘルパー関数を経由する形になったことで、内部構造が隠蔽され、仮に取得に失敗してもエラーとして安全に検知できる(堅牢性が高まる)ようになっています。 また、このアプローチはGo1.20で標準ライブラリに追加された http.ResponseController の設計思想とも通じる部分があり、EchoがGoのモダンな作法に追従しようとしている姿勢がうかがえます。手間は増えましたが、長期的な保守性を高めるための洗練された変更と言えそうです。 URLパラメータの埋め込み方法変更 テストのとき、URL内にリソースのIDを埋め込んで実行する際によく使用していたコードが変更されています。 v5のほうが1行でまとまり直感的に書けるようになっており、これまでのコードよりも可読性が向上しています。 // v4 c.SetParamNames( "id" ) c.SetParamValues( "1" ) // v5 c.SetPathValues(echo.PathValues{{Name: "id" , Value: "1" }}) github.com こちらもぱっと調べて出てこなかったので実際に修正してみて気づいた部分になりました。 echo.POSTのような http.MethodXXX のヘルパーが廃止 これまでHTTPメソッドのヘルパーがあったのですが、v5では廃止されています。 そのため、 echo.POST などのヘルパーを http.MethodPost のように書き換える必要があります。 (何故かここはドキュメントにもAPI変更にも記載がないところでした) 以下がv4の時のコードです。 // HTTP methods // NOTE: Deprecated, please use the stdlib constants directly instead. const ( CONNECT = http.MethodConnect DELETE = http.MethodDelete GET = http.MethodGet HEAD = http.MethodHead OPTIONS = http.MethodOptions PATCH = http.MethodPatch POST = http.MethodPost // PROPFIND = "PROPFIND" PUT = http.MethodPut TRACE = http.MethodTrace ) github.com まとめ 今回は Echo v5 がリリースされたので触れてみました。 v5の新しい機能に触れてみて、その利便性やコードの読みやすさが格段に向上しているのを肌で感じました。これは開発体験を大きく変える可能性を秘めていると思います。 実際に社内のプロダクトで実験しながらアップデート内容に触れてみましたが、試した結果、まだまだ修正が必要な箇所が多く、エラーの解消や動作検証に膨大な時間を費やしました。 正直なところ、完全に商用利用できる状態にするには、まだ多くの修正と検証が必要だと痛感しています。 この道のりは決して楽ではありませんが、v5がもたらす開発体験の向上を考えると、乗り越える価値は十分にあると感じています。 ガッツリとした変更ありつつもv4での記法がすべて変わるわけではないので、地道に修正していく必要があります。 まだまだリリースして日が浅く、公式含めサードパーティ系も更新が活発になってくるかと思うので最新の情報においていかれないように食らいついていこうと思います。 最後まで読んでいただきありがとうございました!
アバター
目次 はじめに エンジニア内定者研修について エンジニア内定者研修の概要 エンジニア内定者研修の目的 エンジニア内定者研修カリキュラム 前回からの改善点 ターミナルおよび Git/GitHub の基礎・プログラム基礎 ネットワーク/インフラ基礎 DB 研修 Web 基礎・Web アプリケーション開発基礎 AI開発基礎 受講者のフィードバック おわりに はじめに こんにちは。 開発本部開発1部トモニテ開発部所属の庄司( @ktanonymous )です。 弊社では、内定者向けとしては2回目となる内定者研修を2026年新卒のエンジニア内定者向けに実施し、 2025年中に全ての講義を完了しました。 今回の記事では、その内容について紹介したいと思います。 エンジニア内定者研修について 弊社では昨年度、エンジニア向けの内定者研修を初めて実施しました。 (昨年度の内定者研修の詳細については以下の記事をご覧ください) tech.every.tv 昨年度の研修では、入社後の新卒研修をよりスムーズに進められるよう、受講者の知識のベースラインを揃えることを目的として実施しました。 今年度も引き続き同様の目的のもと、昨年度の経験やフィードバックを活かしてカリキュラムを改善し、2回目の内定者研修を実施しました。 エンジニア内定者研修の概要 ここでは、エンジニア内定者研修の目的やカリキュラム、それぞれの講義概要について紹介したいと思います。 エンジニア内定者研修の目的 先述の通り、内定者研修では来年度入社する新卒のエンジニアメンバーが、入社後の研修を通じてよりスムーズに開発組織にジョインできるように、 ベースとなる基礎知識を学べる機会を提供することを主な目的としています。 具体的には、以下のような目的と方針を設定しました。 目的 入社前に基本的な技術や知識をキャッチアップする環境を提供する 方針 入社前に身に着けてほしい技術や知識のキャッチアップをサポートする 基礎知識を早期にキャッチアップすることで入社後の研修・オンボーディングをよりスムーズに進められるようになる 上記の目的と方針を踏まえ、4月に入社した新卒メンバーが中心となり資料の作成から当日の講義までを担当してもらいました。 最終的な資料としては社内メンバーのレビューを経て内容を担保するようにしています。 エンジニア内定者研修カリキュラム 今回の研修では以下のテーマで講義を行いました。 各回 90 分を目安に、2 週間に 1 回程度のペースで実施しました。 今回は、昨今のAI技術の発展に伴う開発環境の変化を踏まえて、AI開発基礎を新たなテーマとして取り入れました。 ターミナルおよび Git/GitHub の基礎・プログラム基礎 ネットワーク/インフラ基礎 DB 研修 Web 基礎・Web アプリケーション開発基礎 AI開発基礎 また、遠方から参加する方もいるため、全ての講義はオンラインで実施して録画を残すようにしました。 さらに、今回は講義で使用した資料を以下のリンクの speakerdeck に公開しています。 講義資料だけでなくイベントの登壇などで使用した資料などもご確認いただけますので、この機会にぜひご覧ください。 speakerdeck.com 前回からの改善点 前回の研修では受講者から以下のようなフィードバックがありました。 エンジニアとして知っておいた方が良いことを知ることができた 開催時期に対してスケジュールがタイトに感じられた また、運営面では担当の負荷が偏っていたり講義の準備のスケジュール感に対するフィードバックがありました。 これらのフィードバックを踏まえ、今回の研修では以下のような変更を取り入れました。 テーマ設定は昨年度の良さを引き継ぎつつ、関連性の高かったテーマを統合するとともに、昨今の開発環境の変化を踏まえてAI開発基礎を新たなテーマとして取り入れました。 約半年間の研修スケジュールで間延びしないように2025年内に全ての講義を完了するようにしました。 担当者の負荷が偏らないようにテーマごとの割り振りを均等に調整し、サポート体制なども明確になるように整備しました。 ターミナルおよび Git/GitHub の基礎・プログラム基礎 speakerdeck.com ターミナルおよび Git/GitHub の基礎・プログラム基礎の講義では、2024年度の「ターミナルおよび Git/GitHub の基礎」と「プログラム基礎」を統合し、 CLI(ターミナル)やチームでの開発を行うにあたり弊社でも利用している Git/GitHub の基本的な使い方、およびプログラムの基本的な構造やデータ構造、アルゴリズムについて学びました。 具体的には以下のトピックを取り上げました。 Linux コマンド Git とは? 木構造 配列とリスト ハッシュ ソート 探索 ネットワーク/インフラ基礎 speakerdeck.com ネットワーク/インフラ基礎の講義では、OSI 参照モデルを中心に、 ネットワークやインフラの基礎知識について学びました。 具体的には以下のトピックを取り上げました。 プロトコル TCP/IPとOSI参照モデル 関連するAWSリソース DB 研修 speakerdeck.com DB 研修では、DB の基本概念やバックエンド/データ系それぞれの視点での利用について学びました。 具体的には以下のトピックを取り上げました。 「データ」の種類と構造 SQLによるデータ操作 正規化・インデックス データ基盤の概要 Web 基礎・Web アプリケーション開発基礎 speakerdeck.com Web 基礎・Web アプリケーション開発基礎の講義では、2024年度の「Web 基礎」と「Web アプリケーション開発基礎」を統合し、 API や Web アプリケーションの基本構成や仕組み、バックエンド/フロントエンドそれぞれの役割、アーキテクチャやテスト、コーディング時に意識することなど、 組織/チームでの開発に携わるうえで重要となってくる考え方について学びました。 具体的には以下のトピックを取り上げました。 Web アプリケーションの構成要素 ブラウザが表示するHTMLの取得元について 外部データソースを利用した動的レンダリングについて フロントエンドとバックエンドの役割 開発に必要な知識 アーキテクチャ テスト CI/CD AI開発基礎 speakerdeck.com AI開発基礎の講義では、AIの概要やAIを活用した開発について学びました。 具体的には以下のトピックを取り上げました。 AIの基礎知識 AIの定義と分類 言語モデル(LLM)の仕組み トークン化とコンテキスト長 埋め込み(Embeddings) LLMの要素技術 プロンプトエンジニアリング 推論時のプロンプト手法 RAG(検索拡張生成)技術 LLMの拡張・統合技術 コンテキストエンジニアリング 開発ツールと活用事例の紹介 受講者のフィードバック 研修の改善のために、受講者からのフィードバックをアンケートで収集しており、その中でも以下のようなポジティブな意見が見受けられました。 図解や具体例(例:デリッシュキッチンだとどうか)が豊富だったため、とてもイメージしやすかった 昨今のAIによる開発環境の変化や具体的なモデルの構造などについて振り返ることができてよかった 基本的なことが丁寧に解説されていて分かりやすかった 一方で、「内容のボリュームに対して時間がタイトに感じられた」「実例や背景をより詳細に説明してほしい」といった意見もあり、 テーマに対して取り扱う内容の範囲を適切に設定する難しさを改めて実感しました。 得られたフィードバックを踏まえ、今後の研修運営をさらにブラッシュアップしていきたいと考えています。 講義風景① 講義風景② おわりに 今回の記事では、エンジニア内定者向けの研修についてご紹介いたしました。 内定者研修を通じて、今後入社するエンジニアのメンバーが入社後のオンボーディングをよりスムーズに進められるようにサポートすることができたと考えております。 また、研修の企画・運営に携わった若手メンバーにとっても、 知識の整理や研修の主要メンバーとしての新たなチャレンジの機会となり、貴重な成長の場にできたと感じています。 今回のような取り組みを含めて、今後もエンジニアの成長を支援する取り組みを続けて発信していきたいと思います。 最後まで読んでいただき、ありがとうございました。
アバター
目次 はじめに セッション紹介 zerobus Ingest Agent Bricks [企業セッション] イオンにおけるマルチエージェントシステムの開発 まとめ 最後に はじめに 2025年11月28日に開催された「Databricks DATA + AI WORLD TOUR」に参加させていただきました。 今回は参加レポートとして、セッションの感想をお届けします! dataaisummit.databricks.com セッション紹介 zerobus Ingest 開発1部の吉田です。 Databricks Sessionで紹介されたzerobus ingestについてまとめます。 zerobus ingestは、クライアントからDatabricksへデータを直接ストリーミングできるマネージドサービスです。 従来のような複雑なメッセージバス(KafkaやKinesisなど)を介することなく、レコード単位でのデータ取り込みを実現します。 docs.databricks.com 中間インフラを排除できるため、運用管理の手間とコストが削減できます。 エブリーでは現在、サイネージ端末やWebアプリケーションのログ収集のために専用のインフラを構築・運用しています。 このインフラをzerobus ingestで置き換えることができるか、検証を進めていく予定です。 Agent Bricks こんにちは! 開発1部デリッシュキッチンの蜜澤です。 僕からは、Databricks Sessionで紹介されたAgent Bricksについてまとめます! Agent BricksはノーコードでAIエージェントの構築、評価、最適化できる機能になっています! AIエージェントを作成する際の下記のような課題に簡単に対応することができます。 調整すべき項目が多すぎる 評価するのが難しい コストと品質のバランスを取るのが難しい Agent Bricksではタスクを選び、エージェントの役割をざっくり指定するだけで自動でエージェントを作成してくれるので、調整するべき項目が少なくなっているようです。 ドキュメントを元に質問に答える「ナレッジアシスタント」や、要約・分類などのカスタムテキスト変換を行う「カスタムLMM」、Genieスペースとエージェントを統合する「マルチエージェントスーパーバイザー」などのユースケースがあるようです。 エージェントの評価に関しては、自動でベンチマークを作成し、エージェントを自動で最適化できるようです。 また、good/badのような評価しかできないツールもありますが、Agent Learning from Human Feedback(ALHF)によって、自然言語での指示に基づき、システムを自動調整することもできます。 コストと品質に関しては、「コスト最適化」モデルと「品質最適化」モデルのどちらかを選択することができ、多くの場合は両立も可能なため、自前で構築するよりも高品質かつ低コストなエージェントを作成できることが多いようです。 Agent Bricksを使用してAIエージェントを作成すると、自前で作成する際に課題となる点を簡単に解決し、気軽にAIエージェントを作成できるのが魅力だと感じました。 デモパートでは、マルチエージェントスーパーバイザーを使用して、複数のエージェントを簡単にまとめる方法をご紹介いただきました。 PowerPointやWord形式のドキュメントと役割を与えられたナレッジアシスタントと、テーブルを読み取るためのGenieスペースを登録することで、ユーザーからの質問に対して、ナレッジを元に必要な情報を考え、必要な情報をGenieがクエリを作成しテーブルから取得するというマルチエージェントが簡単に作成できていました。 個人的に特に便利だなと感じたのは、ナレッジを元に回答できないパターンの質問に関しては、ガイドラインを登録し回答を準備しておくことが簡単にできることでした。 2025年11月時点ではasia-northeast1リージョンではAgent Bricksはまだ使用できないのですが、使用できるようになるのがとても楽しみになりました! [企業セッション] イオンにおけるマルチエージェントシステムの開発 開発1部の岩﨑です。私からはイオン様におけるマルチエージェントシステムの開発について紹介します。 イオン様ではマルチエージェントシステムを「業務特化型エージェント」と「顧客向けAIエージェント」に大別して紹介されていました。 中でも業務特化型エージェントでは、自然言語の質問に基づいたクエリを実行して情報抽出するエージェントが紹介されました。 具体的には、データ抽出エージェントや可視化エージェントといったそれぞれのタスクに対するエージェントがあり、それをまとめるマルチエージェントスーパーバイザーをユーザとLLMとのインターフェースとしておくような構造となっています。 これによってクエリの生成から可視化、整合性の評価まで一気通貫で実行するようなマルチエージェントシステムを構築しています。 このエージェントはコンテキストが曖昧だった場合は推測で答えるのではなく、ユーザに聞き返すような仕組みとなっています。 また大規模なクエリをAIが実行しないように、システムリソースを大量に消費するクエリが生成された場合にはエージェントが主体となって実行しない意思決定が行われるようになっていたり、クエリをキャッシュする戦略など大規模データを扱っている企業ならではの工夫点などもみられました。 クエリの実行はDatabricks GenieとUnity Catalogを使用してAPIとして公開することでシームレスかつ低レイテンシで連携することができます。 また、Unity Catalogはガバナンスを意識した設計になっているためAIエージェントによる不正なデータアクセスなどの心配もありません。 さらにエージェントを作って終わりではなく、Genieへのリクエストを収集、フィードバックの作成、最適化のループを回すことによって精度改善にも取り組んでいるそうです。 他にも色々なエージェントの工夫点が紹介されており、非常に有意義なセッションでした。 まとめ 今回のDatabricks DATA + AI WORLD TOURでは、zerobus IngestやAgent Bricksといった機能の紹介から企業様の活用事例まで、幅広いセッションを聴講することができました。 AIエージェントに関するセッションが多く、ノーコードでのエージェント構築から大規模な業務システムへの適用までAIエージェントの活用が急速に広がっていました。 今回得られた知見を活かし、Databricksの機能の検証やAIエージェントの活用を進めていきたいと思います。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
AppleとLINEのネイティブ認証をつくる(サーバー編) この記事は every Tech Blog Advent Calendar 2025 の 29 日目の記事です。 前提 アプリ側のAppleとLINEのネイティブ認証実装 Apple LINE IDトークンと nonce サーバー側の実装 Appleの細かいポイント Bundle ID と Service ID LINEの細かいポイント API IDトークンの検証アルゴリズム 最後に 参考資料 こんにちは!開発1部で食事管理アプリ ヘルシカ のサーバーサイドの開発をしている 赤川 です。約1ヶ月にわたって続いたアドベントカレンダーも最終日となりました。 本記事では、ヘルシカiOS で Apple と LINE のネイティブ認証を導入した経験をもとに、ネイティブ認証のサーバー側の実装と周辺知識、実装時の細かいポイントについてお話しします。 iOS側の実装については、昨日公開の AppleとLINEのネイティブ認証をつくる(iOS編) をご覧ください。 tech.every.tv 前提 ヘルシカではもともと Web アプリベースの認証方法が採用されており、アプリ内で WebView が開きそこでIDやパスワードを入力して サインアップ / サインイン を行います。以下、AppleやLINEのIDプロバイダーを外部IdP、ヘルシカの認証サーバーを単に認証サーバーと呼ぶことにします。認証サーバーではOpenID Connectに則った実装がされており、おおまかには以下のような流れです。 ログインボタンを押す アプリ内のWebViewで外部IdP(AppleやLINE)の入力画面が開く 入力成功後、外部IdPから認証サーバーにコールバック 認証サーバーが外部IdPとToken Exchangeを行い、認証サーバーが外部IdPのトークンを受け取る 外部IdPのトークンの検証に成功した後、ヘルシカAPIサーバー用のトークンを発行し、安全にClientに渡す アプリはほぼWebViewを開くだけで、IdPとメインでやり取りするのはサーバーであることがわかります。 上記の形だと、例えばヘルシカにWebアプリが増えた、という場合でも同じエンドポイントが使え、認証サーバーの追加実装なしに拡張が可能なのがメリットと言えます。しかし、パスワードなどの入力は面倒ですし、忘れがちです。そこで、ユーザーにより簡単にサインアップ・サインインをしてもらえるように、Appleなら顔認証などiOSネイティブな方法、LINEならLINEアプリが開いてワンタップで認証できる方法を新しく採用することになりました。 Appleの認証画面 LINEの認証画面 アプリ側のAppleとLINEのネイティブ認証実装 (サーバー編)とタイトルに入れましたが、サーバー側の設計をするためにはまずアプリ側でネイティブ認証を実装する方法を知らなければなりません。AppleではASAuthorizationAppleIDProviderなどのクラス、LINEではLINEログインSDKというものが用意されています。 developer.apple.com developers.line.biz 元のフローを思い出してみると、外部IdPのトークンを受け取るのは認証サーバーでした。しかし、上記を使用した場合、IDトークンを受け取るのはアプリになります。それぞれについて、少し深掘りしてみましょう。 Apple まず、リクエスト ASAuthorizationAppleIDRequest でAppleIDを含む情報をIdPに送信します。 ASAuthorizationAppleIDRequest は ASAuthorizationOpenIDRequest を継承しており、オプションとして state と nonce を含めることができます。 成功すると、レスポンスとして ASAuthorization を受け取ります。これには ASAuthorizationAppleIDCredential が含まれており、 さらにその中身を見ることで IDトークン や state が手に入ります。 LINE LINEでは、 LoginManager を使います。 LoginManager.shared.login でリクエストを送信しますが、Appleの時と違い nonce と state はSDK内で自動的に生成、検証されます。ただし、 nonce は独自に指定することも可能です。 成功すると、 LoginResult を受け取ります。この中にアクセストークンやIDトークンなどが含まれています。 比較すると、以下の2つが共通していることがわかりました。 リクエストには独自の nonce を設定することができる。 成功すると、アプリがIDトークンなどを受け取ることができる。 IDトークンと nonce サーバー側の設計に進む前に、IDトークンと nonce について復習しておきます。ご存知の方は無視して次のパートに進んでいただいて問題ありません。 まず、IDトークンは次のような形をしています。 eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ. ewogImlzcyI6ICJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsCiAic3ViIjogIjI0ODI4OTc2MTAwMSIsCiAiYXVkIjogInM2QmhkUmtxdDMiLAogIm5vbmNlIjogIm4tMFM2X1d6QTJNaiIsCiAiZXhwIjogMTMxMTI4MTk3MCwKICJpYXQiOiAxMzExMjgwOTcwLAogIm5hbWUiOiAiSmFuZSBEb2UiLAogImdpdmVuX25hbWUiOiAiSmFuZSIsCiAiZmFtaWx5X25hbWUiOiAiRG9lIiwKICJnZW5kZXIiOiAiZmVtYWxlIiwKICJiaXJ0aGRhdGUiOiAiMDAwMC0xMC0zMSIsCiAiZW1haWwiOiAiamFuZWRvZUBleGFtcGxlLmNvbSIsCiAicGljdHVyZSI6ICJodHRwOi8vZXhhbXBsZS5jb20vamFuZWRvZS9tZS5qcGciCn0. NTibBYW_ZoNHGm4ZrWCqYA9oJaxr1AVrJCze6FEcac4t_EOQiJFbD2nVEPkUXPuMshKjjTn7ESLIFUnfHq8UKTGibIC8uqrBgQAcUQFMeWeg-PkLvDTHk43Dn4_aNrxhmWwMNQfkjqx3wd2Fvta9j8yG2Qn790Gwb5psGcmBhqMJUUnFrGpyxQDhFIzzodmPokM7tnUxBNj-JuES_4CE-BvZICH4jKLp0TMu-WQsVst0ss-vY2RPdU1MzL59mq_eKk8Rv9XhxIr3WteA2ZlrgVyT0cwH3hlCnRUsLfHtIEb8k1Y_WaqKUu3DaKPxqRi6u0rN7RO2uZYPzC454xe-mg https://openid.net/specs/openid-connect-core-1_0.html#id_tokenExample 2つのドットがあり3つのパートに区切られていることがわかります。これらは前から順にヘッダー、ペイロード、署名と呼ばれていて、このような形式のトークンを署名付き JWT (JSON Web Token) と言います。それぞれについて詳しく見ていきましょう。 まずヘッダーを Base64url デコードすると、以下のようなJSONが得られます。 { " kid ":" 1e9gdk7 "," alg ":" RS256 " } alg はJWTの署名に使用されたアルゴリズムを表します。 kid は署名検証用の公開鍵の識別子で、公開鍵暗号方式の署名の場合に含まれます。 type: "JWT" というフィールドが含まれている場合もあります。 次に、ペイロードを検証、デコードしてみると、だいたい以下のような JSON が得られます。 { " iss ": " https://server.example.com ", " sub ": " 248289761001 ", " aud ": " s6BhdRkqt3 ", " nonce ": " n-0S6_WzA2Mj ", " exp ": 1311281970 , " iat ": 1311280970 , " name ": " Jane Doe ", " given_name ": " Jane ", " family_name ": " Doe ", " gender ": " female ", " birthdate ": " 0000-10-31 ", " email ": " janedoe@example.com ", " picture ": " http://example.com/janedoe/me.jpg " } 下の方はプロフィール的な情報なので無視して、上側のフィールドの説明を書き込むと以下のようになります。 { " iss ": " トークンを発行したサーバーの識別子 ", " sub ": " ユーザーの識別子 ", " aud ": " トークンを受け取るアプリの識別子(クライアントIDなど) ", " nonce ": " nonce(下で説明します) ", " exp ": " トークンの有効期限(UNIXタイムスタンプ形式) ", " iat ": " トークンの発行日時(UNIXタイムスタンプ形式) " } nonce フィールドがありますね! nonce があることで、どのような利点があるかを以下で説明します。 登場人物は以下です。 IdP: IDトークンを発行する Relying Party: 認証のサービスを使う(アプリや、そのサーバー) ユーザー: ログインしようとしている 攻撃者: IDトークンを盗んで、不正にログインしようとしている まず、 nonce がない場合です。 nonceがない場合 IDトークンが正しく、有効期限内であれば、誰でも何回でもログインできてしまうことがわかります。このような攻撃を、リプレイ攻撃と呼びます。 次に nonce がある場合です。 nonceがある場合 ユーザーのログイン後、保存されていた nonce はすでに削除されているので、攻撃者が盗んだIDトークンは使えなくなっています。 nonce が "Number used once" の略である、ということが納得できるかと思います。 最後に署名です。署名は、ヘッダーとペイロードから、IdPだけが持っている秘密鍵を使って計算、付与されます。IDトークンを受け取ったクライアントは、 alg に書いてある方法に従って署名を検証します。ここで検証に失敗すれば、何者かによってヘッダーまたはペイロードが書き換えられているということになります。 サーバー側の実装 それでは、サーバー側の実装について考えていきましょう。結果的に、エンドポイントは2つになりました。 IDトークンの検証は nonce を含めサーバー側で行います。 LINEログインSDK のように nonce をアプリ側で検証をすることも可能ですが、結局それではIDトークンを何度も使えてしまい、サーバーから見ると nonce を含めていないのと同じ状態になってしまいます。そもそも、クライアントから送られてきたものをサーバーがそのまま信じることはよろしくありません。 そうすると、必然的に nonce の生成もサーバー側が行うことになります。 LINE と Apple の比較で、リクエストには独自の nonce を設定することができる、ということを確認したので、サーバー側から nonce を渡すエンドポイントを作れば良いですね。また、 nonce を生成してキャッシュするのにキーが必要なので、その認証認可セッションの ID もランダムに生成して返却します。これが1つ目のエンドポイントになります(わかりやすさのため、一部フィールドを省略しています)。 リクエスト { app_id: " アプリの識別子 ", service_id: " IdPの識別子(line or apple) ", type : " signup or signin " } レスポンス { nonce : " nonce ", session_id: " その認証認可セッションの識別子 " , } クライアント側はこの送られてきた nonce を使い、IdPからIDトークンを取得し、セッションIDとともにサーバーに送ります。これが2つ目のエンドポイントです。 リクエスト { app_id: " アプリの識別子 ", session_id: " その認証認可セッションの識別子 ", id_token: " IDトークン ", authorization_code: " 認可コード(アクセストークンなどの取得に使う、Appleのみ) " } サーバー側はセッション ID からキャッシュしていた nonce を取り出し、IDトークンの検証時に一致を確認します。検証に成功後、サーバーのDBにユーザーを作成、または更新し、アプリのIDトークン、アクセストークン、リフレッシュトークンを発行して返却します。アプリのトークンの発行部分については Amazon Cognito のカスタム認証を使っているのですが、ここでは触れないことにします。 レスポンス { access_token: " アプリのアクセストークン ", id_token: " アプリのIDトークン ", refresh_token: " アプリのリフレッシュトークン ", expires_in: " トークンの有効期限 " } nonce を使うことで、安全にIDトークンをやり取りしてネイティブ認証を実現できました!最終的な流れは以下の通りです。 ネイティブ認証のフロー LINE の公式ドキュメントでも、同様の方法が推奨されています。図も付いていてわかりやすいので、合わせてご覧ください。 developers.line.biz Appleの細かいポイント Bundle ID と Service ID Bundle ID はアプリ固有の識別子、 Service ID はWebアプリなどがサインインなどのWebサービスを使う際に使用する識別子です。 元々の Web アプリベースの認証方法では Service ID が使われていましたが、ネイティブ認証では Bundle ID が使われます。 この使い分けが必要になるのは、トークンの Exchange の処理、つまり、クライアントから送られた authorization_code を使ってアクセストークンやリフレッシュトークンを取得する処理です。 取得のためには 専用のエンドポイント を叩きますが、リクエストのパラメータの一つに client_secret というのがあります。詳しい作成方法については以下を参照ください。 developer.apple.com client_secret は署名した JWT で、 sub フィールドを持ちます。ここに入るものが、 Web アプリベースの認証なら Service ID 、ネイティブ認証なら Bundle ID となります(上記リンク先には App ID と書いてありますが、Bundle ID でも機能します)。 LINEの細かいポイント API SDKは自動で nonce や state を生成、検証してくれたりとなかなかリッチでしたが、APIも充実しています。特に、IDトークン検証用のエンドポイントが用意されています。 developers.line.biz ローカルでIDトークンを検証するのは少しだけ大変なので、これは有難いです。今回はAppleとIDトークンの検証処理を共通化させたかったので採用しませんでしたが、LINEのみ実装する場合には良い選択肢かと思います。 IDトークンの検証アルゴリズム LINEでは、ネイティブアプリと Web アプリで署名アルゴリズムが異なります。 公式ドキュメント で以下のように記されています。 ネイティブアプリやLINE SDK、LIFFアプリに対してはES256(ECDSA using P-256 and SHA-256)が、ウェブログインに対してはHS256(HMAC using SHA-256)が返されます。 ES256 は公開鍵暗号方式、HS256 は共通鍵暗号方式で、ES256の方が鍵管理のリスクが低いです。私も実装後に知ったのですが、一般に MPA (Multiple Page Application) の Web アプリなどはAPIサーバーとIdPが同一管理下にあることが多く、クライアントで署名検証をすることがないため共通鍵暗号方式でも十分、ネイティブアプリや SPA (Single Page Application) の Web アプリは IdP と API サーバーが分離していることが多く、場合によってはクライアント側で検証をすることもあるため公開鍵暗号方式が推奨されている、という背景があるようです。 最後に 既に動いているサービスで安全な認証認可を実現するために、いろいろな記事、動画を参考にさせていただきました。受け売りですが、認証認可や課金などの実装は、いろいろな方の集合知の上に成り立つ類のものだと考えています。この記事がまた、これから認証認可を実装する方の一助となれば幸いです。 それでは少し早いですが、皆様、今年も1年お世話になりました。 来年もどうぞよろしくお願いいたします。 参考資料 Authentication Services | Apple Developer Documentation アプリとサーバーの間で安全なログインプロセスを構築する | LINE Developers LINEログイン v2.1 APIリファレンス | LINE Developers OAuth 2.0 Threat Model and Security Considerations Final: OpenID Connect Core 1.0 incorporating errata set 1 OAuth 2.0 Threat Model and Security Considerations RS256 vs HS256 What's the difference? | Auth0
アバター
この記事は every Tech Blog Advent Calendar 2025 の 28 日目の記事です。 はじめに こんにちは!開発1部で食事管理アプリ ヘルシカ の開発をしている新谷です。これまでサーバーサイドを担当していましたが、直近ではiOS開発にも携わっています。 ヘルシカiOSでは、これまでWebViewベースの認証を採用していましたが、AppleとLINEのネイティブ認証を導入しました。ネイティブ認証では、Appleなら顔認証やパスコード、LINEならLINEアプリでのワンタップ認証が可能になり、ユーザー体験が大きく向上します。 本記事では、iOS側の実装について解説します。認証の仕組みやサーバー側の設計については、明日公開予定の「サーバー編」をご覧ください。 ネイティブ認証の全体像 ネイティブ認証のフローは以下のようになります。 ポイントは、認証サーバーが生成したnonceをSDKに渡すことです。これにより、サーバー側でID Tokenの検証時にリプレイ攻撃を防ぐことができます。nonceの役割や検証の詳細については、明日の「サーバー編」で解説します。 Sign in with Appleの実装 Sign in with AppleにはAuthenticationServicesフレームワークを使用します。 developer.apple.com ASAuthorizationAppleIDProviderの使い方 import AuthenticationServices func signInWithApple (nonce : String ) { let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName, .email] request.nonce = nonce // サーバーから取得したnonceを設定 let controller = ASAuthorizationController(authorizationRequests : [ request ] ) controller.delegate = self controller.presentationContextProvider = self controller.performRequests() } Delegateでの結果受け取り extension AppleNativeAuthProvider : ASAuthorizationControllerDelegate { func authorizationController ( controller : ASAuthorizationController , didCompleteWithAuthorization authorization : ASAuthorization ) { guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential , let identityTokenData = credential.identityToken, let idToken = String(data : identityTokenData , encoding : .utf8), let authorizationCodeData = credential.authorizationCode, let authorizationCode = String(data : authorizationCodeData , encoding : .utf8) else { // エラーハンドリング return } // idToken と authorizationCode をサーバーに送信 } func authorizationController ( controller : ASAuthorizationController , didCompleteWithError error : Error ) { // ユーザーキャンセルやその他のエラー処理 } } 取得できるもの Sign in with Appleからは以下の情報を取得できます。 項目 説明 ID Token JWTフォーマット。nonceが含まれる Authorization Code サーバーでのトークン取得に使用 User Identifier ユーザーの一意な識別子 Full Name 初回認証時のみ取得可能 Email 初回認証時のみ取得可能 LINE SDKの実装 LINE LoginにはLINE SDK for iOS Swiftを使用します。 developers.line.biz LINE SDKのセットアップ Swift Package Managerで以下のURLを追加します。 https://github.com/line/line-sdk-ios-swift.git Info.plistにも設定が必要です。 <key> LineSDKConfig </key> <dict> <key> ChannelID </key> <string> YOUR_LINE_CHANNEL_ID </string> </dict> <key> CFBundleURLTypes </key> <array> <dict> <key> CFBundleURLSchemes </key> <array> <string> line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER) </string> </array> </dict> </array> <key> LSApplicationQueriesSchemes </key> <array> <string> lineauth2 </string> </array> LoginManagerの使い方 import LineSDK func signInWithLine (nonce : String , from viewController : UIViewController ) { LoginManager.shared.login( permissions : [ .profile, .openID ] , in : viewController , parameters : . init (IDTokenNonce : nonce ) // サーバーから取得したnonceを設定 ) { result in switch result { case .success( let loginResult ) : guard let idToken = loginResult.accessToken.IDToken else { // エラーハンドリング return } // idToken をサーバーに送信 case .failure( let error ) : // ユーザーキャンセルやその他のエラー処理 } } } Appleとの違い LINE SDKとSign in with Appleの主な違いは、Authorization Codeの有無です。Appleではサーバーでのリフレッシュトークン取得にAuthorization Codeが必要ですが、LINEではリフレッシュトークンがSDK内部で管理されます。 共通点として、どちらも独自のnonceを設定でき、ID Tokenを取得できます。 最初の設計と問題点 クリーンアーキテクチャでの設計 ヘルシカiOSではクリーンアーキテクチャを採用しています。アーキテクチャの詳細については ヘルシカiOSアプリのアーキテクチャについて をご覧ください。 当初、ネイティブ認証も既存のアーキテクチャに従って以下のように設計しました。 Feature層(ViewModel) ↓ UseCase層 ↓ Repository層 ↓ Infra層(SDK呼び出し) ↓ 外部SDK(LINE SDK / AuthenticationServices) 問題点:認証処理がViewに影響を与える 実装を進める中で、この設計には問題があることがわかりました。 Sign in with Appleは ASAuthorizationController で認証処理を実行すると認証UIが表示され、 ASAuthorizationControllerPresentationContextProviding で表示先のWindowを指定します。 // Sign in with Apple:認証UIを表示するためにWindowを指定 extension AppleAuthProvider : ASAuthorizationControllerPresentationContextProviding { func presentationAnchor ( for controller : ASAuthorizationController ) -> ASPresentationAnchor { return window } } LINE SDKも同様に、認証処理を呼び出すとLINEアプリまたはWebViewが起動し、Viewに影響を与えます。 // LINE SDK:認証処理を呼び出すとLINEアプリまたはWebViewが起動 LoginManager.shared.login( permissions : [ .profile ] , in : viewController , parameters : . init (IDTokenNonce : nonce ) ) つまり、これらの認証処理を呼び出すとViewレイヤーに影響を与えることになります。 Infra層は本来、外部APIやLocalStorageなど、UIに依存しない外部リソースへのアクセスを担当する層です。 認証処理がViewに影響を与えるものをInfra層に配置するのは、アーキテクチャとして適切でないと考えました。 解決策:NativeAuthパッケージの分離 この問題を解決するために、認証処理をクリーンアーキテクチャの外に独立したパッケージとして分離しました。 新しいアーキテクチャ Feature層(ViewModel) │ ├──────────────────────> NativeAuthパッケージ(独立) │ ├── LineNativeAuthProvider │ └── AppleNativeAuthProvider ↓ UseCase層 ↓ Repository層 ↓ Infra層 ポイント NativeAuthパッケージをクリーンアーキテクチャとは独立した位置に配置 ViewModelから直接NativeAuthProviderを呼び出す構成に変更 UseCase/Repository/Infra層はサーバーとの通信(nonce取得、ID Token検証)に専念 この設計には、UIに依存する処理をInfra層に置かずに済み、認証処理を独立パッケージとして管理できるというメリットがあります。一方で、ViewModelが認証処理を直接呼び出すため、Feature層の責務が増えるというデメリットもあります。 ただ、Infra層にUI依存のコードを置くことの違和感の方が大きかったため、今回はこの設計を選びました。 まとめ 最近サーバーサイドからiOS開発も担当するようになったので、モバイルアプリ特有のアーキテクチャには苦戦しました。 特にViewはサーバーでは意識しない概念だったので、今後も適切な場所に配置できるよう気をつけていきたいです。 明日は「サーバー編」として、nonceの役割やID Tokenの検証など、サーバー側の実装についての記事が公開されます。ぜひそちらもご覧ください。 参考資料 Implementing User Authentication with Sign in with Apple | Apple Developer Documentation ASAuthorizationAppleIDProvider | Apple Developer Documentation GitHub - line/line-sdk-ios-swift: Provides a modern way of implementing LINE APIs. LINEログインSDK | LINE Developers
アバター
この記事は every Tech Blog Advent Calendar 2025 の 27 日目の記事です。 はじめに こんにちは。リテールハブ開発部の清水です。 私たちは小売向けサービスをLaravelで開発しています。 このプロジェクトではGit hooksのpre-commit設定を使用してコミットのタイミングでLaravel Pint, Larastanを呼び出すことでコード品質を整えるための仕組みを使用しています。 この仕組みのベースは、プロジェクト初期に整備されたものを引き継いだもので、今回その内容を見直しながら整理しました。 ちょうど良い機会でしたので、本記事で私たちが使用している設定内容をご紹介いたします。 Git hooksとは? Git hooksとは、git commit や git push などの Git 操作をきっかけに、自動でスクリプトを実行できる仕組みです。 コミット前にチェック処理を挟むなど、人の操作ミスを防ぐための自動処理を組み込む用途で使われます。 https://git-scm.com/docs/githooks Laravel Pintとは? Laravel Pint は、Laravel公式が提供している PHPコードの自動フォーマッターです。 決められたコーディング規約(Laravel / PSR-12 など)に従って、PHPコードの書き方を自動的に統一します。 https://laravel.com/docs/12.x/pint Larastanとは? Larastan は、PHP の静的解析ツール PHPStan を Laravel 向けに拡張したツールです。 コードを実行せずに解析し、存在しないプロパティや型の不整合などの問題を事前に検出します。 https://github.com/larastan/larastan コミットを行った時の処理の流れ git commitを実行すると、Git hooksのpre-commitフックが起動 コミット対象のPHPファイルを取得 Laravel Pintによるフォーマットチェック Larastanによる静的解析 3 or 4のチェックでエラーが検出された場合、コミットを中断 全てのチェックをパスした場合のみ、コミットが完了 pre-commit設定内容 #!/bin/sh set -eu # --- 1) コミット対象(ステージ済み)のPHPファイルだけ拾う --- php_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.php$' || true) # PHPファイルがなければ何もしない if [ -z "$php_files" ]; then exit 0 fi echo "コミット対象のPHPファイルをチェックしています..." # --- 2) Pint:フォーマットチェック(修正はしない) --- echo "Pintでフォーマットをチェックしています..." if ! echo "$php_files" | xargs ./vendor/bin/pint --test; then echo "" echo "❌ フォーマットエラーがあります。" echo " 以下のコマンドで修正してください:" echo " make lint" exit 1 fi echo "✓ フォーマットチェック OK" # --- 3) Larastan:静的解析 --- if echo "$php_files" | grep -qE '^app/'; then echo "Larastanで静的解析を実行しています..." if ! ./vendor/bin/phpstan analyse --no-progress --memory-limit=1G; then echo "" echo "❌ 静的解析エラーがあります。" echo " エラーを修正してから再度コミットしてください。" exit 1 fi echo "✓ 静的解析 OK" fi echo "" echo "✓ 全てのチェックが完了しました。" exit 0 Makeコマンドの紹介 コミットのタイミングだけではなく、手動でLaravel Pint, Larastanを実行したい場面もあります。 以下のMakeコマンドで実行できるようにしています。 # コードスタイルチェック&修正 lint: @files=$$(git diff --cached --name-only --diff-filter=ACM | grep "\.php$$" | sed "s|^$$(basename $$(pwd))/||"); \ if [ -n "$$files" ]; then \ docker compose -f $(COMPOSE_FILE) exec -T $(CONTAINER_PHP) sh -c "./vendor/bin/pint $$files"; \ else \ echo "ステージされたPHPファイルがありません"; \ fi # コードスタイルチェック lint-check: @files=$$(git diff --cached --name-only --diff-filter=ACM | grep "\.php$$" | sed "s|^$$(basename $$(pwd))/||"); \ if [ -n "$$files" ]; then \ docker compose -f $(COMPOSE_FILE) exec -T $(CONTAINER_PHP) sh -c "./vendor/bin/pint --test $$files"; \ else \ echo "ステージされたPHPファイルがありません"; \ fi # 静的解析 larastan: docker compose -f $(COMPOSE_FILE) exec $(CONTAINER_PHP) ./vendor/bin/phpstan analyse --memory-limit=1G 実際にコミットする流れ Laravel Pint, Larastanで弾かれる内容のコードを作成 <?php namespace App\Http\Controllers; // importが名前順になっていない use Retailapp\Common\Models\User; use Illuminate\Http\JsonResponse; use App\Http\Controllers\Controller; class TestController extends Controller { public function index () : JsonResponse { $ user = User :: find ( 1 ) ; $ name = $ user -> undefined_property; // 存在しないプロパティを呼び出している return response () -> json ([ 'name' => $ name ]) ; } } コミットを試みると、フォーマットエラーで弾かれる % git commit -m '弾かれてほしいコミット' コミット対象のPHPファイルをチェックしています... Pintでフォーマットをチェックしています... ⨯ ──────────────────────────────────────────────────────────────────── Laravel FAIL ............................................. 1 file, 1 style issue ⨯ app/Http/Controllers/TestController.php no_unused_imports, ordered_import… ❌ フォーマットエラーがあります。 以下のコマンドで修正してください: make lint pintで自動的に修正 % make lint ✓ ──────────────────────────────────────────────────────────────────── Laravel FIXED ...................................... 1 file, 1 style issue fixed ✓ app/Http/Controllers/TestController.php no_unused_imports, ordered_import… もう一度コミットすると、今度はLarastanで弾かれる % git commit -m '弾かれてほしいコミット' コミット対象のPHPファイルをチェックしています... Pintでフォーマットをチェックしています... ──────────────────────────────────────────────────────────────────── Laravel PASS ............................................................ 1 file ✓ フォーマットチェック OK Larastanで静的解析を実行しています... ------ -------------------------------------------------------------------------- Line Http/Controllers/TestController.php ------ -------------------------------------------------------------------------- :14 Access to an undefined property Retailapp\Common\Models\User::$undefined_property. 💡 Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property ------ -------------------------------------------------------------------------- [ERROR] Found 1 error ❌ 静的解析エラーがあります。 エラーを修正してから再度コミットしてください。 Larastanに違反する部分を修正 <?php namespace App\Http\Controllers; use Illuminate\Http\JsonResponse; use Retailapp\Common\Models\User; class TestController extends Controller { public function index () : JsonResponse { $ user = User :: find ( 1 ) ; $ name = $ user -> name ; // 修正 return response () -> json ([ 'name' => $ name ]) ; } } コミット成功 % git commit -m '通ってほしいコミット' コミット対象のPHPファイルをチェックしています... Pintでフォーマットをチェックしています... . ──────────────────────────────────────────────────────────────────── Laravel PASS ............................................................ 1 file ✓ フォーマットチェック OK Larastanで静的解析を実行しています.. [OK] No errors ✓ 静的解析 OK ✓ 全てのチェックが完了しました。 おわりに 本記事では、私たちの Laravel プロジェクトで使用している Git hookのpre-commit 設定と、その中で Laravel Pint・Larastan をどのように組み込んでいるかをご紹介しました。 同様の仕組みを検討されている方の参考になれば幸いです。 最後までお読みいただきましてありがとうございました。
アバター
Go 1.26で変わるgo fix この記事は every Tech Blog Advent Calendar 2025 の 26 日目の記事です。 はじめに go fixとは Go 1.26での変更点 modernizeとは 使い方 実行例 inlineとは 使い方 実行例 IDEでの修正 新しいgo fixでできること まとめ はじめに 開発本部でデリッシュキッチンアプリ課金ユーザー向けの開発を担当している hond です! 先日2026年2月にリリース予定のgo1.26のRelease Candidate 1であるgo1.26rc1がリリースされました。もうrc1は確認できたでしょうか?確認がまだの方は こちら から確認できるのでぜひ! 今回はGo 1.26でgo fixが大幅に変更されるとのことだったのでそちらについて説明しようと思います。 go fixとは go fix はGo 1リリースにて破壊的変更を含む古いAPIを特定し、新しいものに修正するツールとして追加されました。 go fix によって大まかな修正はできるため、私たちはコアな部分の修正に集中できるようになっていました。 Go 1リリースの際は go fix での修正対象は多く Go 1のRelease Notes で Updating: Running go fix と調べるだけでも14件ヒットし、 ソースコード を確認すると37件ものルールがあることが確認できます。その後も go fix のルールは追加削除が繰り返されていきましたが、Goがメジャーバージョン内での後方互換性を担保しているので最近はなかなか変更が行われず使わないコマンドとなっていました。 Go 1.26での変更点 github.com Go 1.26では cmd/go: fix: apply fixes from modernizers, inline, and other analyzers にて go fix が大幅に変更される旨のProposalがAcceptされました。 この変更の背景は、元のissueである cmd/fix: remove all functionality とGopherCon 2025でのAlan Donovanの発表( Analysis and Transformation Tools for Go Codebase Modernization )から確認できます。具体的には以下の3点が挙げられています。 「go fixとは」でも触れた既存 go fix の機能である context や buildtag の修正が不要になったこと コードレビュー時間の短縮や開発者の教育のためにモダン化が必要だったこと AI/LLMが生成するコードの品質改善のためにモダン化が必要だったこと これらの背景を踏まえ、 modernize や //go:fix inline のようなGoのコードのモダン化を目的とするアナライザーを go vet 同様に go fix でも利用可能にすることがこのProposalの目的となっています。 go vet と go fix の使い方は似ていますが、あくまで go vet はアナライザーを用いて静的解析した診断結果を 報告 するツール、 go fix は検出した問題をアナライザーが提案する修正案に 修正 するツールという使い分けになるようです。 以降では、Go 1.26の go fix で利用可能になる modernize と inline について詳しく説明します。 modernizeとは pkg.go.dev modernize は golang.org/x/tools/go/analysis/passes/modernize で公開されているアナライザーのスイートです。Goのコードを最新の構文や標準ライブラリの機能を利用して、より簡潔でモダンな形式に修正することを目的としています。 現在24個のアナライザーが登録されており、その中から代表的なものを紹介します。Go 1.26で追加される errors.AsType のアナライザーも既に登録されています! Analyzer 説明 any interface{} を any に修正する errorsastype errors.As をGo 1.26で追加された errors.AsType に修正する 使い方 以下のコマンドでmodernizeを実行することができます。 $ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./... また、特定のアナライザーを有効・無効化することも可能です。 # anyアナライザーを有効化 $ modernize -any =true -fix ./... # anyアナライザーを無効化 $ modernize -any =false -fix ./... 基本的には修正を適用しても動作が変わらないよう設計されていますが、 bloop のように完全に動作を保証できないアナライザーもあり、それらはデフォルトでは無効化されています。 実行例 以下では実際に any アナライザーを使って interface{} を any に修正してみます。 修正前のコード: package main import "fmt" type OldInterface interface {} func main() { old := OldInterface( 1 ) fmt.Println(old) } コマンド実行: $ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./... 出力(差分): import "fmt" -type OldInterface interface{} +type OldInterface any func main() { old := OldInterface(1) このように、 interface{} が any に自動で修正されたことが確認できます。 inlineとは pkg.go.dev inline は golang.org/x/tools/go/analysis/passes/inline で公開されているパッケージで、 inline と gofixdirective の2つのアナライザーが含まれています。 inline アナライザーは //go:fix inline コメントディレクティブに基づいて修正を行います。主に非推奨になった関数や定数を新しいものに置き換えるために使用され、 ioutil package などで利用されています。 具体的な使い方は以下の通りです。 関数の場合 :修正したい関数をラップした関数を作成し、その関数にコメントディレクティブを加えることで修正が可能です。 //go:fix inline func Square(x int ) int { return Pow(x, 2 ) } 定数の場合 :修正したい変数を左辺に、修正後の変数を右辺に記述し、コメントディレクティブを加えることで修正が可能です。 //go:fix inline const Ptr = Pointer 使い方 以下のコマンドで inline を実行することができます。 $ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./... また、goplsを用いているIDE上では対象をホバーすることでも修正が可能です。 実行例 以下では、 OldFunction と NewFunction を定義し、 OldFunction を NewFunction に修正する際の例を示します。 修正前のコード: package main import "fmt" func main() { OldFunction() NewFunction() } //go:fix inline func OldFunction() { NewFunction() } func NewFunction() { fmt.Println( "NewFunction" ) } コマンド実行: $ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./... 出力(差分): import "fmt" func main() { - OldFunction() + NewFunction() NewFunction() } このように、 OldFunction が NewFunction に修正されるのが確認できます。 IDEでの修正 コマンドを使わずに、IDE上で対象をホバーすることでも修正が可能です。 IDEでホバーした時の画像 Quick Fixを選択すると、修正候補が表示されクリックすることで修正が可能です。 Quick Fixを押した時の表示 新しいgo fixでできること ここまで説明した modernize と inline は、Go 1.26からは go fix を実行するだけで適用できるようになります。今までは破壊的変更を修正するためのツールだった go fix が、Goのコードをモダン化していくためのツールへとシフトしました。 実際にgo1.26rc1を使って go fix を実行してみます。 $ go1.26rc1 fix ./... modernizeの結果: import "fmt" -type OldInterface interface{} +type OldInterface any func main() { old := OldInterface(1) inlineの結果: import "fmt" func main() { - OldFunction() + NewFunction() NewFunction() } このように、 go fix を実行するだけでコードをモダン化された状態に持っていくことができます。コード品質の担保がより容易になるため、pre-commitフックやCI、AI/LLMの出力後に適用するルールとして定義するのが良さそうです! まとめ 私自身 go fix を使う機会がなかったので、今回のアップデートで既存の go fix とモダン化の為のツールとして進化した go fix を会社のコードに適用するいい機会になりました。既存の go fix での修正点は一部の context のみでしたが、新しい go fix を実行したところ1249の修正点が確認されました。10年もののサービスという事もありこれらのコードを全て手動で特定し、修正していくとなると莫大な労力を要するので公式からこのようなツールが出ていることはとても有り難いなと思いました。 上記で修正されたのは modernize に定義されたアナライザーやそれぞれのpackageにて //go:fix inline が定義されているものですが、 //go:fix inline に関しては普段の開発でも非推奨化したがバージョン互換性担保の観点で削除できないコードを管理する際にとても有用なので積極的に利用して行きたいです。 AIはどうしても既存のコードベースに品質が左右されてしまうので新しくなった go fix を用いて、継続的な品質改善が重要だと感じました。また、公開されたコードベースを元にAIは学習していくので今後の自分を含めたGopherがAIを通してより良いコードを書くためにも積極的にモダン化を行おうと思います。 ここまで読んでいただきありがとうございます。Go 1.26で go fix がどう変わるか迷っている方の助けになれたら幸いです!
アバター
この記事は every Tech Blog Advent Calendar 2025 の25日目の記事です。 目次 はじめに 設計から軌道修正まで 1. 何を目指していたか 2. 運用して顕在化した問題 検索クエリの生成が安定しない 見出しに合うレシピが必ず存在するかどうかはわからない 条件に合わないレシピが含まれてしまう 3. 問題の原因 プロンプトの肥大化 不要な思考(ニーズ分析)を挟んでいた ベクトル検索に対して除外の前後処理を入れていなかった 根本にあった認識の甘さ 4. どう軌道修正したか ニーズ分析のステップを排除 見出しの自由生成・クエリ生成を排除 レシピをベクトル検索からOpenSearch利用に変更 LLM-as-a-Judgeの導入 5. 前提条件の見直し 振り返りから見えたワークフロー設計の勘所 ステップごとに切り分けて進める AIに任せる部分・ルールベースにすべき部分を見極める 運用を見据えたガードレール設計 今後の取り組みと展望 参考文献 はじめに こんにちは。2025年4月にソフトウェアエンジニアとして新卒入社した 黒髙 です。普段は デリッシュキッチン の開発に携わっています。 デリッシュキッチンにはレシピだけでなく、料理に関する情報をまとめた 記事 や レシピ特集 など、多様なWebコンテンツがあります。コンテンツディレクターの業務効率化の一環として、ユーザーニーズを満たすコンテンツ作成をAIでサポートする試みを、私は設計から実装までを一貫して担当しました。 当初はAIに創造的なコンテンツ生成を任せようと意気込んでいたのですが、実運用では想定外の課題が次々と顕在化し、最終的にはルールベースの制約(ガードレール)を多く追加することになりました。 以下画像は、実際に生成された アボカドサーモンの魅力を引き出すレシピ集 | デリッシュキッチン のコンテンツの一部です。 ページの一例 実際に自動生成して公開しているコンテンツは、 セレクション一覧 からもご覧いただけます。 また、記事生成に関わる処理の全体像は次の通りです。AIワークフローの詳細は後述します。 処理の流れとアーキテクチャ 本記事では、AIワークフロー設計で何を期待し、何が起き、どのように軌道修正したかを振り返ります。LLMや生成AI活用の文脈では基本的な話も含みますが、現場で使えるものにするためにどう整えていったのかの判断を中心にまとめます。 設計から軌道修正まで 本章では設計段階から運用に至るまで、AIワークフローに関する部分の振り返りを行います。 1. 何を目指していたか 理想は、AIがWebページの構成を考え、分析結果に応じて組み替えられることです。ただし今回は、早期にコンテンツを公開することを優先したPoC(初期検証フェーズ)として、ページ構成を「見出し+説明文+レシピリンク集」の繰り返しに限定しました。 - 見出し1 - 見出し1の説明文 - 見出し1のレシピリスト - レシピA - レシピB - レシピC - 見出し2 ... (以下繰り返し) 当初は、ニーズ分析→見出し・説明文の生成→関連レシピの検索までをAIが一貫して行うことを目指しました。具体的には、次の5ステップのLLMパイプラインを設計しました。 当初のワークフローと具体例 また、初期検証をできるだけ早く進めるため、 デリッシュAI で利用しているレシピ一覧を埋め込みベクトルに変換したテーブルを使いました。 2. 運用して顕在化した問題 数パターンのキーワードで試したところ、課題は残るものの、記事として形になることは確認できました。そこでバックエンド/フロントエンドを実装し、キーワード一覧をもとにバッチで自動生成を開始しました。 ところが「キーワードに沿ったレシピが選ばれているか」の観点の公開前レビューでは、公開できる記事が想定より少ない結果になりました。さまざまな入力パターンを事前に十分検証し、試行段階で見えていた課題にこの時点で対処しておくべきでした。 具体的な問題としては以下のようなものがあります。 検索クエリの生成が安定しない 今回のフローでは画像の通り、見出しに基づいたレシピ検索クエリをLLMに生成させています。例えば 簡単に作れるごぼうと牛肉レシピ を入力しても、 簡単レシピ や 簡単牛肉 のように意図が欠けたクエリになることが稀にありました。結果として後続のLLMにも意図したリクエストを送れず、適切な記事が生成できない問題がありました。 見出しに合うレシピが必ず存在するかどうかはわからない 検索キーワード ごぼう 牛肉 から ごぼうと牛肉のローストビーフ のような見出しが生成されても、デリッシュキッチンにそのようなレシピがあるとは限りません。具体的に絞り込む見出し提案に従った結果、レシピが登場しないといった問題が発生しました。 条件に合わないレシピが含まれてしまう 例えば 簡単に作れるごぼうと牛肉のレシピ に対して、 味付け簡単! 牛すじとごぼうの塩麹煮 のような適切なレシピが出る一方で、 簡単で本格的な味! 豚バラ肉とごぼうの和風煮 といった、響きは似ているものの材料が欠けているレシピも混ざることがありました。 3. 問題の原因 プロンプトの肥大化 デリッシュキッチンでは、信頼性の高いコンテンツ品質を維持するため、多くの制作ルールが存在しています。以下は一例ですが、実際には数十個程度の記載NG項目があります。 - 健康・栄養に関する過度な表現禁止: ダイエット, ヘルシー ... - 食欲・体調への直接効果を謳う表現禁止: 食欲, 体力, 元気 ... 料理系コンテンツでは連想語がNGになることも多く、AIにとって制約の強い要求です。さらに、これらをすべてのステップで指示していたため、禁止事項を追加しても一度で要求を満たし切れないことがありました。 不要な思考(ニーズ分析)を挟んでいた ニーズに合致した構成にするためにStep 1としてニーズ分析を入れましたが、禁止事項を守らせることや、ヒットするレシピ数を担保するために無難な見出しに寄せる要件とは相性が悪く、逆にノイズになっていました。 ベクトル検索に対して除外の前後処理を入れていなかった レシピ検索としてベクトル検索とコサイン類似度によるスコアリングを用いていましたが、関係のないレシピも検索結果に含まれてしまう問題が発生していました。ベクトル検索は意味的な類似性を捉えるのには優れていますが、それ単体で材料として実際に使われているかどうかを判定することはできません。 根本にあった認識の甘さ これらの問題に共通していたのは、以下の点に対する認識の甘さでした。 運用では検証段階より多種多様なキーワードを扱うこと LLMによる不安定な出力が次段階の生成内容にも影響をもたらすこと 当初は創造的なWebサイト構築を漠然と想像していましたが、実際に必要だったのはAIが生成した記事をできるだけ早く公開し、生成から公開までのサイクルを回しながら検証を進めることでした。 しかし、少数のキーワードではそれなりに記事が生成できたことで安心してしまい、安定運用できるかという観点が抜け落ちたまま次の開発に進んでしまっていました。 4. どう軌道修正したか 問題を踏まえて、以下のような軌道修正を行いました。 以前のステップ 変更点 1. キーワードのニーズ分析 削除 2. 小見出し生成 LLMによるテンプレート選択 3. 検索キーワード抽出 ルールベースで生成 4. コンテンツ生成 変更なし 5. レシピ検索 OpenSearch + LLM-as-a-Judgeの追加 ニーズ分析のステップを排除 前述の通り、ノイズとなっていたStep 1(キーワードのニーズ分析)を削除しました。 将来的には分析データが揃い次第、数値に基づくニーズ分析を復活させ、LLMによる構成検討も行いたいと考えています。 見出しの自由生成・クエリ生成を排除 対象を絞り込まない抽象的な見出しにしたいこと 禁止ワードを含めたくないこと を踏まえると、見出しのパターンはある程度限られてくることが分かりました。ヒットするレシピを安定して出すために、LLMに自由生成させるのではなく、見出し候補を用意して選択してもらう方針に転換しました。 SUBHEADING_OPTIONS = [ { "kind" : 1 , "template" : "簡単に作れるxxのレシピ" }, { "kind" : 2 , "template" : "xxとxxの絶妙な組み合わせ" }, { "kind" : 3 , "template" : "xxを使ったアレンジレシピ" }, { "kind" : 4 , "template" : "新しいxxの料理アイデア" }, # ... ] KIND_KEYWORD_MAPPING: dict [ int , str ] = { 1 : "{keyword} 簡単" , 2 : "{keyword} 組み合わせ" , 3 : "{keyword} アレンジ" , 4 : "{keyword} アイデア" , # ... } また、見出しに基づくクエリ生成もLLMに任せるのをやめ、(検索キーワード+見出しの特徴)をルールベースで組み立てる方針に戻しました。 HyDE(Hypothetical Document Embeddings) のような手法も検討しましたが、1見出しに最大9個のレシピを網羅的に取得したい要件には合わないと判断し、簡易的で汎用的なクエリで対応することにしました。 レシピをベクトル検索からOpenSearch利用に変更 構想段階の試用では、デリッシュAIの基盤を一部利用したベクトル検索が手軽で都合が良かったので採用していました。 しかし、ワークフローを見直し、扱うキーワードや見出し表現をある程度固定化した結果、検索に求める要件も変化しました。 以下の理由から、レシピ検索機能で既に用いているOpenSearchを利用する方針としています。 多くの対象キーワードにおいて、通常検索の方が期待したレシピが安定してヒットすること レシピ検索機能で用いられているシノニム辞書をそのまま活用できること 数パターンに固定化した見出し表現においては、ベクトル検索の強みである意味的類似度の必要性が相対的に薄れたこと なお、今後扱うキーワードの幅や要件が変わった場合には、再びベクトル検索を含めた見直しを行う余地があると考えています。 LLM-as-a-Judgeの導入 更なるガードレールとして、レシピが見出しの意図に合っているかを審査するステップを追加しました。この判定を導入することで、条件に合わないレシピをより厳格に除外できることが期待できます。 { " recipe_id ": " 206162500360602656 ", " accept ": true , " reason ": " タイトル・材料に「牛すじ(牛肉)」とごぼうが含まれており、サブ見出しの「ごぼう 牛肉 簡単」に合致します(味付けが簡単な点も明記)。調理手順は煮込み工程があるものの、小見出しの意図と矛盾していません。 " } , { " recipe_id ": " 163718318442676716 ", " accept ": false , " reason ": " ごぼうは使われていますが、主な肉材が豚バラ肉であり、小見出しが想定する『牛肉』と一致しません。 " } , まとめると、フローは次の通りです。 ワークフロー 初期段階では1記事あたり3〜10件以上の修正指摘が発生していたものが、これらの改善によって修正指摘が0件のケースが大半となり、あっても1〜2件程度に収まるようになりました。 5. 前提条件の見直し ワークフローの見直しを進める中で、そもそもどのキーワードを生成対象とすべきかという前提条件にも課題があることが分かりました。 当初は、食に関連するキーワードを一律に処理していましたが、キーワードによって求められる判断や前処理の難易度が大きく異なることが次第に明らかになりました。 例えば カニ レシピ では、カニカマを許容するかどうかの意思決定が必要で、LLMには難しいです。また、 酒のあて のように抽象度の高いキーワードでは、どの切り口で掘り下げるかという追加設計が求められます。 こうした性質の異なるキーワードを同一の前提でAIワークフローに流していたことが、後段のLLM-as-a-Judgeや人手レビューの負荷を高め、全体の不安定さにつながっていました。 ひとまずは、AIワークフローの前段としてキーワードの取捨選択を行い、判断コストが高そうなものや、追加設計が必要なものは対象外とする方針で生成していきます。 振り返りから見えたワークフロー設計の勘所 これまでの試行錯誤を振り返ると、技術的な工夫以上に ワークフロー設計そのものの考え方 が重要だったと感じています。 ステップごとに切り分けて進める 今回のようにLLMの出力結果が後続の処理に大きく依存するワークフローでは、各ステップが単体で正しく機能するかをまず確認する必要があります。想定動作・実装方針・失敗時のハンドリングまで詰めたうえで次に進むことが、品質担保と手戻り削減の観点で重要であると感じています。 AIに任せる部分・ルールベースにすべき部分を見極める 本取り組みを通じて、LLMに任せる部分/任せない部分を明確にすることが、ワークフロー全体の安定性に直結することが分かりました。 特に意識したのは、次の3点です。 決定的に定義できる処理は、LLMに任せない 出力の揺れが後段に大きく影響する箇所では、強いガードレールを設ける 曖昧さを許容できる工程のみ、LLMの生成能力を活かす このように役割分担を整理したことで、LLMにすべてを任せる構成から、壊れにくいパイプラインへと移行できました。 運用を見据えたガードレール設計 最終目標(ユーザーニーズを満たすコンテンツ制作の工数をできるだけ減らすこと)自体は当初から変わっていません。一方でビジネス側には、まず記事を公開し、ランキングの変化を見ながら改善したいという目的がありました。そのためには、改善サイクルを回せるだけの安定した記事生成が前提になりますが、創造性を高める工夫を優先してしまったことが手戻りの原因でした。 今後の取り組みと展望 今回生成している記事は、文章の整合性や、検索キーワードに紐づくレシピになっているかという点では、一定の信頼性を確保できています。一方で、訪問ユーザーのニーズを満たすコンテンツとしては、まだ伸びしろがあると感じています。 一例ですが、以下を検討しています。 Self-Reflection によるLLMの自動改善サイクルの導入 幅広い検索キーワードに対応するルーティングの追加 ベクトル検索の再利用とレシピのスコアリング レシピページ以外のアセット活用(記事、特集、カテゴリページ) トレンドや訪問ユーザーのログを活用した記事の再生成 これらの改善により、更に品質の高い記事生成を目指していきます。 参考文献 Precise Zero-Shot Dense Retrieval without Relevance Labels Self-Reflection in LLM Agents: Effects on Problem-Solving Performance
アバター
この記事は every Tech Blog Advent Calendar 2025 の 24日目の記事です。 はじめに Swift 5.9で導入された Observation フレームワークは、 @Observable マクロを用いた簡潔な記述が可能で、特にSwiftUIのView更新において高いパフォーマンスを発揮します。 一方で、既存の Combine フレームワーク( ObservableObject )からの移行を検討する際、課題となる点がありました。それは ViewModel や Service など、 UI以外の場所での値の監視 です。Combine では @Published プロパティのProjected Valueを用いて値の変化をストリームとして扱えますが、それと同等の標準的な手段がこれまでの Observation フレームワークには不足していました。 Swift 6.2では、この点が解消されています。 @Observable クラスのプロパティの変化を AsyncSequence として監視する機能( Observations )が追加され、Combine に依存することなく、標準APIのみで値の監視が可能になりました。 本記事では、この新しい監視手法について整理します。 ObservableObject による値の監視 Observation フレームワークが登場する以前、SwiftUI では ObservableObject と Combine を用いた状態管理が一般的でした。 @Published プロパティの projected value( $ )を利用することで、値の変化をストリームとして扱える点は大きな利点でした。 import Combine class CounterViewModel : ObservableObject { @Published var count : Int = 0 private var cancellables = Set < AnyCancellable > () } let viewModel = CounterViewModel() // $をつけることで Publisher として扱える viewModel. $count .sink { value in print( "count changed:" , value) } .store( in : & viewModel.cancellables) この仕組みにより、View の更新だけでなく、ViewModel や Service など UI 以外のレイヤーでも、値の変化を一貫した方法で監視できていました。この点は、ObservableObject が持つ強みの一つです。 Observation フレームワークと withObservationTracking Swift 5.9 で Observation フレームワークが導入されると、View 更新のパフォーマンスと記述の簡潔さは大きく改善されました。一方で、View 以外で値の変化を検知する方法として利用されているのが withObservationTracking です。 import Observation @Observable class Counter { var value : Int = 0 } let counter = Counter() func observe () { withObservationTracking { // 1. ここで値を読む(アクセスする)ことで監視対象にする print( "Current value: \( counter.value ) " ) } onChange : { // 2. 変更前(willSetタイミング)に呼ばれる print( "Value will change..." ) Task { // 3. 再帰的に監視を続ける observe() } } } withObservationTracking は、クロージャ内で読み取られたプロパティへの依存関係を自動的に追跡し、それらが変更された際に onChange を呼び出します。これは SwiftUI の View 更新を支える中核的な仕組みです。 withObservationTracking の課題 しかし、この方法をロジック層で使うには、いくつかの問題がありました。 値そのものを直接受け取れない onChange で通知されるのは「変更される」という事実のみで、変更後の値は再度読み取る必要があります。 再登録が必要 変更を継続的に監視するためには、onChange の中で再び withObservationTracking を呼び出す再帰的な実装が必要です。 非同期処理との相性 withObservationTracking 自体は同期的であり、Callbackベースの記述になるため、async/await のフローと組み合わせる際に直感的な記述が難しくなります。 Swift 6.2: AsyncSequence による監視の導入 Swift 6.2 では、こうした課題を解消する形で、 @Observable クラスのプロパティの変化を AsyncSequence として監視できる Observations 型が追加されました。 import Observation @Observable class Counter { var value : Int = 0 } let counter = Counter() // 監視対象を定義 let counterChanges = Observations { counter.value } Task { for await value in counterChanges { print( "value changed:" , value) } } withObservationTracking が持っていた課題は解消され、簡潔な記述で安全に値の変化を監視できるようになりました。 Combine フレームワークに依存せず、自然な形でSwiftの並行処理モデルに直接統合されています。 まとめ Swift 6.2 の Observations 導入により、ObservableObject が持っていた「Combine による値の監視」という優位性は解消されました。 ただし、この機能を利用するためには iOS 26以降の環境が必要となるため、サポートOSの要件によっては、すぐにプロダクションコードで全面的に採用することは難しいかもしれません。 とはいえ、「View 以外での監視」という最大の課題に対する標準的な解法が示された意義は大きく、安心して Observation への移行を進められる環境が整ったと思います。
アバター
Goエンジニアになって半年経ったので振り返る この記事は every Tech Blog Advent Calendar 2025 の 23 日目の記事です。 はじめに こんにちは!デリッシュキッチンで主にバックエンドの開発を担当している秋山です。 私は今年の6月にエブリーへバックエンドエンジニアとして中途入社し、そこから実務でGo言語を使い始めました。 それまでは約3年間主にRuby on Railsを触っていました。 この記事では、Goに転向して半年経った今感じていることや学びを振り返りたいと思います。 Goを使い始めて感じたこと 他の言語から来ると最初は戸惑う部分もありますが、使っていくとGoの良さが見えてきます。 ここでは、他言語から移ってきた人の視点で感じたことを書きます。 Goバージョン間の後方互換性が嬉しい 2012年にGoの1系が出てから毎年2回メジャーアップデートされますが、Goでは後方互換性の維持を考慮されています。 そのため、比較的容易にGoのバージョンアップを行うことができます。 業務の中でGoのバージョンアップを行わなければならないことがありました。 Goに触れる前は「メジャーアップデートに破壊的変更はつきもの」だと勝手に認識していたので、Goのバージョンアップの容易さに驚かされました。 後方互換性が保たれているおかげで「アップデートしたら動かなくなるかも」という不安が少なく、安心して最新バージョンを追えるのが良いポイントだと思います。 エラーハンドリングに違和感があった Goで書かれたアプリケーションコードを初めて見た時の話です。 try/catch(rubyの場合はbegin/rescueですが)のエラーハンドリングに慣れていたこともあり、下記のようにnilチェックを行うエラーハンドリングに当時違和感がありました。 x, err := call() if err != nil { return err } ちなみに、rubyでは通常このようにエラーハンドリングが行われます。 begin x = call() rescue => e # call()で例外が発生した時にここの処理が実行される 今ではGoの書き方に慣れてきまして、エラーを値として扱うことで明示的に処理フローを追いやすくて良いなと思います。 GoのFAQが便利 他にもRuby/RailsからGoに移った時に クラスってないの? 継承ってないの? などのような疑問が出ましたが、下記のFAQに回答がありました。 go.dev このFAQを読んだだけでもGoの思想に対する理解を1歩進められそうです。 FAQ以外にもgithub上には議論が白熱しているissueやdiscussionもあるので、Goの機能追加の背景なども知れてより理解が深められそうです。 エラーハンドリングについても議論が白熱したみたいです。 github.com go.dev go.dev AI活用で転向のハードルは下がっている? AIにコードを書いてもらう機会が増えたり言語理解にAIを使用することができるようになり、新しい言語を学ぶハードルは確実に下がっていると思います。 ただ、Goを始める前「AIにコードを書いてもらうから転向もすんなりいけるかな」のように思っていた節がありましたが、実際にはそんなことはありませんでした。 日々の業務ではCursorを使わせてもらっており、Goの知識が少なかった私にとってはCursorは非常に強力なツールです。 しかし、AIが書いたアプリケーションコードを人間が全く確認せずにそのままリリースすることは現時点でリスクが高いので、AIが出力したコードをまずは自分でレビューします。 この時結局Goの知識がないとAIコードのレビューに時間がかかってしまいます。 また、チーム内でのレビューもGoの理解なしには難しいところがあるので、結局はGoの理解が大事だなと思いました。 Goエンジニアの成長を促進するエブリーの環境 Goエンジニアへの転向において、会社の環境も大きな助けになりました。 外部イベントへの積極的な参加 エブリーでは外部イベントの積極的な参加や登壇を推奨しており、実際に登壇を行っているメンバーもいます。 私自身も今年の9月に行われた Go Conference 2025 に参加しました! ちなみに、エブリーは今年もGo ConferenceにプラチナGoルドスポンサーとして協賛しました。 Go Conferenceの様子は下記ブログをご覧ください。 tech.every.tv 会社としてこのようなイベントに積極的に協賛しているのは私自身モチベーションにつながります。 また、つよつよエンジニアの方と話す機会もあり、僕もつよつよに成長したいと感じました。 定期的なGo勉強会 エブリーでは現在 2週に1回の頻度でGoの勉強会 が開催されています。 この勉強会では、持ち回りで担当者がテーマを持ってきて使い方を学ぶだけでなく、なぜそうするのか・どうしたら良さそうかなど参加者それぞれが疑問に思ったことをどんどん深掘りしていくスタイルになっています。 この勉強会に参加することで、日頃の業務で使っているだけでは得られないGoの知識をの取得や理解を深めることができています。 また既存のコードに対しても「本当はこの書き方の方が正しいよね」という気づきも多く、実務のコード品質向上にも繋がっています。 今後も日々邁進 AIが活躍する現在も言語理解や日頃の情報キャッチアップは大事だと思っています。 そのため、GoももちろんですがGoに限らずこれからも技術向上に邁進していきます! 参考 https://github.com/golang/go/issues/32437 https://go.dev/blog/error-syntax https://go.dev/issue/71460 https://tech.every.tv/entry/2025/09/28/195717
アバター
この記事は every Tech Blog Advent Calendar 2025 の 22 日目の記事です。 こんにちは @きょー です! 先日 Go Workshop Conference 2025 IN KOBE に参加してきました。とても楽しかったので記事として皆さんにも共有できればなと思います! 会場の様子 はじめに Go Workshop Conference とは? gwc.gocon.jp 公式の HP にも書いてありますが、聞くだけでなく実際に手を動かすワークショップを中心とした Go 言語のイベントです。ソフトウェアからハードウェアなど幅広いワークショップがあり、Go 言語に関心のある方は楽しめる内容になっていました。 ワークショップは午前と午後に分けられ以下のようなものがありました。(資料は自分が見つけられたものを記載してます。既にアップロードされているもの等ありましたら @きょー まで連絡お願いします!) 午前に開催されたワークショップ 低レベルコンテナランタイム自作講座 ~コンテナ技術の地盤を理解する~ オーガナイザー: @Takuto Nagami 資料: https://gwc2025.logica0419.dev/ Gopher のための「自由な話し合い」ワークショップ オーガナイザー: @chihiro TinyGo Keeb Tour at GWC オーガナイザー: @さご 資料: https://github.com/sago35/keyboards/blob/main/conf2025badge/build/build.md はじめての Go 言語教室 オーガナイザー: @Ryuji Iwata 資料: https://www.docswell.com/s/qt-luigi/ZLVDPP-the-first-go-room-setup-20251213, https://www.docswell.com/s/qt-luigi/5PGDP1-the-first-go-room-intro-20251213, https://www.docswell.com/s/qt-luigi/5X6X7X-the-first-go-room-basic-short-20251213, Go カードゲームで遊ぼう オーガナイザー: @瀬上祐匡 並行処理スピードアップコンテスト オーガナイザー: @kuro 資料: https://github.com/nnnkkk7/go-concurrency-workshop 午後に開催されたワークショップ 動かして理解する適材適所のプロファイリング オーガナイザー: @task4233 資料: https://gwc-profiling.vercel.app/docs/01_workshop/ Gopher のためのチームビルディングするインプロワークショップ オーガナイザー: @ysaito Gopher くん基板を作って TinyGo で遊ぼう オーガナイザー: @satoken 資料: https://github.com/sat0ken/gopher-board-workshop/blob/main/README.md Go Doc Comments 完全理解ハンズオン オーガナイザー: KOBE.go オーガナイザー @たくてぃん @uji @is_ryo 資料: https://docs.google.com/presentation/d/1mWg8TbfQPbOEjtmh1wmddzEZmWSsDmDA3U_K0sij9LA/edit?slide=id.p#slide=id.p, https://docs.google.com/presentation/d/1toI89vJrF68C4Gfb6XF0BPHnyTTUtOyPujh4mN8p4hc/edit?slide=id.g3952349bba0_0_0#slide=id.g3952349bba0_0_0 Go with AI オーガナイザー: @tenntenn 資料: https://github.com/gohandson/adk-ja はじめての Go 体験!そしてあなたのキーキャップがおしゃれになる! オーガナイザー: @micchie 資料: https://github.com/mi-bear/name-contract-go, https://github.com/mi-bear/keycap-nail-art-workshop/ どれも面白そうなワークショップですよね!自分は TinyGo を触ってみたいという思いから午前は「TinyGo Keeb Tour at GWC」午後は「Gopher くん基板を作って TinyGo で遊ぼう」に参加してきました。 何か選ぶということは何かを選ばないということ、魅力的なワークショップが集まっている中で二つしか選べないというのはとても苦しいものですね... 参加したワークショップ 午前の部、TinyGo Keeb Tour at GWC まず席に着くと視界に映るハンダゴテ。普段の開発では触る機会が全くないためこの時点でハードをいじることへのワクワクと「Go Workshop Conference にきたんだ...!!」という実感が湧いてきて小躍りしそうになったのを覚えています。 以下の写真に写っているものを最初に配られました。 (余談ですが、この Gopher の基板デザインがとにかく可愛くて、TinyGo Conference の基板を再利用しているのもエコでいいなと思いました。) ハンダゴテしやすいように台座(3D プリンターで作られたらしい)も用意していただいたのでそれを組み立てたりすると ↓ のような感じに。 裏側にも Gopher 発見! 実際にハンダ付けしている様子。 これは LED。こんな小さいけどお前、いけるのか、、、? だんだん形になってきた。 ディスプレイ、スピーカーをつけて(さらば真ん中の Gopher) キーボードもつけたら完成!! 実際に動かしてはないですが、ここまで形になるとなんかもう動きそうですね(というか動いてくれ...!!) 今回プログラムを載せる場所は普通のサーバーとは異なり ↑ の写真の左上のマイコンです。 言語は C? C++? いえ、ここは Go Workshop Conference。もちろん Go です。ただ普段使っている Go のバイナリを置こうとするとマイコンのスペック的に容量が足りなかったりするため、より小さいバイナリを作れる TinyGo というコンパイラを使っていきます。 tinygo.org 少し悲しくなりますがここからはハンダゴテは使わないのでさよなら、、、 パソコンと ↓ の README をもとに今まで作ってきた基盤を動かしていきます。 https://github.com/tinygo-keeb/workshop-conf2025badge サンプルプログラムには LED を光らせたり、音を鳴らしたりするものだけでなく、ディスプレイとキーボードを使ってゲームをできるようにするものもありました。 無事自分の基盤でも LED を光らせることができました!感動!!(あの小さかった LED がこんなに発光するのすごい) オーガナイザーの @さご さん、サポートしてくださったスタッフの方々、ハンダゴテをたくさん貸し出ししてくださった @ysaito さんありがとうございました! 昼の部、はじめての Go 体験!そしてあなたのキーキャップがおしゃれになる! お昼ご飯を食べ終え会場に戻ってみると何やら面白そうなものを発見。キーキャップをデコってオシャレにできるらしい 自分も挑戦してみることに 最初にキーキャップのベースとなる色を決めていきます。いろんな色がありワクワクしてきますね。「こんな綺麗にできるのか?」という不安もありましたが一番下の右から二番目の色を選びました。直感です。 塗ったらライトを当てて固めていきます(硬化というらしい) 固まったらデコるために好きな素材を選んでいきます(写真撮るの忘れてしまったのですが、めっっっちゃ色々な素材がありました!) 自分は月、星、キャンディー、そして今の時期話題のクマを選びました。各素材が大きいのでキーキャップからはみ出てしまい載せるのが結構難しかったです。ただそこは運営の方々のサポートもあったのでなんとか思った形に完成させることができました! テーマは「ゆめかわ」。我ながら満足のいく作品ができたと思っています。え、可愛くないですか??? 午前中に作った基盤と合わせるとこんな感じ。ん〜〜〜可愛すぎて押せない!! オーガナイザーの @micchie さん、 @mikichin さんをはじめスタッフの方々ありがとうございました! 午後の部、Gopher くん基板を作って TinyGo で遊ぼう 午後も基盤をいじっていきます! もう基盤が Gopher になっている時点でテンション上がりますね。このほかにも色々な基盤がありました。(写真撮り忘れた...orz) オーガナイザーの satoken さんのブログに新作の武将 Gopher をはじめたくさんの基盤が載っているのでぜひ見にいってみてください! zenn.dev 午前に比べるとハンダ付けも慣れたものでどんどん進めていくことができました。 まずはボタンをつけて ディスプレイやスピーカー、マイコンなども取り付けて完成! TinyGo で LED が光るプログラムをマイコンに置いて動作確認!( 資料: https://github.com/sat0ken/gopher-board-workshop/blob/main/README.md ) 光らず。原因を探っていきます。 ソフトウェアだけでなく、ハードウェアのデバッグもしていく必要があるため怪しいところを一つ一つ潰していきました。 ハンダ付けがしっかりできているか確認 プログラムは問題なくビルドできているか、マイコンに置けているか確認 ブレッドボードは問題ないか、使っているジャンパー線や LED は問題ないか確認 色々調べていくうちにジャンパー線が悪いことがわかり、交換してみると、、、 光った!!! ただ LED が光るだけではありますが、ソフトウェア・ハードウェア全て繋がり思った挙動をしてくれるとめちゃくちゃ嬉しいですね。 何も分からなかったデバッグを一緒に手伝っていただいた方々本当にありがとうございました...! ディスプレイに「私は Gophers!」と表示させてみたり Gopher の画像を表示させてみたりしてたくさん遊ばせていただきました!楽しい! 最終的にここの画像は自分の推し日本酒である写楽を表示させたりして遊んでました。 写真からだとわかりずらいかもですが目の LED を高速でチカチカさせることもでき、その時の Gopher がかなりカオスで面白かったです。 オーガナイザーの @satoken さん、スタッフの方々ありがとうございました! 展示 会場には展示ブースもありました。 Go 製のゲームエンジンと、TinyGo を使ったハンコンを組み合わせた展示 こちらは @のぼのぼ さんの展示です。 Go 製のゲームエンジンで開発されたレースゲームを、ハンドルコントローラーで操作できるようにする内部の仕組みを作られたそうです。コントローラーから送る信号も、ゲームから返ってくる信号も双方向でやり取りできるようにしているため、ゲーム内のアクションが手元にダイレクトに伝わってきて臨場感がすごかったです。 実際に運転してみた動画もあるのでよければ見てみてください! https://x.com/Keyl0ve_/status/1999731494458392895?s=20 TinyGo と 3D プリンターなどの展示 こちらは @さご さんの展示です。 TinyGo と 3D プリンターを組み合わせた展示がいろいろ並んでいました。特にキーボード周りが面白く、手前に見える「Go」と表示されているものも自作とのこと。5x5 のキーボードを 4 つ組み合わせた基盤に合わせてプログラムを書く必要があり、こだわりが詰まっていました。 0 次会 懇親会まで時間があったので知り合った方々と 0 次会!こういう交流があるのもカンファレンスならではでいいですよね 懇親会 肝心の懇親会の写真を撮り忘れてしまいました。楽しいとついつい写真撮るの忘れてしまいますよね(他責) Go Workshop Conference の参加者や運営の方々で集まりわいわい楽しく過ごせました!希望者で2次会にいったりもして本当に楽しい1日を過ごすことができました。 終わりに 改めてものづくりって楽しいなと思いました。普段のソフトウェア開発ももちろん楽しいですが、ハードウェアが組み合わさり目に見えてものが出来上がっていく様子は本当に楽しかったです。(小学校の図工の時間を思い出しました。) また、今回はワークショップのみで構成されていたので、座学中心のカンファレンスとは違って「自分の手で体験を作る」感覚がとても強かったです。同じ枠に参加していても完成するものは人それぞれで、自分だけのワークショップカンファレンスを形にしているようで新鮮でした。 TinyGo についても学びが多かったです。Go のソースはそのままにコンパイラを TinyGo に切り替えるだけでバイナリのサイズが小さくなることや、ツールチェーンもほぼ Go と同じ感覚で触れることを学べた点が良かったです。 運営をはじめ、関わっていただいたスタッフの方々、一緒に楽しんでくださった参加者の皆様本当に楽しいカンファレンスをありがとうございました!
アバター
この記事は every Tech Blog Advent Calendar 2025 の 21日目の記事です。 はじめに こんにちは。 開発本部 開発1部 デリッシュリサーチチームでデータエンジニアをしている吉田です。 今回はコンピュートシステムテーブルとDatabricks Genie Research Agentを利用して、Jobのコンピュートリソースの最適化を試みた事例をご紹介します。 背景 これまで、Databricks Jobに割り当てるコンピュートリソースの最適化は、実際の実行メトリクスをUI上で確認しながら手動で調整を行う必要があり、手間のかかる作業でした。 しかし、 system.compute.node_timeline テーブルが追加されたことにより、コンピュートリソースの利用状況に関する詳細なメトリクスをSQLで直接取得できるようになりました。 このテーブルをGenieに連携させることで、メトリクスに基づいた分析が可能になり、Genieを活用して最適なコンピュートリソース構成の提案を受けることができるようになります。 system.compute.node_timelineテーブル system.compute.node_timeline テーブルは、Databricks上のコンピュートリソース(All-Purpose Compute, Jobs Compute等)におけるノードレベルのリソース使用状況を記録するシステムテーブルです。 各ノード(ドライバーおよびワーカー)について、1分粒度でCPU使用率、メモリ使用率、ディスクI/O、ネットワークトラフィックなどの主要なメトリクスが格納されています。 コンピュート システムテーブル リファレンス 主なスキーマは以下の通りです(一部抜粋)。 カラム名 説明 cluster_id クラスターID instance_id インスタンスID node_type ノードタイプ(例: i3.xlarge ) driver ドライバーノードか否か (boolean) cpu_user_percent ユーザーランドでのCPU使用率 (%) mem_used_percent メモリ使用率 (%) start_time / end_time 計測期間(1分間隔) これまでGangliaなどのUIでグラフとして確認していた情報が、SQLで直接クエリ可能なテーブルとして提供されるようになった点が特徴的です。 これにより、特定のJob実行時のリソース使用率の平均やピーク値を集計したり、リソース余剰が常態化しているクラスターを抽出したりといった分析が容易になります。 今回はこのテーブルのデータをGenie(DatabricksのAIアシスタント機能)に参照させることで、人間がグラフを目視で確認する代わりに、AIに最適なリソース構成を提案してもらうフローを構築してみます。 Genie Research Agentとは Databricks Genie Research Agent(以下、Research Agent)は、従来のGenieの機能を拡張し、多段階の推論と仮説検証を用いて複雑なビジネス上の質問に取り組むことができるAIエージェントです。 Genie spacesのResearch Agent 通常のGenieがユーザーの質問に対して単発のSQLクエリを生成・実行して回答するのに対し、Research Agentは以下のような高度なプロセスを実行します: 調査プランの作成: 質問に対する最適なアプローチや検証すべき仮説を立案します。 反復実行: 複数のSQLクエリを実行し、その結果(中間データ)を分析して、必要に応じて次のクエリを調整するループ処理を行います。 包括的なレポート: 最終的に、調査結果のサマリー、根拠となるデータ、可視化グラフ、そして具体的な推奨事項を含む詳細なレポートを生成します。 今回の「Jobのリソースを最適化するにはどうすればよいか?」という問いは、単一のクエリで解決する問題ではなく、「CPUの使用状況はどうだったか?」「メモリに余裕はあるか?」「ボトルネックはどこか?」といった複数の観点での深掘りが必要となるため、Research Agentの強みが活かせます。 Research Agentを利用する 今回、以下のテーブルを登録したGenie Spaceを利用します。 system.lakeflow.jobs Jobの実行履歴 system.billing.usage 課金リソースの使用状況 system.billing.prices SKU価格 system.compute.node_timeline ノードレベルのリソースメトリクス 実際に以下の質問を投げてみます。 〇〇Jobについて、コンピュートリソースを最適化したいです 現在のリソース使用量からドライバーノードとワーカーノードそれぞれに最適なインスタンスタイプを教えて下さい また、ストレージが付属しないインスタンスタイプの場合、最適なEBSボリュームの容量も教えて下さい するとResearch Agentは以下のように、複数のクエリを実行し探索的なデータ分析を行います。 内容をみると、まずはJobの実行IDを特定し、次にその実行におけるクラスターIDを取得、そして node_timeline からCPU/メモリ使用率を集計する、といった多段階の推論が行われていることがわかります。 最終的に以下のような詳細なレポートと提案が出力されました。 レポートによると、対象Jobはメモリ使用率が平均25%程度と低く、CPUリソースも余裕があることが判明しました。 この結果に基づき、Research Agentからは以下のような具体的な構成変更が推奨されました: インスタンスタイプの変更: メモリ最適化インスタンス( r5d.large など)から、より安価な汎用インスタンス( m5d.large )への変更 ワーカー数の削減: オートスケーリングの最大数を8台から4台へ縮小 特筆すべきは、単に「使用率が低い」という指摘にとどまらず、実際のAWSインスタンスタイプ名を挙げて具体的な代替案を提示してくれる点です。 まとめ system.compute.node_timeline の登場により、Databricks上のコンピュートリソースの実際の利用状況が詳細に可視化されるようになりました。 さらに、Genie Research AgentのようなAIエージェントを活用することで、膨大なメトリクスデータの中から「どこを改善すべきか」というインサイトを自動で抽出し、具体的なアクションプランにまで落とし込むことが可能になります。 これまでUIを確認する必要があったリソース調整を、効率的に最適化できるこの手法は、コスト削減と運用効率化の両面で非常に有用であると感じました。
アバター
この記事は every Tech Blog Advent Calendar 2025 の 20 日目の記事です。 はじめに こんにちは、リテールハブ開発部の杉森です。 小売向けサービスのインフラ基盤を管理している中で、マルチテナント対応を行うことになりました。 本記事では、既存のTerraformコードをマルチテナント対応させた際の取り組みと、意識したポイントについて紹介します。 実施概要 変更前の構成 既存のTerraformの構成は以下のような形になっていました。 この構成では、新しいテナントを追加する際に variable.tf から全てのファイルをテナントごとに複製して作成する必要がありました。 各テナントごとに共通化している部分と独立しているリソースがあり、独立したリソースの追加や共通化しているリソースの変更が困難な状態でした。 変更後の構成 以下のような構成に変更しました。 この変更により、 variables.tf にテナント情報を追記して terraform apply するだけでテナントの追加ができるようになりました。 以下は簡略化したコード例です。 # variables.tf variable "tenants" { default = { tenantA = { ... } tenantB = { ... } } } # main.tf module "s3" { ... tenants = var.tenants } # s3.tf resource "aws_s3_bucket" "tenant_media" { for_each = var.tenants ... } 意識したこと 大幅なTerraformのコード修正をする上で、意識したことを紹介します。 ※マルチテナント対応する上で、各リソースをどのように分離と共通化をしたかという設計部分の内容は記載しておりません。 1. AIエージェントで徹底的に疑問を解消する 入社して間もなかったため、既存のインフラ構成に詳しくありませんでした。また、AWS自体は触ったことがありましたが、Terraformは未経験でした。 そこで「既存のインフラ構成について」と「Terraformについて」の2つについて、気になることがなくなるまでClaude Codeに壁打ちをしました。例えば「この設定は何のためにあるのか」「この書き方はTerraformとして適切か」といった疑問を一つずつ解消していきました。 十分な知識がない状態でいきなりコードを書き始めると、表面的な動作確認だけで終わってしまいがちです。土台や前提を理解することで、自分が書いたコードに責任を持てるようになると考えています。 2. 不必要にリファクタリングをしない コード全体に影響する対応をしていると、「ここも直したい」という改善点が次々と見えてきます。しかし、逐一対応してしまうと本来のゴールから外れ、タスクの完了が後ろ倒しになってしまいます。 そこで今回は、マルチテナント対応の障壁にならないリファクタは実施せず、Asana上にチケットとして起票して後回しにしました。 「ついでに直す」は一見効率的に見えますが、スコープが曖昧になりやすいです。裁量のあるエンジニアであればあるほど曖昧になりやすいため、目の前のタスクに集中し、改善点は別途管理する意識が大切だと考えています。 3. AWS側のリソース変更を最小限にする 今回の対応ではすでに本番稼働しているテナントAと新規で追加するテナントBが存在します。 そのため、可能な限りテナントAのリソース変更やダウンタイムは最小限になるようにしました。 その施策の一つに、 moved ブロックの活用があります。 terraformはデフォルトの挙動として、リソース自身の構成が同一の内容であったとしても、アドレスが変更された場合、破棄と作成が実施されます。 リリースをした際に適切に moved ブロックを実施し、破棄が発生しないようにしました。 また、将来的に移行した内容をgithub上で追跡できるように、 state mv コマンドではなく、 moved ブロックを利用しました。 以下は moved ブロックの例です。 # リソースアドレスの変更を追跡 moved { from = aws_s3_bucket.tenant_media to = aws_s3_bucket.tenant_media [ "tenantA" ] } 参考リンク https://developer.hashicorp.com/terraform/language/modules/develop/refactoring 4. AIコードレビューを活用する 今回の対応では、CodeRabbitとGitHub Copilotを活用してコードレビューを実施しました。 大規模なコード変更では、人間のレビュアーだけでは見落としが発生しやすくなります。AIコードレビューを併用することで、以下のようなメリットがありました。 変更漏れの検出: 大量のファイル変更の中から、意図しない変更や修正漏れを指摘してもらえた ベストプラクティスの提案: Terraformの書き方について、より良い記述方法を提案してもらえた AIによるコードレビューを効果的に活用するためには、些細な指摘や精度の低い指摘であっても無視せず、一つ一つの内容を精査し、意図を汲み取ろうとすうる姿勢が大切だと感じました。 5. 最終確認をしっかりする 大規模な変更では、PRレビューだけでは全体の整合性を見落としやすくなります。特にインフラの変更は、Terraformのコードだけでなく、バックエンドやフロントエンドのコード、リリース手順書など、関連する様々な領域に影響を及ぼすことがあります。 そこでリリース前に、チームメンバー全員で変更内容を横断的に確認する会を設けました。実際に考慮漏れを発見でき、各メンバーの理解も深まったため、時間をかけた価値がありました。 最後に 今回の対応により、新規テナントの追加が variables.tf への追記と terraform apply だけで完結するようになりました。 まだまだ改善の余地がありますが、予定通りリリースでき、運用負荷の軽減を実現できました。 今後もより良い構成を目指して改善を続けていきます。
アバター
はじめに こんにちは。株式会社エブリーの開発1部の村上です。 この記事は every Tech Blog Advent Calendar 2025 の 19日目の記事です。 弊社では各チームでアラートやインシデントの対応をしており、発生から調査までを各エンジニアが自ら行なっています。その調査自体はナレッジが溜まりつつあるものの、この時代であればよりAIを活用して、迅速な調査やサービス復旧ができないのかという疑問は常にありました。 そんな中で、Datadogから 最近GAされたBits AI SRE という機能が気になったので他の機能のトライアル期間中に検証した内容を共有します。 Bits AI SREとは Bits AI SREは、Datadogが2025年12月2日にGAした、システム障害発生時に自律的にアラートを調査し、数分以内に根本原因を特定するAIエージェントです。 特徴 迅速な根本原因特定 明確な根本原因を数分以内に特定・提示 平均修復時間(MTTR)を最大90%短縮するとも言っている 経験豊富なSREレベルの推論能力 複数の仮説を並行して検証 システム全体の膨大なシグナルを数秒で分析 Datadogの広範なデータセットと数千の実際のインシデントを基に学習 対話型のインシデント対応 チャット形式で質問に回答 調査結果を明確に説明し、推奨アクションを提示 期待される効果 従来のインシデント対応では、エンジニアが深夜に大量のログやメトリクスを手動で調査する必要がありましたが、Bits AI SREの導入により以下のことが期待できます。 エンジニアが創造的な活動(顧客価値創出、システム信頼性向上)に集中可能 迅速かつ自信を持ったインシデント解決 オンコール負荷の大幅軽減 使い方 基本的にBits AI SREはmonitor設定のアラートからトリガーできるようになっており、monitor詳細画面から「Investigate With Bits AI SRE」のボタンで該当のアラートを選択することでチャット画面に移り、自動で調査を開始します。 Investigate With Bits AI SREボタンでのメニュー 基本的には何も指示を出さなくてもこれまでの調査履歴や学習したデータをもとに自動で調査してくれますが、対話形式でアラート調査を依頼したり、回答に対して質問を行うこともできます。Bits AI SREの特徴的な機能として、調査の思考過程が完全に透明化されています。チャット画面は以下のような構成になっています 左側: チャット欄 ユーザーとの対話形式でのやり取り 日本語での指示や質問が可能 調査結果の要約と推奨アクションを表示 右側: 思考過程パネル 調査フローの可視化: AIが実行している各ステップがリアルタイムで表示 仮説検証プロセス: 複数の仮説を立て、それぞれを検証していく過程を表示 データソースアクセス: どのメトリクス、ログ、トレースにアクセスしているかを明示 判断根拠の詳細: なぜその結論に至ったかの論理的根拠を段階的に表示 信頼度の表示: 各結論の確信度やさらなる調査の必要性を提示 思考過程パネルは日本語のネイティブサポートにまだなっていないため、思考過程や調査詳細を閲覧する際は英語での表示となります。ただ、技術的な内容が多いため、エンジニアであれば十分理解可能なレベルです。 Bits AI SREのチャット画面 料金形態 公式の 料金ページ には年間契約の場合は1ヶ月20調査あたり625ドルになっていました。これを高いとみるかどうかはプロダクトやチーム状況によっても変わってくると思いますが、調査単位での従量課金なので何でも見てもらうというよりは何を調査してもらうかは人間がある程度判断しないとコストが増えていきそうに思います。 調査シナリオを作って検証してみる では、実際に実務レベルで使うことができる機能でしょうか。 AWS上に構築した検証環境で、2つの典型的な問題シナリオを意図的に作り出し、Bits AI SREの調査能力を検証しました。 ① 攻撃による5xxエラー急増 検証設定 項目 内容 監視対象 ALBの5xxエラー急増アラート 連携データ ALBメトリクス、ALBアクセスログ 再現方法 複数IP・偽装UserAgentによる大量アクセス攻撃 Bits AI SREの調査フロー 「アクセス傾向を分析して、DDoS攻撃の可能性などを分析してください」と調査依頼を出したところ、以下のステップで分析から回答が生成されました。 Step 1: メトリクス分析 アラート期間のアクセス数、新規コネクション数、応答時間を自動分析 リアルタイムで調査内容を可視化し、詳細確認用のリンクも提供 ALBのメトリクス分析 Step 2: ログパターン分析 ALBログから取得可能フィールドを自動特定 ClientIP、User-Agentに着目したアクセスパターン解析 ALBログからパターン分析 Step 3: 調査結果 これらの調査から実際に以下のような一次回答を出してくれました。アウトプットとしては全体で1-2分ほどで事象をよく理解した回答がもらえました。 ALBへのアクセス傾向とDDoS攻撃の可能性を分析しました。 **アクセスパターン分析結果** **リクエスト量の推移:** - アラート発生時(13:29 UTC)前後、2,200〜4,300リクエスト/分で推移 - 13:39以降、リクエストが急激に停止(0リクエスト) - アクティブ接続数は480〜650接続で安定的に推移 **エラー傾向:** - 30分間で合計43,223リクエストのうち、**41,977件(97%)が5xxエラー** - 4xxエラーはわずか13件 - バックエンドレスポンスタイムは1〜5秒で、極端な遅延は見られない - ALBレベルでの拒否接続は0件 **トラフィック送信元の特徴:** 分析したエラーログから以下の特徴が判明: 1. **少数のIPからの集中アクセス** - `xx.xxx.xxx.xx`: 頻繁に出現(最多送信元) - `xx.xx.xxx.xx`: 2番目に頻繁 - `xx.xx.xxx.xx`: 散発的 2. **User-Agentパターン** - 多様なクローラー/ボット: BLEXBot, MJ12bot, Python requests, Java, Wget, PostmanRuntime等 - 正規ブラウザを模倣したUA: Chrome, Firefox, IE等 - **単一IPから複数の異なるUser-Agentを使用** → 典型的な攻撃パターン 3. **リクエストの特徴** - すべてのリクエストがルートパス `/` への GET - 全て同一ホスト宛 - バラエティに富んだポート番号(短時間での接続・切断を繰り返す) **DDoS攻撃の可能性: 高** **根拠:** - ✅ 少数IP(3〜5個程度)からの大量リクエスト - ✅ 単一IPから複数User-Agentを使い分け(偽装) - ✅ 同一パターンのリクエスト(GET / のみ) - ✅ 97%がバックエンド5xxエラー → バックエンドが過負荷で応答できない状態 - ✅ クローラーを装った自動化ツールの使用 **攻撃タイプ:** アプリケーション層DDoS(Layer 7攻撃)の可能性が高い。少数のIPから大量のHTTPリクエストを送信し、バックエンドリソースを枯渇させる手法。 **推奨対策:** 1. 疑わしいIPアドレス(`xx.xx.xxx.xx`, `xx.xx.xxx.xx`, `xx.xx.xxx.xx`)のブロック 2. AWS WAFでのレート制限ルール設定 3. User-Agent検証ルールの追加 4. ALBアクセスログの継続監視とAmazon GuardDutyの有効化 この攻撃により `targetgroup:targetgroup/xxxxxxxx/xxxxxxxx` のバックエンドが過負荷状態になっていると判断されます。 基本的には数値的根拠をベースに、攻撃元IPの特定、User-Agentパターンの分析まで、手動調査で行う項目を過不足なくカバーしています。 対策提案についても、疑わしいIPの具体的なブロックから予防策(AWS WAF、GuardDuty)まで実行可能な内容が提示されており、解決策の検討も一緒に行うことができます。さらにここから疑わしいIP一覧や疑わしいIPアドレスごとのUser-Agent使用パターンを依頼するとそれらを列挙する形で会話を進めることができました。 基本的にはチャットでの回答の方が理解しやすく、不足もないですが右側のパネルでも結論に至った背景を見ることができます。 ② データベース処理によるレイテンシー悪化 検証設定 項目 内容 監視対象 APIレスポンスタイムの悪化 連携データ ALBメトリクス、APMトレース 再現方法 DB接続数制限による接続枯渇状態での負荷テスト Bits AI SREの調査フロー 「レイテンシーが悪化している原因となっているエンドポイントの特定とボトルネックとなっている箇所の詳細分析をしてください」と調査依頼を出したところ、以下のステップで分析から回答が生成されました。 Step 1: エンドポイント特定 APMトレースからレイテンシーの高いエンドポイントを自動特定 /api/* への負荷を正確に検出 エンドポイントの特定 Step 2: ボトルネック分析 最もレイテンシーが高いトレースの詳細分析 SQLクエリやDB接続に関するエラーログを自動検索 ボトルネック分析 Step 3: 調査結果 実際に一次回答ではコネクションの枯渇や接続確立の遅延を問題視している回答となり、期待に近い回答になりました。こちらもレイテンシーの悪化を単なる「遅い」という表現ではなく、具体的な処理時間の内訳まで詳細に分析してくれています。 トレース分析の結果、レイテンシー悪化の原因とボトルネックが特定できました。 **問題のエンドポイント:** - `GET /api/v1/search` - 検索APIエンドポイント **レイテンシー状況:** - 最大レイテンシー: **129ms** (通常時13-16ms) - アラート時のp95レイテンシー: **84-102ms**に急増 - 通常時の約**6-8倍**のレイテンシー **ボトルネックの詳細分析:** 完全なトレース分析により、129msのリクエストにおける処理時間の内訳: 1. **データベース接続確立: 94.3ms** (73%) - `postgres.query` (Connect) - PostgreSQLへの接続処理 - RDSインスタンス: `bits-ai-sre-postgres.cmm4mneym0sd.ap-northeast-1.rds.amazonaws.com` 2. **メインクエリ実行: 18.4ms** (14%) - 複雑なCTE (WITH句) を使用した検索クエリ - ILIKE パターンマッチング、ROW_NUMBER() ウィンドウ関数、複数JOIN 3. **トランザクション開始: 13.5ms** (10%) - `Begin` トランザクション処理 4. **その他の処理: 2.9ms** (3%) - 集計クエリ、Commit処理 **根本原因:** **データベースコネクションプールの枯渇または接続確立の遅延**が主要因です。接続確立に94ms (全体の73%) かかっており、これは明らかに異常です。通常、コネクションプーリングが適切に機能していれば、接続は再利用されるため数ms以下になるはずです。 **推奨される対策:** 1. **コネクションプール設定の見直し** - 最大接続数を増加 - アイドルタイムアウトの調整 - 接続の事前ウォームアップ 2. **SQLクエリの最適化** - 複雑なCTE + ILIKE + ウィンドウ関数の見直し - インデックスの追加 (`name`, `description`, `category`列) - フルテキスト検索エンジン(Elasticsearch等)への移行検討 3. **RDSパフォーマンスの確認** - CPU/メモリ使用率 - ネットワークレイテンシー - スケールアップまたはリードレプリカの追加 今回はアプリケーションのログまで連携できていませんが、アプリケーションのログやSentryのエラーまでもDatadogに連携できればより精度の高い問題の分析を行うことができそうです。 検証結果の所感 応答速度の速さ 今回の検証を通じて、1-2分で深い分析と具体的な対策提案まで完了するBits AI SREの応答速度には驚かされました。従来の手動調査と比較して圧倒的な時間短縮です。 データ集約による精度向上の可能性 Datadogにログを集約していけばいくほど、より広いコンテキストを共有でき、分析精度が向上していく可能性を感じました。今回はALBログとAPMトレースを中心とした検証でしたが、より細かい情報まで漏らさずに連携できれば、さらに精密な問題分析が期待できそうです。 エンジニアの調査業務への影響 人間では数十分かかる調査や、複数のメトリクスを横断して初めてわかるような見つけにくい課題を数分で解決できる可能性が見えてきました。SREがいる組織ではSRE自体のパフォーマンスを向上させるサポーターとしての役割を果たし、インフラ経験が浅いエンジニアにとっても頼れるパートナーのような存在になりえると感じます。 コスト面の課題 一方で以下のコスト面は組織によって負担が大きく、導入効果とコストバランスの慎重な検討が必要そうです。 Datadogへの全ログ集約コスト AI利用コスト(月20調査で625ドル) ただし、AIコストは技術の発展でより安価に提供される可能性もあるでしょう。 終わりに 今回は、Datadogが新しく発表したBits AI SREについてお話ししました。 生成AIによるインパクトはすでにコーディング業務においてはかなり大きいですが、今後こうしたアラート調査などのSRE領域でも大きな変化が起こりそうです。エブリーでは今後もこうした変化を柔軟に取り入れながら、より自社プロダクトの品質担保やさらなるパフォーマンス向上に努めていきたいです。 エブリーでは一緒に働く仲間を募集中です! エンジニアブログをきっかけに少しでも興味も持っていただけたら、まずはカジュアルに面談しましょう!
アバター
この記事は every Tech Blog Advent Calendar 2025 の 18 日目の記事です。 はじめに AgentCoreの全体アーキテクチャ AWS Provider バージョン要件 Gateway の構築 必須パラメータ authorizer_type の選択 protocol_type について Gateway Target の構築 必須パラメータ target_configuration のターゲット種類 tool_schema の定義 credential_provider_configuration SigV4署名によるGateway呼び出し Identity Provider の構築 必須パラメータ APIキーの指定方法 Secrets Manager との連携 エージェントからのAPIキー取得 IAM設計のポイント AgentCore Runtime用ロール 必要なアクション Gateway呼び出し権限 GatewayからLambdaを呼び出す権限 AgentCore Runtime のデプロイ agentcore configure agentcore launch まとめ 参考リンク はじめに こんにちは、開発1部に所属している25卒の江﨑です。 2025年10月に一般提供開始されたAmazon Bedrock AgentCoreは、AIエージェントの構築・運用を支援するマネージドサービスです。エージェントの実行環境(Runtime)、既存APIやLambdaをツールとして呼び出すためのインターフェース(Gateway)、認証管理(Identity)など、エージェント開発に必要な機能がまとめて提供されています。 今回、私が担当するデリッシュリサーチというサービスで分析エージェントを構築する中で、AgentCoreのインフラをTerraformで管理しました。 しかし、AgentCore関連のTerraformリソースは、事例がほとんど見当たりませんでした。 本記事では、実際にAgentCoreをTerraformで構築した経験をもとに、各リソースの設定方法とポイントを解説します。 AgentCoreの全体アーキテクチャ 今回構築したシステムの全体像は以下のとおりです。 アーキテクチャ図 このエージェントは、レシピのレビューコメントを分析し、インサイトを生成します。処理の流れは以下のとおりです。 まず、DynamoDBに保存されたキャッシュを確認します。同じレシピに対する分析結果が24時間以内に存在すれば、それを返却して処理を終了します。 キャッシュがない場合、AgentCore Gateway経由でLambda関数を呼び出し、Athenaからレビューデータを取得します。次に、取得したレビューコメントをOpenAI APIに送信し、カテゴリにグルーピングします。最後に、グルーピング結果をもとに要約文を生成し、DynamoDBにキャッシュして返却します。 Terraformで管理する主なリソースは以下の3つです。 リソース 役割 aws_bedrockagentcore_gateway エージェントからのリクエストを受け付けるGateway aws_bedrockagentcore_gateway_target Gateway配下のターゲット(Lambda等) aws_bedrockagentcore_api_key_credential_provider API Key認証情報の管理 AWS Provider バージョン要件 AgentCore関連リソースを使用するには、AWS Provider 6.17以上が必要です。 terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 6.17" } } } 2025年10月に、Provider 6.17と6.18で、AgentCore関連のリソースが一気に追加されました。 v6.17.0で追加 aws_bedrockagentcore_gateway / aws_bedrockagentcore_gateway_target aws_bedrockagentcore_api_key_credential_provider aws_bedrockagentcore_agent_runtime / aws_bedrockagentcore_agent_runtime_endpoint aws_bedrockagentcore_browser / aws_bedrockagentcore_code_interpreter v6.18.0で追加 aws_bedrockagentcore_memory / aws_bedrockagentcore_memory_strategy aws_bedrockagentcore_oauth2_credential_provider aws_bedrockagentcore_workload_identity / aws_bedrockagentcore_token_vault_cmk これにより、AgentCoreの主要なリソースはTerraformで管理できるようになっています。 Gateway の構築 AgentCore Gatewayは、既存のAPI、Lambda関数、各種サービスをMCP互換のツールに変換して、AIエージェントから呼び出せる統一したインターフェースを提供してくれます。 resource "aws_bedrockagentcore_gateway" "review_analysis" { name = "RecipeReviewAnalysisGateway" description = "Gateway for Recipe Review Analysis Agent" authorizer_type = "AWS_IAM" protocol_type = "MCP" role_arn = aws_iam_role.agentcore_gateway.arn } 必須パラメータ Gatewayの作成には以下の4つのパラメータが必須です。 パラメータ 説明 name Gatewayの名前 authorizer_type 認証方式( AWS_IAM または CUSTOM_JWT ) protocol_type プロトコル(現時点では MCP のみ) role_arn GatewayがAWSサービスにアクセスする際に使用するIAMロールのARN authorizer_type の選択 authorizer_type は認証方式を指定します。 値 説明 AWS_IAM IAM認証(SigV4署名) CUSTOM_JWT カスタムJWT認証(OpenID Connect対応) 今回は AWS_IAM を採用しました。理由は以下のとおりです。 IdentityやCognitoの設定・管理が不要 追加の認証設定が不要でシンプル CUSTOM_JWT を選択した場合は、 authorizer_configuration ブロックでOpenID Connectの設定(discovery_url等)が必要になります。 protocol_type について protocol_type はGatewayが使用するプロトコルを指定します。現時点では MCP (Model Context Protocol)のみがサポートされています。 MCPはエージェントがツールを呼び出す際の標準プロトコルで、ツールスキーマの定義やレスポンス形式が規格化されています。 Gateway Target の構築 Gateway Targetは、Gatewayが呼び出す具体的なツール(Lambda関数など)を定義します。 resource "aws_bedrockagentcore_gateway_target" "recipe_review_fetcher" { gateway_identifier = aws_bedrockagentcore_gateway.review_analysis.gateway_id name = "RecipeReviewFetcher" description = "Fetch recipe reviews via Lambda" target_configuration { mcp { lambda { lambda_arn = data.aws_lambda_function.get_recipe_reviews.arn tool_schema { inline_payload { name = "fetch_reviews" description = "指定されたレシピIDのレビューコメントを取得します" input_schema { type = "object" description = "レビュー取得リクエスト" property { name = "recipe_id" type = "string" description = "レビューを取得するレシピのID" required = true } } } } } } } credential_provider_configuration { gateway_iam_role {} } } 必須パラメータ Gateway Targetの作成には以下の3つのパラメータが必須です。 パラメータ 説明 name Targetの名前 gateway_identifier 親GatewayのID target_configuration ターゲットエンドポイントの設定 target_configuration のターゲット種類 target_configuration の mcp ブロック内で、以下のターゲット種類を選択できます。 種類 説明 lambda Lambda関数をターゲットにする(今回使用) mcp_server 外部のMCPサーバーをターゲットにする open_api_schema OpenAPIスキーマベースでAPIを定義 smithy_model Smithyモデルベースで定義 今回は lambda を使用し、Athenaでレビューデータを取得するLambda関数を呼び出しています。 tool_schema の定義 tool_schema はエージェントがツールを呼び出す際のインターフェースを定義します。 inline_payload で直接定義するか、 s3 でS3上のスキーマファイルを参照できます。 tool_schema { inline_payload { name = "fetch_reviews" description = "指定されたレシピIDのレビューコメントを取得します" input_schema { type = "object" description = "レビュー取得リクエスト" property { name = "recipe_id" type = "string" description = "レビューを取得するレシピのID" required = true } } } } この定義により、エージェントは「fetch_reviews」というツールを認識し、 recipe_id パラメータを渡して呼び出すことができます。 credential_provider_configuration credential_provider_configuration はターゲット呼び出し時の認証方式を指定します。 認証方式 説明 gateway_iam_role GatewayのIAMロールを使用(今回使用) api_key APIキー認証(外部API向け) oauth OAuth認証(外部サービス向け) 今回は gateway_iam_role を使用し、GatewayのIAMロールでLambdaを呼び出しています。 credential_provider_configuration { gateway_iam_role {} } gateway_iam_role を指定すると、GatewayのIAMロールを使用してLambdaを呼び出します。これにより、別途認証情報を管理する必要がなくなります。 SigV4署名によるGateway呼び出し Gatewayの authorizer_type に AWS_IAM を指定した場合、エージェントからGatewayを呼び出す際にSigV4署名が必要です。 以下は、MCPクライアントを使用してSigV4署名付きでGatewayを呼び出す例です。 import boto3 from mcp import ClientSession from mcp_lambda.client.streamable_http_sigv4 import streamablehttp_client_with_sigv4 session = boto3.Session(region_name= "ap-northeast-1" ) credentials = session.get_credentials() async with streamablehttp_client_with_sigv4( url=gateway_url, credentials=credentials, region= "ap-northeast-1" , service= "bedrock-agentcore" , ) as (read, write, _): async with ClientSession(read, write) as mcp_session: await mcp_session.initialize() result = await mcp_session.call_tool( "RecipeReviewFetcher___fetch_reviews" , arguments={ "recipe_id" : recipe_id} ) ポイントは以下のとおりです。 mcp_lambda パッケージの streamablehttp_client_with_sigv4 を使用 service には bedrock-agentcore を指定 ツール名は {TargetName}___{tool_name} の形式(アンダースコア3つ) Identity Provider の構築 AgentCore Identityは、エージェントが外部APIにアクセスする際の認証情報を管理します。今回はOpenAI APIのキーを管理するために aws_bedrockagentcore_api_key_credential_provider を使用しました。 # Secrets ManagerからAPIキーを取得 data "aws_secretsmanager_secret" "openai_api_key" { name = "bedrock-agentcore/openai-api-key" } data "aws_secretsmanager_secret_version" "openai_api_key" { secret_id = data.aws_secretsmanager_secret.openai_api_key.id } # AgentCore Identity ProviderにAPIキーを登録 resource "aws_bedrockagentcore_api_key_credential_provider" "openai" { name = "OpenAIApiKey" api_key_wo = jsondecode (data.aws_secretsmanager_secret_version.openai_api_key.secret_string) [ "OPENAI_API_KEY" ] api_key_wo_version = 1 } 必須パラメータ パラメータ 説明 name Providerの名前(変更するとリソースが再作成される) APIキーの指定方法 APIキーは以下の2つの方法で指定できます。 方法 パラメータ 説明 通常 api_key APIキー値がTerraform plan/stateに表示される Write-Only(推奨) api_key_wo + api_key_wo_version stateファイルに保存されずセキュア 本番環境では api_key_wo の使用を推奨します。 api_key_wo は Write-Onlyの属性です。Terraformの state ファイルには保存されず、セキュリティが確保されます。 api_key_wo_version はキーのバージョン管理に使用します。キーを更新する際はこの値をインクリメントすることで、Terraformに変更を検知させます。 Secrets Manager との連携 API Keyは直接Terraformに記述せず、Secrets Managerから取得しています。事前に以下のようなシークレットを作成しておきます。 aws secretsmanager create-secret \ --name "bedrock-agentcore/openai-api-key" \ --secret-string '{"OPENAI_API_KEY":"sk-..."}' \ --region ap-northeast-1 このリソースを作成すると、AgentCore側でも自動的にSecrets Managerにシークレットが作成され、 api_key_secret_arn 属性で参照できます。 エージェントからのAPIキー取得 登録したAPIキーは、エージェントのコード内で @requires_api_key デコレータを使用して取得できます。 from bedrock_agentcore.identity.auth import requires_api_key IDENTITY_OPENAI_PROVIDER = "OpenAIApiKey" # Terraformで登録したname @ requires_api_key (provider_name=IDENTITY_OPENAI_PROVIDER) async def get_openai_api_key (*, api_key: str ) -> str : """api_key はデコレータにより自動注入される""" return api_key デコレータが自動的にIdentity Providerから認証情報を取得し、 api_key 引数に注入してくれます。これにより、エージェントのコード内でAPIキーを直接扱う必要がなくなります。 IAM設計のポイント AgentCoreを運用するには、適切なIAMロールとポリシーの設計が重要です。 AgentCore Runtime用ロール resource "aws_iam_role" "agentcore_review_analysis" { name = "RecipeReviewAnalysisAgentRole" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { Service = "bedrock-agentcore.amazonaws.com" } Action = "sts:AssumeRole" } ] } ) } 信頼ポリシーで bedrock-agentcore.amazonaws.com を指定することで、AgentCoreサービスがこのロールを引き受けられるようになります。 必要なアクション AgentCore Identity経由でAPI Keyを取得する場合、以下のアクションが必要です。 data "aws_iam_policy_document" "agentcore_identity" { statement { sid = "TokenVault" effect = "Allow" actions = [ "bedrock-agentcore:GetResourceApiKey" ] resources = [ "arn:aws:bedrock-agentcore:ap-northeast-1:$ { var.account_id } :token-vault/*" , "arn:aws:bedrock-agentcore:ap-northeast-1:$ { var.account_id } :workload-identity-directory/*" ] } OAuth2認証を使用する場合は、 bedrock-agentcore:GetResourceOauth2Token も追加します。 Gateway呼び出し権限 エージェントがIAM認証でGatewayを呼び出すには、 bedrock-agentcore:InvokeGateway アクションが必要です。 data "aws_iam_policy_document" "agentcore_invoke_gateway" { statement { sid = "InvokeGateway" effect = "Allow" actions = [ "bedrock-agentcore:InvokeGateway" ] resources = [ "arn:aws:bedrock-agentcore:ap-northeast-1:$ { var.account_id } :gateway/$ { aws_bedrockagentcore_gateway.review_analysis.gateway_id } " ] } } GatewayからLambdaを呼び出す権限 Gateway Targetで credential_provider_configuration に gateway_iam_role を指定した場合、GatewayのIAMロールに lambda:InvokeFunction 権限が必要です。 resource "aws_iam_role_policy" "agentcore_gateway_lambda" { name = "agentcore-gateway-lambda-invoke" role = aws_iam_role.agentcore_gateway.id policy = jsonencode ( { Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "lambda:InvokeFunction" Resource = data.aws_lambda_function.get_recipe_reviews.arn } ] } ) } この権限がないと、GatewayからLambdaを呼び出す際にエラーが発生します。 AgentCore Runtime のデプロイ AgentCore Runtimeについては、Terraformではなく AgentCore CLI を使用してデプロイしました。CLIがビルド・デプロイ・環境設定を自動化してくれるため、Runtimeの管理には CLI の方が適していると判断しました。 agentcore configure まず agentcore configure でエージェントの設定を行います。 オプション 説明 --entrypoint エージェントのエントリーポイントとなるPythonファイル --name エージェント名 --execution-role エージェントが使用するIAMロールのARN --requirements-file 依存ライブラリを記載したファイル --region デプロイ先のリージョン agentcore configure \ --entrypoint ./invoke.py \ --name recipe_review_analysis_agent \ --execution-role arn:aws:iam::<account-id>:role/RecipeReviewAnalysisAgentRole \ --requirements-file ./requirements.txt \ --region ap-northeast-1 --execution-role には、Terraformで作成したIAMロールのARNを指定します。CLIがデプロイ時に自動でこのロールをRuntimeに割り当ててくれます。 agentcore launch 設定完了後、 agentcore launch でAWSにデプロイします。 --env オプションで環境変数を渡すことができます。 agentcore launch \ --env GATEWAY_URL="https://<gateway-name>.gateway.bedrock-agentcore.ap-northeast-1.amazonaws.com/mcp" TerraformでGatewayを作成した際に出力されるURLを、 --env で環境変数としてエージェントに渡しています。 まとめ 本記事では、Amazon Bedrock AgentCoreをTerraformで構築する方法を解説しました。 主なポイントは以下のとおりです。 AWS Provider 6.17以上が必要 aws_bedrockagentcore_gateway でGatewayを作成し、認証方式とプロトコルを指定 aws_bedrockagentcore_gateway_target で呼び出し先(Lambda等)とtool_schemaを定義 aws_bedrockagentcore_api_key_credential_provider で外部API用のAPIキーを管理 IAMロールの信頼ポリシーに bedrock-agentcore.amazonaws.com を指定 RuntimeはAgentCore CLIでデプロイし、Terraformで作成したIAMロールを --execution-role で指定 AgentCoreはまだ新しいサービスであり、ドキュメントや事例が少ない状況です。私たちも試行錯誤しながら構築を進めている段階ですが、本記事が同様の構築を行う方の参考になれば幸いです。 参考リンク Terraform AWS Provider - bedrockagentcore_gateway Terraform AWS Provider - bedrockagentcore_gateway_target Terraform AWS Provider - bedrockagentcore_api_key_credential_provider Amazon Bedrock AgentCore CLI
アバター
この記事は every Tech Blog Advent Calendar 2025 の 17 日目の記事です。 目次 はじめに 最近のAIの動向に関する所感 AI駆動開発を意識したドキュメント運用の観点 何をドキュメンテーションするか どこにドキュメントを保存するか どのようにドキュメントを更新するか どのようにAIに利用させるか トモニテでの運用方針 1. ドキュメント用リポジトリの作成 2. ドキュメントリポジトリへのドキュメントの集約 3. ドキュメントの活用 4. 今後について おわりに はじめに こんにちは。 開発本部開発1部トモニテ開発部所属の庄司( @ktanonymous )です。 本記事では、AI駆動開発を意識したドキュメント運用について、 どのような選択肢があるのか、社内での運用例、自チームでの運用方針などの側面から考えてみたことを簡単にまとめていきたいと思います。 なお、本記事の内容は「正解」ではないので、「それは違うのでは?」「もっと〇〇した方が良いよ」といったご指摘などがありましたら、 ぜひぜひ X(旧Twitter) などで取り上げていただけますと幸いです。 ※ 本記事で触れないこと プロトコルや各種手法の原理 マルチエージェント モデルやツールの比較 最近のAIの動向に関する所感 最近は、AI技術の発展により開発フローにとどまらずドキュメントの作成や運用のあり方も大きく変わってきていると感じています。 少し前までは、ドキュメントの作成・運用とその他の作業はほとんど独立していました。 「必要に応じてドキュメントを作成し、それを参照しながら開発する」という基本的な流れは、 LLMが登場してからもそれほど大きくは変化していなかったように思います。 精度向上に伴いAIを活用するシーンこそ増えていましたが、チャットベースでその都度コンテキストを与えながら、 局所的にサポートしてもらう形が主だったんじゃないかなという印象があります。 しかし、ここ1年くらいで MCP(Model Context Protocol) を筆頭にLLMの機能が目まぐるしい勢いで拡張され、 LLMの機能のツール化やLLM自体のツール化、LLM同士の協調などが可能になってきました。 エディタでのローカル開発ひとつ取っても、コードベースや要件がまとめられたドキュメントへのリンクなどの最低限のコンテキストだけを与えて依頼すれば、 「必要な情報を調べる -> 実装計画を提案する -> 計画に沿って実装する -> 実装結果をまとめる・レビューする」といった一連のフローを自律的に行わせることが可能になっています。 「AI駆動開発(AI-Driven Development)」という言葉も浸透してきているほどAIが担う役割が増えてきており、多くのAIサービスベンダーが「コンテキストエンジニアリング」を重要視しているように、 AIに与えるコンテキストの重要性も増してきています。 AIに与えたコンテキストからより正確な出力を得るうえで、内部の人間しか知り得ないプロジェクトに関する知識を含めることが重要になります。 そういった情報は何かしらのツールを用いて社内ドキュメントとしてまとめられていることが多いかと思います。 それらを踏まえて、何をドキュメンテーションするのか、どこにドキュメントを保存するのか、どのようにドキュメントを更新していくのか、 どのようにドキュメントを利用していくのか、といった観点が重要で難しい問題になるのかなと感じています。 AI駆動開発を意識したドキュメント運用の観点 前述の通り、AIとの共同作業においてはAIに与えるコンテキストが出力結果に大きく影響します。 今回は、AI駆動開発を意識したドキュメント運用を考えるにあたり、以下の4つの観点から整理していきたいと思います。 何をドキュメンテーションするか どこにドキュメントを保存するか どのようにドキュメントを更新するか どのようにAIに利用させるか 何をドキュメンテーションするか ドキュメントを作成するにあたり、何を目的としてどんなドキュメントを残すのかという観点があります。 例えば、ドキュメントの読者が誰かという点について、エンジニアだけを想定するのか他の職種の読者も想定するのか、 もしくはAIが読めれば十分という考え方もあると思います。 ほかにも、何をドキュメントに残したいのか(要件定義書なのか、仕様書なのか、設計書なのか、etc...)ということも考えられます。 ドキュメントに記載すべき内容やその構成は、読者やドキュメントの役割のようなドキュメンテーションをする目的によって変わってくるので、 チーム内での認識の共有が重要になるかと思います。 どこにドキュメントを保存するか そもそも、ドキュメントはどこにどんな構成で置いておくのが良いのかという観点もあります。 保存場所としては、GitHubやNotion、Confluenceなど様々な選択肢が考えられます。 構成についても、1箇所にまとめるのか分散させるのか、どのような単位でネームスペースを区切るのかなど様々な要素があると思います。 ここでは、AIにコンテキストとして渡しやすいか、人が読む際に必要な情報にアクセスしやすいかなどの側面から適した保存場所や構成が考えられると思います。 例えば、AIのコンテキストという観点ではローカルかMCP経由で参照しやすいツールの方が適しているかもしれません。 職種を問わずアクセスのしやすさを重視するのであれば、組織内で利用しているツールにまとめて、 開発などで使う際には別途AIに参照させられるような機構を作ってしまう方が良いかもしれません。 ドキュメントの管理方法については、コードベースごとに対応する内容のドキュメントをまとめるやり方や コードベースとは別にドキュメントを集約するやり方など、そのほか様々な選択肢があるかと思います。 どのようにドキュメントを更新するか ドキュメントをどのように更新していくかという観点も重要なポイントになります。 ドキュメントの更新に関しては、更新の方法やタイミング、誰が更新するのかなどの要素が挙げられます。 例えば、更新の方法については、人が手動で修正する方法やCIなどで自動生成する方法はもちろん、 AIに更新内容を渡してドキュメントを修正させるといったアプローチも採れるようになってきました。 また、更新のタイミングについても、変更が発生するたびに都度更新するのか、定期的に棚卸しをして対応するのか、 あるいは、1度作成したものは魚拓として更新はせずに変更は差分として新しくドキュメントを作成するという考え方もできるかもしれません。 更新する担当者に関しては、変更に携わる人が更新も担当するやり方や別途責任者を決めて更新作業を集約するやり方などが考えられると思います。 ドキュメントのAI活用を考慮する場合、ドキュメントの情報の正確性はAIに与えるコンテキストの質に大きく影響してしまいます。 ドキュメントの鮮度を維持する(最新情報が明確に判別できるようにする)ことが、AI駆動開発を意識する上では重要な要素になると言えるかと思います。 どのようにAIに利用させるか 整備したドキュメントを活用するために、コンテキストや参照情報としてドキュメントの情報をAIに与える方法も考える必要があります。 CursorやClaudeCodeのようなAIエディタを利用してローカルから参照させる方法もありますし、 手元の作業環境とは別に情報を集約するようなAIツールを作ってしまうという方法も考えられます。 最近では、MCPを利用したドキュメント参照が普及してきており、GitHubやAtlassianのMCPで組織内のコードやドキュメントを参照したり、 AWS Knowledge MCPのように外部ドキュメントを参照したりすることが日常的になっているかと思います。 これを踏まえると、MCP経由で情報を参照させるのが手っ取り早いと考えることもできるかもしれません。 Cursor の GitHub MCP でのファイル参照について GitHub MCP には get_file_contents という tool があり、 ファイルパスを指定することで該当ファイルの内容を参照できるようになっています。 しかし、2025年12月10日時点で、 Cursor からこのツールでファイルの内容を参照しようとしても正しくレスポンスを読み取ることができずに失敗してしまうようです 1 。 古いバージョンを利用することで解決したという報告もありますが、コミット履歴やローカルからファイル内容を参照させることも検討する必要があります。 (※ 筆者が Cursor 以外で確認できていないので、Cursor 以外で同様の事象が発生するかは言及を避けさせていただきます) また、単に接続するだけでなく「どのタイミングでどのドキュメントを参照すべきか」をAIに指示することも重要です。 例えば AGENTS.md 2 などのルールファイルに「DB設計については docs/schema を参照すること」のようにドキュメントへの導線をAIのシステムプロンプトやルールに組み込むことで、 適切なタイミングで必要な情報を取得するように教えて効率よく質の高い出力を得やすくなります。 トモニテでの運用方針 トモニテでは、前述の観点も踏まえつつ、以下の状態を目指したドキュメント整備を構想しています。 サービスの構成や施策の背景・仕様が確認できる AIが容易にコンテキストを取得できる 具体的に実践しようとしている内容を簡単に紹介します。 1. ドキュメント用リポジトリの作成 開発作業で自然に連携できるよう、ドキュメントは GitHub リポジトリにマークダウン形式で集約しようと考えています。 弊社ではエンジニアの他にPMも GitHub やAIエディタが利用できるような体制であり、 想定されるドキュメントの基本的な利用者がエンジニアとPMということもあり、GitHubを利用する判断をしました。 また、GitHubのマークダウン形式ではMermaid記法が利用でき、図をテキストベースで管理できるというメリットもあります。 テキストベースで管理できるため、画像ファイルよりもAIが内容を解釈しやすく、修正もしやすいのが良い点です。 なお、仕様の変更などが発生した場合は、その変更に関する担当者が責任を持ってドキュメントに反映するという運用方針でのドキュメント管理を意識しています。 2. ドキュメントリポジトリへのドキュメントの集約 トモニテではインフラ、APIサーバー、プロダクトAのフロントエンド、プロダクトBのフロントエンド、のように複数のリポジトリが存在しています。 (一部モノレポを採用しているケースもありますが、話の本筋は変わらないので個別に言及することはしません) 今回新たに作成したドキュメントリポジトリは、トモニテのドキュメントマスタとしての役割を持たせることを構想しています。 そのため、各施策の前提となるような、サービス全体の構成や各施策の背景・仕様が確認できるようなドキュメントはドキュメントリポジトリに作成し、 各領域にフォーカスした知識は各リポジトリにドキュメントを作成するような構成をイメージしています。 これにより、AIも人も、ドキュメントリポジトリを確認することで全体観を把握しつつ、 特定の領域に関する知識への導線から必要な情報を適切に取得することができるようになると考えています。 トモニテでのドキュメント運用イメージ ドキュメントリポジトリ /documents/ ├── project_a/ │ └── design_docs/ │ ├── overview.md │ ├── current_system.md │ ├── feature_a.md │ ├── feature_b.md │ ├── architecture.md │ ├── api_design.md │ ├── database.md │ ... │ └── README.md ... └── README.md ※ 既存ドキュメントが充実しておらず新しくジョインするメンバーもいたので現在のシステムを1つのドキュメントにまとめました。 APIサーバー /api-server/ ├── .cursor/ │ └── rules/ │ └── docs-context.mdc ├── docs/ │ ... │ └── ai/ ... └── README.md ※ チームメンバー全員が cursor を利用していて、AGENTS.md がまだ普及していないタイミングだったので .cursor/rules/ ディレクトリを作成しました。 ※ ツールごとにルールファイルを作りたくなかったので、 docs/ai/ ディレクトリを参照するようにマスタとなるルールファイルを作成しました。 3. ドキュメントの活用 トモニテでのドキュメントリポジトリの運用は始めたばかりなので、まだまだ内容は不足していますが、 直近のプロジェクトで design docs 3 を作成してリポジトリに保存しています。 プロジェクトを進めるにあたり、design docs を参照することでAIの実装精度が高まっていることも実感できていますし、 実装中に仕様に微修正が入った際に、実装PRを参照してドキュメントを更新させることもできています。 4. 今後について AI駆動開発を見据えたドキュメント運用は、まだまだ対応すべきことも検討すべき課題も多いと感じています。 より本格的に運用を進めていかないと見えてこない課題もあるかと思います。 少しずつ運用を本格化していき、社内の他のチームや社外の事例からも学びながら、 より良いドキュメント運用を目指して改善していきたいと思います。 おわりに 今回の記事では、AI駆動開発を意識したドキュメント運用について幾つかの観点から考えたことを整理してみました。 これまでは人間が読むことを前提に作られていたドキュメントですが、AIのコンテキストとして利用することを意識すると、 保存場所や書き方の最適解も少しずつ変わってくるのかなと感じています。 ドキュメントの運用自体が始まったばかりで、まだまだ試行錯誤が必要な段階ではありますが、 実際の運用を通じて、自分たちのチームに合った形を少しずつ見つけていければと思います。 本記事の内容が、皆さんのチームでのドキュメント運用を考えるきっかけや参考になれば幸いです。 最後まで読んでいただき、ありがとうございました。 cursor fails to read get_file_contents of github official mcp(2025年12月10日閲覧) ↩ AGENTS.md(2025年12月10日閲覧) ↩ Design Docs at Google (2025年12月10日閲覧) ↩
アバター
この記事は every Tech Blog Advent Calendar 2025 の 16 日目の記事です。 はじめに こんにちは、リテールハブ開発部でバックエンドエンジニアをしているホシと申します。 現在、小売アプリ開発で Laravel 11 を利用しながら日々サービス開発に取り組んでいます。 先日、サービスのパフォーマンス改善を目的として、MySQL の SQL チューニングを行う機会がありました。 これまでも EXPLAIN を使って実行計画を確認することが多かったのですが、以前から「EXPLAIN の内容と実際の動作が一致しない」ケースをいくつか経験していました。今回のチューニングでも同じような状況があり、実際に SQL を実行しながら挙動を確かめる必要がありました。 しかし MySQL 8.0 系では、より深い分析が可能な EXPLAIN ANALYZE が導入され、実際の実行内容を踏まえた「リアルな実行計画」を確認できるようになっています。 私自身、SQL チューニングから少し遠ざかっていたこともあり、しっかり活用できていなかったのですが、意外とまだ使っていない方もいるのではと感じました。 そこで本記事では以下を中心にお話できればと思っています。 EXPLAIN / EXPLAIN ANALYZE の違いについて 実行計画の読み方と注意点 推定と実測が大きく乖離するケース 使いどころと避けるべき点 まとめ 1. EXPLAIN / EXPLAIN ANALYZE の今までの経緯 MySQL における実行計画確認は長らく EXPLAIN 一択 でした。 「EXPLAIN」は初期から存在し、5.6 からは更新系も可能に ただし長年「推定計画のみ」で、実際の動作は把握しにくい 行数推定やコスト推定が外れるケースも度々あった その後 MySQL 8.0 系でオプティマイザが大幅に改善され、 MySQL 8.0.18(2019 年)で EXPLAIN ANALYZE が追加 されました。 特徴: 実際にクエリを実行する 実測行数(actual rows) 実行時間(actual time) 実際の JOIN 順 内部処理の詳細 が取得できるようになり、より精細な実行計画解析が可能になりました。 2. MySQL の EXPLAIN / EXPLAIN ANALYZE の違い EXPLAIN と EXPLAIN ANALYZE でもっとも大きな違いは、「実際に SQL を実行するかどうか」にあります。 ■ EXPLAIN(推定) SQL を実行せず、オプティマイザの「推定計画 」を表示 使用インデックス、JOIN 順序、読み込む推定行数などがわかる 統計情報に依存するため、実際と大きく異なる場合がある ■ EXPLAIN ANALYZE(実測) SQL を 実際に実行して 実行計画を取得 実際の行数、実際の実行時間、ループ回数などが表示される 推定と実測のギャップが明確にわかり、ボトルネック特定に非常に有効 ただし、DELETE / UPDATE(INSERT は未対応) の更新系クエリでは、 実際に実行されるため、トランザクション内による実行、ロールバックを行わないとデータに影響が出てしまいます。 また、通常と同様にロックが発生する点にも注意が必要です。 3. 実行計画の読み方 実際の実行計画の読み方を簡単ですがご紹介します。 次のようなシンプルなテーブル JOIN を例にします。 SELECT * FROM orders o JOIN order_items i ON o.id = i.order_id WHERE o.status = ' PAID ' AND i.price > 1000 ; 実行計画は次のようになります。 EXPLAIN 結果 id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE o ALL PRIMARY NULL NULL NULL 3 33.33 Using where 1 SIMPLE i ref order_id order_id 4 retail-app.o.id 1 33.33 Using where 補足 orders (o) テーブルは 全件走査(type = ALL) のため、条件列にインデックスがない状態。 order_items (i) は ref 結合 で、 order_id インデックスを使用。 両方 Using where のため、最終フィルタはクエリ条件で行われている。 さらに JOIN が増えると以下のような多段構造になっていきます。 SQL例 SELECT o.id AS order_id, c.name AS customer_name, i.product_id, i.price FROM orders o JOIN customers c ON o.customer_id = c.id JOIN order_items i ON o.id = i.order_id WHERE o.status = ' PAID ' AND i.price > 1000 ; EXPLAIN 結果(orders → customers → order_items) id select_type table type possible_keys key key_len ref rows filtered Extra 1 SIMPLE o ALL PRIMARY, customer_id NULL NULL NULL 3 33.33 Using where 1 SIMPLE c eq_ref PRIMARY PRIMARY 4 retail-app.o.customer_id 1 100.00 NULL 1 SIMPLE i ref order_id order_id 4 retail-app.o.id 1 33.33 Using where 補足メモ orders(o) : インデックス不使用 → 全件走査(ALL) customers(c) : 主キーで eq_ref → 高速な1件特定 order_items(i) : ref 結合で order_id を利用 orders の絞り込みが弱いと、JOIN 全体の効率が下がりやすい構造になる 多段 JOIN では特にデータ数がテーブルごとに極端に異なっていたりすると推定ミスが起きやすく、EXPLAIN ANALYZE の価値がより高まります。 4. EXPLAIN では見えない「ズレ」の例 極端にはなりますが、以下に2つの例を挙げてみました。 ■ 例 1:統計情報が古い SELECT * FROM users WHERE created_at >= CURDATE(); EXPLAIN(推定) の結果を抜粋 type=range, rows=10 → MySQL は「現在のデータは 10 行くらい」と予測している。 EXPLAIN ANALYZE(実測) -> Filter: (users.created_at >= curdate()) (cost=0.35 rows=10) -> Table scan on users (cost=0.35 rows=1,204,293) rows examined: 1,204,293 actual time=0.001..1200.226 rows=1,204,293 loops=1 → 実際には 120万行を全件スキャンし、1.2 秒も時間がかかっている。 なぜ EXPLAIN では10行と予測していたのに、実際は120万件となり「ズレる」場合があるのか? 統計情報は自動更新されるが、更新タイミングは一定でない 大量 INSERT の直後は「古い推定」のままのことがある 古い統計のままだと「10行程度」など誤判断をしてしまう場合がある ■ 例 2:JOIN 順の推定が不適切 個人的にはこちらの方が特に厄介かなと感じています。 SELECT * FROM products p JOIN product_tags t ON p.id = t.product_id WHERE t.tag = ' SALE ' ; 期待する動作: product_tags から SALE 行のみ抽出 その product_id を使って products を引く しかし EXPLAIN の推定(抜粋): table type key rows Extra p ALL NULL 1200000 Using where t ref tag 10 Using index → products が 120 万行フルスキャンされるプランになっている。 EXPLAIN ANALYZE(実測) -> Nested loop inner join (cost=0.90 rows=10) -> Table scan on p (cost=0.35 rows=1,200,000) rows examined: 1,200,000 actual time=0.004..5500.891 rows=1,200,000 loops=1 -> Filter: (t.tag = 'SALE') (cost=0.55 rows=10) -> Index lookup on t using product_id (product_id=p.id) actual time=0.05..0.10 rows=1 loops=1 → 実際には 5.5 秒かけて 120 万行を読み切っている。 上記は、 p テーブルを120万件フルスキャン p の各行に対して t テーブルを product_id でインデックス検索 見つかった t 行の中で tag = 'SALE' のものだけ採用 「120万行をひとつずつ見て、その都度 t テーブルを1件検索する」構造になっています。 この部分が重くなってしまう原因です。(Nested Loop JOIN) 考えられる原因: product_tags.tag のカーディナリティ推定が外れていた MySQL が「SALE は大量にあるだろう。フィルタにならない」と誤解 そのため、「products を先に読んだほうが速い」という間違った結論になったりする 実際には SALE 件数が少なければ、 product_tags を起点に読む方が圧倒的に速い。 このケースが特に起きた時に一見問題ない SQL のつもりが、「突然クエリが数秒〜十数秒に悪化する」場合もありえます。 統計情報が最新でも起きる可能性があり、これがかなり厄介だったりします。 5. EXPLAIN ANALYZE が教えてくれる「実際の実行計画」 EXPLAIN ANALYZE では以下が明確になります。 ■ 1. どこがボトルネックか actual time / rows / loops により、処理の重い箇所を特定できる。 ■ 2. 推定と実測のズレ 推定 rows と actual rows の差を見ることで、オプティマイザの誤判断を発見できる。 ■ 3. 実際の実行時間 ミリ秒単位でどの処理が時間を使っているか把握できる。 ■ 4. JOIN 順序が適切か loops 値などから、JOIN の選択順が正しいか判断できる。 ■ 5. インデックスが効いているか 行数の多さから、フルスキャンかどうか一目でわかる。 チューニングをする際、これをやれば解決といった方法は明確にないため、 ケースごとにインデックス見直し、結合方法の改善、条件指定の組み替えなどを行いながら解決する必要があります。 EXPLAIN ANALYZE の使用でそれが以前よりも実際の原因がわかりやすくなるのは大きな違いかなと思います。 6. EXPLAIN ANALYZE を本番で使う際の注意点 本番で使用する際には必ず以下を理解しておく必要があります。 更新系の EXPLAIN ANALYZE は特に注意する 先ほども少し触れましたが、更新系の場合、ANALYZE は実際に実行されてしまうので、 トランザクション内で確実に戻せることが確認できた上であれば実施は可能です。 しかし、 クエリ自体は本当に実行される ロックは実際に取られる トリガーが動く可能性がある(環境による) ロックの影響で他の処理が待たされる という点があり、実行には細心の注意が必要です。 特に「ロック中に他処理が待ち状態になるのは本番ではリスク」になります。 また、更新系ではキャンセル時の挙動も考慮する必要があります。 キャンセル時のロールバック、想定外のロックの遅延なども怖い要因かなと思います。 本番で更新系 ANALYZE は特別な理由がない限り実行しない 上記のことから、基本は以下のルールで行うのが良いのかなと思います。 本番で EXPLAIN ANALYZE を使うのは SELECT のみに限定 UPDATE / DELETE (INSERT は未対応)の ANALYZE は 検証環境で行う 重いクエリの ANALYZE は本番で実行しない ただ、本番でしか再現しないケースなどでは、本番で実行して試したいところですが、 上記ルールに従い、リスクを軽減できるようにしていきたいです。 7. ヒント句や FORCE INDEX は最終手段 また、EXPLAIN 機能の話とは少しずれますが、 チューニングを行なう際、想定通りの実行計画になかなかならず、 ヒント句やFORCE INDEX などを使用したいケースがあります。 (使うと解消できる状況) ただ、ヒント句は便利ですが、以下の理由で「最終手段」とした方が良いのかなと個人的には思います。 データ量・分布が変わると逆効果になる場合がある 計画が固定されるため柔軟性が落ちてしまう 長期メンテコストを考えると、逆効果の場合も そのため、まず優先すべきは: 統計情報の更新 正しいインデックス設計 JOIN 条件の見直し SQL の簡潔化 を行った上で、それでも必要であればという意識が必要かなと感じています。 8. まとめ いかがでしたでしょうか。 最後にEXPLAIN ANALYZEについての比較を表にまとめました。 比較項目 EXPLAIN EXPLAIN ANALYZE 実行されるか 実行しない(推定) 実行される(実測) 出力 推定行数・推定コスト 実行行数・実際の時間 統計情報の影響 大きい 小さい 本番への影響 小さい ロックなど注意 更新系クエリ 実行されない 実行→ロールバック 主な用途 計画確認 実挙動の把握 精度 誤差が大きい場合あり 実測で高い精度 推定と実測は大きく異なることがあり、特に JOIN や統計情報が絡むケースでは差が顕著になる 多くのパフォーマンス問題は、統計情報の更新・インデックス改善・JOIN や SQL の見直しで解決可能 ヒント句は強力だが、状況が変わると逆効果になる場合もあるため「最終手段として使った方が良い」 以上、MySQL の実行計画についてのお話でした。 今後はどちらも有効に活用して改善対応を進めていければと思っています。 少しだけでもSQLチューニング作業の参考になれば幸いです。 最後までお読みいただきありがとうございました。
アバター
この記事は every Tech Blog Advent Calendar 2025 の15日目の記事です。 はじめに こんにちは! 開発1部デリッシュキッチンの蜜澤です。 今回はクラスタリングとcos類似度を用いて表記揺れ辞書を作成してみたので、どのように作成したかを紹介させていただきます。 本記事では具体的なコードは記載せず、実際に行った手順の紹介のみになります。 やりたいこと デリッシュキッチンでユーザーが検索したワードの中で意味が同じものをまとめて、同じワードに変換するための辞書を作成します。 ユーザーが実際に検索したワード(sub)と統一した表記(main)が格納された以下のような辞書が今回作成したいものになります。 sub main 竜田揚げ 竜田揚げ 竜田あげ 竜田揚げ たつたあげ 竜田揚げ 課題 一定の検索回数以上だったワード約17000ワードを対象に、表記揺れ辞書の作成を行うため、人力で行うと膨大な時間がかかってしまいます。 生成AIを使うにしても一気に約17000ワードを渡す必要があるため、処理にかなり時間がかかることが予想されます。 今回試した方法 前述の課題を踏まえて、今回は以下のような手順で、クラスタリングとcos類似度を使用して表記揺れ辞書の作成を試みました。 1.対象ワードを全てひらがなに変換 2.ひらがな変換したワードをベクトル化 3.ベクトルに対してクラスタリングを実施 4.クラスタごとに、cos類似度でグループ分け 1.対象ワードを全てひらがなに変換 「竜田揚げ」と「たつたあげ」のように漢字とひらがなの表記は最終的には同じグループにしたいのですが、そのままベクトル化してしまうと、離れてしまい、同じクラスタにすらならない可能性があるため、まずは全ワードにひらがなを振ります。 OpenAI APIを利用して、以下のように元のワードをひらがなに変換したカラムを作成しました。 word word_hira 竜田揚げ たつたあげ 竜田あげ たつたあげ たつたあげ たつたあげ 2.ひらがな変換したワードをベクトル化 1.で変換したひらがなのカラムを、OpenAI APIを利用してベクトル化します。 ベクトル化には既存のパッケージなどを利用しても良いですが、今回は手軽にできるAPI利用にしました。 3.ベクトルに対してクラスタリングを実施 2.で作成したベクトルに対して、総当たりでcos類似度を求めてしまうと計算量が膨大になってしまうので、総当たりの組み合わせ数を減らすために、まずはK-means法でクラスタリングを行います。 K-means法は最初にクラスタ数を決める必要があります。 今回はワードが約17000語あり、表記揺れのパターンは3~4つくらいになることが多いという経験則から、k=5000にしました。 かなり適当な決め方であり、最適なクラスタ数を求めたらもっと少なくなると思います。 しかし、今回はとにかく手軽に行いたい、かつ、クラスタリングはあくまでも大雑把に分けることが目的なので、クラスタ数の最適化は行いませんでした。 「竜田揚げ」が含まれるクラスターは以下のようになりました。 word word_hira cluster_id 竜田揚げ たつたあげ 0 竜田あげ たつたあげ 0 たつたあげ たつたあげ 0 竜田 たつた 0 たつた たつた 0 たつくり たつくり 0 4.クラスタごとに、cos類似度でグループ分け 3.で作成されたクラスタごとにword_hiraのcos類似度行列を作成し、類似度の閾値を超えたものに対して同じgroup_idを振ります。 今回は処理時間のことも考えて、一度グループに割り当てられたワードは処理順が後のワードとの類似度の方が高くても、別のグループに再度割り当てられない設計にしたので、閾値を高く設定して、ミスマッチが減るようにしました。 cos類似度の閾値を0.8、0.9、0.95の場合で試した結果、0.9がちょうど良い結果になりました。 「たつたあげ」「たつた」「たつくり」がそれぞれ別のグループになっているので、理想的と言えます。 word word_hira cluster_id group_id 竜田揚げ たつたあげ 0 1 竜田あげ たつたあげ 0 1 たつたあげ たつたあげ 0 1 竜田 たつた 0 2 たつた たつた 0 2 たつくり たつくり 0 3 閾値を0.8にした場合は「たつた」と「たつたあげ」が同じグループになったり、「ごまだれ」と「ごまだれそうめん」が同じグループになったりしたので、条件が緩すぎました。 閾値を0.95にした場合は「なすとあつあげ」「なすあつあげ」が違うグループになってしまい、同じグループにしたい組み合わせが違うグループになってしまったので条件が厳しすぎました。 実行結果まとめ 今回1.~4.の手順を実行した結果、17354ワードが11886グループに分かれました。 グループごとにwordの中から統一後の表記にするものを決めれば、今回作成したかった表記揺れ辞書が作成できる状態にできました。 1つしかワードがないグループがかなり多い結果となりましたが、検索回数が少ないワードに関しては他に表記揺れパターンがない場合も多々あるので、ある程度納得できました。 最も肝心な精度に関しては、全グループを目視で確認したわけではないので体感にはなってしまいますが、8割程度はあっているものが作成できたと思います。 現状の表記揺れ辞書の運用では、最終的には人が目視で内容を確認する工程を入れているため、叩き台を作ろうくらいの気持ちでの試みだったので、十分な精度かなと思います。 今後の課題 今回のクラスタリングとcos類似度を用いたやり方で多くのワードの表記揺れ辞書の作成はできますが、以下のような対応しきれないパターンもいくつかありました。 意味的に(ほぼ)同じもの 「パスタ」「スパゲッティ」 組み合わせワードの順不同対応 「大根と豚肉」「豚肉と大根」 小文字と大文字 「 きゃべつ」「きやべつ」 濁点 「たつくり」「たづくり」 これらのパターンはクラスタリングの時点で違うグループになってしまうので、別のアプローチを試す必要があります。 特に「パスタ」と「スパゲッティ」を同じグループにするのはかなり大変なのではないかと思っています。 まとめ 本記事ではクラスタリングとcos類似度を用いて、多数のワードの中から表記揺れ辞書を作成する方法を紹介させていただきました。 体感で8割くらいあっている辞書を作成できたものの、目視確認なしで運用できるほどの精度ではなかったので、今後も良いやり方がないかを考えていきたいと思います。 最後まで読んでいただきありがとうございました。 表記揺れの対応に苦しんでいる方の一助になれたら幸いです!
アバター
この記事は every Tech Blog Advent Calendar 2025 の 14 日目の記事です。 はじめに こんにちは。デリッシュキッチン開発部でバックエンドエンジニアをしている鈴木です。 Docker を使ってローカル環境で開発をしている方なら、かつて macOS 上の Docker Desktop でコンテナ内のファイルアクセスが非常に遅いという問題に悩まされた経験があるかもしれません。ホットリロード付きの開発サーバーがファイル変更に反応するのが遅かったり、テストスイートやビルドに時間がかかったりするケースです。 この問題の背景には、 開発の利便性とパフォーマンスのトレードオフ が存在していました。ホスト上のコードをコンテナに共有すれば編集が簡単ですが、macOS 特有のアーキテクチャ 1 により性能が低下します。一方、コンテナ内部にファイルを置けば高速ですが、ホストから直接編集できず開発体験が損なわれます。 本記事では、なぜ macOS でホスト上のコードをコンテナに共有する際に遅延が発生するのかをその仕組みから解説し、Docker Desktop が開発の利便性とパフォーマンスのトレードオフにどう対処してきたか、そして最新の技術 (VirtioFS, Synchronized File Shares) によってどのように両立が可能になったかを紹介します。 1. なぜファイル共有の仕組みが必要なのか Docker コンテナは通常、ホスト OS と分離された独立のファイルシステムを持ちます [1]。開発を行う際には、ホスト上のディレクトリをコンテナ内に共有する仕組みが必要になります。 Bind マウントと Named Volume Docker には、ホストとコンテナ間でファイルを共有する主な方法が2つあります。 Bind マウント は、ホスト上の特定ディレクトリ (例: /Users/suzuki/myapp ) をコンテナ内のパス (例: /app ) に直接共有する機能です [2]。ホストのエディタでファイルを編集すると、その変更がすぐにコンテナに反映されます。この利便性から、 開発中のソースコードの共有に最適 です。 Named Volume は、Docker 自身が管理するストレージ領域です [3]。ホストのファイルシステムから独立しており、Docker が内部で管理します。永続化が必要なデータベースファイルや、頻繁に変更しない依存ライブラリの保存に適しています。 以下の表は、両者の特性を比較したものです [2], [3]。 観点 Bind マウント Named Volume データの実体 ホスト OS 上のディレクトリ Docker 管理領域 性能 (macOS) 遅い (ホスト OS - VM 間の通信が発生) 速い (VM 内で完結) ホストからの編集 可能 (リアルタイム反映) 困難 (docker cp で取り出す必要) ユースケース 開発中のソースコード 永続化が必要なDBや依存ライブラリ 開発ワークフローでは、ホスト上のエディタでコードを書き、それをコンテナ内で即座に実行できることが重要です。そのため、性能面での課題はありますが、 本記事では Bind マウントに焦点を当てます 。 macOS特有の問題 ここで重要なポイントがあります。 Mac 版 Docker Desktop では、Linux コンテナがホスト OS 上で直接動作していない という点です [4], [5]。 Docker Desktop は内部で軽量の Linux 仮想マシン (VM) を動かしており、コンテナはこの VM 上で動作しています [4]。そのため、ホストの macOS ファイルシステムと VM 内部の Linux コンテナとの間でファイル共有のための仲介が必要になります [4]。 Linux ネイティブ環境との違い Linux ホスト上 : コンテナはホストカーネルを共有しており、Bind マウントしてもオーバーヘッドなくカーネル経由で直接ホスト FS にアクセスできる [6] Docker Desktop (macOS) : ホスト OS - VM 間の通信が発生し、これが遅さの原因となる Fig. 1 アーキテクチャ比較 (a) Linux ネイティブ環境 (b) Docker Desktop (macOS) 環境 このように、ファイル共有の仕組みはホストとコンテナ間でファイルをやり取りするために必須ですが、Docker Desktop (macOS) では VM 層が存在するため、ファイルアクセスのたびにホスト OS - VM 間の通信が発生します。この 通信の積み重ねが、性能低下の根本原因 となっています [7]。 2. Docker Desktop のファイル共有の仕組み では、ホスト OS - VM 間の通信を発生させているファイル共有の仕組みとは、具体的にどのようなものなのでしょうか。 Docker Desktop のファイル共有は、ホスト OS と VM 間でファイルシステムを中継する仮想ドライバによって実現されています [8]。 この仕組みは以下の主要なコンポーネントから構成されています。 構成要素 ホスト側ファイルサーバー (osxfs / gRPC-FUSE / virtiofsd など) ホスト(macOS)上で動くプロセス [8] 共有対象ディレクトリの実際のファイル操作を担当する VM 側ドライバ (FUSE クライアント / VirtioFS ドライバ) Linux VM 内で動作するファイルシステムドライバ [8] コンテナからのファイルIO要求を受け付ける 通信チャネル (vsock/Hypervisor 共有メモリ 等) ホストと VM 間でデータをやり取りするためのチャネル Fig. 2 Docker Desktop のファイル共有アーキテクチャ これらのコンポーネントがどのように連携するかを見てみましょう。 コンテナ内のファイル操作は、VM 内のドライバが受け取り、ホスト OS 上の対応するファイルに処理を渡し、結果をコンテナに返すというリモートプロシージャーコール的な処理が行われます [8]。この一連の流れが、すべてのファイルアクセスで繰り返されることが、性能問題の直接的な原因となります。 3. 性能問題の実例と実際に問題になるケース この仕組みが実際の開発現場でどのような問題を引き起こすのか、具体例を通して見ていきましょう。 Node.js開発環境での問題 大量の小さなサイズのファイルを含む Node.js プロジェクトをコンテナで動かすケースで、問題を具体的に見てみましょう。 シナリオ あなたは Mac で React アプリケーションの開発をしています。ソースコードはホスト上にあり、 docker run -v ~/myapp:/usr/src/app ... のように Bind マウントしてコンテナに共有しています。 コンテナ内で npm install を実行すると、node_modules に大量のパッケージ (数万ファイル) がインストールされます。続いて開発サーバーを起動すると、依存関係を解析するために node_modules 以下のファイルを再帰的に読み取ります。 何が起きているか コンテナ内プロセスが発行する大量のシステムコール ( open , read , stat , readdir など) が、一つ一つホストとの間を往復します。 Fig. 3 Bind マウントでのファイルアクセスフロー この往復を依存ファイル数だけ繰り返すため、以下の問題が発生します。 ホスト OS - VM 間の通信遅延が積み重なり、ファイル数に比例して処理時間が増大 [7] メタデータ操作 (stat, readdir) が特にボトルネックになる [7] 結果 : ある空の React アプリ (約 37k の小ファイル) でのベンチマークでは、Linux ネイティブ環境に比べて約 3.5 倍の時間がかかり、従来の gRPC-FUSE 実装では最大 10 倍以上遅くなるケースも確認されている [9] Docker 公式も「モダンな開発ツール (コンパイラやパッケージマネージャ) は何千もの readdir() , stat() , open() を発行する。仮想ファイルシステムではその一つ一つがホストとVMの間を渡らなければならない」と指摘しています [7]。 実際に問題になるケース 上記の Node.js 例に限らず、以下のような状況で、Bind マウントの性能問題が実際に表面化します。 依存ファイルが多いプロジェクトのビルド/起動 : Node.js の node_modules 、Java の Maven 、Python の venv など [7] ホットリロードやファイル監視 : 対象ディレクトリ全体を監視するために繰り返される stat 操作 自動テストの実行 : テストごとに多数のモジュールをインポートする操作 パッケージの依存関係解決・インストール : npm install や composer install など ポイント : 小規模なアプリや数個の大きなファイルを扱う程度であれば、体感できる差は出にくいです。問題は、大量のファイルに対して頻繁にアクセスするケースに集中します [7]。 4. 技術の進化による解決策 第 3 章で見たように、Bind マウントには性能問題がありました。しかし、ホストで快適に編集できるという利便性は開発には不可欠です。このトレードオフに対し、Docker Desktop はどのように対処してきたのでしょうか。実は、段階的な技術改善により、このトレードオフの緩和、そして最終的には解決が実現されてきました。 技術の進化 Legacy osxfs (初期) : Docker for Mac の独自実装。大量ファイルで遅いという問題があった [9] gRPC-FUSE (2020~2022年頃) : プロトコル効率化を図ったが、依然としてボトルネックは残った [9] VirtioFS (近年) : Docker Desktop 4.x で macOS 12.5 以降でデフォルト採用。ほとんどの開発者に十分な性能を提供 [7], [10] Synchronized File Shares (同期共有) (Docker Desktop 4.27+) : 大規模プロジェクト向けに、利便性と性能の完全な両立を実現 [10] この中で、現在利用可能な主要な2つの技術 (VirtioFS と Synchronized File Shares) について、詳しく見ていきましょう。 VirtioFS: ほとんどの開発者に十分な解決策 従来の osxfs や gRPC-FUSE では、ホスト OS - VM 間の通信の実装が非効率でした。各ファイル操作がネットワークプロトコルに似た方式で処理され、大きなオーバーヘッドが発生していました。 VirtioFS は、この通信経路を根本から見直します。仮想化技術の標準規格である virtio に基づいた専用の高速通信チャネルを使用し、仮想マシンとホストが同じ物理マシン上にあるという事実を活かした最適化を実現します [11]。依然として Bind マウントでありホスト OS - VM 間の通信は発生しますが、通信プロトコルとデータパスが大幅に効率化されることで、劇的な性能向上を達成しています。 Fig. 4 VirtioFS による通信経路の最適化 この改善により、ファイルシステム操作の時間が従来の Bind マウントと比較して最大 98% 短縮されました [7]。実際のベンチマークでは、ある PHP プロジェクトで約 4 倍の速度向上 (93.7 s → 25.5 s) が確認されています [12]。Docker 公式も「ほとんどの開発者とプロジェクトにとって優れた初期設定の解決策」と評価しており [10]、VirtioFS は ほとんどの開発者にとって十分な性能 を提供します。ただし、特に大規模なプロジェクトでは、さらなるパフォーマンスが必要になる場合があります [10]。 Synchronized File Shares (同期共有) : 大規模プロジェクト向けの完全な解決策 VirtioFS はほとんどのケースで十分ですが、数千~数百万のファイルを含む大規模プロジェクト (100,000 ファイル以上の大規模リポジトリや、数百 MB ~ 数 GB の総容量のプロジェクト) では、追加のパフォーマンスが必要になります [10]。 VirtioFS で通信効率は大幅に改善されましたが、大規模プロジェクトでは依然としてホスト OS - VM 間の通信そのものがボトルネックになります。数万回、数十万回のファイル操作が発生する場合、いくら通信を効率化しても、その回数の多さが性能に影響するのです。 Synchronized File Shares は、この問題に対して根本的に異なるアプローチを取ります。 コンテナのファイルアクセスとホスト OS - VM 間の通信を完全に分離 するのです [10]。具体的には、VM 内に ext4 形式のキャッシュ領域を作成し、ホストのファイルのコピーをここに保持します。コンテナはこの VM 内キャッシュに直接アクセスするため、すべてのファイルシステムコール ( readdir() , stat() , open() / read() / write() / close() ) が Linux カーネルで直接処理され、ホスト OS - VM 間の通信が発生しません。一方、オープンソースのファイル同期技術である Mutagen [13] がバックグラウンドでホストとキャッシュを超低遅延で双方向同期します [10]。コンテナからは VM 内ローカルアクセスとなり、ネイティブ Linux 並みの性能を実現しつつ、ホストでの編集も自動的にコンテナに反映されます。 Fig. 5 Synchronized File Shares のアーキテクチャ その結果、従来の Bind マウントと比較して 2 ~ 10 倍の速度向上が実現されました [10]。 ただし、ファイルを二重に保持するためディスク容量を余分に消費します [10]。また、同期は非同期で行われるため、ホストで編集してからコンテナに反映されるまで若干のラグが発生する可能性がありますが、通常の開発では問題にならない程度です。なお、この機能は有料アカウント (Docker Pro、Team、または Business) が必要です [10]。 Synchronized File Shares (同期共有) は、 大規模プロジェクトにおいて利便性と性能の両立を実現 します。VirtioFS で十分な性能が得られない場合の解決策となります。 まとめ macOS 上の Docker Desktop では、コンテナが VM 上で動作するというアーキテクチャ上の制約により、ファイル共有に特有の性能問題が発生します。開発にはホストで編集できる Bind マウントが適していますが、macOS ではホスト OS - VM 間の通信が頻繁に発生し、大量の小さなファイル操作が必要な開発環境では、この通信コストが積み重なって性能が大幅に低下します。 この問題の背景には、 利便性とパフォーマンスのトレードオフ が存在していました。Bind マウントはホストから直接編集できるため開発体験が優れていますが、ホスト OS - VM 間の通信により性能が犠牲になります。一方、Named Volume は VM 内部で完結するため高速ですが、ホストから直接編集できないという不便さがありました。 Docker Desktop はこの問題に対し、段階的に解決策を提供してきました。 VirtioFS は、ホスト OS - VM 間の通信の効率を大幅に改善し、ほとんどの開発者にとって十分な性能を実現しました。さらに、 Synchronized File Shares (同期共有) は、VM 内に ext4 キャッシュを配置してコンテナのアクセスを高速化しつつ、バックグラウンドでホストと同期することで、大規模プロジェクトにおいても利便性と性能の両立を可能にしました。 このアーキテクチャの変更により、利便性と性能のトレードオフが解消され、開発者はホストでの快適な編集とネイティブ Linux 並みの性能を同時に享受できるようになりました。 Appendix: FUSE による追加のオーバーヘッド 本文ではホスト OS - VM 間の通信を主なボトルネックとして説明しましたが、初期の Docker Desktop 実装 (osxfs/gRPC-FUSE) では、さらに FUSE によるオーバーヘッド も存在していました。 FUSEとは FUSE (Filesystem in Userspace) は、ファイルシステムの処理をユーザ空間のプロセスで実装できる仕組みです [14]。通常のファイルシステム (ext4 など) はカーネル内で処理が完結するため高速ですが、FUSE では追加の処理が発生します。コンテナ内のアプリケーションがファイル操作を行うと、その要求はまずカーネル内の FUSE モジュールに届きます。FUSE モジュールはこれをユーザ空間のファイルサーバープロセスに転送し、処理結果を再びカーネル経由でアプリケーションに返します。 Fig. 6 FUSE によるファイルアクセスのデータパス この カーネル空間とユーザ空間の間のコンテキストスイッチ が、ホスト OS - VM 間の通信に加えて追加のオーバーヘッドとなっていました [7]。 VirtioFSによる改善 VirtioFS は、より効率的な通信パスを使うことで、この FUSE スタックのオーバーヘッドを大幅に削減しています。これが、VirtioFS で 4 倍程度の性能向上が実現された理由の一つです。 参考文献 [1] Docker Inc., "Manage data in Docker," Docker Documentation. [Online]. Available: https://docs.docker.com/storage/ [2] Docker Inc., "Use bind mounts," Docker Documentation. [Online]. Available: https://docs.docker.com/storage/bind-mounts/ [3] Docker Inc., "Use volumes," Docker Documentation. [Online]. Available: https://docs.docker.com/storage/volumes/ [4] Docker Inc., "Docker Desktop for Mac," Docker Documentation. [Online]. Available: https://docs.docker.com/desktop/mac/ [5] Docker Inc., "Docker Desktop for Windows," Docker Documentation. [Online]. Available: https://docs.docker.com/desktop/windows/ [6] Docker Inc., "Docker storage drivers," Docker Documentation. [Online]. Available: https://docs.docker.com/storage/storagedriver/ [7] Docker Inc., "Speed boost achievement unlocked on Docker Desktop 4.6 for Mac," Docker Blog, Mar. 2022. [Online]. Available: https://www.docker.com/blog/speed-boost-achievement-unlocked-on-docker-desktop-4-6-for-mac/ [8] Docker Inc., "Change your Docker Desktop settings," Docker Documentation. [Online]. Available: https://docs.docker.com/desktop/settings-and-maintenance/settings/ [9] P. Mainardi, "Docker on MacOS is slow and how to fix it," CNCF Blog, Feb. 2023. [Online]. Available: https://www.cncf.io/blog/2023/02/02/docker-on-macos-is-slow-and-how-to-fix-it/ [10] Docker Inc., "Announcing Docker Desktop 4.27: Speed Boost with Synchronized File Shares," Docker Blog, Feb. 2024. [Online]. Available: https://www.docker.com/blog/announcing-synchronized-file-shares/ [11] S. Hajnoczi et al., "Virtio-fs: A shared file system for virtual machines," virtio-fs Project. [Online]. Available: https://virtio-fs.gitlab.io/ [12] J. Geerling, "New Docker for Mac VirtioFS file sync is 4x faster," Jeff Geerling's Blog, Mar. 2022. [Online]. Available: https://www.jeffgeerling.com/blog/2022/new-docker-mac-virtiofs-file-sync-4x-faster [13] Docker Inc., "Mutagen joins Docker," Docker Blog, Jan. 2022. [Online]. Available: https://www.docker.com/blog/mutagen-acquisition/ [14] "Filesystem in Userspace," Linux Kernel Documentation. [Online]. Available: https://www.kernel.org/doc/html/next/filesystems/fuse.html この問題は Windows (WSL2) でも同様に発生します。基本的な仕組みと解決策は Windows にも適用できます。 ↩
アバター