タイミーのブログ - TECH PLAY

TECH PLAY

タイミー

タイミー の技術ブログ

全321件

こんにちは、タイミーで開発生産性改善チームのマネージャーをしている新谷です。 みなさん Devin を使っていますか?タイミーでは日々 Devin が仕事をしてくれています。 Devinは単にタスクを依頼するツールにとどまらず、ソフトウェアファクトリーのフレームワークになりました。最近の進化が著しいので紹介させてください。本当にすごい。 ソフトウェアファクトリーとは LayerX CTOの松本さんが、 Xの投稿 で次のように述べていました。ソフトウェア開発は、人間がCoding Agentをどう使うかを考える段階を過ぎた。これからは開発プロセスそのものを、ループが閉じたプロダクトとして作る段階で、そこまでやって初めてファクトリーと言える、という主張です。 添付されていたスライドには、ループが3つ描かれています。 Inner Loop: 計画→実行→検証→補正を回して、1つのタスクを完了させる Outer Loop: 検証の結果から学習し、知識・記憶として次の計画に活かす Harness Optimization Loop: 人間の評価を、エージェントを動かすハーネスやプラットフォームの改善につなげる この記事では、3つのループが人の手を離れて回っている開発の仕組みをソフトウェアファクトリーと呼びます。以下は、その内容をもとに、自分なりに整理した図です。 成熟レベルはループがどこまで閉じているかで決まる 3つのループがどこまで閉じているかを、自分なりに段階に分けて整理してみました。 レベル 段階 状態 Lv.0 人がエージェントを使う 手元のエージェントに書かせて、人が見て直す Lv.1 Inner Loopが閉じている タスクを渡すと、エージェントが自分で動かして確かめ、直したうえでレビューできるPRを出してくる Lv.2 Eventで起動する 人がプロンプトを打たなくても、バグ報告・Issue・アラート・問い合わせをきっかけにエージェントが動き出す Lv.3 Outer Loopが閉じている 失敗やレビューでの指摘が知識になり、同じ指摘の繰り返しを減らせる Lv.4 Harness Optimization Loopが閉じている マージした、差し戻した、手で直した、といった人の評価を集計して、ハーネスそのものを改善する ループが閉じるほど、遅さの原因は人間側の承認フローに移っていきます。この成熟レベルを上げるには、人間がトリガーしなくても仕事が始まり、人間が全部を見なくても承認できることが必要です。 ループを増やすにはプラットフォームが必要 こうした仕組みは個別の部品を作るだけなら、GitHub Actionsなどの既存のツールでも実現できます。たとえば、イベントをきっかけにエージェントを動かし、結果を通知する、といった小さな自動化です。 ただ、小さなループをその都度作って積み重ねていくと、設定や実行状況を管理するのがだんだん大変になり、いずれ手に負えなくなる可能性が極めて高いと考えています。 本気でソフトウェアファクトリーを作るなら、トリガーとなるイベント、エージェントの行動、終了時や失敗時の通知などを共通の方法で設定・管理できる、一般化したプラットフォームが必要です。 *1 Devinはソフトウェアファクトリーのフレームワーク この面倒なところを肩代わりしてくれるのが、いまのDevinです。最高。 Devinの機能を少し前に提示した画像に当てはめると、次のようになります。 ここからは、Devinが持つパーツを順に見ていきます。 起動:オートメーションで仕事を始める 1つ目の部品はオートメーションです。Lv.2のEventで起動する仕組みを誰でも作れるようにしています。オートメーションは、次のような部品を組み合わせて設定します。詳しくは ドキュメント をご覧ください。 部品 内容 トリガー Slackの投稿やリアクション、GitHubのPR・Issue・CIの結果、セキュリティスキャン、決まった時刻、Webhookなど トリガーの条件 「labelがbugのときだけ」「このリポジトリだけ」のように、トリガーを絞り込む条件 プレフライトチェック イベントが条件に合ったあと、Devinを動かす前に通すフィルタ エージェントの定義 新しいセッションを始める、動いている既存のセッションにメッセージを送る、Slackチャンネルを見張るトリアージ役として動く 通知と上限 成功時・失敗時のメール、実行結果のSlack投稿、セッションごとのACUの上限、1時間あたりの実行回数の上限、同時実行数を絞るキュー、ネットワークの制限 人間がトリガーしない仕事を回すには、失敗時の通知、暴走したときのコスト上限などのガードレールが必要です。気を利かせてくれていて良いですね。 プレフライトチェックは自然言語で書けるフィルタ これもあるのかと感心したのがプレフライトチェックで、LLM filterとスクリプトの2種類から選べます。 LLM filterでは、通過させたいイベントを「認証サービスに関するPR」「新谷へのSlack連絡」のように普通の文章で書きます。すると、decision modelがイベントを1件ずつ読んで、合わないものをスキップします。少し前に話題になったTypeSafeのJevや、OpenAIのDecisions APIのように、文章を生成せずに判定だけを返すモデルを使ったフィルタが、サクッと使えるわけです。 スクリプトを選ぶと、スクリプトがセッションとは別の短命なVMで動きます。イベントを読んで、実行するかスキップするかを返すほか、1つのイベントを複数の作業に分けて、作業ごとにセッションを立ち上げることもできます。 最初は自動化の提案から始める とはいえ、部品がそろっていても、何を自動化すればいいかはすぐには思いつきません。スターターキットでいえば組み立て説明書にあたるのが、オートメーションの作成メニューにある Devin におすすめの自動化を提案してもらう機能です。 ボタンを押すと「Automations picked for you」というDevinのセッションが起動します。最初のメッセージは自動で入り、一番役に立つ自動化を提案し、その提案をSlackのDMでも送るようDevinに頼む内容です。Devinはユーザー自身のSlack・GitHub・これまでのDevinのセッションを見て、どんな自動化を導入するとよさそうかを、そのセッションの中で提案してくれます。 Devinのセッションは通常課金対象です。ところがこの機能を通じて起動したセッションはこちらからメッセージを送信しない限りは課金されません。Devinに提案してもらう分は無料なので、全人類やるべきだと思います。一方、オートメーションが有効化されれば、その分はDevinの費用として計上されるわけです。提案を無料にするのは、プロダクトとしてよくできているなと感心しました。 しかも、自動化の提案には、確かに自分が普段やっているな、と思う作業が出てきます。加えて、PRを作るだけで終わらず、Slack投稿やメール通知までがオートメーションの内容になってます。設定するだけで、その仕事からある程度解放されるわけです。 実行・検証・承認:変更を出荷できる状態にする Inner LoopはDevin Reviewが回す 11月に出た Devin Review 2.0 では、8月と比べてPR1件あたりのレビューコストが82%下がりました。 Devinが作ったPRでは、Devin Reviewの指摘をDevinが調べ、指摘ごとに直すか、見送るか、人間に回すかを判断します。人が戻ってきたときには、人の注意が必要な問題や判断に集中できるわけです。 ただ、最初のうちはレビューの指摘がいつも正しいとは限りません。Devin Reviewでは、リポジトリの REVIEW.md にレビューの指示を書けます。重点的に確認したい箇所やプロジェクト固有のルールを書いておくと、Devin ReviewがPRの分析時に参照します。 ここでオートメーションの出番です。たとえば、週に1回Devin Reviewの指摘を集計し、レビューの指示を直すPRを作るオートメーションを用意すればいいわけです。そうすれば、Devin Reviewのチューニングも半自動で回せます。Devinが指摘を直すところまでがLv.1のInner Loopで、レビューの指示を直し続けるのはLv.3のOuter Loopを閉じる動きです。 Devin Securityもオートメーションに載る 10月に出た脆弱性診断を行ってくれる Devin Security 2.0 も、オートメーションと組み合わせられます。オートメーションからコードスキャンを定期的に走らせたり、新しいコミットが入ったら再スキャンしたりといった使い方です。SnykやWizなど他のツールの指摘を取り込むこともでき、指摘ごとにDevinが修正のPRを作ります。 いろいろな脆弱性の情報をきっかけに、修正まで自動で進む状態を作ることができます。 承認はAutoapproverで仕分けする もう1つすごいと思ったのが、Devin Reviewの設定にあるAutoapproverです。ルールで対象のPRを選ぶと、Autoapproverが自動で承認するか、人にレビューを回すかを判断します。 AUTOAPPROVE.md には、チームが自動で承認してよいと考えるものや、必ず人の確認が必要なものを記載できます。 これを使えば、人間が確認するものとAIが自律的に承認してよいものの線引きを、フレームワークに乗った形で決められます。自動化を増やしていくと、最後は「誰が✅を出すか」が詰まるわけで、そこを自社で一から考えなくていいわけです。 学習・改善:実行結果を次に活かす Outer LoopはMemoryとscratchpadが回す Outer LoopはInner Loopの検証から出て、学習を通り、知識・記憶として次の計画に戻ってきます。Devinでこれにあたるのが、10月に発表された Memory and Dreaming と、オートメーションのscratchpadです。 Memoryは、Devinが仕事をしながら学んだことを短いメモとして残し、次のセッションに持ち越す機能です。ユーザーが訂正した前提や、何度か試してようやくうまくいった環境の落とし穴などが残ります。Dreamingは毎日動くバックグラウンドのセッションで、過去の会話とメモを見直して、重なったメモをまとめたり、使われていない古いメモを消したりします。 ただ、Memoryは組織で共有されるものではなく、ユーザーごとのものです。そこで真価を発揮するのがscratchpadです。scratchpadはオートメーションの種類ごとに共通で使える長期メモリーです。前の実行で詰まったことをここにメモしておくと、次の実行で同じ問題を避ける手がかりになります。イベント駆動で何度も動くオートメーションにとっては、実行をまたいで学びを持ち越すOuter Loopそのものです。 Harness Optimization Loopもオートメーションで組み立てられる Lv.4のHarness Optimization Loopには、専用の機能があるわけではありません。ただ、Devinの部品を組み合わせれば、既存の機能で半自動までは組み立てられます。 たとえば、スケジュール実行のオートメーションを1つ用意します。毎週、イベント駆動のオートメーションが実行したセッションを見直し、精度が微妙なものや、もっと少ないターン数で終わらせられそうなものを探します。そのうえで、ハーネスの改善を提案させます。直す先は、オートメーションの設定かもしれないし、コードベースの中にあるハーネスかもしれません。 ただ、提案を採用するかどうかは、今のところ人が判断する部分が大きいと感じています。何を良しとしてハーネスをどう変えるかは、まだ人が介入する余地があります。 これからAI活用を進めていく会社ほど効果的 このフレームワークは、AI活用がそこまで進んでいない会社にとって特に便利だと考えています。トリガー、通知、レビュー、セキュリティ、承認の線引きまでがひとつながりの機能として用意されているので、仕組みをすべて自前で作る必要がありません。自動化の提案をもらい、気に入ったものを設定するところから始めれば、その会社のAI活用レベルを一気に底上げできるのではないでしょうか。 将来、Devinを使い続けずにソフトウェアファクトリーを内製することにしたとしても、まずはDevinを試して、お手本となる仕組みを学ぶのは効果的です。Devinを使い続けるかどうかにかかわらず、一度試してみることをおすすめします。 まとめ Devinは、ソフトウェアファクトリーのフレームワークになっています。起動にはオートメーション、Inner LoopにはDevin Reviewと REVIEW.md によるレビュー指示、承認にはAutoapproverとAUTOAPPROVE.md、Outer LoopにはMemoryとscratchpadがそろっています。これらの部品を組み合わせれば、Lv.2〜3に向けて一気に進めるのではないでしょうか。Lv.4のHarness Optimization Loopも、定期的なオートメーションで見直しと提案までは組み立てられ、人は判断に集中できます。 …と、ここまでDevinの機能や組み合わせ方を紹介してきましたが、タイミーでも、これらをすべて活用できているわけではありません。これから活用を広げていきたいと思っています。 *1 : これをやりきっている LayerX は本当にすごい https://speakerdeck.com/layerx/built-our-own-background-agent-at-layerx-number-aidevex-findy
こんにちは!このたびタイミーは、2026年10月16日(金)・17日(土)にベルサール渋谷ガーデンで開催されたKaigi on Rails 2026に、Rubyスポンサーとして協賛し、ブースを出展いたしました。 kaigionrails.org 素敵な場をつくってくださった運営のみなさま、セッションやLTを聴いてくださったみなさま、そしてブースにお越しいただいたみなさま、ありがとうございました!また、420名以上の方がタイミーのブースに立ち寄ってくださいました。 タイミーのエンジニアはKaigiPassという制度を利用して参加しました。この記事では、ブース企画・スポンサーLT・ブースでいただいた質問のご紹介に加え、参加メンバーが印象に残ったセッションをレポートします。 執筆者はこちらのメンバーです。 バックエンドエンジニア:神山(X:@ dak2 ) バックエンドエンジニア:阿部(X:@abe_actors) バックエンドエンジニア:志賀(X:@akitoshiga) プロダクトエンジニア:四方(X:@shikatadesu) DevEnable室:maho,masa,meg ブース企画の紹介 タイミーは、「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービス「タイミー」を開発・運営しています。プロダクトの中核にはRuby on Railsがあり、今回のブースでは「何を使っているか」だけでなく、「なぜその設計判断をしたのか」をエンジニアと直接お話しできる場を目指しました。 企画①アーキテクチャ図とドメインの紹介 タイミーのシステム構成に事業ドメインを重ねたアーキテクチャ図を展示しました。事業の課題とシステム構成・設計判断がどうつながっているのかを、図をご覧いただきながらエンジニアがご説明しました。 企画②データ設計クイズ「あなたならデータをどう持つ?」 タイミーで実際に起こりうる場面をお題に、「あなたならデータをどう持つ?」を4つの選択肢から選び、シールを貼って投票していただきました。正解・不正解ではなく、「なぜその選択肢を選んだのか」をきっかけに、設計の考え方についてお話しする企画です。 ご回答いただいた方には、お礼としてガチャを1回まわしていただき、景品をお渡ししました! お題①求人の条件が変わったら? 時給1200円の求人に申し込みが完了。その後、お店が1300円に編集しました。あなたならデータをどう持つ? 選択肢 得票傾向 1. 求人を更新し、契約も同じデータを参照する 少なめ 2. 申し込み時点の条件を契約として保存する 多い 3. 求人の変更履歴から、その時点の条件を復元する 少なめ 4. 求人を版管理し、契約から該当する版を参照する 最多 「申し込み時点の条件を残す」という点では多くの方の考えが一致し、その残し方として、契約側にコピーするか、求人を版管理するかで票が分かれました。 お題②勤務時間の修正を申請されたら? 予定より長く働いたのでワーカーが勤務時間の修正を申請。お店が内容を確認し、承認・拒否します。あなたならデータをどう持つ? 選択肢 得票傾向 1. 勤務に確定時間・申請中の時間・承認状態を持たせる 中くらい 2. 勤務とは別に修正依頼を作り、申請内容と承認状態を持たせる 最多 3. 報酬に変更希望額と承認状態を持たせる 少なめ 4. 汎用の承認申請データを作り、勤怠修正もその一種として扱う 少なめ 「修正依頼」を勤務とは別の概念として切り出す案に票が集まりました。一方で、勤務に状態を持たせるシンプルな案や、承認フローを汎用化する案を選ぶ方もいて、それぞれの理由をお聞きするのが楽しかったです。 スポンサーLTの紹介 10月16日(金)には、志賀がスポンサーLTに登壇しました。 タイトル:スキマバイトという雇用形態は存在しないので、雇用期間1日の1日単位の雇用契約を事業者とワーカーが結ぶ手続きを支えるのがタイミーなんです 資料: Speaker Deck スキマバイトは、実は「雇用期間1日の1日単位の雇用契約」です。1日10件のマッチングがある事業者なら、毎日10人が「入社して退職する」ことになります。このLTでは、そんなスキマバイトを実現するために、タイミーが裏側で担っている手続きを紹介しました。 労働条件通知書の作成・交付 働く上での確認事項への対応(検便・資格など) 税務帳票や法定三帳簿の発行 報酬の計算・立替払い あわせて、モジュラーモノリスの本体APIを中心に、労務・税務、報酬計算、出退勤、マッチング、請求・与信など多様なドメインが広がるシステムアーキテクチャや、サービスの改善余地、今後の事業領域の広がりについてもお話ししました。 LTのあとには、ブースで「LTの続きを聞きたい」と声をかけてくださる方もいらっしゃいました。 ブースでいただいた質問と回答 ブースで特に多くいただいた質問と、その回答をご紹介します。 ブースでは、スキマバイトを実現するために「何を、どう実現しているのか」を説明する場面が多くありました。スポンサーLTで仕組みの前提を共有できていたことも、会話のきっかけになったようです。 特に「検便」の話には、想像以上に反応がありました。飲食店などには、働くにあたって検便の結果が陰性であることの確認を必須としている事業所もあります。健康診断などで検便を経験したことがある方も多いと思いますが、タイミーがこうした確認にも関わっていることは意外だったのかもしれません。 技術選定について 「なぜID基盤にGoを使っているのか」「データベースには何を使っているのか」といった質問もありました。 【回答】 ID基盤を既存のRailsアプリから独立させる際、Goは言語仕様がシンプルで、必要な機能だけを実装しやすいと考えました。型安全性・保守性の面でも長期運用に向いていると判断し、Goを採用しました。 また、OAuth 2.0/OIDC の認可サーバーに採用した Ory Hydra も Go 製で、必要に応じて内部実装をコードリーディングで理解しやすい点もメリットでした。 本体APIシステムのデータベースにはAWS RDS の Aurora MySQLを使用しています。 HandbookとAIの活用について Handbookに関連して、AIをどのように活用しているかという質問もありました。 【回答】 タイミーでは、設計・実装・運用のガイドラインをまとめたHandbookを、Claude CodeやCursorで利用できるスキルとして提供しています。たとえば概念モデリングでは、AIが必要なガイドラインを参照し、ドメインの境界やビジネスルールを質問しながら、開発者と一緒に設計を進めます。答えを出すだけでなく、「なぜそう考えるのか」や参照元も示し、人間の理解を置き去りにしないことを大切にしています。 ご興味のある方は下記の記事をご覧ください! バックエンド開発Handbookを届けるために ― AI時代の知の高速道路を敷く - Timee Product Team Blog セッションレポート ここからは、KaigiPassで参加したメンバーが、印象に残ったセッションをご紹介します。 神山 【Domain Modeling with Vanilla Rails: 「操作」から再考するRailsの設計論】 Domain Modeling with Vanilla Rails: 「操作」から再考するRailsの設計論 | Kaigi on Rails 2026 [JA] Domain Modeling with Vanilla Rails: 「操作」から再考するRailsの設計論 - Speaker Deck 【セッションの概要】 “操作”を元にモデルの責務を再考していくセッションでした。ドメインモデリングが肥大化する要因とそれを打開するための着眼点として”操作”に着目してドメインを構築していく流れがありました。 【印象に残ったポイント】 モデルの CRUD 操作を中心とするアプローチでは、更新系のユースケースに固有の振る舞いが同じモデルに蓄積することがモデル肥大化の原因であるということ。まさにそうだな、上手い整理だなと思いました。操作のまとまりが独立した概念にまとめられるなと思うことはよくありますよね。あとはその主体がなんなのか、命名に気をつけたいですね。 Kaigi on Rails 2025 の joker さんの 今改めてServiceクラスについて考える 〜あるRails開発者の10年〜 - Speaker Deck の発表と合わせて読むと、より責務に対するメンタルモデルが育まれそうです。 【 How to Design Configuration for Rails Applications 】 How to Design Configuration for Rails Applications | Kaigi on Rails 2026 https://speakerdeck.com/willnet/how-to-design-configuration-for-rails-applications 【セッションの概要】 Rails は CoC とはいえ、設定をいくらか記述するからうまいことまとめたい。そのためにどういう設計をすれば良いのか。実際のケーススタディを見ながら、思考を育んでいくような発表でした。 【印象に残ったポイント】 起点・中継点・終点という捉え方が非常に参考になりました。起点と中継点がセットになることが多い点も印象的でした。クラスを分けることで、ユニットテストを書きやすくなり、環境ごとに管理もしやすくなるのは納得です。一つ思ったのは、クラス分けするメリットは十分に理解できるんですが、設定に関する参照先って一つにしたいんだよなという考えが自分の中にあります。設定の関心事によってクラスを分けられると思います。ただ、アプリケーションコードだとクラスを分けることに違和感はないんですが、設定に関してはなんか参照先あんまり分散させたくないんだよなという気持ちがあります。なんでだろ。自分の中でまだなんとなく腹落ちしてないので、誰か示唆をください。 【 Web Framework Benchmark Battle 2026, Any% 】 Web Framework Benchmark Battle 2026, Any% | Kaigi on Rails 2026 https://speakerdeck.com/a_matsuda/web-framework-benchmark-battle-2026-any-percent 【セッションの概要】 今回一番印象に残ったセッションです。DHH の Keynote は Rails World 2026 の内容とほとんど同じだったし、Matz のセッションも Spinel いいなという感じでした。という中での matsuda さんの発表は痺れました。Spinel on Rails を実現するための Pines。その過程でのワークアラウンドなどの紹介がありました。 【印象に残ったポイント】 正直、DHH の Rails World 2026 を見て、Rails ってどうなるんや…という気持ちを持っていました。そこでの matsuda さんの Spinel on Rails の話です。Rails の伸び代まだまだあるなという気持ちになりました。matsuda さんが Spinel と Rails に言及されたという意味も大いにあったと思います。Spinel コンパイルの型推論が長すぎて、型のエコシステムの重要性が上がってきているのも個人的にアツいポイント。本当に聞けてよかった。 【自分やチームの経験との比較、今後活かしたいこと】 設計の話は意識していきたいですね。コードはあまり見なくなったとしても、I/F や責務はきちんと設計したいという気持ちがあるんですよね。個人的には Spinel on Rails に注目してるので、型のエコシステムにも貢献していきたい。 阿部 【 現場と育てるRailsモデリング 〜Spreadsheetから始めたモデル設計〜 】 https://kaigionrails.org/2026/talks/siroemk/ https://speakerdeck.com/siroemk/genba-to-sodateru-rails-moderingu-spreadsheet-kara-hajimeta-moderu-sekkei 【セッションの概要】 校舎ごとに異なるSpreadsheetで管理されていたレッスン報告を、段階的にRailsアプリへ移行した事例です。いきなりシステム化するのではなく、まず全校舎で必要な項目に絞った共通Spreadsheetを作り、半年間の運用を通して業務概念を整理していく過程が紹介されました。 【印象に残ったポイント】 単に「アプリ化する」「Spreadsheetを統合する」という当初の要求だけに着目するのではなく、共通Spreadsheetを、現場とエンジニアが一緒に業務を理解するための探索環境として活用していた点が印象に残りました。 運用を重ねる中で、登壇者自身もドメインエキスパートとして業務知識を蓄え、何が本質的な課題で、どのような情報が必要なのかの解像度を高めていました。このように、現場とともに業務を理解しながらシステムをデザインする力は、今後のエンジニアにより一層求められると感じています。 データの形をそのままシステムへ移すのではなく、現場での使われ方や目的を理解することで、作るべきモデル自体が変わる点も大きな学びになりました。 【自分やチームの経験との比較、今後活かしたいこと】 この取り組みは、タイミーが大切にしている「越境」にも通じると感じました。 エンジニアが現場から要件を受け取るだけではなく、自ら探索やユーザーインタビューに参加し、課題や作るべきものを発見していくことを、私たちも日々大切にしています。 最初から完成したモデルを決めるのではなく、小さく作ってすぐに見せ、フィードバックを受けながらモデルを育てていく。 今後の開発でも、このような探索のプロセスを大切にしたいです。 【 AIエージェントのデバッグ能力を向上させよう — AI時代にRailsが輝く理由 】 https://kaigionrails.org/2026/talks/rira100000000/ https://speakerdeck.com/player/f28cbf18ddf349c8bba5405fd7866ca5 【セッションの概要】 AIにデバッグを任せると、確信のないまま修正して、人間が動作確認する「不毛なループ」が起きがちです。このセッションでは、debug gemを使って外からデバッガをつなぎ、MCPサーバー(debug-mcp)経由で、人間がデバッガで確かめていることをAIにもやってもらう方法が紹介されました。 【印象に残ったポイント】 デモでは、レガシーデータが原因で起きているバグを題材にしていました。AIがモデルから「emailが読み取り専用」であることを確認し、ブレークポイントを置いて更新処理を追うことで、実際にはemailが書き込まれていないことを突き止めていました。 これまで人間が使っていたbindingなどのデバッガをAIが使えるようになれば、デバッグをより確実に進められますし、ファクトがある状態のほうがAIも調査しやすいですよね。 (そういえばAIが登場する以前から、アタリをつけたうえでbindingなどで調査していましたね。) AIに特別な仕組みを用意するのではなく、人間がやってきたデバッグの手順をそのままAIに渡すという発想が学びになりました。 【自分やチームの経験との比較、今後活かしたいこと】 debug-mcpという道具そのものより、「人間が確かめるのに使っていた手段をAIにも渡す」という考え方が大事だと感じました。 イベントログや監視基盤のログなど、普段の確認に使っているものをAIに渡せば、検証もAIと一緒に進められそうです。どこまでAIに任せるかを決めたうえで、小さく試していきたいですね。 参加メンバーの一言感想 志賀: スポンサーLTを聞いてくださった皆さんありがとうございました!!! Ruby on Rails最高!!! 四方: タイミーのブースへお越しいただいた皆さま、ありがとうございました! セッションの内容や、ブースで伺ったエンジニアの皆さまの働き方を通じて、AIの業務活用が広がる今だからこそ、要件を受け取ってRailsで実装するだけでなく、開発と業務現場の境界を越え、ともに課題を見つけていくことの大切さをひしひしと感じました。 コードを書く楽しさを教えてくれたRailsへの感謝を胸に、これからも現場とともにプロダクトを育てていきたいと改めて思いました。 タイミーの開発文化やサービスについてもっと知りたい!という方は、ぜひお気軽にXのDMなどでお声がけください。オフラインイベントも定期的に開催しています! おわりに ここまで読んでくださり、ありがとうございました! 2日間を通じて、ブースでは設計判断やドメインについて多くの方とお話しでき、セッションからもたくさんの学びを持ち帰ることができました。 これからもRuby・Railsコミュニティを盛り上げていけるよう、継続して関わっていけると嬉しいです。 最後になりますが、タイミーでは一緒にはたらく仲間を募集しています。ご興味をお持ちいただけましたら、ぜひカジュアル面談にお申し込みください! プロダクト組織の概要や募集職種 カジュアル面談のお申し込みはこちら
はじめに こんにちは、株式会社タイミーでプロダクトAIエンジニアとして働いている貝出です。直近は、LLMを利用した求人審査AIの開発や性能改善を行っています。 LLMを使った機能を開発していると、LLMの生成文を実際にユーザーに見せるケースがあります。そのときには「生成文に問題がないか」を評価する必要があります。解決策の一つとして、生成文をLLMに評価させる “LLM-as-a-Judge” があります。 ただし、LLM-as-a-Judgeを導入するには、そのLLM-as-a-Judge自体が意図通りに評価できているかを事前に確かめる必要があります(以下、この評価をメタ評価と呼びます)。メタ評価用のデータをきちんと用意しようとすると、思った以上に準備に時間がかかります。 本記事では、メタ評価用の正解データを事前に作り込むことをあえてやめた取り組みを紹介します。小さく始めて、運用の中で評価基準を育てていきました。「LLM-as-a-Judgeを入れたいけれど、正解データを作る余裕がない」という方の参考になれば幸いです。 💡 前提・おことわり 本内容は自社プロダクトにおける実践例の一つで、汎用的な最適解を保証するものではありません。取り組みの参考としてご覧ください。 背景:なぜLLM-as-a-Judgeが必要だったか 求人審査AIと「判定理由」「判定箇所」 タイミーでは、求人審査AIによって掲載前の求人を確認しています。目的は次の2つです。 掲載基準に適合しない求人や高リスクな求人が、ワーカーの目に触れるのを防ぐこと 審査オペレーターの判断や、事業者による求人の修正を支援すること 求人審査AIは、判定結果だけでなく「判定理由」と「判定箇所」も生成します。審査オペレーターは、これを見て判断の根拠を確認できます。求人を作成した事業者も、どの記載に問題があるのかを理解でき、自分で修正するときの参考にできます。 評価したいものと、使いたい場面 判定理由や判定箇所が誤っていると、オペレーターの確認や事業者の修正をかえって迷わせてしまいます。そこで我々のチームでは、「判定理由」と「判定箇所」の品質を評価する仕組みを作ることにしました。想定した活用シーンは次の2つです。 オフライン検証 :新しいモデルやプロンプトが、満たすべき最低ラインを下回っていないかを確認する(ガードレール指標としての利用) 本番モニタリング :本番環境で求人審査AIがおかしな挙動をしていないかを検知する 従来の手法では難しかったこと まず考えたのは、ルールベースと人手評価です。 ルールベースでは、「判定理由が読み手にとって納得できるか」のような主観的な妥当性の評価が課題になります。何をもって妥当とするかを、明確な判定条件として定義することが難しいためです。 人手評価は、評価の質という面では理想的です。しかし判定理由の文量も求人の数も多く、すべてを人が確認するのは現実的ではありません。評価者ごとのブレや、確認作業そのものの運用負荷も無視できません。 そこで、評価観点をYes/Noの質問に分解し、その質問にLLM-as-a-Judgeが答える形で自動評価する仕組みを導入することにしました。 課題:メタ評価用データセットを準備する時間 当初の想定 当初は、LLM-as-a-Judgeの品質を事前にメタ評価しながら改善する計画でした。Guら( arXiv:2411.15594 )は、LLM-as-a-Judgeに関するサーベイの中で、LLM-as-a-Judgeの評価では人間の判断との一致度が最も基本的な観点になると整理しています。多くの研究が、LLMを仮想的なアノテーターとみなし、人間のアノテーターとの一致率などで測っているとも述べています。そこで我々も、人間が付けた正解ラベルとの一致を確認しながら改善する方針としました。チームでは、具体的な件数や手順を次のように計画しました。なお、これらは論文で規定されたものではありません。 評価指標ごとに、網羅性の高いデータを30〜50件ずつ集める アノテーションガイドラインを整備する 訓練したアノテーターにラベル付けしてもらう そのラベルにLLM-as-a-Judgeの判定が合うように調整(アライン)する 評価指標は、TnS(Trust and Safety)チームのドメインエキスパートと議論して3つに決めました。決まった指標はドメイン知識を要求しないものだったので、アノテーションはチーム内のメンバーが担当する想定でした。 直面したハードル ところが計画を立ててみると、1指標あたり30〜50件という件数自体は多くないものの、3指標分で計90〜150件になります。さらに、その前段にもまとまった時間が必要だとわかりました。データの選定、ガイドラインの作成と調整、アノテーターの訓練、アノテーションの質の担保といった事前準備です。 しかも、この準備はメタ評価を始める前に終わらせておく必要があります。準備の間は、チームの他の開発が止まります。当時は、メタ評価の事前準備にまとまった時間をかけるよりも、他の開発を進めることを優先していました。 正解データがないとLLM-as-a-Judgeを信頼できない。かといって、正解データを作る時間も取れない。このジレンマをどう解消するかが課題でした。 解決方針:厳密さを割り切り、負荷を分散して運用で育てる 何を割り切ったか 最終的に、メタ評価データを事前に作り込むのはやめて、次の2点を割り切ることにしました。 厳密さの割り切り :ガイドラインの整備やアノテーターの訓練は行わず、筆者1人が少数の事例を見て判断する。統計的に精度を保証することは目指さない。ただし、見つかった問題点と改善方針はチームのレビューを受けてから反映する。 負荷の分散 :評価を始める前にまとめて時間を取るのではなく、隔週の少量の確認として日々の開発の中に分散させる。 割り切っても問題ないと判断した理由 今回、事前準備を簡略化したのは、求人審査AIそのものの評価ではなく、「判定理由」と「判定箇所」の品質を評価するLLM-as-a-Judgeのメタ評価です。 この判断の前提は、 LLM-as-a-Judgeの結果を、アラート通知やリリース可否の判断には使わないこと でした。本番モニタリングでは参考値として記録するだけにし、オフライン検証でも参考値として扱いました。これにより、Judgeが誤判定しても、その結果がアラート通知やリリース可否を直接左右しない運用に限定しました。 このように評価結果の使い道を限定したうえで、厳密なメタ評価を運用開始の条件にはしませんでした。ただし、今後アラート通知やリリース可否の判断に使う段階では、改めて厳密な検証が必要です。 運用しながら育てる流れ ベースラインのプロンプトを作ったら、すぐに本番トレースでの実行(本番環境での運用)を開始しました。判定結果は参考値として記録するだけです。そのうえで隔週で、評価指標ごとに6件だけ判定を目で確認し、人間の感覚とズレた事例をもとに、プロンプトと評価観点の両方を見直しました。手順の詳細は後半の「運用ループ」で説明します。 図:LLM-as-a-Judgeを運用しながら改善するループ まとめると、今回の方針は次の一文になります。 💡 完璧な正解データを待たず、結果の使い道に見合った厳密さで小さく始め、評価基準自体を運用を通じて育てる。 実践プロセス:LLM-as-a-Judgeの構築と運用ループ ※ 具体的に何を評価しているかは、今回のブログではスコープ外とします。以下の例はすべて説明用に簡略化・架空化したものです。 LLM-as-a-Judge プロンプトのベースライン プロンプトは、Leeら( arXiv:2403.18771 )が提案するCheckEvalを参考にしました。CheckEvalは、評価基準を細かい観点に分解し、それぞれをYes/Noで答えられる質問にしてLLMに回答させる枠組みです。論文では、質問に分解することで評価者モデル間の一致度が高まり、判断の根拠をたどりやすくなると報告されています。今回はこれを簡易化し、次の手順で作成しました。 何を評価したいのかを言語化する それを判定するための観点を複数に分解する 各観点を、できるだけ判断がブレにくい具体的なYes/Noの質問にする 質問ごとに理由とYes/Noを出力させ、すべてYesならPass、1つでもNoならFailとする たとえば「判定理由が原文に基づいているか」を評価したい場合は、次のような質問に分解します。 Q1. 判定箇所として示された文言は、求人原文に含まれているか Q2. 判定理由は、判定箇所の内容について述べているか Q3. 判定理由は、原文に書かれていない事実や推測を根拠にしていないか タイミーの求人掲載の注意事項を参考に、架空の求人で考えてみます。 項目 内容 求人原文 「ホールスタッフ募集(男女問わず)。未経験歓迎」 判定箇所 「男女問わず」 判定理由 「募集対象を女性に限定しており、男女雇用機会均等法に抵触するおそれがある」 判定箇所の文言は原文にあります(Q1: Yes)。判定理由も募集対象の性別について述べています(Q2: Yes)。しかし「女性に限定」という内容は原文のどこにもなく、むしろ原文と逆の事実を根拠にしています。そのためQ3はNoとなり、結果はFailです。 { " Q1 ": { " reason ": " 判定箇所の文言は原文に含まれている ", " answer ": " Yes " } , " Q2 ": { " reason ": " 判定理由は募集対象の性別について述べている ", " answer ": " Yes " } , " Q3 ": { " reason ": " 原文は『男女問わず』であり、女性に限定する記載はない ", " answer ": " No " } , " result ": " fail " } 質問ごとに理由を残しておくと、どの観点で引っかかったのかを後から追えます。これは後述する運用ループで、人間とLLM-as-a-Judgeの判断がズレた原因を分析するときにも役立ちます。 また、Self-Enhancement Bias(LLMが自分と同じモデルの出力を高く評価しがちな傾向)への対策として、LLM-as-a-Judgeには生成モデルとは異なるモデルファミリーを使用しています。 運用ループ(隔週サイクル) 運用ループでは、隔週で次の4ステップを繰り返しました。 実行 :本番トレースから一定割合をサンプリングし、LLM-as-a-Judgeを継続的に実行する 収集 :LLM-as-a-Judgeの判定結果から評価指標ごとにPassとFailを3例ずつ(1指標あたり6例)ランダムに抽出し、筆者が目で確認して、人間の判断とのズレを探す 定性分析 :ズレた事例について、なぜ意図通りに評価できなかったのかを分析する 改善 :問題点と改善方針をまとめてチームに共有し、レビューでの指摘を踏まえてプロンプトや評価観点に反映する 1指標あたり6例と少なくしたのは、目的が統計的な精度の推定ではなく、ズレのパターンを見つけることだったからです。日々の開発に影響が出ない量に抑えました。 事例の確認と分析は筆者1人で担当しました。一方で、改善方針が独りよがりにならないよう、反映前には必ずチームのレビューを挟みました。 定性分析のイメージを、先ほどのQ3で例示します(架空の例です)。 項目 内容 求人原文 「営業マン募集。ルート営業のため未経験でも安心」 判定箇所 「営業マン」 判定理由 「『営業マン』は男性を表す職種名であり、一方の性別を対象とした募集と受け取られるため、男女雇用機会均等法に抵触するおそれがある」 LLM-as-a-JudgeのQ3 No(理由:「原文には募集対象の性別に関する記載がない。『営業マン』が男性を表すというのは、原文に書かれていない推測である」) LLM-as-a-Judgeの結果 Fail 筆者が期待する判断 Q3はYes(結果はPass) 原文に性別を指定する記載がないのは、LLM-as-a-Judgeの指摘どおりです。しかし、「営業マン」という記載が法令や掲載基準に照らしてどう評価されるかを説明することは、判定理由に本来求められている役割です。このように解釈すると、法令や掲載基準に照らした妥当な説明まで、『原文にない推測』としてNoと判定してしまうおそれがあります。 先ほどの「男女問わず」の例では、判定理由が「女性に限定」という原文にない事実を前提にしていたので、Q3はNoが正しい判断でした。一方、今回の判定理由は、原文にある「営業マン」という記載そのものを基準に照らして解釈しているだけなので、Yesが正しい判断です。2つの事例を並べると、Q3の「推測」という言葉がこの2つを区別できていないことがわかります。 このケースでは、問題点と次の追記案をチームに共有します。 原文の記載を法令や掲載基準に照らして評価することは推測に含めない。原文にない事実を前提にしている場合のみNoとする。 レビューを受けたうえで、この追記をプロンプトに反映します。LLM-as-a-Judgeの判定理由が観点ごとに残っていると、ズレの原因を探りやすく、修正方針も立てやすくなります。チームへの説明にも役立ちました。 なお、改善の手段としては、プロンプトの修正のほかにより強いモデルを採用したりもしていました。 評価観点そのもののアップデート 運用ループを繰り返していると、プロンプトの言い回しだけでなく、評価観点そのものへの気づきも出てきました。たとえば次のようなものです。 「この観点、説明が足りなくてLLM-as-a-Judgeが迷っているな」 「この観点を入れていたけど、実際の業務ではそこまで重要じゃなかった」 前者の例として(架空の例です)、「判定理由は簡潔か」という観点があったとします。次の判定理由で考えます。 本求人には「営業マン募集」という記載があります。営業職は多くの業界で需要が高い職種です。「マン」は男性を指す表現であり、男女雇用機会均等法に抵触するおそれがあります。 この質問では、「3文で短いので簡潔」としてYesと判断する場合と、「判定と関係ない文(「営業職は…職種です。」)を含むので簡潔でない」としてNoと判断する場合が考えられます。 原因は、「簡潔」が文の短さを指すのか、余計な記述がないことを指すのかを定義していなかったことです。この場合は「結論が冒頭に書かれているか」「判定と無関係な記述を含んでいないか」のように、より具体的な質問に分解し直す必要があります。 結果・学び 評価観点の修正と評価指標の整理 最初に用意していた観点は、複数回のループの中で何度も追加・修正・削除しました。一番多かったのは、LLM-as-a-Judgeのプロンプトに記述したYes/Noの質問文が曖昧で、人間の意図と異なる判定になっていたケースの修正です。 また、要件には入っていたものの「あったらいいな」レベルだった評価指標は、当面は不要だと気づくことができました。そのため、評価指標は3つから2つに絞りました。机上で要件を整理していた段階では、どの指標も同じくらい大事に見えていたので、これは運用してみて初めてわかったことです。 最初に気づかなかった評価観点が見つかった LLM-as-a-Judgeの判定理由を読んでいくうちに、最初の設計では漏れていた観点にも気づきました(詳細は割愛します)。 運用を始めた後に知ったのですが、似た現象は研究でも報告されていました。Shankarら( arXiv:2404.12272 )は、LLMの出力を評価する仕組みづくりを支援するツールEvalGenを、LLM開発経験のある実務者9名に使ってもらうユーザースタディを行いました。その中で、出力を評価するうちに評価基準そのものが明確になったり変わったりする現象を観察し、Criteria Drift(基準ドリフト)と呼んでいます。 最初から完璧な基準を決めるのは難しく、実際の出力を見ながら基準を更新していくほうが現実的だ、ということです。今回の経験は、まさにこれに近いものでした。 筆者以外の目による最終確認 隔週の確認では、筆者の意図と異なる判定を見かけることが次第に少なくなりました。ただし、確認事例や評価観点が変わっているため、同一条件で精度向上を測定した結果ではありません。 ただ、改善方針はチームレビューを経ていたものの、どの事例を「ズレ」とみなすかは筆者1人が判断していました。そのため、LLM-as-a-Judgeが筆者個人の感覚に過剰適合しているリスクは残っていました。 そこで、レビューに参加していたチームメンバー2人に、個々の事例に対するLLM-as-a-Judgeの判定を確認してもらいました。評価指標ごとにPassとFailを3例ずつ(計12例)用意し、2人とも同じ事例について、判定とその理由が妥当かを見てもらう形です。結果、2人とも全事例でLLM-as-a-Judgeの判定に違和感はないと判断しました。 件数が少なく、LLM-as-a-Judgeの判定を見せたうえでの確認でもあるため、統計的な検証とは言えません。それでも「調整した本人以外が見ても妥当」というサニティチェックにはなり、運用を次の段階に進める判断材料になりました。 やってみて分かった注意点 アノテーションの負荷は件数ほど軽くない 開発を止めずに続けられる量には抑えられました。ただ、1件の判断に5~10分ほどかかります。確認件数も当初は3指標分で隔週18件(2指標に絞った後は12件)あり、「1指標あたり6件」という数字から想像するほど軽くはありませんでした。 確認者1人の感覚に寄っていくリスク 事例の確認を1人で担当すると、LLM-as-a-Judgeがその人の感覚に寄っていくリスクがあります。改善方針をチームでレビューしていても、レビューの対象はその人が見つけたズレに限られるからです。最終確認でこの偏りを部分的に補いましたが、判定を見せたうえでの少数件の確認なので、LLM-as-a-Judgeの判定に引っ張られた可能性も否定できません。 おわりに 本記事では、メタ評価用の正解データを事前に作り込まず、小さく始めて運用の中で評価基準を育てていったLLM-as-a-Judgeの取り組みを紹介しました。 振り返ると、このアプローチは自社の現在のフェーズにおいて有用でした。LLM-as-a-Judgeの判定結果を読み解く中で、事前に用意した評価観点そのものが見直されていったことは、個人的にも面白い発見でした。「最初から完璧な基準を作らないと」と身構えるよりも、まず動かしてみて、出力を見ながら基準を固めていくほうが、少なくとも今回のケースでは近道だったと感じています。 現状、本番モニタリングではLLM-as-a-Judgeを参考値としての利用にとどめています。アラート通知に使う段階では、より厳密な検証を行う予定です。具体的には、判定を伏せた状態での確認、レビュー担当の持ち回り、確認件数の拡大を検討しています。あわせて、評価観点の拡張や、他のAI機能への横展開にも取り組んでいきたいと考えています。 We're hiring! 現在、タイミーでは、データサイエンスやエンジニアリングの分野で、共に成長し、革新を推し進めてくれる新たなチームメンバーを積極的に探しています! product-recruit.timee.co.jp また、気軽な雰囲気でのカジュアル面談も随時行っておりますので、ぜひお気軽にエントリーしてください。 参考文献 Gu et al., "A Survey on LLM-as-a-Judge" (2024) https://arxiv.org/abs/2411.15594 Lee et al., "CheckEval: A reliable LLM-as-a-Judge framework for evaluating text generation using checklists" (2024) https://arxiv.org/abs/2403.18771 Shankar et al., "Who Validates the Validators? Aligning LLM-Assisted Evaluation of LLM Outputs with Human Preferences" (2024) https://arxiv.org/abs/2404.12272 Hamel Husain, "Using LLM-as-a-Judge For Evaluation: A Complete Guide" https://hamel.dev/blog/posts/llm-judge/
ℹ️ この記事について この記事に出てくるコードとスクリーンショットは、すべて 検知の挙動を確かめるために用意した検証用のサンプル です。 実際のサービスのコードではありません。 ツールが反応するかを見るために意図的に脆弱に書いているので、 そのまま流用しないでください。 はじめに こんにちは、タイミーのプラットフォームエンジニアリング1グループに所属している久慈です。 この記事では、全社のソースコード向け SAST(静的アプリケーションセキュリティテスト)の選定で、Amazon Inspector Code Security と GitHub Advanced Security(以下 GHAS)の GitHub Code Security を、同じ検証用コードで比較した結果を紹介します。検知結果に加え、言語対応や運用、コストを評価し、GHAS を選んだ理由をまとめます。GitHub Code Security の Code Scanning では CodeQL を解析エンジンとして使いました。以下、検知結果については「CodeQL」、修正提案やマージ保護、組織単位の設定といった周辺機能も含む比較では「GitHub Code Security」と書き分けます。 どの SAST ツールが自社の要件に合うのかは、公式の機能表だけでは分かりません。どのドキュメントにも「主要な脆弱性を検出します」と書いてありますし、対応言語表のチェックの付き方も似たようなものです。 結論から言うと、 同じ脆弱コードを両方に流してみて初めて差が見えました 。その過程で得たものを、うまくいかなかったところも含めてできるだけそのまま書いていきます。 アラートの扱いや通知・対応ルールなど、運用設計については別記事で紹介します。 比較した観点と、得られた知見は次のとおりです。 比較した観点 得られた知見 検知結果 検出件数だけでなく、仕込んだ脆弱性との突き合わせが必要 全社要件への適合 言語対応に加え、修正提案や PR 上の挙動にも差がある コスト 課金軸が異なり、スキャン構成やリポジトリサイズの仮定が試算を左右する 以下は検証時点の設定・環境で確認した結果です。製品の機能や挙動は変更される可能性があるため、現在の仕様は各公式ドキュメントをご確認ください。 全社で共通して運用できることを評価の前提にした 今回の比較では、個別のリポジトリだけでなく、 全社で共通して運用できること を評価の前提にしました。社内には複数の言語で書かれたリポジトリがあり、規模や変更頻度も異なります。 1リポジトリにとっての最適解と、全社で運用するときの最適解は別物です。 ツールの評価では、検知機能だけでなく、その周辺の運用も見ておく必要があります。CI のパイプラインへの組み込み、開発者への結果の伝え方、通知経路や対応ルール。リポジトリごとに別のツールを使えば、こうした設定や周知もそれぞれ必要になります。 AWS を利用している当社にとって、Amazon Inspector Code Security は候補の一つでした。SAST に加え、SCA(依存関係の脆弱性検査)や IaC のスキャンもまとめて扱える点は魅力です。一方で、AWS との親和性だけでは、全社のソースコード向け SAST として要件に合うかは判断できません。 そこで、言語対応や検知結果に加え、開発者へのフィードバックや設定・通知の運用まで含めて比較することにしました。 ソースコードを外部に送るということ 機能比較の前に、もうひとつ整理しておきたいことがありました。解析をどこで実行し、ソースコードや検出結果がどこに送信・保存されるかです。SAST 自体が外部へのコード送信を必須とするわけではありませんが、外部サービスを利用する構成では、サービス側に渡る情報の範囲と扱いを確認する必要があります。 自社のセキュリティ評価では、機能要件とは別に次のような論点を見ました。 論点 見たこと コードの保存 スニペットがサービス側に保存されるか、保存先リージョンはどこか 学習利用 送ったコードの扱いが契約・利用規約でどう定められているか(競合目的での利用禁止の記載など) 準拠証明 SOC 2 / PCI DSS 等、提示できる準拠情報があるか アクセス範囲 ツールが読めるリポジトリの範囲を絞れるか 外部サービスの評価では、コードや検出結果の取り扱いに加え、 シークレット保護との組み合わせ も評価項目に含めました。SAST の検知機能だけでなく、シークレット保護も含めた設定や通知、開発者へのフィードバックをどう運用できるかを比較しました。 Amazon Inspector Code Security を検証した 前提が整ったので、まず Inspector を実際に動かしました。 検証用のリポジトリを用意して、意図的に脆弱なコードを仕込んだ PR を作ってスキャンさせる、というやり方です。仕込んだのは SQL インジェクション、コマンドインジェクション、XSS、パストラバーサル、弱いハッシュ関数、それに古い依存パッケージと Terraform のミスコンフィグです。 検出件数 スキャン種別 検出件数 内訳 SCA(依存関係) 18件 Critical 3 / High 15 SAST(コード) 9件 Critical 1 / High 8 SCA は古いパッケージを入れておけば素直に出ます。SAST も主要なものは拾ってくれました。ここまでは想定通りです。 動かして分かった挙動のクセ 細かい実務寄りの話ですが、先に知っておきたかったポイントです。なお、以下はいずれも検証時点の当社環境で確認した振る舞いです。 1. PR を契機にしたスキャンとデフォルトブランチのスキャンで結果の出力先が違う 今回検証した Inspector Code Security のリポジトリ連携では、PR を契機にしたスキャンと、デフォルトブランチのスキャンで結果の見える場所が違うので、どちらを主に使うかを最初に決めておかないと、「この指摘はどこを見ればいいのか」が分かりにくくなります。 実際の画面で見るとこうなります。まず、脆弱なコードを含む PR を作成し、リポジトリ連携のスキャンを実行すると、結果は bot の PR コメントとして返ってきます。 Inspector の指摘は bot による PR コメントで届く 一方、同じリポジトリを AWS コンソールで見ると、検出結果は0件のままです。 AWS コンソール側の同じリポジトリ。デフォルトブランチのスキャンは走っているが、検出結果は0件 コンソールが見ているのはデフォルトブランチのスキャン結果なので、PR ブランチにだけ入れた脆弱コードはここには出てきません。設定どおりの振る舞いですが、「コンソールの一覧を見れば全部把握できる」と思っていると取りこぼします。コンソール側に集約したいなら、デフォルトブランチへのマージ後のスキャンか定期スキャンを前提に設計することになります。 2. PR への追加コミットでは再スキャンされない Amazon Inspector のリポジトリ連携では、既存の PR に追加コミットしても再スキャンはトリガーされません。新しいスキャンを実行するには、PR を close → reopen する必要があります。公式ドキュメントでも、この挙動が説明されています。今回の検証でも同じ結果になりました。 sequenceDiagram participant Dev as 開発者 participant PR as Pull Request participant IS as Inspector Dev->>PR: PR を作成 PR->>IS: スキャン実行 IS-->>PR: 指摘コメント Dev->>PR: 修正を push Note over PR,IS: 再スキャンされない Dev->>PR: close → reopen PR->>IS: スキャン実行 IS-->>PR: 指摘が解消 「修正したのに指摘が消えない」という状態は、開発者の信頼を失いやすいところだと思います。最終的には「とりあえず close/reopen してください」と案内することになるので、全社展開を考えるとここは軸の一つになりました。 3. 結果は PR コメントとして届く 検出結果は bot による PR コメントで通知されます。フィードバックがコードレビューの文脈に入ってくるのはありがたいのですが、同じ行に複数のコメントが重複して付くことがありました。後述の CodeQL との比較で使った PRでは、再スキャン分も含めて PR 上のコメントが20件を超えました。アラート一覧の画面で見るのとは違うので、件数が多いと PR 上だけで全体像を追うのは難しくなります。 IaC の検証では、PR コメントの見え方に注意が必要だった Terraform のミスコンフィグを仕込んだ検証コードをスキャンしたところ、IAM・暗号化系の4件について PR コメント上で検出を確認できました。 仕込んだ内容 PR コメント上の確認結果 IAM ポリシーの Action: * ✅ 検出を確認:Restrict IAM asterisk action Action: * + Resource: * ✅ 検出を確認:Restrict AWS IAM policy with full administrative privileges RDS の暗号化無効 ✅ 検出を確認:Disabled AWS RDS Encryption EBS ボリュームの未暗号化 ✅ 検出を確認:Unencrypted EBS Volumes S3 のパブリックアクセスブロックを全 false PR コメント上では確認できず セキュリティグループで 0.0.0.0/0 全ポート開放 PR コメント上では確認できず 古いモジュールバージョンの利用 PR コメント上では確認できず 実際の検証 PR の画面です。 検証 PR。IAM・暗号化系の4件について PR コメント上で検出を確認できた ここで注意が必要なのは、 PR コメントだけでは IaC ルール全体の検出可否を評価できない ことです。今回の設定では、PR コメントには Critical / High の検出結果の上位25件のみが表示されます。たとえば S3 のパブリックアクセスブロックに関するルールには Medium のものがあるため、PR コメントに表示されなかったことは未検出を意味しません。 ここで示す結果は、検証時点の設定・環境で PR コメント上に表示された範囲のものです。今回の PR コメントの結果だけで、Inspector の IaC スキャン全体のカバレッジや Terraform 専用ツールの代替可否を断定することはできません。 全社展開を見据えて要件を言語化した Inspector を単体で見ても、「XSS が一つ見逃された」が致命的なのか許容範囲なのかは分かりません。そこで、比較の基準を先に作りました。機能・運用・非機能の3分類で要件マトリクスにして、ツールごとに○×を付けていく形です。 とくに重く見たのは次のポイントです。 社内6言語(Go / TypeScript / Kotlin / Ruby / Python / Swift)をカバーできるか。非対応の言語があると、そのチームだけ別ツールになる 既存コードへの指摘によってマージを一律に止めず、新しい変更で生じた指摘に対処できるか 誤検知を抑えられるか。抑える手段が抜け道になっていないか 修正提案が出るか。指摘だけだと開発者の調査時間がそのまま乗る 通知とダッシュボードが揃っているか。気づける導線がないと放置される 新規リポジトリを自動でスキャン対象にできるか 2つ目は少し説明が必要です。既存コードに対する指摘への対応と、日々の開発を両立できることを重視しました。マージ制御を行う場合は、 既存コードに対する指摘と、新しい変更で生じた指摘を区別し、差分に対してゲートをかけられるか を確認しました。 ツール比較は、誰かが「あっちはここがいいらしい」と言い出すと印象論になりがちですが、要件が先にあれば「それはどの要件の話か」「必須か、あればいいか」に分解できます。候補となるツールを同じ表で評価できたのは、要件をマトリクスとして整理していたからです。 実際に使ったマトリクスから、ポイントになった項目を抜き出したものがこちらです(✅ 満たせる / △ 条件付き・追加設計が必要 / ❌ 満たせない)。評価は、検証時点の Ruby の実地検証と公式ドキュメントに基づくものです。 区分 要件 Inspector GitHub Code Security(解析:CodeQL) 判断の根拠 機能 社内6言語をカバーできる △ ✅ 公式ドキュメントで 5/6 と 6/6。Inspector は Swift が対象外 機能 代表的な脆弱性を検知できる(SQLインジェクション、コマンドインジェクション、XSS) △ ✅ 同じ検証コードで CodeQL は 5/5。Inspector は XSS の見逃しと誤検知 機能 既存の指摘によってマージを一律に止めない ✅ ✅ GitHub Code Security は変更箇所の指摘に基づいてマージをブロック可能。Inspector は指摘の有無にかかわらずマージをブロックしない 機能 修正提案が出る ❌ ✅ Inspector は説明と一般的な対策文まで。修正コード例はない 運用 PR で自動スキャンされ、PR 内で結果を返す △ ✅ Inspector は PR への追加コミットで再スキャンされないケースがあった 運用 新規リポジトリを自動でスキャン対象にできる ✅ ✅ Inspector は GitHub App の対象範囲とデフォルトスキャン設定、GitHub Code Security は org の security configurations で対応可能 この段階で ❌ が付いたのは修正提案の1項目だけです。それ以外は △ の積み上げで、1つずつ見るとそれほど致命的ではないところが難しいところでした。 同じ脆弱コードで CodeQL と比べた 要件マトリクスができたので、次は比較です。GitHub Code Security の Code Scanning を CodeQL で試すことにしました。 やり方はシンプルで、Inspector に流したのと同じ脆弱コードを、そのまま CodeQL にも流すというものです。検証は Ruby のコードを対象に行いました。ツール比較の記事はいくらでもありますが、自分たちで用意した同じ検証コードを両方のツールで解析した結果でないと、要件の○×は埋められません。 検証時の設定は次のとおりです。 ツール セットアップ・実行方式 スキャントリガー CodeQL デフォルトセットアップ デフォルトセットアップの標準設定 Inspector Code Security リポジトリ連携 Pull_request / Push 検知結果 仕込んだのは5パターンです。SQL インジェクション1件、コマンドインジェクション3件(バックティック / %x / system )、Reflected XSS 1件(ERB テンプレート)。 XSS の検証では、Sinatra の ERB に escape_html: true を設定し、HTML エスケープを有効にしています。そのうえで、 <%== ... %> をエスケープせずに出力する「生出力」として扱う構成にし、リクエストで受け取った値をこの記法で出力しました。エスケープが有効な構成でも、明示的な生出力によって XSS が生じるケースを検証対象にしています。 仕込んだ脆弱性 Inspector CodeQL SQL インジェクション(1件) ✅ 検知 ✅ 検知 コマンドインジェクション(3件) ✅ 検知 ✅ 検知 Reflected XSS(ERB テンプレート・1件) ❌ 見逃し ✅ 検知 誤検知 SQLi / コマンドインジェクションの各行に XSS が重複 なし CodeQL は仕込んだ5件すべてを検知し、意図していないアラートは出ませんでした。 CodeQL のアラート画面。仕込んだ脆弱性が検知されている Show paths ボタンで展開すると、source から sink までのデータフローをたどれる 気になったのは Inspector 側の XSS です。意図して仕込んだ ERB テンプレートの XSS は検知されず、一方で XSS ではない SQLi・コマンドインジェクションの4行すべてに XSS の指摘が重複して付きました 。見逃しと誤検知が同時に起きている状態で、これは件数だけ数えていると気づけません。「XSS を検出している」という集計上は、正しく動いているように見えてしまいます。 ERB テンプレートの XSS はCodeQLでは検知されたが、Inspector では検知されなかった Inspector では SQL インジェクションの行に XSS の指摘も重複して付いた(このコードでは実行結果をHTMLで返していないので、XSSにはなり得ない) 何を検知したかではなく、仕込んだものと検知されたものが一致しているか を1件ずつ突き合わせる必要があります。 検知精度以外の差 観点 Inspector GitHub Code Security(解析:CodeQL) 社内6言語のカバー(公式ドキュメント) 5/6(Swift 非対応) 6/6 修正提案 説明と一般的な対策文(修正コード例はなし) 修正コード例が出る スキャン時間(Ruby) — 約47〜53秒 言語対応は、Amazon Inspector Code Security の対応言語とCodeQL の対応言語・フレームワークを参照しました。表は言語名ベースの対応を示しており、実際にコードを流して確かめたのは Ruby だけです。言語のバージョンやフレームワークごとの制約は、各公式ドキュメントで確認する必要があります。それでも言語カバレッジは要件に直結しました。Swift が非対応だと、モバイルのチームだけ別ツールを使うことになり、運用が二重化します。 修正提案の差も、実運用では地味に効きます。ここで比較しているのは CodeQL 自体の解析機能ではなく、GitHub Code Security 側の修正提案機能です。Inspector 側も脆弱性の説明と一般的な対策文、参考リンクは付きますが、該当行をどう直すかは自分で考えることになります。修正コード例が添えられていれば、少なくとも調べ始める手がかりにはなります。指摘の件数が増えるほど、この差は大きくなります。 GitHub Code Security 側の修正提案機能で、CodeQL の指摘に対する修正コード例を確認できる この比較に使った fixture は、一度作れば使い回せます。別の候補を評価するときも、同じ基準で比較できます。ただ、その fixture 作りでは罠にハマりました。 [コラム]CodeQL の fixture でハマった話 比較を始めたとき、最初に書いた Ruby の脆弱コードはこんな感じでした。リクエストで受け取った値が name に入ることを想定して、文字列連結で SQL を組み立てています。 def unsafe_query (name) # name に何が渡るかは、この関数だけを見ても分からない DB .execute( " SELECT * FROM users WHERE name = ' #{ name } ' " ) end 絵に描いたような SQL インジェクションです。これを CodeQL に流したところ、 検知は0件でした 。 最初はツールを疑っていました。こんな分かりやすい例が出ないのだから、セットアップが間違っているのか、それとも検知の精度が低いのか。 原因はツールでなく、自分のコードでした。 CodeQL のデータフロー解析は、ユーザー入力の入口(source)から危険な出口(sink)まで値が流れていることを追いかけて初めて脆弱性と判定します。上のコードには sink( DB.execute )はあるけれど、source がありません。 name は単なるメソッド引数で、外部から来た値だという根拠がどこにもありません。人間が見れば「いかにも危ないコード」ですが、データフローとしては繋がっていません。 最初は上のようにメソッドだけを書いていました。そこに Sinatra を入れてルーティングを用意し、HTTP のリクエストパラメータから受け取る形に直したところ、あっさり検知されました。 get ' /users ' do name = params[ ' name ' ] # source DB .execute( " SELECT * FROM users WHERE name = ' #{ name } ' " ) # sink end 差は source があるかどうかだけです。 flowchart LR subgraph NG["検知0件"] A1["メソッド引数 name"] -. source なし .-> B1["DB.execute (sink)"] end subgraph OK["検知される"] A2["params['name'] (source)"] -- データフロー --> B2["DB.execute (sink)"] end これに気づくまでしばらく時間を使いました。 「検知されない」と思ったら、まず fixture の書き方を疑う 。少なくとも今回は、こちらのコードが原因でした。SAST の検証コードを書くときは、脆弱な処理だけでなく、外部入力の入口までセットで置く。ルーティングやコントローラを含めて、実際のアプリコードに近い形で書くのが安全です。 コストを試算した 検知精度の比較が終わって、次はお金の話です。ここはかなり悩みました。 先に結論を言うと、デフォルトブランチの作業ツリーを Inspector の課金対象サイズと仮定した今回の試算では、GHAS(Code Security と Secret Protection の合計)と、Inspector の変更ベース+週次構成の月額は同水準でした。以下で触れる「約120倍」は GHAS との比較ではなく、Inspector 内でスキャン構成を変えた場合の差です。 そもそも、 2つのツールの課金軸がまったく違います 。 GHAS(Code Security + Secret Protection) Inspector Code Security 課金の単位 アクティブコミッター数 スキャン回数 × リポジトリサイズ 伸びる要因 人が増える コミット・コード量が増える 予測しやすさ 高い(人数は読める) 低い(構成次第で桁が動く) 人数課金と従量課金を比べるには、自社の実績値を入れて同じ単位に落とすしかありません。GHAS 側はアクティブコミッター数をもとに、Code Security と Secret Protection の両方の費用を合算しました。Inspector 側は、対象リポジトリと直近の PR 実績(変更ベースのスキャン回数は PR の open と merge で算出)、それに各リポジトリのサイズを入れて月額に組み直しました。サイズには、全履歴を含む pack ではなく、各リポジトリのデフォルトブランチの作業ツリーを使いました。これは試算上の仮定であり、Inspector が実際に課金対象として計上するサイズを確認したものではありません。なお、具体的な金額は公開していないので、以下は比率と傾向で書きます。 試算していて気づいたこと 1. トリガー構成を変えると、金額が約120倍動いた これは試算表を作っていて驚いた点です。Inspector 側を「週次1回の定期スキャンのみ」で試算した場合に比べて、「変更ベース(PR の open + merge)+週次1回」にすると、 月額が約120倍 になりました。これは Inspector 内でのスキャン構成の比較であり、GHAS との金額差ではありません。後者のコストは99%以上が PR 起因のスキャンでした。 従量課金のツールは、「いくらか」でなく「どう使うといくらか」を決めないと比較表に乗せられません。そして、安い構成はそのまま「検知が遅い構成」でもあります。コストの話が、いつの間にか運用設計の話になっていました。 2. 「リポジトリサイズ」が何を指すのか分からない 従量課金の計算にはリポジトリサイズが入ってきます。料金体系には10MB単位の記述があるので、最初はそれを掛ければいいと思っていました。が、その10MB が何の測定値なのかは分かりません。 自社のリポジトリで実測したサイズを10MB単位に換算すると、測り方によって次の差がありました。デフォルトブランチの作業ツリーを1とした、課金単位の比率で示します。 測り方 10MB単位に換算した課金単位の比率 pack 全体(全履歴込み) 80以上 デフォルトブランチの作業ツリー 1 作業ツリーは実際にチェックアウトして手元に展開されるコードで、pack は過去の全履歴を含みます。 どちらを採るかで課金単位が80倍以上変わります 。課金対象サイズの定義が分からないままでは、試算の確度を上げられません。 この点は、公式ドキュメントを読んだ限りでは特定できませんでした。今回の試算では、デフォルトブランチの作業ツリーを課金対象サイズと仮定しました。実際の計上値は、15日間の無料トライアル期間中にスキャンを走らせ、コンソールの使用状況で確かめる方針にしました。以下のコスト比率や月額比較も、この仮定に基づく試算であり、使用状況や請求額の実測結果ではありません。 コストの偏り もう一つ、リポジトリサイズと PR 実績をもとにした試算で分かったことがあります。従量課金だと、コストがいくつかのリポジトリに極端に偏ります。 当社の試算では、Inspector の試算額全体の約8割を上位3リポジトリが占め、最大の1リポジトリだけで約56%を占めていました。メインのサービスのリポジトリが、サイズもコミット頻度も圧倒的に大きいからです。 これは別の角度から言うと、 一番スキャンしたいリポジトリが一番高くつく ということです。コストを下げるためにスキャン頻度を落とすと、一番守りたいところの検知が遅くなります。このジレンマになるのが、今回は制約として大きかったです。 今回の試算では月額は同水準でしたが、だからコストは決め手にならない——と最初は書こうとしました。しかし、それは正確ではないと気づきました。金額は同じでも、 伸び方と、伸び方を予測できるかが違います 。この観点が、最後の判断を分けました。 GHAS を選んだ理由 最終的に、当社は GHAS(Code Security + Secret Protection)を選びました。決め手は3つあります。 1つ目は、検知精度と言語カバレッジ です。Ruby で同じ脆弱コードを流したところ、仕込んだ5パターンがすべて出て、意図していないアラートはありませんでした。そして公式の対応言語は社内の6言語をすべて含みます(実地検証は Ruby のみ)。全社展開を前提にすると、「このチームだけ別ツール」を作りたくありません。 2つ目は、コストの予測性 です。前述のサイズの仮定に基づく試算では、月額はどちらも同水準でした。でも、人数課金は採用計画から来年分を社内に説明できます。スキャン回数課金は、トリガー構成とコミット量で桁が動きます。とくに AI でのコーディングが広がるなかで、人数が変わらないままスキャン回数だけが伸びていくリスクは、予算の説明責任を持つ立場では受けにくいものでした。 同じ金額なら、読める軸を選びたい という判断でした。 3つ目は、Code Security と Secret Protection を組み合わせ、同じプラットフォームで運用できる ことです。 前述のとおり、今回の選定では、SAST とシークレット保護を組み合わせた運用も評価項目に含めました。別々のツールで揃えると、契約や通知経路、開発者への周知をそれぞれ管理する必要があります。Code Security と Secret Protection を同じプラットフォーム上で運用できることは、こうした管理の負担を抑えるうえで利点でした。 Inspector を選ばなかった理由 Inspector Code Security が劣っているから選ばなかったわけではありません 。今回の全社展開の要件には、GHAS のほうが合っていた、が正しいところです。 Inspector の強みは、コンテナイメージや EC2、Lambda、ソースコードの検出結果を同じ AWS コンソールで横断して確認できることです。AWS 上のワークロードとソースコードを一元的に把握したい場合には、有力な選択肢です。 今回の選定対象は、全社のソースコード向け SAST でした。検知精度と言語カバレッジ、コストの予測性、Code Security と Secret Protection を同じプラットフォームで運用できることを重視すると、GHAS のほうが要件に合っていました。 IaC についても、今回の PR コメントの確認だけで Inspector の IaC スキャン全体のカバレッジや、Terraform 専用ツールの代替可否を判断することはできません。これをもって「IaC スキャンは使えない」と言いたいわけではなく、全 Severity の結果も含めて別途評価する必要があります。 そして、この判断ができたのは、 先に要件を言語化して、同じコードで測っておいたから です。「なんとなく GitHub のほうが使いやすそう」で決めていたら、後で説明できなくなっていたと思います。 まとめ 今回の比較で重視したのは、検知結果だけでなく、全社で共通して運用できるかという視点です。最後に、選定で役立ったポイントをまとめます。 全社要件を言語化する。 言語対応や開発者へのフィードバック、設定・通知の運用を要件として整理することで、候補を同じ基準で比較できました。要件がないと、ツール比較は「あっちのほうがいいらしい」の応酬になります。 公式の対応表でなく、同じ脆弱コードで測る。 両ツールとも、公式の検出ルールには XSS 用のルールがありますが、実際に流すと見逃しと誤検知が出ました。IaC も同じで、スキャンが動くことと、自社で守りたいルールが拾えることは別です。何が拾えて何が拾えないかは、自分たちのコードを流して初めて分かります。 課金軸が違うツールは、自社の実績値を入れて初めて意思決定に使える。 人数課金と従量課金は、料金表を並べても比べられません。そして比較するときは、金額だけでなく伸び方と予測しやすさも軸に入れたほうがいいです。 ツールの選定では、検知結果だけでなく、アラートの優先順位、対応の役割分担、通知経路まで考える必要があります。 こうした運用設計については、別記事で紹介する予定です。 同じように SAST のツール選定で悩んでいる方の参考になれば幸いです。
こんにちは。タイミーでAI・データサイエンス部に所属している栁です。 タイミーでは、募集内容やレビューなどの投稿コンテンツを審査するContent Moderationの仕組みにLLMを導入しています。この領域で品質を継続的に高めるには、プロンプトやモデルの改善だけでなく、評価データ、本番ログ、そして審査に関するドメイン知識を接続する仕組みが欠かせません。 本記事では、その評価・改善ループと、ドメインエキスパートの専門知識をループに組み込むプロセスの設計について、取り組みを紹介します。 Content Moderationの二つの課題:誤判定を減らし、正解を定義する Content Moderationとは、ユーザーの投稿が規約や法令、プラットフォームの安全性基準に違反していないかを確認し、必要な対処を行うプロセスです。タイミーでは、審査の品質向上を目的として、審査フローにLLMを導入しています。 Content ModerationでLLMを活用するうえでは、二つの難しさがあります。 一つは、求められる判定品質の高さです。一般的に、Content Moderationでは二種類の誤判定がそれぞれ異なるリスクにつながります。 見逃し(False Negative) :違反コンテンツを通してしまい、プラットフォームの健全性・安全性を損なう 空振り(False Positive) :正常なコンテンツを差し戻し、掲載遅延やユーザーの機会損失を生む 「それなりに判定できる」だけでは十分ではありません。それぞれのリスクを踏まえ、バランスを考えながら適切な品質を目指す必要があります。 もう一つは、正解を定義する難しさです。違反かどうかの判断には深いドメイン知識が必要であり、何を正解とするかは自明ではありません。運用の中では、既存の審査ルールだけでは判断できない事例も出てきます。そのため、LLMの判定を改善するだけでなく、判断の基準そのものを明確にしていく必要があります。 つまり、Content Moderationでは、 「LLMの誤判定をどう減らすか」 と 「何を正解とするかをどう定義するか」 の二つが課題になります。継続的に品質を高めるには、この両方を改善対象として捉えることが重要です。 1. 比較できる土台を作り「雰囲気評価」から脱却する プロンプトやロジックを変更した際、いくつかの出力を見て「なんとなくよくなった」と判断するだけでは、評価が属人的になり、変更の妥当性を再現可能な形で検証できません。 再現性のある評価の出発点になるのが、信頼できる ゴールデンデータセット です。入力と期待する出力のペアを用意し、同じデータセットに対して現行のロジックと改善案を比較することで、変更の妥当性を客観的に評価します。 正解ラベルに加え、分析に必要な情報を残す データセットでは、ラベルの正確性と事例の網羅性を重視します。また、単に正解ラベルだけではなく、適切なメタデータを付与しておくと、評価・分析の切り口として役立ちます。 以下は、管理するメタデータの一例です(スキーマを説明するための例で、実際の投稿や審査基準を示すものではありません)。 reason :判定に至った理由や根拠を残す taxonomy :ケースを分類し、事例の網羅性を把握する difficulty :典型例と判断の難しいエッジケースを区別する { " input ": " 〜な方は応募をご遠慮ください... ", " expected_output ": " REJECTED ", " metadata ": { " reason ": " 募集を不当に限定するためNG ", " taxonomy ": " 不当な募集条件 ", " difficulty ": " easy " } } 一方で、管理項目を増やすほどメンテナンスコストも上がります。最初から完璧なスキーマを目指すのではなく、小さく始め、評価・分析に必要になった項目を段階的に拡充します。 ゴールデンデータのスコアは本番性能そのものではない また、ゴールデンデータで評価する際は、データセットの用途を明確にしておくことも必要です。 ゴールデンデータはさまざまなケースを収集した事例集として、ロジック間の相対比較や、個別ケースに対する振る舞いの確認に役立ちます。一方、事例の網羅性を重視して集めたデータは、一般に本番環境の分布を代表しません。そのため、スコアをそのまま本番環境における性能とみなすことはできません。 改善案を比較するための評価と、本番品質を把握するための評価は、役割が異なります。この区別が、次の改善ループの設計につながります。 2. 改善ループの全体像:Inner LoopとOuter Loop 開発と本番を接続しデータセットを育てる ゴールデンデータを一度作って終わりにすると、リリース後に本番環境で明らかになった問題が、評価やその後の改善に反映されません。そこで、開発時と運用時の二つのフィードバックループを接続します。 Inner LoopとOuter Loopによる評価・改善ループの全体像 Inner Loopは、ゴールデンデータに対して審査ロジックを評価し、その結果をもとに改善するループです。対象には、プロンプトやモデルを含むLLM Workflowはもちろん、「何を違反とするか」を定めた審査ポリシーも含まれます。評価で見つかった問題に対して、推論の仕組みと判断基準の両方から改善を検討します。 Outer Loopでは、本番環境の入出力を監視して品質を把握するとともに、誤判定が疑われる事例を収集します。収集した事例の正解と根拠を確認し、正解ラベルを付けてゴールデンデータに追加することで、開発時の評価に還元します。 Inner LoopとOuter Loopを接続した全体の改善プロセスは以下のとおりです。 本番環境で入出力を記録・監視する 誤判定が疑われる事例を収集する 事例の正解と根拠を確認し、データセットに追加する 拡充したデータセットで審査ロジックを評価・改善する 改善をリリースし、再び本番で監視する 二つのループの接続によって、運用で得た知見が次の評価に活用されます。改善するのはロジックだけではなく、改善を判断するためのデータセット自体でもあります。 最初から完全を目指さない 最初から完璧な改善ループを構築することは困難です。そこで、以下のようなステップを踏みつつ、段階的に改善ループを発展させていくことを考えます。 Level 状態 Level 1. 主観による評価 品質を担保したデータセットがなく、属人的に判断している Level 2. Inner Loopの確立 ゴールデンデータに基づく評価・改善が開発環境で回っている Level 3. Outer Loopとの接続 本番のエラーケースが継続的に収集され、改善に使われている Level 4. 評価とリリースの接続 明確なリリース基準に基づき、自動デプロイまでつながっている タイミーの現在地としては、本番のエラーケースを改善につなぐ段階(Level 3相当)にあります。最初から評価もリリースも完全自動化するのではなく、少数の事例から評価を始め、運用の中でプロセスやリリース基準をブラッシュアップしながら、ループ自体を育てていくことが重要です。 ツールの活用によりループを素早く回す LLMOpsツールは、評価や監視に必要な機能を通じて、改善ループを円滑に回すために役立ちます。多くのLLMOpsツールは、改善ループ構築のための以下のような機能をサポートしています。 Dataset & Experiment :データセットの管理・バージョニング、オフライン評価・実験管理 Tracing :本番ワークフローの追跡、トレースからデータセットへの事例追加 Online Evaluation :LLM-as-a-Judgeなどによる本番環境での継続的な品質評価、品質劣化の検知 Langfuse、MLflow、Datadog Agent Observabilityなどは、こうした機能を提供する代表的なツールです。ただし、重要なのはツールの導入自体ではなく、本番ログ、評価データ、実験結果がつながる状態を作ることです。 また、個々の作業を効率化するには、AI Skillの活用も有効です。エラー候補の抽出、傾向ごとのグルーピング、データセットへの追加提案など、人手で行うとボトルネックになりがちな作業をAI Skillとして定型化することで、改善ループをより素早く回せます。 3. 誤判定の原因を切り分ける:LLM Workflowと審査ポリシー 改善ループの中でエラー分析を進めると、プロンプトやロジックの調整だけでは解決できないケースにぶつかります。どれほどLLM Workflowの品質を高めても、審査ポリシーに事例の扱いが明文化されていなければ、「何を正解とすべきか」を確定できないためです。 Content Moderationにおける審査ロジックは、「どう判定するか」を担うLLM Workflowと、「何を判定するか」を定める審査ポリシーの二つから成り立ちます。エラーケースごとに、判定の仕組みに問題があるのか、判断基準に曖昧さがあるのかを切り分け、それに応じた改善を進めることが大切です。 審査ロジックはLLM Workflowと審査ポリシーから成り立つ この切り分けは、改善における役割と責務を考える上でも欠かせません。エンジニアやデータサイエンティストは、判定システムの設計・実装を通じて「どう判定するか」に責任を持ちます。一方、ドメインエキスパートは、審査基準の定義や個々の事例に対する最終判断を通じて「何を判定するか」に責任を持ちます。 改善には双方の関与が欠かせません。判定の仕組みと判断基準のどちらか一方しか見直せなければ、改善は行き詰まります。 私たちも、実際にLLMの評価・改善を進める中で、次のような難しさに直面しました。 エンジニアだけでは判断が難しい事例があった :当初は、改善と評価をエンジニア中心に進めようとしました。しかし、専門知識が必要な事例では、期待する判定やロジックの妥当性の判断が難しく、ドメインエキスパートの知見を取り込む必要があると気づきました。 知見を審査ポリシーに還元する仕組みに改善の余地があった :誤判定の分析を進める中で、現場の知見や例外的な事例の扱いを、判断基準に取り込む必要があると分かりました。個別の事例への対応にとどまらず、得られた知見を次の評価・改善につなげる仕組みを強化する必要がありました。 こうした経験から、必要なときにドメインエキスパートへ相談するだけでなく、その専門知識を審査ポリシーやデータセットに継続的に還元できるプロセス自体を適切に設計することが重要だと気づきました。 4. 専門知識を改善ループに組み込む ポリシー更新とアノテーションに介入ポイントを設ける こうした学びを踏まえ、私たちはドメインエキスパートが関与するタイミングと役割を整理し、専門知識を継続的に取り込めるプロセスを整えてきました。 一つ目の介入ポイントは、審査ポリシーの更新です。その前提として、ポリシーを継続的に更新・管理できる形に整えます。例えば、違反種別ごとに以下の情報を関連づけて管理します。 判定基準 :抽象化したルール OK/NG例 :ルールを補う具体例 個別事例 :エッジケース 加えて、Draft → Review → Active → Archiveというライフサイクルと、バージョニングのルールを定義します。エラー分析で見つかった論点をもとに、エンジニアが改定案を起票し、ドメインエキスパートがレビューして内容を決定したうえで有効化します。基準を定義する責任はドメインエキスパートにあり、エンジニアは論点を整理し、議論を前に進める役割を担います。 現場の知識をプロンプトへの追記だけで済ませず、判定基準や具体例として構造化することで、審査ポリシーそのものを継続的に改善できます。 二つ目の介入ポイントは、本番から収集した事例に正解ラベルを付けるアノテーションのプロセスです。私たちは、以下の流れでアノテーションを行っています。 誤判定が疑われる事例を収集する: 本番ログから事例を抽出します。AI Skillなどによる自動化も活用します 既存ポリシーに基づいて一次判定する: 既存の基準で判断が明確なケースはエンジニアが一次判定し、複数人によるダブルチェックで正確性を担保します 判断できないケースをエスカレーションする: ドメインエキスパートに判断を依頼し、必要に応じて、その判断を審査ポリシーに反映します このプロセスでは、既存の基準で判断できる事例と、基準そのものの検討が必要な事例を分けています。個別の正解ラベルを得るだけでなく、判断できなかった事例を審査ポリシーの改善にもつなげることが重要です。 タスクの性質に応じて適切な協業体制を選ぶ ここまで紹介したプロセスは協業の一例です。タスクの性質に応じて、エンジニアとドメインエキスパートの役割分担を適切に選択することも大切です。 LLMの1回の呼び出しで完結する軽量なタスクであれば、エンジニアが評価基盤を提供し、ドメインエキスパートが自力で評価・改善・リリース判断を進めるスタイルが考えられます(エキスパート自律型)。この方式には、エンジニアの負荷を抑えながら、専門知識を持つドメインエキスパートが主体となって素早く改善を進められるという利点があります。 一方、複雑なLLM WorkflowやAgentの設計が必要なタスクでは、エンジニアが改善を牽引し、ドメインエキスパートが専門知識と意思決定を提供するスタイルが適しています(エンジニア主導型)。綿密な連携が必要ですが、LLM Workflowと審査ポリシーの両方を一体的に改善できます。 このように、責務を固定的に分断するのではなく、タスクと双方の体制に合わせて、改善が滞らない役割分担を選ぶことが大切です。 まとめ:円滑なプロセスを設計し、愚直に改善を回す LLMの評価・改善に、銀の弾丸はありません。実際のエラーケースに一つずつ向き合い、その結果を次の評価・改善に活かすという地道な取り組みが必要です。 この泥臭さの中で、継続的な改善を進めていくために重要なのは、次の三点です。 信頼できるゴールデンデータを用意する :感覚ではなく、同じ土台で変更を比較できるようにする 開発と本番を接続して改善を回す :本番のエラーを評価データに還元し、ロジックとデータセットをともに育てる 専門知識を取り込む仕組みを設計する :正解ラベルの判断と審査ポリシーの更新を、改善ループに組み込む プロンプトを変更するだけでは、評価の信頼性や判断基準の曖昧さは解消できません。モデルの出力を改善することと同じくらい、正解を定義し、評価し、運用から学び続けられるプロセスを作ることが重要です。 We’re Hiring! タイミーでは、データサイエンスやエンジニアリングの領域で、共に成長し、革新を推し進めてくれる新たなメンバーを積極的に探しています! 現在募集中のポジションはこちらです。カジュアル面談も行っておりますので、興味のある方はぜひお気軽にお申し込みください!
はじめに こんにちは、株式会社タイミーでデータサイエンティストをしている藤井です。 普段は推薦エンジンの改善を担当しています。 最近は推薦エンジンそのものの改善に加えて、LLM を使って推薦エンジンの改善サイクル(仮説→実験→評価)をいかに速く回すかにも取り組んでいます。 5か月前の前回記事 では、Claude の Skill を使って半自動で推薦システムを改善する取り組みを紹介しました。 現在はそこから一歩進んで、推薦エンジンの自動改善の運用を開始しており、以下のような構成をとっています。 エージェントは Claude Agent SDK で実装しています。 図1. 推薦エンジン自動改善システムの全体像 図2. AI Agent Block同士の組み合わせについて 1回の改善サイクルは、次の順に進みます。 仮説構築 → 実装 → レビュー → 評価 → 長期記憶への書き込み → 仮説構築に戻る ただ、実際に回してみると課題も見えてきました。 たとえば、体感で研究案の半分ほどが損失関数の微調整になります。 また、GPU コストに見合わない小さな改善案も出てきます。 さらに、長期記憶のステップを外して回した場合は、以前試した案をそのままもう一度出してきたり、過去に失敗した案と似た案を出してきたりしました。 これらは推薦に固有の問題ではありません。 「似た案ばかり出る」「コストの高い実験を無駄打ちする」「結果を次に活かせない」 は、AI に研究を自動で進めさせる自動研究(AI が自分で仮説を立て、実験し、次を決めるループ)では、分野を問わず起きる問題です。 最近の自動研究の論文は、数学・化学・機械学習など題材は違いますが、これらの問題に取り組んでおり、その解き方の多くは推薦にも応用できます。 本記事では、自動研究の最近の論文をもとに、上のループのどこを改善するかを次の3つの問いで考えます。 何を試すか (仮説構築):似た案ばかりにならないようにする 実験予算をどこに使うか (仮説構築と実装の間):実装・評価に進める案を、実行コストに見合うものだけに絞る 今の結果をどう次に活かすか (長期記憶 → 仮説構築に戻る):途中の結果や教訓を、次の案出しで使える形で残す なお、この後紹介する仕組みの多くは、タイミーでまだ実装していない 「これから試したい設計の候補」 です。実際に運用しているのは上の図の範囲です。 論文に入る前に、まず OpenAI のエージェント群が数学の難問(Navier–Stokes 方程式の問題)を解いた事例を見ておきます。 多数のエージェントを協調させて未解決問題を解いた事例で、その動かし方に、本記事の3つの問いに対応する工夫が含まれているからです。 前置き:エージェント群が数学の難問を解いた仕組み(Navier–Stokes) 2026年9月、OpenAI は、流体の動きを表す Navier–Stokes 方程式に関するミレニアム懸賞問題(数学の代表的な未解決問題に懸けられた懸賞)を、内部モデルで動くエージェント群が解いたと発表しました。 約88時間で解に到達し、その後 Lean(証明が正しいかを機械でチェックする言語)で検証されています [1] 。 数学の中身は本記事では扱わず、エージェントの動かし方に絞って紹介します。 ⚠️ 注意:実装の詳細は非公開です。 OpenAI はハーネス(エージェントを動かす周辺の仕組み)の実装(群の数と大きさ、通信の宛先の決め方、集約の頻度と形式、実行環境、人間の介入の範囲など)を公開していません。ここでは OpenAI の公式記事 [1] と、OpenAI の研究者 Noam Brown が Dwarkesh Podcast(AI 研究者などへのインタビュー番組)で語った内容 [2] に書かれている範囲だけを紹介します。 OpenAI の公式記事に書かれていること [1] エージェントを複数の群に分け、通信を同じ群の中に限定した。Navier–Stokes の解に到達した群では、約1万のエージェントが同時に稼働していた 群ごとに異なる問題設定を割り当てた。Navier–Stokes では、証明を目指す問題と反証を目指す問題を別々の群に担当させた 一定時間が経過した後、Codex(OpenAI のコーディングエージェント)が各群の有用な中間成果を集約し、追加のプロンプトとして各群に共有した より易しい関連問題(外力なしの Euler 方程式。粘性を無視した流体の方程式)が先に解けたことを受け、他のミレニアム懸賞問題に割り当てていたエージェントを Navier–Stokes に移し、Euler 方程式の解をプロンプトに加えた 実行中に、より訓練の進んだモデルへエージェントを切り替えた Noam Brown が語っていること [2] タスクを割り振る役割を頂点に置く固定的な階層構造は採用せず、他のエージェントへメッセージを送るための単純な通信手段だけを与えた。エージェント同士がどのように協調するかは、エージェント自身に委ねた 特別な工夫をしなければ、各エージェントが互いに連携せず、それぞれ独立して問題に取り組んでしまう。そのため、協調する能力をモデルの訓練によって身につけさせた 約1万のエージェントを動かしたことが結果にどの程度寄与したかは検証していない。成果の主因はモデル自体の能力であり、マルチエージェントの寄与は限定的だと述べている この事例から読み取れること 公開情報は断片的ですが、特に次の点に注目しています(以下は筆者の解釈です)。 最初の3点は、この後の問い1〜3にそれぞれ対応します。 1. 群ごとに異なる問題設定を割り当てている 約1万のエージェントがいても、全員が同じ情報を見ていれば似たアプローチに偏ります。 そこで通信を群の中に限り、証明と反証という対立する問題設定を別々の群に割り当てて、群ごとに違うアプローチを試させています。 2. 易しい関連問題が解けてから、Navier–Stokes に資源を移している Euler 方程式の問題は、Navier–Stokes にどれだけ資源を配分するかを、少ないコストで決めるための予備実験だったと考えられます。 3. 途中の成果を集めて、各群に戻している Codex が各群の有用な中間成果を集約し、追加プロンプトとして群に戻しています。 Euler 方程式の解も、Navier–Stokes のプロンプトに入れています。 ある群で得た結果を、他の群でも使っています。 集約を「ときどき」にしているのは、共有しすぎて群ごとの多様性を潰さないためだと考えられます。 補足:外側の仕組みは最小限にし、協調は訓練で身につけさせている 固定の階層は置かず、単純な通信手段だけを渡して、協調の仕方は訓練でモデルに身につけさせています。 API 経由でモデルを使う私たちは、モデルの外側の仕組みで工夫することになります。 以降の章では、論文がこうした外側の仕組みをどう作り込んでいるかを見ていきます。 ※公式記事によると、問題設定の割り当て、Navier–Stokes への資源の集中、中間成果の集約といった方針は、研究チームが決めています。それ以外の判断に人間がどこまで関与したかは公開されていないため、ここでは「人間+エージェント群の仕組み」として扱います。 自動研究の論文を3つの問いで読む ここからは、自動研究の最近の論文を、冒頭の3つの問いに沿って見ていきます。 研究 誰の研究か 一言で言うと 対応する問い Co-Scientist Google 仮説を対戦させて順位を付ける。似た案どうしを優先して戦わせ、未探索の領域の仮説も出させる 1. 多様性 EvoDiverse Georgia Tech ほか 「既存の延長の枠」と「別系統の枠」を並べ、ときどき案を交換する 1. 多様性 Arbor 中国人民大学 / Microsoft Research 仮説を木で管理し、捨てた理由を次の案出しの制約にする 1. 多様性 RPM Meta まだ実行していない改良案を、2案ずつ比べて1つに絞る 2. 予算配分 EEM 中国科学院 計算技術研究所 / Baidu 過去の経験から改善しないと分かる案は実験せずに見送り、経験が足りなければ予備実験で補う 2. 予算配分 PrimeScientist カリフォルニア大学サンディエゴ校 / ジョンズ・ホプキンス大学 残り予算に応じて、手広く試すか絞るかを切り替える 2. 予算配分 MARS Google Cloud AI Research 新しい案をその時点のベストと比べて教訓にし、他の枝の案でも使う 3. 結果の活用 MLEvolve Shanghai AI Lab ほか 行き詰まったら、自分の枝の履歴や他の枝の良い解を次の案に渡す 3. 結果の活用 AgentX-Model 快手(Kuaishou) 推薦の本番で、最良ラウンドのコードと評価条件を次の実験に引き継ぐ 3. 結果の活用 問い1:何を試すか —— 多様性を設計する 筆者の仕組みでも、体感で研究案の半分ほどが損失関数の微調整でした。 エージェントに自由に案を出させると似た案に偏りがちで、推薦だと損失関数の微調整や、モデルの小さな変更ばかりが並びます。 前置きの Navier–Stokes の事例では、通信を群の中に限り、証明を目指す群と反証を目指す群を分けることで、多様性を保っていました。 以下の3本の論文も、「多様な案を出して」と頼むだけでなく、案の選び方や枠の分け方といった構造で多様性を保とうとしています。 ① 似た案どうしを戦わせ、まだ探っていない領域の仮説も出させる(Co-Scientist) Google の Co-Scientist [3] は、研究者が研究の目標を自然言語で渡すと、仮説を提案してくれるシステムです。 中では、仮説を出す役、批判する役、順位を付ける役、改良版を作る役、似た仮説同士を見分ける役など、役割の違うエージェントが並行して動いています。 土台にあるのはトーナメントです。 仮説に「何点」と付けるのではなく、2つの仮説を討論させて「どちらが良いか」を判定し、その勝敗の積み重ねから順位を付けます。 仕組みはチェスや将棋のオンライン対戦のレーティング(Elo レーティング)と同じで、各仮説が持ち点を持ち、強い相手に勝てば大きく上がり、弱い相手に負ければ大きく下がります。 この持ち点の高い順が仮説の順位になり、研究者はこの順位の上位から、実験で確かめる仮説を選びます。 ただ、上位の案を改良するだけだと、候補は似た案ばかりになります。 Co-Scientist は、このトーナメントに、候補の幅を保つ工夫を3つ加えています。 似た案どうしを優先して対戦させる :専用のエージェントが仮説どうしの似ている度合いを測り、似た案をまとめたり、重複を除いたりします。対戦の組み合わせもこの似ている度合いを使って決め、似た案どうしが当たりやすくなっています。論文は、この仕組みで重複を除き、目標に関わる幅広い案を示せると説明しています。筆者は、似た案どうしで優劣が付くため、似た案が上位を独占しにくくなると考えています。 まだ探っていない領域の仮説を出させる :新しい仮説を出すときは、既存の仮説と、トーナメント全体を振り返った講評を読ませ、まだ探っていない領域を見つけさせます。改良版を作る役にも、上位の案を改善したり組み合わせたりする指示のほかに、一部の既存の案とは異なる発想で新しい案を作る指示があります。 改良版は元の案を上書きしない :改良版も改良前の案も残し、どちらも同じトーナメントで競わせます。候補が「最新版だけ」に絞られず、改良が失敗しても元の案は失われません。 ② 「既存の延長の枠」と「別系統の枠」を並行して持つ(EvoDiverse) Georgia Tech などの EvoDiverse [4] (ICML 2026)は、「スコア上位の案だけを残して改良を続けると、候補が似た案ばかりになる」という問題を扱った研究です。 EvoDiverse が改善の対象とするのは、次のような探索です。 LLM に案を出させて採点し、スコアの良かった案を複数選んで、それらを元に新しい案を作らせます。 新しい案も採点し、この手順を繰り返します。 論文では、この手法を「進化的な探索」、新しい案の元にする案を「親」と呼びます。 毎回スコア上位の案だけを元にすると、次の案はそれを少し変えたものになり、それがまた上位に残って次の元にする案になります。 これを繰り返すと、数回のうちに候補のほとんどが同じ案を少し変えただけのものになります。 最初はスコアが低くても改良すれば伸びたかもしれない別系統の案は、一度も元にする案に選ばれないまま消えていきます。 そこで EvoDiverse は、候補を入れておく枠を2つ用意し、枠ごとに「次に元にする案をどう選ぶか」を変えます。 既存の延長の枠 :元にする案はほぼスコア順に選びます。今いちばん良い案を、少しずつさらに良くしていきます 別系統の枠 :元にする案はスコアの差をあまり反映せずに選びます。たとえば1位の案と5位の案が、同じくらいの確率で選ばれます。スコアがまだ低い別系統の案にも元にする案に選ばれる機会があるので、改良される前に消えずに済みます さらに工夫が2つあります。 枠ごとに LLM への指示を変える :別系統の枠では「構造が大きく異なる案を出して」、既存の延長の枠では「良いとわかっている案を改良して」と指示します 有望な案だけを既存の延長の枠に移す :数回ごとに、「別系統の枠」にある案を「既存の延長の枠」に移します。ただし移すのは、「既存の延長の枠」にある案とスコアを比べて勝負になりそうなものだけです。無条件に移すと2つの枠の中身が似てしまい、枠を分けた意味がなくなります 結果として、分子設計・数式の発見・アルゴリズムの発見のいずれでも、 同じ評価回数で案の質と多様性がともに上がっています 。 ③ 改善しなかった研究の方向を禁止し、別の研究の方向の仮説を出させる(Arbor) 中国人民大学と Microsoft Research の Arbor [5] は、これまでに試した案を木構造で管理するシステムです。 たとえば「モデルの精度を上げる」という目標なら、「データを増やす」「モデルを大きくする」のような研究の方向を木の1段目に、「データを2倍にする」「外部データを足す」のような具体案をその下の段に置きます。 具体案を1つ試すたびに、結果と「なぜそうなったか」をその案のところに書き込みます。 この木を使って、同じ研究の方向の探索を続けないようにしています。 研究の方向ごとに結果を教訓にまとめ、否定された研究の方向は次の案出しの制約にする :具体案の結果がたまると、上の研究の方向に「データを増やしても、ノイズが増えるだけで精度は上がらない」のような結論をまとめます。見込みがなければその研究の方向を打ち切り、理由を次の案出しに破ってはいけない制約として渡します。打ち切った研究の方向には戻れないので、次の案は別の研究の方向から出すしかありません 同じ研究の方向で現ベストを超えなければ、新しい研究の方向を加えさせる :ある研究の方向の最近の具体案がどれも現ベストを超えなければ、失敗のパターンをまとめさせたうえで、1段目に新しい研究の方向を足すよう促します 全体が伸び悩んだら、発想の転換を促す :改善しない実験が続くと、回数に応じて「警告」→「発想を大きく変えろ」→「停止」の信号を段階的に出します Arbor の要点は、木で履歴を管理すること自体よりも、 研究の方向ごとに結果を教訓にまとめ、否定された研究の方向を次の案出しの制約にすることで、同じ研究の方向の案を出し続けるのを防ぐ 点にあります。 問い2:実験予算をどこに使うか —— 実行の前に選ぶ 実際に改善サイクルを回してみると、GPU コストに見合わない小さな改善案も出てきます。 エージェントは改良案を一瞬で何十個も書けますが、学習と評価には数時間かかり、推薦ならその先に本番 A/B テストの枠という強い制約もあります。 どの案に実行予算を使うかは、研究の進み具合を左右する重要な要因です。 Navier–Stokes の事例でも、易しい Euler 方程式の問題が解けてから、Navier–Stokes に資源を移していました。 以下の研究では、実行する価値があるかを改良案ごとに事前に判断します。 ① 学習・評価の前に、過去の実験結果を根拠に LLM に案を選ばせる(RPM) Meta の RPM(Research Preference Models) [6] は、エージェントが書いた改良案の中から「実際に学習・評価するのはどれか」を、事前に LLM に選ばせる仕組みです。 たとえば15個の改良案から1個を選び、その1個だけを学習・評価します。 選び方の工夫は3つです。 2案ずつ比べて勝ち残りを決める :「この案は何点か」ではなく「A と B のどちらが伸びそうか」を判定させ、勝った方を次の比較に進めます(Co-Scientist と同じ考え方) 判断材料として過去の実験結果を渡す :これまでに学習・評価した案のコードとスコアを一緒に見せます。論文の分析では、過去の実験結果を根拠にした判断は正解率が高く、「構造的に良さそう」といった印象だけの判断は正解率が低くなっていました 小さく試してから選ぶ :短時間の予備実験をしてから選ぶ版も用意しており、こちらの方が成績が良くなりました 選別なしで24時間かけて届いた成績に、 約15時間(実行予算の3分の2以下) で到達しています。 ② 過去の経験から改善しないと分かる案は、実験せずに見送る(EEM) 中国科学院 計算技術研究所と Baidu の EEM(Experimental Experience Modeling) [7] は、案を試す前に「そもそも実験する必要があるか」を判断する仕組みです。 過去の実験から「この条件では、この変更をしても精度は上がらなかった」のような経験をまとめてためておき、新しい案が来たら関連する経験を探して判断材料にします。 改善しないと分かる案は、実験せずに見送る :似た案で改善しなかったと分かっていれば、実験せずに見送ります 経験だけで判断できなければ、判断に必要な点だけを小さな予備実験で調べる :たとえば「データが少ない条件でも効くか」だけが分からなければ、その条件だけを小さく試します 結果を経験に追加する :失敗した実験や途中で止めた実験も含めて経験に加え、次の判断に使います 既存の自動研究システムより成績が高く、LLM とのやり取りも少なくて済んでいます。 ③ 残り予算に応じて、手広く試すか絞るかを切り替える(PrimeScientist) カリフォルニア大学サンディエゴ校とジョンズ・ホプキンス大学の PrimeScientist [8] は、次に予算を使う案を評価に応じた確率で選び、残り予算が減るほど評価の高い案が選ばれやすくなるようにする仕組みです。 Arbor と同じように、試している案を木の形で持ち、それぞれの結果を書き込んでおきます。 序盤は評価の低い枝も選ばれるようにし、終盤は評価の高い枝を選びやすくする :序盤は評価の低い枝にも予算を回し、残り予算が減るほど評価の高い枝に集中させます。 採用しなかった案も木に残す :評価の低い案とその結果も消さずに残し、後から戻れるようにします 評価済みの案を発展させるときは、本当に違う案だけを出させる :ハイパーパラメータだけを変えた案は禁止し、別の仮説を最大3つまで出させます 12の AI 研究タスクで、同じ予算のもと既存システム(AutoResearch)より 平均スコアが10.3%高く、試行回数は約半分 でした。 問い3:今の結果をどう次に活かすか 改善サイクルを回すと、案ごとの結果が積み上がっていきます。 ただ、結果を記録しておくだけでは次の案は良くなりません。 筆者の仕組みでも、長期記憶のステップを外すと、同じ案を繰り返したり、過去の失敗と似た案が出てきたりしました。 問題は、結果を どんな形にまとめ、次のどの判断に渡すか です。 Navier–Stokes の事例では、Codex が各群の有用な中間成果を集約し、追加プロンプトとして群に戻していました。 以下の3本も、結果のまとめ方と渡し先を工夫しています。 ① ベストとの比較で、結果を教訓にまとめる(MARS) Google Cloud AI Research の MARS [9] (ICML 2026)は、試した案の結果を教訓にまとめてため、次の案づくりに使う仕組みです。 結果を単独で見ても何が改善につながったのかは分かりにくいため、新しい案をその時点で一番良い案と比べ、違いに絞って次の3点を書き残します。 何を変えたか :例)ベストとの違いは、学習率を下げたことだけです 結果とその理由 :例)損失の振れが収まり、検証スコアが0.81から0.83に上がりました。学習時間は変わっていません 今後も使えるルール :例)この種のデータでは、学習率を下げると改善します 渡し方にも工夫があります。 教訓は全体で共有し、使ったら引用させる :次の案を作るときに教訓を参照させ、どの教訓を使ったかを明示させます 重複を除き、直近の教訓だけを渡す :既存の教訓と内容が同じ教訓は捨て、渡すのは直近30件までにします 教訓がたまるほど案を高度にする :教訓がないうちは軽いモデルから始め、たまるにつれて教訓を取り込んだ高度な案に進みます 案の約66%が既存の教訓を使っており、 使われた教訓の63%は別の枝(探索の系統)で得られたもの でした。 MLE-bench(機械学習コンペのベンチマーク)では、同じ条件で比べたオープンソースの仕組みの中で最も高い成績を出しています。 ② 伸び悩んだら、同じ系統の過去の試行や他の系統の良い解を、次の案づくりに渡す(MLEvolve) Shanghai AI Lab などの MLEvolve [10] は、前処理から学習・予測までのパイプライン一式を、木探索で少しずつ改良していく仕組みです。 従来の木探索では枝(改良の系統)が互いに孤立していて、ある枝で効いた工夫が別の枝に伝わりませんでした。 MLEvolve は、行き詰まりの度合いに応じて、次の案を作るときに渡す過去の結果を切り替えます。 同じ枝で3回続けてスコアが更新されない :同じ枝の直近の試行を振り返らせ、何が改善につながり何が失敗したかを踏まえて次の案を出させます。例)正則化をいくつ試しても頭打ちだった枝で、エージェントが「ボトルネックはモデル構造だ」と判断し、構造を変える案を出しました 探索の後半で、他の枝にスコアの高い解がある :他の枝の上位の解を参考として渡します。例)Focal Loss で頭打ちの枝に、別の枝で改善につながっていた Asymmetric Loss を取り込みました 全体のベストスコアが6ステップ更新されない :複数の枝の良い部品を組み合わせ、新しい枝の出発点にします 部品を1つずつ外す実験では、「同じ枝の振り返り」を外したときの落ち込みが最も大きく、 メダル獲得率が66.7%から33.3%に半減 しました(9タスクでの比較)。 著者は、直近の履歴を使うことが同じ失敗の繰り返しを防いでいると解釈しています。 他の枝の結果を渡すのは、行き詰まったときだけです。 筆者は、これが Navier–Stokes で集約を「ときどき」にしていたのと同じく、枝ごとの多様性を保つ効果もあると考えています。 全体では、MLE-bench において12時間の予算(標準の半分)で平均メダル獲得率65.3%と、比較した中で最高の成績です。 ③ 最後ではなく一番良かったラウンドから次の実験を始め、進み具合を3つの基準で測る(AgentX-Model) 快手(Kuaishou)の AgentX-Model [11] は、産業用の推薦モデルの改善研究を、LLM エージェントに何週間も続けさせた本番運用の報告です。 次の実験を計画するエージェントと、実装・学習・評価をするエージェントを分け、実験が終わるたびに次の4つを引き継ぎます。 引き継ぐもの 次の実験で何を決めるのに使うか どのラウンドのコードと、その基準モデル どの実装から再開するか 比較対象・評価条件・測定値 どの結果どうしを比べてよいか 診断での観察と、未検証の仮説 原因を見分けるテストの設計 組み合わせる各実験の変更全体 重複の解消と、組み合わせ方の設計 特に参考になるのは次の2点です。 最後ではなく、一番良かったラウンドのコードを残す :複数ラウンドにわたる189件の実験のうち、 48%で最後のラウンドが途中のベストを下回っていました 。最新版を上書きで引き継ぐと、良い出発点を失います 進み具合を3つの基準で別々に測る :「本番モデルより良いか」「元にした実験(親)より良いか」「それ以前のどの祖先よりも良いか」。最も長く運用したシナリオで改良した実験を見ると、親と比較できる91件のうち、46件が親を上回りました。一方、祖先すべてと比較できる77件のうち、祖先すべてを上回ったのは7件にとどまりました。親との比較だけでは、一度下がったスコアが戻っただけなのか、新記録なのかを区別できません 4つのシナリオすべてで本番モデルの AUC を上回り(最大 +0.0237)、直近5件の本番 A/B テストでも改善を報告しています。 研究を踏まえて、改善ループをどう変えるか 前置きで見たとおり、既存のモデルを使う私たちが工夫できるのは、モデルの外側の仕組みです。 ここまでの研究を踏まえて、冒頭の改善ループの変え方を2つ考えます。 現状 仮説構築 → 実装 → レビュー → 評価 → 長期記憶への書き込み → 仮説構築に戻る 案1:仮説を2つの枠に分け、トーナメントで実装する案を選ぶ 仮説構築( 既存の延長の改善策 / 別系統の改善策 ) → 長期記憶による足切り → 仮説プールから枠ごとのトーナメント → 実装 → レビュー → 評価 → 講評の作成 → 長期記憶への書き込み( 結果+講評 ) → 仮説構築に戻る 仮説構築 :既存のモデルを改良する「既存の延長の改善策」と、構造が大きく異なる「別系統の改善策」を、別々の指示で出させます(EvoDiverse) 長期記憶による足切り(新) :長期記憶に、同じ条件で同じ変更をして改善しなかった記録があれば、実験せずに見送ります。残った案には関連する過去の実験結果を判定材料として添え、見送った案も理由とともに記録します(EEM、RPM) 仮説プールから枠ごとのトーナメント(新) :既存の延長の改善策どうし、別系統の改善策どうしで2案ずつ討論させ、得点を付けます。似た案どうしを優先して対戦させ、各枠の上位案を実装に回します。実装しなかった案や改良前の案もプールに残し、次のサイクル以降も対戦させます(Co-Scientist) 講評の作成(新) :評価結果を見て、トーナメントで勝った案が実際に改善したか、どんな案が勝ちやすかったか、どんな案がまだ試されていないかをまとめさせ、長期記憶に残します。次の仮説構築では、この講評をもとに、まだ試していない種類の別系統の改善策を出させます(Co-Scientist) 案2:仮説方向ごとに実験を積み上げ、集約(リリース)は別の実験として行う 案2では、「特徴量」「モデル構造」といった仮説方向を決め、案1のループを仮説方向ごとに独立して回します。 それぞれの成果を組み合わせて本番に載せる集約(リリース)は、別の実験として行います。 仮説方向ごとの実験 〔仮説方向ごとに〕 仮説構築( 既存の延長の改善策 / 仮説方向内での新規の改善策 ) → 長期記憶による足切り → 仮説プールから枠ごとのトーナメント → 実装 → レビュー → 評価 → 講評の作成(トーナメントの講評+進み具合の判定) → 長期記憶への書き込み → 仮説構築に戻る 仮説構築 :その仮説方向で過去最高だったモデルを改良する「既存の延長の改善策」と、同じ仮説方向の中でこれまでと異なる手法を使う「仮説方向内での新規の改善策」を、別々の指示で出させます(EvoDiverse) 仮説プールから枠ごとのトーナメント(新) :同じ仮説方向・同じ枠の案どうしで対戦させます。次の実験は、最後のラウンドではなく、一番良かったラウンドのモデルから始めます(Co-Scientist、AgentX-Model) 講評の作成(新) :トーナメントの講評に加えて、「本番モデルより良いか」「直前の親実験より良いか」「その仮説方向の過去最高を更新したか」の3つの基準で進み具合を判定し、次の仮説構築に渡します(AgentX-Model) 仮説方向どうしは結果を直接見せ合わない :探索の多様性を保つためです(Navier–Stokes の群の分け方) 集約(リリース)実験 各仮説方向の過去最高を集める → 組み合わせ仮説の作成 → 仮説プールからトーナメント → 実装 → レビュー → 評価( 本番モデルと比較 ) → リリース判断 → 教訓を長期記憶に戻す 各仮説方向の過去最高を集める(新) :それぞれで一番良かったラウンドのモデルについて、変更内容と評価結果を集めます 組み合わせ仮説の作成(新) :集めた変更全体を見て、「どの変更をどう組み合わせるか」の仮説を複数出させます。重複する変更は、仮説の段階で解消します(AgentX-Model、MLEvolve) 仮説プールからトーナメント(新) :組み合わせ仮説どうしを2案ずつ討論させ、上位の仮説を実装に回します。判定材料として元の実験結果を添え、実装しなかった仮説もプールに残します(Co-Scientist、RPM) 本番モデルと比較・リリース判断(新) :仮説方向ごとの実験とは別の実験として、比較対象と評価条件を揃えて評価します。本番モデルを上回った組み合わせだけを、リリース候補にします 教訓を長期記憶に戻す(新) :組み合わせても改善した変更と、組み合わせると改善しなくなった変更を、教訓として戻します。コードは仮説方向ごとに分けたまま、教訓を共有して他で得た結果を活かせます(MARS、Navier–Stokes の集約) まずは既存のループに組み込みやすい案1から試し、改善が積み上がってきた段階で、案2のように仮説方向ごとの実験と集約(リリース)を分けることを検討したいと考えています。 まとめ 本記事では、自動研究の論文を紹介しながら、その仕組みを推薦システムの改善ループにどう取り入れられるかを考えました。 改善ループをどう組むかによって、推薦エンジンの最終的な精度も変わるはずです。 また、 dots や grok bot などの登場も踏まえ、エージェントにどのような役割やツールを与え、どう協調させるかにも注目しています。 たとえば、仮説方向を選ぶ役と具体的な仮説を作る役を分けるように、役割の切り方にも工夫の余地があります。 自動研究の動向を追いながら、推薦システムの改善に活かせる設計を引き続き探っていきます。 We’re Hiring! 現在、タイミーでは、データサイエンスやエンジニアリングの分野で、共に成長し、革新を推し進めてくれる新たなチームメンバーを積極的に探しています! 現在募集中のポジションは こちら です。 また、気軽な雰囲気での カジュアル面談 も随時行っています。 「話を聞きたい」と思われた方は、ぜひお気軽にエントリーしてください。 参考文献 [1] OpenAI, “ On the Navier–Stokes Millennium Prize Problem ” (2026) [2] Noam Brown, Dwarkesh Podcast (2026) [3] Gottweis et al., “ Accelerating scientific discovery with Co-Scientist ,” Nature (2026) [4] “ EvoDiverse ,” arXiv:2606.10587, ICML 2026 [5] “ Arbor ,” arXiv:2606.11926 [6] “ AI Research Preference Models ,” arXiv:2608.13940 [7] “ Experimental Experience Modeling (EEM) ,” arXiv:2609.39392 [8] “ PrimeScientist ,” arXiv:2609.17846 [9] “ MARS ,” arXiv:2602.02660, ICML 2026 [10] “ MLEvolve ,” arXiv:2606.06473 [11] “ Advancing Model Research in AgentX ,” arXiv:2609.30001
こんにちは。株式会社タイミーのバックエンドエンジニアの神山( @dak2 )です。 2026/9/19 に名古屋の大須演芸場で開催された名古屋 Ruby 会議05 に登壇してきました。 タイミーでは、世界中で開催される技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があり、そちらを活用して現地でカンファレンスに参加してきました。 今回は『特殊変数大全』というテーマで登壇しました。印象に残ったセッションや、当日の様子を紹介します。 Road to 名古屋 Ruby 会議05 会場の大須演芸場は、大須観音や大須商店街のある賑やかな大須界隈に位置する、1965年開館の寄席です。 www.osuengei.nagoya そんな場所で喋れる機会があったら、もちろん気になるじゃないですか? しかも今回は出囃子を生演奏してくれるとのことで、これは絶対に壇上で喋りたいやつじゃん…!となるわけです。また、せっかくなので落語の着物を着たいと思ってました。 ということで、勢いよくプロポーザルを出しました。あと、このプロポーザルは自信がありました。 Accepted された時は嬉しかったですね。 2026/9/19(土) が開催日でしたが、前日入りで名古屋に訪れました。 ホテル代が高騰してた(アジア競技大会)のでカプセルホテルに泊まったんですが、かなり居心地良かったですよ。ウェルビー栄というサウナの聖地なんですがね。最高でした。 うなぎ食べたり、名古屋城行ったりしつつ、なぜか名古屋城のほとりに金シャチ横丁という食べ歩きができる施設があるんですが、そこでスライドを直してましたね。まさかお城の周辺でスライド直すとは思ってなかった。 自分のセッション 落語の寄席は流れがあって、前半 → 仲トリ → 仲入り → 食いつき → ヒザ → トリ とざっくりこういう流れで進んでいきます。 仲入りは、寄席の途中に設けられる休憩です。今回、自分は昼休憩直前の『仲入り前』の枠で登壇しました。 speakerdeck.com 本当にたまたま Ruby のリファレンスで特殊変数を読んでいて、Ruby::Box を有効にすると $_ はどう動くんだろう?と試したら、古い値が返るバグを見つけたところから探索が始まりました。 特殊変数、本当に奥深いんですよね、一枚岩じゃないんですよ。同じ $ でもプロセスやスレッド、ブロックごとに違ったりとか。それを注意深く追ってると、Box と Ractor で扱いが交差してくるしで一筋縄ではいかない。 スライドの内容を考えるにあたって、Ruby の処理系を真面目に追うことになりました。 皆さんも特殊変数追ってみてください(根気は必要そうです) スライドは少しでも落語調にしたくて UI を調整しました。スライド右の方にはめくりを表す白の余白、スライド下部には緋毛氈を表す装飾を施しました。フォントは寄せ文字の有料フォントです。このためにフォントを初めて買いました。(会場のコントラストの関係で、黒赤だと見づらかったようです) 演奏していただいた出囃子の楽曲は、たぬきという曲でした。化けるたぬきということで、$ の形をしながらもいろんな場所で姿を変える特殊変数を表したいというコンセプトでした。 これは撮影してもらった登壇時の写真です。初めて落語の着物を着ました。せっかくの機会なので自費でレンタルしました。めちゃくちゃ暑かったけど、こんな機会は滅多にないので着れてよかったです。 Special thanks to @tonyfactory210 他の方のセッション 片月亭 黒曜さんの型の話 「型 再考」、お聞きいただきありがとうございました! こちら発表資料です!(一応Claudeで可能な限り裏取りしましたが、なにか間違った記述あったらすみません) https://t.co/QGlKiZGnra #nagoyark05 — 黒曜@Leaner Technologies (@kokuyouwind) 2026年9月19日 やはり、漸進的型付けはそれ自体が難しいという感想を抱いた。型付け自体の有用性は認めつつ、型付けで表現するということは、その表現可能な集合に文法が規定されるんだなという感想を持ちました。Ruby のように言語自体の表現力が非常に高い言語だとそりゃ難しいよなあと。untyped とかそもそも型検査を skip するようなハッチが生まれがち。それを直す契機は大体バグが起きた時なのもまた難しいポイントですね。 盆暗亭 しおいさんの Socket.getifaddrs の話 名古屋Ruby会議05にてお話しした『時うどん〜Socket.getifaddrsで学ぶネットワーク編』の発表スライドです。出囃子は『ふるさと』でした。お付き合いいただきましてありがとうございました! https://t.co/A5sjIqClTD #nagoyark05 — Misaki Shioi (しおい) (@coe401_) 2026年9月20日 上方落語の時うどんをアレンジしていてこだわりを感じました。うどん1杯を食べるために1文足りない主人公がいま何刻かを店主に言わせて、そこから数えて1文をちょろまかす話ですが、それがネットワークインターフェースになっており、近代化を感じました(?) ネットワークインターフェースの数の数え方は勉強になりました。 Socket オブジェクトを触ってみましたが、 UDPSocket などあって探索しがいがあり面白そう。追ってみたいなと思いました。 kamipo さんの基調講演 名古屋Ruby会議05の登壇資料です #nagoyark05 https://t.co/8akp5Xk9y5 — Ryuta Kamizono (@kamipo) 2026年9月22日 Rails の JOIN とどう向き合ってきたかというお話しでした。 joins、元々種類ごとに並び替えられてたんですね。left_joins 最後だったと。string joins の位置、join alias で where を先に書かないといけない問題の対応とか。 where.missing と where.associated 周りの話は興味深かったです。関連の有無を判定するSQLについて、JOINを使う方法と、NOT EXISTS / EXISTSを使う方法の違いが印象に残りました。着実な最適化がされてきたんだなと ActiveRecord の進化の歴史の一部を感じられました。 LATERAL JOIN 知らなかったので勉強になりました。サブクエリを行単位で適用できるのか。 観光など 台湾料理 矢場味仙 で台湾ラーメン食べたんですが、美味しかった!ただ、非常に辛かったです…! 愛知県長久手市にある Totopia Brewery というクラフトブルワリーにお邪魔しました。Hazy IPA のラインナップを推していて非常に香り高い一杯を提供してくれます。ずっと行きたかったので行けて最高でした! Noxphobia - TDH Hazy Double IPA 終わりに 名古屋Ruby会議05に参加してよかったです。滅多にできないことをやれましたし、何より勉強になるセッションが多く、気づきを得られました。 改めてですが、タイミーでは、世界中で開催される技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。こちらご興味をお持ちいただけましたら、ぜひカジュアル面談にお申し込みください! product-recruit.timee.co.jp カジュアル面談のお申し込みはこちら
はじめに こんにちは。プラットフォームエンジニアリングチームに所属している徳富( @yannKazu1 )です。 VPC Flow Logs に、 VPC 内のサーバーがインターネット上の IP の 6379 番ポートと通信している 行が記録されているのを見つけました。6379 は Redis のポートなので、字面だけ読むと「インターネット側に Redis がいて、そこに繋ぎに行っているのか?」と読めてしまいます。 この記事では、このログをパケットのヘッダーとしてどう読むか、そしてそこから立てた仮説を Flow Logs だけでどう裏付けたかを書いていきます。 きっかけ VPC Flow Logs を眺めていたときのことです。 Redis のポートである 6379 に絞ってフィルタをかけたら、見慣れないパブリック IP が出てきました。 本記事の IP アドレスについて 実際の値は伏せ、すべてダミーに差し替えています。グローバル IP はドキュメント用に予約されている RFC 5737 のレンジ( 192.0.2.0/24 、 198.51.100.0/24 、 203.0.113.0/24 )、CGNAT 内部のアドレスは RFC 6598 の 100.64.0.0/10 、VPC 内と宅内のアドレスは RFC 1918 のプライベートレンジから適当な値を置いています。ENI ID と ISP 名も伏せました。 srcaddr dstaddr dstport action 10.0.1.100 203.0.113.45 6379 ACCEPT 10.0.1.100 は VPC 内のプライベート IP、 203.0.113.45 はインターネット上のパブリック IP です。つまりこの行をそのまま読むと、 VPC 内から外部の IP の 6379 番ポートへ通信している ことになります。 6379 は Redis のポートです。となると「知らないうちに外部の Redis に繋ぎに行っているのか?」「何かに乗っ取られて外のキャッシュにデータを投げているのか?」という絵が浮かんでくるログです。少なくとも、VPC 内で使っている Redis は VPC に閉じているはずなので、この行の存在自体が説明できない。思わず手が止まる 1 行だと思います。 パケットレベルで考えれば、当たり前だった ここで思い出すべきは、 Flow Logs がどのレイヤーの記録なのか です。 Flow Logs に残るのは L3/L4、つまり IP ヘッダーと TCP ヘッダーの中身だけです。入っているのは「送信元 IP / 送信元ポート / 宛先 IP / 宛先ポート」の 4 つ組といったメタ情報だけです。 この目で読み直すと、結論は簡単です。 この ENI が ALB のものだと仮定すると、クライアントの送信元ポートが 6379 なら、戻りのパケットはその 6379 宛になります (ALB であることは後述のログで確認しています)。改めて送信元・宛先のポートを両方一緒に出力してみると、内部側は 443 番でした。TCP は行きと戻りで送信元と宛先が入れ替わるだけなので、この行は 203.0.113.45:6379 → 10.0.1.100:443 の戻りと考えれば一目瞭然です。6379 を名乗っているのはクライアント側で、 6379 は宛先ポートでなく送信元ポート 。接続ごとに振られる使い捨ての番号で、Redis とは関係ありません。 しかもクライアントの送信元ポートは、端末から ALB に届くまでに何度も NAT で付け替えられます。そのどこかで 6379 が選ばれるのは、 十分にあり得るし、何も不思議ではないシナリオ です。 この流れを整理しておきます。 端末から ALB の ENI までに起きていること 一般ユーザーがブラウザでサイトを開いてから、そのパケットが ALB の ENI に届くまで、いくつもの機器を経由します。そしてそのたびにヘッダーが書き換えられます。 flowchart TD A["端末(スマホ・PC)<br/>src 192.168.1.5:51000<br/>dst 192.0.2.10:443"] B["宅内ルーター(NAT)<br/>src 100.64.12.7:40277<br/>dst 192.0.2.10:443"] C["ISP の CGNAT<br/>src 203.0.113.45:6379<br/>dst 192.0.2.10:443"] D["Internet Gateway<br/>src 203.0.113.45:6379<br/>dst 10.0.1.100:443"] E["ALB の ENI<br/>ここで Flow Logs に記録される"] A -->|"送信元を書き換え"| B B -->|"送信元を書き換え → 6379 が誕生"| C C -->|"宛先を書き換え"| D D --> E 各段階を順に見ていきます。 1. 端末 ブラウザが接続を開くとき、OS が送信元ポートを 1 つ払い出します。これは「この通信を識別するための整理番号」であって、サービスの種類とは無関係です。Linux なら 32768〜60999、Windows なら 49152〜65535 から選ばれます(設定で変更可能)。 宛先は DNS で引いた ALB のパブリック IP と 443 番です。 2. 宅内ルーター 家庭内のプライベート IP のままでは外に出られないので、ルーターが NAT で送信元を書き換えます。このとき送信元ポートも付け替えられます。 3. ISP の CGNAT ← ここで 6379 が生まれる IPv4 アドレスが枯渇しているため、ISP は複数の利用者で 1 つのグローバル IP を共有する構成を取ります。CGNAT と呼ばれる仕組みです。 同じグローバル IP を共有するので、「誰の通信か」はグローバル IP とポート番号の組み合わせで見分けることになり、ここでもう一度ポートが付け替えられます。しかも多数の利用者分をさばくため、 well-known ポート以外の広い範囲を、ポートのプールとして使う実装があります 。OS がエフェメラルポートとして使う 49152〜65535 の範囲に収まらず、登録済みポート帯の番号が払い出されることもあります。 そして今回、たまたま 6379 が払い出されました。この瞬間から、この通信はずっと「6379 番」を名乗って進むことになります。 4. Internet Gateway VPC の入口です。ここでは宛先側が書き換えられます。端末は ALB のパブリック IP 宛に送っていますが、VPC 内部ではプライベート IP に変換されます。 5. ALB の ENI 到着。この時点のヘッダーがそのまま Flow Logs の 1 行になります。そして ALB が返事を返すと、送信元と宛先が反転したもう 1 行が記録されます。 sequenceDiagram participant C as クライアント(CGNAT 通過後)<br/>203.0.113.45 participant E as ALB の ENI<br/>10.0.1.100 C->>E: 203.0.113.45:6379 → 10.0.1.100:443 Note over E: Flow Logs 1 行目(インバウンド) E->>C: 10.0.1.100:443 → 203.0.113.45:6379 Note over E: Flow Logs 2 行目(アウトバウンド) 最初に目に入ったのは、この 2 行目でした。 行きのパケットの送信元ポートが、そのまま戻りのパケットの宛先ポートになる。TCP としてはごく当たり前の挙動ですが、これをポート番号だけで「6379 宛の通信」と読んでしまうと、一気に怪しい通信に見えてきます。 ログで裏付ける とはいえ、ヘッダーの読み方だけでは「そう解釈できる」までです。ログで裏付けを取りました。確認したのは 2 点です。 対向のインバウンドフロー( 外部:6379 → 内部:443 )が同じ ENI・同じ時間帯に存在するか その ENI が ALB のものかどうか 対向フローは全件見つかった 該当した 3 件すべてに、ペアとなるインバウンドが存在しました。 方向 送信元 宛先 パケット バイト → リクエスト 203.0.113.45:6379 10.0.1.100:443 31 10,164 ← レスポンス 10.0.1.100:443 203.0.113.45:6379 39 18,401 → リクエスト 198.51.100.72:6379 10.0.3.200:443 4 285 ← レスポンス 10.0.3.200:443 198.51.100.72:6379 7 405 → リクエスト 198.51.100.72:6379 10.0.3.200:443 14 4,027 ← レスポンス 10.0.3.200:443 198.51.100.72:6379 16 7,670 4 つ組がきれいに反転していて、1 対 1 で対応しています。完全な TCP セッションのペアです。 通信は外から始まっている 。VPC 内部から外の 6379 に繋ぎに行ったわけではない、という有力な裏付けになりました。 ENI は ALB のものだった 該当した 2 つの ENI に流れている他のトラフィックを見てみると、すべてが 外部IP:エフェメラルポート → 10.0.x.x:443 の形でした。送信元は国内の大手 ISP ばかり。 要するに、これは ALB の ENI です。一般ユーザーが HTTPS でサイトを見に来ている入口そのものでした。 送信元 IP を引いてみる 念のため GeoIP で送信元を確認しました。 IP 種別 203.0.113.45 国内コンシューマー ISP(モバイル回線) 198.51.100.72 国内コンシューマー ISP(固定回線) どちらも一般家庭・モバイル向けの ISP です。攻撃の出所になりやすいデータセンターや VPS 事業者の IP ではありません。 ただし、WHOIS や GeoIP で分かるのは「どの ISP の、コンシューマー向けらしいレンジか」までです。 その IP が CGNAT の出口かどうかは、外から見て断定できません 。ここで言えるのは「一般利用者の回線から来ていると考えて矛盾がない」という裏付けです。 バイト数の傾向も HTTPS らしく、レスポンスのほうがリクエストより大きい(10,164 → 18,401)。Web ページを返しているなら自然な形です。 結論 何も起きていませんでした。 ISP の CGNAT が送信元ポートを付け替える際に、たまたま 6379 を割り当てただけ。それ以降その通信はずっと「6379 番」を名乗って進み、ALB がそこへ返事を返した、というだけの話でした。 そしてこれは「たまたま今日起きた珍事」ではなく、 仕組み上、アクセスがある限り起こり続ける現象 です。6379 に限った話でもなく、3306(MySQL)でも 5432(PostgreSQL)でも同じことが起きます。 この仕組みを知らないとハマる落とし穴 今回は調査で済みましたが、理解していないと実害が出るケースもあります。 NACL の outbound DENY で本番障害になる これが一番怖いパターンです。セキュリティ強化のつもりで NACL に outbound: 6379 DENY を入れていると、 たまたま送信元ポート 6379 を引いたユーザーだけサイトが見られない という障害になります。 しかも症状が最悪です。再現しない。特定の人だけ。時間が経つと(NAT のポートが変わると)直る。問い合わせを受けても「こちらでは再現しません」という回答になりやすい類の事象です。 セキュリティグループはステートフルなので戻りが自動で通り、この問題は起きません。ハマるのはステートレスな NACL のほうです。同じ理由で、ステートレスなファイアウォールや、ポートベースの ACL も同様に要注意です。 Flow Logs の限界を知らないと結論を出しすぎる Flow Logs は ENI 単位の記録なので、見えるのは「ENI に届いた時点のヘッダー」だけです。 flowchart LR A[端末] --> B[宅内ルーター] --> C[ISP の CGNAT] --> D[Internet Gateway] D --> E subgraph OBS["VPC Flow Logs が観測できる範囲"] E["ALB の ENI"] end 見える — 送信元/宛先の IP とポート、プロトコル、パケット数、バイト数、ACCEPT/REJECT、TCP フラグ( tcp-flags 。SYN / SYN-ACK から接続を開始した向きも分かる) 見えない — 端末や宅内ルーターの実 IP(NAT で消えている)、ALB のパブリック IP(IGW が書き換えた後しか届かない)、通信の中身、パケット 1 つごとの TCP フラグ( tcp-flags は集約単位のビットマスクなので、ACK や PSH は含まれない) 先ほど 5 段階で経路を追いましたが、Flow Logs に残るのは最後の 1 段階だけです。手前で何度 NAT されたかは記録に一切現れません。 なので今回証明できたのは「この通信は ALB 宛の正常な HTTPS である」ところまでです。その中身が正当かを問うなら、 見に行くべきログのレイヤーが変わります 。どのパスにリクエストが来て何を返したのかならアクセスログ、そのリクエストがシステムの中で何をしたのかならアプリケーションのログ。知りたいことがどのレイヤーの話なのかで、見るログを選ぶ必要があります。今回の論点は L3/L4 の話だったので、Flow Logs だけで決着がつきました。 おわりに 結果は空振りでしたが、ヘッダーの向きでついた見当を、対向フロー・ENI・送信元 IP で裏付けて「正常です」と言い切れたのは良かったと思います。 そしてその見当がついたのは、端末 → 宅内ルーター → CGNAT → Internet Gateway → ENI という流れと、各段で何が書き換わるのかが頭に入っていたからです。 どのレイヤーのログを見れば決着がつくのかを選べるかどうかは、経路の全体像を持っているかで決まる と実感しました。
こんにちは。タイミーのデータエンジニアリング部DSグループでMLOpsを担当しているYukitomoです。 前回の記事では、Python + uvのモノレポ環境において、Renovateを用いて依存関係更新のプルリクエストを制御するための基本設計を紹介しました。 その後、実際に運用してみると、Renovateの管理対象はPythonの依存パッケージだけでは収まりませんでした。 devcontainer.json やCloud Runのマニフェストなどへ対象を広げるにつれて設定が複雑になり、Mend.io経由で動かして結果を待つだけでは、意図通りに動かない設定の原因を追いにくくなりました。 そこで今回は、前回からの更新として、以下の4点を紹介します。 Renovate設定をローカルやCI/CDで検証できるようにしたこと 検証手順をAIエージェント用スキルとして整備したこと devcontainer.json やCloud Runのマニフェストをアプリケーション単位のプルリクエストにまとめる設定 グルーピングされた更新で、対象となるすべてのファイルを bumpVersions で更新する設定 この記事の想定読者 Renovate設定を継続的に改善・デバッグしたい方 Mend.io経由でRenovateを運用しており、設定変更の検証に課題を感じている方 プライベートなGoogle Cloud Artifact Registryを使うモノレポでRenovateを運用している方 AIエージェントにRenovate設定の調査・修正・dry runを任せたい方 要約(TL;DR) Mend.ioへ反映する前に、ローカルやCI/CDでRenovateのconfig validationとdry runを実行できるようにした 検証手順をAIエージェント用スキルにまとめ、調査手順と実行環境を揃えた managerごとに異なる packageFileDir を正規化し、アプリケーション単位で更新をまとめた グルーピングされたプルリクエストでは、 upgrades に含まれるディレクトリを列挙し、すべての pyproject.toml / uv.lock を bumpVersions で更新した 前提とするモノレポ構成 今回の内容を説明するため、次のような簡略化したモノレポを想定します。 . ├── .renovaterc.json5 ├── group-x │ ├── project-a │ │ ├── .devcontainer │ │ │ └── devcontainer.json │ │ ├── kustomize │ │ │ ├── base │ │ │ └── overlays │ │ ├── pyproject.toml │ │ └── uv.lock │ └── project-b │ ├── pyproject.toml │ └── uv.lock └── group-y └── ... 記事中で扱う設定の全体像は次の通りです。説明を簡潔にするため、実際の設定から関係する部分だけを抜き出しています。 { extends : [ "config:recommended" ] , devcontainer : { managerFilePatterns : [ "/(?:^|/)\\.devcontainer/(?:[^/]+/)?devcontainer\\.json$/" , "/(?:^|/)\\.devcontainer\\.json$/" , ] , } , packageRules : [ { matchFileNames : [ "group-x/**" ] , groupName : "group-x" , groupSlug : "group-x" , additionalBranchPrefix : "{{packageFileDir}}/" , } , { matchFileNames : [ "**/devcontainer.json" ] , additionalBranchPrefix : "{{replace \"/\\.devcontainer\" \"\" packageFileDir}}/" , } , { matchFileNames : [ "**/kustomize/**" ] , additionalBranchPrefix : "{{{replace '/kustomize/.*' '' packageFileDir}}}/" , } , { matchUpdateTypes : [ "major" , "minor" , "patch" ] , bumpVersions : [ { bumpType : "patch" , filePatterns : [ "/^({{#each (distinct (lookupArray upgrades \"packageFileDir\"))}}{{{.}}}{{#unless @last}}|{{/unless}}{{/each}})\\/pyproject\\.toml$/" , ] , matchStrings : [ "version\\s*=\\s*\"(?<version>[^\"]+)\"" ] , } , { bumpType : "patch" , filePatterns : [ "/^({{#each (distinct (lookupArray upgrades \"packageFileDir\"))}}{{{.}}}{{#unless @last}}|{{/unless}}{{/each}})\\/uv\\.lock$/" , ] , matchStrings : [ "name = \"[^\"]+\"\\nversion = \"(?<version>[^\"]+)\"\\nsource = \\{ (?:editable|virtual) = \"\\.\" \\}" , ] , } , ] , } , ] , } AIエージェント用スキルは独立したリポジトリで管理しているため、そのファイル構成は後述の2章で説明します。 1. Renovate設定はローカルやCI/CDで検証してからデプロイする 一般的なソフトウェア開発では、変更をメインブランチに入れる前にローカルやCI/CDで検証します。Renovateの設定も同じように扱えるよう、ローカルでconfig validationとdry runを実行するスクリプトを用意しました。このスクリプトは、2章で説明するAIエージェント用スキルに含めています。 実際のスクリプトはもう少し複雑ですが、Google Cloud Artifact RegistryやGitHubへの認証情報を環境変数へ設定した上で、固定バージョンのRenovateを実行するのがポイントです。 RENOVATE_HOST_RULES="$(gcloud auth print-access-token | jq -Rc '[ {matchHost:"asia-northeast1-docker.pkg.dev",hostType:"docker",username:"oauth2accesstoken",password:.}, {matchHost:"asia-northeast1-python.pkg.dev",username:"oauth2accesstoken",password:.} ]')" \ GITHUB_COM_TOKEN="$(gh auth token)" \ RENOVATE_CONFIG_FILE=.renovaterc.json5 \ npx --yes --package "renovate@${RENOVATE_VERSION}" renovate-config-validator --strict # または、同様に環境変数を設定してdry runを実行 # TIER = extract | lookup | full npx --yes --package "renovate@${RENOVATE_VERSION}" renovate --platform=local --dry-run="$TIER" Renovateの設定項目は多岐にわたります。Mend.io経由で実行していると、一見エラーなく終了していても、一部の設定が意図通りに適用されていないことがありました。 Mend.io側の環境変数を変えてメインブランチ以外で動作させ、詳細ログを確認する方法でもデバッグできます。ただし、Gitリポジトリの管理者権限が必要になる場合があり、誰でも簡単に試せるわけではありません。また、設定変更をプルリクエスト化し、承認後にメインブランチへ反映してから実行結果を確認する方法では、フィードバックループが長くなります。 そのため、現在はRenovate設定を「ローカルで検証してからデプロイする対象」として扱うようにしました。 2. AIエージェント用スキルとして準備する Renovateのdebug logは非常に大きく、リポジトリが大きくなるとdry runにも時間がかかります。人が毎回同じ前提を確認しながらログを読むのではなく、AIエージェントが一定の手順で設定を調査・修正できるよう、検証手順をスキルとして整備しました。 構成は以下です。 plugins/renovate-config-tester/ ├── assets │ ├── sample.gha.yml # サンプルのGitHub Actions │ └── sample.renovaterc.json5 # Renovateのサンプル初期設定 ├── scripts │ └── renovate-local.sh # 1章で説明したRenovate実行スクリプト └── SKILL.md SKILL.md には、主に次の前提と調査手順を記載しています。 Renovateのバージョン Renovateは必ず固定バージョンで実行します。Renovateの管理対象となるファイル(例:GitHub Actionsのworkflow)にバージョンを記述し、その更新はRenovate自身に任せます。スキルは、このファイルから実行するバージョンを取得します。 # CI用GitHub Actions jobs : validate-and-dry-run : runs-on : ubuntu-latest env : # renovate: datasource=npm depName=renovate RENOVATE_VERSION : 44.29.4 # Renovate自身が更新し、スキルはここから取得する steps : - name : Install Renovate run : npm install -g "renovate@${RENOVATE_VERSION}" - name : Renovate dry run run : renovate --platform=local --dry-run=lookup Node.jsの必要バージョン RenovateはNode.js applicationであるため、Renovateのバージョンに対応したNode.jsが必要です。 本記事で扱っているRenovate 44系は、Node.js 24の環境で検証しました。手元ではNode.js 26環境で正常に実行できなかったため、スキルにはRenovateが対応するNode.jsバージョンを確認し、検証済みのruntimeを使うよう明記しています。 gcloudやghの認証情報が必要であること Private Artifact RegistryやGitHubにアクセスする必要がある場合は、事前に gcloud や gh の認証を済ませる必要があります。 ただし、AIエージェントにtokenを直接扱わせるのではなく、スクリプト( renovate-local.sh )の中で RENOVATE_HOST_RULES やGitHub tokenを組み立てるようにしています。AIエージェントはスクリプトを実行するだけで、credentialの組み立て方を毎回判断する必要はありません。 必要なツール類 RenovateのログはJSONL形式で出力されるため、調査には jq などが必要です。リポジトリや設定によっては yq も利用します。こうした周辺ツールもスキルの前提に含めることで、AIエージェントが調査できない理由を切り分けやすくしています。 実行ログは一旦ファイルに記録してから読む 対象リポジトリが大きい場合、debug logをそのまま読み込ませるとLLMのtokenを多く消費します。そこで、ログを一旦ファイルへ保存し、対象パッケージやbranchに関係する行だけを grep / jq で抽出します。 "${CLAUDE_SKILL_DIR}/scripts/renovate-local.sh" > /tmp/renovate.log 2>&1 スキルとして独立させたことで、自分が担当するリポジトリ以外にも同じ調査手順を展開できます。Renovate設定の調査方法を人へ説明する代わりに、AIエージェントが同じ前提と手順で調査できる状態にしたことが、今回の大きな変更です。 3. devcontainer.jsonやCloud Runのマニフェストもアプリケーション単位のプルリクエストにまとめる Renovateは当初、Pythonの依存パッケージ更新を中心に導入しました。その後、 devcontainer.json やCloud Runのマニフェストから参照するコンテナイメージなどにも対象を広げています。 前提とするモノレポで単純に対象managerを増やすと、同じアプリケーションに関する更新であっても、ファイルの場所によって別のプルリクエストになります。 pyproject.toml / uv.lock の更新 .devcontainer/devcontainer.json のimage/feature更新 kustomize配下にあるCloud Runマニフェストのコンテナイメージ更新 これらは「同じアプリケーションの実行・開発環境に関する更新」としてまとめて確認したいケースが多く、プルリクエストが細かく分かれるとレビューの負担が増えます。そこで、managerごとに異なる packageFileDir を正規化し、アプリケーション単位のbranch prefixへ揃えます。 A. モノレポ内のdevcontainer.jsonを検出する 各アプリケーション配下の .devcontainer/devcontainer.json もRenovateの対象へ含めるため、devcontainer managerの managerFilePatterns を追加します。 devcontainer : { managerFilePatterns : [ "/(?:^|/)\\.devcontainer/(?:[^/]+/)?devcontainer\\.json$/" , "/(?:^|/)\\.devcontainer\\.json$/" , ] , } , B. packageFileDirをアプリケーションのディレクトリへ正規化する 前回の記事で紹介した共通ルールでは、 packageFileDir を additionalBranchPrefix に使っています。しかし、devcontainer managerの packageFileDir には /.devcontainer が含まれるため、そのままでは pyproject.toml / uv.lock の更新と別のbranchになります。 そこで、devcontainer managerから抽出したdependencyについては、 packageFileDir 末尾の /.devcontainer を取り除きます。 // 前回の記事で紹介した共通ルール { matchFileNames : [ "group-x/**" ] , groupName : "group-x" , groupSlug : "group-x" , additionalBranchPrefix : "{{packageFileDir}}/" , } , // devcontainer.json用のルール { matchFileNames : [ "**/devcontainer.json" ] , additionalBranchPrefix : "{{replace \"/\\.devcontainer\" \"\" packageFileDir}}/" , } , これにより、たとえば group-x/project-a/.devcontainer は group-x/project-a へ正規化されます。 Cloud Runのマニフェストが kustomize/base や kustomize/overlays/env に分かれている場合も、同じ考え方で /kustomize/** を取り除きます。 { matchFileNames : [ "**/kustomize/**" ] , additionalBranchPrefix : "{{{replace '/kustomize/.*' '' packageFileDir}}}/" , } , この設定により、Python dependency、devcontainerのimage/feature、Cloud Runマニフェストのコンテナイメージを、アプリケーション単位のプルリクエストとして確認しやすくなります。 ただし、複数アプリケーションの更新をさらに1つのプルリクエストへまとめると、別の問題が発生しました。 bumpVersions.filePatterns で {{packageFileDir}} だけを参照すると、グループ内に複数ある pyproject.toml と uv.lock のバージョンを更新しようとしても、一部しか更新されなかったのです。4章ではこの対処方法を紹介します。 4. グルーピング時のbumpVersionsで対象ファイルを漏れなく更新する 前回記事は bumpVersions をtop-levelに置く例を紹介しました。現在はpathを条件に加えているため、 packageRules の中に移しています。 通常であれば、 filePatterns は {{packageFileDir}}/pyproject.toml のように書きたくなります。しかし、複数のアプリケーションを1つのプルリクエストへグルーピングした場合、この記述ではいずれか1つの pyproject.toml / uv.lock しか更新されませんでした。 RenovateのDiscussion #35770を参考に、グループ内の upgrades からすべての packageFileDir を取り出し、正規表現の選択肢として組み立てるよう変更しました。 { matchUpdateTypes : [ "major" , "minor" , "patch" ] , bumpVersions : [ { bumpType : "patch" , filePatterns : [ "/^({{#each (distinct (lookupArray upgrades \"packageFileDir\"))}}{{{.}}}{{#unless @last}}|{{/unless}}{{/each}})\\/pyproject\\.toml$/" , ] , matchStrings : [ "version\\s*=\\s*\"(?<version>[^\"]+)\"" ] , } , { bumpType : "patch" , filePatterns : [ "/^({{#each (distinct (lookupArray upgrades \"packageFileDir\"))}}{{{.}}}{{#unless @last}}|{{/unless}}{{/each}})\\/uv\\.lock$/" , ] , matchStrings : [ "name = \"[^\"]+\"\\nversion = \"(?<version>[^\"]+)\"\\nsource = \\{ (?:editable|virtual) = \"\\.\" \\}" , ] , } , ] , } , lookupArray で各upgradeの packageFileDir を取り出し、 distinct で重複を除き、正規表現の選択肢として連結しているのがポイントです。 設定 グループ内の対象 更新結果 {{packageFileDir}} のみを参照 複数ディレクトリ いずれか1組だけ更新 upgrades からディレクトリを列挙 複数ディレクトリ 対象となる全組を更新 この記述はRenovateのtemplate contextに含まれる upgrades を利用します。導入時には、使用するRenovateバージョンでconfig validationとdry runを実行し、想定するすべてのファイルが更新対象になっていることを確認してください。 まとめ 今回、Renovateの設定をMend.ioへ反映する前に検証できるようにし、その手順をAIエージェント用スキルとして整備しました。configだけでなく、実行環境やcredentialの渡し方、ログの調べ方まで含めて管理することで、同じ前提と手順で継続的に設定を改善できます。 また、 packageFileDir の正規化によって更新をアプリケーション単位にまとめ、複数アプリケーションをグルーピングする場合は、 bumpVersions の対象もグループ全体へ広げました。モノレポのRenovate設定は、検証と運用を繰り返しながら育てることが重要だと感じています。 このようなMLOps基盤や開発環境の改善に、一緒に取り組むメンバーを募集しています。 We’re Hiring! 現在、タイミーでは、データサイエンスやエンジニアリングの分野で、共に成長し、革新を推し進めてくれる新たなチームメンバーを積極的に探しています! 現在募集中のポジションは こちら です。 また、気軽な雰囲気での カジュアル面談 も随時行っています。「話を聞きたい」と思われた方は、ぜひお気軽にエントリーしてください。 References 前回記事: Renovateの設定 - Timee Product Team Blog Renovate configuration options Renovate package rules Feedback thread: bumpVersions · renovatebot/renovate · Discussion #35770
タイミーでAndroidアプリの開発をしている、みかみとmurataです。 2026年9月1日から3日に開催されたDroidKaigi 2026に、タイミーでAndroidアプリを開発するメンバーで参加しました。Event Day(9月1日)ではポスターセッションでタイミーのAndroid開発について発表し、Conference Day(9月2日、3日)ではセッションを聞きました。 この記事では、ポスターで発表した内容と、参加後にSlackや社内勉強会で話したことを紹介します。 ポスターセッションに参加しました DroidKaigi 2026では、Event Dayの新しい企画としてポスターセッションが行われました。ポスターの準備は Hunachi さんがリードし、私たちは次のテーマで発表しました。 〜週2リリースを支える開発基盤とAuth Tab実装知見〜 『スピード』と『安定性』を両立するタイミーのAndroid開発 タイミーでは、週2回のリリースを支える開発基盤を整えながら、開発スピードと安定性を両立するための改善を進めています。ポスターでは、開発基盤に関する取り組みとして、変更リードタイムの短縮に向けた開発基盤の改善、AIレビュー・自動検証・人によるレビューの組み合わせ、Lightweight Architecture Decision Records(LADR)の活用を取り上げました。あわせて、Auth Tabの実装で得た知見も発表しました。 LADRの運用については「 Androidチームが3倍規模に急拡大しても独自LADR(意思決定記録)は機能し続けたのか? 」、Auth Tabの実装についてはtick-takuさんの「 Android の OIDC ログインは Auth Tab を使おう 」の記事で、それぞれ詳しく解説しています。 会場には、企業やチーム、個人によるさまざまなポスターが並びました。気になったテーマの前で足を止め、そのまま発表者と話せるのがポスターセッションならではの良さです。私たちも、普段の開発プロセスや実装上の工夫を直接伝えることができました。 ポスター発表をリードしたHunachiさんは会場インタビューにも参加し、その映像はConference Dayの幕間コンテンツとして流れました。 セッションをきっかけに話したこと 参加後は、Slackや社内勉強会で気になったセッションについて話しました。セッション動画は開催後にYouTubeで公開され、公式サイトの各セッションページからも視聴できます。勉強会では、現地に参加したメンバーだけでなく、当日参加できなかったメンバーも動画や公式サイトを見ながら、気になったセッションについて一緒に話しました。 以下では、タイミーのAndroid開発でも試してみたいと話した二つのテーマを取り上げます。 mobile-mcp www.youtube.com デバイス操作はAIエージェントの時代へ。mobile-mcpを活用したAndroid UI/E2Eテストの挑戦 mobile-mcp は、AIと外部ツールをつなぐための仕様であるModel Context Protocol(MCP)を使い、AIエージェントからモバイル端末やエミュレータ/シミュレータを操作するためのツールです。今回はAndroidでの活用事例が紹介されました。自然言語で指示すると、画面操作や状態確認をAIに任せられます。セッションでは、目的の画面や状態を再現し、操作後の結果を検証する方法も紹介されました。 手元での動作確認や不具合調査に使ってみたいという声があった一方で、CIで継続的に実行するとトークン消費が積み上がることや、操作が安定せずテスト結果が不安定になる可能性を懸念する意見も出ました。まずはローカルで試し、精度やコストを確かめてみようと話しています。 AIを使ったコードレビューとテスト www.youtube.com www.youtube.com コードレビューのセッションでは、Gemini Code Assistを使ったPRレビューの運用事例が共有されました。登壇者の事例では、Android固有の構成やプロジェクトの前提がAIに共有されていないと、意図と異なる指摘が出ることもあるとのことでした。参考になったのは、コードの書き方や定型的な確認はAIレビューに任せ、人は仕様・設計・意図・リスクを判断するという分担でした。その前提として、設計方針やコーディング規約、テストの考え方をAIが参照できる形に整える必要があります。 Compose Screenshot Testingのセッションでは、Compose Previewから生成した画像を基準画像と比較し、UIの変更を検出する仕組みが示されました。CIからPRへ差分画像を投稿すれば、エミュレータを起動しなくても変更内容を確認できます。スクリーンショットをギャラリーにまとめ、UI仕様を見返せるようにしていた点も参考になりました。AIが実装したUIをレビューするときも、コードだけでなく見た目を確認できます。 コードレビューは変更が増えるほどボトルネックになりやすく、人とAIの責務を整理すれば、人は仕様・設計・リスクの判断に集中できるのではないかという話が出ました。PRの性質をAIも使って分類し、あらかじめ定めた条件を満たす低リスクな変更は、AIレビューと既存テスト・ビジュアルリグレッションテストを中心に完結させる、という案です。コードの整理のように影響範囲が小さい変更なら、検証の仕組みとセットで実装自体をAIエージェントに任せられないか、試してみたいと考えています。どの変更を任せられるか、人が最後に見るべき点はどこかを、これからの課題だと考えています。 DroidKaigi 2026に参加して 私たちが聞いたセッションでは、AIエージェントの活用や開発生産性に関する話が多くありました。コードレビューやテストにAIをどう組み込むか、その前提をどう整えるかというテーマが目立ち、私たちが取り組んでいる内容と重なる部分もありました。 ポスターセッションでは、普段の開発プロセスや実装上の工夫を、社外の参加者に直接伝えることができました。今年初めての企画でしたが、聞き手の反応を見ながら説明を変えられたり、その場で質問をもらえたりと、セッション登壇とは違う手応えがありました。自分たちの取り組みを言葉にする過程で、あらためて整理できたこともありました。 社内勉強会で出たアイデアは、できるところから試してみます。こうしたイベントで得た知見を持ち帰って議論し、開発に取り込んでいくサイクルは続けていきたいです。DroidKaigi 2026の運営や登壇に携わった皆さま、ポスターを見てくださった皆さま、ありがとうございました!
はじめに こんにちは。プラットフォームエンジニアリングチームに所属している徳富( @yannKazu1 )です。 MySQL が遅いとき、まず EXPLAIN で計画を見る。見積もりが怪しければ EXPLAIN ANALYZE で実測と突き合わせる。たいていのケースはこの二つで十分で、実際自分もそれで困ったことはほとんどありませんでした。 ところが先日、この二つをいくら眺めても答えが出ない場面に当たりました。最後に頼ったのが Optimizer Trace でした。普段のチューニングではまず使わないので、すっかり引き出しの奥にしまっていた手札です。そこに至るまでの経緯も含めて書いておきます。 まず、何が起きていたか Aurora MySQL を 3.04.2(8.0.28 互換)から 3.10.3(8.0.42 互換)にアップグレードしたら、通知配信バッチの特定クエリが 4,000〜5,000 秒 滞留するようになりました。アップグレード前は同じクエリが全体コスト 18.27 で、何事もなく一瞬で返ってきていたクエリです(なお、通知配信そのものへの影響は限定的でした)。 クエリは NOT EXISTS の中に IN サブクエリがネストされた構造でした(テーブル名は適当に置き換えています)。 SELECT users.id FROM users INNER JOIN notification_settings ON notification_settings.user_id = users.id WHERE ... AND NOT EXISTS ( SELECT * FROM user_events WHERE event_id IN ( SELECT id FROM events WHERE start_at BETWEEN ? AND ?) AND user_id = users.id) EXPLAIN を見て「?」となる events テーブル(5,000 万行くらい)が type: ALL でフルスキャンされていました。 possible_keys: PRIMARY, idx_events_start_at key: NULL 使えるインデックスが候補に挙がっているのに、使っていない。Datadog DBM の実行計画履歴を見ると、アップグレード前は idx_events_start_at を使った range でした。 試しに SET optimizer_switch='semijoin=off' にしてみると range に戻って見積もり 89,078 行に絞られたので、「semijoin へのフラット化まわりが怪しい」ことはわかりました(後述しますが、犯人はフラット化そのものではありませんでした)。わかったんですが、 なぜこうなるとインデックスが使えなくなるのか はまったくわからない。 EXPLAIN ANALYZE で統計情報の線を消す 次に疑ったのは統計情報です。今回は Blue/Green でアップグレードしてクラスターが丸ごと入れ替わっているので、統計情報がずれている可能性は十分にありそうだと思ったためです。 結果、テーブル行数の見積もりは 5,060 万に対して実測 5,300 万で 1.05 倍。インデックス経由の range も 89,078 に対して 52,129 で 1.7 倍。 どっちも妥当 でした。 一方で、フルスキャン経路だけが明らかにおかしかった。 -> Filter: (events.start_at between ? and ?) (cost=2.64e+9 rows=50.6e+6) (actual rows=52,129) -> Table scan on events (cost=2.64e+9 rows=50.6e+6) (actual rows=53e+6) 同じ BETWEEN 述語なのに、インデックス経由では 89,078 行と正しく見積もるのに、テーブルスキャン経路では選択率がまったく効かず 5,060 万行。実測の約 971 倍です。この誤差が上位ノードでどんどん増幅していました。 ここまでで「統計の問題じゃない、アクセスパス評価のほうがおかしい」とは言えるようになりました。 つまり、 EXPLAIN 系だけだと「変な計画が選ばれている」までしか言えない 。ここで手札が尽きました。 EXPLAIN / EXPLAIN ANALYZE / Optimizer Trace の使い分け Optimizer Trace の話に入る前に、三つの違いを整理しておきます。ここが曖昧だと Trace を出すタイミングが判断できないので。 EXPLAIN でわかるのは決まった計画だけ EXPLAIN SELECT ...; EXPLAIN FORMAT=JSON SELECT ...; -- コスト値も見たいときはこっち 結合順序、各テーブルのアクセス方法、使われたインデックス、推定行数。 FORMAT=JSON にすればコスト値も出ます。基本的にクエリは実行されないので(実体化される派生テーブルなど、一部内部的に実行が走る例外はあります)、重いクエリでも本番で気軽に打てます。日常的なチューニングはこれで十分です。 ただ、EXPLAIN が返してくれるのは、オプティマイザが検討を終えて定めた「こう実行するつもりです」という計画だけです。そこに至るまでに何を検討して、何を却下したのかは出てきません。 だから possible_keys にインデックスが並んでいるのに key: NULL という出力を見ても、「使えるはずのものを使わなかった」という事実がわかるだけで、理由はさっぱりわからない。まさに今回詰まったところです。 EXPLAIN ANALYZE は見積もりと実測を突き合わせる EXPLAIN ANALYZE SELECT ...; MySQL 8.0.18 以降。クエリを実際に走らせて、どう実行したのかと、実行計画の見積もりが実測とどれだけズレていたかを並べてくれます。 -> Filter: (events.start_at between ? and ?) (cost=2.64e+9 rows=50.6e+6) (actual time=22733..27300 rows=52129 loops=1) rows= が見積もり、 actual ... rows= が実測です。これが効くのは「 統計情報がずれているのでは? 」という仮説を切り分けたいとき。推定行数が実測とほぼ一致していれば統計は問題なし、逆に大きくズレていればそこが怪しい、という判断ができます。今回もこれで統計の線を消しました。 ただしこれも、見積もりが合っていたかしかわかりません。なぜその計画を選んだのかは、EXPLAIN と同様に出てきません。また、実行を伴うため、数千秒かかるクエリには気軽に打てません。 Optimizer Trace は「なぜその計画を選んだのか」 MySQL 5.6 から標準で入っている機能で、追加インストールも要りません。オプティマイザが何を検討して何を却下したのかを丸ごと吐き出してくれます。 「なぜその計画になったのか」を知りたいときはこれ一択でした。これも クエリの実行は不要 で、 EXPLAIN に対して取れば中身が出ます(後述)。 三つの違い 雑にまとめると、こうなります。 EXPLAIN … どう実行するつもりか(クエリ実行:不要) EXPLAIN ANALYZE … その見積もりは実測と合っていたか(クエリ実行: 必要 ) Optimizer Trace … なぜその計画を選んだのか(クエリ実行:不要。 EXPLAIN で取れる) 前の二つは「変な計画だ」で止まり、その計画を選んだ理由まで踏み込めるのが Trace です。 今回も、テーブル全体の行数見積もりは妥当なのに、 フルスキャン経路の行数見積もりだけ が実測の約 971 倍に崩れていました。そこまでは EXPLAIN 系で追えたのですが、なぜそうなるのかはどこにも出てきません。そこで Optimizer Trace を開くことにしました。 Optimizer Trace の出し方 拍子抜けするくらい簡単です。セッション変数を on にして、クエリを投げて、 information_schema から読む。それだけ。 SET SESSION optimizer_trace = ' enabled=on ' ; SET SESSION optimizer_trace_max_mem_size = 16777216 ; -- 16MB EXPLAIN SELECT ... ; SELECT * FROM information_schema.OPTIMIZER_TRACE\G SET SESSION optimizer_trace = ' enabled=off ' ; ファイルに落とすなら、後で jq に食わせる前提で JSON として保存します。 TRACE カラムの中身は JSON なので、ヘッダや罫線が入らないよう -N --raw --batch で出します。 mysql --comments -N --raw --batch -u user -p -e " SET SESSION optimizer_trace='enabled=on'; SET SESSION optimizer_trace_max_mem_size=16777216; EXPLAIN SELECT ... ; SELECT TRACE FROM information_schema.OPTIMIZER_TRACE; " | awk '/^\{/{found=1} found' > trace.json EXPLAIN の結果行も同じ標準出力に流れてくるので、 awk で JSON が始まる行以降だけを拾っています。対話シェルで目で見たいだけなら \G でもいいですが、 \G 付きの出力は *** 1. row *** や TRACE: といった装飾が付くので、そのまま jq には渡せません。 本番で打つときに安心できる点を二つ。Trace はオプティマイザの動きの記録なので、 実行を伴わない EXPLAIN でも中身が出ます 。数千秒かかるクエリを本番で流す必要はありません。そして SET SESSION で有効化した Trace は その接続にしか効かない ので、他のセッションを巻き込まず本番でも打てます。 Optimizer Trace を読む 最初に Trace を開いたときの感想は「うわ」でした。数 MB の JSON が出てきて、どこから見ればいいのか見当もつかない。 先に言っておくと、 全部読む必要はありません。 見るべき場所は数箇所です。以下、それぞれのステップが何を記録しているのかと、今回のTraceで何が見えたのかを並べて見ていきます。 全体の構造 トップレベルは三つに分かれています。 { " steps ": [ { " join_preparation ": { ... } } , { " join_optimization ": { ... } } , // 9割ここ { " join_explain ": { ... } } // 実行した場合は join_execution ] } 三つ目は、 EXPLAIN で取ると join_explain 、実行を伴うと join_execution になります。どちらにしても、読むのはほぼ join_optimization です。まずはどんなステップがあるかを一覧します。 # 最適化フェーズのステップ一覧 jq '.steps[] | select(.join_optimization) | .join_optimization.steps[] | keys' trace.json # 前処理(サブクエリ変換など)はこちら jq '.steps[] | select(.join_preparation) | .join_preparation.steps[] | keys' trace.json select() を挟んでいるのは、 .steps[] が上記三つの要素を全部回すためです。そのまま .join_optimization.steps[] と書くと、 join_preparation の要素で null に当たって Cannot iterate over null で止まります。 jq '.steps[].join_optimization?.steps[]? | keys' と ? を付ける書き方でも同じです。 これでどんなステップが並んでいるかがわかります。その中で見るのは、実質この四つでした。 transformations_to_nested_joins … サブクエリがどう変換されたか(これだけ join_preparation 配下) rows_estimation … 各テーブルで range が使えるかの判定 considered_execution_plans … 候補とそのコスト。一番ボリュームがある attaching_conditions_to_tables … 条件の割り当て。ここで計画が書き換わることがある transformations_to_nested_joins :サブクエリがどう変換されたか join_preparation 配下にあり、 IN や EXISTS のサブクエリが semijoin / antijoin にフラット化されたかどうかが記録されています。フラット化されるとサブクエリという単位は消え、通常の結合として最適化されるので、計画が大きく変わる出発点になります。 今回の Trace はこうなっていました。 { " select# ": 5 ," from ":" IN (SELECT) "," to ":" semijoin "," chosen ": true } { " select# ": 4 ," from ":" IN (SELECT) "," to ":" antijoin "," chosen ": true } 変換自体は起きていました。ただ、旧バージョンの EXPLAIN JSON にも is_not_null_compl が出ていたので、旧版でも同じように変換されていたと見てよさそうです(後に AWS サポートも両バージョンで同一と確認)。 semijoin=off で直ったので真っ先に疑っていた「フラット化そのものが犯人」という仮説は、ここで消えました。 rows_estimation :range が使えるかどうか テーブルごとに、range スキャンが使えるかを評価した結果が入っています。 " range_analysis ": { " table_scan ": { " rows ": 50628522 , " cost ": 5887250 } , " potential_range_indexes ": [ { " index ": " PRIMARY ", " usable ": false , " cause ": " not_applicable " } , { " index ": " idx_events_start_at ", " usable ": true , " key_parts ": [ " start_at ", " id " ] } ] , " analyzing_range_alternatives ": { " range_scan_alternatives ": [ { " index ": " idx_events_start_at ", " rows ": 89078 , " cost ": 10450 , " chosen ": true } ] } } ありがたいのは、 usable: false に多くの場合 cause が付いてくるところです。「なぜ候補から外れたか」がここで一段階わかります。 今回の events を見ると、 idx_events_start_at は usable: true 。range の見積もりも 89,078 行・cost 10,450 と妙なところはなく、この時点ではインデックスがちゃんと候補に挙がっていました。 もう一つ大事なのは、ここで chosen: true (そのステップの比較でベストだと判断された、という印)になっていても最終的に使われないケースがあることです。今回はまさにそれでした。 considered_execution_plans :候補の比較 一番量が多いところです。結合順序の組み合わせごとに、各テーブルの best_access_path が延々と記録されています。 { " plan_prefix ": [ " users ", " user_events " ] , " table ": " events ", " best_access_path ": { " considered_access_paths ": [ { " access_type ": " ref ", " index ": " PRIMARY ", " usable ": false , " chosen ": false } , { " access_type ": " range ", " range_details ": { " used_index ": " idx_events_start_at " } , " rows_to_scan ": 89078 , " resulting_rows ": 89078 , " cost ": 308026000 , " chosen ": true } ] } } 候補とコストが横に並ぶので、「どれがいくらで、なぜ選ばれたのか」が数字で見えます。今回の Trace では、この段階では range が chosen: true でした。つまりコスト計算の上ではインデックスを使うつもりだった、ということになります。 よく使う jq: # 特定テーブルのアクセスパス候補だけ jq '.. | objects | select(.table? == "events") | .best_access_path' trace.json # chosen になったものだけ拾う jq '.. | objects | select(.chosen? == true)' trace.json attaching_conditions_to_tables :ここで計画が書き換わる 名前からすると地味な後処理に見えるし、自分も最初は読み飛ばしていました。ところが今回の原因はここでした。 見るべきは rechecking_index_usage です。「条件を割り当て直したので、インデックスの使い方をもう一度検討する」という処理で、結合の内側に回ったテーブルについてはここで range を組み直します。今回の Trace はこうでした。 " rechecking_index_usage ": { " recheck_reason ": " not_first_table ", " range_analysis ": { " table_scan ": { " rows ": 50628522 , " cost ": 5887250 } , " best_covering_index_scan ": { " index ": " idx_events_start_at ", " cost ": 5143080 , " chosen ": true } , " setup_range_conditions ": [] } } setup_range_conditions が空になっています。組み直すための BETWEEN が渡ってきていません。range アクセスは「どこからどこまで読むか」という範囲条件とセットでしか成立しないので、条件がなければ range は候補として再構成されません。残るのは table_scan とカバリングインデックスの全スキャンだけで、安いほう(cost 5,143,080)が選ばれる。つまり、 range が負けたのではなく、range が候補として作られなかった わけです。 これで、 possible_keys にインデックスがあるのに key: NULL になる現象も、EXPLAIN ANALYZE で見えていた「テーブルスキャン経路だけ選択率が効かず約 971 倍ズレる」現象も説明がつきました。オプティマイザはインデックスを使うつもりでコストを計算していて、最後の工程でその手段を失っていただけでした。 AWS サポートに投げたら、バグだった 「コスト計算で選ばれた range が確定処理で消えている」というのは、どう考えても正常な最適化には見えません。Trace の該当箇所を添えて AWS サポートに問い合わせたところ、実体化経路については MySQL の既知バグに該当することが確認できました。 Bug #117770 : rechecking_index_usage causes the materialized table to be generated using table scan in Materialize semijoin strategy(問い合わせ時点で Status: Verified、修正バージョンなし) フラット化(FirstMatch)経路のほうは該当する公開バグ報告が見つかっていないものの、同じ機構で range が失われることを再現確認した、とのことでした。 回避策としては、対象クエリ 1 文に限定して、オプティマイザヒントで events に使うインデックスを明示する方針にしました。ステージングで EXPLAIN を取り、 events が idx_events_start_at の range に戻ることを確認してから入れています。 SELECT /*+ INDEX(events idx_events_start_at) */ users.id FROM users ... 振り返って この件で Trace が役に立ったのは、原因を具体的に書けるようになったからだと思っています。 EXPLAIN 系だけだと「変な計画が選ばれる」までしか言えませんが、Trace を読んだことで「確定処理の再チェックで range が捨てられている」と書けるようになりました。おかげで、最適化の結果ではなくバグではないかと疑えたし、問い合わせるときも該当箇所を貼るだけで話が通じました。 ここで Trace を思い出せなかったら、「アップグレードしたら遅くなったので戻します」で終わっていたと思います。 まとめ Optimizer Trace は毎回出すものではないです。順番としては、 EXPLAIN で変な計画を見つける EXPLAIN ANALYZE で見積もりのズレ方を見て、統計情報が原因かを見極める それでも「なぜこの計画なんだ」が説明できないとき、 Trace を開く EXPLAIN でわかるのは何が起きたかで、Trace でわかるのはなぜ起きたかです。 Trace を取るのに必要なのは SET SESSION optimizer_trace='enabled=on' の一行だけです。使う頻度は低いですが、詰まったときの選択肢として覚えておくと便利だと思います。
こんにちは!このたびタイミーは、9/11(金)開催のGo Conference 2026 にGoルドスポンサーとして協賛し、ブースを出展いたしました。 gocon.jp 素敵な場をつくってくださった運営のみなさま、そしてブースにお越しいただいたみなさま、ありがとうございました! Closingでの発表によると、参加者は会場とオンライン合わせて700人を超えたそうです。その内、300人以上の方がタイミーのブースに立ち寄ってくださいました。 この記事では、タイミーのブース企画と、ブースでいただいた質問をご紹介します。執筆者はこちらの4名です。 Engineering Manager: 新谷( @euglena1215 ) Backend Engineer: 成田( @7riatsu ) Backend Engineer: 永田( nagataaaas · GitHub ) DevEnable室: shihorin( @shihorin_kjy ) 企画紹介 企画①タイミーでのGo活用紹介 株式会社タイミーは、「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービス「タイミー」を開発・運営しています。 バックエンドはRuby on Railsで主に開発していますが、認証基盤にはGoも活用しています。トータルプラットフォームを実現する構想の中で、Goの出番はこれから広がっていく可能性があります。 ブースでは、システム構成図やプロダクト戦略のイメージ図をご覧いただきながら、Go活用の現状と今後の可能性についてお話しました。 企画②Goの良いところを教えていただくアンケート ブースでは、アンケートへのご回答をタイミーでのお仕事に見立てた体験企画をご用意しました(実際の求人ではありません)。ご回答いただいたお礼にガチャを1回まわしていただき、ストレスボール、ぷくぷくシールなどのノベルティをお渡ししました。 アンケートの設問は「Goを選んでよかったこと、教えてください!」です。タイミーではこれからよりGoを活用していく可能性があります。そのため「Goエンジニアの先輩方からGoの良いところを教えていただきたい!」という意図でこの設問にしました。 結果はこちらです! 元々用意していた選択肢に加えて、「バージョンUPしやすい」「Gopherがかわいい」という選択肢も途中で追加していただきました。 良いところ 票数 チームで読みやすく、保守しやすい 134 ビルド・デプロイがしやすい 94 並行処理を扱いやすい 69 ツールがそろっていて開発しやすい 49 性能を出しやすい、省リソース 43 Gopherがかわいい 35 まだ使っていない 11 バージョンUPしやすい 10 並行処理の扱いやすさや省リソースであることよりも、ビルド・デプロイの方がかなり票を集めていることに驚きました。 1エンジニアとしての言語の扱いやすさにとどまらず、プロダクトの一部として見たときの取り回しの良さが評価されているのかな?と考察しています。 企画③Reading List タイミーのプロダクト開発をもっと知りたい方向けに、「Reading List(おすすめ記事リスト)」をQRコードで掲示しました。その中から、特に反響のあった記事を紹介します。 タイミーの1,200万超ユーザーを支える認証基盤を Go と Ory Hydra で作っている話 バックエンド開発Handbookを届けるために ― AI時代の知の高速道路を敷く - Timee Product Team Blog プロダクト開発に関する記事の公開情報や登壇情報は、Xでも発信しています。ぜひご覧ください! Timee Engineering / タイミーエンジニアリング 公式 (@TimeeDev) / X ブースで特にいただいた質問 + 回答 特に多くいただいた質問と、質問への回答をご紹介します。回答の中で出てくる用語は、以下の構成図でご確認ください。 再掲:ブースでお話ししながらご覧いただいた、システム構成図 Q1.「タイミーはGoを使っているのですか?Rubyの印象でした」 A.Goも使っています!基幹API(スポットワークのタイミー)はRubyで動いているのですが、ワーカー(toCユーザー)向けの認証基盤をGoで開発しています。 これまでバックエンドはRubyだけだったのですが、ここからバックエンドの言語のバリエーションを増やすことを前向きに検討していて、その中にGoが含まれています。 Q2.「タイミーの認証基盤はどういうもの?何のために作っている?」 A.複数事業展開を見据え、特定プロダクトに依存しない共通アカウント基盤が必要になったためです。 もともと認証機能は基幹 API(スポットワークのタイミー) に同居していましたが、それを最近切り出しました。スポットワークに隣接する事業を始めるために、同じアカウントでのログインや、データ連携ができるアカウント基盤が必要になったことがきっかけです。既存の認証機能は基幹APIの一部として実装されており、そのままでは共通の認証機能として利用できませんでした。そこで、独立したIdPとしてゼロから構築し、段階的に移行する判断をしました。 再掲:ブースでお話ししながらご覧いただいた、プロダクト戦略のイメージ図 Q3.「認証基盤だけGoなのはなぜ?なぜ認証基盤だけRails以外の技術選定をしたの?」 A.認証機能を既存のRailsアプリから独立したサービスとして切り出すにあたり、言語仕様がシンプルで、標準ライブラリや機能を絞った軽量なライブラリを組み合わせて必要な機能だけを実装しやすいGoを採用しました。型安全性や保守性を確保しやすく、長期運用する基盤との相性がよいことも理由の一つです。 また、OAuth2/OIDCに採用したOry HydraもGo製で、必要に応じて内部実装をコードリーディングで理解しやすいこともメリットでした。 Q4.「今後もGoを選定する予定はありますか?」 A.タイミーでは今後新規事業にも取り組んでいくことを検討しています。その際の技術選定の有力な選択肢としてGoを考えています。(検討・検証段階なので確定ではありません) 技術選定の参考にしたいという意図もあり、今回のブース企画を実施させていただきました。 Q5.「既存RailsアプリはGoにリプレイスするのですか?」 A.言葉通りのリプレイスは考えていません。 先述した認証基盤と同様、複数事業展開に伴い、いくつかの事業で同様の基盤が欲しくなることがあると思います。そういったユースケースが生まれた際に、基幹APIから共通基盤を切り出し移行する可能性は十分にあります。共通基盤の技術選定で、Goは候補のひとつになると考えています。その上で、Goが選ばれるのか、Rubyが選ばれるのか、はたまた別の言語が選ばれるのかはプロジェクト次第になりそうです。 Q6.「整合性チェッカーは何のためにあるのか?」 A.新たに構築した認証基盤(IdP)への移行途中である現在は、既存DBとIdP DBの双方へ書き込むダブルライト構成です。2つのDBでデータがずれる可能性があるため、整合性チェッカーが定期的に両DBを比較し、不整合を検知・通知しています。これは移行期間中の一時的な安全装置であり、読み書きをIdP DBへ完全に切り替え、ダブルライトを終了した後は廃止予定です。 おわりに ここまで読んでくださり、ありがとうございました!これからもGoコミュニティを盛り上げていけるよう、継続して関わっていけると嬉しいです。 最後になりますが、タイミーでは一緒にはたらく仲間を募集しています。ご興味をお持ちいただけましたら、ぜひカジュアル面談にお申し込みください! プロダクト組織の概要や募集職種 シニアバックエンドエンジニア(プラットフォーム領域)採用情報 カジュアル面談のお申し込みはこちら
はじめに タイミーでは、世界中で開催される技術系カンファレンスの参加を支援する「Kaigi Pass」という制度があります。今回は、2026年9月5日(土)に中野セントラルパークカンファレンスにて行われた、Product Engineering Conference 2026に5名が現地参加してきました。 本レポートでは、参加したエンジニアが注目したセッションごとに、参加者自身の視点で学びや気づきをまとめました。読者の皆様にとって、今後の学びの参考になれば幸いです。 営業・CSを「開発の当事者」にする — 少人数チームで複数プロダクトを動かすDev<>Ops連携の方法 speakerdeck.com こちらのセッションは、estieの山本龍平さん・北村大助さんによる、少人数で複数プロダクトを動かすなかで、営業・CSがプロダクト開発の当事者になる取り組みについての共同発表でした。 Ops(営業やCS)は、要望をDev(開発)に渡して実装を待たない。AIでモックを作り顧客に当て、課題と解決策の仮説を磨き込んでからDevに渡す。現場に近いOpsが仮説検証を一定担うことで、質と速度を上げる、というのが主題でした。 印象的だったのは、Opsの北村さんの話です。小さく早く検証することを前提に、動くものを用意して顧客とのコミュニケーションコストを下げ、検証精度を上げていることでした。 仮説検証はDevの役割だと思い込んでいた私にとって、Opsが回せるよう検証環境で支えるという体制は、前提を覆される話でした。また、仮説検証に対する「小さく、早く」といった姿勢をどのようにOpsに共有しているのかという疑問が浮かびました。 そういったプロダクト開発に対する理解向上の取り組みとして タッグ開発 Night が紹介されていました。社内のハッカソンに近いイベントで、Opsが顧客要望をもとにAIでプルリクエスト(PR)を出す。仮説を試す部分をDevと進める場でした。 このほか、 Preview環境でPRごとに専用DBを用意する取り組み など、Opsの越境を助ける具体事例も紹介されていました。自社でも、まずは「動くものを顧客に当ててからDevに渡す」ところから試せそうです。自身の役割を越境するだけでなく、担当領域への越境を助ける環境づくりまで含めて、組織としてプロダクト開発を進める視点を得た発表でした。 津守( @ytsumori59 ) エンジニアリングは、どこまで拡大解釈できるか 心技体をつないだままで事業責任者になった話 speakerdeck.com 柳川さんのセッションでは、エンジニアリングを「責任を引き受ける営み」として捉え、技術の実装にとどまらず、プロダクトのアウトカムまで責任範囲を広げる考え方が語られていました。 責任範囲を広げる鍵として示されたのが、責任を負う実感(心)、スキル(技)、現場感覚(体)です。3要素を切り離さずに育てることが重要だと、私は受け取りました。 AIはスキルを伸ばす助けになります。一方、責任を引き受ける実感は、組織環境にも左右されます。そのため、個人プロダクトのリリースなど、社外で自ら意思決定と結果を引き受ける経験も有用だと提示されていました。 技術や知識の活用をAIが支援する範囲は、今後さらに広がっていきます。だからこそ、アウトカムに責任を持つ姿勢の重要性は増すのではないかと感じました。 私は、「プロダクトエンジニア」という言葉には、アウトカムへコミットする姿勢が含まれると考えています。その延長線上に事業責任者という役割がある、という捉え方には納得感がありました。 この話を聞き、仕事で大切にしている「ジブンゴト」という言葉を思い出しました。物事を自分の責任として捉え、行動する姿勢です。今回のテーマにも通じると感じました。 発表では、「エンジニアは元来ラストマンである」という問いかけもありました。ここでいう「ラストマン」とは、成果物の最終責任を引き受ける立場を指します。また、組織の中に暗黙的な親子関係が生まれる、という経営学的な視点も印象に残りました。 AIが成果物の生成を支援する範囲が広がるほど、最終的な責任を誰が担うのかを意識する必要があります。自分はどこまで責任を引き受けるのか。改めて考える機会になりました。 また、また、柳川さんの発表そのものも素晴らしいものでした。「責任」を起点に話を展開し、帰納的にセッションのテーマへ収束させる構成や、洗練された言葉選びが強く印象に残りました。 これまで聴講したセッションの中でも、特に心を動かされました。エンジニアとして、職業人として、こんなふうに生きたいと思わせてくれるセッションでした。 志賀( @akitoshiga ) その機能が使われないのは、業務の捉え方を間違えたから?─プロダクトエンジニアのメタモデル設計論 特に印象に残ったのは、機能をリリースするというアウトプットがあっても、顧客の業務が良くなるというアウトカムにはつながらないことがある、という話です。その原因が機能の不足ではなく、そもそもの業務の捉え方にあったという点が、日々の開発を振り返るきっかけになりました。 このセッションで紹介されたのが、業務をどのような概念や関係性で捉えるかという「メタモデル」の考え方です。その土台には、一次情報から業務の流れを理解することがあります。データがどう移動するかに加えて、誰が何を判断し、どのように仕事を進めているのかを捉える。現場を観察し、可能であれば体験し、専門家との対話で判断の背景を確かめる。業務モデルは会議室の議論だけでは完成しない、という話が心に残っています。 業務の中心となる概念を見極める観点も参考になりました。例えば契約であれば、さまざまな処理の軸に、顧客と交わした約束があります。個々の処理やデータ項目を整理するだけでなく、それらが何を軸につながっているのかを考えることが、モデルの土台になるのだと受け取りました。 また、良い業務モデルは、顧客が業務の進め方を組み立て、運用する負担を吸収するという話もありました。柔軟な機能を用意しても、その組み合わせや使い分けを顧客が一から考える必要があれば、負担は残ります。業務理解をモデルに反映することは、コードの構造だけでなく、顧客がどれだけ無理なく使えるかにも関わるのだと思いました。 ここからは、タイミーの身近な機能を題材に、この考え方を自分なりに当てはめてみます。 例えば求人を公開する際には、募集人数が定員に達していない場合に、設定に応じて公開範囲を自動で広げる仕組みがあります。詳しくは、 求人の公開範囲 についてをご覧ください。 この仕組みを単なる機能拡張ではなく「業務の流れ」として捉え直してみます。すると、応募状況や残り時間を踏まえて「どのタイミングで求人を届ける相手を広げるか」という事業者の判断を支えていることが見えてきます。 言われてみれば当然のことですが、日々の開発に追われているとつい見落としがちな視点です。機能が裏で支えている判断や作業にまで解像度を戻してこそ、本質的な設計や検証ができるのだと改めてハッとさせられました。  効果を確かめる際も、自動で公開範囲が広がったかというシステム挙動だけでなく、事業者様の手間が減り必要な人数の確保につながったかを見る必要があると思います。同時に、ワーカーさんにとっても希望や条件に合う仕事との出会いにつながったかも重要です。「募集が埋まる」という数値だけでなく、その先の就業が双方の期待に沿う体験になったかまで視野を広げることが、まさにセッションで聞いた「業務を捉える」ということなのだと感じました。今後の自分の開発でも、この視点を忘れずに向き合っていきたいです。 細野 越境負債:称賛されるほど、負債は返済されない 「越境をしても、越境が必要になってしまう状況そのものは越境では改善されない」という内容。越境が越えている境界を組織のそれであるとし、プロダクトの価値最大化のために越境は必要だが、一方でそういう境界を最適化する手法として組織再編を挙げていた。つまり、プロダクト価値の創出のために越境が必要だという状況は、組織の境界が最適でないということを意味する。もちろん越境は組織再編行為ではないため、最適でない状況そのものは、越境では改善されない。 たしかにそういう状況ならそう言えるかもしれない。組織の境界を越える行為を越境と呼ぶならば、組織再編が根本解決手段の一つであり得る。またそれを役割の境界であるとするなら、職務記述やロールの定義を更新すべきなのかもしれない。 ただどうしても、境界があれば越境が可能になる。境界を意識させないために私達は、これまでにも「何でも屋」、「遊撃部隊」、「フルスタックエンジニア」、「FDE」などを生み出してきた。実際のところ何でもできる人の手が空いているのなら何でも幅広く任せるほうが、少なくとも短期的なプロダクト価値の最大化にとっては最適だと言える。境界がある限り、越えたくなる。そして価値を創出している限り、越境は称賛されなければならない。 個人的には、どこかに線を引く限り越境はなくならないと思うし、それは称賛されるべきだとも思う。 口藏( @mizunokura ) おわりに Product Engineering Conferenceは初開催ながら、多くの参加者が集まり、皆さんのプロダクトエンジニアリングに対する熱量を感じました。 各セッションを通じて、プロダクトエンジニアリングについて改めて考える機会になりました。今回得た学びはタイミーでのプロダクト開発にも活かしていきたいと思います。 次回の開催については未定とのことですが、開催される際はぜひ次回も参加したいと思います。
こんにちは。タイミーでプロダクトエンジニアをしている津守です。 普段は、エンジニアとして機能の開発をメインの仕事としていますが、最近は営業チームの商談に同行する機会が増えてきました。そこで突きつけられたのが、タイトルの問いです。 自分が作った機能を、自分で売れるか。 機能は作れる。仕様も説明できる。でも、顧客を前にして「これはあなたの課題をこう解決します」と語り、納得してもらえるか。やってみると想像以上に難しく、同時に開発者として得るものが大きい体験でした。 この記事では、商談同行を重ねる中で見えてきたことと、そこから営業メンバーとの間に育っていったコミュニケーションパスについてまとめます。9月5日開催の Product Engineering Conference 2026 (以下、PdEConf 2026)が掲げる「職能の壁を越え、プロダクト価値を最大化させる」というテーマの、ひとつの実践記録として読んでもらえると嬉しいです。 なぜ「自分で売ってみよう」と思ったのか きっかけは、チームで開発した機能がなかなか顧客に活用されない、という課題感でした。 タイミーでは、営業が顧客にしっかり伴走しながらプロダクト導入後もサポートするコンサルティングに近い構造です。機能案内なども都度丁寧に行う体制ではありますが、新しく作った機能の導入は思うように広がりませんでした。チームとしては必要とされる機能を開発している認識でしたが、それがなかなか使われない。なぜ使われないのかという疑問に加え、使われなければ当初予定していた検証も進みません。 どうすれば営業メンバーの理解を得て、機能を届けられるのか。スプリントレビューの運用を見直して情報伝達を改善し、営業行動ログから提案状況も追いました。しかし、解消には至りませんでした。 チーム内に閉じた取り組みでは答えが出ないので、まずは「自分が作った機能は本当に顧客に売れるのか」を現場で感じ取りたい。そう考えて営業メンバーに相談し、商談への同行を始めました。 売ろうとすると、機能の強みと弱みがわかる 実際に商談の場で機能を説明して真っ先に気づいたのは、価値を感じてもらえる点と解決できていない点が同時に見えることでした。概要の説明だけで納得してもらえる部分がある一方で、答えに詰まる指摘や、対応しきれていない側面が容赦なく浮き彫りになります。 素朴な質問に十分答えきれず「将来的には対応していきます」としか言えない——。 答えに詰まる箇所は、そのまま営業メンバーが提案しづらい箇所です。 逆に、その場で納得してもらえた説明の切り口は、そのまま営業が使える強みになります。 ユーザーインタビューと違い、商談では自分が価値を証明する側に立ち、顧客も「使うか、使わないか」を判断します。そのため、「使うけれど、ここは不満」という改善要望と、「ここが解決されなければ使わない」という利用の障壁を切り分けられます。既存の運用や他の選択肢との比較も含め、機能がまだ使われていない理由を直接受け取れるのが、商談ならではです。 ユーザーインタビューと商談同行の違い ただし、注意しなければいけない点もあります。商談で聞いた一社・一人の意見は実在する課題ですが、優先すべき課題かは別問題です。 セールス主導開発の滑りやすい坂道 が指摘するように、商談同行は使い方を誤れば、一社の要望を過度に優先した開発の入り口にもなります。複数のユーザーに聞いて「重なる課題感」に手をつけながら、個別事例には深く踏み込む。この具体と抽象の行き来と、 定常的に顧客接点を持ち続ける ことが必要です。 「自分が売れた」で終わらせない ― プロダクトを売れる形で提供する 自分が同行した商談での提案がきっかけで売上に貢献できると、素直に嬉しいです。「自分で作った機能を自分で売れるんだ」という手応えは、ものづくりをする人間にとって強烈な体験ではないでしょうか。ただ、この体験は感動が大きい分、意識しないと「自分で売ること」自体を目指してしまい本来の役割を見失ってしまいます。 エンジニアが商談に同行する意義は、自分が売れるようになることではないと考えています。自分の中で売り方の検証を回し、そこでの学びをプロダクト開発に反映することが本筋です。そのうえで、刺さった提案の仕方や文脈を型化し、営業資料のひな形として渡していく。つまり、 プロダクトを「売れる形」にして提供すること です。改善した機能と、それを説明するための型をセットで渡せば、組織全体で価値を届ける力になります。 営業とともに開発する体制を築く 商談同行は、一度きりで終わらせず継続することで真価を発揮します。同行を重ね、商談後にはその場で営業メンバーと振り返り、機能の強みと弱みを一緒に整理して目線を揃えていく。これを繰り返すことで、営業メンバーとの間に「機能を届ける」ためのコミュニケーションパスができていきました。 一番大きな変化は、商談に関する相談をされるようになったことです。機能説明の商談を控えたメンバーから「この機能はどう勧めるのがいいか」と事前に相談が入る。商談を終えたメンバーから「この質問に上手く答えられなかった」と持ち込まれる。以前はプロダクトへのフィードバックの機会がほぼスプリントレビューだけでしたが、Slack で非同期にこうしたやり取りが行われるようになりました。これらは営業の困りごとであると同時に、機能に対するフィードバックそのものです。相談で出た論点は開発に反映されたり、営業資料のひな形に反映し共有知として残したりしています。 変化は開発チーム側にも起きました。チーム内で商談の内容を共有していくうちに、機能をどう売るかを主題として会話する機会が増え、メンバーの関心が自然と営業の動きへ向いていきました。営業メンバーの日報に開発メンバーがリアクションする光景は、今では当たり前です。 この双方向の関心が重なった結果としてスプリントレビューにも変化がありました。開発チームからの共有会に近かった場が、お互いが「この機能をどう提供するか」の視点で意見を交換し合い、どんなプロダクトにしていくべきかを議論する場になりました。 商談同行で生まれたコミュニケーションパスの変化 この関係性が当たり前になると、 作る前から「これは本当に届けられるのか」という問いに対して、解像度の高い状態で開発を進める環境 が生まれます。一度の商談で得られるのは顧客の課題ですが、続けることで得られるのは、課題が自然に集まってくる関係そのものです。 おわりに 「そのプロダクト、自分で売れるか?」 自分で売ろうとすれば機能の強みと弱みが同時に見え、その学びをプロダクトの価値に還元する。売れるかという問いへの答えを探す過程で、営業メンバーと「作る前から『届けられるか』を検証できる土壌」が育っていきました。 この取り組みは、自分の役職に囚われない動きも必要でしたが、それ以上に営業メンバーが日頃から築いてきた顧客との関係性があったから起こせた行動でもあります。良いことも悪いこともはっきりフィードバックしてもらえる顧客が多いのは、営業メンバーの日々の取り組みのおかげなので本当に感謝しています。 PdEConf 2026 に参加される方で、同じような取り組みをされている方、これから始めようとしている方がいれば、ぜひ会場でお話しできると嬉しいです。
こんにちは、タイミーでバックエンドエンジニアをしている志賀( @akitoshiga )です。 先日、複数の顧客企業にまたがる、複雑なデータ統合プロジェクトに取り組みました。 このプロジェクトには、次のような難しさがありました。 要件が曖昧で、影響範囲も見えづらい 統合パターンごとに状況や制約が異なり、考慮すべき条件が多い 関係者が多く、合意形成に時間がかかりやすい 事業上、変更しづらい期限がある 当時の体制では、バックエンド領域を主担当として動けるメンバーが限られていた このまま進めるだけでは、期限内の完了が難しい状況でした。 結論から言うと、次の3つを実践することで、期限内に完了できました。 AIを使ってコードと業務を調査し、自分で判断できるメンタルモデルを作った AI-DLCを使い、主担当領域の異なるメンバーも実装を進められる状態を作った ドキュメントを通じて論点を絞り、関係者の認知負荷を下げた 本記事では、この取り組みを紹介します。 プロジェクトの概要 タイミーでは、契約・請求・拠点・アカウント・アクセス権限など、複数の業務データが企業情報と結びついています。 データ統合は、単に参照先を変更するだけでは終わりません。 統合後にどのような状態であるべきかを定義し、その状態に合わせて関連データを整合させる必要があります。 今回のプロジェクトでは、統合パターンごとに移行範囲や制約が異なりました。 そのため、対象データごとに「何を移すのか」「何を残すのか」「どの状態を正とするのか」を判断する必要がありました。 複雑な統合パターンの存在 実案件を抽象化すると、次のような関係でした。 ある統合元は、すべての拠点を統合先へ移す 別の統合元は、一部の拠点だけを統合先へ移す さらに別の統合元は、別の企業へすべての拠点を移す この図は、実案件を説明用に抽象化・簡略化したモデルです。 ポイントは、同じ対象が、ある統合では移行元になり、別の統合では移行先にもなることです。 そのため、すべてのデータを一律に移すことはできません。 残すデータと移すデータを、対象範囲ごとに判定することが求められました。 また、別の統合によって入ってくるデータも考慮する必要がありました。 そのため、統合の実行順序にも依存関係が生まれました。 アクセス権限の組み合わせ爆発 顧客に紐づく各種データについても、場合分けが必要でした。 その一例が、管理画面アカウントとアクセス権限です。 アクセス権限には、全体に関わるものと、一部の範囲に関わるものがあります。 移行後も、本来アクセスできない範囲へ権限が広がってはいけません。 一方で、移行前に必要だった範囲へのアクセスを失わせることもできません。 この両立が、難しいポイントでした。 権限の種類、移行範囲、統合パターンの組み合わせによって、望ましい状態が変わるためです。 関連データごと種類ごとにこれらの判断が必要になる 他のデータも同様でした。 統合パターン、移行範囲、状態、関連データの制約が組み合わさり、判断すべき条件が増えていました。 自分は、関連するすべてのドメインに詳しいわけではありませんでした。 影響範囲を把握し、統合後の状態を決めるには、まず調査が必要でした。 どう進めたか AIを使って調査し、メンタルモデルを作る まず、AIを使ってコードを解析しました。 そのうえで、実際に画面を操作し、コード・画面・データの対応関係を確認しました。 自分の中でメンタルモデルを作れるまで、時間をかけて調査・検証しました。 前例がなく、自分たちで統合後の正しい状態を定義する必要があったためです。 ここで言うメンタルモデルとは、次のようなものです。 どのデータが、どの業務概念を表しているか どの操作で、どのデータが変わるか 統合後に、何が変わってはいけないか 異常系や例外パターンで、どこに影響が出るか この理解をもとに、PdMや関係者と相談しながら統合仕様を決めていきました。 短納期とチーム構成を踏まえてAI-DLCを採用する データ統合には、バックエンドのフレームワーク上で動作するスクリプトの実装が必要でした。 一方で、変更しづらい期限がありました。 自分だけで進めるには、時間的な制約が大きい状況でした。 そこで、主担当領域の異なるメンバーと一緒に進めるため、AI-DLCを採用しました。 AI-DLC(AI-Driven Development Life Cycle) とは、AWSが提唱するAIを中心に据えた開発手法です。 要件整理、計画、設計、実装、テスト、運用まで、開発ライフサイクル全体にAIを組み込んでプロジェクトを進めます。 タイミーでは、以前AI-DLCを業務で活用するための合宿を実施しました。 3日間のUnicorn Gymが1ヶ月で組織を変えた —— データで見るAI-DLC導入の波及効果 - Timee Product Team Blog 今回は詳細設計までを、複数人でAIの出力を確認するモブワークで進めました。 実装に必要な前提と完了条件を共有した後は、主担当領域の異なるメンバーにも、非同期で実装を進めてもらいました。 AIを使うことで、バックエンドの文法や実装パターンの理解を補助できます。 ただし、仕様の正しさまでAIに任せることはできません。 そのため、前提・制約・完了条件を人間が明確にしました。 そのうえで、AIの出力をレビューできる状態を作ることを重視しました。 合意のための認知負荷を下げる このプロジェクトは影響範囲が広く、多様な関係者が関わっていました。 全員に統合仕様のすべてを理解してもらおうとすると、合意までに時間がかかります。 また、関係者がそれぞれ判断すべきポイントも異なります。 そこで、意思決定のためのADR(Architecture Decision Record)では、必要な論点を絞りました。 レビュー観点、選択肢、推奨方針、影響、リスクを整理しました。 関係者が、それぞれの判断範囲に集中できるようにするためです。 仕様を説明・相談する際にも、認知負荷を下げるためのドキュメントを用意しました。 具体的な手法はここでは割愛しますが、以下の書籍を読んだ経験が自分の中で大きな礎となっています。 『ノンデザイナーズ・デザインブック』 『プログラマー脳』 『実装パターン』 その取り組みの結果、短いリードタイムで方針への合意を得られました。 終盤の考慮漏れに向き合う 仕様が複雑だったため、終盤のテスト工程で考慮漏れが見つかりました。 また、終盤で体制変更があり、権限に関する仕様判断を自分が引き取る必要がありました。 ただし、序盤にメンタルモデルを作っていたため、どこを確認し、誰に相談し、どの制約を守るべきかを自分で判断できる状態になっていました。 追加対応は発生しました。 それでも、影響範囲を絞り、対応順を決め、関係者と優先度を確認し、期限内に実装とテストを終えることができました。 まとめ 約1か月半という短い期間かつ限られた体制で、前例のない複雑なデータ統合プロジェクトに取り組みました。 この状況に対して、次の3つを実践しました。 AIを使ってコードと業務を調査し、自分で判断できるメンタルモデルを作った AI-DLCを使い、主担当領域の異なるメンバーも実装を進められる状態を作った ドキュメントを通じて論点を絞り、関係者の認知負荷と合意形成のリードタイムを下げた 結果として、複数のメンバーで実装を分担し、期限内に実装とテストを完了できました。 一方で、AI-DLCを使っても、仕様の正しさまで保証されるわけではありません。 前提となるドメイン理解と、人間による意思決定が重要です。 AIは、単なるコード生成ツールではありません。 理解・合意・協働を加速する存在として開発プロセスに組み込むこと。 それが、AI時代のプロダクトエンジニアリングに必要な姿勢だと感じました。
iOSDC Japan 2026 にシルバースポンサーとして協賛します こんにちは!DevEnable室のshihorinです。 このたびタイミーは、 iOSDC Japan 2026 にシルバースポンサーとして協賛いたします 。 開催に向けて準備を進めてくださっている運営のみなさま、ありがとうございます! iOSDC Japan は、iOS関連技術をコアテーマにした技術者のためのカンファレンスです。 詳細は 公式サイト をご覧ください。 開催日程:2026年9月11日(金)〜9月13日(日) 会場:有明セントラルタワーホール&カンファレンス/オンライン iosdc.jp 「iOSDCチャレンジ」に参加しています 今回の開催期間中、「iOSDCチャレンジ」企画が実施されます。 会場やWeb上で「iOSDCトークン」を探していただき、見つけた数に応じて抽選券を獲得。 会場の抽選カウンターでノベルティが当たる抽選に参加できる企画です。 タイミーもこの企画に参加しています。 この記事の最後にもiOSDCチャレンジトークンを掲載していますので、ぜひご覧ください。 タイミーとiOS開発について タイミーは、「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービス「タイミー」を開発・運営しています。 ワーカーは働きたい仕事を選ぶだけで、履歴書・面接なしですぐに働くことができ、勤務後すぐにお金を受け取ることができます。 ワーカー向けのアプリはiOS・Androidのネイティブアプリとして開発しています。iOSエンジニアは日々の機能開発だけでなく、テスト戦略やCI/CD、開発生産性の改善にも取り組んでいます。 1,420万人のワーカー(※2026年4月末時点)に登録いただいているサービスのアプリを、限られた人数でどう安定して速く届けるか——こうした課題に向き合えるのも、iOS開発のおもしろさのひとつです。 iOSDC参加者のみなさま向け:おすすめ記事・資料 タイミーのiOSエンジニアが公開している中から、iOSDCに参加されるみなさまに楽しんでいただけそうな記事・登壇資料を紹介します。 ① 半自動E2Eで手っ取り早くリグレッションテストを効率化しよう 昨年のiOSDC Japanでの登壇資料と動画です。「完全自動化」の前に挑戦した、半自動E2Eの技術選定やプロセスを紹介しています。 ② iOSエンジニアがAIと協調してAndroidプロジェクトにコミットしてみた iOSエンジニアがAIを相棒に、あえてAndroidプロジェクトへコミットしてみた記録です。プラットフォームの壁をAIでどこまで越えられるのか、勘所やぶつかった壁が書かれています。 おわりに ここまで読んでくださり、ありがとうございました! iOSDCチャレンジトークンはこちらです👇 #iosdc2026_timee そしてタイミーでは、一緒にはたらく仲間を募集しています。 プロダクト組織の情報や募集職種は、以下のページからご覧ください。 product-recruit.timee.co.jp ご興味をお持ちいただけましたら、ぜひカジュアル面談にお申し込みください。
こんにちは。タイミーでプラットフォームエンジニアをしている菅原です。普段はAWSを中心に、インフラの設計と運用に取り組んでいます。 今回は、ランサムウェア対策として構築したバックアップと、そのバックアップが正しく機能していることを継続的に監視する仕組みを紹介します。 ランサムウェア攻撃は、データを暗号化したり盗んだりするだけでは終わりません。復旧そのものを妨害するために、既存のバックアップまで削除や改ざんの対象にするケースが増えています。この脅威に備え、私たちはAmazon Auroraクラスタのバックアップを、クロスアカウントの論理エアギャップVault(Logically Air-Gapped Vault、以下LAGV)へ継続的にコピーする仕組みを作りました。 ただ、実際に手間がかかったのはバックアップの経路を組むことよりも、それが本当に機能し続けているかを検証することでした。設定上は正しく組まれているように見えても、タグの付け忘れやジョブの失敗、Vault Policyの不備などで、気づかないうちにコピーが止まっていることがあります。 背景 クロスアカウントの論理エアギャップバックアップ 想定する脅威とアカウント分離 前提となる脅威シナリオはこうです。攻撃者はワークロードのAWSアカウントで管理者権限を奪うと、復旧を妨害するためにAWS Backupなどのバックアップ設定を無効化・改ざんし、IAMロールやユーザーの権限も書き換えて正規の管理者による復旧作業を封じます。そのうえでデータを外部に持ち出したのち、DB本体と既存バックアップ(スナップショット)を削除する、というのが想定する動きです。 この脅威に対応するため、バックアップに関わるアカウントは役割ごとに分離しています。なお、このアカウント分離は、AWS Storage Blog「Building cyber resiliency with AWS Backup logically air-gapped vault」で紹介されているData Bunker、Forensics、Recovery Accountの分離パターンを参考にしています。 アカウント種別 役割 アクセス Backup Admin AWS Backupのポリシー管理、Organization全体のバックアップ状況の監視 管理者による日常的なアクセスを許可(ワークロードアカウントと同等のセキュリティレベル) Data Bunker 論理エアギャップVaultを保有し、バックアップデータを保管 管理者であっても日常的なアクセスは禁止 Forensics Data Bunkerに保管されたバックアップを定期的にリストアし、復旧の整合性をテスト 管理者であっても日常的なアクセスは禁止 Workload Recovery インシデント時の復旧先となるクリーンな環境 インシデント発生時のみ、Data Bunkerから共有された復旧ポイントを参照 バックアップの実体を持つData Bunkerアカウントには、平常時は誰もログインさせず、ワークロードアカウントとのネットワーク疎通もありません。加えてAWS Backupの論理エアギャップVaultは仕様上コンプライアンスモードが強制され、ルートユーザーであっても保持期間中は復旧ポイントもVault自体も削除できません。この多層の防御によって、攻撃者がワークロードアカウントの管理者権限を握ってもバックアップを壊すことが困難な構成になっています。 アカウント構成 暗号化キーの種類とコピー経路 論理エアギャップVaultにコピーできるリソースには仕様上の制約があります。AWS Backupのドキュメントによれば、「フル AWS Backup 管理」をサポートしていないリソースタイプは、AWS管理のKMSキーで暗号化されている場合、論理エアギャップVaultへのコピーがサポートされません。カスタマーマネージドキーで暗号化されているか、暗号化されていないことが条件になります(参考: Copying backups to a logically air-gapped vault )。Auroraはこのフル AWS Backup 管理非サポートのリソースタイプに該当するため、サービスマネージドキーのままでは直接コピーできません。また、Auroraでは既存クラスタの暗号化キーを直接変更できず、異なるキーを使用する場合は新しいクラスタへの移行が必要です。 そこで、既存クラスタの構成変更を前提とせず、キー種別にかかわらずバックアップを論理エアギャップVaultへ保管できるよう、2つのコピー経路を用意しました。カスタマーマネージドキーであれば、定期実行されるバックアッププランでData Bunkerアカウントの論理エアギャップVaultへ直接コピーします。サービスマネージドキーの場合は、まず中間Vault(カスタマーマネージドキーを設定した通常のVault)にコピーして暗号化キーを変換し、そこから論理エアギャップVaultへコピーする形です。中間Vaultへのコピー完了イベントをトリガーに、Step Functionsが論理エアギャップVaultへのコピーを実行します。なお、この中間Vaultを介して暗号化キーを変換し、クロスアカウントコピーを行う構成は、AWS Storage Blog「Protecting encrypted Amazon RDS instances with cross-account and cross-Region backups」で紹介されている方式を参考にしています。 サービスマネージドキーの場合のバックアップ経路 タグでバックアップ対象を制御する 各ワークロードアカウントに個別のバックアップ設定を持たせると設定漏れのリスクが上がるため、AWS Backupのバックアップポリシー機能でBackup Adminアカウントから組織標準のバックアッププランを配布しています。エンジニアがやることは、Auroraクラスタにバックアップ有効化と暗号化キー種別を示すタグを付けるだけです。ここでは例示名で記載します。 タグキー 設定値 説明 BackupEnabled true false 組織標準バックアップを有効化するか KeyType aws-managed customer-managed 前述のコピー経路を切り替えるためのキー種別 このオプトイン方式には、タグの付け忘れがそのままバックアップ対象外につながるという副作用があります。これを防ぐため、AWS Organizationsのタグポリシーで許可されない値を検出できるようにし、Terraform AWS Providerとも統合して、plan実行時に必須タグの欠落をエラーとして検出できるようにしました。 背景の説明はここまでにして、ここからが本題の監視です。 バックアップが取れているはずを信じない 上のアーキテクチャを組んだだけでは、気づかないうちにバックアップが取れなくなっているリスクはまだ残ります。タグの付け忘れや意図しないタグ変更でバックアップ対象から外れることもあれば、バックアップジョブ自体が失敗することもあります。中間Vaultから論理エアギャップVaultへのコピーが、Step Functions経由で失敗することもあります。また、Data Bunker側のVault Policyに不備があり、Organizationからの CopyIntoBackupVault が許可されていないケースもあります。 どれも、バックアッププランは設定されているのに論理エアギャップVaultには実際のデータが届いていない状態を引き起こします。設定が存在することではなく、コピーが実際に完了したという事実を継続的に検証する必要があります。 なぜAWS標準のコントロールではなく自作したのか 論理エアギャップVaultにリソースが入っているかを確認する仕組みは、実はAWSの標準機能にも存在します。AWS Backup Audit Managerのフレームワークで「リソースは論理的に隔離された保管庫の中にある」というコントロールを有効にすると、内部的にAWS ConfigのマネージドルールAURORA-RESOURCES_IN_LOGICALLY_AIR_GAPPED_VAULTが作成されます。これにより自動でチェックできます。 まずはこの標準機能で要件を満たせないか検討し、クロスアカウントのコピー構成でも正しく評価できるかをAWSサポートに問い合わせましたが、クロスアカウントで復旧ポイントをコピーした場合、現時点ではこのコントロールではチェックできないことがわかりました。 つまりこのマネージドルールは、同一アカウント内でのVaultへのコピーは評価できます。一方で、私たちのようにコピー先の論理エアギャップVaultが別アカウント(Data Bunker)にあるクロスアカウント構成では、正しくコピーが完了していても常に非準拠として扱われます。責務分離のためにアカウントを分けたこと自体が、標準コントロールの前提と噛み合いませんでした。 この制約で標準のマネージドルールを使う選択肢は消え、クロスアカウントのコピージョブの実行結果を直接確認するカスタムルールを自作する方針に切り替えました。 監視の仕組み AWS Configカスタムルール AWS ConfigのカスタムルールとしてLambda関数を実装し、各アカウント内のAuroraクラスタについて、一定期間内に論理エアギャップVaultへのコピーが完了した実績があるかを評価しています。評価結果は3種類で、期間内にLAGVへのコピー完了を確認できれば COMPLIANT 、確認できなければ NON_COMPLIANT 、対象のAuroraクラスタが存在しなければ NOT_APPLICABLE になります。 Lambdaの処理は大きく4ステップです。 AWS Configから定期評価イベントを受け取り、ルールパラメータを読み取ります。 Auroraクラスタ一覧を取得し、必要に応じてタグでフィルタをかけます。 各クラスタについて、評価対象期間内にLAGVへのコピー完了実績があるかを確認します。 最後に結果をAWS Configに報告します。 必要なIAM権限は、Auroraクラスタとバックアップジョブの参照、およびAWS Configへの評価結果登録に限定しています。 この監視をどう組織全体に展開するか この監視は1つのLambda関数とConfig Ruleで構成していますが、対象となる全ワークロードアカウントに1つずつデプロイして回るのは現実的ではありません。今後アカウントが増えるたびに設定を追加する運用も避けたいところです。 そこでCloudFormation StackSetsを使い、対象OU配下の全アカウントに自動で配布する構成にしました。 工夫したところは3つあります。 1つ目はLambdaのデプロイ管理をCFn StackSets委任管理者アカウントに集約したことです。CFn StackSetsはOrganizationsの管理アカウントではなく委任管理者アカウントからスタックセットを管理できます。バックアップリソース自体はBackup AdminとData Bunkerのアカウントに閉じていますが、全アカウントにLambdaをばらまくという関心事は別の委任管理者に持たせ、責務を分けました。 2つ目はLambdaコードとインフラのリポジトリを分けつつ、S3のバージョンIDでつないだことです。Lambdaの実装は通常のアプリケーションと同じくアプリケーションリポジトリで管理し、GitHub ActionsからOIDCでAssumeRoleしてビルド成果物をS3にアップロードします。StackSetの定義はTerraformリポジトリ側で管理し、両者をつなぐためにTerraformからS3オブジェクトの version_id を参照して、それをStackSetのCloudFormationパラメータとして渡します。Lambdaコードの更新はS3へのアップロードだけで完結し、インフラ側は参照するバージョンを更新してapplyするだけで全アカウントへの再配布が終わります。アプリケーションのデプロイフローとインフラのデプロイフローを混ぜずに済む構成です。S3バケットはバージョニングを有効化したうえで、 noncurrent_version_expiration で古いバージョンを一定期間後に自動削除し、オブジェクトが無制限に溜まらないようにもしています。 3つ目はクロスアカウントの成果物配布をOrganization ID条件で絞ったことです。各メンバーアカウントのLambdaが実行時にS3から成果物を取得できるよう、バケットポリシーで組織内アカウントからの s3:GetObject を許可しています。 aws:PrincipalOrgID 条件を使い、組織に所属するプリンシパルだけにアクセスを絞ることで、意図しない外部アカウントからの参照を防いでいます。 検知後の運用フロー 監視の仕組みだけ作っても、検知後にどう動くかが決まっていなければ意味がありません。 NON_COMPLIANT が検出された場合の一次切り分け手順を運用ガイドラインとして整備しました。 日常的な状態確認はダッシュボードで行い、加えて全アカウントの評価結果はSecurity Hub CSPMを介して委任管理者アカウントに集約しています。このカスタムルールでの新規違反検出にフィルタする Security Hubのインサイト を作成することで、対応が必要な検出結果が残っているアカウントを一覧できるようにしています。これにより、個別のワークロードアカウントを一つずつ見て回らなくても、組織全体のバックアップの健全性を俯瞰できます。 制約と今後の展望 今の監視は、定期監査として組織全体の状態を継続的に確認する役割を担っています。 より即時性が必要な場面では、ジョブ状態変更イベントをEventBridgeで拾い、失敗時にアラートを上げる仕組みを別途組み合わせることを考えています。定期監査は取りこぼしなく全体を俯瞰する役割、EventBridge側は異常をすぐ知らせる役割と、分けて考えています。 おわりに クロスアカウントの論理エアギャップVaultは、アカウントを分けて壊せないバックアップを組むところまでは、比較的素直に設計できます。難しいのは、その仕組みが実際に動き続けていることをどう証明し続けるかです。今回のプロジェクトも、試行錯誤を重ねるなかであらためてそのことを実感しました。 参考文献 AWS Backupユーザーガイド, Copying backups to a logically air-gapped vault AWS Storage Blog, Building cyber resiliency with AWS Backup logically air-gapped vault AWS Storage Blog, Protecting encrypted Amazon RDS instances with cross-account and cross-Region backups
はじめに 2026年7月にタイミーへ入社した細野です。現在はバックエンドエンジニアとして、ワーカーさんと事業者様双方の体験をより良くするためのプロダクト開発に携わっています。 この記事では、私がタイミーに興味を持った背景と、入社後に感じていることを書きます。タイミーに少しでも興味を持っている方にとって、入社後のイメージを持つ材料になれば嬉しいです。 簡単な自己紹介 私はもともと、紳士服販売からキャリアをスタートしました。その後、プログラミング講師を経て、2021年からエンジニアとして働いています。 エンジニアとしては、スタートアップや事業会社でフロントエンドからインフラまで、幅広く開発に携わってきました。プレイングマネージャーとして開発推進や技術面のリードを担いながら、新入社員メンターや中途採用にも関わらせていただきました。 直近ではニフティ株式会社で、ポイントサービスのモダナイゼーションに取り組んでいました。特に、データベースの PostgreSQL 移行など、長く運用されてきたシステムを今後も継続的に改善できる状態に近づける取り組みに関わらせていただきました。 参考: ニフティ株式会社「 Oracle Database Enterprise Edition から Amazon Aurora PostgreSQL への移行によりメンテナンス時の対応コストを50%削減 」(出典:同社公開記事) タイミーに興味を持つまでの背景 私は、エンジニアリングを通じて、人の選択肢や可能性を広げることに貢献できる仕事に、強いやりがいを感じます。 人生の中で、仕事が占める時間はとても大きいものです。だからこそ、どんな仕事に向き合うかは、自分自身の幸福度にも大きく関わると考えています。 以前、「ジョブ・キャリア・コーリング」という考え方を知ったとき、自分は収入を得るためだけでも、キャリアを積み上げるためだけでもなく、自分なりに意味を感じられる仕事に向き合いたいのだと気づきました。 その後、プログラミング講師として受講生の方々に向き合う中で、人の選択肢を広げることに関わる仕事への思いはより強くなりました。さらに、教えるためにプログラミングを学び続けるうちに、その面白さに強く惹かれるようになり、エンジニアとしてプロダクトを通じて価値を届けたいと思うようになりました。 特に印象に残っているのは、自社サービスの開発で、事業部、ディレクター、CS、デザイナーなど多くのメンバーと一緒に、要求定義からリリース、リリース後の改善まで取り組んだ経験です。技術だけで完結するのではなく、ユーザー体験や事業成果に向き合いながら、チームでプロダクトを育てていくことに強いやりがいを感じました。 タイミーに興味を持ったきっかけ タイミーに興味を持ったきっかけは、以前から業務で参考にしていたテックブログや、求人媒体で見た募集情報でした。 タイミーが掲げる Vision「一人ひとりの時間を豊かに」と Mission「『はたらく』を通じて人生の可能性を広げるインフラをつくる」は、自分が大切にしてきた価値観と強く重なるものでした。加えて、モジュラーモノリスやチームトポロジーなど、プロダクトと組織の成長に向き合うための技術的・組織的なチャレンジがあることにも惹かれました。 また、エンジニアとして技術を深めるだけでなく、PdM、アーキテクト、EM、テックリード、シニアエンジニアなど、さまざまなキャリアの可能性があることも魅力でした。自分自身、技術、プロダクト、人・チームの成長のどれにも関心があるので、入社後の選択肢を広く持ちながら挑戦できそうだと感じました。 入社して感じていること 実際に入社してみると、事業、プロダクト、開発組織、ドメイン知識など、キャッチアップすることはたくさんあります。正直、まだまだ目の前のことを理解しながら、なんとか現場についていっている段階です。 ただ、チームトポロジーを踏まえた組織設計、Notion上のドキュメント、バックエンド開発Handbook、AIエージェント向けのスキルなど、必要な情報にたどり着きやすい仕組みが整っていることは、とても心強く感じています。 tech.timee.co.jp 加えて、入社後はメンターの方と毎日1on1の時間があり、分からないことをそのままにせず相談できる環境があります。キャッチアップ量は多いですが、一人で抱え込まずに前に進める安心感があります。 正直、まだ一度聞いただけで理解しきれることばかりではありません。だからこそ、分からなかったことや後で見返したいことは Notion DB に残し、タスクやキャッチアップ用のメモとして少しずつ整理しています。レビューでいただいたフィードバックも、同じ指摘を繰り返さないように Claude や Cursor の Skill として残し、次の実装やレビュー前に見返せるようにしています。文字だけでは把握しづらいドメインやシステムの関係性は、Miro で図にしながら、自分にとって理解しやすい形に変換しているところです。 また、単に仕様を実装するだけではなく、「なぜそれをやるのか」「誰にどのような価値があるのか」を考える機会が多いことも印象的です。ドメインやプロダクトの変化に向き合う難しさはありますが、その難しさも含めて、プロダクトやユーザーに近い場所で価値を届けるエンジニアリングに取り組めていることを面白く感じています。 これからやりたいこと まずは担当領域でしっかり価値を出しながら、ワーカーさんと事業者様双方にとってより良い体験を届けられるよう、プロダクト理解と技術力の両方を深めていきたいです。 そのうえで、継続的に学び、学びをチームや組織に還元できるエンジニアでありたいと思っています。目の前の課題に対して横着せず、背景や構造を理解しながら、論理的に考え、周囲と協力して成果につなげていきたいです。 また、バックエンドエンジニアとしての専門性を高めつつ、これまでの経験も活かして、プロダクトやチームの成長にも貢献していきたいです。 タイミーが向き合っている「はたらく」の領域には、ワーカーさんと事業者様双方の体験をより良くすること、複雑なドメインをプロダクトとして分かりやすく届けること、事業成長に耐えられるシステムを作ることなど、エンジニアリングで向き合えるテーマがたくさんあると感じています。これから少しずつ、自分なりの形で価値を届けていけるよう頑張っていきます。 おわりに ここまで読んでいただき、ありがとうございました。 タイミーはいま、第二創業期ともいえるフェーズにあります。新規事業や既存領域の進化がいくつも並行して走っており、意思決定や仮説検証のサイクルを速めるための仕組みづくりも盛んです。 まだ入社して間もないですが、このスピード感の中でプロダクト開発に向き合えることは、率直にとても面白いと感じています。 もしタイミーの Vision、Mission、Value に共感し、同じ熱量でプロダクト開発に関わりたいと感じる方がいれば、ぜひ一度カジュアル面談でお話しできると嬉しいです。 product-recruit.timee.co.jp
はじめに こんにちは、タイミーのプラットフォームエンジニアリングチームに所属している徳富( @yannKazu1 )です。 この記事では、スキマバイトサービス「タイミー」のバックエンド(Rails API)に Datadog Test Impact Analysis(TIA) を導入し、PRごとのCIフィードバックを大幅に高速化した取り組みについて紹介します。 なお、バックエンドはRailsアプリケーションなので、テストフレームワークは RSpec を前提として話を進めます。 サービスが成長してプロダクトの機能が増えてくると、テストも当然増えていきますよね。開発者が増えればPRの数も増え、CIの待ち時間がボトルネックになってくるのも、割とあるあるな課題だと思います。同じような悩みを抱えているチームの参考になれば嬉しいです。 背景:膨れ上がるテストとの戦い タイミーのバックエンドはモジュラーモノリスを採用しており、複数のインターフェースを1つのRailsアプリケーションで提供しています。 担当するバックエンドエンジニアの数もかなり多く、当然テストの数もそれに比例して増えていきます。 テスト数は約35,000。そして月に約2,000テストのペースで増加していました。 さらに最近はAIコーディングツールの活用も進み、テストが増えるペースが加速しています。人手に加えてAIコーディングツールの活用も進む中で、テストの増加速度が既存のチューニングでは吸収しきれない状況になりつつありました。 これまでの高速化の取り組み もちろん手をこまねいていたわけではありません。たとえば、 self-hosted runnerの活用 、GitHub Actionsのjobの 35並列 分割、 split-test による実行時間ベースの均等分割、ジョブ内のステップ並列実行(Redis/ESの起動とRuby/bundleセットアップをbackground + wait-allで同時進行)、 MySQLマイグレーションキャッシュ 、Dockerイメージキャッシュなど——泥臭いチューニングを積み重ねてきました。その結果、35,000テストを 約10分 で完走させるところまで持っていくことができました。 しかし、毎月2,000テストずつ増えていく状況では、並列数を増やし続けるのにも限界があります。ノード数を増やせばその分CIコストも増えていきますし、どこかで別のアプローチが必要になるのは明らかでした。 今後の成長を考えると、 すべてのPRですべてのテストを実行するのは持続可能ではない と判断しました。 Datadog Test Impact Analysis(TIA)という選択肢 そこで目をつけたのが Datadog Test Impact Analysis(TIA) です。 TIAは、コード変更の差分を解析し、その変更に関係のあるテストだけを選択的に実行する仕組みです。Datadogが各テストのコードカバレッジを収集・保持しており、「このファイルを変更したなら、このテストを実行すべき」という判断を自動で行ってくれます。 これにより、PRごとのCIでは 変更に関連するテストだけ を実行してフィードバックを高速化しつつ、品質の担保は別の仕組みで行うという戦略が取れるようになります。 TIAを使うための前提条件 TIAを使うには、まず Test Optimization を導入しておく必要があります。Test Optimizationはテスト結果やパフォーマンスデータをDatadogに送信して可視化する仕組みで、TIAはその上に乗っかる機能です。 具体的には以下が必要になります。 Test Optimization の設定が完了していること : datadog-ci gem( >= 1.0 )を導入し、CIからテスト結果をDatadogに送信できる状態にしておく Datadog側でTIAを有効化すること : Test Service Settingsページから、 Intelligent Test Runner Activation 権限を持つユーザーがTIAを有効にする必要がある 弊社ではもともとTest Optimizationを使ってテストの実行結果をDatadogで可視化していたので、TIAの導入自体は追加コストなしでスムーズに進められました。 設計方針:GitHub Flow + マージキューを活かす ここからが今回のキモです。 弊社は基本的に GitHub Flow を採用しています。mainにマージされるとリリースが走るというシンプルな方針です。そして、mainへのマージには マージキュー(Merge Queue) を利用しています。マージキューの導入については、 こちらの記事 で詳しく紹介しています。 全体のフローはこんな感じです。 flowchart TD A["featureブランチで開発・push"] --> B["PR CI(ci_branch)<br/>TIA有効 → 関連テストのみ実行<br/>⚡ 高速フィードバック"] B -->|CI通過| C["レビュー & Approve"] C --> D["マージキューに投入"] D --> E["マージキュー CI(ci)<br/>全テスト実行<br/>+ Datadogカバレッジ収集"] E -->|全テスト通過| F["mainにマージ"] F --> G["リリース"] style B fill:#e8f5e9,stroke:#4caf50 style E fill:#fff3e0,stroke:#ff9800 ポイントは、 PRのCIとマージキューのCIで役割を分けている ところです。この「マージキュー」の特性をうまく活かすことで、以下のような構成を実現しました。 PRブランチ( ci_branch ワークフロー) TIAを有効化 して、差分に関連するテストのみを実行 開発者へのフィードバックを高速化 # ci_branch.yml jobs : ci : uses : ./.github/workflows/_ci.yml with : skip : false itr_enabled : true # TIAによるテストフィルタリングを有効化 secrets : inherit マージキュー( ci ワークフロー merge_group イベント) 全テストを実行 (TIAフィルタリングは無効) Datadogへのカバレッジ収集も同時に実施 これを通過しないとmainにマージされない # ci.yml on : merge_group : jobs : ci : uses : ./.github/workflows/_ci.yml with : skip : ${{ github.event_name != 'merge_group' }} # マージキューでは全テストを実行しつつカバレッジを収集 dd_coverage_enabled : true tia_test_skipping_mode : suite tia_force_run_all : true # gem側のスキップも無効化して全spec実行 secrets : inherit (コード内の tia_test_skipping_mode: suite は、テストの選定を「テストファイル単位」で行う設定です。なぜ suite にしたのかは、後述の「suiteモードを選んだ理由」で詳しく触れます。) つまり、 PRでは「速さ」を、マージキューでは「安全」を という役割分担です。 マージキューのテストを全件パスしないとmainにマージされず、mainにマージされないとリリースされない。この構造があるからこそ、PRのテストを絞っても品質を担保できるわけです。 カバレッジ収集はマージキューで TIAの精度を保つには、カバレッジデータを最新に保つ必要があります。 マージキューは mainにマージされる直前のコミットで全テストを実行 するので、ここでカバレッジを収集すれば常に最新の状態が保たれます。 # ci.yml(merge_group イベント時) dd_coverage_enabled : true # カバレッジ収集を有効化 tia_test_skipping_mode : suite # suite単位で収集 tia_force_run_all : true # gem側のスキップも無効化して全spec実行 当初はこのカバレッジ収集を別ワークフロー(Coverage Build)でも回していたのですが、マージキューで毎回「全テスト実行+カバレッジ収集」を行う構成にしたことで一本化でき、運用がシンプルになりました。 ddtest plan によるテスト選定と分割の工夫 ddtest plan の基本 TIAによるテスト選定には、Datadogが提供する ddtest CLIツールを使っています。 ./ddtest plan \ --platform ruby \ --framework rspec \ --min-parallelism 1 \ --max-parallelism 1 \ --test-skipping-mode suite \ --tests-location "**/*_spec.rb" \ --tests-exclude-pattern "vendor/**" ddtest plan はDatadog APIと通信して、現在のコミットの差分に対してスキップ可能なテストを判定し、実行すべきテストファイルの一覧を .testoptimization/runner/test-files.txt に出力します。 gem の環境変数スキップではなく ddtest plan を採用した理由 実は ddtest コマンドを使わなくても、datadog-ci gemを導入していれば、環境変数 DD_CIVISIBILITY_ITR_ENABLED=true の設定だけでTIAによるテストスキップを有効にできます。最も手軽な方法です。 ただ、弊社ではこの方法を採用しませんでした。理由は 35並列との相性が悪い からです。 gemの組み込みスキップは、RSpecの実行時に各テストケースを個別にスキップします。そのため、35ノードにテストを分割した後に各ノード内でスキップが走るので、「このノードは割り当てられたテストの大半がスキップされて30秒で終わったけど、別のノードは全部実行対象で5分かかった」といったことが起きます。結果としてノード間のばらつきが大きくなり、並列化の効率がガクッと落ちるんですよね。 そこで、テストの選定は ddtest plan で 実行前に 行い、選定されたファイルだけを split-test で均等に分割する、という方式を取りました。スキップの判断をRSpec実行の外に出すことで、各ノードに割り当てるテスト量を事前にコントロールできるようにしています。 ちなみに ddtest plan 自体にも --min-parallelism / --max-parallelism オプションによるノード分割機能があります。ただ、検証してみたところ split-test と比べてノード間の実行時間のばらつきが大きかったため、分割は引き続き split-test に任せる構成にしました。 ddtest plan は「どのテストを実行するか」の選定だけに使い、「どう分割するか」は split-test に委ねる、という役割分担です。 カバレッジ収集時の罠と DD_TIA_FORCE_RUN_ALL もう一つ、 DD_CIVISIBILITY_ITR_ENABLED 周りで地味にハマったポイントがあります。 TIAが正しくテストを選定するには、各テストがどのソースコードを通過するかという カバレッジデータ をDatadogに送る必要があります。カバレッジ収集は DD_CIVISIBILITY_ITR_ENABLED=true で有効化できます。ただし、この環境変数を true にすると、 カバレッジ収集と同時にテストのスキップも有効になります 。 つまり、カバレッジを集めたいだけなのに、TIAが「このテストはスキップしてOK」と判断したテストのカバレッジが収集できないという矛盾が起きます。これだとカバレッジデータに穴が空き、次回以降のTIA判定精度が落ちていきます。 この問題を解決するために、 datadog-ci gem が提供する datadog_itr_unskippable というRSpecメタデータを活用しています。 datadog-ci gem は、 DD_CIVISIBILITY_ITR_ENABLED=true のときにDatadog APIからスキップ可能なテストの一覧を取得し、該当するテストをスキップします。しかし、RSpecのメタデータに datadog_itr_unskippable: true が設定されているテストは、スキップ対象から除外されて必ず実行されます。 これを利用して、 spec_helper.rb で以下のように 全テストにunskippableメタデータを付与 しています。 # spec_helper.rb if ENV [ ' DD_TIA_FORCE_RUN_ALL ' ] == ' true ' # DD_CIVISIBILITY_ITR_ENABLED はカバレッジ収集と suite スキップを同時に有効化するため、 # 収集専用ジョブでは全 example を unskippable にして forced run させる RSpec .configure do |config| config.define_derived_metadata do |metadata| metadata[ :datadog_itr_unskippable ] = true end end end define_derived_metadata はRSpecの機能で、全テストのメタデータにデフォルト値を追加できます。これにより、gem側のスキップ判定が働いても「このテストはスキップ不可」と判定されるので、結果的に全テストが実行されます。 # マージキューでの環境変数設定 DD_CIVISIBILITY_ITR_ENABLED : true # カバレッジ収集を有効化 DD_TIA_FORCE_RUN_ALL : true # 全テストをunskippableにして必ず実行 DD_CIVISIBILITY_ITR_ENABLED=true でカバレッジ収集の仕組みを有効にしつつ、 DD_TIA_FORCE_RUN_ALL=true で全テストにunskippableメタデータを付与してスキップを防ぐ。これにより、全テストを実行して完全なカバレッジデータを収集できるようになります。 マージキューではこの組み合わせを使い、PRブランチでは DD_CIVISIBILITY_ITR_ENABLED=false (カバレッジ収集もスキップも無効)にして余計なオーバーヘッドを排除しています。 suiteモードを選んだ理由 TIAには testモード と suiteモード の2つの粒度があります。 testモード : 個々のテストケース( it ブロック)単位でスキップを判定 suiteモード : テストファイル( _spec.rb )単位でスキップを判定 今回は suiteモード を採用しました。 理由はシンプルで、 testモードでは35,000テストの分割判定に約2分かかるため です。差分が小さいPRなら問題ありません。しかし全テストに影響する変更(例えば spec_helper.rb の変更など)をした場合は、2分かけて「全テスト実行」という結論になり、かえってオーバーヘッドが大きくなってしまいます。その点、suiteモードなら対象がファイル単位なので、この判定が高速(5s程度)に完了します。 split-test との組み合わせ 前述の通り ddtest plan で選定したファイルを split-test で均等分割するのですが、ここが地味に苦労したポイントです。 split-test はファイルリストを直接受け取れず --tests-glob しか受け付けません。そこで、以下のようにシンボリックリンクを使ってglobのマッチ対象をTIA対象ファイルだけに制限するアプローチを取りました。 # TIA対象ファイルのシンボリックリンクを一時ディレクトリに作成 SPLIT_DIR=$(mktemp -d) while IFS= read -r f; do [ -f "$f" ] && mkdir -p "$SPLIT_DIR/$(dirname "$f")" && ln -s "$WORKSPACE/$f" "$SPLIT_DIR/$f" done < .testoptimization/runner/test-files.txt # 一時ディレクトリ内で split-test を実行(TIA対象のみが均等に分割される) (cd "$SPLIT_DIR" && "$WORKSPACE/split-test" \ --junit-xml-report-dir "$WORKSPACE/tmp/rspec_junit" \ --node-index $NODE_INDEX \ --node-total 35 \ --tests-glob "spec/**/*_spec.rb" \ --tests-glob "packs/*/spec/**/*_spec.rb") ちょっとトリッキーですが、これにより TIAで絞り込んだテストを、実行時間ベースで均等に35分割する ことが実現できました。 なお、35並列のうちTIAの絞り込みで対象テストがゼロになるノードも出てきます。その場合は空のダミーJUnitレポートを出力しておき、後続のジョブが正常に動くようにしています。 PRの全変更を正しく評価するための工夫 もう一つ、導入時にハマったポイントがあります。 ddtest はHEADコミットのdiffだけを見てテスト選定を行います。PRに複数コミットがある場合、最後のコミットの変更しか考慮されません。これだと初期コミットで変更したファイルに関連するテストがスキップされてしまう可能性があります。 そこで、GitHub APIで merge-base(分岐点) を取得し、PRの全変更を1つのdiffとして ddtest に認識させる仕組みを入れています。 # PRの分岐点を取得 MERGE_BASE=$(gh api "repos/$GITHUB_REPOSITORY/compare/main...$GITHUB_SHA" \ --jq '.merge_base_commit.sha') # 分岐点を親、現在のツリーを内容とするコミットを作成 git fetch --depth=1 origin "$MERGE_BASE" --no-tags TREE=$(git rev-parse "HEAD^{tree}") TIA_COMMIT=$(git commit-tree "$TREE" -p "$MERGE_BASE" -m "TIA: all PR changes") git reset --soft "$TIA_COMMIT" これにより、HEADのdiffが「PR全体の変更」を表すようになり、TIAが正しく全変更を評価できるようになります。 全体のアーキテクチャまとめ 最終的な構成を図にすると以下のようになります。 flowchart TD A[開発者がPRを作成] --> B subgraph PR["ci_branch ワークフロー(PRブランチ)"] B["ddtest plan (TIA)\n関連テストのみ抽出"] --> C["split-test\n35並列に均等分割"] --> D["RSpec実行\n(関連テストのみ)"] end D -->|テスト通過| E[マージキューに投入] E --> F subgraph MQ["ci ワークフロー(merge_group イベント)"] F["全テスト実行\n(TIAフィルタリングなし)"] --> G["Datadogカバレッジ収集\n(TIA精度を維持)"] end G -->|全テスト通過| H["mainにマージ → リリース"] style PR fill:#e8f5e9,stroke:#4caf50 style MQ fill:#fff3e0,stroke:#ff9800 導入時のハマりポイントまとめ 実際に導入してみて、いくつかハマったポイントをまとめておきます。 1. testモードの分割コスト 前述の通り、testモードでは35,000テストの分割判定に約2分かかります。suiteモードなら数秒で完了します。テスト数が多い場合はsuiteモード一択だと思います。 2. split-test との組み合わせ split-test がファイルリストを直接受け取れないため、シンボリックリンクを使った一時ディレクトリ方式を採用しました。ちょっとトリッキーですが、確実に動きます。 3. マルチコミットPRの差分評価 ddtest がHEADコミットのdiffしか見ない問題は、merge-baseからのコミットを作り直すことで解決しました。 4. カバレッジの鮮度 TIAの精度はカバレッジデータの鮮度に依存します。当初は別ワークフロー(Coverage Build)でも収集していましたが、マージキューで毎回全テスト+カバレッジ収集を行う構成にしたことで一本化でき、シンプルになりました。 5. TIAで全テストがスキップされるノードの対策 35並列のうち、TIAで対象テストがゼロになるノードが出てきます。その場合はダミーのJUnitレポートを出力して後続のジョブが正常に動くようにしています。 成果 TIA導入前は、PRのCIで全テストを実行していたため 約10分 かかっていました。TIA導入後は変更の差分によって実行されるテスト数が変わりますが、差分が小さいPRでは 1〜2分 で完了するケースもあり、大幅に高速化しています。 おわりに Datadog TIAの導入により、PRごとのテスト実行時間を大幅に短縮しつつ、マージキューで全テストを実行するという安全な構成を実現できました。 ポイントをまとめると: PRブランチ : TIAで関連テストのみ実行 → 高速フィードバック マージキュー : 全テスト実行 + カバレッジ収集 → 品質担保 suiteモード : テスト数が多い場合はtestモードよりsuiteモードが実用的 ddtest plan + split-test : TIAフィルタリングと均等分割の組み合わせがキモ GitHub Flow + マージキュー : この組み合わせがTIA導入の前提条件として非常にフィットした テスト数がどんどん増えていく中で、「全部回す」から「必要なものだけ回す」へのシフトは避けて通れない道だと思います。同じような課題を抱えているチームの参考になれば幸いです。
1. はじめに こんにちは。プラットフォームエンジニアリングチームに所属する小泉( @naotoko_ )です。 本記事は、同チームの徳富( @yannKazu1 )が執筆した「 消えるランナーの観測基盤をどう選んだか — Datadog・マネージド・OSS を料金体系で比べて Loki + Prometheus に決めた話 」の本番環境への導入編です。EKS Auto Mode でホストしている Self-hosted Runner の監視基盤に Grafana・Prometheus・Loki・Alloy を導入した際にハマったポイントと解決策を、各コンポーネントごとにお伝えします。 Grafana・Prometheus・Loki・Alloy をどのような構成で実装したかは、上記ブログの「 実装:どう組んだか 」をご覧ください。 2. Alloy - メトリクスが重複する・取れない 2-1. DaemonSet の重複 scrape 問題 Alloy は DaemonSet でデプロイしており、全ノードに1つずつ Pod が立ち上がる構成となっています。 デフォルトのまま使用すると、Alloy はメトリクス収集時にクラスター全体から scrape 対象の一覧を取得し、そのリストに対して定期的に HTTP リクエストを送ります。DaemonSet の各 Pod がそれぞれ独立してこれを実行するため、全 Pod が同じターゲット一覧を取得してしまいます。 たとえば kube-state-metrics や ARC controller のようにクラスターに1つしかないエンドポイントは、全ての Alloy Pod が同じエンドポイントを scrape しに行くためノード数分重複します。さらに、node-exporter や kubelet のようにノードごとに存在するエンドポイントでも、各 Alloy Pod がクラスター全ノード分のエンドポイントを発見して全て scrape するため、同様にノード数分重複します。その結果、全ての scrape 対象で同一のメトリクスが重複して Prometheus に送られてしまいます。 各 Alloy Pod がそれぞれ独立にクラスター全体のターゲット一覧を取得するため、クラスターに1つしかない kube-state-metrics も、ノードごとに存在する kubelet も、全 Pod から scrape される。 なぜログ収集では同じ問題が起きないか ログ収集は各 Alloy Pod がノードのローカルファイル( /var/log/pods/ )を読む方式です。ノードAの Alloy はノードAのログだけ、ノードBの Alloy はノードBのログだけを読むため、Pod 間でデータが重複しません。読む対象がノード単位で自然に分割されているため、メトリクスのような重複の問題が起きませんでした。 解決策:Alloy のクラスタリングを有効化する Alloy にはクラスタリング機能があり、Pod 同士がクラスターを形成して scrape 対象を自動的に分担します。(詳しい仕組みは割愛) Alloy Pod 同士がクラスターを組み、各ターゲットの担当をいずれか1つの Pod に決める。担当はハッシュで決まるため、図のように自分と同じノード上のターゲットを担当するとは限らない。 設定は2段階必要です。まず Helm chart の values でクラスタリングを有効化し、DaemonSet の全 Pod がクラスターを形成するようにします。 alloy : clustering : enabled : true そのうえで、分担させたい各 prometheus.scrape ブロックに clustering { enabled = true } を付けます。 prometheus.scrape "kube_state_metrics" { targets = [...] clustering { enabled = true } forward_to = [prometheus.remote_write.default.receiver] } 注意点として、この2つはセットで初めて機能します。Helm values 側を有効化せずに prometheus.scrape 側だけ書いても no-op(何もしない)になり、逆に Helm values 側だけ有効化しても clustering ブロックを付けていない scrape コンポーネントは従来どおり全 Pod が独立に scrape し続けます。 2-2. kubelet scrape の InternalIP 対応 Grafana でメトリクスを確認すると Pod の CPU・メモリが No data になっていました。調べると kubelet の scrape が全ノードで失敗していました。 原因は Alloy が __meta_kubernetes_node_name (ノード名)を scrape 先のアドレスとして使っていたためです。EKS Auto Mode ではノード名がインスタンス ID になるため名前解決できず、全ノードでタイムアウトしてしまっていました。 通常の EKS ではノード名は ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal のようなプライベート DNS 名になるため、 __meta_kubernetes_node_name でも名前解決できます。一方 EKS Auto Mode では i-xxxxxxxxxxxxxxxxx のような EC2 インスタンス ID がノード名になるため、名前解決できないという状況でした。 解決策:InternalIP を使う __meta_kubernetes_node_address_InternalIP に切り替えることで、ノード名ではなく IP アドレスで直接アクセスするようにしました。 rule { source_labels = ["__meta_kubernetes_node_address_InternalIP"] regex = "(.+)" replacement = "${1}:10250" target_label = "__address__" } 3. Prometheus - OOM と欠損との戦い 3-1. OOM 問題 導入後、Prometheus サーバーが OOMKilled → 再起動を繰り返すようになりました。原因は1つではなく、複数が重なっていました。 原因1:Alloy と Prometheus の二重取り込み Prometheus の Helm chart はデフォルトでいくつかの scrape job( kubernetes-nodes-cadvisor ・ kubernetes-pods 等)が有効になっています。今回の構成では Alloy が一元的に scrape して Prometheus に remote_write する設計のため、Prometheus 自身の scrape job と Alloy の remote_write で同じメトリクスが2経路で入っていました。 これは Alloy が scrape するターゲットと重複している chart デフォルトの scrape job を無効化することで解決することができます。 scrapeConfigs : kubernetes-nodes : false kubernetes-nodes-cadvisor : false kubernetes-pods : false # ... 原因2:cAdvisor の高 cardinality ラベル cAdvisor は kubelet に組み込まれており、コンテナのリソース使用量を収集するコンポーネントです。Alloy は kubelet の /metrics/cadvisor エンドポイントを scrape することでこのメトリクスを取得しています。cAdvisor のメトリクスには image ・ name などのラベルが付いており、同時稼働するランナー数が多いほど cardinality(系列数)が爆発的に増えてしまいます。 そこで、Prometheus に送る前の段階で Alloy 側でメトリクスやログ調査に不要なラベルを drop することで、Prometheus に送られる系列数を極力減らすようにしました。 ちなみに、 image はコンテナイメージ名、 name はコンテナランタイム上のコンテナ ID(containerd では 64 桁の 16 進文字列)が入ります。 name はコンテナが起動するたびに異なる値になるためほぼユニークであり、集計や絞り込みには使わないと判断し、この2つのラベルを drop しました。一方で、メトリクスのクエリで実際に使う namespace ・ pod ・ container といったラベルは残しています。環境に応じて、調査に使わないラベルは可能な範囲で drop するようにするのが無難です。 rule { regex = "image|name" action = "labeldrop" } ただし、 id ラベルだけは残す必要があります。cAdvisor は同じメトリクス名で複数のコンテナの情報を収集しており、それぞれを区別するために id ラベルが使われています。 id を drop すると異なるコンテナのメトリクスがラベルセット上で同一系列として扱われていしまいます。その結果、同じタイムスタンプに複数のサンプルが届き、 Prometheus が duplicate sample for timestamp エラーを返してしまうため注意が必要です。 原因3:Head Block のメモリ常駐 Prometheus はメトリクスを受け取ると、書き込み効率のためにまずメモリ上に一時的に貯めます。これが Head Block です。 一定時間が経つとメモリから EBS 上のファイルに書き出されます。デフォルトでは Head Block が3時間分(min-block-duration の1.5倍)に達した時点で古い2時間分がブロックとして書き出されます。つまり常に1〜3時間分のデータがメモリに残り続けるため、メトリクスの量が多いほどメモリ使用量が増えます。 storage.tsdb.min-block-duration を 2h から 30m に短縮することでこの「一時的に貯める時間」を短くし、より頻繁に EBS に書き出すことでメモリ常駐量を削減しました。その代わり、以前はメモリから取得できていたデータが EBS から取得されるためクエリが遅くなるトレードオフがあります。Self-hosted Runner の監視という性質上、確認したいのは基本的に直近の状況であるため、30分より古いデータの取得が多少遅くなっても問題ないと判断しています。 なお、 storage.tsdb.min-block-duration は --help にも表示されない hidden フラグで、公式には「テスト用途」とされています。挙動を理解したうえで利用してください。 extraArgs : storage.tsdb.min-block-duration : 30m 3-2. out-of-order サンプル問題 Alloy クラスタリングを有効化してから、Prometheus に out of order sample エラーが大量に出るようになりました。 Alloy のクラスタリングは Pod の増減をトリガーにターゲットの再配分を行います。Self-hosted Runner はジョブの増減に応じてノードが頻繁にスケールするため、Alloy の Pod も増減し、再配分が頻繁に起きます。Alloy のクラスタリングによるターゲットの分担は eventually consistent なモデルであるため、再配分の引き継ぎのタイミングによっては同じターゲットが一時的に2つの Pod から scrape されることがあります(grafana/alloy の issue #1611・#2348 でも報告されている既知の挙動です)。この状態で同じ時系列のサンプルが2つ届くと、後着のサンプルのタイムスタンプが先着より古い場合があります。すると Prometheus はこれを out-of-order として拒否し、該当のサンプルを捨ててしまいます。その結果、 Grafana で確認できるメトリクスに欠損が生じてしまいます。 解決策:out-of-order time window を設定する out_of_order_time_window を設定することで、指定した時間内の過去のタイムスタンプを受け入れるようになります。弊社の環境では、最終的に 10m に落ち着きました。out-of-order のサンプルを保持するためのメモリが若干増えますが、 storage.tsdb.min-block-duration を 30m に短縮して Head Block のメモリを節約できているため、特に問題ないと判断し、10m に設定しています。 tsdb : out_of_order_time_window : 10m 4. Loki - chart 移管の罠と起動しない Pod 4-1. Helm chart リポジトリ移管の罠 Loki の Helm chart はもともと grafana/helm-charts ( https://grafana.github.io/helm-charts )で配布されていました。 しかし chart v6.55.0 を最後に OSS Loki 向けの chart は grafana-community/helm-charts ( https://grafana-community.github.io/helm-charts )へフォークされ、v7.0.0 以降は community 側からリリースされています。従来のリポジトリに残った loki chart は Grafana Enterprise Logs(GEL)向けのメンテナンス専用になりました。 そのため、 grafana/helm-charts を使い続けると、気づかないうちに GEL 向けの chart を引いてしまいます。 合わせて以下の変更もあるので、注意が必要です。 フォーク後はメジャーバージョンの上がるペースが非常に速い(v7.0.0 から数ヶ月で v17.x、執筆時点の最新は v18.x) deploymentMode の値が SingleBinary → Monolithic にリネーム(chart v12.0.0)。なお values のキーは singleBinary のまま変わっていません resource "helm_release" "loki" { repository = "<https://grafana-community.github.io/helm-charts>" # ここが変わった chart = "loki" version = "17.1.6" ... } # chart v12.0.0 以降 deploymentMode : Monolithic # SingleBinary から変更 4-2. StorageClass が自動作成されない Loki の Pod がスケジュールされず、以下のエラーが出ていました。 eks-auto-mode/compute Failed to schedule pod, unbound pvc must define a storage class 通常の EKS では EBS CSI driver addon がデフォルトの StorageClass を自動作成しますが、EKS Auto Mode では作成されません。StorageClass が作成されていないと、PVC がバインドできず Pod が起動しません。 そのため、StorageClass を手動で作成し、Loki の PVC に指定することで解決しました。 # StorageClass の手動作成 apiVersion : storage.k8s.io/v1 kind : StorageClass metadata : name : auto-ebs-sc provisioner : ebs.csi.eks.amazonaws.com volumeBindingMode : WaitForFirstConsumer parameters : type : gp3 encrypted : "true" # loki.yaml singleBinary : persistence : storageClass : auto-ebs-sc 5. Grafana - デプロイ戦略とコード管理 5-1. EBS(RWO)と RollingUpdate の相性問題 Grafana を helm upgrade したとき、Pod が新しくなるはずが延々と Pending のまま膠着しました。原因は EBS の制約です。 EBS は RWO(ReadWriteOnce)のため、1つの Node にしか同時にアタッチできません。デフォルトの RollingUpdate は新しい Pod を起動してから古い Pod を落とす順番なので、新旧の Pod が同じ PVC を取り合って Multi-Attach error が発生します。 解決策:Recreate 戦略に変更する Recreate にすると「旧 Pod 終了 → EBS detach → 新 Pod 起動」という順番になります。replicas=1 の構成なので更新時に短時間のダウンタイムが発生しますが、監視基盤という特性を踏まえて瞬断は許容し、 Recreate に設定しています。 deploymentStrategy : type : Recreate 5-2. Dashboard・Alerting のコード管理 なぜ Terraform provider ではなく Helm values で管理するか Grafana のダッシュボードとアラートルールをコード管理する方法として、 grafana Terraform provider を使う方法もあります。しかし今回は Helm values(YAML/JSON)での管理を選びました。 理由はシンプルで、Grafana の UI でダッシュボードやアラートルールを作り込んだあと、そのまま JSON/YAML でエクスポートして Helm values に貼り付けるだけでコード管理できるからです。Terraform のリソース定義に落とし込む手間が不要で、UI で確認しながら作ったものをそのまま反映できます。 Grafana の Helm chart は provisioning の仕組みを持っており、values に書いたダッシュボード定義やアラートルールを起動時に自動で読み込みます。 YAML 管理による制約:削除は deleteRules に明示が必要 アラートルールを YAML から削除しても、Grafana 上のルールは消えません。アラートルールの provisioning は追加・更新してくれますが、削除は行いません。 これは Grafana の設計上の安全策です。file-based provisioning はステートレスで、Grafana は「この YAML がルールの全量である」という保証を持てません。複数ファイルから同時にプロビジョニングできる設計上、「ファイル A にないルール」がファイル B で管理されているかもしれず、Grafana にはどのファイルが何を管理しているか判断できません。また設定ファイルのバグやマウント失敗で一時的にファイルが読めなくなったとき、自動削除だとアラートルールが全消えするリスクもあります。Terraform が管理対象を state ファイルで追跡しているから自動削除できるのと対照的で、file-based provisioning にはその state 概念がないため、削除の意図を明示する仕組みとして deleteRules が用意されています。 ルールを削除するには以下のように UID を指定します。 alerting : rules.yaml : groups : - ... deleteRules : - orgId : 1 uid : hoge-alert # 削除したいルールの UID を明示 6. 専用 NodePool への分離 Prometheus・Loki・Grafana・Alloy といった観測性コンポーネントを、Runner と同じ NodePool に混在させていると2つのリスクがあります。 1つ目は、Prometheus の OOM のような観測性コンポーネントのリソースプレッシャーが同じノード上のランナーの動作に悪影響を与えるリスクです。 2つ目は、Karpenter の consolidation に巻き込まれるリスクです。runner が使う NodePool は業務時間帯の consolidation を無効にしており、早朝の限られた時間帯のみ有効になる設定にしています。この時間帯に consolidation が走ったとき、同じ NodePool にいる観測性コンポーネントの Pod が別ノードに移動させられ、メトリクスやログが欠損するリスクがあります。 専用 NodePool に分離することで、これらの問題をランナーから切り離せます。 Karpenter で dedicated=observability:NoSchedule の taint を付けた専用 NodePool を用意し、Prometheus・Loki・Grafana などの Deployment / StatefulSet 系コンポーネントに対応する toleration と nodeSelector を設定しました。なお、DaemonSet である Alloy と node-exporter は全ノードで動かす必要があるため toleration のみ付与しています。 # NodePool taints : - key : dedicated value : observability effect : NoSchedule # 各コンポーネント tolerations : - key : dedicated value : observability effect : NoSchedule nodeSelector : karpenter.sh/nodepool : observability 監視用コンポーネントについてもランナーと同じく、arm64 on-demand インスタンスを使用するようにしています。 ただし、インスタンスタイプの指定が甘いと Karpenter がコンピュート最適化インスタンス(c6g.large / 4GB RAM)を選んでしまい、Loki や Grafana が OOMKill されるという問題が起きてしまいます。observability スタックのメモリ使用量を実際に確認したうえで、NodePool の requirements に 8GB 以上のインスタンスを指定するなど、きちんとメモリ要件に合ったインスタンスが選ばれるように注意が必要です。 requirements : - key : eks.amazonaws.com/instance-memory operator : Gt values : [ "7168" ] # 8GB 以上 7. まとめ Grafana・Prometheus・Loki・Alloy を Self-hosted Runner の監視基盤として導入するにあたり、各コンポーネントで様々なハマりポイントがありました。特に Prometheus の OOM は複数の原因が重なっており、1つ解決しても次の問題が出てくる形で対応に時間がかかりました。また EKS Auto Mode には通常の EKS(マネージドノードグループ等)と設定において異なる部分があるため、EKS Auto Mode に初めて触る方や移行を考えている方は特に注意してください。 同じ構成を検討している方の参考になれば幸いです。