株式会社エブリーのブログ - TECH PLAY

TECH PLAY

株式会社エブリー

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

469

はじめに この度、株式会社エブリーは、2026年9月11日(金)〜13日(日)に開催される「iOSDC Japan 2026」に、ゴールドスポンサーとして協賛することになりました! 弊社はこれまで Go Conference や TSKaigi などに協賛してきましたが、今年は9月頭の DroidKaigi 2026 に続いて、iOSDC Japan にも初めて協賛します。初めての参加ということで、ブースでどんな方々とお話しできるのか、今から楽しみにしています。 本記事では、開催概要と弊社のブース企画、iOSDCチャレンジのトークン紹介、そしてアフターパーティーのご案内をお届けします。 iOSDC Japan 2026 とは? iosdc.jp iOSDC Japan は、iOS 関連技術をコアテーマとしたソフトウェア技術者のためのカンファレンスです。会場でのトークセッションやスポンサーブースに加えてオンライン配信もあり、iOS エンジニアが年に一度集まる大規模なイベントです。 今年の開催概要は以下のとおりです。 開催日時 Day 0: 2026年9月11日(金) Day 1: 2026年9月12日(土) Day 2: 2026年9月13日(日) 開催場所 有明セントラルタワーホール&カンファレンス 開催形態 オフライン / オンライン(ニコニコ生放送) ブース出展日程 2026年9月11日(金)〜9月13日(日)の3日間 ブース企画 弊社のブースでは、次の2つの企画を用意しています。開発本部のメンバーが現地に参加しますので、ぜひお気軽にお立ち寄りください! アンケートボード「iOSアプリ開発、AIにどこまで任せていますか?」 「iOSアプリ開発、AIにどこまで任せていますか?」をテーマにした参加型のアンケートボードを設置します。 実装からレビュー、テストまで、日々の iOS 開発のどこまでを AI に任せているのか、エンジニア歴とあわせて皆さんのリアルな声をお聞かせください。 集計結果は、イベント後の事後レポート記事で公開する予定です。シールを1枚貼るだけで参加できますので、セッションの合間にぜひ立ち寄ってみてください! 新しくなったデリッシュAI をブースで触れます! また、iOS版でリリースしたばかりのデリッシュキッチンの新しい「デリッシュAI」を、実機でデモ展示します! 新しいデリッシュAI は、チャットで相談するとレシピを提案してくれる機能です。たとえば「子供に人気のレシピ教えて」と聞くと、まず「ハンバーグ / カレー / から揚げ …」と選択肢で聞き返してくれて、「ハンバーグ」を選ぶと、子供向けのハンバーグレシピをすぐに提案してくれます。その下には「もっと簡単」「野菜入り」のような次の絞り込みや、「こんな質問もできます」の候補が並ぶので、文字を打たなくてもタップだけで会話が進みます! 技術的には、サーバー側の AI エージェントが「どの UI をどう並べるか」まで生成し、iOS アプリがそれを SwiftUI で描画する、いわゆる Generative UI の作りになっています。 「LLM に UI を任せてデザインは崩れないの?」「サーバーが返した UI を SwiftUI でどう描画してるの?」と気になった方は、ぜひブースへ! 実機を触りながら、iOS エンジニアが設計の中身までお話しします。 デリッシュAI は最新版のアプリでご利用いただけます。iOSDC までにぜひ一度、「今日の夕飯、何がいいかな」と聞いてみてください! iOSDCチャレンジのトークン紹介 iOSDC Japan では、会場をはじめ様々なところに散りばめられた「iOSDCトークン」を探し、見つけた数に応じて抽選券を獲得できる「iOSDCチャレンジ」が開催されます。集めた抽選券は、会場の抽選カウンターでノベルティが当たる抽選に使えます。 弊社もこの企画に参加しており、トークンは全部で3つ用意しました。トークンは「#」から始まるスペースを含まない文字列です。この記事ではそのうち2つを見出しとして掲載し、残りの1つはブースで公開します。 #AIファースト・カンパニー 弊社は「AI ファースト・カンパニー」を掲げ、AI を前提に組織や仕事の進め方を組み替えている最中です。 Claude の全社導入で個人の AI 活用は一気に進みました。ここからは、そこで生まれたツールやプロンプト、スキルを個人に閉じたままにせず共有資産へ育てる「個人から組織へ」を進めきることと、AI に任せられる範囲が広がるなかで人間の役割を意思決定と設計へ寄せていくことが、次のテーマです。 #誰でも簡単においしく作れる デリッシュキッチンが大切にしているのは、「誰でも簡単においしく作れる」レシピ動画を届けることです。 料理が得意な人だけでなく、料理に苦手意識のある人、忙しい日の夕飯に悩んでいる人、料理を始めたばかりの人にも「今日はこれ作ってみよう」と思ってもらえるように、レシピの選び方から作り方の見せ方まで、サービスの体験全体をこの言葉を軸に作り込んでいます。 残りの1つはブースで 3つ目のトークンは、弊社のブースに掲示しています。 アンケートボードやデリッシュAI の展示を見に来ていただいたついでに、ぜひ探してみてください。 アフターパーティーのご案内 DroidKaigi 2026 の協賛記事でもご案内しましたが、ゆめみ、フェンリル、Yappli、WealthNavi、セーフィー、エブリーの6社合同で「DroidKaigi & iOSDC After Talks Night 2026」を開催します! DroidKaigi 2026 と iOSDC Japan 2026 の合同アフターパーティーですので、iOS エンジニアと Android エンジニアが同じ場に集まり、モバイルエンジニア全体で技術交流や LT などを楽しめる会になる予定です。 開催日時 2026年10月2日(金)19:00〜21:00 開催場所 住友不動産麻布十番ビル アクセンチュア・イノベーション・ハブ 東京 開催形態 オフライン / オンライン コンテンツ ・各社の Android & iOS に関するセッション ・懇親会 詳細・お申し込みは以下からご確認ください。皆さんのご参加をお待ちしています! yumemi.connpass.com おわりに 初めての iOSDC Japan への協賛ということで、チーム一同、当日をとても楽しみにしています。 ブースでは、デリッシュキッチンの iOS 開発の裏側や技術スタック、AI 時代のアプリ開発についてのご質問や雑談も大歓迎です。「トークンを探しに来た」「新しいデリッシュAI の技術的な中身を聞いてみたい」「ちょっとエンジニアと話してみたい」くらいの軽い気持ちで構いませんので、ぜひ弊社のブースに足をお運びください。 有明の会場、そして10月のアフターパーティーで、皆さんとお会いできることを楽しみにしています! 最後までお読みいただき、ありがとうございました!
Go Conference 2026 に 今年はSilver スポンサーとして協賛いたします! はじめに この度、株式会社エブリーは、2026 年 9 月 11 日(金)に開催される「Go Conference 2026」に、Silver スポンサーとして今年も協賛することになりました! Go Conferenceとは? gocon.jp プログラミング言語 ”Go”ユーザーのためのカンファレンスです。今年はハイブリッド開催で、会場でのセッションやワークショップに加え、セッションのオンライン配信も予定されています! 今年の開催概要は以下のとおりです。 開催日時 2026年9月11日(金) 開催場所 東京都中野区中野4丁目10番2号 中野セントラルパーク サウス 1F / B1F 開催形態 オフライン / オンライン(セッションのみ配信) コンテンツ ・基調講演 ・セッション ・ワークショップ ・Official Party(懇親会) 昨年は Go 1.24 / 1.25 の新機能や言語内部を深掘りする発表が中心でしたが、今年はすでにGoコミュニティに参加している方、はじめてGoコミュニティに参加する方、みんなの学びや好きがより遠く深く広がるようなカンファレンスになるよう「 Go Far, Go Together 」というテーマが掲げられています。 また、プロポーザルの審査基準にも「自身の経験、知見に基づく、独自性のある内容であるか」とあるように今年の タイムテーブル を見ると独自性に富んだテーマが多く見られます。 例えば、海上で動くGoサーバー、Go におけるコンソールゲーム開発、9年のOSS保守で見た標準ライブラリとtestingの進化などです。 筆者個人としては、convto さんの「標準パッケージに uuid が追加された背景から見る Go らしい意思決定」が気になっています! イベント当日について エブリーのブースでは、料理に関するクイズや、ノベルティ・キッチングッズなどが当たるくじ引き、アンケートボードを設置しています! デリッシュキッチン 食クイズ ノベルティは弊社CTO慣習のオリジナルドリップコーヒー「CTO Blend」とステッカーをご用意しています! ノベルティ 当日は弊社の Go エンジニアも現地に参加しますので、会場でお見かけの際はぜひお気軽にお声がけください! 非公式アフターイベントのご案内 Go BASH Vol.3 ANDPAD、OPTiM、Resilire、エブリーの4社合同で、非公式アフターイベント Go BASH Vol.3 を開催します! 今回の会場はエブリー本社です! Go Conference 2026 の感想戦や各社のセッションなどのコンテンツを用意していますので、みんなで盛り上がりましょう! 開催日時 2026年9月30日(水) 19:30〜21:00(19:15 開場) 開催場所 東京都港区六本木3-2-1 住友不動産六本木グランドタワー38F 株式会社エブリー本社 開催形態 オフライン コンテンツ ・各社のGoに関するセッション ・Go Conference 2026 感想戦 ・交流会 お申し込みは以下で行っております。みなさんのご参加をお待ちしています! connpass.com 最後までお読みいただき、ありがとうございました!
Codespaces で開発環境を配布する はじめに エブリーでデリッシュキッチンの開発をしている本丸です。 毎年、エブリーでは夏季にインターンシップを行っているのですが、今年は今まで行っていた長期のほかに短期でのインターンシップも行っています。 そこで問題になったのが、参加者の開発環境をどうするかというものでした。 本記事では、GitHub が提供している Codespaces を利用してどのように開発環境を用意するのか、組織で Codespaces を利用する上で設定した項目についてお伝えできればと思います。 GitHub Codespaces とは GitHub Codespaces は、GitHub のリポジトリに対してクラウド上の開発環境を立ち上げ、ブラウザや手元の VS Code から接続して開発できるサービスです。Codespaces は Dev Container の仕組みの上で動いており、リポジトリに devcontainer.json を置いておくと、Codespaces はそれを読んで環境を組み立てます。 devcontainer.json とは何を決めるファイルか devcontainer.json は、「この開発環境はこう作る」という手順をリポジトリに置いておくための設定ファイルです。Codespaces 専用の形式ではなく、同じ定義をローカルの VS Code(Dev Containers 拡張)などで利用することができます。 { "name": "...", "build": { "dockerfile": "Dockerfile" }, "remoteUser": "node", "features": { "ghcr.io/devcontainers/features/github-cli:1": {} }, "postCreateCommand": "pnpm install", "forwardPorts": [3000] } build でベースになるイメージを決め、 features でそこに入っていないツール(ここでは GitHub CLI)を後乗せし、 postCreateCommand で作成後に流すコマンドを書き、 forwardPorts で見せるポートを指定する、という構成です。ほかにも docker compose を丸ごと指定したり、VS Code の拡張機能やエディタ設定を一緒に配ったりと、開発環境まわりのことは一通り設定できます。 起動方法や使い方のポイントなど 使い方 Codespace の作成は、 https://github.com/codespaces を開き、 New codespace から対象のリポジトリを選んで起動するのが基本の流れです。 codespace からの起動 Create codespace のボタンを押した後、数秒から数分待つと Codespace がブラウザで立ち上がり、そのまま開発を始められます。 devcontainer.json さえ置いてあれば、ほとんど設定不要で開発環境を作成できます。 Codespace の停止は、 https://github.com/codespaces から対象の「…」メニュー → Stop で行えます。 ポイント ポート ポートは forwardPorts と portAttributes を使って、ラベル付きで転送しています。 転送されたポート一覧 転送されたポートには、Codespace ごとの URL( https://<codespace 名>-3000.app.github.dev のような形式)が発行されます。たとえば Next.js の開発サーバーを 3000 番で動かしておけば、この URL をブラウザで開くだけで動作確認ができます。 転送されたポートには GitHub の認証がかかっているため、URL を知られただけでは開けません。 また、必要であればポートをローカルに転送することもできます。たとえばローカルの GUI クライアントから DB を見たい場合は、次のコマンドで 3306 番をローカルに持ってこられます。 gh codespace ports forward 3306:3306 Prebuild Codespace は何も設定しないと作成のたびにイメージのビルドから始まります。ビルドに時間のかかるリポジトリでは立ち上がりが遅くなるため、そうした場合は Prebuild を設定しておくと初回起動が速くなります。ビルド済みのイメージをあらかじめ GitHub 側に用意しておき、Codespace の作成時にはそれを取ってくるだけで済ませる仕組みです。 組織アカウントでの運用 最初の設定 組織アカウントに対して、次の設定が必要になります。 まず budget の設定です。 budget の設定 ここで設定した budget を上限として Codespaces を利用していくことになります。 次にアクセス権の設定です。 アクセス権の設定 どのメンバーが Codespaces を利用できるかの設定です。 加えて owner の設定と、必要に応じて policy の設定を行います。 所有権の設定 所有者の設定 ここでは組織所有かユーザー所有かを切り替え、課金対象のユーザーを指定します。 注意しなければならないのは、支出上限に達すると新規作成も起動もできなくなり、稼働中のものも停止されることです。イベント中に全員が同時に止まる事故になり得るため、上限の設定は慎重に行う必要があります。 コストの構造 課金される軸は3つだけですが、止まっていても課金され続けるものがあるのがポイントです。 項目 課金単位 課金されるタイミング compute コア時間(コア数 × 稼働時間) 稼働中のみ (Stop すれば止まる) storage GB-month 停止中も継続 (削除するまで止まらない) prebuild 生成は Actions 分数 / 保管は Codespaces storage 常時 (無効化するまで) 組織ポリシー 組織所有の Codespace に対しては、マシンタイプの制限、アイドルタイムアウトの最大値、保持期間、1ユーザーあたりの上限といった制約を設定できます。 リポジトリ単位のポリシー compute に関しては稼働中のみコストがかかる仕組みになっているので、アイドルタイムアウトを設定することで無駄なコストがかかることを防げます。 なお、ポリシーの項目にユーザー単位の上限を含めると、適用するリポジトリの選択ができなくなります。 ユーザー単位のポリシー 一定以上のスペックが必要な場合やコストを抑える目的などで設定をする場合が多いかと思います。 他メンバーの Codespace 利用の確認 ブラウザ上で見えるのは自分の Codespace だけです。組織オーナーであっても、他メンバーの Codespace を一覧する導線がありません。棚卸しや停止・削除は gh コマンドなどから行うことになります(admin 権限が必要で、対象は組織所有のものだけです)。 # 組織の Codespace を一覧(owner / 状態 / ブランチ / 作成日時) gh codespace list --org ORGANIZATION # 停止(compute 課金を止める。作業内容は保持される) gh codespace stop --org ORGANIZATION --user USER --codespace CODESPACE_NAME state が Available であれば稼働中(compute 課金中)、 Shutdown であれば停止中(storage のみ)です。 CLI から確認した Codespace の状態 その他 Prebuild の設定は組織自体の設定ではなく、個別のリポジトリごとの設定になっているので注意が必要です。 まとめ 組織で利用する場合は、課金の仕組みや所有権まわりで注意しなければならない点があります。とはいえ、 devcontainer.json を置いておくだけで、GitHub アカウントさえあれば誰でも同じ開発環境を作れるのは非常に便利で、環境構築に時間を取られることがありません。 実際、インターンシップ当日も概ね問題なく動きました。こうしたイベントに限らず、普段の開発でも使い道はありそうだと感じました。 参考文献 GitHub Codespaces とは GitHub Codespaces の課金について 組織内の Codespace を一覧する Codespace のマシンタイプ タイムアウト期間を設定する
はじめに 2026年に開催された DroidKaigi 2026 に、弊社の開発本部から 3 名のエンジニアが参加してきましたので、イベントの様子や印象に残ったセッションをご紹介します。 イベントの様子 スポンサーブース エブリーは今回、ゴールドスポンサーとしてブースを出展させていただきました! 足を運んでいただいた皆様、本当にありがとうございました! ブース企画 アンケートボード ブースでは、「AI時代!どこまで越境したいですか?」をテーマにした参加型のアンケートボードを実施しました! Android開発をベースにしつつ、バックエンドやPdM、データサイエンスといった他の領域へどのようにスキルを広げていきたいか、皆様のリアルな声を聞かせていただきました! 回答いただいた多くの皆様、ありがとうございました!最終結果はこちらです……! 2日間でいただいたシールは、合計およそ 400 枚。領域ごとの内訳は次のようになりました。 最も票が集まったのは「Android」でしたが、Backend と iOS がほぼ同数で並び、この3つが上位を分け合う形になりました。一方で全体を見ると、Android 以外の領域に貼られたシールは全体の 8 割弱。「Android を軸に据えつつ、その外側にも手を伸ばしていきたい」という方が多数派でした。 また、シールを貼っていただきながら、こんな声も聞かせていただきました。 モバイル領域が好きなので、クロスプラットフォームでやっていきたい コードは AI が書いてくれるので、プロダクトをどうグロースさせるかを考えられるようになりたい 「AI 時代にどう越境するか」という問いに対して、技術の横方向に広げていく方向と、プロダクトづくりそのものへ踏み込んでいく方向、その両方のリアルな声を伺うことができました。 ※シール数は写真からの集計のため、概算値です。 Xフォロー&くじ引き エブリー開発部の X アカウントをフォローいただくと、くじを1回引けるという企画も実施しました。 景品は、レンジ調理鍋・まな板・計量スプーン・しゃもじ・お箸など、普段の料理で使えるキッチングッズです。 ハズレの方にも、CTO 自らがテイスティングして選んだ「CTO ブレンド」のコーヒーをお渡ししていたので、くじを引いてくださった方には全員何かしらお持ち帰りいただけるようにしていました。 キッチングッズが当たった方に喜んでいただけて、こちらも嬉しかったです! またXをフォローいただいた皆様、本当にありがとうございました!X ではテックブログの更新情報も発信しているので、ぜひチェックしていただけると幸いです! ネイル体験会 会場ではプロのネイリストによるネイル体験会が開催されており体験してきました! 流れとしては、ネイルをする指を2本選び、それぞれのデザインを決めていくというもの。ベースカラーはネイリストの方と相談しながら決められるので、ネイルに詳しくなくても安心して選ぶことができました。 デザインは、DroidKaigi のキャラクター3種類とロゴの中から好きなものをチョイスできました。指先に DroidKaigi のキャラクターがいてくれるので、ふとした拍子に目に入るたび嬉しくなります。 他社のスポンサーブース REALITY さん REALITY さんは、AEP 対応に関するアンケートを行っていました! AEP (Apps Experience Program) は、Google が指定した要件を満たすと認定を受けられ、Google Play の新しい料金表の適用などの特典が得られるプログラムです。 Material3 は対応済み (80% 以上) の回答が多く、予想以上でした。一方でフルコンポーズ化は、まだ対応中・検討中という回答の方が多いようでした。 アーキテクチャも公開されていました。3D アバター以外の箇所はネイティブで作成されているとのことで、Unity を使っていると思っていたので驚きました。 BIZREACH さん BIZREACH さんは、AI が進化して楽になったことについてアンケートを行っていました! テストコードの生成やエラーの原因調査のような、コーディング業務の補佐的な立ち位置に留まらず、相談相手として活用している方が多く面白かったです。 晩ご飯の献立については、他と比べると少ないようでした。 デリッシュキッチン の出番のようです! エムスリー さん エムスリーさんは、毎年恒例の、プログラムのコードが印刷されたクリアファイルを配布されていました。 なんと去年よりコードが短くなっているとのことでした! クリアファイルの詳細については、昨年版のものになりますが エムスリーさんのテックブログ で公開されていますので、ぜひご覧ください! セッション紹介 なんとかする力 〜Androidエンジニアからマネージャー、さらにその先へ?〜 発表者: m.coder さん(フラー株式会社) レポート: 岡田 m.coder さんに、「目の前の課題を『なんとかする』の積み重ねが今の自分を作ってきた」という考えをもとに、キャリアとの向き合い方を語っていただいたセッションでした。 仕事のやりやすさは「何を・いつまでに・どこまでやるか」が決まっているかで大きく変わり、曖昧な箇所を明確にして不確実性を下げること自体が価値ある仕事だという話から始まりました。 印象的だったのは、役職が上がっていくにつれ、皆等しく抽象度の高い課題の解決を求められるというお話です。「なんとなくチームの雰囲気が悪い」「なんかプロジェクトの品質が悪い気がする」といった、課題かどうかすら曖昧なものを扱う必要があるという具体例に痛く納得しました。こういった漠然とした課題については、どうしても目を瞑りがちなので、自身のマインドセットを見直す必要があるなと痛感しました。 またテックリードとマネージャーは向き合い方が違うだけで、どちらも「自分以外の領域(チームや組織)をなんとかする」役割だという整理も面白かったです。 自分のキャリアを考えるうえで、抽象度の高い問題に立ち向かうべきという方針や、それを実現させる方法について非常に学びになりました。ご自身の経験から語られている箇所も多く、熱いメッセージをいただいた気持ちになりました。 また冒頭で『エンジニアリング組織論への招待』を紹介していただきました。1 章だけでも読む価値があるとのことなので、ネクストアクションとしてはこちらを読もうと思います。 あなたのANRはどこから? — 発生する仕組みを診断し、症状別に処方する 発表者: chomi さん(NRIネットコム株式会社) レポート: 岡田 会場が皆うなずいていたセッションだったように思えます。 メインスレッドについての解説を経て、まずは誰しもが経験したことがあるであろう、メインスレッドでの I/O についてのお話から始まりました。その後起動時の重い初期化、ロック競合と進みました。 起動時の重い処理については、特にレガシーコードを触ったことがある人なら対応したことがあるのではないかと思います。本当に Application で初期化すべきかを考えるというのは ANR 以外にも、パフォーマンスの観点から非常に重要です。例として FirebaseSDK の初期化について出ましたが、こちら誰しもがなんとかならないかなと調べたことがあると勝手に思っているので面白かったです。また固有端末依存や Binder 経由の呼び出し先での ANR などについても話があり、やはり皆さん困っているのだなと共感しました。 何より構成と見せ方が完璧だったと思います。スライドは要点だけが目に入る作りで、定義や例などもとても丁寧でしたので、スッと内容が入ってきました。終章の ANR 診断フローチャートについても綺麗にまとめられており、参考になりました。 発表での再現には、公開されているサンプルアプリ DorodoroTimer を用いたそうです。デモモードをONにすると上記の ANR が実際に発生し、コード内の [ANR-xx] マーカーから問題箇所と修正版を見比べられます。 AndroidにおけるServer-Sent Events: 工場の現場を生き抜くリアルタイムストリーム 発表者: Mr. Jasveen Sandral (Industrial Android, Toyota Group Japan) レポート: 鈴木 ( @0muji4_eng ) 本講演は、AndroidにおけるServer-Sent Events(SSE)を用いたリアルタイムストリーミング実装の課題と、その具体的な解決策について論じています。 Webブラウザとは異なり、Androidの標準的なライブラリ(OkHttpなど)にはSSEの自動再接続や状態管理の機能が不足しており、通信障害時にエラーを検知できず画面のデータがフリーズしてしまうエラーケースが存在します。講演者はこの事象を "The Trap of Silence" (沈黙の罠) と呼んでいました。この問題を克服するためには、サーバーに依存するのではなく、クライアント側(Android側)で堅牢な自己回復機能を持つ独自の仕組みを設計する必要性が生じます。 具体的には、サーバーからの定期的な通信(ハートビート)を監視してタイムアウトなどの切断を検知する仕組みや、厳密な状態管理(ステートマシン)の実装が不可欠です。あわせて、再接続時には最後に受信したID(Last-Event-ID)をサーバーへ送信することで、通信断絶中のデータ欠落を補完し、安全にストリーミングを再開する必要があります。 また、頻繁な双方向通信に適したWebSocketとの技術的な比較や、端末がオフラインになった際の適切なUI制御にも触れられています。最終的に、一方向のデータ監視システムにおいてSSEを有効に活用するには、サーバー側でのバッファリングといった設計だけでなく、クライアント側がいかにして通信の切断と復帰に耐えうるアーキテクチャを構築できるかが重要であると結論付けています。 WebAssembly in Android Apps 〜 WASMはJNIの夢を見るか 発表者: keiji_ariyama さん (C-LIS CO., LTD.) レポート: 鈴木 ( @0muji4_eng ) 本講演は、Androidアプリ開発において、C++などで書かれた既存のネイティブライブラリ(OpenJPEG など)を、WebAssembly(Wasm)を用いて安全に再利用するためのアーキテクチャ設計について論じています。 背景として、運転免許証やパスポート、マイナンバーカードなどに格納されている顔写真データ(JPEG 2000形式など)を読み込む際、従来のJNI(Java Native Interface)経由の直接実行では、悪意のある細工された画像データによって深刻な脆弱性を突かれ、アプリ全体が危険にさらされるリスクがありました。 この課題に対する実践的な解決策として、講演者は Wasm と Jetpack JavaScript Engine を組み合わせた多層防御(Defense-in-Depth)を提案しています。Wasmによってシステムコールを持たないメモリ隔離環境(第一層)を構築し、さらにJS Engineによってネットワークやローカルファイルへのアクセス権限を持たない別プロセス(第二層)として実行します。これにより、万が一デコーダーの脆弱性を突かれても、被害をサンドボックス内に完全に封じ込め、アプリ本体への影響を防ぐことが可能になります。 また、実装上の大きな障壁となる プロセス間のデータ転送コスト についても詳細な検証が行われています。文字列変換によるデータ受け渡しでは、プラットフォーム側にネイティブ実装が存在する Base64 を使用するのが最もパフォーマンスが高いことが実証されました。しかし現在では、JS Engine バージョン1.1.0で導入された MessagePort API を活用することで、バイナリデータの双方向通信が可能となり、エンコードのオーバーヘッドが劇的に解消されることが解説されています。あわせて、プロセス間通信の1MB容量制限も、RAM 上のファイルディスクリプターを介することで安全に回避できる点が示されています。 結論として、Wasm はメモリコピーが発生する点(ゼロコピー不可)やコードの隠蔽化に向かない点においてJNIとトレードオフの関係にあります。しかし、外部からの信頼できないデータを処理する要件においては、過去の優れたネイティブ資産を極めて安全にモバイル環境へ持ち込むための、非常に有効なベストプラクティスであると位置づけています。 まとめ 今年は例年と違い、 AI 関連のトピックが増加した印象です! ブースでは AI を用いた開発に関してのアンケートが多数見受けられました! セッションでは デバイス操作はAIエージェントの時代へ。mobile-mcpを活用したAndroid UI/E2Eテストの挑戦 や AI に Inclusive UI を書かせよう — Design Rules Skill で Compose UI を作り直す のような AI を開発効率化に用いる内容から、 Google のオープンモデル Gemma を活用した最新の AI 開発のトレンド のような AI 開発そのものについてまで幅広く講演されており、時代の変化を感じました! またブースには本当に多くの方に足を運んでいただき、たくさんの人にエブリーを知っていただけて、とても良い機会でした! これからもデリッシュキッチン、エブリーのことをよろしくお願いいたします! 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv さらに、 DroidKaigi & iOSDC After Talks Night 2026 を、ゆめみ、フェンリル、Yappli、WealthNavi、セーフィー、エブリーの6社合同で開催いたします!なお、今回はiOSDC Japan 2026のアフターパーティーも兼ねているので、AndroidエンジニアだけでなくiOSエンジニアの方も交えて、プラットフォームの垣根を越えた活発な技術交流や情報交換をお楽しみいただけます。両カンファレンスの熱気をそのままに、各社によるLTセッションや懇親会をご用意しております。 項目 詳細情報 開催日時 2026年10月2日(金) 19:00 ~ 21:00 開催場所 東京都港区三田一丁目4番1号 住友不動産麻布十番ビル 開催形態 オフライン / オンライン コンテンツ 各社のAndroid & iOSに関するセッション / 懇親会 詳細や参加登録につきましては、以下のリンクよりご確認ください。 yumemi.connpass.com 最後までお読みいただき、ありがとうございました!
はじめに 株式会社エブリーは、2026年9月に開催される DroidKaigi 2026 にゴールドスポンサーとして協賛いたします。「エンジニアが主役のAndroidカンファレンス」である本イベントは、2026年9月1日(火)から3日(木)にかけて ベルサール渋谷ガーデン で開催されます。 項目 詳細情報 イベント名称 DroidKaigi 2026 開催日程 2026年9月1日(火)〜 9月3日(木) 会場 ベルサール渋谷ガーデン(東京都渋谷区南平台町) スポンサーランク ゴールドスポンサー ブース出展日程 2026年9月2日(水)〜 9月3日(木)の2日間 エブリーはこれまでも、 Go Conference 2025におけるPlatinum "Go"ld スポンサー や、 TSKaigi 2026におけるゴールドスポンサー としての参加など、技術コミュニティを積極的に応援してきました。今回のDroidKaigiへの協賛も、自社の開発現場で得られた知見をコミュニティに還元し、エンジニアの皆様と共に成長していくための大切な取り組みの一環です。 tech.every.tv tech.every.tv ブース出展:「AI時代!どこまで越境したいですか?」 9月2日および3日に出展するエブリーのブースでは、「AI時代!どこまで越境したいですか?」をテーマにした参加型のアンケートボードを設置します。Android開発をベースにしつつ、バックエンドやPdM、データサイエンスといった他の領域へどのようにスキルを広げていきたいか、皆様のリアルな声をお聞かせください。 また、 エブリーの公式X(旧Twitter) をフォローしていただいた方には、ハズレなしのくじ引きをご用意しています。お鍋や計量スプーン、まな板など、デリッシュキッチンならではの実用的なキッチングッズをプレゼントしますので、ぜひお立ち寄りください。 TSkaigiでもお配りした景品例 事後レポートを公開予定です! イベント終了後の9月3日(木)には、 every Tech Blog にて最速事後レポートを公開する予定です。開発部メンバーによるセッションの感想や、アンケートボード「AI時代の越境」の集計結果など、現場のリアルな熱量をお届けします。過去のイベント協賛時と同様に、オフラインで得られた知見をいち早くコミュニティに共有していきます。 アフターパーティーも開催します! さらに、 DroidKaigi & iOSDC After Talks Night 2026 を、ゆめみ、フェンリル、Yappli、WealthNavi、セーフィー、エブリーの6社合同で開催いたします!なお、今回はiOSDC Japan 2026のアフターパーティーも兼ねているので、AndroidエンジニアだけでなくiOSエンジニアの方も交えて、プラットフォームの垣根を越えた活発な技術交流や情報交換をお楽しみいただけます。両カンファレンスの熱気をそのままに、各社によるLTセッションや懇親会をご用意しております。 項目 詳細情報 開催日時 2026年10月2日(金) 19:00 ~ 21:00 開催場所 東京都港区三田一丁目4番1号 住友不動産麻布十番ビル 開催形態 オフライン / オンライン コンテンツ 各社のAndroid & iOSに関するセッション / 懇親会 詳細や参加登録につきましては、以下のリンクよりご確認ください。 yumemi.connpass.com おわりに 株式会社エブリーでは、技術コミュニティの発展を応援するとともに、開発現場で得た知見や知恵を共有し合う文化を大切にしています。今回の DroidKaigi 2026 への協賛を通じて、多くのエンジニアの皆様と技術やキャリアに関するお話ができることを楽しみにしています。 当日のブースでは、デリッシュキッチンをはじめとするプロダクト開発のリアルな話や技術スタック、AI時代におけるエンジニアの挑戦に関する雑談・ご質問も大歓迎です。「ちょっとノベルティのくじ引きをしてみたい」「エンジニアと軽く話してみたい」といった軽い気持ちで構いませんので、ぜひ気軽にエブリーのブースへ足をお運びください。 DroidKaigi 2026 の会場、そして10月のアフターパーティーで、皆様とお会いできることをチーム一同、心より楽しみにお待ちしております!
はじめに 前提:エージェントの構成 課題:画面を離れると回答が消える 設計方針:実行の作成と購読を分離する DB スキーマ:残す履歴と実行中の状態を分ける 保存単位は「AG-UI の Message 1 件 = 1 行」 DynamoDB ではなく Aurora MySQL を選んだ理由 会話履歴はサーバーが組み立てる 書き込み設計:イベントは保存せず「生成途中の回答」を上書きする 再合流:会話全体のスナップショットで追いつく 同時実行制御:実行中ロックを NULL 可のユニーク列で作る ロックの解放漏れに備える 停止:切断とキャンセルを区別する Redis は要るか まとめ はじめに 開発本部 開発1部の いくまる です。 私たちのチームでは、Web アプリの新機能として、チャット形式でデータを分析できる AI エージェントを開発中です。開発を進める中で、「回答の生成中に画面を離れると、その回答を受け取れなくなり、会話も残らない」という課題に向き合うことになりました。 本記事では、この課題を解決するために行った「会話履歴の永続化」と「バックグラウンド実行」の設計と実装を紹介します。DB スキーマ・実装コード・検討して捨てた案まで含めて書きます。 前提:エージェントの構成 このエージェントは次の構成で動いています。 ブラウザ(チャット UI) │ AG-UI イベント(SSE) ▼ Next.js(API Route ── ブラウザと AgentCore の中継役) │ InvokeAgentRuntime(SSE) ▼ Amazon Bedrock AgentCore Runtime(Strands Agents 製エージェント) │ MCP ▼ MCP サーバー(自社データの検索・集計ツール群) Amazon Bedrock AgentCore : AI エージェントの実行基盤となる AWS のサービスです。セッションごとに microVM 単位で実行環境が分離されます。 Strands Agents : AWS が公開しているオープンソースの AI エージェント SDK です。 AG-UI : エージェントとフロントエンド間のイベントストリーミングのプロトコルです。 RUN_STARTED ・ TEXT_MESSAGE_CONTENT ・ TOOL_CALL_* ・ RUN_FINISHED などのイベント型を定めています。転送方法は SSE に限定されませんが、このアプリでは SSE で流しています。 ユーザーが質問を送ると、エージェントが MCP ツールでデータを取得・分析し、回答をストリーミングで返します。ツールを繰り返し呼ぶため、1 回の回答に数十秒かかることがあります。 課題:画面を離れると回答が消える AI チャットで広く使われるのは「POST + ストリーミング応答」の構成です。ブラウザが質問を POST し、サーバーが生成イベントを流し、画面に逐次表示します。画面離脱を想定しなければ、これで十分に機能します。私たちの初期実装もこの構成でした。 私たちの場合は回答に数十秒かかるため、「生成中に画面を離れても実行は完走してほしい」という要件が加わりました。この要件を満たそうとすると、3 箇所が問題になります。 改修前の構造。画面遷移した瞬間に、以降のイベントを受け取る手段がなくなる 1 つ目はフロントエンドです。 この構成では、チャット画面の hook が unmount 時に実行を中断( abortRun() )する作りになりがちです。画面遷移がそのまま実行中断になります。 // チャット画面の hook(unmount 時に実行を中断する作り) useEffect( () => () => { stopRequestedRef. current = true ; agentRef. current ?.abortRun(); } , [] , ); 2 つ目は中継役の API Route です。 ブラウザと AgentCore の間で SSE を中継する Next.js の API Route は、クライアントの切断を上流の AgentCore への読み取りキャンセルとして伝播します。 ReadableStream の cancel() は「クライアントがもう読まない」ときに呼ばれるコールバックで、そこで上流の読み取りも止めると、切断とキャンセルの区別がなくなります。 // 中継処理(クライアントが切れると上流の読み取りも止まる作り) const readable = new ReadableStream( { async start ( controller ) { // 上流(AgentCore)の SSE を読み、そのままクライアントへ中継する } , cancel () { reader.cancel(); } , } ); 3 つ目は保存先です。 イベントは中継されるだけで、どこにも保存されません。仮に 1 つ目と 2 つ目を直して実行が完走するようにしても、戻ってきた画面に表示するデータがありません。 「実行状態を React のグローバルストアに持てば、画面遷移に耐えられるのでは」という案も検討しました。しかしこのアプリでは、チャット画面から他の画面への遷移が window.location.href によるページ全体の再読み込みで実装されています。再読み込み後のページは JavaScript の実行環境ごと新しく作られるため、React の state やグローバルストアに入れた値は引き継がれません。 設計方針:実行の作成と購読を分離する 大きく変えたのは次の 3 つです。 作成と購読の分離 : POST /runs は実行を開始して 202 { runId } を即座に返します。表示は GET /runs/{runId}/events の SSE で受け取ります。この「SSE を受信し続けること」を、本記事では「購読」と呼びます。購読はいつ切れてもよく、何度でも再開できます。 実行ワーカーの独立 : AgentCore の SSE を最後まで読み切って記録する処理(実行ワーカー)を、HTTP レスポンスから独立した非同期タスクにしました。ブラウザが切断しても実行は完走します。 二層の保存 : 実行中は「生成途中の回答」を DB に上書き保存し続け、完了したら完成したメッセージを DB の履歴テーブルに保存します。テーブル構成は次の節で説明します。 なお、実行ワーカーは Next.js と同じプロセス内で動かしているため、リクエストごとに実行環境が終了するサーバーレス環境ではこの形は取れません。現在は検証段階のためこの構成にしていますが、Next.js のデプロイに走行中の実行が巻き込まれないようにするため、本来は実行ワーカーを独立したプロセスに切り出す方が望ましいと考えています。現状、プロセスがデプロイなどで止まる場合の後始末は、同時実行制御の節で説明する回収の仕組みが担います。 改修後の構造。実行は接続と無関係に完走し、購読は何度でも再入場できる ブラウザとサーバーの間の API は次の 5 本です。 API 役割 POST /runs 実行を作成して 202 { runId, conversationId } を即返す GET /runs/{id}/events SSE 購読。切断・再入場が自由 POST /runs/{id}/cancel 明示的なキャンセル GET /conversations 会話一覧(履歴サイドバー用) GET /conversations/{id} 会話の全メッセージ + 実行中の run(あれば) 最後の API がポイントです。リロードや別タブで会話を開いた直後、クライアントは会話 ID しか知らず、実行中の run があるかどうかも分かりません。そこで GET /conversations/{id} は、会話のメッセージに加えて「実行中の run の ID」を返します。クライアントはその ID で GET /runs/{id}/events を購読し、生成途中から表示を再開します。 DB スキーマ:残す履歴と実行中の状態を分ける このアプリでは以前から、本体機能のデータを Aurora MySQL 8.0 + Prisma で管理しています。エージェントの履歴も同じ DB に、3 つのテーブルで持つことにしました。ずっと残す「履歴」と、実行中だけ使う「実行状態」でテーブルを分けています。 区分 テーブル 役割 行の扱い 履歴 conversations 会話スレッド 1 件のメタ情報 ずっと残す 履歴 conversation_messages メッセージ 1 件 = 1 行。完成した発話を保存 ずっと残す 実行状態 runs 実行 1 回の状態 + 生成途中の回答 + ロック 行は実行 1 回ごとに増え、終了後も記録として残る。生成途中の回答やロックは実行中だけ使う ER 図 Prisma スキーマは次の通りです。実際に採用したものから、タイムスタンプ列・リレーション定義・enum 定義(RunStatus / MessageStatus)を省いています。 model Conversation { id String @id @default(cuid()) companyId Int @map("company_id") userId String @map("user_id") threadId String @map("thread_id") @db.Char(36) title String @db.VarChar(255) deletedAt DateTime? @map("deleted_at") @@unique([companyId, userId, threadId]) @@map("conversations") } model ConversationMessage { id String @id @default(cuid()) conversationId String @map("conversation_id") runId String? @map("run_id") sequence Int role String @db.VarChar(16) parts Json status MessageStatus @default(complete) @@unique([conversationId, sequence]) @@map("conversation_messages") } model Run { id String @id @default(cuid()) conversationId String @map("conversation_id") clientTurnId String @map("client_turn_id") @db.VarChar(64) status RunStatus @default(queued) lockKey String? @unique @map("lock_key") ownerInstanceId String? @map("owner_instance_id") @db.VarChar(64) errorCode String? @map("error_code") @db.VarChar(64) partialState Json? @map("partial_state") heartbeatAt DateTime @default(now()) @map("heartbeat_at") @@unique([conversationId, clientTurnId]) @@index([status, heartbeatAt]) @@map("runs") } lockKey の UNIQUE、 clientTurnId の複合ユニーク、 [status, heartbeatAt] のインデックスがそれぞれ何のためにあるかは、後の節で順に説明します。 また、本記事には 4 種類の ID が登場します。ここで整理しておきます。 ID 何を指すか conversationId DB 上の会話。API で会話を指すときに使う threadId AG-UI 上の会話 ID。DB 上の会話( conversationId )と 1 対 1 で対応 runId 質問 1 回ぶんの実行 runtimeSessionId AgentCore の実行環境を束ねる ID。 t{companyId}-u{userId}-{threadId} の形式で、会話ごとに固定 保存単位は「AG-UI の Message 1 件 = 1 行」 conversation_messages は追記専用で、AG-UI の Message をそのまま parts (JSON)に格納します。テキストだけのターンは user / assistant の 2 行、ツールを使うターンは assistant(ツール呼び出し)と tool(結果)の行が挟まって 4 行以上になります。 1 会話のメッセージ行の例。ツールを使うターンは user・assistant(ツール呼び出し)・tool・assistant の 4 行、使わないターンは 2 行になる この保存単位を選んだ理由は、フロントの表示ロジックの作りにあります。ライブ表示は「AG-UI の Message 配列を受け取り、ターンの区切りやツールの実行ステップ表示を組み立てる純粋関数」として自前で実装しています。保存した Message 列をそのままこの関数に渡せば、画面を離れなかった場合と同一の表示が再現されます。保存時に表示用の形へ加工してしまうと、同じ表示を再現できなくなります。 DynamoDB ではなく Aurora MySQL を選んだ理由 会話履歴のアクセスパターン(会話 ID + 連番で順に全件取得、追記専用、JSON 主体)は DynamoDB の得意領域で、実際に移行案も検討しました。それでも Aurora MySQL 一本にしています。 まず、トランザクション要件が構成の選択肢を絞ります。質問の受付時には「会話 + user メッセージ + 実行(ロック)の INSERT」を、完了時には「assistant メッセージの INSERT + 実行の完了 + ロック解放」を、それぞれ単一トランザクションで行う必要があります。「履歴は DynamoDB、実行状態は Aurora」のように 2 つのストアに分けると、この原子性を保証できません。原子性が無いと、たとえば次のような壊れ方をします。 回答は残ったのに次の質問ができない : 完了処理の「回答を保存」と「ロック解放」の間でプロセスが落ちると、画面には回答が出ているのに DB はロックを握ったままになり、次の質問が「実行中です」と拒否され続けます。 答えのない質問が履歴に残る : 送信の二度押しで 2 本目が「質問を保存 → ロックで弾かれる」の順に進むと、誰も回答しない質問だけが履歴に残ります。1 トランザクションならロック取得の失敗と同時に質問の保存も取り消され、エラー応答だけを返せます。 したがって選択肢は「全部 Aurora」か「全部 DynamoDB」に絞られます。後者も技術的には成立します(DynamoDB でも TransactWriteItems で複数の項目をまとめて原子的に書けます)。 それでも Aurora にしたのは、既存の運用との一貫性のためです。このアプリの他のデータはすべて Aurora + Prisma で管理していて、マイグレーションの手順やレビューの観点といったチームの運用もそこで揃っています。データストアを 2 つにすると、この運用も 2 系統になります。規模の面でも、DB への書き込みはピークでも毎秒数十回の見積もりで、Aurora で十分に受けられます。DynamoDB のスケール性能が必要になる水準ではありません。 会話履歴はサーバーが組み立てる エージェントは毎回の呼び出しで会話の全履歴を受け取り、状態をゼロから組み立て直す作りにしています。この全履歴を誰が用意するかには 2 つの形があります。クライアントが手元の Message 配列を毎回送るか、 サーバーが DB から組み立てる かです。私たちは後者にしました。クライアントが送るのは新しいメッセージ 1 件だけです。 前者を避けた理由は、バックグラウンド実行と相性が悪いからです。実行を放置して別のタブで完走させると、元のタブが持っている履歴は古いままになります。その古いタブから次の質問を履歴ごと送ると、完走したはずの回答がモデルへの入力から抜け落ち、会話のつじつまが合わなくなります。後述するロックは実行中しか効かないため、この事故は防げません。最新の会話を常に持っているのは DB だけです。 なお、AgentCore 側に会話の状態を持たせる案も 2 つ検討し、見送りました。 実行環境(microVM)のメモリに持つ : セッション ID は会話ごとに固定なので、同じ会話の呼び出しは同じ実行環境に届き、メモリに状態を残すこと自体はできます。ただしこの環境は無操作 15 分(デフォルト)などで終了し、メモリごと消えます。時間を空けて続く会話の置き場にはできません Memory サービスに持つ : AgentCore には会話を保存する Memory というサービスもあります。ただし履歴はどのみち表示のために自前の DB へ保存するので、足すと同じ役割の保存先が 2 つになります POST /runs のボディは { conversationId, message, clientTurnId } だけです。モデルへ渡す履歴は、実行ワーカーが Aurora から組み立てます。 // 実行ワーカーの一部:DB から会話履歴を読み出し、モデル入力用に整える export async function buildModelMessages ( conversationId : string ): Promise < ModelMessagesResult > { const rows = await prisma.conversationMessage.findMany( { where : { conversationId , status : "complete" } , orderBy : { sequence : "asc" } , select : { parts : true } , } ); return normalizeHistory(rows); } // 実行ワーカーの一部:組み立てた履歴をエージェントに渡して生成を開始する const history = await buildModelMessages(claimed.conversationId); const { companyId , userId , threadId } = claimed.conversation; const agent = new AgentCoreAgent( { threadId , initialMessages : history .messages, agentArn , runtimeUserId : `c ${ companyId } :u ${ userId } ` , runtimeSessionId : `t ${ companyId } -u ${ userId } - ${ threadId } ` , } ); こうすると、会話の内容はサーバー(DB)だけが持つ構造になります。AgentCore の microVM がタイムアウトで終了しても、クライアントがリロードで状態を失っても、会話は Aurora から再構成できます。 書き込み設計:イベントは保存せず「生成途中の回答」を上書きする 1 回の回答で AG-UI イベントは数十〜数百個流れます。本文の断片(デルタ)1 つ 1 つやツール呼び出しがそれぞれイベントになるため、回答が長いほど増えます。これを 1 行ずつ INSERT すると、1 回答ごとに大量の行が永久に積み上がります。採用したのは次の形です。 時点 DB 操作 内容 質問送信 INSERT conversations に会話(新規会話のときのみ)、 conversation_messages に質問、 runs に実行レコード(ロック取得を兼ねる)の最大 3 行 生成中 runs.partial_state を上書き UPDATE AG-UI のイベントでは本文が細切れ(デルタ)で届く。実行ワーカーはそれをつなぎ合わせた「その時点のメッセージ配列」を保持しており、これを数秒ごとに同じ 1 行へ上書き保存。行は増えない 完了 conversation_messages に INSERT そのターンで生まれたメッセージ(回答、ツールを使った場合はその呼び出しと結果も)を保存。 runs.partial_state を空にし、ロックを解放 質問送信時のトランザクションは以下のようになっています。ロック( lockKey )の取得と質問の保存が、同時に成立するか同時に失敗するかのどちらかになります。 // API Route の一部:質問受付時の書き込み return prisma.$transaction( async ( tx ) => { const conversationId = existingId ?? ( await tx.conversation.create( { /* 省略 */ } )).id; // ロック取得に失敗したら、下で保存する質問ごと取り消される(同一トランザクションのため) const run = await tx.run.create( { data : { conversationId , clientTurnId , lockKey : conversationId } , select : { id : true } , } ); // aggregate(集計クエリ)で会話内の最大 sequence を取り、次の連番を振る const highest = await tx.conversationMessage.aggregate( { where : { conversationId } , _max : { sequence : true } , } ); await tx.conversationMessage.create( { data : { conversationId , runId : run. id , sequence : (highest._max.sequence ?? 0 ) + 1 , role : "user" , parts : { id : randomUUID(), role : "user" , content : message } , } , select : { id : true } , } ); return { runId : run. id , conversationId } ; } ); 生成中の partial_state は 3 秒間隔で間引いて書きます。間引きに加えて「前の UPDATE が完了するまで、次の UPDATE を発行しない」という制御も入れています。UPDATE を発行した順と DB に反映される順は一致するとは限らないため、古い内容の UPDATE が新しい内容の後に適用されると、保存済みの「生成途中の回答」が巻き戻ってしまうからです。 // 実行ワーカーの一部:生成途中の回答を数秒ごとに DB へ上書き保存する const PARTIAL_STATE_INTERVAL_MS = 3_000 ; return { schedule() { // 前の書き込みが完了するまで次をスケジュールしない(古い内容への巻き戻りを防ぐ) if (timer || writing || truncated) return ; timer = setTimeout (() => { timer = null ; writing = true ; void writePartialState(runId, ownerInstanceId, produced()) . then (( state ) => { truncated = state.truncated; } ) . catch (( error : unknown ) => console .error( "partial_state の更新に失敗しました" , { runId , error } ), ) . finally (() => { writing = false ; } ); } , PARTIAL_STATE_INTERVAL_MS); } , } ; 数秒おきに UPDATE を発行し続けて DB の負荷は大丈夫なのか、という点は検討しました。結論としては、同時に走る生成が多くても数十本という規模では問題になりません。更新は各実行が自分の 1 行だけを主キー指定で行い、実行間のロック競合はありません。さらに、接続中のユーザーの画面へは実行ワーカーがメモリ上のイベントを直接流すため、 partial_state の用途は後述する再合流だけです。毎秒書く必要も、イベントを 1 個ずつ書く必要もありません。 完了時は「回答の確定保存」「実行ステータスの完了への更新」「ロック解放」「生成途中の回答( partial_state )の削除」を 1 トランザクションで行います。 // 実行ワーカーの一部:完了時の書き込み。lock_key を外し損ねるとその会話が永久に 409 になる return prisma.$transaction( async ( tx ) => { const claimed = await tx.run.updateMany( { where : terminableWhere(runId, ownerInstanceId), data : { status , errorCode , finishedAt : new Date (), lockKey : null , partialState : Prisma.DbNull, } , } ); // 別の経路(キャンセルや、後述する異常終了時の回収処理)が先にこの実行を // 終わらせていたら、生成物は保存しない if (claimed. count === 0 ) return false ; await tx.conversationMessage.createMany( { data : messages. map (( message , index ) => ( { conversationId : targetId, runId , sequence : base + index + 1 , role : message.role, parts : message as Prisma.InputJsonValue , status : messageStatusAt( status , message. id , openMessageIds), } )), } ); return true ; } ); メッセージの status は通常 complete で保存します。キャンセルやエラーで実行が正常に終わらなかった場合は、そこまでに生成できていた分を partial(部分的、の意味)として保存し、画面に残せるようにしています。 再合流:会話全体のスナップショットで追いつく 実行中の会話に購読者が入ってくると、サーバーはまず RUN_STARTED (「実行が進行中です」の合図)と MESSAGES_SNAPSHOT を送ります。どちらも上流から届いたイベントの中継ではなく、この購読のためにサーバーが新しく作って送るものです。 MESSAGES_SNAPSHOT を受け取ったクライアントは、手元のメッセージ一覧を捨てて、スナップショットの内容で丸ごと置き換えます。そのため、スナップショットに生成途中の 1 件だけを入れると、過去のメッセージが画面からすべて消えてしまいます。必ず会話の全メッセージを入れて送ります。 その後の配信は 2 つのモードに分かれます。分かれ目は「購読がいつ始まったか」です。 モード いつ使われるか 配信内容 live 質問の送信直後から購読している場合(生成イベントがまだ 1 件も流れていないうちに購読が始まったとき) 実行ワーカーが受け取る生成イベント( TEXT_MESSAGE_CONTENT など)を、メモリからそのまま逐次中継 poll それ以外すべて(リロード・別タブ・離脱して戻ってきた場合) 1 秒間隔で DB を読み、確定済み履歴と partial_state の生成途中回答をマージした会話全体の MESSAGES_SNAPSHOT を、内容が変わったときだけ送り直す。実行が終わったら RUN_FINISHED (または RUN_ERROR )で締める つまり、途中から戻ってきた購読者が受け取るのは live 配信のイベント列ではなく、「会話全体のスナップショットの送り直し」です。 // 配信モードの選択。イベントが 1 件でも流れた後に始まった購読は poll に回す export function attach ( runId : string , signal : AbortSignal ): LiveSubscription | null { const fanout = fanouts. get (runId); if (!fanout) return pollInstead(runId, "fanout_absent" ); if (fanout. closed ) return pollInstead(runId, "fanout_closed" ); // 1 件でも中継済みなら列の途中からになるので、履歴を出せる poll に任せる if (fanout.relayed > 0 ) return pollInstead(runId, "already_relayed" ); // 省略(購読者を登録し、生成イベントを流す AsyncGenerator を返す) } // poll 配信のループ。会話全体のスナップショットを、内容が変わったときだけ送り直す while ( true ) { const event = snapshot(stored, readPartialMessages(progress.partialState)); const serialized = JSON . stringify (event); if (serialized !== previous) { previous = serialized; yield event; } if (isTerminal(progress. status )) break ; await sleep(POLL_INTERVAL_MS, signal); progress = await readProgress(run. id ); if (isTerminal(progress. status )) stored = await loadConversationMessages(run.conversationId); } yield terminalEvent(run, progress); 途中合流の購読者を live のイベント列に合流させず poll に回すのは、正しさを優先したためです。デルタの続きから流すには、「スナップショットに含めた分」と「これから流すデルタ」の境界を厳密に合わせる必要があります。境界がずれると、 content += delta の積み上げで本文が二重に連結されます。会話全体のスナップショットを送り直す形なら、毎回が丸ごとの置き換えなので、この事故が原理的に起きません。その代わり、poll 配信の画面は live 配信のようなストリーミング表示にはならず、数秒おきに文章がまとまって進む表示になります。途中合流でもストリーミング表示にすることは、後続の課題にしています。 同時実行制御:実行中ロックを NULL 可のユニーク列で作る 「同一会話に実行中の run は 1 本だけ」を DB で強制します。PostgreSQL なら 部分インデックス (partial index。 CREATE UNIQUE INDEX ... WHERE status IN ('queued','running') のように、条件を満たす行だけへ一意制約をかけられます)で書けますが、MySQL 8.0 には相当する構文が用意されていません。 代わりに runs.lock_key (NULL 可・UNIQUE)を使いました。実行中は lock_key = conversationId 、終了時に NULL へ戻します。MySQL のユニークインデックスは NULL を重複として扱わない ため、終了済みの run は何本でも共存でき、実行中は会話ごとに 1 本に絞られます。 同一会話への 2 本目の POST /runs は、INSERT 時にユニーク制約違反のエラーとして原子的に弾かれます。重複には 2 種類あります。1 つは「同じ送信の二度押し」です。クライアントは送信 1 回ごとに ID( client_turn_id )を発行し、リトライでも同じ ID を送るため、この列の重複で検出できます。送信ボタンの連打はフロントでも抑止できますが、ネットワーク不調時の自動再送などフロントの制御では防げない経路が残るため、DB でも守ります。もう 1 つは「別の質問の並行送信」( lock_key の重複)で、同じ会話を複数のタブで開いているときに起きます。どちらだったかを引き直して、応答を分岐します。 // API Route の一部:重複キーエラーの解釈 } catch (error) { if (!isUniqueViolation(error)) throw error; // 二度押しなら、先行の run をそのまま返す(同じ送信は 1 回として扱う) const raced = await findRunByClientTurnId(params); if (raced) return { ok : true , runId : raced. id , conversationId : raced.conversationId } ; // 並行送信なら、実行中の run を添えて拒否へ const activeRunId = await findActiveRunIn(conversationId); if (activeRunId) return { ok : false , reason : "active_run" , activeRunId } ; } // API Route の一部:並行送信への応答 if (result.reason === "active_run" ) { return Response .json( { errors : "この会話はいま実行中です" , activeRunId : result.activeRunId } , { status : 409 } , ); } 409 のレスポンスに activeRunId を含めているのは、UI がそれを使って「拒否」ではなく「実行中の run への購読切り替え」に変換できるようにするためです。 ロックの解放漏れに備える ロックには解放漏れへの備えも必要です。サーバーのプロセスが突然落ちると、running のままロックを握った run が残ります。そうなると、誰も実行していないのにその会話への質問が「実行中です」と拒否され続けます。備えは 2 つの仕組みの組み合わせです。 実行ワーカーは、実行中の run の heartbeat_at を定期的に現在時刻へ更新します(処理が続いていることの記録です) それとは別の掃除処理が、 heartbeat_at の更新が一定時間止まっている queued / running の run を「担当プロセスが異常終了した」とみなして failed にし、ロックを解放します。 heartbeat_at は INSERT 時に現在時刻が入るため、202 を返した直後・実行が始まる前にプロセスが落ちて queued のまま残った run も、この経路で回収されます(スキーマの @@index([status, heartbeatAt]) はこの検索用です) 「サーバー起動時に、残っている running を全部 failed にする」というより単純な方法は採れませんでした。デプロイ中は新旧のサーバーがしばらく同時に動いており、旧サーバーがまだ実行している最中の run を、新サーバーの起動処理が誤って failed にしてしまうためです。run に owner_instance_id (どのサーバーがその実行を担当しているか)を持たせているのも同じ理由です。掃除処理は、自分のサーバーがいま実行している run を誤って回収しないよう、この ID とメモリ上の実行一覧を突き合わせて判定します。 この回収の仕組みは、デプロイやスケールインでプロセスごと止められた場合の後始末も兼ねています。止まったプロセスが抱えていた実行は途中から再開できませんが、heartbeat が途絶えるため数分以内に failed になり、そこまでの生成分は partial として履歴に残り、会話のロックも解放されます。ユーザーは失敗を確認して、すぐ次の質問に進めます。 停止:切断とキャンセルを区別する この設計では、タブを閉じる・画面を遷移するのは「切断」であり、実行は継続します。明示的に止めたいときは POST /runs/{id}/cancel を呼びます。実行ワーカーにキャンセル要求の印を立てて上流への購読を切り離し、実行を「キャンセル」として記録します。記録後に遅れて届いた生成物は、前述の完了時トランザクションの「別の経路が先に終わらせていたら保存しない」分岐で破棄されます。 // API Route の一部:キャンセル処理 if (getActiveRun(runId) || run.ownerInstanceId === OWNER_INSTANCE_ID) { // 終了の記録は実行ワーカーに任せる。ここで書くと、ワーカーが「先に終了済み」と判定して生成物を捨てる const active = registerRun(runId); active.cancelRequested = true ; await active.agent?.detachActiveRun(); return { ok : true } ; } // 別のサーバーが担当している run。会話のロックを解放するために記録だけ書く await finalizeRun( { runId , status : "cancelled" , finalizedBy : `cancel: ${ OWNER_INSTANCE_ID } ` } ); 実行中の会話への追加送信は、前述のロックにより 409 で拒否されます。ただし 409 を返すだけだと、「戻ってきたら画面が止まって見える → もう一度送る → エラー」という流れになりやすいため、途中経過の可視化(再合流)を初回リリースの範囲に含めています。実行中であることが見えていれば追加送信は起きにくく、方向を変えたい場合も「停止してから送る」導線に誘導できます。 Redis は要るか 同種の設計では、実行中イベントの共有に Redis(Redis Streams)を使う構成がよく知られています。ただし Redis が必要になるのは「タスクを 2 つ以上に増やし、かつストリーミング表示を保ちたい」場合です。今回はどちらにも当てはまらないため、入れていません。 現在は ECS 1 タスクで動かしており、通常時は再接続のリクエストが実行ワーカーと同じプロセスに届きます。イベントはプロセス内のメモリで手渡せるため、Redis なしで live 配信が成立します。 タスクを 2 つ以上に増やすと、購読のリクエストが実行ワーカーのいない方のタスクへ届くことがあります。live 配信はワーカーと同じプロセスのメモリを介して成り立っているため、別のタスクに届いた購読では使えません。ただし DB はどのタスクからも読めるので、poll 配信はそのまま動きます。つまり live 配信できたはずの購読が poll 配信になり、ストリーミング表示が数秒おきの更新になるだけで、履歴も途中経過も見られます。設計方針の節で触れた「実行ワーカーを独立したプロセスに切り出す」場合も、ワーカーと購読者が必ず別プロセスになるため、同じく中継が必要になります。当面は 1 タスクで足りる規模のため、現時点ではこの構成にしています。 まとめ 実行の作成と購読を分離し、実行ワーカーを HTTP 接続から独立させることで、画面を離れても実行が完走する構造にしました。 履歴は「メッセージ 1 件 = 1 行」でずっと残し、生成途中の回答は上書き更新の 1 行に分けました。イベントの逐次保存はせず、モデルへ渡す履歴もサーバーが DB から組み立てます。 同時実行制御は MySQL の「NULL 可ユニーク列」によるロックで実現しました。キャンセルは購読の切り離しと、実行を「キャンセル」として記録することで実現し、切断とは明確に区別しています。 AI チャットの「履歴」と「バックグラウンド実行」は別々の機能に見えますが、作ってみると、どちらも「会話の状態はサーバー側で持つ」という同じ設計に行き着きました。AI チャットの実行基盤を作る際に共通して現れる論点だと思うので、同じものを作る方の参考になれば幸いです。
目次 はじめに Partial Prefetchingは何を解決するのか すべてを先読みする方法と、何も先読みしない方法の中間を作る OISHYでPartial Prefetchingを試す 検証条件 計測方法 実験1:クリック前の取得を減らすと、通信と遷移はどう変わるか 実験2:URL固有のデータを待つ間に何が見えるか 補足:適用範囲によって先読みの形が異なった まとめ 参考文献 はじめに こんにちは、開発本部の 黒髙 です。最近はアメリカ向けレシピサービスである OISHY の開発に関わっています。 Next.js 16.3 では、ページ遷移の応答性を改善する仕組みとして Instant Navigations が追加されました。クリック前に再利用できるUIやデータを準備し、クリック後に必要な部分を取得することで、遷移直後から次のページを描画しやすくする仕組みです。 Instant Navigationsを構成する機能のうち、今回取り上げるのは Partial Prefetching です。本記事では、Partial Prefetchingを紹介したうえで、OISHYの既存ページでは通信と画面遷移にどのように現れるのかを検証します。 Partial Prefetchingは何を解決するのか すべてを先読みする方法と、何も先読みしない方法の中間を作る Next.jsの <Link> によるprefetch(先読み) では、リンクが画面内に入ると、ユーザーがクリックする前に遷移先のデータを取得します。クリック時にデータが揃っていれば、遷移先をすぐに表示できます。 一方、リンクが多い画面では、実際にはクリックされないリンクのデータも取得します。先読みを無効にすれば事前通信はなくなりますが、その場合はクリックしてから遷移先のデータを取得することになります。 Next.js 16.3のPartial Prefetchingは、この二つの中間を作る機能です。Cache Componentsを使うページへの既定の <Link> では、URLごとの完成形をすべて先読みする代わりに、同じ種類のページで再利用できるApp Shellを先読みします。クリックされたURLに固有のデータは、必要になった時点で取得します。 App Shell とは、URL固有のデータを待たずに表示できるページの共通部分です。サイト共通のレイアウトや、データの読み込み中に表示するUIなどが含まれます。 レシピ詳細ページを例にすると、レシピ名や材料はURLごとに変わりますが、それらを待っている間の枠組みは複数のレシピで共有できます。 同じルートを指す複数のリンクで一つのApp Shellを再利用できる ため、リンクごとに同じ共通部分を取得し直す必要もありません。 この仕組みは、次のようなページで効果が現れやすいと考えられます。 商品、記事、レシピなど、同じ種類の詳細ページへのリンクが多数並ぶ 表示されたリンクのうち、実際にクリックされるのは一部だけである 詳細ページに、共通レイアウトやデータ取得中の表示など、URLをまたいで再利用できる部分がある OISHYのトップページにも、同じ /recipes/[id] へ遷移するレシピリンクが多数並びます。そこで今回は、レシピ詳細ページをPartial Prefetchingへ切り替えたとき、クリック前の通信とクリック後の表示がどう変わるかを確かめました。 OISHYでPartial Prefetchingを試す OISHYでは、すでに Cache Components を有効にしています。まず、レシピ詳細ページへの遷移にPartial Prefetchingを適用するため、次の設定を追加しました。 // app/recipes/[id]/page.tsx export const prefetch = 'partial' ; 公式リファレンスでは、 prefetch = 'partial' はリンクではなく遷移先に設定する ものと説明されています。この設定により、レシピ詳細ページをPartial Prefetchingへ段階的に切り替えます。 検証条件 主な検証では、次の3条件を比較しました。 条件 Next.js Partial Prefetchingを適用する場所 データ取得中の表示 条件A 16.2.11 適用しない なし 条件B 16.3.1 レシピ詳細ページ なし 条件C 16.3.1 レシピ詳細ページ スケルトン表示 実験1では条件Aと条件Bを比較し、クリック前の取得を減らしたときに通信と画面遷移がどう変わるかを確認しました。実験2では条件Bと条件Cを比較し、URL固有のデータを待つ間にApp Shellが画面にどう現れるかを確認しました。 以下で扱う条件B・Cと補足検証の先読み通信は、Next.js 16.3.1で観測したものです。内部的な通信の形は、今後のバージョンで変わる可能性があります。 OISHYの既存構成では、Next.jsの standalone server をECSで動かし、その前段にCloudFrontとALBを置いています。今回の計測もこの配信経路で行いました。 計測方法 対象 :同じ10件のレシピを、それぞれ3回ずつ計測 操作 :トップページを新しいタブで開き、対象リンクが画面内に入る位置までスクロール。5秒待ってからクリックし、クリック前の先読み通信とレシピタイトルが表示されるまでの通信・時間を記録 ブラウザ条件 :画面サイズは1280×900、回線速度の制限はなし。ブラウザ側のHTTPキャッシュは計測のたびに無効化し、スクロール位置とクリックまでの待ち時間を統一 集計 :各レシピの3回の中央値を求めたあと、10件の中央値を代表値として使用。通信量にはChrome DevTools Protocolの Network.loadingFinished.encodedDataLength を使い、ブラウザが受信したデータ量に近い値を比較 実験1:クリック前の取得を減らすと、通信と遷移はどう変わるか 最初に、Next.js 16.2.11の条件Aと、16.3.1へ更新してレシピ詳細ページをPartial Prefetchingへ切り替えた条件Bを比較しました。この比較にはNext.js自体の更新も含まれるため、Partial Prefetchingだけの効果ではなく、OISHYで16.3.1への更新と機能導入を行った前後差として扱います。 ここでいう RSC(React Server Components)データ は、Next.jsがクライアント側の画面を更新するために送るデータです。 指標(中央値) 条件A(変更前) 条件B(レシピ詳細ページに適用) クリック前のRSCリクエスト 75 6 クリック前の転送量 約319KB 約20KB クリック後のRSCリクエスト 0 1 クリック後の転送量 0KB 約20KB クリック前後の合計リクエスト 75 7 クリック前後の合計転送量 約319KB 約38KB クリック前のリクエスト数は約92%、転送量は約94%減りました。クリック後には、選択したレシピのRSCリクエストが1件発生しています。それを含めた合計転送量も約88%減っており、クリック前の通信がそのまますべてクリック後へ移ったわけではありませんでした。 変更前は、画面内に並ぶ多数のレシピについてURL固有のRSCデータを取得していました。変更後は、共通部分を先読みし、選択したレシピのデータをクリック後に取得しています。今回の画面では、クリックされなかったレシピへの先読みが減ったことが、通信量の差として大きく現れました。 次の画像は1回の計測例で、表の数値は反復計測から求めた代表値です。 条件Bでのクリック前のNetwork記録の例 一方、クリックからレシピタイトルが表示されるまでの代表値は、変更前が約44ms、変更後が約82msで、どちらも100ms未満でした。URL固有のデータをクリック後に取得するようになったことと整合しますが、今回の条件では目視できるほど長い待ち時間にはなりませんでした。大きく変わったのは、完成画面が出るまでの見た目よりも、クリック前に取得するデータの量でした。 なお、この結果は回線速度を制限しない環境でのものです。Partial Prefetchingは、クリック前の転送量を減らす代わりに、URL固有データの取得をクリック後へ移す仕組みです。低速な回線では、クリック後の待ち時間が今回より長くなる可能性がある一方、クリックされないリンクへの先読みを抑える効果は大きくなるため、有効に働くかどうかは通信環境によってトレードオフになり得ます。 実験2:URL固有のデータを待つ間に何が見えるか Partial Prefetchingでは、URL固有のデータがクリック時点で揃っていなければ、クリック後にデータの取得を待つ時間が生じます。その間にApp Shellがどのように現れるかを見るため、条件Cではレシピデータの取得中に表示するスケルトンを loading.tsx として定義しました。 loading.tsx 自体はNext.js 16.3の新機能ではありません。同じルート階層のページを Suspense 境界で囲み、定義したUIをデータの準備中に表示する仕組みです。Partial Prefetchingでは、 この代替表示を含むApp Shellが先読みの対象になります 。 // app/recipes/[id]/loading.tsx export default function RecipeDetailLoading () { return < RecipeDetailSkeleton /> ; } スケルトンとは、完成後のレイアウトに近い枠を先に表示し、データを待っている場所を示すUIです。今回のレシピ詳細ページでは、次のスケルトンがApp Shellとして先に表示される部分になります。 レシピ詳細ページのスケルトン表示例 30回中25回はスケルトンを経由せず、完成したレシピ詳細が直接表示されました。スケルトンが先に現れたのは5回で、クリックから14〜22msで表示され、レシピタイトルより約68〜856ms先行しました。 この結果から、スケルトンは毎回挟まる中間画面ではなく、レシピ固有のデータがクリックまでに揃わなかった場合だけ、遷移先の枠組みとして先に表示されることが分かりました。 また、条件Cの通信を追加で確認すると、条件Bとは異なる挙動が見つかりました。条件Bではレシピ固有のデータをクリック後に取得していた一方、条件Cの補完計測では、10回中4回で複数のレシピ固有データをクリック前に取得するRSC通信が並びました。 条件Cでクリック前に観測したレシピ固有データの先読み この違いがPartial Prefetchingの適用範囲と関係するのかを確認するため、追加計測しました。 補足:適用範囲によって先読みの形が異なった 条件Cでは、レシピ詳細ページだけに prefetch = 'partial' を設定していました。 prefetch = 'partial' は、設定したページより上位のルート階層まで同じ設定にするものではありません。 Next.js 16.3.1の実装 では、それぞれのルート階層が、その階層で指定された設定かアプリ全体の既定値を参照します。 この挙動を調べる中で、 段階導入時の先読みを扱うNext.js 16.3.1の公式テスト を見つけました。このテストにも、動的なページだけをPartialにし、未設定の上位階層を従来の方法で扱う構成で、リンク先固有のデータまで先読みする例があります。 そこで、条件Cの loading.tsx とページ側の設定を残したまま、 アプリ全体の既定値をPartialにする partialPrefetching: true だけを追加しました。 // next.config.ts const nextConfig = { cacheComponents : true , partialPrefetching : true , } ; 同じ導線を10回計測した結果は次のとおりです。 Partial Prefetchingの適用範囲 URL固有データの先読み 共有部分の先読み レシピ詳細ページのみ 10回中4回で観測 あり アプリ全体 10回中0回 あり 同じ loading.tsx を残したまま適用範囲を変えると、今回の導線ではURL固有データの先読みが10回中4回から0回になりました。一方、App Shellを構成する共有部分の先読みは続いていました。Next.js 16.3.1の実装と公式テストを合わせると、今回観測した通信にはPartial Prefetchingの適用範囲が関係していたと考えられます。 まとめ Partial PrefetchingをOISHYのレシピ一覧で試したところ、16.3.1への更新と機能導入後、クリック前のRSC転送量が約94%減り、クリック後を含む合計でも約88%減りました。 一方、変更前からレシピタイトルまでの表示は約44msと十分に速く、回線速度を制限しない今回の環境では、通信量ほど大きな見た目の差はありませんでした。スケルトンを含むApp Shellは、URL固有のデータがクリックまでに揃わなかった場合だけ、完成したページより先に表示されました。 また、レシピ詳細ページだけに適用した場合と、アプリ全体に適用した場合では、クリック前の先読みも異なりました。ページ単位で試せる機能ではありますが、今回の検証では適用範囲も実際の通信に関係していました。 Partial Prefetchingは、商品・記事・レシピの一覧のように、同じ種類の詳細ページへのリンクが多く、その一部だけがクリックされる画面を想定しています。クリックされないURL固有データの先読みを抑えながら、データが間に合わない場合にはApp Shellを先に表示する仕組みです。今回のOISHYでは、先読み通信量には大きな差が出た一方、見た目の差は小さいという結果でした。 参考文献 Next.js 16.3 Next.js 16.3: Instant Navigations Instant Navigationガイド Adopting Partial Prefetching <Link> のprefetch(先読み) Cache Components Server and Client Components output: 'standalone' prefetch のルート設定(Next.js 16.3.1) 同じルートでApp Shellを再利用する説明(Next.js 16.3.1) partialPrefetching の全体設定(Next.js 16.3.1) loading.tsx とSuspense境界(Next.js 16.3.1) loading.tsx の代替表示を先読みする説明(Next.js 16.3.1) ルート階層ごとの先読み設定を扱う実装(Next.js 16.3.1) 段階導入時の先読みを確認する公式テスト(Next.js 16.3.1) Chrome DevTools Protocol: Network.loadingFinished
title はじめに こんにちは、株式会社エブリーでデリッシュキッチンのiOSアプリの開発をしている成田です。 現在は、プレミアムユーザーの登録数の向上やプレミアムユーザーの体験をより良くすることを目的としたチームで開発をしています。 サブスクリプションの解約は、これまで開発者にとってブラックボックスでした。ユーザーが App Store の管理画面で「サブスクリプションをキャンセルする」を押すとき、アプリ側にできることは何もありません。引き止めのメッセージも、オファーの提示も、そもそも解約されようとしていることを知ることさえ、その瞬間にはできませんでした。 WWDC26 で発表された Retention Messaging は、ここに初めて介入手段を与える機能です( セッション309 )。解約確認画面に、アプリからのメッセージやオファーを差し込めるようになります。 Appleによれば、この機能を導入したサブスクリプションでは、解約を取りやめて利用を継続するユーザーの割合が改善されているとのことです。解約抑止にも取り組む立場としては、無視できない機能です。 まだ一般提供はされていませんが、具体的に何ができるのか、どう始めればよいのか、現時点で分かっていることを整理します。 何ができるのか ユーザーが App Store のサブスクリプション管理から解約しようとすると、確認画面が出ます。Retention Messaging を設定しておくと、この画面にアプリからのコンテンツが表示されます。 表示できる形式は3つです。 形式 内容 メッセージのみ ローカライズ済みの引き止め文言 メッセージ + 画像 テキストメッセージに Asset Library 等から設定した画像を添えて訴求 メッセージ + オファー 割引や無料期間付きなどのオファーを提示する ユーザーがオファーの対象である場合、オファーの表示が画像を置き換えます。「解約する前に、3ヶ月無料で続けられるオファーがあります」のような画面を、Apple の解約フローの中に出せるわけです。この画面は App Store 側が描画するもので、公式ドキュメントによれば iOS 15.1 以上で利用できます。アプリの最低対応バージョンと関係なく届くのは、地味に嬉しいところです。 オファーが引き換えられたかどうかはサーバー側で確認できます。署名付きトランザクションに新しいオファー種別が入ります。 { " offerType ": 5 , " offerIdentifier ": " Yoga_2026_cancel_free_3m ", " offerDiscountType ": " FREE_TRIAL ", " offerPeriod ": " P3M " } offerType はトランザクションに「どの種類のオファーが絡んだか」を刻むフィールドで、これまで4種類あったものに追加で Retention Offer が加わった形です。 offerType 種類 1 お試し 2 プロモーション 3 オファーコード 4 再獲得 5 Retention Offer メッセージを用意する方法は2つある ここまでが主に「解約確認画面に何が出るか」の話です。次は、その表示をアプリ側がどう用意するかです。 方法は2つあって、手軽さが大きく違います。App Store Connect で設定するだけの方法と、自前のサーバーを立てて顧客ごとにリアルタイムで出し分ける方法です。順に見ていきます。 方法1: App Store Connect 側での設定 App Store Connect 上でメッセージ・画像・オファーを設定し、対象のサブスクリプションにマッピングするだけです。自前のサーバー実装は不要で、Apple 側が表示を担います。 流れはこうです。 ローカライズ済みのメッセージ文言を作る 任意で Asset Library の画像、Retention Offer を添える 1つ以上のサブスクリプションにマップする Sandbox 環境でテストして公開 方法2: リアルタイム API 型(ユーザーごとの出し分け) 方法1の弱点は、全員に同じものしか出せないことです。 新しく発表された Retention Messaging API を使うと、「誰に・何を出すか」を自社のデータで決められるようになります。 誤解しやすい点を先に書いておくと、 解約の理由そのものが Apple から届くわけではありません 。リクエストに入っているのは「誰の契約か( originalTransactionId )」までです。ただ、この ID で自社のユーザーデータを引けば、手持ちの情報が使えます。例えば、購読してからどれぐらいか、最後にアプリを開いたのはいつか、月額プランか年額プランか、過去にオファーで引き止めたことがあるかなどがあるでしょう。 理由そのものは分からなくても、こうしたデータから仮説は立てられます。 出し分けのロジックが自前のサーバーにあることで A/B テストでの検証も可能になるはずなので、その仮説を検証することもできそうです。 次に、仕組みを見ていきます。 Retention Messaging API を使うと、解約操作が起きたまさにその瞬間に、App Store から自前のサーバーへ問い合わせが来ます。 // App Store からのリクエスト { " originalTransactionId ": " 123456789 ", " appAppleId ": 6745974591 , " productId ": " Yoga_summer_2026 ", " userLocale ": " en-US ", " requestIdentifier ": " c03248af-dd76-4e9b-9c1e-4489cd19a768 ", " environment ": " Production ", " signedDate ": 1780920000000 } これに対して、 このユーザーに何を出すか を返します。返せる応答は3種類です。 ① メッセージ { " message ": { " messageIdentifier ": " 551ee7c0-... " } } ② プラン切替の提案(alternateProduct) { " alternateProduct ": { " messageIdentifier ": " ed7f25fc-... ", " productId ": " Yoga_summer_2026_annual " } } 同じサブスクリプショングループ内の別プランへの乗り換えを提案できます。「月額×12ヶ月コミット」の新プランタイプとも連動していて、たとえば年額プランを解約しかけた人に「月額の12ヶ月コミットなら続けやすいですよ」という導線が作れたりしそうです。 ③ プロモーショナルオファー { " promotionalOffer ": { " messageIdentifier ": " 80135e2b-... ", " promotionalOfferSignatureV2 ": " eyJhbGciOiJFUzI… " } } プロモーショナルオファーは、開発者が特定のユーザーだけに提供できる限定オファーです。 対象ユーザー以外には利用されないように、開発者のサーバーが秘密鍵を使って「このユーザーに、このオファーを適用してよい」という署名を発行します。アプリはこの署名を使って、オファーが正しく発行されたものかを確認します。 promotionalOfferSignatureV2 は、このデジタルな許可証を標準的な形式である JWS(JSON Web Signature) で表現する新しい仕様です。 ところで、ここまでの応答例が messageIdentifier という ID しか返していないことに気づいたでしょうか。メッセージの文言や画像の実体は、あらかじめ Apple に登録しておく設計になっています。解約フローの真っ最中に文言ごと送るのではなく、実体は事前登録しておいて、その場では「どれを出すか」を ID で選ぶだけです。 この事前登録を担うのが、管理用のエンドポイント群です。メッセージや画像の登録のほか、リアルタイム問い合わせの受け口 URL の設定、性能テストの実行もここで行います。 POST /messages // メッセージ登録 GET /messages // 登録済み一覧 DELETE /messages/{messageId} // 削除 POST /images // 画像登録 GET /images DELETE /images/{imageId} POST /defaultMessages // デフォルトメッセージ設定 GET /defaultMessages/{productId}/{locale} DELETE /defaultMessages/{productId}/{locale} POST /realtimeUrl // 受け口URLの設定 GET /realtimeUrl DELETE /realtimeUrl POST /performanceTests // 性能テスト GET /performanceTests/{testId} フォールバックは段階的 リアルタイム応答が使えない・不正な場合は App Store Connect で設定されたものに、それも無ければ API で設定したデフォルトメッセージにフォールバックされます。 また、Sandbox には自社サーバーの応答性能を測るためのテスト用エンドポイントが用意されています。解約フローの中で同期的に呼ばれる API なので、応答が遅ければ体験を壊します。本番前にここで確認しておく、という建て付けだと理解しています。 まとめ Retention Messaging は、解約確認画面という最後の接点に初めて介入できる機能 用意する方法は2つ。サーバー不要の App Store Connect 設定型と、ユーザーごとに出し分けるリアルタイム API。フォールバックも整理されている 返せるのはメッセージ、プラン切替提案、プロモオファーの3種 メッセージや画像の実体は事前登録しておき、リアルタイム応答では ID で選ぶだけ。Sandbox には応答性能のテスト用エンドポイントも用意されている 解約はこれまで、起きてから初めて知るものでしたが、今回からは解約を思いとどまってもらうための施策を、解約のタイミングに合わせて出せるようになります。サブスクリプションを運営しているチームは、サービス提供が始まる前に、どんなメッセージを出すか、どんなオファーを用意するかを検討しておくとよさそうです。
Cloudflare Walletsが使う決済プロトコル x402 を理解する 目次 はじめに x402が対象とする課題 プロトコルフロー ② 402 Payment Required と PaymentRequirements scheme が取る値 — exact と upto ③ authorization への署名 ④ PaymentPayload の送信 ⑤ verify / settle と facilitator 決済の確定と不可逆性 HTTPを利用する構成のメリット 支払ってよいかの判断は仕様の範囲外 まとめ 参考 はじめに こんにちは、開発本部開発1部の 赤川 です。食事管理アプリ ヘルシカ の開発をしています。 ダイエット・食事管理・体重管理・カロリー計算 - ヘルシカ every, Inc. ヘルスケア/フィットネス 無料 2026年8月4日、Cloudflareが Cloudflare Wallets を発表しました。これは、Web上でサービスの購入や入金を行うためのウォレットで、Cloudflareのアカウントに紐づきます。現時点ではウォレットの識別子の予約のみが提供されており、僕もとりあえず予約してみました。 Cloudflare Wallet Tagを予約した Cloudflare Walletsでは、アカウントが持つWalletから、エージェントが使うVirtual Walletに支出権限を委任できます。エージェントはVirtual Walletを使って、API、MCPツール、コンテンツなどを購入できます。 この購入のやり取りには、 x402 という決済プロトコルが使われています。x402は、HTTPのリクエストとレスポンスの中だけで完結し、支払いが必要であることを示すのに、HTTPステータスコードの 402 Payment Required 1 を使っています。 本記事ではこの x402 について、ドキュメントをあたりながらまとめてみました。 x402が対象とする課題 例えば有料APIを利用しようと思ったら、我々は一般に次の手順を踏みます。 サービスにサインアップする クレジットカードなどの決済手段を登録する APIキーを発行する そのキーをリクエストに付けて呼ぶ 月末にまとめて請求される 認証と課金がフローとして完全に分離しており、いずれも事前のサインアップを前提としています。 人間が利用する分には何の問題もないですが、クライアントがAIエージェントである場合、この前提が制約になります。手順1〜3のうち、サインアップページは人間向けのUIとして作られており、決済手段の登録も人間の承認が必要です。エージェントは事前登録された識別子も支払い手段も持たないため、この3手順の実行には人間の介在が必要になります。 x402は、事前の登録手続きなしに支払いを成立させる方式を定義しています。 プロトコルフロー プロトコルのフローは次のとおりです。この章の図や記述は、 x402 Specification v2 と Cloudflare の x402 ドキュメント に基づいており、執筆時点で最新のv2について書いています。 x402のプロトコルフロー このフローには、アカウントやセッション、事前に共有したAPIキーなどは含まれません。ClientとServerはこのリクエストで初めて通信し、支払いが成立します。 ※ 実際の支払いは、ブロックチェーン上でトークンを送ることで成立します。USDCなどのステーブルコイン(米ドルなどの法定通貨に価値を連動させたトークン)が使われます。 ② 402 Payment Required と PaymentRequirements ①のリクエストに対して、Serverは②で 402 Payment Required を返します。支払いの条件は PAYMENT-REQUIRED ヘッダに、Base64エンコードされたJSONとして載ります。 このJSONの accepts フィールドに、支払いの条件が配列で入ります。要素1つが「この条件で払ってくれれば、このリソースを渡すよ」という1件分の提示にあたり、これを PaymentRequirements と呼びます。配列なのでServerは条件の異なる複数の選択肢を並べられ、Clientはその中から1つを選びます。 PaymentRequirements は次のフィールドから構成されます。 フィールド 内容 scheme 支払いスキーム( exact / upto ) network 対象ブロックチェーン(Base、Ethereum、Solana など) amount 金額 asset トークン種別(USDC など) payTo 支払いの宛先アドレス maxTimeoutSeconds タイムアウト scheme が取る値 — exact と upto scheme フィールドは支払い方式を指定します。 Cloudflare のドキュメント に記載されているのは次の2つです。 exact : 固定額の送金です。EVM(Ethereum Virtual Machine) 2 上では USDC を payTo のアドレスに送金します。②の時点で金額が確定しているケースに対応します。 upto : 上限額を先に承認し、実際の課金額を決済時に確定します。LLMの推論APIのように、実行するまでトークン数が確定せず、金額が事後に決まるケースに対応します。 ③ authorization への署名 Clientは、 accepts の中から採用する PaymentRequirements を1つ選び、その内容を authorization オブジェクトに反映して署名します。authorization の形は、 scheme とチェーンの種類の組み合わせで決まり、EVM上で exact を使う場合は次のフィールドを持ちます。 フィールド 内容 from 送金元アドレス to 宛先アドレス value 金額 validAfter / validBefore 有効期間 nonce 一度きりの値 署名は自身の秘密鍵で行います。署名が保証するのは、その鍵の保有者にしか生成できないこと、署名対象が1バイトでも変われば検証に失敗すること、の2点です。 nonce は一度きりの値であり、 validBefore で有効期間が区切られるため、同じ署名を他の支払いに再利用することはできません。 ④ PaymentPayload の送信 署名と authorization は PaymentPayload にまとめられ、 PAYMENT-SIGNATURE ヘッダに載って、同じURLに再送されます。 PaymentPayload は次の構造です。 { " x402Version ": 2 , " resource ": { ... } , " accepted ": { /* ②の accepts から選んだ PaymentRequirements */ } , " payload ": { " signature ": " ... ", " authorization ": { " from ": " ... ", " to ": " ... ", " value ": " ... ", " nonce ": " ... " } } } ⑤ verify / settle と facilitator ⑤でServerが行う検証と決済は、facilitator に委譲できます。facilitator は、支払いの検証とブロックチェーンへの送信を担うサービスで、次の2つのエンドポイントを提供します。 POST /verify : 支払いペイロードが有効かを、ブロックチェーン上で送金を実行せずに検証する POST /settle : 検証済みの支払いをブロックチェーンに送信し、送金を確定させる facilitator の利用は必須ではありませんが、ブロックチェーンとのやり取りを抽象化できるため、Cloudflareのドキュメントでは推奨されています。 Clientは facilitator と直接通信せず、ServerとHTTPで通信します。⑤の内側を展開すると、次のようになります。 facilitator を含む⑤の内側 決済が確定すると、Serverは⑥でリソースを返します。決済の確認は PAYMENT-RESPONSE ヘッダに載ります。 決済の確定と不可逆性 x402の決済は、1回の送金で確定します。カード決済のように、支払いを承認する段階と実際に資金が動く段階が分かれることはありません。これにより、リソース1件ごとのような少額の支払いでも短時間で決済できますが、同時に次の制約が生じます。 決済を取り消せない プロトコルとして返金手段を定義していない 誤送金や詐取による送金があった場合、プロトコルの範囲では資金は戻りません。返金や救済を実装する場合は、アプリケーション層で別途定義することになります。 HTTPを利用する構成のメリット x402は決済専用のプロトコルを新設せず、支払いのやり取りをHTTPのリクエスト/レスポンスサイクルの中で表現します。そのためクライアントを選びません。エージェント、curl、サーバ間通信のいずれも対象になり、SDKも必須ではありません。 402応答は特定のURLに対する支払い要求なので、値付けの単位もURLになります。エンドポイントごと、MCPツールごとに価格を返せます。 支払ってよいかの判断は仕様の範囲外 x402の仕様が定義しているのは、3つのHTTPヘッダ、 PaymentRequirements と PaymentPayload の構造、facilitator の verify / settle の手順、および scheme です。「どのような条件で支払いを承認するか」は定義されていません。仕様上、正しく署名されたペイロードは正当な支払いとして処理されます。 Cloudflare Walletsは、この判断をウォレット側で縛る仕組みを持っています。エージェントに渡すVirtual Walletには、使ってよい金額の枠(allowance)、支払い先の許可リスト(allow list)、1回あたりの上限額(maximum transaction size)を設定できます。 これらはウォレット側の設定なので、エージェントが何をしようとしても、枠を超える送金や、許可リストにない相手への送金は成立しません。x402が定義していない「支払ってよいか」の判断を、エージェント任せにせず、ウォレットの設定として持たせている構成です。 まとめ x402の仕様を整理すると、次のようになります。 決済専用のプロトコルを新設せず、HTTPのリクエスト/レスポンスサイクルで支払いを表現する 事前のアカウントもAPIキーも使わず、402応答とその再送だけで支払いが成立する scheme として exact (固定額)と upto (上限額の事前承認)を定義する 検証と決済は facilitator に委譲できる。利用は必須ではない 決済はブロックチェーン上で確定し、プロトコルとして取り消し手段を持たない 支払ってよいかの判断は仕様の範囲外で、クライアント側に委ねられる 最後までお読みいただきありがとうございました。 参考 Announcing Cloudflare Wallets: The programmable wallet for the agentic Internet - The Cloudflare Blog (2026年8月4日) x402 - Cloudflare Agents docs x402 Specification v2 - coinbase/x402 RFC 9110: HTTP Semantics - 15.5.3. 402 Payment Required 現行のHTTP仕様である RFC 9110 では "The 402 (Payment Required) status code is reserved for future use." と記述されています。402が標準として用途を定義されてこなかったことを、僕は今回の記事を書くまで知りませんでした。 ↩ Ethereum が採用しているブロックチェーンの実行環境です。これを採用するチェーンは多く、Cloudflareのドキュメントも EVM に対応するチェーンとして Base、Ethereum、Polygon などを挙げています。 ↩
はじめに こんにちは、株式会社エブリーのデリッシュキッチンにて7月から約1か月間エンジニアとしてインターンシップに参加していました、亀井と申します。 この記事では、インターン中に取り組んだことの中心である「Databricks LakebaseへのOAuth M2M認証の導入」と、その先でデータを読むための「GoのPostgreSQL向けライブラリ選定」の2つについて、どう実装・検証・調査したのかを書きたいと思います。 インターンで取り組んだこと インターンでは、Go言語で書かれたデリッシュキッチンのAPIサーバーを主な対象として、次の4つに取り組みました。 デリッシュAIの表示順ソートのロジックの実装 Databricks LakebaseへのOAuth M2M認証の実装 GoのPostgreSQL向けデータアクセスライブラリ(ORM)の比較調査 M2M認証を動かすための環境変数・ECSタスク定義の追加 この記事では主に2と3について書きます。 開発の背景 デリッシュキッチンのサーバーから、Databricks上の新しいリソースに接続したいという需要が生まれていました。接続先はLakebase Autoscaling PostgresというDatabricksが提供するフルマネージドなPostgreSQLで、projects → branches → endpointsの3階層でリソースを管理する新世代のサービスです。 Databricks本体は分析寄りで、アプリが求める個別レコードを低レイテンシで引く用途には向きません。そこで、Databricksでテーブルを一元管理するカタログ機能であるUnity Catalog側のテーブルをsourceとしてLakebaseのPostgresに自動同期し(synced table)、アプリからは読み取り専用のPostgresテーブルとして高速に読む、という構成を取ります。 問題は認証です。既存のDatabricks接続はPAT(Personal Access Token)という長命トークンで認証していました。しかしPATは失効管理が手動で、そもそもLakebaseのPostgres認証(OAuth role)は「発行から60分で失効するトークン」を前提としており、PATでは接続できません。静的なパスワードで認証するroleも作れますが、DatabricksのID管理に紐づかない長命の静的クレデンシャルになるため、漏洩時のリスクを考えると避けたいところです。 Databricksはサービスプリンシパルの認可にはOAuth 2.0を推奨しており、ここではOAuth M2M認証、すなわちサービスプリンシパルとclient credentialsフローの組み合わせを実装しました。用語を整理すると: サービスプリンシパル(SP): 人間ではなくアプリ自身に与えるIDアカウント。人に紐づけると退職や異動等で処理が止まり、権限も広くなりがちなので、機械用のアカウントを別に立てます client credentialsフロー: ユーザーの同意画面を介さず、アプリがClient IDとClient Secretを認可サーバーに提示してアクセストークンを得るOAuth 2.0のフロー つまり、サーバーが自分自身のIDとシークレットでトークンをもらい、そのトークンでAPIを呼ぶ仕組みです。 M2M認証の実装と検証 設計方針 設計方針の要点は次の3つです。 環境分離はbranchで行う:Lakebaseのbranchはコピーオンライト(CoW)でデータを分岐でき、 production (親)と development (子)で本番と開発を分離します。SPは環境別に2本立て、dev用SPのOAuth roleを production branchに作らないことが、dev環境から本番データへの到達を防ぐ唯一の境界になります。roleの状態はbranch間で独立している、というのが公式の仕様です。 認証専用のパッケージは作らない:トークンの発行・キャッシュ・更新はすべてSDK内部の責務で、自前コードに共通化すべきロジックが発生しないためです。 環境変数はSDKの標準名( DATABRICKS_HOST / DATABRICKS_CLIENT_ID / DATABRICKS_CLIENT_SECRET )を使う:databricks-sdk-goの統一認証チェーンは、これらの環境変数があればSPとしてM2M認証し、なければ開発者個人のCLIプロファイルに自動でフォールバックします。本番はSP・ローカルは個人認証という切り替えが分岐コードゼロで手に入り、SPのシークレットを開発者の端末に配らずに済みます。 この設計方針の下で実装を進めてきました。 実装 実装は公式ドキュメントのGoサンプルをほぼそのまま踏襲する形式になりました。核になるのは、GoのPostgreSQL接続ライブラリpgxが提供する接続プールの BeforeConnect フックです。 BeforeConnect は、プールが新しい接続を張る直前に毎回呼ばれる関数で、ここで接続に使う設定を書き換えられます。 w, err := databricks.NewWorkspaceClient(&databricks.Config{}) // 空のConfigでSDKの統一認証チェーンに委ねる: // DATABRICKS_*環境変数(本番ECS)→ CLIプロファイル(ローカル) // Lakebaseは全接続にSSL/TLSを必須とするため、sslmodeはコード側で固定する poolCfg, err := pgxpool.ParseConfig( "sslmode=require" ) // credentialは発行から60分で失効する。BeforeConnectは新規接続にしか // 効かないため、失効前に接続自体を作り直させる(公式サンプル準拠)。 // Jitterで寿命をばらつかせ、一斉失効による再接続の集中を防ぐ poolCfg.MaxConnLifetime = 45 * time.Minute poolCfg.MaxConnLifetimeJitter = 5 * time.Minute poolCfg.BeforeConnect = func (ctx context.Context, connCfg *pgx.ConnConfig) error { cred, err := w.Postgres.GenerateDatabaseCredential(ctx, postgres.GenerateDatabaseCredentialRequest{ Endpoint: endpointName, // projects/{project}/branches/{branch}/endpoints/{endpoint} }) if err != nil { return err } connCfg.Password = cred.Token // 接続確立ごとに新鮮なトークン return nil } ポイントは3つあります。 Database Credentialはキャッシュしない:60分で失効するトークンをキャッシュすると、失効間際のトークンを掴む事故が起きます。毎回発行するとAPIコールが増えそうに見えますが、発行が走るのは接続プールが新規接続を作るときだけなので、実際の頻度は低く抑えられます。 接続寿命をトークンのTTL(有効期間)未満に制限する: BeforeConnect が効くのは新規接続だけで、確立済みの接続にあとから新しいトークンを渡す手段はありません。接続を作りっぱなしにすると、認証に使ったトークンが失効した接続がプールに残り続けます。そこで公式サンプルと同じく MaxConnLifetime = 45 * time.Minute で、失効前に接続自体を作り直させます。さらに、プールの接続はデプロイ直後などにまとまって作られるため、寿命が同じだと作り直しのタイミングも一点に集中します。これをずらすのが、寿命に上乗せするランダムな幅であるジッターです。ジッターを足しても接続寿命は最長50分で、TTLの60分を確実に下回ります。 接続先のdatabaseはコードで切り替えられるようにする:これはレビュー指摘で直した点です。当初は接続先のdatabase名を環境変数 PGDATABASE 任せにしており、接続先が1つに固定されていました。しかし今後は複数のdatabaseを読む可能性があります。そこでdatabase名を型として定義し、database単位に接続プールを分けて、接続文字列の dbname で明示指定する形に改めました。pgxでは接続文字列の設定が環境変数より優先されるためです。endpointと認証credentialはdatabaseを問わず共通なので、 BeforeConnect の仕組みは全プールでそのまま共有できます。 環境変数とタスク定義 このコードを本番で動かすには、AWSのコンテナ実行サービスであるECSのタスク定義への環境変数の追加が必要です。 DATABRICKS_* の3変数に加えてPostgres接続用の PG* 変数を、dev / prdそれぞれのタスク定義に追加しました。ひとつ注意が要るのは PGUSER で、本番ではSPのclient ID、ローカルでは開発者個人のIDと、環境で値の種類そのものが違うため、ハードコードせず環境ごとに注入します。 検証 作った接続コードが「本当に意図通り動くのか」を示すために、疎通確認用のCLIをリポジトリ内に用意しました。確認したいことが3層に分かれているので、CLIも段階を選べるようにしてあります。 --auth-only : Postgresには触らず、ワークスペースAPIへの認証だけを確認します フラグなし: credentialを発行してPostgresにログインし、 SELECT 1 を実行します --count 2 --interval 65m : 接続寿命を跨いで再接続できるかを確認します なぜ分けるかというと、1と2は通信経路も認可の仕組みも別物だからです。段階1はワークスペースのAPIに届くかの確認で、認可は、ワークスペースの機能を使う権利であるエンタイトルメントが担います。段階2は発行されたcredentialでPostgresにログインする確認で、認可はbranch単位のOAuth roleが担います。一気に確認すると、失敗したときにどの層が原因か切り分けられません。 データを読むライブラリをどう選ぶか 認証が通ったら、次はその接続でデータを読む実装です。ここで「GoからPostgreSQLを扱うのに何を使うか」というライブラリの比較を行いました。 結論から書くと、第一候補はBob、第二候補はsqlcです。理由は、クエリの書きやすさ、型の安全性、そして上で作った既存の *pgxpool.Pool をそのまま使えることです。 選定の前提は次の3つで、それぞれが結論の理由に対応しています。 対象はsynced tableです。スキーマはUnity Catalog側が所有し、アプリからは ALTER を発行しません:主キーのないテーブルを扱えること、スキーマ変更に気付けることが効いてきます 接続は、 BeforeConnect による認証をバイパスしないよう既存の *pgxpool.Pool を使います:プールを直接扱えるライブラリが有利です 読み取るテーブルは今後増えていきます:1テーブルなら手書きでも書けますが、増えるほど書きやすさと型安全性が効いてきます 選定の理由 選定の過程では生成AIを用いてORMを列挙させ、上記の観点から7つのライブラリを選出しました。利点と欠点で比較をし、参考程度にDocker上のPostgreSQLで実測を行いました。その後議論を行い、以下のORMを選定しました。 Bob: 既存の *pgxpool.Pool を公式に直接扱えます。ビルダでクエリを書きやすく、コード生成で型安全性を足せます。主キーのないテーブルでも生成が通り、読み取り専用のモデルになるためsynced tableの性質と噛み合います。 sqlc: SQLを書いてコードを生成する型で、列名や型の誤りを生成時に検出できます。スキーマをUnity Catalog側が所有する今回の状況では、上流のスキーマ変更を再生成時のコンパイルエラーとして捕まえられる固有の強みがあります。 他の候補を見送った理由も簡単に説明します。GORMはフルORMの利点が今回の要件では効きません。entはジェネレータが主キー id を必須とするため、主キーのないsynced tableへの全件取得が失敗します。scanyは短く書けますが開発が止まっています。手書きのpgxは型が go build 時点で固定される書き方ですが、テーブルが増えるほど手書きの Scan が積み上がります。 まとめ インターンを通して、実際にユーザーに使われているデリッシュキッチンのサーバーに触れられたのは、とても貴重な経験でした。チームの設計方針や、既存のコードを読み解きながら実装を行う経験は開発現場だからこそ得られるものだと思います。インターンシップで学んだことも活かしつつ、今後もさまざまな技術に触れながら、エンジニアとして成長していきます。 最後に、1か月間サポートしてくださったデリッシュキッチンの皆様、本当にありがとうございました。 参考 Connect external app to Lakebase using SDK OAuth machine-to-machine (M2M) authentication Branching Manage Postgres roles Connection security databricks-sdk-go Bob: pgx driver — NewPool ent: Fields — ID
はじめに こんにちは。リテールハブ小売アプリ開発チームの池です。 小売アプリチームでは、昨年から CodeRabbit を利用して PR レビューを行っています。日々活用してはいるものの、アップデートの内容を追いきれていませんでした。そこで本記事では、2026 年にアップデートされた内容を整理し、気になった機能をピックアップして紹介します。 なお、本記事の内容は 2026 年 8 月 12 日時点の情報をもとにしています。CodeRabbit はアップデートの頻度が高いため、機能の挙動やプラン要件は変わっている可能性があります。最新の情報は公式ドキュメントをご確認ください。 また、私たちのチームでは Pro プランを利用しているため、Pro プランで利用できる範囲を中心に、Pro+ プラン限定の機能はその旨を明記しながら紹介します。 2026 年アップデートの全体像 CodeRabbit の Changelog および 公式ドキュメント をもとにアップデートの全体像を整理します。2026 年 1〜8 月の changelog は約 100 件あり、主なアップデート内容を独自に抜粋して整理すると次のようになります。 カテゴライズ 主なアップデート(月) 概要 レビューUX ・Multi-Repo Analysis(2 月、6 月拡充) ・Review auto-pause(2 月) ・Slop PR 検出(3 月) ・Quiet プロファイル(7 月) マルチリポ分析の登場とレビューノイズの削減 Finishing Touches ・Autofix(2 月) ・Simplify(3 月) ・Resolve Merge Conflicts(3 月) ・Custom Recipes(3 月) ・Fix CI(7 月) レビュー後の修正作業をエージェントに委譲 CLI・コーディングエージェント連携 ・Claude Code Plugin(2 月) ・CodeRabbit Skills(2 月) ・CLI --agent モード(3 月) ・Codex plugin(4 月) Claude Code などのコーディングエージェントにレビューを統合 静的解析ツールの追加 ・Trivy / TFLint(2 月) ・zizmor(5 月) ・oasdiff(6 月) ・ast-grep の Dart 対応(6 月) ・e18e(7 月) 対応言語・領域の拡大(今年だけで 10 種超) Analytics・ダッシュボード ・Learnings ダッシュボード(2 月) ・ダッシュボード再編(3 月) ・利用メトリクス(6 月) ・レートリミット可視化(7 月) 運用状況の可視化が大幅に強化 外部ツール連携・CodeRabbit Agent ・Slack 向け Agent(4 月) ・メッセージトリガー自動化(4 月) ・Post-Merge Actions(7 月) ・Agent Skills(7 月) Slack を入口にした汎用エージェントと SaaS 接続の拡大 Plan ・Issue Planner(2 月) ・VS Code での Plan(6 月) Issue からの実装計画生成 セキュリティ ・Betterleaks(3 月) ・Security Agent(7 月) ・Secrets 検証ステータス(7 月) ・Git 履歴スキャン(7 月) PR レビューからコードベース全体のスキャンへ ナレッジ・Learnings ・承認フロー(6 月) ・Learnings API(6 月) ・Code Guidelines 管理 UI(6 月) 学習内容の可視化と統制 組織管理・ガバナンス ・Custom Roles(2 月) ・Audit Logs(3 月) ・Pro+ プラン導入(4 月) ・Global Overrides(4 月) エンタープライズ向け統制と料金体系の再編 Change Stack ・semantic diff / Code Peek(5 月) ・Change Stack 各プラットフォーム展開(5〜7 月) ・アプリ内レビュー会話(7 月) 意味的に構造化された PR レビュー専用UI と各プラットフォーム展開 ここからは、この中から気になった 5 つのテーマをピックアップして紹介します。 ① レビューUX 2 月: Multi-Repo Analysis が登場 3 月: Pro プランでリンク可能リポジトリ数が 1→2 に増加 6 月: レビュー対象 ref の固定(Multi-Repo Ref Selection)、自動リポジトリリンクとその管理 UI(Pro+ プラン) Multi-Repo Analysis は、PR のレビュー時に関連リポジトリのコードも合わせて参照して分析する機能です。単一リポジトリの diff だけでは見つけられない、リポジトリをまたぐ問題(API の破壊的変更、型の不一致、依存のドリフトなど)を検出できます。たとえばモバイルアプリと API サーバーが別リポジトリに分かれている場合、サーバー側の API 変更に対して「アプリ側の呼び出しが壊れないか」という観点をレビューに加えられます。 Pro プランでは自動リンク機能が使えないので、管理画面から手動で関連するリポジトリを設定しないと本機能は適用されないようです。設定方法を見ていきます。 設定方法 リンク設定は、管理画面の Review > Repositories > Settings > Knowledge base にあります。手動でのリンクと自動リンクの 2 通りがあります。なお、リンクできるリポジトリ数について、changelog には 3 月に Pro プランで 1→2 に増加したと記載がありますが、執筆時点の管理画面では 1 リポジトリまででした。 手動でリンクする リポジトリ設定はデフォルトで Organization 設定を継承しており、この状態ではリポジトリ単位のカスタマイズができません。Review > Repositories > Settings > General にある「Use Organization Settings」のトグルを一度クリックすると、設定の継承は on のまま、リポジトリ単位で編集できるようになります。 Linked repositories でリンクするリポジトリを選択し、Instruction にリポジトリ間の関係を記述できます。 保存すると Linked repositories の一覧に表示され、リンク設定は完了です。 自動リンク(Automatic repository linking) Automatic repository linking を有効にすると、Organization 内の関連するリポジトリが自動的に検出されリンクされます。なお、自動リンクは Pro+ プラン以上の機能です。 Multi-Repo Analysis のレビューでの見え方 Multi-Repo Analysis の結果は、PR Walkthrough の Review details > Additional context used に、リンクされたリポジトリ名ごとにグループ化されて表示されます。自動リンクされたリポジトリが含まれる場合は、Review info セクションに CodeRabbit が参照したリポジトリの一覧が表示され、それぞれに手動リンクか自動検出かのラベルが付きます。また、関連がある場合はインラインのレビューコメントやコメント返信にも指摘として現れます。 なお、Review details セクションを表示するには、 .coderabbit.yaml で review_details を有効にしておく必要があります。 # .coderabbit.yaml reviews : review_details : true ② Finishing Touches Finishing Touches は、レビュー後の仕上げ作業(指摘の修正、docstring や単体テストの作成など)を、PR コメントや PR Walkthrough のチェックボックスから CodeRabbit のエージェントに任せられる機能群です。 2 月: Autofix(未解決の指摘への修正を自動適用)、Chat code editing(Early Access) 3 月: Simplify、Resolve Merge Conflicts、Custom Recipes 7 月: Fix CI(CI 失敗の調査から修正・PR 作成までを行う) Finishing Touches の一覧 執筆時点で利用できる Finishing Touches は以下の 7 種類です。一部は Pro+ プラン限定となっています。 Autofix(指摘の自動修正) Docstrings(docstring の生成) Fix CI(CI 失敗の修正、Pro+ プラン) Resolve Merge Conflicts(コンフリクトの解決、Pro+ プラン) Unit Tests(単体テストの生成、Pro+ プラン) Simplify(コードの簡素化、Pro+ プラン) Custom Recipes(カスタム定義による修正、Pro+ プラン) トリガー方法は 2 種類あります。 PR コメント: @coderabbitai generate docstrings のようなコメントを投稿する チェックボックス: PR Walkthrough のコメント上のチェックボックスにチェックを入れる Autofix Autofix は、PR に残っている未解決のレビュー指摘を対象に、サンドボックス内で修正を作成する機能です。修正の反映先は、現在のブランチへの直接コミットか、stacked PR かを選べます。 以下のようにコメントすることで利用できます。 @coderabbitai autofix # 修正を現在のブランチにコミット @coderabbitai autofix stacked pr # 修正を stacked PR として作成 Autofix 機能はデフォルトで有効になっています。リポジトリ単位で無効にしたい場合は、 .coderabbit.yaml で設定します。 # .coderabbit.yaml reviews : finishing_touches : autofix : enabled : false # Autofix を無効化 実際に試したところ、人間のレビュアーのコメントに対して @coderabbitai autofix stacked pr を実行しても、以下のようにスキップされました。 Autofix の対象は、CodeRabbit 自身が投稿した修正手順つきの未解決レビューコメントのみで、人によるレビューコメントの修正は任せられないようです。 次に、CodeRabbit の修正手順つき未解決コメントが残っている PR で実行したところ、今回は受け付けられました。特定の 1 件の指摘スレッドへの返信として実行しましたが、PR 上の未解決指摘 17 件すべてが修正対象になりました。コメントを投稿した場所に関係なく、PR 単位で一括適用される挙動のようです。 実行から約 6 分半で、12 ファイルの修正を含む stacked PR が作成されました。 ③ CLI・コーディングエージェント連携 CodeRabbit CLI は、ターミナルから手元のコード変更に対して CodeRabbit のレビューを実行できるツールです。PR を作成する前に、ローカルで指摘の検出と修正を済ませられます。2026 年は、この CLI を土台にした Claude Code などのコーディングエージェントとの統合が進みました。 2 月: Claude Code Plugin 対応、CodeRabbit Skills(open agent skills 標準) 3 月: CLI --agent モード(構造化 JSON 出力)、CLI 用 Usage-based アドオン(従量課金オプション) 4 月: CLI ブラウザ完結のサインイン、Codex plugin 対応 5〜8 月: coderabbit doctor 、 --light モード、レビュー信頼性の改善 CLI は無料プランでも利用できます(基本的な静的解析、1 時間あたり 3 回まで)。Pro プランにすることで、組織の Learnings を活用した強化レビューになり、レートリミットは 1 時間あたり 5 回に引き上げられます。 コーディングエージェントに CLI を統合するメリット CodeRabbit は、Claude Code などのコーディングエージェントと CLI を組み合わせる意義を、次のような役割分担として説明しています。 専門家による問題検出 : 一般的なリンターでは見逃しやすい競合状態・メモリリーク・論理エラーを CodeRabbit が検出する AI を活用した修正 : Claude Code が CodeRabbit の分析結果に基づいて修正を実装する コンテキストの保持 : 問題の発生場所・深刻度・推奨される対処法が簡潔に Claude Code へ渡される 継続的なワークフロー : ツールを切り替えることなく、レビュー → 修正 → 反復のループを開発の流れの中で回せる Claude Code への導入方法 導入は「CLI のインストール・認証」と「Claude Code へのプラグイン追加」の 2 段階です。 まず CodeRabbit CLI をインストールします。 curl -fsSL https://cli.coderabbit.ai/install.sh | sh # または brew install coderabbit 次に認証を行います。4 月の CLI v0.4.0 からサインインはブラウザで完結するようになりました。 coderabbit auth login Claude Code 側では、公式プラグインマーケットプレイスに CodeRabbit が用意されているので、選択するだけで使えるようになります。 プラグインの内容 導入すると、Claude Code 内で coderabbit-review コマンドと、code-review・autofix の 2 つのスキルが利用できるようになります。 /coderabbit:coderabbit-review : 手元の変更に対して CodeRabbit のレビューを 1 回実行し、結果を表示するコマンド /coderabbit:code-review : CodeRabbit のレビューをワークフローとして扱うスキル。エージェントがレビューを必要と判断した場面でも自動起動する /coderabbit:autofix : GitHub PR のレビュースレッドにある CodeRabbit の指摘を、変更ごとに承認を挟みながら安全に適用する また、プラグインとは別に、open agent skills 標準に対応した CodeRabbit Skills も提供されています。Claude Code の場合はプラグインの導入でスキルも一緒に入るため追加の作業は不要ですが、Cursor / Codex / Gemini CLI など他のエージェントでも使いたい場合は、以下のコマンドで各エージェントのスキルディレクトリに導入できます。 coderabbit skills code-review と autofix の 2 つのスキルの実体は、ワークフローを記述した Markdown ファイル(SKILL.md)です。それぞれ次の内容がワークフローとして明記されています。 code-review : 「実装 → レビュー → Critical/Warning の修正 → 再レビュー → クリーンになるまで繰り返す」という自律ループ autofix : エージェント自身が指摘の妥当性をローカルコードで検証し、1 件ずつユーザーの承認を取りながら修正を適用する手順 ループが前提として設計されている点、必要な箇所で人の承認を挟んでいる点から、AI を使った開発ワークフローに CodeRabbit を自然に組み込める仕組みだと感じました。 ④ 静的解析ツールの追加 静的解析ツールの追加は活発に行われており、1〜8 月だけで 10 種を超えるツールが追加・更新されています。対応しているツールの一覧は 公式ドキュメントの Tools 一覧 を参照してください。 2 月: Trivy(IaC のセキュリティスキャン)、TFLint(Terraform)、Stylelint(スタイルシート)、TruffleHog(シークレットスキャン)、OpenGrep(Semgrep 互換の静的解析エンジン)など 9 種 3 月: Betterleaks(シークレットスキャナを Gitleaks から Betterleaks へ置き換え。既存の gitleaks 設定キーがそのまま使える) 5 月: zizmor(GitHub Actions ワークフローのセキュリティ解析) 6 月: Infer(C / C++ / Java の null 参照・リソースリーク・並行処理バグ検出)、oasdiff(OpenAPI の破壊的変更検出)、React Doctor(React のセキュリティ・パフォーマンス・アクセシビリティの問題を検出)、ast-grep の対応ファイルタイプ拡大(Dart、TOML、Markdown など) 7 月: e18e ESLint plugin(モダンな代替がある依存や、メンテナンスされていないパッケージを指摘) これらの静的解析ツールは CodeRabbit ではほぼすべてデフォルトで有効になっており、該当するファイルタイプの変更を検出すると自動で実行されます。 ルールの詳細を制御したい場合は、各ツールの設定ファイルを使います。リポジトリに .eslintrc.js や pyproject.toml があれば CodeRabbit はそれを尊重するため、既存の設定をそのまま生かせます。 また、対象領域は、シークレット・IaC・CI 設定といったアプリケーションコードの周辺へと拡大しています。PR レビューの対象が、コードそのものからリポジトリ全体の安全性へ広がっていると感じました。 複数のリポジトリを運用していると、リポジトリごとに静的解析ツールを選定して管理していくのはそれなりのコストになります。ツールの追加・実行・アップデートを CodeRabbit 側が担ってくれることで、この導入・運用コストを下げられるのはメリットの一つだと思いました。 ⑤ Analytics Analytics は、CodeRabbit の管理画面で組織のレビュー活動を可視化するダッシュボード機能です。レビューされた PR の数や指摘への対応状況、レビューにかかった時間、Learnings などナレッジの活用状況といったメトリクスを、リポジトリ・ユーザー・期間で絞り込みながら確認できます。 2 月: Learnings ダッシュボード(KPI カード、利用回数・最終利用日などのメタデータ) 3 月: ダッシュボード再編(Git プラットフォームレビューと IDE/CLI レビューの分離、Knowledge Base / Pre-merge Checks / Reporting などの新ページ) 6 月: Path 指示・Finishing Touches の利用メトリクス 7 月: レートリミット影響の可視化(Usage Rate-Limit Insights)、Explore ページのレコメンデーション 3 月の再編により、Analytics は大きく「PR reviews」と「IDE/CLI reviews」の 2 つに分かれています。それぞれ見ていきます。 PR レビューのメトリクス PR レビュー側は Summary / Quality Metrics / Time Metrics / Knowledge Base / Organization Trends / Pre-merge Checks / Reporting / Data Metrics のページで構成されており、すべては書ききれないほど多くのメトリクスがあります。以下は Summary ページの画面です。 ここでは代表的な項目を挙げます。 レビュー成果 : レビュー済みでマージされた PR 数、投稿コメント数、Acceptance Rate(開発者が指摘に対応した割合)、Reviewer Time Saved(AI 推定のレビュー工数削減時間) 時間系 : レビュー可能になってから「マージ」「最初の人間レビュー」などまでの時間(平均・中央値・P75・P90)と週次トレンド ナレッジ系 : Learnings の作成数・適用率、Path-based Instructions の利用率、MCP サーバー別の PR カバレッジ 運用系 : Pre-merge Checks や Finishing Touches の実行数と結果、レートリミットの影響 これらのメトリクスは、開発プロセスの改善、効果の定量的な説明、CodeRabbit 自体のチューニングなど、幅広く活用できそうです。 IDE/CLI レビューのメトリクス IDE / CLI がチームでどれだけ使われているかを示すメトリクスです。 アクティブユーザー数・レビュー数・レビューコメント数を、IDE 拡張 / CLI の種別ごとに集計 Learnings や Path-based Instructions がローカルレビューに適用された割合 SAST・リンターによるツール検出の内訳(ツール別・重要度別) ユーザー別の明細(拡張の種別、導入日、最終利用日時、レビュー数) 従来見えづらかった PR に到達する前の品質活動を追えるのは、開発フローの改善を測るうえで示唆がありそうです。 おわりに 本記事では、CodeRabbit の 2026 年 1〜8 月のアップデートを整理し、気になった 5 つのテーマをピックアップして紹介しました。 アップデートの内容を追うと、CodeRabbit が PR レビューのツールにとどまらず、開発ライフサイクル全体を支えるエージェントプラットフォームへと広がっていると感じました。 AI によりコードの生成量が増大する中で、いかに効率良く品質を担保するかは、AI 活用における課題の一つだと思います。CodeRabbit を適切に活用することは、この課題を解決する方法の一つになると感じています。 私たちのチームでも、今回整理したアップデート内容を実際の開発フローに組み込みながら、活用を広げていければと思います。 本記事が少しでも参考になれば幸いです。最後まで読んでいただきありがとうございました。
はじめに こんにちは、開発1部でデリッシュキッチンの開発をしている蜜澤です。 現在データ基盤はDatabricksを使用しており、データはUnity Catalog(以下UC)のテーブルです。 UCのテーブルをプロダクトのAPIから配信したいという状況が直近の1年で増えてきました。 例えば、「レコメンド結果をアプリの画面に出したい」「集計したアプリのログデータを分析できる社外向けのダッシュボードツールを作成したい」といった要望があります。 これをアプリ側から読めるようにするには、UCのテーブルのデータをAPIが参照できるDBに複製する必要があります。DatabricksのLakebase(マネージドPostgreSQL)を使えば、これを実現できそうです(Lakebaseは 2026年6月30日に東京リージョンで利用可能になりました )。 ただ、複製の方法はLakebase以外にもあります。本記事では、従来の方法と比較しながらLakebaseで実現するのが良いのかどうかを考えてみます。複製先のDBは、既存のアプリのDB(MySQL互換のRDS)かLakebaseのどちらかになります。 比較する際に考慮したのは以下の4点です。 ダウンタイム(DBのデータが空や一部欠けた状態で読まれる時間)が発生するかどうか アトミック性(一連の更新がすべて反映されるか、まったく反映されないかのどちらかになる) 実装コスト(初期整備や、配信テーブルを増やすたびに必要な実装の量) 金銭的コスト(追加で発生するインフラ費用) 紹介するのは以下の3種類の方法です。 DatabricksからRDSに直接書き込む バックエンドのバッチで取り込む Lakebaseのsynced tableを使用する DatabricksからRDSに直接書き込む 概要 DatabricksからJDBC接続で、アプリのRDSに直接書き込みます(Sparkの df.write.format("jdbc") )。 UC (Delta) --[Spark JDBC writer]--> アプリの RDS --> API アプリ側から見れば、普通のDBのテーブルが1つ増えるだけになります。 メリット 実装コストが小さいです。 バックエンド側の実装はテーブル定義のマイグレーションのみで、Databricks側の実装は書き込み処理を書くだけであり、最小限の実装で済みます。 ただし、これはあくまでアプリケーションコードの話で、DatabricksからRDSにネットワーク的に到達できるようにする設定(VPCピアリングなど)は別途必要です。 デメリット mode("overwrite") で書き込みを行うと、ダウンタイムが発生してしまいます。 mode("overwrite") はテーブルを空にしてから書き直しますが、Sparkはパーティションごとに独立したトランザクションでコミットするため、この一連の処理を1つのトランザクションで囲む方法がありません。結果として、テーブルが空の状態や一部しか入っていない状態が、そのままAPIから読めてしまいます。つまり書き込みが終わるまでの間、データが欠損した状態で配信されるダウンタイムが発生します。 この問題は mode("append") を使えば解決できます。 append は既存行を消さないため、テーブルが空の状態や一部しか入っていない状態が読まれることはありません。 ただし append は追記しかしないため、全件を更新したい場合は使えません。ダウンタイムを発生させずに全件更新したい場合は、本番とは別のテーブルに全件を書き込んでおき、 RENAME TABLE で本番のテーブルと入れ替えることで実現できます。 RENAME TABLE は1つの文に渡した複数テーブルの付け替えをアトミックに実行するため、読み手からは入れ替えの前か後のどちらかの状態しか見えず、空や一部欠けた状態が読まれることはありません。 なお、厳密には入れ替え時にメタデータロックの取得待ちが発生し得ます。長時間のクエリや未コミットのトランザクションが同じテーブルに触れていると、 RENAME TABLE が待たされるだけでなく後続の読み取りクエリもその後ろで待たされるため、入れ替えは長時間のクエリと重ならないタイミングで実行するのが安全です。 この構成では「アプリのDBに対するテーブルの入れ替え(DDL)をDatabricks側から実行する」ことになります。アプリ側がマイグレーションで管理しているスキーマをETLが作り替える形になるため、動きはするものの設計としてはあまり綺麗ではない印象です。 また、外部キー制約がある子テーブルを書き込もうとすると、キーの管理が煩雑になります。参照先の親テーブルがアプリのDBにしか存在しない場合、そのidを確認するためにアプリのDBを見る必要があります。子テーブルのデータを作るには、アプリのDBからidを取得してきて、それと整合するようにDatabricks側で外部キーを振る必要があり、idの管理をDatabricks側で抱え込むことになります。 用途 向いている 検証段階で、まず動くものを最短で作りたい場合 データの全件更新が不要で増分更新のみでよい場合 全件更新時の短時間のダウンタイムを許容できる場合 ダウンタイムを許容できない場合でも、 RENAME TABLE による入れ替えを許容できる場合 向いていない ダウンタイムなしの全件更新が必要で、 RENAME TABLE による入れ替えを許容できない場合 外部キー制約で既存のテーブルと繋がる子テーブルを作成したい場合 バックエンドのバッチで取り込む 概要 DatabricksからUCのテーブルのデータをCSVファイルとしてS3に出力し、それをアプリのバックエンドに実装した取り込みバッチで読み込んでRDSに書き込みます。 Databricks側はS3にファイルを置くところまでを責務とし、そこから先の取り込みはアプリ側が担います。 UC (Delta) --> S3(CSV) --> [アプリ側の取り込みバッチ] --> アプリのRDS --> API メリット 全件更新したい場合でもアトミックな切り替えが可能になります。アプリの通常のコードなので、単一プロセスから単一トランザクションで書けます。実際にはアプリのコードでS3のCSVを読み込んでバルクINSERTを繰り返す形になりますが、構造としては以下の通りです。 BEGIN ; DELETE FROM xxx; INSERT INTO xxx ... ; COMMIT ; DELETE はトランザクショナルなので、確定は COMMIT の1回だけです。読み手は切り替わりの前か後しか見ません。 取り込み時に加工・検証ができます。参照先マスタの実在チェック、正規化、既存データとの整合確認をアプリのドメインロジックで書けます。外部キー制約も張れるので、参照先が消えたときの挙動をDBに任せられます。 デメリット 実装コストが大きくなりがちです。Databricksから直接書き込む方法ではDatabricks側に書き込み処理を数行書くだけで済みますが、この方法では数百行程度とはいえ取り込み処理を追加で実装する必要があります。さらに、以下のような定期実行の仕組みがバックエンド側に整っていない場合は、それらも合わせて実装する必要があります。 スケジューリング : バッチを動かす実行環境の定義と、デプロイパイプラインへの組み込み 資格情報 : S3の読み取り権限とDBの書き込み権限、それぞれのIAMとシークレットの管理 監視とアラート : 取り込みの失敗に気づくための通知の仕組み 冪等性と再実行 : 途中で失敗しても安全にやり直せる作り データ受け渡しのルール : ファイル形式・置き場所・命名の取り決めや、書き込み途中のファイルを読まないための完了通知 ここを整えないと、取り込み処理自体はできているのに手動実行の運用が残ってしまう、ということになりがちです。 また、S3上のファイルとDBの両方にデータのコピーが存在することになるため、ETLからS3へファイルを置く処理が失敗した場合、ETL側の再実行だけでは復旧できず、バックエンドの取り込みバッチも合わせて再実行する必要が出てきます。 用途 向いている 取り込み時に加工・検証・既存マスタとの整合が必要なとき 外部キーでDBの他テーブルと繋がるような場合 向いていない アプリ側に実行基盤が整っておらず、整備する工数を取れない場合 Lakebaseのsynced tableを使用する 概要 DatabricksのLakebase(マネージドPostgreSQL)を使い、UCのテーブルをPostgresのテーブルとして同期します。 UC (Delta) --[synced table]--> Lakebase (Postgres) --> API 同期モードは3つあり、同期のされ方が異なります。 Snapshot : 実行のたびに同期元の全量を取得し、テーブルを丸ごと置き換える。手動・API・スケジュールで起動する Triggered : 初回に全量を取得し、以降は前回実行からの差分だけを適用する。起動方法はSnapshotと同じ Continuous : 初回に全量を取得し、以降はパイプラインが常時実行され、変更をほぼリアルタイムで適用し続ける 参考: Databricksの公式ガイド メリット 全件更新をアトミックに行えます。Snapshotモードの同期は、全量をコピーしたうえでテーブルをアトミックに置き換えるため、空の状態や一部だけ入れ替わった状態が読まれることはありません。Databricksから直接書き込む方法では RENAME TABLE を使って自前で実装する必要があった入れ替えを、プラットフォームの機能として提供してくれています。 また、アプリ側に取り込みバッチを実装する必要がありません。バックエンドのバッチで取り込む方法で課題になった定期実行の仕組みの整備が不要で、スケジューリングも監視もETL側の仕組みに閉じます。 読み取りは通常のPostgresと同じなので、配信のレイテンシもミリ秒オーダーです。配信するテーブルを増やしたい場合もsynced tableを1つ追加するだけでよく、アプリ側は接続の仕組みを共有できます。 デメリット 他の方法と比べて金銭的なコストが大きくなります。ここまでの2つの方法は既存のRDSにデータを入れるため追加のインフラコストがほとんどかかりませんが、この方法ではRDSとは別にLakebaseを新しく持つことになるため、その分の料金が追加で発生します。Autoscalingプランは従量課金で、Computeの料金は1 CU-hourあたり$0.111程度(2026年8月12日時点の 公開価格 )、これに加えてストレージなどの料金がかかります。使っていない時間にコンピュートを落とすscale-to-zeroという機能もありますが、常時リクエストが来るAPI配信ではそもそも落ちる時間がなく、むしろコールドスタートを避けるために最小キャパシティを確保しておく必要があります。 また、接続まわりの作り込みが必要です。認証はOAuthトークンかネイティブPostgresロールパスワード認証(Autoscalingプロジェクトではデフォルト無効)の2方式で、OAuthの場合はトークンの有効期限が1時間のため、新規接続のたびにトークンを取得し直す仕組みをアプリ側に組み込む必要があります。認証方式の詳細は 公式ドキュメント を参照してください。 synced tableは読み取り専用として扱う必要があります。データのソースはUC側であり、Postgres側で直接更新すると同期元との整合が壊れてしまうため、読み取りのみにすることが推奨されています。なお、Lakebase自体は通常のPostgresなので、synced tableとは別に書き込み可能なテーブルを作ること自体は可能です。ただし、アプリのDB(RDS)とは別のデータストアであることに変わりはないため、アプリのマスタテーブルとの外部キー制約は張れません。参照先のマスタが消えた場合の整合性は、ETL側で担保するか配信時のフィルタで守ることになります。 外部依存が増えるため、Lakebaseに障害があったときにアプリの機能全体を落とさないような設計も必要になります。 用途 向いている 分析基盤が作ったデータをそのままAPIから配信したいとき アプリ側に取り込みの仕組みを作らず、ETL側で完結させたいとき 向いていない 接続まわりの初期実装に工数をかけられない場合 配信データを既存のアプリのDBと同じDBで管理したいとき(別のデータストアになるため不可) Lakebaseの予算を捻出できない場合 まとめ 今回紹介した3つの方法をまとめると以下になります。 Databricksから直接書き込む バックエンドのバッチで取り込む Lakebaseのsynced table ダウンタイムなしの全件更新の実現方法 別テーブルに書き込み、 RENAME TABLE で入れ替え(自前で実装) 単一トランザクションでの書き込み Snapshotモードの同期 初回のみ必要な整備 VPCピアリングなどの経路設定 定期実行の仕組みの整備 Lakebaseの作成 + トークン管理など接続まわりの作り込み テーブルごとに必要な作業 書き込み処理の実装 取り込み処理の実装 synced tableの追加 追加のインフラコスト ほぼなし ほぼなし あり(Lakebaseの従量課金) データの加工・検証を行う場所 Databricks側(書き込み前) アプリ側(取り込みバッチ内) Databricks側(同期元テーブルの生成時) 既存テーブルとの外部キー制約 張れる(id管理が煩雑) 張れる 張れない ETLが失敗したときの再実行範囲 ETLのみ ETL + 取り込みバッチ ETL + 同期 従来の方法とLakebaseを使用する方法を比較しましたが、LakebaseはUCのデータをアプリに届ける選択肢になり得ると感じました。ダウンタイムなしの全件更新がプラットフォームの機能として手に入り、アプリ側に取り込みの仕組みを作らずETL側で完結できるのは、従来の2つの方法にはない強みです。 ただし、Lakebaseを新しく持つことによる金銭的コストと接続まわりの初期実装が必要で、既存のテーブルと外部キーで繋ぐこともできないため、どんな場合でもLakebaseが最適というわけではありません。 どの方法も一長一短ではあるので、使用する状況に合わせて選定するのが良いと思います。個人的には、次のフローチャートに沿って判断するのが良いかなと思っています。 最後まで読んでいただきありがとうございました! この記事がいつか誰かの参考になれば嬉しいです!
はじめに こんにちは!デリッシュキッチンで主にバックエンドの開発を担当している秋山です。 AIツールの導入などで、ツール費用が増えている方も多いのではないでしょうか。 有料ツールを導入すると当然支出は増えますが、予算が無限にあるわけでもありません。 私も先日、有料ツールを導入したいと考えたときにコストの壁にぶつかりました。 そこで「先に既存のムダを削って、浮いた分の範囲内でまずは導入/検証する」という進め方を取ることにしました。 その中でコストとどう向き合うか考えるきっかけになったので、その時のことを紹介していきます。 どうやってコスト削減に取り組んだか コスト削減の対象として真っ先に思いついたのは、AWSでした。 インフラの費用は金額が大きいので、削れた時の効果もそのぶん大きくなります。 とはいえ、どこにムダがあるのかを自力で探し回るのは大変です。 そこで今回はAWS Cost Optimization Hubを使いました。 Cost Optimization Hub(コスト最適化ハブ)とは Cost Optimization Hubとは、コスト最適化に関する推奨事項を統合的に閲覧できるAWSのコストツールです。 どのリソースでどのくらいコスト削減できそうかがわかります。 また、表示される削減額は購入済みのReserved InstancesやSavings Plans等を考慮した上で、請求額がどれだけ変わるかを見積もった金額になっているようです。 そのため、定価どうしの単純な比較ではなく、実際の請求に近い形で優先順位を付けられます。 Cost Optimization Hub による機会の特定 - AWS コスト管理 削減額機会のページには、下記の項目があります。 削減額機会の一覧のヘッダ 削減額機会の一覧のヘッダ2 「最も推奨されるアクション」の欄には、「アップグレード」「アイドル状態または未使用のリソースを削除」「適切なサイズ設定」など、どのような方法でコスト削減が期待できるかが表示されます。 そのため、明確なアクションを計画することができます。 また、「実装作業」の欄には、実際にアクションする際の作業コストが「非常に低い」「高」などランクづけされて表示されます。 ただし、実装作業コストが低いからといって、安易に実行してよいとは限りません。 例えばReserved InstancesやSavings Plansの購入は、作業としては購入するだけなので手間はありませんが、年単位のコミットメントを負うことになり、購入後に条件を変更することはできません。 作業の手間と判断の重さは、別のものとして考えたほうがよさそうです。 結果 今回は比較的実装作業コストが低い、不要なリソースの削除や開発時に使用しているリソースのスケールダウンなどを行いました。 その結果、導入したかったツールの検証費用分は削減できました。 コスト削減に向き合う中で得た気づき 実際にコスト削減をしてみて、改めて大事だと思ったことがいくつかありました。 無駄を減らす 当たり前のことではあるのですが、コスト削減において無駄を減らすのは最も確実な手段だと思います。 使われていないリソースは、止めても何かが動かなくなるわけではありません。 そのため誰かが困って報告することもなく、気づかれないまま動き続ける可能性があります。 一方で、一旦使われていないことがわかれば、削除作業自体はとても簡単にできます。 技術的負債の解消でコストも減る 技術的負債の解消により、実装コストだけでなく、ツールやインフラのコストも減る可能性はあると思いました。 監視ツールを例に挙げると、多くは扱うデータ量に応じた従量課金です。 無駄な処理が多ければ、そのぶん送られるデータも増えて料金が上がります。 逆に処理を整理すればデータ量が減り、その分安くなります。 負債を抱えたままでも動いてはいるので、普段は困りません。 ですが動かし続けるための費用は、その裏で払い続けていることになります。 もっとも、これは削減額として測りにくい部分でもあります。 「この負債を解消したから月いくら下がった」と切り出すのは難しいので、あくまで副次的な効果として捉えています。 また、上述の「無駄を減らす」にも繋がるのですが、使用していないツールやインフラのリソースは、それ自体がメンバーの認知負荷を高める負債にもなり得ます。 そのため、負債を減らすと言う意味でもやはり「無駄を減らす」のは大切だと思いました。 キャッチアップが大事 コスト削減においても技術的な情報のキャッチアップは大事だと思いました。 AWSなどの外部サービスを使っていると、新サービス・新機能を使ったり、バージョンをアップデートするだけで料金が下がることがあります。 たとえばAWSで言うと、 EBSをgp2からgp3にアップデートする インスタンスをx86からGravitonに移行する などでコスト削減を行うこともできます。 Amazon EBS 汎用 SSD ボリューム - Amazon EBS ARM Processor - パフォーマンスプロセッサ - AWS EC2 Graviton - AWS こういったことは、日々キャッチアップをしていないとなかなか気づきづらいかなと思います。 というのも、一度組んだ構成は動いている限り見直す機会は多くありません。 そのため、安くなる選択肢が出ていても、こちらから探しにいかないと気づけないままになります。 その点では、Cost Optimization Hubのようなツールに頼るのも一つの手だと思いました。 アップグレードの推奨を出してくれるので、自分が追いきれていない部分を拾ってもらえます。 まとめ 本記事では、有料ツールの導入に向けてAWSのコストを削減した話を紹介しました。 Cost Optimization Hubを使うと、どこにムダがあるかと、それを直す作業のコストまで一覧で見られます。 まずはここを見て、作業コストの低いものから手を付けるのが取りかかりやすいと思います。 そして今回やってみて感じたのは、コスト削減は一度やって終わりではないということです。 無駄なリソースは気づかないうちに増えますし、新しい機能を追えていないだけで払い続けている分もあります。 最後まで読んでいただきありがとうございました! 参考文献 Cost Optimization Hub による機会の特定 - AWS コスト管理 削減の機会の表示 - AWS コスト管理 月間節約額の見積り - AWS コスト管理
こんにちは。開発1部でデリッシュキッチンのプレミアム機能を開発している新卒エンジニアの野村です。 初めて渡されたタスクの話です。実装がほぼ終わったタイミングで、大きな手戻りに気づきました。新しく追加するはずだった機能の一部が、すでに存在していました。 なぜ最後まで気づけなかったのかを振り返り、AIエージェントを使った開発で何を見落としやすいか、既存のシステムにどう向き合うべきかを考えました。 Claude Codeと進めたタスクで、既存機能を見落とした 2026年4月にエブリーへ新卒として入社し、研修を終えて最初に渡されたタスクは、アプリのLPに出すボタンの種類を切り替える機能でした。 デリッシュキッチンには個人プランとファミリープランの2種類の課金プランがあり、それぞれに無料期間があります。どちらのプランを出すかと、その無料期間を何日にするかを、それぞれダッシュボードから変更できるようにする、というのが依頼の内容でした。 タスクの進め方は、DesignDocument(DD)を作成してレビューを受け、サーバーを実装し、最後にダッシュボードを実装する流れでした。DDには、何を作るかに軽く触れたうえで、どう作るかを中心に書きます。エブリーではClaude Codeが全社で使えるようになっていたので、DDの作成からサーバーの実装まで、Claude Codeに相談しながら進めました。 フロントエンドの実装に入ったとき、修正箇所のコードに「無料期間」と書かれたフィールドがすでにあることに気づきました。調べてみると、個人プランの無料期間は、すでにダッシュボードから変更できるようになっていました。DDを見返すと、テーブル設計にもAPI設計にも、既存の無料期間フィールドはきちんと書かれていました。 DDはClaude Codeと相談しながら自分で出したものでしたが、そこに書かれていた既存の無料期間を、実装が終わるまで読み取れていませんでした。 ゼロから作っていた学生時代は、AIの出力を判断できた 学生時代は、インターン先でiOSアプリやWebサービスの開発をしていました。2023年から2025年にかけての時期で、何を作るかは案件ごとに違いましたが、共通していたのは、設計から実装まで自分でゼロから考えて作っていたことです。 当時はまだAgentのように自律的に動くAIはなく、ChatGPTへのコピペや、コード補完としてAIを使う程度でした。システムがどう動いているかは自分自身で作っているので、人に詳しく説明できる状態で開発を進めていました。 2025年の半ばにClaude Codeを使い始めてからは、それまでの実装時間よりもずっと速く開発できるようになりました。この速さを知った状態で、2026年4月にエブリーへ入社しました。 ゼロから作っていた開発では、AIが何を出力しても、それが正しいかどうかを自分の頭の中にある設計と照らし合わせて判断できました。そう判断できたのは、システムがどう動くべきかという理解が、判断軸として自分の中に自然にあったからです。 判断軸がないまま、AIに進めてもらった 私が渡されたタスクは、自分でゼロから作るものではありませんでした。何年も運用されてきた既存のシステムに、新しい機能を追加するものでした。 学生時代の開発速度をイメージしていましたが、実際に進めてみると、想像していたよりも、私のタスクを進めるスピードは遅いと感じていました。AIの出力の意味が自分にはわからないことが、進みを遅くしていました。 説明されてもそれが事実かどうか腑に落ちないまま、新卒として早く成果を出したい気持ちと、以前の開発速度への意識に押されて、そのまま進めていました。DDに対してはPRでレビューをもらえる仕組みがあり、そこで安心感を得ていた面もあります。 既存のシステムで実際に製品として触れる部分があっても、自分の手で触るのではなく、AIに調べてもらうという進め方をしていました。 AIとのやり取り越しでは、既存の仕様に気づけない 無料期間フィールドの存在に気づかなかったのは、コードを読む以前の話です。プロダクトのUIを自分で触っていれば、あるいはAPIの入出力を自分で確認していれば、個人プランの無料期間がすでにダッシュボードから変更できると気づけたはずでした。AIとのやり取り越しに見ているだけでは、そこに気づく機会がありませんでした。 自分が頼んだのは「個人プランの表示・非表示を切り替え、表示する場合は無料期間を設定できるようにする」という指示でした。この指示自体に、既存の無料期間フィールドとの整合性を確認する視点が入っていませんでした。既存にすでにある機能を、新しくAPIに追加しようとしている、という違和感に、指示を出す前に気づく余地はありました。 そこに気づかないまま、AIが返す説明に納得してしまうという状態が続いていました。たとえば「本当にそうなっていますか」とAIに確認しても、返ってくるのはこちらが納得できる説明です。その説明が実際に妥当かどうかを見極める判断軸がないまま、説明を読んで納得してしまう、という繰り返しでした。 DDを自分の手で書き、コードを自分の手で追った この見落としのあと、進め方を変えました。DDは自分の手で書き、AIは補助として使うようにしました。 DDのテンプレートに、目的、ゴール、非ゴール、システムの概要図、DBとAPIの変更、といった項目を自分で作りました。このテンプレートに沿って、まず自分の言葉で埋めていくようにすると、タスクを進めるうえで意識しなければならないことが、それまで抜け落ちていたと気づきました。 なかでも効いたのは、ゴールの項目に「どうなれば成功と言えるか」を自分の言葉で書く作業でした。ここが埋められていなければ、AIが出した実装がそれを満たしているかどうかも判断できません。 コードについても、AIに解説してもらうことはありましたが、最終的には自分の手で追うようにしました。この2つを変えただけで、考慮漏れは明らかに減りました。 AIと対話を重ねて理解しようとする進め方には、限界がありました。目の前のタスクは解決しますが、次の課題に当たったときにはまた同じように対話を重ねる必要があり、理解が場当たり的になっている感覚がありました。コードという一次情報を自分で追うようにしてからは、理解が積み上がっていく実感がありました。 まとめ: 判断軸は、一次情報からしか作れない 検証する判断軸は、AIの解説を聞くだけでは作れないと感じています。AIの解説は、こちらが理解できているかどうかにかかわらず、同じようにわかりやすく返ってきます。そのため、判断軸がないままでも、わかったつもりになってしまいます。 判断軸を作るには、DDを自分の手で書く、コードを自分の手で追うといった、一次情報に自分であたる作業が必要だと思います。ゼロから作る開発では、実装そのものが一次情報になるので、判断軸は作業の中で自然にできあがります。既存のシステムに新しく加わる開発では、自分からあたりにいかない限り判断軸はできあがらないというのが、今回の経験でした。その作業を積み重ねることが、次の課題にも使える判断軸を作る方法だと考えています。 新卒として早く成果を出したい、AIを使って早く実装したい、という気持ちは今もあります。同じように感じている方は多いのではないかと思います。ただ、急いで進めた結果が今回の手戻りでした。遠回りに見えても、一次情報にあたって理解を積み上げていくほうが、最終的には早いのだと感じています。 今回は新卒として入社した自分の経験ですが、新卒に限った話ではないとも思っています。中途入社や異動で既存のシステムに新しく参画するときも、そのシステムについての判断軸はまだ持っていない状態から始まります。自分が理解できていないシステムに携わるときは、同じことが当てはまるのではないかと感じています。 ここに書いたのは、新卒としてエブリーで働き始めてまだ数ヶ月の自分が、今の時点でたどり着いた解決策です。自分が経験を重ねることでも、AIの精度が上がっていくことでも、この考え方自体は更新されていくのだと思います。
こんにちは、トモニテ開発部 iOS エンジニアの村田です。 iOS エンジニアもしくは Android・クロスプラットフォームを含めてモバイルエンジニアの皆さんに対して気になっていることがあります。 みなさん AI 開発どんな感じでやってますか?どんなハーネス組んでますか? エンジニアリング業界では、単に指示を出して書いてもらう「バイブコーディング」の次の段階として、AI エージェントの自律化や「ハーネスエンジニアリング」「ループエンジニアリング」といった話題が急速に広がっています。 Web やサーバーサイド領域では、こうした最新の AI 活用の情報が活発に共有されている一方、iOS(モバイル)開発における実践的なナレッジはまだまだ各所に散らばっており個々で孤軍奮闘している印象を受けています。 私自身、久しぶりに iOS 開発に取り組んでいて、Claude Code と git worktree を用いた並行開発をする中で iOS 特有のハマりどころをいくつか感じました。 今回は「並行開発」という観点にフォーカスして、iOS で並行開発したときに感じた課題と対処方法を記載します。 開発環境 前提として、以下のような環境・仕組みで開発しています。 使用技術 AIエージェント : Claude Code エディタ : VSCode 並行開発 : Git Worktree 対象 : iOS アプリ(Swift / UIKit / SPM / .xcodeproj) ビルド方法 : XcodeBuildMCP(Claude 経由の MCP)・XcodeBuildMCP(CLI 版)・Xcode(GUI) ディレクトリ構成 <repo>-trees/ ← worktree develop/ ← メイン worktree(base ブランチ) feature-A/ ← タスクごとの worktree(並列) feature-B/ ← タスクごとの worktree(並列) refactor-C/ ← タスクごとの worktree(並列) ... 開発手順 タスク開始時に git worktree add で develop ブランチから新規 worktree を切る worktree 配下で Claude Code のセッションを開始し、要件定義 → 実装 → PR 作成など開発フローを進行 タスクが終わったら、その worktree ごと片付ける ---bin なぜ並行開発が欠かせないのか あちこちで語られている話なので、要点だけ。 AI に開発させると、人間の役割は「実装する人」から「複数のエージェントを監督する人」に変わります。「どこまで自律的に AI に開発させられているか」にも依りますが、設計・実装・ビルド・テストと多くのフェーズで人間の手が空くようになるため、1 タスクを直列で進めるよりも複数タスクを並行で回す方が効率よく進められます。 そこで注目を浴びたのが git worktree です。ブランチごとに独立した作業ディレクトリ(worktree)を持てるため、作業ファイルの競合を気にせず、安全に複数タスクを同時進行できます。 そうして git worktree を用いて iOS 開発を始めたのですが、いくつか課題が発生しました。 問題その1: DerivedData がストレージを食い尽くす 何が起きたか iOS のビルドでは DerivedData を生成します。DerivedData には Xcode がビルドのたびに吐き出す中間生成物(ビルドキャッシュ・インデックス・成果物など)が含まれており、デフォルトでは ~/Library/Developer/Xcode/DerivedData/ に生成されます。XcodeBuildMCP でビルドする場合は、 ~/Library/Developer/XcodeBuildMCP/workspaces/<worktree>-<hash>/DerivedData/<プロジェクト名>-<hash> に生成されます。 DerivedData は worktree の内部ではなく、ユーザー共通の場所にまとめて溜まるため、worktree の数に比例して独立したビルドキャッシュが蓄積されていきます。 私の環境では 1 worktree あたり数 GB〜20 GB 台、合計およそ 86 GB になっていました。 結果としてディスクの空き容量が枯渇し、スワップ領域も確保できなくなって、ビルドどころではなくなりました。 # Xcode(GUI)の既定 — プロジェクト名 + ハッシュ名 ~/Library/Developer/Xcode/DerivedData/ ├── <プロジェクト名>-a1b2c3d4efgh…/ ├── <プロジェクト名>-e5f6g7h8ijkl…/ ├── <プロジェクト名>-i9j0k1l2mnop…/ ├── <プロジェクト名>-q7r8s9t0uvwx…/ └── … # XcodeBuildMCP の既定 — worktree 名 + ハッシュ名 ~/Library/Developer/XcodeBuildMCP/workspaces/ ├── develop-a1b2c3…/DerivedData/ 19 GB ├── feature-A-d4e5f6…/DerivedData/ 21 GB ├── feature-B-g7h8i9…/DerivedData/ 11 GB ├── feature-C-j0k1l2…/DerivedData/ 8.8 GB └── …(他 4 worktree) 26 GB 計 86 GB キャッシュを削除しようと試みたところで Xcode の場合、DerivedData のフォルダ名がハッシュ化されていて、フォルダ名だけではどの worktree に対応するかわからない ビルドしなくてもインデックス更新などで更新日時が変わるため、「更新日が古い=不要」という判断も効かない といった理由により、使い終わった worktree のゴミだけを削除することが難しかったです。 かといって全部消すと、全 worktree がコールドビルドに逆戻りしてしまいます。 XcodeBuildMCP はデフォルトでフォルダ名に worktree 名が入るため対応関係は分かりやすいのですが、 git worktree remove (worktree の削除)をしても、この孤児フォルダが残り続ける点は Xcode の既定と同じです。 対処方針: DerivedData を worktree 配下に保存する 対応策として DerivedData の置き場所を worktree フォルダの直下( <worktree>/DerivedData )に固定すると、次のメリットが得られました。 どのキャッシュがどの worktree のものか、置き場所を見れば分かる git worktree remove すれば DerivedData も一緒に消える 開発時のライフサイクルは以下のようなイメージです。 DerivedData の置き場所を変更する方法 ① Xcode の GUI でビルドする場合 全プロジェクト一律でよい場合 : Xcode → Settings → Locations → Derived Data を Default から Relative に変える 特定のプロジェクトだけ有効にしたい場合 :対象プロジェクトを Xcode で開いた状態でメニューの File → Workspace Settings… ( .xcworkspace を開いていない場合は Project Settings… )を選び、 Derived Data を Workspace-relative Location に切り替える 後者の場合、実態としてはプロジェクト内の WorkspaceSettings.xcsettings ( xcuserdata 配下・通常は Git 管理外のユーザーローカル)の設定と同等のため、GUI を使わず次の内容を直接書いても実現できます。 <? xml version = "1.0" encoding = "UTF-8" ?> <!-- <project>.xcodeproj/project.xcworkspace/xcuserdata/<user>.xcuserdatad/WorkspaceSettings.xcsettings --> <! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" > <plist version = "1.0" > <dict> <key> DerivedDataLocationStyle </key> <string> WorkspaceRelativePath </string> <key> DerivedDataCustomLocation </key> <string> DerivedData </string> </dict> </plist> ② XcodeBuildMCP でビルドする場合 こちらは .xcodebuildmcp/config.yaml の derivedDataPath で決まります(worktree のルートからの相対パスで解決されます)。 # <worktree>/.xcodebuildmcp/config.yaml schemaVersion : 1 sessionDefaults : projectPath : "<プロジェクトパス>" scheme : "<スキーム名>" derivedDataPath : "./DerivedData" platform : "iOS" useLatestOS : false bundleId : "<バンドル ID>" 💡 どちらの方法でも DerivedData がリポジトリ配下に生成されるため、 .gitignore に DerivedData/ を追加する必要があります。 ただし「Xcode 版で特定のプロジェクトだけ Relative にしている場合」や「XcodeBuildMCP 版で .xcodebuildmcp/config.yaml を Git 管理していない場合」は、新しい worktree を作るたびにこれらの設定を行う必要があります。毎回手動で設定するのは面倒なので、Git の post-checkout hook で worktree の作成時に自動設定されるようにするのがおすすめです。 post-checkout hook で DerivedData の設定を自動化する git worktree add や git checkout を実行すると、 post-checkout という hook が発火します。hook は共通の Git ディレクトリ( git rev-parse --git-common-dir )に置かれ全 worktree で共有されるので、そこに設定することで新規 worktree 作成時の処理を仕込むことができます。 下記のような処理を .git/hooks/post-checkout に記述することで、新規 worktree 作成時に「DerivedData を各 worktree 配下( <worktree>/DerivedData )に保存する」ように設定できます。 なお以下の hook では ② の config.yaml を直接出力していますが、正の config.yaml をデフォルトブランチなどで管理し、post-checkout で cp する方式でも構いません。 #!/bin/bash # .git/hooks/post-checkout if [ " $3 " = "1" ]; then PROJECT = $( find . -maxdepth 1 -name " *.xcodeproj " | head -1 ) if [ -n " $PROJECT " ]; then # ① Xcode GUI 用: WorkspaceSettings に worktree 相対の DerivedData を書く DIR = " ${PROJECT} /project.xcworkspace/xcuserdata/ $( whoami ) .xcuserdatad " PLIST = " ${DIR} /WorkspaceSettings.xcsettings " mkdir -p " $DIR " [ ! -f " $PLIST " ] && plutil -create xml1 " $PLIST " plutil -replace DerivedDataLocationStyle -string WorkspaceRelativePath " $PLIST " plutil -replace DerivedDataCustomLocation -string DerivedData " $PLIST " # ② XcodeBuildMCP 用: config.yaml を worktree 直下に生成 if [ ! -f .xcodebuildmcp/config.yaml ]; then mkdir -p .xcodebuildmcp cat > .xcodebuildmcp/config.yaml <<'YAML' schemaVersion: 1 sessionDefaults: projectPath: "<プロジェクトパス>" scheme: "<スキーム名>" derivedDataPath: "./DerivedData" platform: "iOS" useLatestOS: false bundleId: "<バンドル ID>" YAML fi fi fi 問題その2: シミュレータを複数セッションが奪い合う 何が起きたか iOS 開発では、シミュレータ上での実操作が必要な場面が多々あります。 画面情報・遷移情報の取得 コード変更後の動作確認 E2E テストの実行 AI に並行開発させている中でこれらの動作をさせようとすると、1 台のシミュレータを複数のセッション(エージェント)が奪い合い、開発が滞る問題が発生しました。 操作の割り込み・競合: あるエージェントの検証中に、別のエージェントが起動・操作を行って画面を奪い合う 状態の破壊: 実行中のアプリ領域やデータが上書きされ、E2E テストや動作確認が誤判定される 処理の順番待ち: シミュレータの空き待ちが発生し、並行開発のスピード感が失われる 対処方針: worktree ごとに専用シミュレータを持たせる 対策として、 worktree ごとに専用シミュレータを持たせる運用にしました。 具体的にはシミュレータ名を <worktree 名> - <元の機種名> (例: feature-A - iPhone 16 Pro )のように紐付けます。これにより「このセッションが触っていいのはこの 1 台のみ」と明確にし、他のセッションで稼働中のシミュレータへの誤干渉を防いでいます。タスクが完了して役目を終えたシミュレータはシャットダウンし、別の worktree での開発で名前を変更して再利用する流れです。 💡 基本的には AI による並行開発を前提としていますが、人間が最終的な動作確認を行う場合にもメリットがあります。各 worktree 専用のシミュレータ上にビルド済みアプリがそのまま残っているため、スムーズに動作確認を進められます。 シミュレータの選定ルール ある worktree の開発で使うシミュレータの選定ルールは、以下のようにしました。 worktree 名で始まるシミュレータが起動済みなら、それをそのまま使う worktree 名で始まるシミュレータが未起動なら、起動して使う 無ければ、起動していないシミュレータを <worktree 名> - <元の機種名> にリネームして起動する これを実装に落とし込んだものが以下のコードです。 resolve_udid は ①→③ の順に「その条件に合うシミュレータがあるか」を確認していき、最初に見つかった 1 台を、その worktree に対応するシミュレータの UDID(デバイスの一意 ID)として採用します(見つかった時点で残りは確認しません)。あとはこの UDID を指定してビルドすれば、その worktree 専用のシミュレータに向けて実行できます。 wt = " $( basename " $PWD " ) " # worktree 名(例: feature-A)をシミュレータ名の接頭辞に使う # シミュレータ一覧を JSON で取得して devices_json にキャッシュする refresh_devices() { devices_json = $( xcrun simctl list devices available --json ) ; } # 未起動のシミュレータを起動する boot_sim() { xcrun simctl boot " $1 " 2 > /dev/null ; } # UDID から元の機種名を引く(既に worktree 名が付いていれば落とす。付け足すと再利用のたびに接頭辞が積み重なるため) sim_name() { printf ' %s ' " $devices_json " | jq -r --arg u " $1 " \ ' [.devices[][] | select(.udid == $u) | .name][0] // empty | sub("^.+ - "; "") ' } # 指定した起動状態(booted / unbooted)で、worktree 名で始まるシミュレータの UDID を返す(無ければ空文字) pick_named() { printf ' %s ' " $devices_json " | jq -r --arg wt " $wt " --arg want " $1 " \ ' [.devices[][] | select((.name | startswith($wt + " - ")) and (if $want == "booted" then .state == "Booted" else .state != "Booted" end)) | .udid][0] // empty ' } # 未起動(Shutdown)の空きシミュレータを1台返す(無ければ空文字) pick_spare() { printf ' %s ' " $devices_json " | jq -r \ ' [.devices[][] | select(.state == "Shutdown") | .udid][0] // empty ' } resolve_udid() { refresh_devices # ① worktree 名で始まるシミュレータが起動済みなら、そのまま使う udid = $( pick_named booted ) ; [ -n " $udid " ] && { echo " $udid "; return; } # ② worktree 名で始まるシミュレータが未起動なら、起動して使う udid = $( pick_named unbooted ) ; [ -n " $udid " ] && { boot_sim " $udid "; echo " $udid "; return; } # ③ 無ければ、空きシミュレータの接頭辞を「<worktree 名> - 」に付け替えて起動する spare = $( pick_spare ) [ -z " $spare " ] && { echo " 空きシミュレータがありません " >&2; return 1 ; } xcrun simctl rename " $spare " " $wt - $( sim_name " $spare " ) " boot_sim " $spare " echo " $spare " } 開発フローとクロージング処理 ここまでの処理を開発フローに沿って並べると次のようになります。 開始 : タスクごとに worktree を切り、専用の作業場を用意する 実装 : その worktree で Claude セッションを開始し、1 つの機能を実装する 動作確認 : その worktree 専用のシミュレータを起動し、ビルド & 実行・テストを行う レビュー : 実装が固まったら PR を作成する クロージング処理 : PR マージと同時に、worktree 削除・Issue クローズ・シミュレータのシャットダウンを行う 開発フローの最後には、クロージング処理を置いています。 close-feature のようなスキルにまとめ、PR マージ → Issue クローズ → worktree 削除 → 専用シミュレータのシャットダウンを一括で実行しています。 このクロージング処理をフローに組み込むことで、問題その1で挙げた DerivedData によるストレージ圧迫を防ぎつつ(worktree を削除すれば配下の DerivedData も一緒に消える)、起動したままのシミュレータが積み上がってメモリを圧迫するのも同時に抑えられます(シャットダウンした端末は、次のタスクで再利用される)。 問題その3: .xcodeproj のコンフリクトが多発する .xcodeproj を Git 管理しているとブランチの切り替えやマージのたびにコンフリクトが起きやすい、という問題があります。これ自体は昔からある iOS 開発の悩みですが、AI に複数タスクを並行開発させるようになってコンフリクトの頻度も増え、無視できないコストになったと感じています。 トモニテの PJ では実現できていませんが、下記の理由により XcodeGen を用いた YAML 管理への移行を検討しています。 1. 並行開発でのコンフリクト軽減 git worktree などを活用して複数ブランチでの並行開発を進める場合、 project.pbxproj のコンフリクトが多発する可能性があります。 特に AI の活用によって開発の速度や並行性が上がるほど、その発生頻度も多くなりがちです。 XcodeGen を導入して .xcodeproj を自動生成の成果物として扱い .gitignore に追加することで、 .xcodeproj でのコンフリクト問題を解消できると考えています。 2. AI と YAML の相性 Xcode のプロジェクト管理は GUI 操作を前提とした仕組みであり、AI エージェントとは相性が良くありません。これを project.yml による宣言的なテキスト管理に切り替えることで、「YAML での管理・変更 → コマンド実行( xcodegen generate )」という AI に適したフローで構成を変更できるようになると考えています。 3. コードレビューのしやすさ コード差分が複雑な pbxproj からシンプルな project.yml に変わることで、変更内容を容易に把握できます。 AI がコードを書き、人間がレビューするフェーズでは可読性の観点でもメリットがあると考えています。 まとめと残課題 ここまで git worktree を用いた iOS の並行開発で出会った問題点と、その対処方法を紹介してきました。 とはいえ、こうして並行タスクを回す仕組みを整えても、human-in-the-loop 的な体制では並行開発の効果も限界があると感じています。監督する人間のコンテキストスイッチがボトルネックになるため私の場合、同時に見られるのは 3〜4 タスクほどでした。これでは開発生産性も頭打ちになり、「プロダクトや事業をどう伸ばすか」という本質的な問いに集中できないと感じています。 この課題を突破するためには、やはり「ループエンジニアリング」など AI がより自律的に開発を回せるような仕組みを追求していくことが不可欠だと感じています。 関連リンク XcodeBuildMCP XcodeGen
こんにちは、開発1部で食事管理アプリ ヘルシカ の開発をしている井上です。 App Store のオファーは種類が多く、対象になるユーザーも、適用のしかたも、サーバ署名の要否も、種類ごとに違います。オファーごとの対象ユーザーと、種類が絡んで分かりにくい所を1本にまとめてみました。 オファーは4種。「誰に出すか」で選ぶ サブスクリプションの月額や年額といったプランは、 サブスクリプショングループ という単位でまとまります(同じグループで同時に有効なのは1つ)。 オファー は、一定期間だけ無料や割引で購読してもらう仕組みで、プランごとに設定します。オファーには、新規向けの お試しオファー (Introductory Offer)、購読履歴のある人向けの プロモーションオファー (Promotional Offer)、解約者を呼び戻す 再獲得オファー (Win-back Offer)、コードを配って引き換えてもらう オファーコード (Offer Codes)の4種類があります。どのオファーを選択するかは、ユーザーがどの購読状態にいるかで決まります。 ユーザーのサブスクリプションの購読状態は、次の3つに分類されます。 新規 :まだ加入していない人 既存 :購読中の人 離脱 :解約して今は購読していない人 段階と対応づけて、オファーの種類ごとの違いを次の表と図にまとめます。 オファー種類 主な対象 適用のしかた ① お試し 新規と、お試し未使用の離脱者 資格があれば自動で提示 ② プロモーション 既存か離脱のユーザー サーバ署名を付けて購入 ③ オファーコード 新規/既存/離脱(配布先で指定) コードを配布し引換入力 ④ 再獲得 離脱 Apple が資格判定。App Store にも表示 購読ライフサイクル(新規→既存→離脱)と各段階で使えるオファーの対応図 オファーコードは、どの段階にも配れます。 また、プロモーションはそのユーザーがプロモーションを受け取れるユーザーであることをサーバー署名を行い証明する必要があります。 オファーの出し方(支払いモード) どの種類でも、オファーの出し方は共通で、3つの 支払いモード から選びます。 支払いモード 課金のしかた 例 無料トライアル( freeTrial ) 一定期間は無料 7日間無料 継続割引( payAsYouGo ) 割引価格を毎請求期間、指定回数ぶん払う 3ヶ月間、毎月300円(通常600円) 前払い割引( payUpFront ) 割引価格をまとめて一括前払い 1年分を6,000円(通常7,200円) オファー期間が終わったあと、通常価格で継続課金するか、終了するかは、設定によって決まります。期間は3日から1年までの決まった選択肢から選びます。割引率を直接指定する枠はなく、地域ごとの価格設定の差が結果的な割引率になります。 オファーやプランが絡むと迷うところ オファーは単体なら分かりやすいものの、お試しやオファーコード、プラン変更が絡むと途端に分かりにくくなります。 お試しはいつ使えなくなるのか お試しを使えるかどうかは、プランごとではなくサブスクリプショングループ単位で判定されます。そして、同じグループで使えるのは一度だけです。たとえば月額と年額が同じグループにあるとします。新規のユーザーは必ず対象になります。過去に解約した離脱者も、同じグループでまだお試しを使っていなければ対象になりえます。逆に、月額でお試しを一度使うと、同じグループの年額ではもう使えなくなります。 プロモーションは新規の獲得に使えるのか 割引を配れるのだから、プロモーションは新規の獲得にも使えそうに見えます。ところが実際には、購読履歴があることが大前提で、新規には使用できません。対象になるのは、既存か離脱のユーザーです。資格は2段構えです。 ① Apple の判定 :いま購読中の人か、購読が期限切れになった人だけを対象と認めます。 ② 自社の絞り込み :Apple が認めた対象から、独自の条件で「誰に出すか」を決め、署名してオファーを提示します。 購読履歴のない新規は①の時点で対象外なので、署名しても購入は失敗します。新規に強く訴求したいなら、お試しかオファーコードを使います。 オファーコードの「自動更新」と「お試し併用」 オファーコードを作るときに選ぶ「自動更新」と「お試し併用」は、適用後の課金の挙動を大きく変えます。 オファーコードの自動更新ON/OFFとお試し併用による課金の時間推移図 自動更新 ON :コードのオファー期間が終わったあと、通常価格で更新が続きます。 自動更新 OFF :オファー期間で終わり、更新されずに失効します。設定できるのは無料オファーだけで、お試し併用は選べません。 お試し併用 :お試しを先に適用すると、その分だけコードのオファー期間と更新が後ろにずれます。適用しなければ、コードだけになり、お試しの資格は残るので、あとで改めてユーザーに提示できます。 オファーコードは、配布先に特定のユーザーを指定することができません。対象は、 New(未購読)/Existing(現在購読中)/Expired(解約や期限切れ) の3カテゴリと、配信先の国や地域で絞ります。カテゴリは作成後に変えられず、1つのコードは1つのプランに紐づきます。 コードには、ワンタイムとカスタムの2種類があります。 ワンタイム :特定の1人が1回だけ。有効期限は必須(最大6ヶ月)。 カスタム :上限まで何人でも。無期限にもできます。 お試しも使ってもらって、ちょうど6ヶ月無料をオファーコードで配れるか たとえば「お試しも実際に使ってもらったうえで、ちょうど6ヶ月を無料にし、コードで配りたい」。一見できそうですが、3つを同時には満たせません。お試しを使ってもらうにはコードの「お試し併用」を Yes にするしかなく、するとお試し期間がコード期間の前に上乗せされます。無料の合計が「お試し+コード期間」になるうえ、選べる期間も決まった値だけなので、ちょうど6ヶ月には合わせられません。 無料トライアルだけで解約した人は呼び戻せるか 無料トライアルだけで解約した人を呼び戻すなら、解約者向けの再獲得オファーが使えそうに見えます。ところが、再獲得オファーの対象には含まれません。 再獲得オファーの資格は、App Store Connect で条件を設定して決めます。設定できるのは、有料購読期間の最小月数、失効(購読の終了)からの経過期間、前回のオファーからの間隔です。いずれも有料で購読した実績を前提にしています。無料トライアルだけで解約した人は、有料で購読した期間が0か月です。そのため、有料購読期間を条件にすると、資格を満たせません。 無料トライアルだけで解約した人に各オファーが届くかを示す分岐図 呼び戻すには、プロモーションかオファーコードを使います。プロモーションは期限切れの購読履歴がある人を離脱者として対象にでき、オファーコードは対象に Expired を含められます。プロモーションもオファーコードも有料で購読した実績を条件にしません。 プラン変更(乗り換え)とオファーの関係 同じサブスクリプショングループのプランには、グレード(上位と下位の序列)をつけて階層構造で管理します。プラン変更(乗り換え)の挙動は、変更前と変更後のグレードで決まります。上位へのアップグレードは即時に反映され、未使用分が日割り返金されます。下位へのダウングレードや、期間の違う同じグレードのプランへの変更(クロスグレード)は、次の更新で反映され、返金はありません。 プラン変更に、オファーは必ずしも必要ありません。上位プランへ移るだけなら、購入すれば自動的にアップグレードされて上位のプランが適用されます。割引してプランのアップグレードを促したいときにだけ、プロモーションオファー(サーバ署名が必要)を添えてプランを提示します。(個人プランからファミリープランへの移行を初回だけ割引するなど) まとめ オファーを選ぶ軸は、機能の名前ではなく「誰に、いつ出すか」です。新規にはお試し、購読履歴のある人へ自社が対象を決めて出すならプロモーション、どの層にも配りたいならオファーコード、解約者を App Store 経由で呼び戻すなら再獲得を選びます。 自分の理解を整理するために書いた記事ですが、同じようにオファーの種類の多さに戸惑っている人の理解の助けになればうれしいです。 出典 Implementing introductory offers in your app Implementing promotional offers in your app Set up win-back offers Set up subscription offer codes Offer auto-renewable subscriptions
こんにちは。開発本部の河野です。 このたび、社内の運用チームから寄せられる問い合わせに一次対応する Bot を、Claude Managed Agents を使って構築しました。本記事では、アーキテクチャ・ツールの使い分け・設計上の判断を中心に、検討の過程を紹介します。これから導入を検討される方の参考になれば幸いです。 本記事について  Claude Managed Agents は、本記事の執筆時点(2026 年 7 月)では パブリックベータ です。今後の正式提供(GA)に向けて、仕様や挙動が変わる可能性がある点にご留意ください。最新の情報は公式ドキュメント( Claude Managed Agents overview )をご確認ください。 背景:実データを確認しないと答えられない問い合わせ はじめに、前提を簡単に共有します。 社内には、キャンペーン応募の運用を担う管理画面つきの業務ツールがあります。ビジネス職はこの画面から、応募フォームの設問や表示条件を組み立て、集まった応募データの変換ルールを定義し、クライアントへ送付するデータの形式を設定します。 このツールは、案件ごとに設問や変換ルールを自由に組み合わせられるぶん、設定次第で挙動が変わり、意図どおり動くかの判断が難しくなります。そのため開発チームには、次のような問い合わせが日々寄せられていました。 設定方法や仕様の質問 :「この設問に回答した人にだけ画像を表示するには?」「この項目は設定できない認識で合っているか」 エラーやデータ不整合の調査 :「テスト時に出力データの件数が想定と合わない」「応募フォームを修正して公開したが、修正前の内容が表示される」 前者はドキュメントや仕様を調べれば答えられますが、後者は設定の内容・変換処理の実行状況・出力されたデータを、その都度確認しなければ答えられません。 つまり、あらかじめ用意した FAQ を返すだけでは、後者のような問い合わせに一般論しか返せません。そこで求めていたのが、実データまで確認したうえで一次回答を返すエージェントでした。これが今回の出発点です。 なぜ Claude Managed Agents を選んだか この問い合わせは、どこか一箇所を検索すれば済むものではありません。回答を組み立てるには、管理画面の設定、その裏側で変換処理を動かしている AWS、ドキュメント、ソースコードといった複数の情報源を横断しながら考える必要があります。 自前でエージェントのループ(モデルに推論させ、ツールを呼び出し、その結果を再び渡す、という処理を繰り返す仕組み)を実装する選択肢もありました。しかし、そのループの実行をマネージド側に任せられれば、私たちは「どのようなツールを持たせるか」「プロンプトをどう設計するか」に集中できます。今回の課題ではこの点が最も効くと考え、Claude Managed Agents を採用しました。あわせて、この取り組みは新しい技術に挑戦する社内の機会「挑戦WEEK」の中で始まったもので、当時登場して間もなかった Claude Managed Agents を実地で使ってみたい、という動機もありました。 補足  同様のことは、Amazon Bedrock AgentCore をはじめとする他のマネージドなエージェント基盤でも実現できます。今回は「新しい技術を試す」という挑戦WEEK の主旨から Claude Managed Agents を選びましたが、これから腰を据えて構築するのであれば、こうした選択肢も比較検討に加える価値があります。 Claude Managed Agents とは 本題に入る前に、前提となる部分だけ簡単に整理します。 Claude Managed Agents は、大まかに言うと、プロンプト・ツール・スキルをまとめた "Agent" を定義しておくと、推論ループの実行自体はマネージド側が担ってくれる仕組みです。 自前でループを実装する場合と比べると、開発側が担う範囲は次の3つに絞られます。 Agent 定義 (プロンプト・利用するツール・スキル・MCP の接続設定のまとまり) custom tool の実装 (自社システムを呼び出すコード) セッションの運用 (会話やイベントのハンドリング) 主なプリミティブは以下のとおりです。まずは名称だけ挙げておきます。 Agent :バージョン管理される Agent 定義 Session :会話の単位(Sessions API) custom tool :自前で実装するツール MCP :外部サービスとの既製の連携 skill / memory store :手順やナレッジ、過去事例の蓄積先 custom tool と MCP について補足 このあと繰り返し登場するため、この2つだけ先に補足します。 custom tool は、モデルに持たせる「道具」です。モデルが「この道具をこの引数で使いたい」と判断すると、実際に動作するのは開発側が実装したコードで、その結果を返すとモデルが推論を続けます。定義は name / description / input_schema からなり、とりわけ description (その道具が何をするものか)の記述が精度を大きく左右します。 一方の MCP は、外部サービスやデータ源をエージェントに接続するための標準的な仕組みです。Confluence や Slack のように公式の MCP サーバがすでに提供されているものは、それを繋ぐだけで使えます。ただし MCP サーバは自作することもできるため、「MCP か custom tool か」は "既製か自前か" という単純な対立ではありません。 では、この2つをどう使い分けたのか。ここが本記事の中心となるため、次章でまとめて扱います。 構成と、ツールの使い分け・設計上の判断 ここからが本題です。 全体の流れ Bot の動作自体はシンプルです。 運用担当が Slack でメンションして質問する 実行環境がセッションを開始し、イベントを受け取る Agent が必要な custom tool / MCP を呼び出しながら、実データを集めて推論する まとまった一次回答を Slack のスレッドに返す 図で示すと、Slack を入口として、実行環境がセッションとツール実行を担い、サーバ側の Agent が custom tool と MCP を使い分けながら回答を組み立てる、という構成です。 持たせているツールと、その使い分け Bot に持たせているツールは、現時点ではすべて read-only(参照のみ)です。設定の書き換えやジョブの再実行は行いません。まずは「調べて答える」ことに用途を絞りました。 情報源ごとに、custom tool にするか・MCP にするか・リポジトリを参照させるかを分けています。 情報源 参照するもの 実現方法 社内管理画面 現在の設定(案件・変換・送付設定など) custom tool(管理画面 API を呼び出す) AWS のジョブ実行状況 ジョブが成功したか、どこで失敗したか custom tool AWS の出力データ 出力データ(テーブル・カラム)が実在するか custom tool Confluence 運用手順・仕様のドキュメント MCP Slack 問い合わせ元スレッドの文脈 MCP GitHub 設定や画面では分からない「実際の挙動」 リポジトリをマウントして参照 AWS 側では、変換ジョブとその出力データを確認しています。内部的には一般的な構成ですが、本記事では「AWS 上のジョブと出力データを custom tool 経由で確認している」程度の粒度にとどめます。 設計上の判断①:custom tool と MCP をどう使い分けたか ツールを整理するうえで最も悩んだのが、「この情報源は custom tool と MCP のどちらにすべきか」という使い分けでした。一般論としての比較ではなく、今回の Bot で実際にどう判断したかを述べます。 判断は、大きく2段階でした。 まず、 公式の MCP サーバがすでにある情報源は、MCP で繋ぐ ことにしました。Confluence や Slack がこれにあたります。既製のものがあるのに、わざわざ自作する理由はありません。 問題は、管理画面や AWS のように 既製の連携が存在しない情報源 です。これらはエージェントから触れるようにするために、何かしら自分たちで実装する必要があります。ここで MCP サーバを自作する道もありましたが、 MCP サーバを立てるよりも、custom tool として実装するほうがコストが低く、早く動かせます 。まずは動くものを優先したかったので、これらは custom tool にしました。 公式の MCP がある(Confluence / Slack)→ MCP で繋ぐ 既製の連携がない(管理画面 / AWS)→ 実装が必要。より軽い custom tool を選択 コードを事実として参照させたい(GitHub)→ リポジトリをマウントして参照 (後述) ここで大事なのは、「MCP か custom tool か」は "既製か自前か" では決まらない、という点です。MCP サーバも自作できるので、実際に効いてくるのは "どちらのほうが軽く・早く目的を果たせるか" でした。 なお、GitHub には公式の MCP も用意されています。それでもリポジトリをマウントする方式にしたのは、用途がソースコードの読解だったためです。MCP 経由ではファイルを API で1つずつ取得する形になりがちですが、リポジトリをマウントしてしまえば、エージェントがコード全体を grep や横断参照でたどれます。「実際の挙動をコードで確認する」という用途には、こちらのほうが向いていました。MCP が力を発揮するのは、issue や PR の操作といった場面です。ここでも、「MCP があるかどうか」だけで機械的に決めるのではなく、その情報源に対して何をしたいかで選ぶ、という判断でした。 設計上の判断②:調べる範囲を先に絞る 情報源を渡す際には、範囲を絞ってから渡すことを徹底しました。 Confluence は「正典」とするページに限定する :関連しそうなページを無制限に参照させると、古い記述や書きかけのページに引きずられ、誤った回答につながります。「これが正である」と定めたページのみを参照先とすることで、誤読を防いでいます。あわせて、その正典ページ自体を最新に保ち続ける運用も欠かせません。参照先を絞るほど、そのページの正しさがそのまま回答の質に直結するためです。 ツールは read-only に限定する :前述のとおり、書き込み系の操作はまだ持たせていません。参照に用途を絞ることで、安心して社内に展開できる状態を先に整えました。 範囲をあとから広げるのは容易ですが、広げすぎたものを絞り込むのは困難です。そのため、先に狭く定めておく方針としました。 設計上の判断③:self-hosted 型ではなく Cloud 型を選んだ Claude Managed Agents には、実行基盤を自前で持つ self-hosted 型 と、マネージドな Cloud 型があります。当初は self-hosted 型を検討していましたが、最終的に Cloud 型を選びました。その経緯を書きます。 self-hosted 型では、マネージド側のエージェントが「このツールを実行してほしい」と判断したとき、その実行を受け取って処理するワーカーを、自社の環境で常時動かしておく必要があります。ワーカーというプロセスを一つ立て、接続を保ち、動き続けているかを監視する。エージェントのループ自体はマネージドに任せられても、その足回りは自分たちで抱えることになります。 さらに決め手になったのは、運用負荷そのものよりも、私たちが最も頼りにしたかった機能との噛み合わせでした。過去事例を蓄積する memory store や、マウントしたソースコードをエージェント自身に読ませる仕組みは、いずれもマネージドな環境の中で動くことを前提に設計されています。self-hosted 型ではその実行が自前ワーカー側に委譲され、期待どおりに動かない場面がありました。活かしたい機能ほど、実質的に Cloud 型を前提としていたのです。 結局 self-hosted 型は、「ワーカーの運用という手間が増える」一方で「使いたかったマネージド機能は活かしきれない」という、狙いと逆向きの選択になっていました。「実行を任せて設計に集中する」という当初の目的からすると本末転倒です。そこで Cloud 型に切り替えました。ワーカーは不要になり、構成は Bot 本体ひとつに単純化され、サーバ側の機能もそのまま利用できるようになりました。 補足:self-hosted 型が適する場面もあります。 たとえばツールの実行を自社ネットワーク内に閉じ込めたい、といった要件がある場合です。今回はそうした制約がなく、マネージドの利点が上回ったため Cloud 型を選びました。要件次第で判断は変わる、という点は添えておきます。 設計上の判断④:デプロイを2種類に分ける 運用する中で、変更には性質の異なる2種類があることが分かってきました。 プロンプトやツール定義のみの変更 → agents.update で新しいバージョンを作成すれば反映される(コンテナの再ビルドは不要) custom tool のコードの変更 → コンテナの更新が必要になる この2つを分けて扱うようにしたことで、「プロンプトを少し修正したいだけ」という場合にコンテナのデプロイを待たずに済み、改善のサイクルを回しやすくなりました。 実際にできるようになったこと ここまでの構成により、Bot は実際に次のような回答を返せるようになりました。 設定の確認:「この案件の現在の設定はこうなっています」 エラーの一次切り分け:ジョブの実行状況を確認し「ここで失敗しています」 出力データの確認:「データは生成されています/されていません」 手順の案内:Confluence の正典ページを参照して手順を返す 挙動の確認:GitHub のソースコードを参照し「実際の動作はこうです」 文脈の把握:Slack のやり取りから状況を把握する そして、この Bot の最大の強みは合わせ技にあると考えています。たとえば「一般的な手順(Confluence)」と「その案件の現在の状態(管理画面・AWS)」を突き合わせ、1回の回答としてまとめて返すことができます。 固定的な FAQ との最大の違いはここにあります。ドキュメント・ソースコード・実データを組み合わせ、その案件に固有の一次回答を返せることが、実データを確認しにいくエージェントとして構築した狙いそのものでした。 運用して効果を感じた工夫 description を丁寧に書く :custom tool の説明が不十分だと、モデルが道具を適切に選べません。「どのような場合に使うツールか」まで記述すると精度が向上しました。 参照範囲を固定する :前述の「正典」ページの例と同様に、参照させる範囲を絞るほど誤読が減ります。 read-only の境界を保つ :安全に展開できるため、社内に広げる際のハードルが下がります。 skill / memory store に過去事例を蓄積する :類似の問い合わせに関するナレッジを蓄積し、次回以降の回答に活かしています。 成果 まだ試運転の段階ですが、当初ねらっていた形は実現できています。 一次調査の負荷が下がった :これまで開発チームが都度、管理画面や AWS を確認して切り分けていた類型的な問い合わせについて、その最初の調査をエージェントが肩代わりできるようになりました。試運転として開発チームのチャンネルで運用し、これまでに約30件の実際の問い合わせへ一次回答を返しています。件数は多いが定型的な「まず状況を調べる」部分を任せられるようになったのが、大きな変化です。 回答が推測ではなく実データにもとづくものになった :固定的な FAQ では「一般的にはこうです」までしか言えませんでしたが、6つの情報源(管理画面・AWS のジョブ実行状況・AWS の出力データ・Confluence・Slack・GitHub)を横断し、その案件の現在の状態まで確認したうえで回答できるようになりました。 回答品質を数値で追えるようになった :実際の問い合わせを集めた32件の評価用データセットを用意し、回答を5つの観点・計10点満点で自動採点する仕組みを整えました。現時点の平均は8.1点/10点(32件中21件が合格ライン)です。この仕組みがあることで、「プロンプトやツールを直す → 同じデータセットで測り直す」という改善ループを、感覚ではなく数値で回せるようになりました。実際、点数の内訳を見ると「原因の特定」が弱点として表れており、次に何を直すべきかが具体的に分かります。 補足:採点の仕組み。 採点そのものも Claude に担わせています(いわゆる LLM-as-a-judge)。各問い合わせにはあらかじめ「期待される原因・対処」を正解として添えてあり、採点役はそれと Bot の回答を突き合わせて、次の5つの観点に各0〜2点をつけます。(1) 原因を特定できているか、(2) 対処が管理画面の正式な操作まで具体的か、(3) 申告した確信度は妥当か、(4) 開発へエスカレーションすべき場面を正しく見極められているか、(5) 読み手に伝わる構成か。合計10点満点で、8点以上を「合格」としています。単発のスコアは多少ぶれるため、絶対値そのものより「どの観点が弱いか」という傾向を読み、改善の的を絞るのに使っています。 散在していた知見が集約された(副次的な効果) :エージェントに正しく答えさせるには、担当者の頭の中や個別のやり取りに散らばっていた運用知識を、参照可能な形に整える必要がありました。正典ページの整備や過去事例の蓄積を進めた結果、Bot のためだけでなく、人が参照する資産としても知見がまとまりました。 課題とこれから もちろん、まだ発展途上です。 対象範囲は、安全に広げられるところから段階的に拡大しています。本番データを扱う範囲については、アクセス制御の設計を前提に、慎重に進めていきます。 回答精度や確信度の調整は、引き続きの課題です。「自信がない場合にどう振る舞うか」の扱いは、今後詰めていきます。 そもそも Managed Agents 自体の事例がまだ少なく、運用しながら整備している状況です。 今後目指しているのは、確信度によって振り分けを行い、ビジネス職が自ら一次回答にたどり着ける状態です。「自信のある回答はそのまま返し、不確かなものは人へエスカレーションする」といった振り分けが実現できれば、より安心して任せられるようになると考えています。 最後に。Claude Managed Agents は、うまく活用すれば「実データを調べて答える」エージェントを現実的な工数で構築できる仕組みだと感じています。本記事が、これから導入を検討される方の一助になれば幸いです。
AI ファースト・カンパニーに向けて 株式会社エブリーでCTOを務めている今井( @imakei_ )です。 本記事は AIブログリレー 第10本目 、そして最終回です。7/1から開発部長陣がリレー形式で「組織とAI」について書いてきて、3人で計9本を積み上げてきました。 書いているうちに気づいたのですが、9本はバラバラのトピックを並べたようでいて、実は一つの共通した背骨で繋がっていました。結論から言うと、それは 「オントロジー」 です。会社の現実を人間とAIの共通言語として定義するというこの考え方が、組織・データ・プロダクトという別々のレイヤーで、姿を変えながら何度も顔を出していました。この最終回では、その背骨に沿って全体を振り返ります。 まずは9本の振り返り はじめに、各記事を一言ずつで振り返っておきます。読み返す入口として使ってもらえればと思います。 オントロジーと組織OSとこれからのエンジニア 会社の現実を「名詞・動詞・ルール」として定義し、人とAIが意思決定を回す 組織OS をつくる。これはエンジニア組織が主導すべきで、最初の一歩は社内FDE。 全社のAIファーストを牽引するエンジニアの役割 Claudeの全社導入で「業務に一番詳しい人が作るものが一番使える」状態が生まれた。次はタスク効率化から 業務フローの再設計 へ、個人から組織へ。それを担うのがFDE。 AIエージェントのハーネスとは何か 同じモデルでも実力差が出るのは ハーネス (モデルを実世界のタスクに接続する装備一式)の差。コンテキスト・ツール・権限・検証ループという要素で捉え直す。 AIファーストな組織デザイン 組織OSという「仕組み」に対して、それを動かす「かたち」の話。 少数化 × 事業部アライン(縦の深掘り) × 横のつながり(横の連携) で組織を編む。 組織のAI活用を支えるMCPゲートウェイ 個人の「使ってみる」と組織の「運用する」は別物。MCPが増えると統制と活用の両面で壁にぶつかり、それを支えるのが MCPゲートウェイ というコンセプト。 レガシーシステムをAIでどうアップデートしていくか 単純リライトは機能しない。まず 正しさを定義できる状態 へ。理解する → 検証可能にする(特性テスト)→ 段階的に移行する(ストラングラーフィグ)。 AI活用をどう評価するか 評価指標はフェーズで乗り換える。 TokenMaxxing(使う)→ TokenOptimization(使いこなす)→ アウトカム へ。良い指標ほど、いずれ自らを不要にしていく。 AIエージェントを "動かす" データ基盤 「読ませる」から「動かす」へ。参照できる(メダリオン)→ 解釈を間違えない(セマンティックレイヤー)→ 行動できる(オントロジー) の3ステップ。 AI時代、ソフトウェアは3層で考える 生成が安くなり「作って捨てる」が合理になる。 Core / Connectors / Disposable の境界を引く鍵は、プロダクトの世界観(=オントロジー)を定義すること。 レイヤーによらず、AI時代のテーマは「オントロジー」である 改めて並べてみると、1本目で持ち出した「オントロジー」が、連載の後半で二度、別のレイヤーに姿を変えて再登場しています。 組織のレイヤー (1本目)では、会社の現実を「名詞・動詞・ルール」として定義し、人とAIが意思決定を回す土台にする、という話でした。 データのレイヤー (8本目)では、それが業務のオブジェクト・関係・アクションを機械可読にする、データ基盤の最上段として現れました。参照できる・解釈を間違えない、の先にある「行動できる」を支えるものです。 プロダクトのレイヤー (9本目)では、そのソフトウェアの世界観を定義することが、そのまま「長く残す Core」と「捨てて作り直せる Disposable」の境界を引くことになる、という形で戻ってきました。 同じ考え方が、組織・データ・プロダクトの三段で繰り返し効いてくる。これは事前に設計したわけではなく、書き進めるうちに自然とそうなったものです。裏を返せば、 AIを現実に接地させるという課題は、どのレイヤーでも本質的に同じ形をしている のだと思います。何が中心的な概念で、何ができて、どんな制約があるのか——この「意味の定義」を人とAIで共有できるかどうかが、あらゆる場面で成否を分けていました。 三段で読むと、全体像が見えてくる オントロジーを背骨とすると、9本は大きく三つの塊に分けて読めます。 1つ目は「土台をつくる」話です。 組織OS(1)、ハーネス(3)、MCPゲートウェイ(5)、データ基盤(8)。これらはすべて、AIを実世界のタスクに安全に接続するための装備の話でした。モデルそのものは誰でもAPIで呼べる。難しいのはその先、つまりAIを自社のデータ・権限・業務プロセスに配線し、信頼して任せられる状態まで持っていく部分です。ハーネスはそれを一つのエージェントの単位で、MCPゲートウェイとデータ基盤は組織全体の単位で解こうとした、と整理できます。 2つ目は「組織のかたちを変える」話です。 エンジニアの役割(2)と組織デザイン(4)。AIで一人あたりの成果が変わると、人を増やして解く発想から、少数のチームがAIをレバレッジに事業を丸ごと引き受ける発想へと移ります。縦(事業)で深掘りし、横(職能・AI活用の知見)で編む。そしてその現場で課題定義から実装まで担うのがFDEでした。 3つ目は「つくり方・回し方を変える」話です。 レガシー刷新(6)、評価(7)、ソフトウェア3層(9)。ここに共通していたのは、 AIに任せる範囲と、人間が握り続ける範囲の線引き です。レガシー刷新なら「正しさを定義する判断」、評価なら「指標を乗り換え、廃止する判断」、ソフトウェア設計なら「Coreとして残すものの判断」。手を動かす難しさが、考える難しさへ移っていく、という点で3本は響き合っていました。 ここから、どうAIファーストにしていくか 背骨(オントロジー)と三つの塊で連載全体を整理してきましたが、では ここからエブリーをどうAIファーストにしていくのか 。9本を書き終えて、その方向は二つに定まったと感じています。 一つは、 「個人から組織へ」を進めきる ことです。Claudeの全社導入で、個人のAI活用は一気に進みました。ここからの勝負は、そこで生まれた便利ツールやプロンプト、スキルを個人に閉じたままにせず、共有資産へと育てられるかどうかです。組織OSも、横のつながりも、MCPゲートウェイも、評価指標の組織展開も、狙いはすべて「個人技を組織の力に変える」の一点にあります。個人の「使ってみる」を、組織の「運用する」へ引き上げる——AIファーストへの一番大きな一歩は、ここだと考えています。 もう一つは、 人間の役割を、意思決定と設計へ寄せていく ことです。「決まった仕様を実装する」から「何を意思決定すべきかの土台を整える」へ。実装者から、業務とシステムの両方を握るFDEへ。画面を手で磨く人から、世界観と境界を設計する人へ。AIに任せられる範囲が広がるほど、人間の価値は、Howそのものよりも 「What(何をやるか)」と「Why(なぜそうするか)」を握る ところに移っていきます。AIはHowを高速に片づけられても、WhatとWhyは決められないからです。この重心移動を組織として後押しできるかが、AIファーストになれるかどうかを分けると考えています。 この二つの方向は、どちらも「まだ道半ば」です。実際、ここまで書いてきたことの多くは、正直まだ「これから」の段階でもあります。社内FDEの立ち上げも、MCPゲートウェイの本格運用も、評価指標のTokenOptimizationへの移行も、走りながら具体化している最中です。この連載は完成した答えの発表ではなく、いま自分たちが立っている場所と、これから向かう方向をまとめた地図のようなものだと思っています。 おわりに 一つだけ確かなのは、ここで書いた「仕組み・かたち・つくり方」は一度作って終わりではなく、事業とAIの進化に合わせて組み替え続けるものだ、ということです。指標が役目を終えたら廃止するように、組織のかたちもソフトウェアのCoreも、更新され続ける前提で設計していく。その組み替えの一つひとつが、また次のブログのネタになるはずです。得られた学びは、折を見てまた書ければと思います。 エブリーは「AI ファースト・カンパニー」として、個人のAI活用から組織のAI活用へと軸足を移そうとしている最中です。ここで書いてきたテーマに面白さを感じてくれる方がいれば、ぜひ一緒に手を動かせればうれしいです。 この連載の全記事 オントロジーと組織OSとこれからのエンジニア 全社のAIファーストを牽引するエンジニアの役割 AIエージェントのハーネスとは何か AIファーストな組織デザイン 組織のAI活用を支えるMCPゲートウェイ レガシーシステムをAIでどうアップデートしていくか AI活用をどう評価するか AIエージェントを "動かす" データ基盤 AI時代、ソフトウェアは3層で考える AI ファースト・カンパニーへ —— AIブログリレーを終えて(本記事)
AI時代、ソフトウェアは3層で考える 株式会社エブリーでCTOを務めている今井( @imakei_ )です。 本記事は AIブログリレー 第9本目 です。これまで自分は、組織OSや組織デザイン、AI活用の評価といった「AIを活かす土台」を、主に組織の側から書いてきました。今回はその視点を、作られるソフトウェアの側に移します。 結論を先に言うと、AI時代は ソフトウェアの作り方そのものを変える 必要があり、その鍵は ソフトウェアを3つの層で捉え、「長く残す Core」と「捨てて作り直せる Disposable な部分」の境界をどう引くか にある、と考えています。そしてこの境界を引く作業は、組織の回で触れた「オントロジー」を、今度はプロダクトの世界観として定義していく作業でもあります。難しいですが、だからこそ面白い。順に書きます。 「作って長く保守する」前提が崩れる きっかけは、Tuan-Anh Tran氏の "Architecture for Disposable Systems" という記事です。要点はシンプルで、 コーディングエージェントによって生成が安くなると、ソフトウェアは「作って、使って、捨てる」ものへと変わっていく 、というものです。 これまでは「一度作って、長く保守する」のが当たり前でした。作り直しが高くつくからこそ、丁寧に設計し、負債を返し、長期を見据えて磨いてきた。ところが、エージェントが数分で同等の代替物を作れるなら、その前提は崩れます。生成が安くなると、むしろ「保守し続けること」のほうが高コストになる。壊れたら直すより、捨てて作り直す——そういう作り方が合理になっていきます。 だとすれば、 ソフトウェアの作り方も、この変化に合わせて組み替えないといけません 。全部を等しく丁寧に磨く発想から、「何を残し、何を捨てるか」を設計する発想へ、です。 Core と Disposable の境界を、どう引くか ここが本題です。当然ながら、すべてを Disposable にできるわけではありません。参考記事は、ソフトウェアを三つの層で整理しています。長く生き残る Core (耐久コア=中核のロジックやデータモデル)、それらをつなぐ Connectors (契約=インターフェース)、そしてその上で動く Disposable な層 (UIやグルーコード)です。 大事なのは、 Disposable な部分を安心して捨てたり作り直したりできるのは、Core と Connectors が堅いからこそ 、という点です。逆に言えば、AIに任せられる範囲を広げたい——つまりAIを活かしたい——なら、まず「捨てない中心」をきちんと定義することが先になります。境界が曖昧なままAIに作らせると、捨てていいものと捨てられないものが混ざり、結局作り直せなくなる。 AIを活かすための第一歩は、実は「何を残すか」を決めることなのです。 そしてこの「捨てない中心を定義する」作業は、連載で書いてきた オントロジー の話とそのまま地続きです。組織の回では、会社の現実を「名詞・動詞・ルール」として定義する話をしました。同じことをプロダクトの側でやると、 そのソフトウェアの世界観を定義する ことになります。何が中心的な概念(名詞)で、どんな操作(動詞)が許され、どんな制約(ルール)があるのか。この世界観こそが Core であり、それを外に見せる形が Connectors です。 世界観がきちんと定義されていれば、その上に乗るUIや繋ぎこみは、AIに生成させ、要らなくなったら捨てて作り直せばいい。 世界観(オントロジー)を定義することが、そのまま Core と Disposable の境界を引くことになる ——ここが、組織の話とソフトウェアの話がつながるポイントだと考えています。 とくにアプリ開発では、境界がくっきりする ここまでは一般論ですが、自分の出身でもあるアプリ(モバイル)開発では、この Core と Disposable な層の境界が、より生々しくはっきりします。 サーバーサイドと違い、アプリは 利用者の端末にインストールされ、古いバージョンが世の中に残り続けます 。サーバーのように「作り直して即差し替え」とはいきません。だからこそ、簡単には捨てられない中心が明確です。クライアントとサーバーの通信契約(=アプリにおける Connectors にあたる部分)、ローカルに溜まったデータのスキーマとマイグレーション、認証やセッションといったローカルの状態——このあたりは、Disposable にはできない Core です。ナビゲーションの構造も、Deep Link や通知の入口として外部と繋がっている限りは、簡単には作り変えられない Core 寄りの存在です。逆に、個々の画面やレイアウト、View まわりのコードは、生成して作り直せる Disposable な層に寄っていきます。 つまりアプリエンジニアの価値は、 一枚一枚の画面を手で磨くこと から、 アプリの世界観(ドメインモデル・状態の契約・デザインシステム)を定義すること へ移っていきます。ここで強調したいのは、これまで培ってきたUXやインタラクションへのこだわりが不要になるわけではない、ということです。むしろその感性を、 デザインシステムやコンポーネント、レビューの基準として Core 側に埋め込む 。そうすれば、生成された画面もその基準を満たすようになります。手で守っていた品質を、仕組みとして守る側に回る——これは、アプリエンジニアにとってむしろ面白い変化だと自分は思っています。 ここがいちばん難しい とはいえ、この境界を引く作業はかなり難しい。いくつか挙げます。 まず、 何を Core に置き、何を Disposable に落とすかの線引き そのものが難しい。間違えると、捨てられるはずの場所に大事なロジックが紛れ込み、まるごと作り直す羽目になります。 次に、 Connectors は「完璧に」保ち続ける 必要があります。Disposable 側を雑に作れるのは Connectors が堅いからこそで、「中は AI に任せて雑でいい」と「境界は完璧に」という 非対称な厳しさ を同時に成立させないといけません。 さらに、 データやユーザーの状態は簡単には捨てられません 。UIやロジックは作り直せても、蓄積されたデータや利用者からの信頼は作り直せない。捨てていいものと、絶対に捨ててはいけないものを見極める目が要ります。 そして最大の逆説は、 「作り直せる」前提が、かえって設計判断の重みを増す ことです。実装を磨く負担は減る一方で、「どこに境界を引くか」という判断の一発勝負の重要性が上がる。手を動かす難しさが、考える難しさへ移っていく、とも言えます。 だからこそ、これからのソフトウェアづくりは面白い この難しさは、そのまま面白さの裏返しだと思っています。 エンジニアの腕の見せ所が、 「実装をどれだけ丁寧に磨けるか」から、「世界観を定義し、何を残し何を捨てるかの境界を設計できるか」へ移る 。コードの美しさそのものより、捨てられる構造を見抜いて設計する力が問われる。かなり知的な挑戦であり、設計者としての醍醐味が詰まった仕事です。 忘れてはいけないのは、 Disposable を受け入れることは「雑に作る」ことではない という点です。むしろ逆で、「きれいに捨てられるように、世界観と境界を丁寧に設計する」という、より高度な仕事を求められます。 もう一つ、心構えとして大事なのは、 自分が書いたコードへの執着を手放せるか です。時間をかけて磨いたコードほど、捨てるのは惜しい。でも、これからは「惜しくて捨てられないコード」こそがリスクになり得ます。愛着の対象を、個々の実装から、それを生み出し続けられる世界観や境界のほうへ移していく。作品としてのコードから、更新され続ける仕組みとしての設計へ——この切り替えができる人にとって、これからのソフトウェアづくりはとても面白い時代になると思います。 おわりに AI時代のソフトウェアづくりは、「長く磨き上げる」一辺倒から、「世界観を定義し、Core と Disposable の境界を引く」営みへと変わっていきます。それは簡単ではありませんが、難しいからこそ、考えることそのものが面白くなる。組織で定義してきたオントロジーを、今度はプロダクトの世界観として引き直していく——その境界設計にこそ、これからのソフトウェアづくりの面白さがあると感じています。 出典・参考 本記事における「Disposable Systems」の考え方は、以下の記事を参考にしています。 - Tuan-Anh Tran, " Architecture for Disposable Systems "
こんにちは。開発1部の村上です。 本記事は AIブログリレー 8本目 です。 エブリーではAIエージェントを社内のあらゆる業務で活用していくことを目指しています。そのAIエージェントたちを支えるのがデータ基盤です。そしてこの基盤を、AIにデータを「読ませる」ためのものから、AIエージェントを「動かす」ためのものへ進化させていきたいと考えています。本記事では、そのために必要なステップを整理します。 人のための基盤から、AIエージェントを "動かす" 基盤へ これまでのデータ基盤は、暗黙のうちに「人が使う」前提で設計されてきました。ダッシュボードを見るのも、SQLを書くのも人です。「この売上には手数料が含まれているんだっけ?」という定義の曖昧さも、人が文脈と経験で補完してきました。 利用者がAIエージェントに変わると、この暗黙の補完が効かなくなります。さらにエージェントが増えていくことを前提に立つと、基盤は特定の誰かの道具ではなく、すべてのエージェントが立つ共通の足場になります。足場が曖昧なままエージェントを増やすと、曖昧さの解釈がエージェントの数だけ生まれてしまいます。 さらにAIエージェント時代のデータ基盤は、データを参照して質問に答えるといった、データを「読める」ようにするだけでは不十分だと考えています。データにもとづいて業務の中で行動するエージェントを支える、つまりAIエージェントを「動かす」ところまでを、基盤の責務として考えていく必要があります。 このような基盤には、次の3つのステップがあると考えています。 参照できる : あらゆるデータを収集し、AIが参照可能な状態にする(メダリオンアーキテクチャ) 解釈を間違えない : 指標や用語の意味を一元定義する(セマンティックレイヤー) 行動できる : 業務のオブジェクト・関係・アクションを機械可読にする(オントロジー) 順に見ていきます。 あらゆるデータを集め、AIが参照できる状態にする 最初のステップは、構造化・非構造化を問わずデータを収集し、AIが参照できる状態にすることです。ここで採用しているのがメダリオンアーキテクチャです。 メダリオンアーキテクチャは、データをBronze(生データ)、Silver(クレンジング・整形済み)、Gold(ビジネス利用可能)という層に分けて、段階的に品質を昇格させていく設計です。 (出典: What is a Medallion Architecture? | Databricks ) ポイントは「きれいなデータを一発で作る」のではなく、生データを捨てずに保持したまま、信頼できる状態へ段階的に到達させることにあります。AIエージェント時代には、この層の分離がもう一つの意味を持ちます。エージェントにどの層を見せるかを制御できることです。品質保証されたGold層だけをエージェントの参照先にすることで、生データの揺らぎに引きずられた回答を構造的に防げます。 このステップまで整うと、Text to SQLが動き始めます。「先月のチャネル別売上は?」と聞けばエージェントがSQLを書いて答えてくれる。人が結果を確かめながら使う形であれば、AI活用はここから十分始められます。 ただし、「参照できる」と「正しく解釈できる」の間には溝があります。テーブルとカラムが見えることと、その数字が何を意味するかを理解していることは、別の問題だからです。 AIが解釈を間違えない状態にする 2つ目のステップは、セマンティックレイヤーの整備です。指標の定義、ディメンション、シノニム、用語といったビジネス上の意味を、BIツールやエージェントの側ではなくデータ層で一元的に定義します。 たとえばCAC(顧客獲得コスト)の定義がエージェント間で曖昧だったとします。すると、事業KPI分析エージェントとマーケティングエージェントが異なる計算式のCACを見て動いてしまう、といったことが起こります。しかもどちらのSQLも正しいため、この食い違いに気づくのは困難です。 Goldに計算済みのCACテーブルを置く手もありますが、CACのような比率指標は計算済みの値を再集計できません。「全チャネル合算では?」と粒度の違う質問が来た瞬間、エージェントはCACの平均を取って間違えます。だから必要なのは計算結果ではなく、計算式の定義です。 セマンティックレイヤーは、この定義をデータ層で管理します。Databricksの実装で言えば Unity Catalog Business Semantics がこれにあたります。中核となるmetric viewは、測度(CACの計算式そのもの)と、それを集計するディメンションを分離して定義する仕組みで、SQL・ダッシュボード・エージェントのどこから利用しても、同じ定義から同じ計算が決定的に実行されます。加えて glossaryやdomains といった用語・文脈を整備する機能の発表も続いており、この領域への投資はプラットフォーム側でも加速しています。 効果は数字にも表れています。dbtが2026年に再実施した ベンチマーク では、Text to SQL単体の精度は最新モデルで64.5%まで改善した一方、セマンティックレイヤー経由ではカバーされた質問に対してほぼ100%に到達しています。モデルの進化だけでは埋まらない差が、定義の一元化で埋まるということです。 ここまで整うと、エージェントは指標を正しく計算し、要因を正しく分解できるようになります。「読む」はほぼ完成です。しかし、分析だけではなく行動まで促そうとすると、まだ決定的に足りないものがあります。 AIが行動できる状態にする オントロジーとは何か 3つ目のステップがオントロジーの構築です。このブログリレーの 1本目でCTOも紹介 していましたが、オントロジーとは、業務を構成する オブジェクト (顧客、受注、商品...)、その 関係 、そしてそれぞれに実行できる アクション を、機械可読な形で定義したものです。 もともとオントロジーは知識工学の用語で、古典的にはオブジェクトと関係の記述を指します。エンタープライズの文脈ではPalantirがこれを拡張し、オブジェクト・プロパティ・関係というセマンティックな要素に加えて、アクションという「世界を変更する操作」までをオントロジーに含めています(参考: Palantir Ontology )。 業界の現在地 この領域は、まさに製品化が始まったところです。Databricksも2026年6月のData + AI Summitで独自のオントロジーである Genie Ontology を発表しました(現在プレビュー)。テーブル・クエリ・ダッシュボードから事業の文脈を自動抽出して生きたグラフを構築するもので、Unity Catalogで定義したセマンティクスがこのオントロジーに供給される構造になっています。アクション定義までは含まないかもしれませんが、オブジェクトと関係の自動構築という意味で同じ方向に進んでいます。 複数のエージェントが、同じ地図の上で動く 例として、定期購入型のEC(サブスクEC)でオントロジーの一部を考えてみます。 構成要素は次の4つです。 オブジェクト : 顧客、定期契約、定期受注(第n回の個別のお届け)、商品、在庫引当、配送 関係 : 顧客は定期契約を持つ。定期契約は定期受注を生成する。定期受注は商品を含み、在庫を引き当て、配送に紐づく 状態 : 定期受注は「受付中 → 出荷確定 → 発送済み」と遷移する。「お届け日の2日前に出荷確定へ移る」という締切の定義もここに属する アクション : 各オブジェクトに対する操作。前提条件・効果・権限がセットで定義される 配送先を変更する(前提: 状態が受付中) メニューを差し替える(前提: 状態が受付中、かつ、おまかせコース) 次回分の変更を予約する(対象: 定期契約。いつでも可) このオントロジーの上で、複数のエージェントが動くとどうなるか。同じサブスクECで動くCSエージェントと在庫オペレーションエージェントがいるケースを考えます。ここで見たいのは、互いに無関係な2つの出来事です。ある日、CSエージェントには顧客から「配送先を変えたい」という問い合わせが届きます。一方、在庫オペレーションエージェントの側では、入荷遅延によって木曜出荷分の在庫が不足していました。別々のトリガで、2体はそれぞれ独立に動き出します。それぞれの動きを並べるとこうなります。 CSエージェント 在庫オペレーションエージェント 起きたこと 顧客「配送先を変えたい」 入荷遅延で木曜出荷分の在庫が不足 関係の辿り方 顧客 → 定期契約 → 定期受注 商品 → 在庫引当 → 定期受注 受付中の受注への行動 「配送先を変更する」を実行 「メニューを差し替える」で欠品を吸収 出荷確定済みの受注への行動 「次回分の変更を予約する」に切り替え、「次回分から新しい住所にお届けします」と案内 引当済みのため対象外。手を付けない この2体は、会話もしていなければ、仕事の引き継ぎもしていません。起点も違います。片方は顧客から、もう片方は商品から関係を辿り、同じ定期受注オブジェクトに到達しているだけです。そして2体が共通して参照しているのは、定期受注の「状態」と、その状態を前提条件に持つアクションの定義です。顧客対応と欠品対応という別々の業務が矛盾しないのは、「出荷確定後は変更できない」というルールがアクションの前提条件として基盤に一つだけ存在して、両方がそれを参照しているからです。 ルールを各エージェントのプロンプトやMCPツールの説明文に書けば済む、と思うかもしれません。実際、エージェントが1〜2体のうちはそれで動きます。しかしそれは定義のコピーです。10体に定義がコピーされた世界では、ルール変更のたびに10箇所の改修が発生し、直し漏れたエージェントは旧ルールで動き続けます。API側で変更を拒否することはできますが、それで防げるのは誤った実行までで、エージェントは「変更できますよ」と案内してから実行に失敗する可能性もあるかもしれません。オントロジーはこの知識を参照に変えます。エージェントが何体いても、参照先は1つです。 この構造の強さは、ルールが変わる日にはっきり現れます。出荷締切を前日から2日前に早める、という業務判断が下りたとします。変更するのは基盤上の定義1箇所。その瞬間から、CSエージェントの案内も、在庫オペレーションエージェントの組み替え範囲も、将来作られる何体目かのエージェントの行動も、すべて同時に新しいルールに従います。 3層の上でエージェントを動かす ここまでの3ステップを一度に眺めると、こうなります。ステップ1でデータが見える。ステップ2で数字を正しく読める。ステップ3で「何ができて、実行すると何がどう変わるか」が分かる。 ここまではCSと在庫オペレーションの2体で見てきましたが、この土台の上には、マーケティングエージェント、予実管理エージェント、と業務ごとのエージェントを並べていけます。全員が同じ指標定義を読み、同じオブジェクトとアクション定義の上で動く。データ収集だけを進めてエージェントを増やすと、指標・フロー・ルールの定義が各エージェントのプロンプトへサイロ化していきますが、3層が整った基盤の上では、すべてのエージェントが同じ世界を見て行動します。 エブリーはどう取り組むか エブリーではステップ1にこれまで投資を続けてきており、メダリオンアーキテクチャによる基盤は整いつつあります。ステップ3のオントロジーは技術的にもまだ新しく、すぐに全面適用できる段階ではありません。そこで今期は、ステップ3を意識しながら、ステップ2のセマンティックレイヤーを整備していきます。 このステップ2と3は、アプリケーションのコードと人の暗黙知に散在している世界の定義を、基盤上の宣言的で機械可読な一箇所へ引き上げていく作業です。「CACの計算式」も「出荷確定後は変更できない」という定義も、今この瞬間もアプリケーションの実装の中に、そしてオペレーションを担う人の頭の中に存在しています。 そして、ここが重要なのですが、この作業はデータチームが外から観測するだけでは完遂できません。「締切を何時にするか」「CACに何を含めるか」は、データの中に答えがある問題ではなく、ビジネスの意思決定そのものだからです。セマンティックレイヤーもオントロジーも、技術の問題である以前に、ビジネスと一緒に定義を決めにいく組織の問題です。だからこそ、このリレーの 1本目でCTOが書いた 、エンジニアが事業の中に入ってその基盤を作っていくという話になるのだと思っています。 基盤を育てることと、ビジネスと共に定義を決めること。この両方が揃ったとき、データ基盤はAIにデータを読ませる基盤から、AIエージェントを動かす基盤になります。 さいごに 本記事では、AIエージェントを「動かす」ためのデータ基盤を、参照できる・解釈を間違えない・行動できる、という3つのステップで整理しました。エブリーではこの基盤づくりをこれから本格化させていきます。進捗や学びは、またこのブログで共有していく予定です。 エブリーでは一緒に働く仲間を募集中です! エンジニアブログをきっかけに少しでも興味も持っていただけたら、まずはカジュアルに面談しましょう!