タイミーのブログ - TECH PLAY

TECH PLAY

タイミー

タイミー の技術ブログ

308

こんにちは。タイミーでプロダクトエンジニアをしている津守です。 普段は、エンジニアとして機能の開発をメインの仕事としていますが、最近は営業チームの商談に同行する機会が増えてきました。そこで突きつけられたのが、タイトルの問いです。 自分が作った機能を、自分で売れるか。 機能は作れる。仕様も説明できる。でも、顧客を前にして「これはあなたの課題をこう解決します」と語り、納得してもらえるか。やってみると想像以上に難しく、同時に開発者として得るものが大きい体験でした。 この記事では、商談同行を重ねる中で見えてきたことと、そこから営業メンバーとの間に育っていったコミュニケーションパスについてまとめます。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 に初めて触る方や移行を考えている方は特に注意してください。 同じ構成を検討している方の参考になれば幸いです。
はじめに 株式会社タイミーのプラットフォームエンジニアリングチームに所属している徳富( @yannKazu1 )です。 突然ですが、皆さんの組織では「3ヶ月前のログを見たいんですが……」という依頼が来たとき、どう対応していますか? タイミーではDatadogをログ基盤として利用しています。Datadogは日常的なログ検索やアラート、ダッシュボードなどに使える非常に強力なツールです。一方で、ログの保存にはそれなりのコストがかかります。そのため弊社でも、コストとのバランスを考えてログの種類ごとに14日〜長くても45日程度の保持期間を設定していました。 普段の運用ではこれで十分です。ただ問題となるのは、「保持期間を超えた過去のログを検索したい」という場面が来たとき。今回はこの課題に対してDatadogのFlex Logsを導入し、コストをほぼ変えずに長期ログ検索を実現しようとしている取り組みについてお話しします。 なお、本記事で「長期ログ」と呼んでいるのは、おおむね1年程度遡って検索したいログを指します。また、今回の取り組みは、Datadog上で一定期間ログを「検索できる状態に保つ」ためのものです。ログを永続的に保存できるようになるわけではありません。Flex Logsの最大保持期間は15ヶ月(450日)で、それを超えたログはやはり削除されていきます。あくまで「現実的なコストで、実用的な長さの過去ログを検索できるようにする」取り組みだとご理解ください。 保持期間を超えたログ検索、実はけっこう大変だった エンジニアへの依頼やインシデント対応の中で、保持期間を超えたログの検索が求められるケースは意外と多くありました。セキュリティに関する調査、外部からの問い合わせ対応、過去の操作ログの確認など、数ヶ月前のログが必要になる場面は定期的に発生します。 そうなると、いつものLog Explorerでは当然ヒットしません。代わりに、AthenaでS3上のアーカイブを直接クエリしたり、DatadogのRehydrate機能でアーカイブからログを復元したりする必要があります。最近はArchive Searchを使うケースもあります。いずれも通常とは異なるオペレーションです。 ここで問題になるのは、長期ログ検索の依頼を受けるのが、多くの場合SREではなく担当チームのバックエンドエンジニアである点です。RehydrateやAthenaでのクエリは普段の業務ではなかなか触れる機会がないため、調査のたびに手順を調べ直すことになり、想定以上に工数がかかっていました。操作方法がわからない場合はSREに相談が来ることもあって、いろんな意味で効率が悪い状態だったんですよね。 「この時間を開発に充てられたら、チーム全体の生産性が上がるのでは?」そう考えて、まずは過去に長期ログ検索が必要だったケースを洗い出してみることにしました。 過去1年のログ調査を分析してみた 過去1年間のログ調査タスクを抽出したところ、通常の依頼27件+インシデント4件=計31件が見つかりました。思っていたより多いな、というのが正直な感想です。 保持期間を何日にすればカバーできるか まず「保持期間を何日にすれば、Datadog UIだけで調査が完結できるか」をシミュレーションしてみました。 保持期間 UI完結率 カバーしきれないケース 現行(15〜45日) 約19% 大部分がRehydrate / Athenaにエスカレート 90日(3ヶ月) 約63% セキュリティ調査 / 外部照会、一部クライアント抽出が残る 180日(6ヶ月) 約89% 残り約11%(≒3件/年)のみ 365日(12ヶ月) 約97% 残りはごく少数(年1〜2件程度) 現状だとたった19%しかDatadog UIで完結できておらず、大半がRehydrateやAthenaにエスカレートしていたことが数字で見えてきました。 ちなみにこの31件はあくまでチケットとして起票された依頼の数です。実際には、チケット化されずに個人で対応していたケースもあったはずです。また、「過去ログを確認したいが手間がかかるため諦めた」というケースもあるでしょう。そのため、潜在的なニーズはもっと多いのではないかと感じています。 180日あれば約89%をカバーできますが、後述するコストシミュレーションの結果、12ヶ月でも現状とほぼ同額に収まることがわかったため、余裕を持って 12ヶ月(1年)の保持期間 を採用することにしました。 調査はどのサービスに集中しているか 次に、調査がどのサービスに集中しているのかも見てみました。 少し前提を補足すると、タイミーでは主に以下のようなサービス群でシステムが構成されています。 クライアント画面 : 企業(クライアント)が利用する画面 社内管理画面 : 社内のオペレーションチームが利用する管理画面 ワーカーAPI : ワーカー(働き手)向けアプリのバックエンドAPI これを踏まえて集計した結果がこちらです。 サービス 主な用途 件数 シェア クライアント画面・社内管理画面 アクセス履歴・操作ログの調査など 21件 68% ワーカーAPI 各種オペレーションの調査など 5件 16% その他 — 5件 16% クライアント画面・社内管理画面が調査全体の68%を占めていました。 ここに長期保存を集中させれば、効率よく大半のケースをカバーできそうです。 Flex Logsとは ここで、今回導入したFlex Logsについて説明します。 Flex LogsはDatadogが提供するログストレージの一種です。Standard Tier(従来のインデックス)とArchive(S3等への長期保存)の中間に位置し、いわゆる「Warm Storage」にあたります。 従来のDatadogのログ管理では、Standard Tierの保持期間が過ぎるとそのログはDatadog上から検索できなくなります。Archive(S3等)を設定していれば、ログは取り込み時点でアーカイブにも保存されます。ただし、アーカイブを再度検索するにはRehydrateという復元操作が必要で、手間もコストもかかっていました。また、Rehydrateを使わずにアーカイブを直接検索できるArchive Searchという機能もありますが、こちらは検索はできるものの集計やグラフ化といった分析機能が使えず、結果も専用ページでしか閲覧できないという制約があります。また、コールドストレージをスキャンする仕組みのため、普段のLog Explorerのインデックス済みログ検索と比べると速度面で劣り、利用できるUI機能も大幅に制限されるため、調査に着手するときの心理的なハードルも大きく、調査用途では不便な場面もありました。 Flex Logsはこの問題を解消してくれます。 ストレージコストとクエリ(コンピュート)コストを分離する ことで、大量のログを低コストで長期間保持しつつ、必要なときにはLog Explorerからそのまま検索できるようにした仕組みです。最大15ヶ月(450日)の保持が可能で、Rehydrateのような復元操作は一切不要です。 個人的に一番嬉しいのは、 普段使っているDatadogの操作感がそのまま使える ところです。Log Explorerの画面上部にある「Include Flex Logs」トグルを有効にするだけで、Flex Tierのログも含めて検索できます。クエリの書き方もフィルタの使い方もいつもと同じなので、新しいツールや操作を覚える必要がありません。「保持期間を超えているからAthenaで……」と切り替える必要がなくなるのは、地味ですがかなり大きな変化だと思います。 Flex Logsの課金体系 Flex Logsの課金は大きく分けて2つのプランがあります。 Flex Logs Starter はストレージとコンピュートがセットになったプランで、保存イベント100万件あたり月額$0.60の料金体系です。手軽に始められるのが特徴で、ログ量がそこまで多くない組織に向いています。 一方、 Flex Logs(Scalable) はストレージとコンピュートが分離されたプランです。ストレージは保存イベント100万件あたり月額$0.05(年額請求の場合。オンデマンドだと$0.075)と非常に安価で、コンピュートはXS/S/M/Lのサイズから選ぶ形になります。大量のログを保存しつつ、クエリ頻度に応じてコンピュートサイズを調整できるため、大規模な組織ではこちらの方がコスト効率が良くなります。 ※ 上記はいずれもDatadog公開価格ページの参考値です。実際の単価はリージョンや契約条件により異なるため、詳細はDatadogの料金ページまたは担当営業にご確認ください。 インデックスごとにTierを設定できる Flex Logsの便利なところは、 インデックスごとにStandard Tierの保持期間とFlex Tierの保持期間を個別に設定できる 点です。 たとえば「このインデックスはStandard Tierを15日、Flex Tierを含めて12ヶ月」といった設定が可能です。Standard Tierの保持期間を短くした分のコストをFlex Logsの長期保存に回すことで、トータルのコストを抑えながら長期間のログ保持を実現できます。 設定方法 Flex Logs Starterの場合は、DatadogのLogs > Configuration > Flex Logs Controlの設定画面からセルフサーブで有効化・変更が可能です。 ただし、Scalable Compute(XS/S/M/L)を利用する場合はDatadogへの問い合わせが必要になりますので、その点はご注意ください。 Flex Logsの制限事項 Flex Logsにはいくつかの制限もあります。導入前に知っておきたいポイントです。 Monitorの対象にできない : Flex Tierのログに対してアラートを設定することはできません。アラートが必要なログはStandard Tierに保持する必要があります Watchdog Insightsが利用不可 : Datadogの異常検知機能であるWatchdogはFlex Tierのログには対応していません 検索速度はStandard Tierより遅い : クエリの実行速度はStandard Tierと比較すると遅くなります。ただ、体感としてはものすごく遅すぎるというわけではなく、長期ログの調査用途であれば十分実用的なレベルだと感じています コンピュートの同時実行数に上限がある : 大量のクエリが同時に走るとスローダウンやリトライが発生する場合があります これらを踏まえると、リアルタイムの監視やアラートが必要なログはStandard Tierで保持し、「普段は見ないけど、いざというときにすぐ検索したい」ログをFlex Tierに回す、という使い分けが基本になります。 導入した構成と期待される効果 弊社ではログの種類ごとにStandard Tierの保持期間のバランスを見直しました。そのうえで、先ほどの分析で調査の68%が集中していたクライアント画面・社内管理画面のログに対して、Flex Logsを導入しました。Flex Tierの保持期間は12ヶ月です。全サービスに一律で入れるのではなく、実際に長期検索のニーズが高いサービスに絞って適用しています。 気になるコストですが、各インデックスのStandard Tier保持期間を調整して捻出したコスト削減分をFlex Logsの費用に充てた結果、 トータルのログコストは当初比で約-3% 。コストを増やすどころか、わずかに削減できています。 過去のログ調査実績と照らし合わせると、調査の大半を占めるクライアント画面・社内管理画面のログが12ヶ月分検索可能になるため、 これまでRehydrateやAthenaに頼っていたケースの大半が、いつものLog Explorerで完結できるようになる見込み です。エンジニアの調査工数の削減に、かなり貢献できるのではないかと期待しています。 まだこれから、でも楽しみ 正直なところ、Flex Logsを導入してからまだ日が浅いので、ログの蓄積期間としてはまだまだこれからです。 ただ、1年分のログが貯まったときのことを想像すると、エンジニアからのログ調査依頼への対応は格段に楽になるはずです。「あのログ、もう消えちゃってて見られません……」という返答がなくなる未来が近づいていると思うと、素直に楽しみです。 おわりに Datadogのログはコストが高いイメージがあるかもしれません。でも、保持期間やTierの設定を見直すだけで、同じ金額でもカバーできるユースケースが大きく広がる可能性があります。 「長期ログの検索依頼のたびにRehydrateやAthenaで対応している」「そのたびに手順を調べ直している」…そんな心当たりがある方は、Flex Logsの導入を検討してみる価値があると思います。 皆さんもぜひ一度、自社のDatadogログ設定を見直してみてはいかがでしょうか。
はじめに こんにちは。プラットフォームエンジニアリングチームに所属している徳富( @yannKazu1 )です。 GitHub Actions のセルフホストランナーを運用していると、「あのジョブのログ、後から見たいんだけど……」という場面、けっこうありますよね。普段は気にしないんですが、いざ調査となると地味に困る。しかもランナーは ephemeral(ジョブが終わると Pod が即削除される)なので、見たい頃にはログが残っていない、という状態でした。 今回は、この「消えるランナー」のログとメトリクスを観測できるようにした話です。ただ、構築手順そのものよりも、 「Datadog・マネージド・OSS のどれを、何を基準に選んだのか」 を中心に書いていきます。 先に結論だけ書いておくと、こんな判断をしました。 ログ基盤 :社内標準の Datadog ではなく、 Loki + S3 (料金体系がランナーログと相性が良く、チーム裁量で導入・撤去できるため) メトリクス基盤 :マネージドではなく、 自前の OSS Prometheus (短期保持・内部用途なので最もシンプルで安い) 収集エージェント : Grafana Alloy を DaemonSet で1つ置き、ログもメトリクスも兼ねさせる なぜそう判断したのかを、料金体系やトレードオフの考え方とあわせて書いていきます。 解決したかったこと うちのチームでは、GitHub Actions のセルフホストランナーを EKS 上で動かしています。(詳細は こちら )困っていたのは、大きく2つありました。 ① ログが残らない。 ARC(Actions Runner Controller) のランナーは ephemeral で、ジョブが終わるとその Pod は即座に削除されます。調査しようとした頃には kubectl logs を打っても pod not found が返ってくるだけです。 じゃあどうしていたかというと、 問題が起きそうな状況を手元で再現しながら、 kubectl logs -f でログをファイルに書き出して張り込む 、という運用をしていました。 # こういうのを毎回手でやっていました kubectl logs -f -n arc-runners <さっき立ち上がったばかりの pod> | tee debug.log ランナーが立ち上がる瞬間を待ち構えて、消える前にログを掴む。完全に職人芸です。しんどいし、属人化の温床でした。 ② ランナー群の状態が見えない。 ログは個別のジョブを追うのは得意ですが、pending のまま積み上がっている runner 数、ジョブの待ち時間、idle のランナー台数といった全体像は読み取れません。既存の Datadog でも CPU・メモリは取れていましたが、 ARC 固有のメトリクス( gha_* )は取れていませんでした 。 技術選定:何を基準に、何を選んだか 観測したいものは決まったので、次は「何で実現するか」です。まず判断の軸を先に置きました。 コスト — 取り込み量に比例する SaaS の従量課金は、量が読めないと青天井になりがちです。一方、AWS 側に自分たちで持てば、保持期間やストレージクラスを調整してコストをコントロールできます 導入・撤去のしやすさ(調達・承認のフリクション) — 新しい SaaS を1つ増やすのは、ベンダー審査・セキュリティレビュー・予算確保・データ取り扱い確認……と技術以前の社内手続きが乗ります。一方、 OSS を自分たちの EKS 内に Helm で立てるのは、チーム裁量で完結します 。「自分たちだけで始められて、ダメなら畳める」 枯れたエコシステムであること — 近年はクエリやダッシュボード定義を AI に書かせる場面が日常的にあります。普及している技術ならだいたい書いてくれますが、ニッチなツールだと AI 支援を受けにくくなります。2026 年に技術選定するなら、無視できない観点だと個人的に思っています ランナー周りは我々が単独で管理している領域で、社内標準から外れたスタックを使っても全体への影響は小さく、切り戻しも容易です。 ログ基盤:そもそも他の選択肢はなかったのか 結論としては Loki + S3 を選んだのですが、もちろん最初から絞っていたわけではありません。選択肢を整理すると、こんな感じです。 選択肢 性格 今回の評価 Datadog(社内標準 SaaS) 既に導入済み。追加導入ゼロで楽 コスト構造が量と相性が悪い 他の SaaS(Splunk / New Relic 等) 機能は十分 新規ベンダーの調達・承認コストが乗る CloudWatch Logs(AWS ネイティブ) 既存ベンダー内で完結。承認は軽い 取り込み・スキャンの従量がログ量と相性が悪い Loki + S3(採用) クラスタ内 OSS。S3 ストレージ中心 アクセスパターンに素直にハマる 順に、なぜそれぞれを見送ったかを書いていきます。 Datadog:素直だが、コスト構造が量と合わない 弊社では Observability は基本的に Datadog に寄せる方針で、EKS にも既に Datadog Agent が動いています。 logs.enabled: true を入れれば、全コンテナログの収集がすぐ始められる。素直に考えれば「ランナーのログも Datadog でいいじゃん」です。 でも見送りました。理由は主にコストです。この基盤には社内中のワークフローのランナーが相乗りしていて、日中は大量のランナーが同時に起動します。試しに日中の10分だけログを Datadog に流したら、 その10分で普段の組織全体のログ量のおよそ2倍 になりました。しかもこのログ、見るのはうちのチームだけです。 Datadog のログ課金は 取り込み(GB 単位)+ インデックス(イベント数単位)+ リテンション(保持を延ばすとインデックス単価が上がる) の合算です。取り込み単価は安く見えますが、ログを「使える」状態にする indexing が、イベント数とリテンションの両方に比例して効いてきます。自分たちしか見ないログに同じコスト構造を当てる必要はないよな、と。 他の SaaS:機能ではなく「導入のフリクション」で落ちた Splunk や New Relic、あるいはマネージド Loki である Grafana Cloud——機能面ではどれも十分すぎるほどで、ランナーログの観測くらい余裕でこなせます。ただ、 今回これらを早い段階で外したのは、機能の優劣ではなく「新しい SaaS を1つ増やすこと自体のコスト」 でした。 新規 SaaS の導入はベンダー審査・セキュリティレビュー・契約・予算確保といった社内手続きとセットです。今回観測したいのは「うちのチームしか見ない、内部用途のランナーログ」。 自分たちしか見ないニッチなログのために、組織を巻き込む調達プロセスを回すのは割に合わない と判断しました。Datadog がコスト面で見送りになった時点で、「わざわざ別の新規 SaaS を……」という選択肢は自然と消えていった、というのが正直なところです。 CloudWatch Logs:「新規 SaaS」ではないが…… ここで少し悩ましいのが CloudWatch Logs です。AWS ネイティブなので「新規ベンダーの調達」問題が起きません。Fluent Bit や Container Insights を入れればすぐ始められます。導入のしやすさという軸では、Datadog の次くらいに楽な選択肢でした。 それでも本命にしなかったのは、コストの効き方です。CloudWatch Logs は 取り込み(GB 単位)と、Logs Insights でクエリするたびのスキャン量(GB 単位) に応じて従量課金されます。そのため、ランナーのように多弁なログを大量に流すと、取り込みだけでもそれなりに積み上がります。「書き込みは多いが読むのはたまに」という今回のパターンだと、Loki + S3 のストレージ中心モデルのほうが読みが立てやすい。導入のしやすさでは勝っていましたが、コスト構造で Loki に譲った形です。 残った Loki + S3 が、いちばん素直にハマった 対する Loki + S3 は、課金の中心が S3 のストレージ代+コンピュート です。Loki はログ本体を圧縮した chunk として S3 に置き、ラベルの index だけを別に持ちます。そのため、indexing のようなイベント単位の課金軸がなく、量が増えてもコストが急激に膨らみにくい構造です。 ただし Loki + S3 もタダ同然ではありません 。S3 には PUT/GET/LIST のリクエスト課金がありますし、Loki はクエリのたびにキャッシュになければ S3 から chunk を読むので、調査が増えれば読み取り側のコストが乗ります。「量に比例する軸がゼロ」ではなく、 効く軸が indexing からリクエスト・取り出しに移る 、が正確なところです。それでも「書き込みは多いが読むのはたまに」というパターンでは、読み取り側のコストは限定的です。 主な課金軸 効き方・調整余地 Datadog Logs 取り込み(GB) + インデックス(イベント数×リテンション) index 量・保持に比例。filter 等で抑えられるが、その設計・運用がコストになる CloudWatch Logs 取り込み(GB) + Logs Insights スキャン(GB) 書き込みが多いと取り込みが積み上がる。クエリ頻度でもスキャン課金が乗る Loki + S3 S3 ストレージ + リクエスト・取り出し + コンピュート ストレージ中心。保持・ストレージクラスは自分で握れる 料金体系は執筆時点の公開情報をもとにした概略です。割引やコミット契約でも変わるので、最新は各サービスの料金ページでご確認を。 加えて、Loki には 導入のしやすさと k8s 相性 という後押しもありました。EKS 内に Helm で立てて完結するので、社内承認を巻き込まずチーム裁量で始められます。そして Loki は Grafana エコシステムの一部で、ノードの /var/log/pods を読む DaemonSet から取り込む構成が公式の本線として整っています。ラベルベースの検索モデルは namespace / pod / container といった k8s メタデータとそのまま対応するので、 {namespace="arc-runners", container="manager"} のような絞り込みが自然に書けます。 ※「導入が楽」は運用フリーという意味ではありません。「新規 SaaS の調達フリクションを回避できる」という意味での導入のしやすさで、立てたあとは自分たちで面倒を見る前提です。そのトレードオフを承知のうえで、今回の規模・用途なら割に合う、という判断でした。 メトリクス基盤:マネージド Prometheus か、自前か ログ基盤に Loki を選んでいるので、可視化には同じ Grafana エコシステムの Grafana が相性がいい。メトリクス基盤も Prometheus で揃えれば、ダッシュボード上でログとメトリクスをシームレスに行き来できます。加えて、ARC は gha_* メトリクスを Prometheus 形式( /metrics エンドポイント)で公開しているので、これを scrape するなら Prometheus が自然な選択です。 悩んだのはマネージド(AMP 等)か自前かですが、 今回は自前 OSS Prometheus を選びました 。マネージドは運用を丸ごと預けられるぶんラクですが、 請求の大半を占めるのが取り込み(サンプル量)で、ストレージ代はごく一部 という構造です。取り込み課金は保持期間とは独立して発生するので、保持を短くしてもコストはたいして下がりません。今回の前提は「 1週間保持で十分 」「 見るのはうちのチームだけ 」なので、自前なら EBS を1本ぶら下げるだけで済み、取り込みの従量課金も乗らない。短期保持・内部用途という条件では、素朴な自前 Prometheus が最もシンプルで安かった、という判断です。 料金体系の概略です。最新は各サービスの料金ページでご確認ください。 収集エージェント:Alloy か、それ以外か 最後に、ログとメトリクスを集めて Loki / Prometheus に送る収集エージェントです。 エージェント 特徴 状態 Grafana Alloy ログ・メトリクス・トレースを1つで扱える。Loki 公式が前提に置いている 現行推奨 Promtail Loki 公式の軽量ログ専用エージェント 非推奨(2026年3月に EOL) Grafana Agent Alloy の前身 Alloy に統合され EOL 済み Fluent Bit 軽量で実績豊富なログフォワーダー 現行(ただし Grafana 公式の本線ではない) 今回選んだのは Grafana Alloy です。決め手は、Loki 公式が標準エージェントとして Alloy を位置づけており、ドキュメントも Alloy 前提で整備されていて互換性の問題が起きにくいことです。加えて、 ログとメトリクスの scrape を1つの DaemonSet で兼ねられる こと、将来トレースやプロファイルに拡張する余地があることも理由です。デメリットとしては、設定が独自構文(パイプライン形式)で学習コストがあること、比較的新しくコンポーネントによっては experimental なこと、が挙げられます。また、Fluent Bit は C 言語で書かれたログ専用エージェントでメモリ消費が非常に小さいのに対し、Alloy は複数シグナルを扱うぶんメモリ消費が大きくなります。 実装:どう組んだか runner Pod (ephemeral) controller / listener kubelet, kube-state-metrics, node-exporter │ ▼ Alloy (DaemonSet, 各ノード) ├─ /var/log/pods を読む (ログ) └─ 各 /metrics を scrape (メトリクス) │ ├──[ログ]──> Loki (Monolithic, 1 replica) ──> S3 └──[メトリクス]──> Prometheus (1 replica, EBS 永続化) │ ▼ Grafana ├─ Loki データソース (ログ) └─ Prometheus データソース (メトリクス) 各ノードに DaemonSet で置いた Alloy が、ログ収集とメトリクス scrape の両方を担います。ログは k8s がノードの /var/log/pods/ に書き出しているものを Alloy が読み続けて Loki へ送ります。 Pod が消えてもログファイルはノードに残っているし、そもそも消える前にもう送信済み なので、ephemeral runner でも取りこぼしません。メトリクスは ARC controller-manager / listener の gha_* 、kubelet(cAdvisor)、kube-state-metrics、node-exporter を scrape して prometheus.remote_write で Prometheus に送っています。 なお、今回自分が担当したのは選定・設計・検証までで、本番環境への構築は、6月に入社した小泉( @naotoko_ )が担当してくれました。導入にあたってはいろいろとハマりどころがあったそうなので、その点は続編として書く予定です。お楽しみに。 まとめ Grafana を開けば、ログもメトリクスも同じ画面から引けるようになりました。ログは {namespace="arc-runners", container="manager"} |= "error" で絞り込めますし、メトリクスは gha_controller_pending_ephemeral_runners でランナー群の状態を常時眺められます。何より、 もう Pod が消える前にログを掴みにいかなくていい 。あの kubectl logs -f の張り込みから解放されたのが、体感としていちばん大きいです。 今回いちばん伝えたかったのは、構築手順よりも 「何を基準に選んだか」 のほうです。Datadog か OSS か、マネージドか自前か——一般論としての正解はなくて、 コスト構造・保持期間・誰が見るのか・撤去しやすさ・導入の手続きの重さ といった軸に、自分たちの状況を当てはめて初めて答えが決まります。今回は「内部用途・短期保持・自チーム管轄」という前提だったからこそ自前 OSS スタックにハマりました。全社で見るログや、自前運用のリスクを持ちたくない場面であれば、マネージドや Datadog を選ぶという選択肢も十分あると思います。 似たような観測基盤の選定で迷っている方の、判断の足しになれば嬉しいです。
こんにちは、タイミーでバックエンドエンジニアをしている 福井 (bary822) です。 タイミーのバックエンドは巨大な Rails のモノリスアプリケーションです。以前から「アクセスが集中する特定のテーブル(以下、人気テーブル)への DB マイグレーションが日中に通らない」という問題を抱えており、看過できないレベルになってきたため、本格的に対処に乗り出しました。 この記事では、原因となっていたロングトランザクションに対し、Datadog と Devin を組み合わせた自動修正フローで対処した話と、その設計の裏側を紹介します。 DBマイグレーション失敗のメカニズム 日常的に発生していたのは、人気テーブルへの ALTER TABLE が日中はほぼ通らない、という状況でした。原因は メタデータロック (MDL) です。 Aurora MySQL(8.0) では、SELECT / INSERT / UPDATE / DELETE などの DML が対象テーブルの MDL(共有ロック)を取得する MDL はテーブルなどのメタデータに対して取得されるロックであり、共有 MDL が保持されている間は ALTER TABLE に必要な排他 MDL を取得できずロック待ちになる MDL が解放されるまで ALTER TABLE はブロックされるため、1 本でも長い時間走るトランザクション(以下、ロングトランザクション)があると、その裏で ALTER TABLE がタイムアウトしてしまう つまり、クエリ実行頻度の高い人気テーブルほど日中は触れなくなり、「カラムを別テーブルに切り出す」「カラム、インデックスの削除を諦める」といった、技術的制約が設計を歪める方向に力学が働き始めていました。 このマイグレーション失敗そのものに対しては、これまで strong_migrations gem のロック取得リトライ機能( lock_timeout_retries など)で何とか対策してきました。しかし、これらはあくまで成功確率を上げる ための投機的なアプローチにとどまり、根本原因であるロングトランザクションそのものには手を入れられていませんでした。 ロングトランザクション修正の方針 これまで見てきた通り、根本原因はロングトランザクションそのものです。そこで、リトライで凌ぐ運用から一歩踏み込んで、いよいよロングトランザクション自体を減らしていく方向に舵を切ることにしました。 とはいえ、現時点において目立ったロングトランザクションを頑張って解消したとしても、今後開発者が意図せず新たなロングトランザクションを生み出してしまう可能性は大いにあります。 かといってマージ前にロングトランザクションを検出するのも現実的ではありませんでした。トランザクションの長さは、多くの場合そのレコード(スキャン)量に依存しており、本番で実行してみるまで検知しにくいからです。 そこで本番リリース前の検知は諦めて、リリース後にできるだけ早く検知する方針にしました。また、検知から修正、レビューまでをできるだけ自動化し、人間は最終判断要員として介入するだけで済む状態にすることで持続可能な運用を目指すことにしました。 仕組みの全体像 上記方針をもとにいくつかのプランを検討した結果、タイミーで既に導入されていた Datadog、Devin などを組み合わせ、以下の 5 フェーズからなる自動化フローを構築しました。 準備: ActiveRecord Query Logs を有効化し、クエリの発行元がSQLコメントとして埋め込まれるようにしておく 観測: Datadog Agent から本番 DB に対して定期クエリを実行し、 performance_schema と information_schema の情報をもとに、テーブルごとにMDLを取得するロングトランザクション時間をカスタムメトリクスとして Datadog に送信する テーブルごとにMDLを保持しているトランザクションのうち、計測時点で最も時間が長い秒数を記録する 検知: Datadog Monitor にてテーブルごとに一定のしきい値を超えるロングトランザクションを検知する 修正 : Datadog Monitor で発火されたアラートをトリガーとして、Datadog Workflow Automation を起動。コンテキストを整理して GitHub Actions 経由で Devin Session を起動し、修正 PR を作成 レビュー : 「修正対象のコードに詳しい人」を自動的に判定してアサイン + AI による事前レビュー ロングトランザクション修正フローの構成図 以下、それぞれのフェーズで工夫したポイントを紹介します。 準備: クエリの発行元を明らかにする Rails 7 から標準提供されている ActiveRecord Query Logs には豊富なオプションが用意されており、クエリの発行元をコメントとして付与する対象を限定することができます。 https://railsguides.jp/v8.1/configuring.html#config-active-record-query-log-tags タイミーでは次の設定を入れています。 config.active_record.query_log_tags_enabled = true config.active_record.query_log_tags = %i[namespaced_controller action sidekiq_worker rake_task] 観測: ロングトランザクション発生状況を可視化する MySQL では performance_schema と information_schema の情報を組み合わせることで「テーブルごとのその時点で実行されている最も長いMDLを取得するトランザクション」を特定することができます。 さらにクエリコメントとして付与された発行元の情報を組み合わせることで「どこから実行されたトランザクションが何秒実行されているか」が特定可能になります。 次の例では、テーブル名を table_name 、クエリの発行元を query_source として取得しています( query_source は、実際の出力を見ながら扱いやすいように加工している)。 計測クエリ例 SELECT table_name, CASE WHEN raw_sql LIKE '%namespaced_controller:%' THEN CONCAT( TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(raw_sql, 'namespaced_controller:', -1), '*/', 1), ',', 1)), '#', TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(raw_sql, 'action:', -1), '*/', 1), ',', 1)) ) WHEN raw_sql LIKE '%sidekiq_worker:%' THEN TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(raw_sql, 'sidekiq_worker:', -1), '*/', 1), ',', 1)) WHEN raw_sql LIKE '%rake_task:%' THEN CONCAT('rake:', TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(raw_sql, 'rake_task:', -1), '*/', 1), ',', 1))) ELSE 'unknown' END AS query_source, tx_duration_seconds AS max_tx_duration_seconds FROM ( SELECT CASE WHEN ml.OBJECT_NAME LIKE '#sql-%' THEN 'DDL_IN_PROGRESS' ELSE ml.OBJECT_NAME END AS table_name, COALESCE(esc.SQL_TEXT, it.trx_query, '') AS raw_sql, TIMESTAMPDIFF(SECOND, it.trx_started, NOW()) AS tx_duration_seconds, ROW_NUMBER() OVER ( PARTITION BY CASE WHEN ml.OBJECT_NAME LIKE '#sql-%' THEN 'DDL_IN_PROGRESS' ELSE ml.OBJECT_NAME END ORDER BY TIMESTAMPDIFF(SECOND, it.trx_started, NOW()) DESC ) AS rn FROM performance_schema.metadata_locks ml JOIN performance_schema.threads th ON ml.OWNER_THREAD_ID = th.THREAD_ID JOIN information_schema.innodb_trx it ON th.PROCESSLIST_ID = it.trx_mysql_thread_id LEFT JOIN performance_schema.events_statements_current esc ON th.THREAD_ID = esc.THREAD_ID WHERE ml.OBJECT_TYPE = 'TABLE' AND ml.OBJECT_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys') AND TIMESTAMPDIFF(SECOND, it.trx_started, NOW()) >= 5 ) ranked WHERE rn = 1 このクエリを何かしらの方法で本番DBに対して定期的に実行し、その結果をどこかに貯めておけばロングトランザクション発生状況を可視化できます。 Datadog ではこれを簡単に行うことができました。アプリケーションが実行されているものとは別のサービスとして ECS 上で常時稼働している Datadog Agent にて定期的にクエリを実行し、その結果をカスタムメトリクスとして Datadog に送信しています。 Aurora MySQL での設定方法: https://docs.datadoghq.com/ja/database_monitoring/setup_mysql/aurora 検知: 修正対象のロングトランザクションを絞り込む カスタムメトリクスとして1度 Datadog に取り込んでしまえば、それを使ってアラート(Datadog Monitor)を仕込むことは簡単です。 メトリクスはクエリ発行元( query_source )でグルーピングして監視するようにしました。こうすることで後続のフローに「どのクラス(ファイル)でロングトランザクションが発生したか」を渡せるようになります。 また、発行元が特定できなかったものや定期実行しないバッチなどは対象外としました。 以下が Datadog Monitor のクエリです。( !query_source:rake:tmp:* は定期実行しないバッチを取り除くためのものです) default_zero(avg:custom.mysql.mdl_holder.max_tx_duration_by_table{account:timee-jp-prod,replication_role:writer, !query_source:unknown, !query_source:rake:tmp:*} by {query_source}) しきい値はまずはアラートがノイズにならない程度(後続の修正フローによって作成されるPRのレビューが負担にならない程度)から始めることをおすすめします。 タイミーの場合は当初数百 sec を超えるロングトランザクションが発生していたため、まずは 100 秒をしきい値として設定しました。 この時点でロングトランザクションの発生元が限られている場合は、後続の自動修正フローを構築する前に、まずはそれらだけを対象にいったん修正してみるのも効果的かもしれません。 修正: パターン集で修正アプローチを制御する Datadog Monitor のしきい値超過をトリガーに、Datadog Workflow Automation を起動します。ここでは、Monitor から渡されたロングトランザクションに関する情報(クエリ実行元、発生時間など)を取りまとめ、GitHub Action 経由で Devin Session を起動して、詳細な原因調査と修正PRの作成を行います。 また、数百秒にわたるロングトランザクションでは、Monitor が重複してトリガーされる可能性があります。そのため、同一クエリ発行元に対して Devin Session が重複実行されないようにする必要がありました。具体的には、Session 起動時のタグに query_source を設定し、新しい Session を起動する前に既存の起動有無をチェックして、利用料金の無駄を防いでいます(初期段階ではこのチェックがなく、一夜にして数百ドルかかったことがありました)。 Devin による修正では Datadog MCP 経由で APM などの情報を分析させることで詳細な原因調査を行っていますが、しばらく運用しているうちにロングトランザクションの発生とその修正方法には一定のパターンがあることを発見しました。そこであらかじめ修正パターンをドキュメント化してレポジトリに置いておき、それを Devin に参照させるようにしました。こうすることで調査のアタリをつけやすくなりコンテキストの節約に寄与したり、実行時間を短縮することができました。 修正パターンドキュメント例 # トランザクション内の外部APIコールを排除する ## 概要 トランザクション(`with_lock` / `transaction do`)の内側で外部APIコール(HTTP リクエスト、LLM API、外部 SDK 呼び出しなど)を実行している場合、通信時間の間ずっとMDL(Metadata Lock)が保持され続けます。外部呼び出しの所要時間は秒〜分単位に及ぶことがあり、これがロングトランザクションの**最も典型的な原因**です。 改善の基本方針は、外部呼び出しをトランザクション外に出して **MDL保持時間を最小化** することです。完全な除去ではなく **トランザクションスコープの最小化** を第一選択とし、ロックが守ろうとしていたデータ整合性は別の手段(ステータス管理・楽観的整合性チェックなど)で維持します。 ## 問題のシグネチャ - **コード上の特徴**: - `with_lock do ... end` または `transaction do ... end` の内部に、HTTP クライアント呼び出し(Net::HTTP, Faraday, RestClient など)、AWS SDK 呼び出し、LLM API 呼び出し、メール送信、Slack 通知などが含まれている - 外部呼び出しが完了してから `save!` / `update!` が呼ばれる流れになっている - **APMトレース上の特徴**: - トランザクション開始から終了までのスパン内に、`http.client` / `aws.s3` / `openai.api` 等の子スパンがある - DB クエリの所要時間より外部呼び出しスパンの所要時間のほうが長い - 「DB時間 << 全体時間」のトレースが頻発している ## Before / After ```ruby # Before(外部APIコールがトランザクション内 → MDLを長時間保持) def process with_lock do reload return false unless entered? result = call_external_api! # 外部APIコール → 最大120秒のMDL保持 save_result!(result) end end # After(トランザクションを分離してMDL保持時間を最小化) def process # 短いトランザクション: ステータス確認のみ with_lock do reload return false unless entered? end # 外部APIコールはトランザクション外で実行(MDLを保持しない) result = call_external_api! save_result!(result) end ``` ### 楽観的整合性チェックの追加(再enqueueパターンがある場合) 対象の処理が「データ変更時に再enqueueされる」設計の場合、以下のリスクが生まれます: - Worker A がデータ読み込み後にトランザクションを終了 - レコードが更新され Worker B が enqueue - Worker A が古いデータで重い処理を続行 - Worker B が新しいデータで上書き(結果整合性は保たれるが Worker A の処理は無駄になる) このリスクを緩和するため、トランザクション終了後に再enqueueトリガーと同じ変化検知ロジックでデータの鮮度を確認し、変化があれば中断する楽観的チェックを追加します。 ```ruby # トランザクション内でスナップショット取得 before_checker = SomeChecker.new(record) data = load_data_in_transaction # トランザクション外で鮮度確認(重い処理の前) current_record = Record.includes(...).find(id) return if before_checker.changed?(current_record) # Worker Bに任せる # 重い処理を実行 process(data) ``` ## 効果 - MDL保持時間が **秒〜分単位** で短縮される(外部呼び出しの所要時間ぶん) - ロングトランザクション(長時間 MDL 保持)アラートの発火回数が大幅に減少することが期待される - 同テーブルへの他アクセス(マイグレーション・更新クエリ)の待ち時間も短縮される ## 注意点・トレードオフ - **排他制御が弱まる可能性**: トランザクション外に出すことで排他制御が弱まる場合があります。 ` retry: false ` の Sidekiq Worker など、同一レコードが同時処理されるリスクが低い場合は許容できます - **堅牢化の選択肢**: より堅牢にするには、トランザクション内でステータスを ` processing ` に変更してから外部呼び出しを行うパターンが有効です(スキーマ変更が必要な場合は別PRで対応) - **楽観的整合性チェックの適用条件**: 対象レコードの更新が同一Workerの再enqueueをトリガーする設計になっている場合のみ必要。再enqueueしない設計では不要です - **完全除去は最終手段**: ロックの完全除去は、保護が不要であることを論理的に説明できる場合にのみ行ってください。経緯( ` git log ` / ` git blame ` )を確認せずに削除すると、過去に修正済みのバグを再発させるリスクがあります ``` # Before(外部APIコールがトランザクション内 → MDLを長時間保持) def process with_lock do reload return false unless entered? result = call_external_api! # 外部APIコール → 最大120秒のMDL保持 save_result!(result) end end # After(トランザクションを分離してMDL保持時間を最小化) def process # 短いトランザクション: ステータス確認のみ with_lock do reload return false unless entered? end # 外部APIコールはトランザクション外で実行(MDLを保持しない) result = call_external_api! save_result!(result) end ``` ### 楽観的整合性チェックの追加(再enqueueパターンがある場合) 対象の処理が「データ変更時に再enqueueされる」設計の場合、以下のリスクが生まれます: - Worker A がデータ読み込み後にトランザクションを終了 - レコードが更新され Worker B が enqueue - Worker A が古いデータで重い処理を続行 - Worker B が新しいデータで上書き(結果整合性は保たれるが Worker A の処理は無駄になる) このリスクを緩和するため、トランザクション終了後に再enqueueトリガーと同じ変化検知ロジックでデータの鮮度を確認し、変化があれば中断する楽観的チェックを追加します。 ``` # トランザクション内でスナップショット取得 before_checker = SomeChecker.new(record) data = load_data_in_transaction # トランザクション外で鮮度確認(重い処理の前) current_record = Record.includes(...).find(id) return if before_checker.changed?(current_record) # Worker Bに任せる # 重い処理を実行 process(data) ``` ## 効果 - MDL保持時間が **秒〜分単位** で短縮される(外部呼び出しの所要時間ぶん) - ロングトランザクション(長時間 MDL 保持)アラートの発火回数が大幅に減少することが期待される - 同テーブルへの他アクセス(マイグレーション・更新クエリ)の待ち時間も短縮される ## 注意点・トレードオフ - **排他制御が弱まる可能性**: トランザクション外に出すことで排他制御が弱まる場合があります。 ` retry: false ` の Sidekiq Worker など、同一レコードが同時処理されるリスクが低い場合は許容できます - **堅牢化の選択肢**: より堅牢にするには、トランザクション内でステータスを ` processing ` に変更してから外部呼び出しを行うパターンが有効です(スキーマ変更が必要な場合は別PRで対応) - **楽観的整合性チェックの適用条件**: 対象レコードの更新が同一Workerの再enqueueをトリガーする設計になっている場合のみ必要。再enqueueしない設計では不要です - **完全除去は最終手段**: ロックの完全除去は、保護が不要であることを論理的に説明できる場合にのみ行ってください。経緯( ` git log ` / ` git blame ` )を確認せずに削除すると、過去に修正済みのバグを再発させるリスクがあります Devin は与えられたコンテキストとパターン集を照らし合わせ、当てはまるパターンがあればこれを参考に修正。なければ新規パターンとしてドキュメントを追加します。 つまり、Devin が直せば直すほど、次の Devin が使えるドキュメントが増えていくループを、リポジトリ内で完結する形で作っています。プロンプトの調整も普通の PR ベースで行えるので、レビュアーからのフィードバックが自然と AI 側の挙動改善に還元されていきます。 レビュー: 「そのコードに詳しい人」を特定する ロングトランザクション修正は、コードの表面的な変更だけでは判断できないケースが多く、実装の意図やドメイン背景を知っている人のレビューが不可欠です。 そこで、次の手順でレビュアーを決めています。 コードオーナーが設定されていれば、その人(チーム)をレビュアーとする なければ、直近 1 年間で最も多くそのファイルに commit したユーザーとその時点での所属チーム 1 年以内に commit がなければ、特定チーム(私が所属するチーム) これはプロンプトベースだと間違ったアサインを行うことがあったため、スクリプト化しました。 さらに、作成された PR に対して AI レビューを実行しています。Devin はレビューに対して自動で対応を行うため、人間レビュアーの目に届く時点で、AI 同士の一次すり合わせは終わっている状態になっています。 運用上のポイント 昨今、コーディングエージェントの性能向上やその周辺ツールの充実により、このような自動修正フローを簡単に構築することができるようになりました。 一方で「作った仕組みを普段の開発フローの中で無理なく運用する方法」をセットで実装することは以前に増して重要になってきたように思います。 今回のケースでは下記3点を特に意識して実装に落とし込みました。 人間の目に触れる前までに無駄を削ること 人間が対応する場合の工数を可能な限り小さくすること 無理なく運用できるペースで継続できること AI による相互レビューで無駄を削る 前述の AI 相互レビューでは次の観点でPRの妥当性を判断しています。 この変更は本当に長時間MDLを生み出すボトルネックにアプローチしているか? この変更が長時間MDLを解消するための必要最小限の変更か? 長時間MDLを解消しつつ、元の振る舞いを極力維持できているか? たとえ修正によってあるトランザクションがMDLを取得する時間が短くなったとしても、それが検出されたロングトランザクションを十分に解消する(アラートが鳴らなくなるレベル)でなければ修正する価値はありません。 また、修正できたとしてもその変更範囲が膨大になってしまえばレビュアーの負荷が高くなり、いつまでもマージできないことで運用が回らなくなってしまいます。 AI レビューでこれらの観点を満たさない場合は PR を クローズする運用を行っています。 「対応しない」ことも選択肢におく 継続的な運用で意外と重要なのが、「対応しない」判断を尊重することです。 Devin が作った PR が、レビュアーの目から見て対応しないと判断されることは普通にあります。多くの場合トランザクションの範囲を小さくしたりトランザクション自体を無くすことはデータの整合性とトレードオフの関係にあるからです。 このとき単にクローズして終わりだと、次に同じクエリ発行元( query_source )でトリガーされたときにまた同じ PR が生成されてしまいます。 これを避けるために、「対応しない」ことがあるという前提で運用を考えました。また、対応しない場合の工数もできる限り小さくなるようにしています。 対応しないものは query_source 単位で Ignore List として管理し、リポジトリに含めておく Ignore List の実体はただの query_source のリスト(フォーマットは JSON、YAML など何でもいい) レビュアーが PR に long-transaction-wontfix ラベルを付けるとGitHub Actions が起動し、それまでの commit を破棄して Ignore List に追加する ⚠️ Ignore List は query_source 単位なので、同じ query_source の別箇所で新たにロングトランザクションが発生しても検知されなくなります。厳密な検知性より運用のシンプルさを優先した割り切りで、必要があれば粒度を後から変えられるようにしています。 しきい値を下げて対象を広げていく ここまでの仕組みは、Datadog Monitor のしきい値(初期構築時は 100 s)を超えたロングトランザクションを対象にしています。運用初期はやや保守的な値に置き、専用のダッシュボードにまとめたロングトランザクション発生状況や作成された修正 PR 数やマージ数、レビュアーの偏りを見ながら、段階的に下げていく運用を行っています。 現在では無理なく運用しながらしきい値を 50s まで引き下げられており、人気テーブルによっては MDL 保持時間が以前の半分以下になりました。 定期観測しているダッシュボード。画面上部のメトリクス(MDL保持時間)が時間が進むにつれて改善されている(短くなっている)ことがわかる おわりに 以前投稿した Flaky Test 自動修正の取り組みとテーマは違いますが、同じようなパターンでロングトランザクションを改善する仕組みの実装と運用ノウハウを紹介しました。 tech.timee.co.jp 今回のケースでは変更によるトレードオフが発生する特性があるため、「対応しない」という選択も同じように尊重する必要がありました。そこでロングトランザクションを駆逐するのではなく、あくまでも現状を緩和することをターゲットに置いたことで現実的に持続可能な運用に落とし込むことができました。 問題の発生を検知し、自動で原因分析から修正 PR の作成まで行うパターンは、他の問題にも適用できる汎用性があります。そのため、ついつい多用したくなってしまいます。しかし、開発サイクルのどこかに人間が介在する限り持続可能な運用に落とし込むことが重要になっていることをあらためて実感しています。 最後までお読みいただき、ありがとうございました!
はじめに こんにちは。タイミーで Platform Engineer をしている小河原( @kgwryk28 )です。 現在、タイミーのシステムで利用しているメインのデータベース(Aurora MySQL)のバージョンアップを進めています。 前回の記事 では、アップグレードに伴う SQL の互換性や性能の検証について共有しました。 この記事では、そのアップグレードと並行して取り組んでいる Aurora MySQL の GTID モード有効化 を行うにあたって直面した課題と、それぞれをどう解決したかを紹介します。 GTID やレプリケーションに詳しくない方にも読んでいただけるよう、必要な前提はその都度補足しながら説明します。 背景 きっかけは、現状利用しているAurora MySQL 3.x 系(MySQL 8.0 相当の互換性)から Aurora MySQL 8.4 系(MySQL 8.4 相当の互換性)へのアップグレードが視野に入ってきたことです。 タイミーでは Aurora MySQL のデータを BigQuery に連携するため、Google Cloud の Datastream を利用しています。 一方、 Datastream の MySQL ソース対応バージョン によると、 MySQL 8.4 は「GTID ベースのレプリケーションでのみサポート」 とされています。 現状 Datastream の接続方式として バイナリログの位置ベース です。そのため、8.4 以降を Datastream のソースにするには GTID ベースのレプリケーションが必須 になります。 つまり、将来のバージョン追従を見据えると、どこかで GTID ベースの接続方式へ移行することは避けられません。 現状 Aurora MySQL では GTID モードが有効化されていないため、その前段として Aurora 側で GTID モードを有効化 しておく必要があります。 これが今回 GTID モードの有効化を行う動機です。 前提 本題に入る前に、この記事を読むのに必要な前提を 3 つ押さえます。 ① GTIDについて GTID(Global Transaction Identifier)は、データベース上でコミットされた各トランザクションにクラスター全体で一意な ID を振る仕組みです。 GTIDモードが有効になるとバイナリログ(binlog)に GTID が記録されます。無効の場合はバイナリログに GTID は記録されません。 GTID は、レプリカとしてバイナリログを受け取った際に『どのトランザクションまで実行したか』を管理するために使われます。 GTIDモード が無効なマスターに対してレプリケーション接続する場合、GTIDは利用できません。そのため、バイナリログのファイルとポジションでどこまで実行されたかを管理します。 本記事では用語を統一するため、以下のように呼びます。 GTIDトランザクション :GTIDが含まれているトランザクション 匿名トランザクション :GTIDが含まれていないトランザクション GTID方式 :レプリカが「どこまで実行したか」を、GTID で管理するか バイナリログの位置ベース方式 : レプリカが「どこまで実行したか」を、バイナリログのファイル+ポジションで管理するか ② 4種類のGTIDモード GTIDモードには4種類の設定値があり、まとめると以下のようになります。 gtid-mode マスターとしての書き出し(出力) レプリカとしての受け入れ(入力) OFF GTID なし バイナリログの位置ベース方式 OFF_PERMISSIVE GTID なし 両方OK(バイナリログの位置ベース方式 / GTID方式) ON_PERMISSIVE GTID 付きで書き出す 両方OK(バイナリログの位置ベース方式 / GTID方式) ON GTID 付きで書き出す GTID方式 注目すべき点は、 OFF_PERMISSIVE と ON_PERMISSIVE が移行用の中間状態として設定できることです。この場合、レプリカ側は GTID方式 と バイナリログの位置ベース方式 のどちらでも接続できます。 Aurora MySQLでは、DBクラスターパラメータグループ の gtid-mode で 設定できます。 ただし、これは Static パラメータ であり、既存のクラスターに適用する場合、クラスター全体(すべてのDBインスタンス)の 再起動が必須 です。 ③ GTIDベースの整合性に関する設定 もう一つGTID に関連する設定値として enforce_gtid_consistency という設定があります。 GTIDモードで安全にレプリケーションできないようなSQLの実行を、エラーにするか許容するかを設定できるパラメータになります。 設定値は以下の3種類から選ぶことができます。 enforce_gtid_consistency GTID 非対応クエリ実行時の挙動 OFF 制限なし WARN 実行は許可、警告ログを出力 ON エラーにして拒否 ON で設定すると以下のようなクエリが実行時にエラーになります。(詳細: MySQL :: MySQL 8.0 リファレンスマニュアル :: 17.1.3.7 GTID ベースレプリケーションの制約 ) CREATE TABLE ... SELECT 構文が含まれるクエリ トランザクション内で CREATE TEMPORARY TABLE または DROP TEMPORARY TABLE 構文が含まれるクエリ トランザクション内で普通のテーブル(InnoDBなど)と一時テーブル(Temporary Table)の同時更新が行われるクエリ Aurora MySQLでは、DBクラスターパラメータグループ の enforce_gtid_consistency で設定できます。 ただしこれも同様に Static パラメータ であり、既存のクラスターに適用する場合、クラスター全体(すべてのDBインスタンス)の 再起動が必須 です。 解くべき 2 つの課題 GTID モードを有効化するにあたり、次の 2 つの課題に直面しました。 一つずつ深掘りしていきます。 課題A :どのようにGTID モードを有効化するか 課題B :どのように Datastream を安全に切り替えるか 課題A: どのようにGTID モードを有効化するか 再起動を回避 前述のとおり既存クラスターに対する gtid-mode の変更にはクラスター全体の再起動が必要です。 今回、GTID モードの有効化は、Blue/Green Deployments を利用しました。 元々、データベースのアップグレードはBlue/Green Deployments で行う想定でした。そこで、作成された移行先環境(Green環境)に別途パラメータグループを用意し、Green環境だけでGTIDモードを有効化します。 これにより現行環境(Blue環境)のデータベース再起動を行わずにスイッチオーバーで切り替えることができます。 Blue/Green で Green 側のパラメータを変更する 今回 Green環境で変更したのは以下の 2 つのパラメータです。 項目 Blue(現行) Green(移行先) gtid-mode OFF_PERMISSIVE ON_PERMISSIVE enforce_gtid_consistency OFF WARN それぞれなぜこの値にしたのかを見ていきます。 gtid-mode の設定のうち、GTID を有効化する値は ON と ON_PERMISSIVE の 2 つのどちらかになります。 今回 Green環境の設定値として ON_PERMISSIVE を選んだのは、GTIDモードが 無効になっている Blue環境からの匿名トランザクションを Green環境で実行できるように許容するためです。 Green環境を ON にしてしまうと、匿名トランザクションを実行できません。そのため、Blue/Green Deployments による Blue環境 から Green環境へのレプリケーションを設定してもエラーになります。 また、GTIDベースの整合性に関する設定である enforce_gtid_consistency は、実行を許可しつつ警告ログに記録する WARN を選択しました。 ON にすると非対応クエリがエラーになり、既存クエリにも影響するリスクがあります。一方 WARN はクエリの成否を変えません。そのため、切り替え時にクエリ互換性の再検証は不要で、適用後は警告ログを基に確認できます。 課題B: どのように Datastream を安全に切り替えるか 切り替え時の課題 当初は、シンプルに次の手順を想定していました。 Datastream を一度停止し、アップグレード(スイッチオーバー)時に RDS のイベントへ出力されるGreen環境のファイルポジションを指定して再開する。 ところが、ステージング環境で検証したところ、 この手順では Datastream を再開できずエラーが発生して停止してしまう ことがわかりました。 根本原因は、スイッチオーバーでクラスターエンドポイントの参照先がBlueからGreenに切り替わることです。その結果、Blue環境とGreen環境ではバイナリログのファイルとポジションに互換性がないため、Datastreamを再開できません。 Managing AWS DMS Tasks with RDS or Aurora Blue/Green Deployments の「How Blue Green switchover affects AWS DMS tasks」セクションに、バイナリログのファイル名とポジションは Blue・Green 間で異なると記載されています。これは DMS のドキュメントですが、ファイル名とポジションが変わるのは DB 側の挙動であるため、Datastream でも同様に問題になります。 【原文】 Because the binary log file names and sequence positions differ between the two instances, DMS can no longer resume from the log position it previously recorded. This causes Full Load + CDC tasks and CDC only tasks to fail or enter an error state. 【日本語訳】 2つのインスタンス間(文脈から、BlueとGreenを指している)でバイナリログのファイル名とシーケンスポジションが異なるため、DMSは以前に記録したログポジションから(キャプチャを)再開できなくなります。これにより、CDCタスクが失敗するかエラー状態になります。 この辺りは少しややこしいので補足します。 前提として、バイナリログのファイル名(例:mysql-bin.000123)とポジション(バイトオフセット)は、各クラスタのライターインスタンスがそれぞれ独立して採番します。 そのため Blue環境 と Green環境 の間では、たとえ同じ「ファイル名+ポジション」であっても、それが指している変更内容(=論理的にどこまで進んだか)は全く別物です。 実際にBlue環境とGreen環境のバイナリログのファイルをそれぞれ確認すると、同一ファイル名のバイナリログは存在するが、ファイルサイズは一致していないことが確認できます。 # Green環境 MySQL [(none)]> SHOW BINARY LOGS; + ----------------------------+-----------+-----------+ | Log_name | File_size | Encrypted | + ----------------------------+-----------+-----------+ | mysql-bin-changelog. 000085 | 42576033 | No | | mysql-bin-changelog. 000086 | 157 | No | | mysql-bin-changelog. 000087 | 157 | No | | mysql-bin-changelog. 000088 | 238579 | No | | mysql-bin-changelog. 000089 | 840588 | No | | mysql-bin-changelog. 000090 | 168156 | No | | mysql-bin-changelog. 000091 | 134237510 | No | | mysql-bin-changelog. 000092 | 134217852 | No | | mysql-bin-changelog. 000093 | 134221756 | No | | mysql-bin-changelog. 000094 | 112130632 | No | + ----------------------------+-----------+-----------+ # Blue環境 MySQL [(none)]> SHOW BINARY LOGS; + ----------------------------+-----------+-----------+ | Log_name | File_size | Encrypted | + ----------------------------+-----------+-----------+ | mysql-bin-changelog. 000085 | 134602837 | No | | mysql-bin-changelog. 000086 | 134429601 | No | | mysql-bin-changelog. 000087 | 134218551 | No | | mysql-bin-changelog. 000088 | 134221393 | No | | mysql-bin-changelog. 000089 | 13935369 | No | + ----------------------------+-----------+-----------+ 一方で Datastream は、停止した時点で「Blue環境 のファイル名とポジションで どこまで読んだか」を記憶しています。切り替え後はエンドポイントの参照先が Green環境 に変わるため、Datastream が握っている Blue環境 のファイル名とポジションを Green環境 のバイナリログに対して解釈してしまうことになります。両者に対応関係がない以上、これは正しく再開できません。 しかも厄介なのは、Green環境のファイルとポジションを指定した場合、ズレた地点から再開してしまいます。ズレ方によって、データの不整合が発生するパターンが2パターンに分かれます。 ① 重複適用:Green 環境の同じファイルポジションが、実際に同期済みの地点より「手前」を指していた場合。すでに適用済みのデータをもう一度流してしまう。 ② 欠落(スキップ):Green 環境の同じファイルポジションが、まだ同期していない地点より「先」を指していた場合。未同期のデータが飛ばされてしまう。 問題点をまとめると以下のようになります。 Blue/Green Deployments で作られた Blue環境と Green環境では、バイナリログのファイルとポジションは一致しない RDS のイベントには、切り替え時点の Green環境 のバイナリログのファイルとポジションが出力される。切り替え時点の Green環境 のポジションから、対応する Blue環境 のポジションを探すのは困難 Datastream を再開する際に、Green環境のポジションを指定すると重複適用または未適用のデータがスキップされてデータの不整合が発生してしまう。 つまり、「Blue/Green Deployments による切り替え後に指定すべきファイルとポジションがわからなくなってしまう」という問題でした。 解決した切り替え手順 2026/07/15 一部訂正とお詫び 本文中に「Green環境では GTID方式 で接続しておく」とありましたが、誤りがありました。 正しくは「Green環境では バイナリログの位置ベース方式 で接続しておく」となりますので、該当箇所を修正いたしました。 Datastream を GTIDモードで接続する場合は、接続先のAurora MySQL で gtid-mode を ON に設定しておく必要があります。 詳細は CDC 用に Amazon Aurora MySQL データベースを構成する | Datastream | Google Cloud Documentation をご確認ください。 ご迷惑をおかけした読者の皆様に深くお詫び申し上げます。 そこで、考え方を変えて 「Datastream が参照するクラスターを固定化する」方針にしました。 Blue/Green Deployments を使用した Datastream の切り替え手順 各 Datastream が 同じクラスターのバイナリログを参照し続けられる よう、接続先を「クラスターエンドポイント」から「ライターエンドポイント(特定インスタンス固定)」に切り替える方針にしました。手順は「切り替え前」「切り替え時」「切り替え後」の3段階です。 Blue/Green Deployments による Green環境への切り替え前 Green環境経由の Datastream の別系統をあらかじめ作成しておく。Green環境では GTID方式 バイナリログの位置ベース方式 で接続しておく 既存の Blue / Green それぞれに接続されている Datastream を 一時停止 しておく。 Blue/Green Deployments による Green環境への切り替え後 Datastream のストリームの接続プロファイルの接続先ホストを変更して再開する。 Blue 系統:クラスターエンドポイント → Blue(切り替え前クラスター)のライターエンドポイント に変更 Green 系統:Green のライターエンドポイント → クラスターエンドポイント に変更 事後作業 Datastream の出力先テーブルを参照しているアプリケーションの参照先を Blue環境から Green環境へ切り替える この手順により、スイッチオーバー後も 各Datastream は同一クラスターを参照し続けられます。その結果、ファイルとポジションの不一致を回避でき、安全に切り替えられます。 Blue/Green Deployments で切り替えた後、Blue環境はクラスターから切り離されるため、変更内容はBlue環境には反映されません。ただし、切り替え後のGreen環境を参照元としてBlue 側からレプリケーション接続を張れば、Green環境の変更内容をBlue環境へ同期できます。 ロールバック用クラスターのレプリケーション方法は、以前の記事である Aurora MySQLのアップグレード後ロールバック方法を検討してみた や AWSの公式ブログ に書かれているため、ここでは説明を割愛します。 これらの手順により、Datastream によるレプリケーション接続を安全に切り替えることができます。 まとめ 今回は、Aurora MySQL の GTID モード有効化方法と、Datastreamを安全に切り替えるための方法を紹介しました。 今回の移行が完了してもゴールではなく、この先には gtid-mode = ON への引き上げ(匿名トランザクションの完全な消化、 enforce_gtid_consistency = ON 化)が続きます。 また徳富さん( @yannKazu1 ) さんが 並行してDatastream 関連のネットワーク周りのリアーキテクチャも行なっております。 詳細は以下の資料をご覧ください。 tcpdump で追う Datastream 障害調査と Transit Gateway × VPN のリアーキテクチャ設計 もし、今回の自分と同じように Aurora MySQL の GTID 化を検討している方にとって、この記事が何らかの参考になれば幸いです。 参考リンク MySQL :: MySQL 8.0 リファレンスマニュアル :: 17.1.6.5 グローバルトランザクション ID システム変数 MySQL :: MySQL 8.0 リファレンスマニュアル :: 19.1.4 オンラインサーバーでの GTID モードの変更 MySQL データベースからデータをストリーミングする | Datastream | Google Cloud Documentation
はじめに こんにちは! タイミーでPlatform Engineerをしている @MoneyForest です。 自分が所属しているチームでは、週一で「観測会」を実施しています。 サービスの負荷状況が分かるダッシュボードを確認したところ、ある時期から Aurora MySQL の Reader CPU 使用率が大きく上昇していることに気づきました。 原因になりそうな処理は見えてきましたが、関連する機能はすでに利用されていたため、単純にリバートできる状況ではありませんでした。そのため、インスタンスを追加して一時的にしのぎつつ、根本的な改善を進める必要がありました。 この記事では Reader CPU 急増への対応を題材に、Platform Team が Datadog で事実を収集し、Stream-aligned Team(機能開発チーム)と協力して性能改善を進めた流れを紹介します。 1. Reader CPU が急増し、単純なリバートでは解決できなかった ある時期から、Aurora MySQL の Reader CPU 使用率が通常時よりも上昇しました。 まずはサービスへの影響を避けるため、 Reader インスタンスをスケールアップし、必要に応じて追加する暫定対応を行いました。 ただし、これはあくまで暫定対応です。この状態が長期化するとコスト面で健全ではないため、並行して根本原因の特定に着手しました。 最終的にポイントだったのは、原因である機能を簡単に止められる状況ではなかったことです。その機能はすでに利用されており、利用増加に伴って、もともと非効率だった処理が顕在化した形でした。 そのため、機能として必要な振る舞いを保ちながら、処理をどう改善できるかを見極める必要がありました。 2. Datadog Notebook で事実をまとめ、温度感を揃えた 最初に行ったのは、Datadog Notebook に事象をまとめることでした。 CPU が高いこと、重そうなクエリがあること、暫定対応した時期、関係していそうな処理といった情報が Slack 上に散らばったままだと、認識が揃いません。 特に、「どれくらい危ない状況なのか」「暫定対応の結果どうなったのか」「恒久対応をどう進めるべきか」「どのチームに何を相談したいのか」を一箇所にまとめないと、適切な温度感が伝わりづらくなります。 そこで Datadog Notebook に、以下のような情報を集約しました。 観点 Notebook で整理した内容 添付したウィジェット・リンク 起きていること Reader CPU が通常時より大きく上昇し、いつ障害になってもおかしくない水準まで到達していた Reader CPU の推移が分かるグラフ 負荷の変化 特定クエリの実行頻度が大きく増加し、1回あたりの実行時間や走査行数も高い状態だった クエリ実行頻度、実行時間、走査行数のグラフ 暫定対応 サービス影響を避けるため、Reader インスタンスを追加して一時しのぎしていた 対応時刻やインスタンス追加・入れ替えの時系列 原因の仮説 ある機能の作成件数増加と、クエリ自体の構造的な重さが重なって Reader CPU に影響していそうだった 作成件数増加の時系列、DBM の Query Signature へのリンク 問題の分解 負荷に寄与しているクエリは複数あり、求人作成・更新時に走るものと、毎時の定期バッチで走るものに分けて確認した Query Signature ごとの DBM リンク、APM のトレースリンク 改善方針 一方はクエリ自体の書き換えが必要で、もう一方は既存の結果テーブルを参照する形に変えられる可能性があった 発行元の処理名、トレース、DBM / APM のリンク 相談したい判断事項 既存の結果テーブルを使うと対象者の範囲が変わる可能性があるため、機能仕様として許容できるかを Stream-aligned Team に確認したかった 判断に必要な調査メモと、根拠となる DBM / APM のリンク このとき意識したのは、単にダッシュボードのようにグラフを列挙するのではなく、 関係者が判断できる形に情報を並べること です。 CPU 使用率や重いクエリのメトリクスだけでは、「いま何が起きているのか」は分かっても、「どれくらい急ぐべきか」「誰に何を相談したいのか」「暫定対応で耐えられるのか」は伝わりません。 また、原因仮説は Platform Team 側で、Datadog から見えた処理名や実行タイミングを手がかりに、関連する変更履歴や実装を確認しながら立てていきました。その際はAI も活用し、どの機能・処理と関連していそうか当たりをつけました。 3. DBM / APM で重い Query Signature と呼び出し元を特定した Reader CPU の上昇など、データベースのリソース使用状況の悪化は Metrics で把握できます。しかし、それだけでは何を直せばよいかは分かりません。 そこで次に、どのクエリが Reader に負荷をかけているのかを調べました。 mysql.queries.time と query_signature ここで見たのが、Datadog DBM の mysql.queries.time メトリクスと、そのラベルである query_signature です。 mysql.queries.time は、正規化されたクエリごとの実行時間を表すメトリクスです。ざっくり言うと、 1回あたりの実行時間 × 実行回数 に近い値として見ることができます。 query_signature は、SQL の具体的な値を取り除いて正規化したクエリの識別子です。条件に入る ID や日時だけが異なる SQL を、同じ種類のクエリとしてまとめて確認できます。 ただし、これは直接的に「クエリ単体の性能」だけを表すものではありません。1回あたりは速いクエリでも、実行回数が急増すれば mysql.queries.time は増えます。 逆に、1回あたりが遅いクエリでも、ほとんど実行されなければ全体負荷への寄与は小さく見えます。 一方で、今回のように「Reader CPU が上がっている」という事実に対して、「どの種類のクエリが寄与していそうか」を特定するには、非常に有用なメトリクスです。 調査は、次の流れで進めました。 Metrics で Reader CPU の上昇タイミングを見る mysql.queries.time から相関関係のある query_signature を見る Count / AVG Duration / Rows Scanned を見て、実行回数と1回あたりの重さを確認する DBMのUpstreamからAPMをたどり、そのクエリがどの処理から呼ばれているのかを確認する この調査により、クエリの種類と、その呼び出し元の処理を特定できました。 具体的には、ある機能に関連する非同期処理から発行されるクエリが増加していました。対象データ量の増加に伴って、複数の条件を組み合わせた抽出処理が重くなり、Reader 負荷に大きく寄与している状態でした。 4. Datadog だけでは分からないことを Stream-aligned Team と確認した どのクエリが重いか、いつ実行されているか、どの処理から呼ばれているかは分かりました。 しかし、Datadog だけでは以下は分かりません。 その処理は何の機能のために存在しているのか 抽出対象のデータは、機能上どのような意味を持つのか 他のデータやキャッシュされた結果が使えるのか 機能利用がなぜ増えているのか 最終手段として、機能制限や一時停止が取り得るのか ここで、機能開発を担当する Stream-aligned Team のドメイン知識が必要になりました。 Platform Team 側では Datadog を見ながら負荷の原因を整理し、Stream-aligned Team 側では仕様や処理の中身を確認しました。Slack やハドル、Datadog Notebook で状況を共有しながら、「どの処理が負荷に寄与しているのか」「仕様を壊さず処理を変えられるのか」「もし改善しない場合に、停止などの措置は取り得るのか」を相談しました。 今回重要だったのは、技術的な事実だけでなく、「この状況をどれくらい危険と見ているか」を共有することでした。 Platform Team では、今回の CPU 負荷上昇に対して暫定対応として Reader インスタンスのスケールアップと追加を行いました。 この対応により CPU 使用率を一定以下に抑えることができたため、サービス影響を抑えることはできました。 しかし、依然として以下の問題を抱えていました。 CPU負荷上昇の原因となっているクエリ発行元の機能の利用者は増加傾向にあり、サービス影響が生じる可能性がある。 スケールアップ対応により、インスタンス使用量のコストが対応前より増加している。もともと想定していたDBにかかる費用を超過しているため、コストを抑えたい。 利用増は外部要因の影響もあり、こちらで直接コントロールしづらい状況でした。この背景を踏まえ、機能開発を担当していたチームに、数日で直す必要があることを伝えました。 一方で、機能を担当するチームから見ると、単に「このクエリが重い」と言われても、それがどれくらい急ぎなのか、すぐ直すべきなのか、仕様変更や一時停止まで検討すべきなのかは判断しづらいはずです。 このときの会話は、観点ごとに整理すると以下のようになります。 観点 Platform Team が確認したこと Stream-aligned Team と確認したこと 認識合わせ Notebook にまとめた経緯に認識齟齬がないか 認識齟齬がなく正しそうであること 背景・要因 負荷増加がどの処理と関連していそうか 機能利用の増加が関係ありそうなこと 改善方針 重い処理を改善できないか クエリの修正が可能そうであること ワーストケース 機能制限や一時停止を最終手段として取り得るか 事業部との調整が必要そうであること この会話によって、単なる技術調査ではなく、障害リスク・コスト・仕様影響・事業影響について議論できるようになりました。 最終的には、Stream-aligned Team が仕様上の判断をしたうえで、重い抽出処理を避ける方針やクエリ自体の改善を進めてくれました。 改善前は、複数の条件に該当する対象者を SQL 側で一度に抽出していました。その結果、同じ対象集合を使うサブクエリが複数回展開されたり、複雑な条件が組み合わさったりしていました。対象データ量や実行頻度が増えるにつれて、この構造が Reader に大きな負荷をかける要因になっていました。 改善後は、仕様の互換性を保ちながら、処理をいくつかの小さな取得に分け、アプリケーション側で結果を統合する形に変更しました。これにより、SQL 側で複雑な条件を一度に処理させる必要がなくなり、Reader にかかる負荷を抑えられるようになりました。 5. 改善後も Datadog で効果を確認した クエリを修正すると、Datadog DBM 上の Query Signature が変わります。 そのため、改善前後を見るときに、単純に同じ Query Signature の before / after だけを見ても判断できません。今回も、修正後に対象クエリが分割され、新しい Query Signature が増えました。 そこで、以下の観点で改善効果を確認しました。 変更前の重い Query Signature が減っているか 変更後に増えた Query Signature の AVG Duration は許容範囲か Total Duration は減ったか 対象となる処理の実行時間は改善したか Reader CPU 使用率に変化があったか 結果として、修正前に時間がかかっていたケースが、修正後は主要なクエリで大きく改善していることを確認できました。また、翌日に改めてメトリクスを確認すると、デプロイ以降で Reader CPU にも改善傾向が見られました。 一方で、Reader 全体の実行クエリ数も増えていたため、残る負荷はクエリ単体の問題ではなく、ワークロードの増加として切り分けました。ここまで判断できると、次はアプリケーション改善ではなく、キャパシティやコストの議論として扱えます。 6. まとめ 今回の対応で重要だったのは、重いクエリを見つけることだけではありませんでした。 本番サービスでは負荷の原因になっている機能は単純に止められないことがあります。だからこそ、Datadog DBM / APM / Metrics で負荷を分解し、Datadog Notebook に時系列・メトリクス・仮説・暫定対応・相談したい判断事項をまとめることで、関係者が同じ事実を見ながら会話できる状態を作りました。 そのうえで、Platform Team が整理した事実やリスク、温度感を共有し、受け取った Stream-aligned Team は機能仕様・実装方針・事業影響を踏まえて改善を進めてくれました。 性能課題は、重いクエリを見つければ終わるものではありません。どの負荷が危険で、どの判断が必要で、誰のドメイン知識が必要なのかを整理し、事実を整理して進めることが重要です。 タイミーではこのように、Platform Team と Stream-aligned Team が協力しながら、サービスの成長に伴って生まれる性能課題に向き合っています。
はじめに こんにちは。プラットフォームエンジニアリングチームに所属している徳富( @yannKazu1 )です。 「インスタンスサイズを上げたらコストが下がりました」と言うと、だいたい「?」という顔をされます。スペック上げたらお金かかるに決まってるだろ、と。自分もそう思っていたので気持ちはわかります。 この記事では、Amazon Aurora のReaderインスタンスを db.r7g.8xlarge から db.r7g.12xlarge にスケールアップした結果、I/Oコストが大幅に減り、トータルのAuroraコストがむしろ下がった話を書きます。バッファプールの仕組みと、判断の経緯もあわせて紹介します。 前提:私たちのAurora構成 まず、当時のAurora(MySQL互換)クラスターの構成を簡単に紹介します。 インスタンス クラス vCPU メモリ プロセッサ 役割 reader-1 db.r7g.8xlarge 32 256 GiB Graviton3 API Reader reader-2 db.r7g.8xlarge 32 256 GiB Graviton3 API Reader reader-3 db.r7g.8xlarge 32 256 GiB Graviton3 API Reader writer-candidate-1 db.r5.24xlarge 96 768 GiB Intel Xeon Writer Candidate / Reader writer-candidate-2 db.r5.24xlarge 96 768 GiB Intel Xeon Writer Readerは3台構成で、3つのAZに分散配置しています。加えて、writer-candidate-1もWriter Candidateとしてフェイルオーバーに備えつつ、Readerとして読み取りクエリも処理しています。つまり、読み取りは実質4台で分散している構成です。 バッファプールとは何か 本題に入る前に、今回のキーワードである「バッファプール」について少し説明させてください。 MySQLやAurora(MySQL互換)には InnoDB バッファプール と呼ばれるメモリ領域があります。これはデータベースが頻繁にアクセスするデータやインデックスをメモリ上にキャッシュしておく仕組みです。 データベースがクエリを処理するとき、必要なデータがバッファプールに載っていれば、メモリから直接読み取れるので非常に高速です。一方、バッファプールに載っていないデータが必要になった場合は、ストレージ(ディスク)からデータを読み込む必要があります。これが ストレージI/O です。 ここで重要なのが バッファプールヒット率 という指標です。これは「データベースが必要としたデータのうち、バッファプールから取得できた割合」を示します。 ヒット率100% :すべてのデータがメモリ上にあり、ディスクへのアクセスが発生しない理想的な状態 ヒット率99.9% :一見ほぼ完璧に見えますが、0.1%のミスが積み重なると、秒間数千〜数万回のI/Oリクエストに化けることがあります Aurora ではバッファプールのサイズはデフォルトでインスタンスメモリの75%が割り当てられます。つまり、256 GiBのメモリを持つ db.r7g.8xlarge では、約192 GiBがバッファプールとして使われる計算です。 そしてAuroraの料金体系において見逃せないのが、 I/Oコストは従量課金 であるという点です。ストレージへの読み取りリクエスト(Read IOPS)が増えれば増えるほど、課金額も増える。つまりバッファプールヒット率の僅かな低下が、じわじわとコストに効いてくるわけです。 異変に気づく:「たった0.1%」の落とし穴 事の発端は、以前から認知しつつも対応の優先度を上げきれていなかった、バッファプールヒット率の数字でした。 Readerインスタンスのバッファプールヒット率は、もともと99.9%前後で推移していて、100%には達していませんでした。「99.9%ならほぼ問題ないのでは?」という感覚で、それまで明確な対策は打てていなかった、というのが正直なところです。 しかし、弊社のサービスは非常にアクセス数が多く、それに伴うクエリの総量も膨大です。母数が大きいと、たった0.1%のバッファプールミスでも、ストレージへの問い合わせ回数は凄まじい量になります。 実際にCloudWatchで AuroraStorageReadIOsPS (Aurora ストレージへの秒間読み取りI/O数)を確認すると、reader-1では最大約13,000 IOPS、平均でも約7,488 IOPSに達していました。 そしてこの数字は 1インスタンスあたり の値です。同じスペックのReaderが3台あるため、クラスター全体では単純計算で最大約39,000 IOPS、平均でも約22,000 IOPSものストレージ読み取りが発生していたことになります。AuroraのI/Oコストは従量課金なので、この3台分のI/Oがそのままコストに跳ね返っていました。 「パラメータをいじる」という選択肢はなかったのか 「バッファプールを大きくしたいなら、 innodb_buffer_pool_size パラメータを変更すればいいのでは?」という発想は当然あります。 たしかに、Auroraのパラメータグループからバッファプールサイズを変更すること自体は可能です。デフォルトではインスタンスメモリの75%が割り当てられていますが、これを80%や85%に引き上げれば、インスタンスサイズを変えずにバッファプールを拡大できます。 しかし、この方法にはリスクがあります。バッファプール以外にも、MySQLの内部処理やOS、各種バックグラウンドプロセスがメモリを使用しています。バッファプールの比率を引き上げすぎると、これらに必要なメモリが不足し、最悪の場合OOM(Out of Memory)でインスタンスがクラッシュする可能性があります。 本番環境のReaderで「メモリ配分を攻めた結果、突然落ちました」では笑えません。デフォルトの75%という設定は、こうしたリスクを考慮した上での安全なバランスであり、ここを変更するのは慎重にならざるを得ませんでした。 背中を押した「CPU 100%」 パラメータ変更は避けたいが、状況は悪化していく——そんな中、事態は急変しました。 もともとパフォーマンスの良くないクエリを使う機能が存在していたのですが、それまではあまり利用されていませんでした。しかし、ビジネスサイドの施策推進により、その機能の利用が急増。約2週間のうちにDBのCPU使用率が急上昇し、ついにCPU 100%に張り付く場面が出てきたのです。 ここに至って、インスタンスサイズの引き上げを決断しました。 db.r7g.8xlarge (256 GiB)から db.r7g.12xlarge (384 GiB)への変更です。 メモリが256 GiBから384 GiBに増えることで、バッファプールもデフォルトの75%換算で約192 GiBから約288 GiBへと拡大されます。vCPUも32から48に増えるため、純粋なCPU処理能力の向上も期待できます。 当然ながら、インスタンス単価は上がるので、コスト増は覚悟の上での判断でした。 予想外の結果:コストが「減った」 12xlargeへの変更後、メトリクスの変化は劇的でした。 まず、バッファプールヒット率がほぼ100%に回復しました。これは期待通りの結果です。バッファプールの容量が増えたことで、これまでキャッシュに載りきらなかったデータもメモリ上に保持できるようになったためです。 それに伴い、 AuroraStorageReadIOsPS が急激に減少しました。バッファプールから直接データを返せるようになったため、ストレージへの読み取りリクエストがほとんど発生しなくなったのです。 そしてここからが本題です。具体的なコストの数字を見てみましょう。 まず、インスタンスの日額単価の比較です。 インスタンスクラス 日額単価 db.r7g.8xlarge(3台分) $383.33 db.r7g.12xlarge(3台分) $574.92 Reader 3台合計で見ると、インスタンス料金は約$192/日増加します。普通に考えれば「やっぱり高くなるじゃないか」という話です。 しかし、スケールアップ前のI/Oコスト( APN1-Aurora:StorageIOUsage )を見ると、 毎日$350〜400ほど が発生していました。3台のReaderがそれぞれ秒間数千IOPSものストレージ読み取りを行っていた結果、I/Oの従量課金だけでこれだけの金額が積み上がっていたのです。 12xlargeへの変更後、このI/Oコストは約$80/日まで激減しました。以下のAuroraクラスター全体のコスト推移グラフを見ると、変化が一目瞭然です。 6月15日前後を境に、紫色(db.r7g.8xlarge)がオレンジ(db.r7g.12xlarge)に置き換わると同時に、それまで毎日存在していた赤色の帯——StorageIOUsageがほぼ消滅しています。インスタンス単価の上昇分よりも、I/Oコストの削減幅の方が大きかったため、結果として Auroraのトータルコストはスケールアップ前よりも下がりました 。 「スペックを上げたのにコストが下がる」という一見矛盾した結果ですが、Auroraの料金構造を考えれば理にかなっています。Auroraのコストは大きく分けて「インスタンス料金」と「I/O料金」で構成されており、I/Oが大量に発生している状態では、I/O料金がインスタンス料金を圧迫するほど膨れ上がることがあります。インスタンスサイズの引き上げでバッファプールを拡大し、I/Oを削減することで、増えたインスタンス料金以上のI/Oコスト削減が実現したというわけです。 その後の話:クエリチューニングとさらなるコスト削減の可能性 インスタンスサイズの引き上げで急場を凌いだ一方で、根本原因であるパフォーマンスの悪いクエリについても手を打ちました。DBに悪影響が出始めてから2〜3日でチューニングを実施し、現在はCPU使用率も安定した状態になっています。 こうなると面白いのが、インスタンスサイズを上げたことに加えてクエリも改善されたため、リソースに余裕が生まれているという点です。現在の負荷状況を見ると、Readerの台数を減らせる可能性すら出てきています。 整理すると、こんな流れになります。 インスタンスサイズを8xlargeから12xlargeに上げたことでI/Oコストが大幅に削減され、サイズアップ前よりもトータルコストが減少しました。さらにクエリチューニングによってCPU負荷も安定し、もしReaderの台数削減が実現すれば、インスタンス料金そのものもさらに圧縮できることになります。 台数削減についてはまだ検討段階ですが、「CPU 100%で焦っていたあの頃」から考えると、コスト削減とパフォーマンス改善の両方が実現しつつある今の状況は、結果的に良い方向に転がったと言えそうです。 なお、コスト推移のグラフを見て「なぜオンデマンドで使っているのか」と気になった方もいるかもしれません。現在、Readerインスタンスのr8g系への移行を予定しており、移行が完了したタイミングでReserved Instancesを購入する計画です。インスタンスファミリーが変わる前にRIを買ってしまうと無駄になるため、あえて今はオンデマンドのままにしています。 学んだこと 今回の経験から得られた教訓をいくつか共有します。 バッファプールヒット率の「0.1%」を甘く見ない。 99.9%と100%の差は数字の上では僅かですが、大量のリクエストを処理するシステムでは、この0.1%が秒間数千回のストレージI/Oに化けます。ヒット率が100%から僅かでも低下し始めたら、それはバッファプールの容量が限界に近づいているサインです。 コスト最適化は「安いインスタンスを選ぶ」だけではない。 クラウドの料金体系は複合的です。インスタンス料金だけでなく、I/O料金、ネットワーク転送量、ストレージ料金など、複数の要素が絡み合っています。ひとつの要素を抑えることに固執すると、別の要素が膨れ上がるケースがあります。今回のように、インスタンスコストを「上げる」ことがトータルコストの「削減」につながることもあるのです。 パラメータ変更は慎重に。 バッファプールサイズの拡大はパラメータ変更でも可能ですが、本番環境でデフォルトから逸脱した設定を入れるのは、想定外のOOMなど別のリスクを抱えることになります。インスタンスサイズの変更であればデフォルトのバランスを保ったまま全体的なリソースを増やせるため、より安全なアプローチだと考えています。 おわりに 今回は「インスタンスサイズを上げたらコストが下がった」という、やや直感に反する事例を紹介しました。 振り返ってみると、バッファプールヒット率の僅かな変化に気づき、その背景にあるI/Oコストの構造を理解していたからこそ、適切な判断ができたと思います。ただ漫然と「CPUが高いからスペックを上げよう」ではなく、メトリクスを観察し、原因を分析し、コスト構造を踏まえた上で意思決定する。地味ですが、こうした積み重ねがインフラ運用の質を上げていくのだと改めて感じました。 同じようにAuroraのI/Oコストやバッファプールヒット率で悩んでいる方の参考になれば幸いです。
はじめに こんにちは、初めまして。今年3月に入社し、タイミーで Platform Engineer をしている小河原( @kgwryk28 )です。 現在、タイミーのシステムで利用しているメインのデータベース(Aurora MySQL)のバージョンアップにおける検証を進めています。 データベースのアップグレードで気になるのは、「今まで動いていたSQLが新バージョンでもそのまま動くのか」「アップグレードによって遅くなるクエリはないのか」という点です。 アップグレード後に互換性の問題や性能劣化が本番で発覚すると、影響は計り知れません。そのため、バージョンアップ範囲内の各バージョンの変更内容(changelog)を調べるだけでは不十分だと感じました。 そこで今回は、 Insight SQL Testing を使って検証を実施した際の工夫(期間の絞り込み・重複排除・結果のトリアージ)や検証手順を中心に共有したいと思います。 Insight SQL Testing とは? 「Insight SQL Testing」は、株式会社インサイトテクノロジーが提供する、 データベース移行やバージョンアップ時の SQL テストを自動化・効率化する ソフトウェアです。 移行元のデータベースで実行されていたSQLを、移行先のデータベースでも実際に使えるか検証できます。そのため、異種間の移行やバージョンアップ時に、SQLの互換性やパフォーマンスを確認できます。 実環境の SQL クエリを使用することで、本番環境に近い網羅性で「動くか/遅くならないか」を検証できます。 仕組みとしては以下のようになっています。 Insight SQL Testing を利用した検証の流れ 移行元のデータベース(ソース DB)と移行先のデータベース(ターゲット DB)の 2 つの DB を検証用に作成しておき、それぞれに対して同じクエリを実行します。 今回はデータベースのバージョンアップを行うため、ソース DB を現状の本番環境と同一のバージョンである Aurora MySQL 3.04.2(MySQL 8.0.28 相当)、ターゲット DB を移行先の Aurora MySQL 3.10.3(MySQL 8.0.42 相当)で作成します。 クエリの実行結果(成功/失敗・返ってきた値・実行時間)の突き合わせにより、バージョン間での互換性(片方の DB でだけ失敗するクエリ、結果が食い違うクエリ)、性能劣化(移行先のデータベースだけパフォーマンスが劣化するクエリ)を洗い出すことができます。 なお、入力となる実環境の SQL クエリには、元々 S3 に保存している監査用の Audit Log を使用しました。監査ログのフォーマットはInsight SQL Testingが要求するフォーマットと異なるため、作業用EC2インスタンスであらかじめ変換してから取り込みました。 検証作業の流れ 今回の検証は、おおまかに次の流れで進めました。 検証環境を構築 検証用の SQL セットを準備 Insight SQL Testing による検査(アセスメント)を実施 検査結果を元に SQL の互換性、パフォーマンス劣化クエリの抽出 検査結果を元に評価レポートの作成 検証上の課題 検証にあたって直面した課題が、 検証対象期間での全量テストは現実的に間に合わない という問題でした。 当初、検査対象の期間は 1ヶ月を想定していました。 これは、週次・月次で実行される定期バッチがあるため、クエリの網羅性を担保するためです。 実際に確認してみたところ、以下のことがわかりました。 1 日あたり約 1 億行 のクエリが流れている 1 時間分の量を流すだけでも 24 時間かかる。このペースで 1ヶ月分(約 31 億行)を全量テストすると、 約 2 年 かかる計算になる 当然これでは時間がかかり過ぎてしまいます。そのため、アセスメントの実行時間を短縮するための対策が必須でした。 SQL 互換性検査とパフォーマンス検査を分けて考える まず、 SQL 互換性検査とパフォーマンス検査を分けて実施 する方針にしました。目的が違うので、必要なクエリの範囲や確認方法も変わるからです。 SQL 互換性検査 の目的: アップグレード後の構文エラーの検出 パフォーマンス検査 の目的: アップグレード後に性能が劣化するクエリがないか確認したい データベースに対するアクセス特性による網羅性 タイミーのシステムでは、データベースに対するアクセス特性を大きく以下の 2 つに分類することができます。 常時アクセスが発生するもの。タイミーのワーカー様(アプリ利用者)・事業者様からの API 経由のデータベースアクセスや、CDC(変更データキャプチャ)によるレプリケーション接続(データ連携用)が該当 一定期間のみアクセスが発生するもの。日次・週次・月次の定期バッチジョブなど、システム内部で決められた日時に実行されるもの 「2. 一定期間のみアクセスが発生するもの」が動いている期間には、「1. 常時アクセスが発生するもの」も同時に発生します。そのため完全性は保証できないものの、一定の網羅性は確保できます。 SQL 互換性検査における重複排除による効率化 検査対象のクエリを削減する方法として、SQL パターンごとにクエリの重複排除を行う方法があります。 SQL の互換性検査に関しては、 重複排除 が効率化の鍵になります。同じパターンのクエリを何度もテストする必要はないからです。 重複排除の 3 つの設定 SQL Testing Manager の最新のバージョンでは、入力として実行する SQL(評価 SQL セット)を作成する際に、重複排除の挙動を 3 種類から選ぶことができます。 設定 重複排除 区別の仕方 しない 排除しない すべてのクエリをそのまま対象にする PI Hash 排除する リテラルを除いた SQL で区別する SQL ID 排除する SQL 文の違いで区別する 実際に検証したところ、各設定ごとに重複排除の方法が異なることが確認できました。 まず、以下のような SQL セットを準備します。 /* comment 1 */ SELECT * FROM test_table WHERE id = 1 ; /* comment 2 */ SELECT * FROM test_table WHERE id = 1 ; /* comment 3 */ SELECT * FROM test_table WHERE id = 2 ; 各重複排除モードで評価 SQL セットを作成した結果、行数は以下のようになりました。 重複排除モード 結果行数 区別の仕方 比較イメージ しない 3 すべてのクエリをそのまま対象にする - SQL ID 3 コメントも含めた SQL 文の完全一致による重複排除が行われる /* comment 1 */ SELECT * FROM test_table WHERE id = 1; PI Hash 1 コメントの有無にかかわらず、リテラル値が異なる SQL のみ区別される SELECT * FROM test_table WHERE id = ? PI Hash モードではクエリが完全にパターン化されるため、上記の入力クエリはすべて同一のものとして扱われます。一方、SQL ID での重複排除はコメントも含めたクエリ文の完全一致による比較が行われます。 まとめると以下のようになります。 SQL ID : コメントも含めた SQL 文の完全一致 で重複排除する。 同じクエリでも コメントが異なると別物 として扱われ、重複排除されない。 PI Hash : リテラル値の違い で区別しない(コメントの有無は問わない)。 例えば WHERE id = 1 と WHERE id = 2 のように リテラルだけが違う SQL は同一 として扱われる。 検証方法まとめ これらを踏まえてまとめると、以下のように検証を行うことにしました。 項目 SQL 互換性検証 パフォーマンス検証 目的 構文エラーの検出 アップグレード後の SQL のパフォーマンス劣化可能性の検出 方針 PI Hash によるクエリ構文のパターン化が行われたクエリで構文エラーの確認を行う 利用されている SQL クエリの実行比較を行い、アップグレード前後でパフォーマンスの変化を確認する 利用する重複排除モード PI Hash SQL ID テスト件数 約 31 万件 約 6700 万件 対象期間 過去 1ヶ月の SQL クエリ 全定期ジョブをカバーする最小のテスト期間(3 区間・合計 7 日間) 互換性検査(1ヶ月)が約 31 万件、性能検査(7 日間)が約 6700 万件と、期間が短い性能検査のほうが件数が多くなっています。これは、PI Hash がリテラル値まで畳んでクエリをパターン化するため件数が激減するのに対し、SQL ID はリテラル値の違いを区別するため件数が大きく残るためです。 なお、性能検査の対象期間は、定期バッチの実行タイミングを全てカバーできる最小の組み合わせを選んだ結果、3 区間・合計 7 日間となりました。 SQL IDの重複排除モードを利用するため、あらかじめ入力に使用するクエリからはコメントを除去しておき、SQLクエリに対する重複排除が行われるようにしておきます。 また、パフォーマンス検査はバッチ期間に合わせて3期間に区切り、アセスメント実行期間を高速化するため、独立した3環境を用意して並列で実行しました。 これらの対処により、テスト件数を大幅に削減できたことに加え、パフォーマンス検査は環境ごとに並列で実行したことで、元々約 2 年かかる想定だった検証作業を 5 日で完了することができました。 トリアージ方針 アセスメント(評価 SQL の実行)を実施すると、各クエリごとに「ソース DB/ターゲット DB でそれぞれどうだったか」を示す結果パターンが割り当てられます。これをもとに、次の方針で調査の要否を切り分けました。 各結果パターンの分類と、それぞれのトリアージ方針は以下のとおりです。 結果パターン 対応する検証 トリアージ方針 成功 SQL 互換性検査 問題なし ターゲット DB のみで失敗 SQL 互換性検査 エラーコードを元に原因を調査し、SQL の互換性に起因するエラーでないことを確認 ソース DB のみで失敗 SQL 互換性検査 エラーコードを元に原因を調査し、SQL の互換性に起因するエラーでないことを確認 両 DB で失敗 SQL 互換性検査 アップグレードによる差分は発生していないが、エラーコードごとの集計結果により、SQL の互換性に起因するエラーでないか原因を調査 両 DB で成功したが結果が相違 SQL 互換性検査 結果が異なる原因が検証環境起因のエラーであるか調査 両 DB で成功したがターゲット DB で性能劣化 パフォーマンス検査 ターゲット DB での実行時間が 1 秒以上、かつソース DB での実行と比較して 2 倍以上かかったクエリを抽出して原因を調査 ポイントは以下の点です。 バージョン間で挙動が変わったクエリに調査リソースを集中させることで、膨大な結果の中から本当に見るべきものを絞り込む 「両 DB で成功したがターゲット DB で性能劣化」したものは、わずかな実行時間の差まで拾うとノイズが多くなるため、「 遅くなった倍率(200% 以上) 」かつ「 絶対値としても 1 秒以上かかっている 」という条件で、影響の大きいものだけを抽出する 調査結果 調査の結果、バージョンアップ前後で結果差分が発生していることが判明しました。 PI Hash による重複排除後、EXPLAIN を除いて結果差分が検出されたクエリパターンは16件でした。 これらを調査したところ、いずれもアップグレード前後で行の順番が異なることによる差分であり、各行の内容は同一で、保存されているデータ自体に差分は発生していませんでした。 原因は主に二つありました。 ORDER BY で指定したカラムの値が同一になっている行が複数存在しており、同一の値に対する ORDER BY ... LIMIT の結果は非決定的であること EXPLAIN 結果の比較により、内部的な結合アルゴリズムとして Nested Loop Join が選択されていたクエリが、バージョンアップ後では Hash Join が選択されたことによる結果順序の不定化 このうち、クライアント上で表示される結果順序が変わってしまうなど、実際のプロダクトに問題が発生するクエリについては、該当クエリに ORDER BY を追加する等の対応を入れました。保存されているデータ自体に差分は確認されなかったため、現時点ではこれらの対応により、アップグレードに伴う影響は許容範囲内であると判断しています。 まとめ 今回はマイナーバージョンアップ(MySQL 8.0.28 相当 → 8.0.42 相当)であり、 MySQL 8.0.34 以降は bugfix のみのリリースの方針 であるため、大量の差分が発生することはありませんでした。しかし、調査結果で検出された内部的な結合アルゴリズムの変化は、各バージョンの変更内容(changelog)の調査だけでは気づきにくい部分でもあるため、今回の検証で気づくことができたのは大きな収穫でした。 膨大なクエリを現実的な時間で検証するために効いたのは、次の 4 つの工夫です。 目的で検証を分ける : 網羅性が欲しい互換性検査と、負荷状況を見たい性能検査を切り離し、それぞれに最適な対象範囲を設定 期間を賢く絞る : 定期バッチのタイミングを考慮し、「全定期ジョブをカバーする最小の 7 日間」で性能検査の対象を設計 重複排除で件数を削る : PI Hash / SQL ID の挙動の違いを理解し、目的に合わせて使い分けることで、テスト件数を大幅に削減 効率的なトリアージ : アップグレードに起因する変化に絞り込み、重要な差分に調査リソースを割く もし、今回の自分と同じようにデータベースのアップグレードを検討している方が、この記事が事前検証を行う上で何らかの参考になれば嬉しいです。
はじめに こんにちは! タイミーでPlatform Engineerをしている @MoneyForest です。 2026年6月9日〜10日にニューヨークで開催された Datadog の年次カンファレンス DASH 2026 に参加してきました。弊社からは、MLOpsエンジニアの斎藤が「 How Timee Delivers Day 1 Production Ready LLM Features 」というタイトルで登壇していました。 本記事では、Keynote の全体像、タイミーの登壇セッション、そして Fireside Chat から得たメッセージについてお届けします。 DASH 2026 の全体像 公式のKeynoteの記事 にあるように、まさに「Datadog enables teams to build better with AI」といった内容でした。AI がコードを書くスピードは劇的に速くなったが、それを安全に運用するためのループも同じスピードで回らなければ意味がない。この課題に対し、Datadog は Bits AI というAIエージェント群を中核に据えた新製品群を提示しました。 Keynote Keynote で発表された新プロダクトは、AIエージェントによってループを閉じ、開発を高速化するためのものでした。ループは大きくOps・Dev・AI Agent の3つの軸で整理できます。 全発表の詳細は Datadog 公式の記事 に網羅されているので、ここではループごとの要点に絞って紹介します。 Ops ループ (Detect → Investigate → Remediate) 従来人間が手作業で回していた検知・調査・修復のループを Bits AI で自動化する軸です。 Bits Detection (Preview)はサービストポロジーやデプロイ履歴から何が重要かを推測し、本番が赤くなりそうなときだけ発火するモニターを自動で作成・維持します。大量のフレーキーなモニターリストを置き換えるものです。 Bits Memories (Preview)は Slack でのインシデント対応やポストモーテムから運用上の教訓を学習し、将来の調査に適用します。 Bits Remediation (Preview)は根本原因に対して kubectl コマンド実行やコード修正 PR 作成まで自律的に行います。 Bits Infrastructure Operations (Preview)は OOMKilled や証明書期限切れといった日常的なインフラ問題を自動で検知・修復します。 Dev ループ (Code → Deliver → Evaluate) AI コーディングエージェントが加速する開発スピードに対して、リリースの信頼性を追いつかせる軸です。 Bits Code (GA)は Datadog が問題を検出したあらゆる場所(Error Tracking、APM、Code Security 等)から、本番テレメトリを根拠にした修正 PR を生成します。 Bits Release (Preview)は PR の意図を理解し、「新機能が動くか」「リグレッションがないか」の両面でバリデーションプランを自動生成するリリース検証エージェントです。 Bits Testing Agent (Preview)は URL や自然言語のゴール(例:「黒いサングラスを購入して」)からアプリを自律探索し、セルフヒーリングなテストスイートを生成します。 Agent Console (GA)は Copilot / Cursor / Claude Code 等のコーディングエージェントの利用状況を組織横断で可視化し、非効率なパターンの検出やコストアラートを提供します。 AI Agent ループ (Observe → Evaluate → Experiment → Ship) AI エージェント自体を本番で運用・改善するためのループです。 Agent Observability Patterns (Preview)は、本番の LLM トレースを行動パターンに自動分類します。デモでは15,000件のトレースから想定外の「coordination」パターンが $20,000 のコストを生んでいることを発見し、根本原因分析から修正・検証まで一気通貫で行っていました。 Bits Evals (Preview)はトレース・データセット・プロンプトバージョンを横断して仮説検証やプロンプト改善を自動化します。 Data Observability (GA)はデータパイプライン全体のリネージと異常検知を担います。 Infinite Cardinality Metrics (GA)は課金モデルをカーディナリティベースからメトリクス名+データボリュームベースに変更し、高カーディナリティ環境でのコスト予測可能性を大幅に向上させます。 その他 AI Guard (Limited Availability)はカスタムエージェントとコーディングエージェント双方の入出力をインターセプトし、プロンプトインジェクションやツール悪用をリアルタイムでブロックします。 Runtime Prioritization Engine (Preview)は16,000件の CVE から本番で悪用可能な9件に自動で絞り込み、 Security Analyst (GA)がセキュリティ調査を自動化します。 Journey Monitoring (Preview):Synthetics・RUM・Product Analytics を統合し、ユーザージャーニー単位で可用性・パフォーマンス・コンバージョン率を一つのビューで可視化。「CPU が高い」ではなく「その CPU 高騰でコンバージョンが落ちているか」を見る、ビジネスインパクトへの引き上げが狙い Federated Logs (Preview):Log Explorer から離れずに Databricks 等の外部ストレージを横断クエリ Bits Database Optimization :LLM 生成のクエリ最適化案をシミュレート DB で検証してから PR 化。デモでは500クエリから検証済み30件に絞り、ノイズを9割削減 タイミーの登壇:How Timee Delivers Day 1 Production Ready LLM Features dash.datadoghq.com speakerdeck.com 弊社 MLOps エンジニアの斎藤が、タイミーにおける LLM 機能のプロダクション対応と LLM Gateway の構築について発表しました。内容について要約して紹介します。 背景 タイミーでは LLM がワーカー・クライアント双方の体験を向上させる重要な要素になっています。求人票の自動生成をはじめ、多くのストリームアラインドチームが独立して LLM 機能を活用しており、MLOpsエンジニアがその基盤を支えています。 チェックリストの誕生 最初のプロダクション導入では、LLM の不安定さ(タイムアウト、レイテンシスパイク、レートリミット)を想定した設計を行い、Vertex AI をプラットフォームとして採用しました。この経験から、LLM 機能に求められるプロダクション水準を定義した Production Readiness Checklist を策定しました。一般的なプロダクションの品質基準に加え、LLM 固有のシグナル(フォールバック、モデルレイテンシ、コスト制御など)をカバーするものです。 障害による転機 求人票生成機能の導入後、同一プロバイダー起因で2つの LLM 機能が同時にダウンする障害が発生しました。チェックリスト・モニタリング・ゲートウェイはそれぞれ存在していたものの、採用するかは各チームに依存していたことが根本原因でした。 また、高いスピードで価値を届けるという当然の行動をしていた中で、3人の ML プラットフォームチームが全チームに基準を強制するのは物理的に不可能だったのです。 解決策:LLM Gateway この障害を転機に、全 LLM 呼び出しの共通エントリーポイントとして LLM Gateway を導入しました。Cloud Run 上に構築され、複数の LLM プロバイダーを抽象化しています。 ゲートウェイが提供する価値は3つです。 自由な探索:プロダクトチームがセットアップのオーバーヘッドなしにプロンプトやユースケースを試せる 可観測性:全呼び出しにチーム・機能・環境のメタデータが付与され、トレース・レイテンシ・エラー・フォールバックが一箇所で見える ガバナンス:コスト・使用量・レートリミット・安全性が自動的に強制される これにより、チェックリスト(期待値を定義)× モニタリング(コンプライアンスのエビデンス)× ゲートウェイ(パスの強制)が統一化され、すべてのチームが恩恵を得られる状態になりました。 このセッションは「成功事例ではなく、何が壊れ、何を学び、何ができるかの話」という齋藤の言葉通り、実践的な知見が詰まった内容でした。 Fireside Chat OpenAI や Vercel の幹部を招いた Fireside Chat (対談)が非常に印象的でした。それぞれの視点からエージェント時代のソフトウェア開発について語られましたが、共通するメッセージが浮かび上がってきました。 The New Shape of Engineering(Fireside Chat with Datadog CTO Alexis Lê-Quôc and OpenAI Head of Product and Platform Thibault Sottiaux ) dash.datadoghq.com OpenAI で Codex と API Enterprise を率いる Thibaut との対談では、エージェントが組織にもたらす変化が語られました。 特に印象的だったのは、可観測性の役割が根本的に変わるという点です。エージェントの生産性が人間のレビュー能力を超えるにつれ、「すべてのコードをレビューするのか?」という問いに直面します。システム開発は、コードを一行ずつレビューするのではなく、「症状と振る舞い」で監視する、つまり医者が患者を診断するようなアプローチになるというビジョンが語られました。 また、OpenAI 社内ではフォンブースを使っている人の大半が会議ではなく AI と話していたというエピソードや、Thibaut 自身の Codex の使い方がコーディングから情報の統合と組織の把握へシフトしたという話も、エージェント時代の働き方を象徴していました。 エージェントを正しく使うヒントとして、「生産性の幻想を避ける」(並行して動かしていても、本当に意味のある問題に取り組んでいるか?)と「良い状態を説明する」(同僚に期待値を説明するように、エージェントにも伝えること)が挙げられていたのも実践的でした。 Fireside Chat with Datadog CPO Yanbing Li and Vercel CPO Tom Occhino dash.datadoghq.com Vercel CPO の Tom Occhino との対談では、「意図と実装の距離がほぼゼロに縮まった」という時代認識から出発し、プロダクト開発の変革が議論されました。 特に印象的だったのは、仕事を「2つの波長」として捉える考え方です。 Long Wavelength(基盤作業):コアプラットフォームの品質・信頼性・セキュリティを高める作業。AI を使ってプロセスを加速しても、既存の検証・可観測性・レビューはすべて残る Short Wavelength(グリーンフィールド):誰も依存していない新規領域。AI をエンドツーエンドで使ってどんどんシップする 両方の組み合わせが大事であること、そして Long Wavelength 自体を短くしていくこと、つまりリリースエンジニアリングのエージェント化が次の挑戦として語られていました。 共通メッセージ 両セッションは全く別物ですが、かなり共通する点が多かったのが印象的でした。 「作ること」は安く速くなり、人間の価値は"何を・どう良くするか"の定義に移る OpenAI(Thibaut / The New Shape of Engineering):Thibaut は「良い状態を説明せよ」と語りました。実装が安くなったぶん、エージェントに何を期待するのかを伝えないと、エージェントは勝手に仮定を置いてしまう。難しいプロジェクトの期待値を同僚に説明するのと同じように、成功基準と検証方法を明示することが結果を大きく左右する。 Vercel(Tom / Fireside Chat):Tom は「意図と実装の距離がほぼゼロに縮まった」という時代背景からセッションを始めました。だからこそ PM のコア業務である成功の定義、明確な問題設定、顧客理解はなくなるどころか重要性を増す。仕様が明確であるほどコーディングエージェントの出力は良くなるため。 本番・検証・信頼は"無料ではない" OpenAI:Thibaut は、OpenAIのメインエージェントの全アクションを"元の意図"に照らして検証する Guardian(デュアルエージェント安全システム)を紹介しました。そしてAIエージェントへの信頼は一足飛びには得られない。テストとログへ惜しみなく投資し、実績を積み重ねることで漸進的に築かれていくものだと語りました。 Vercel:Tom は「ソフトウェアの構築はほぼ無料でも、本番は絶対に無料ではない」と強調しました。基盤となる作業(Long Wavelength)では、AI でプロセスを加速しても、可観測性・既存システムへの統合・人間によるレビューはすべて残る、と。 可観測性が検証の中心になる OpenAI:Thibaut は、AIエージェントの生産性が人間のレビュー能力を超えていく中で「すべてのコードをレビューするのか?」と問いを投げかけ、システムは医者が患者を診断するように「症状と振る舞い」で監視する時代になると語りました。過去に発火したアラートを分析してノイズを減らす、アラートのトリアージ自体もエージェントが担い始めています。 Vercel:Tom がユースケースで示したのは、本番から得たインサイトでフィードバックループを閉じる「AIエージェント型インフラ」でした。コアのバイタルを常時監視し、リグレッションが起きれば原因を二分探索し、修正もしくはリバートの PR を自動で出す。コードを一行ずつ追うのではなく、本番の振る舞いから全体の健全性を捉える方向に進んでいます。 まとめ DASH 2026 を通じて感じたのは、「検証、品質、安全性の評価を、個人の規律ではなく経路(path)に作り込む」という考え方が、発表全体を貫いていたことです。 例えばBits Release はリリース検証を、AI Guard はセキュリティ評価を、呼び出し経路に強制的に組み込みます。 この時代におけるプラットフォームエンジニアリングの仕事は基準そのものを作ることではなく、基準が自動で適用される経路(基盤)を作ることだと思いました。
はじめに こんにちは、タイミーでエンジニアをしている徳富( @yannKazu1 )です。 タイミーではメインサービスのバックエンドを Rails で開発しています(Go を採用しているプロダクトもありますが、本記事では Rails を前提とします)。 突然ですが、皆さんのチームでは CI の待ち時間、気になっていませんか? 「Push した、コーヒー淹れた、戻ってきた、まだ回ってる……」みたいな経験は、開発者なら一度はあるのではないでしょうか。 本記事では、そんな状況を改善するために GitHub Actions 上のテスト実行パイプラインで取り組んだ 3 つの高速化テク を紹介します。どれも「知っていれば明日から試せる」くらいの温度感なので、気軽に読んでいただければと思います。 1. キャッシュの保存先を GitHub Cache から S3 に移行 課題: actions/cache が安定して速くない 最初にぶつかった壁が actions/cache の速度でした。 vendor/bundle (数百 MB〜1 GB 超)の save/restore でやたら時間がかかることがあり、リストアだけで数分待たされる場面がちょくちょくありました。これはセルフホストランナーに限った話ではなく、GitHub ホステッドランナーでも起きます。 実際、公式リポジトリにも Extremely slow cache on self-hosted from time to time という Issue が立っていて、セルフホスト・GitHub ホステッド問わず同様の報告が寄せられています。 さらに私たちの場合、 AWS 上のセルフホストランナー を使っているのでなおさらです。 actions/cache のバックエンドは Azure Blob Storage のため、セルフホストランナーからだとインターネット経由のアクセスになり、スループットが 約 20 MB/s まで落ちる ケースも報告されています( Actuated Blog )。突発的に遅いうえに経路も遠い——これでは安定した速度は望めません。 容量面でも、リポジトリあたり 10GB の制限があります。また、7 日間アクセスのないキャッシュは自動削除されます。その結果、ブランチが増えるとすぐに上限に達し、必要なキャッシュが消えてしまうのも地味にストレスでした。 解決策: runs-on/cache で S3 をバックエンドに そこで [runs-on/cache](https://github.com/runs-on/cache) を導入し、キャッシュの保存先を 同一リージョン(東京)の S3 バケット に切り替えました。 前述のとおり、セルフホストランナーで  actions/cache  を使うとスループットが ~20 MB/s まで落ちるケースがあります。一方  runs-on/cache  は同一リージョンの S3 を使えるため、200 MiB/s 以上 のスループットが出ます( 公式ドキュメント )。単純計算で 10 倍近い改善 です。 actions/cache とインターフェースがそのまま同じなので、 uses: を差し替えて環境変数を 1 つ足すだけで移行できました。 # .github/actions/setup-ruby-with-s3-cache/action.yml - name : Restore cache uses : runs-on/cache@v4.2.3-r2 env : RUNS_ON_S3_BUCKET_CACHE : your-gha-cache-bucket with : path : "**/vendor/bundle" key : bundle-v1-${{ runner.os }}-${{ inputs.ruby_version }}-${{ hashFiles('Gemfile.lock') }} restore-keys : | bundle-v1-${{ runner.os }}-${{ inputs.ruby_version }}- bundle-v1-${{ runner.os }}- なぜ runs-on/cache を選んだか S3 をキャッシュバックエンドにする方法は他にもあります( tespkg/actions-cache 、 whywaita/actions-cache-s3 、自前の aws s3 cp スクリプトなど)。その中で runs-on/cache にした決め手はこのあたりです。 環境変数 1 つで切り替え : RUNS_ON_S3_BUCKET_CACHE を設定するだけで S3 バックエンドに切り替わる 自前実装が不要 : 圧縮・展開・キャッシュキーのマッチング・フォールバックなど、地味にめんどくさい部分を全部やってくれる 容量無制限 : S3 なので 10GB の制限もキャッシュの自動削除もなし キャッシュキーの設計 キャッシュキーは 3 段階のフォールバック構造にしています。 bundle-v1-Linux-3.3.6-<Gemfile.lock のハッシュ> ← 完全一致(最速) bundle-v1-Linux-3.3.6- ← Ruby バージョン一致 bundle-v1-Linux- ← OS のみ一致 完全一致しなくても、部分一致したキャッシュをリストアして bundle install すれば差分の gem だけで済みます。ゼロからインストールするより圧倒的に速いので、新しいブランチでもほぼキャッシュが効く状態を維持できます。 OIDC 認証で安全に S3 にアクセス AWS へのアクセスには OIDC 認証 を使っています。長期的なアクセスキーをシークレットに保存しなくて済むので、セキュリティ面でも安心です。 - name : Configure AWS credentials uses : aws-actions/configure-aws-credentials@v6 with : role-to-assume : arn:aws:iam::123456789012:role/your-gha-role aws-region : ap-northeast-1 2. マイグレーション結果をまるごとキャッシュ 課題: 毎回のマイグレーションが地味に重い テストジョブは毎回データベースをセットアップします。ここで問題になったのが、マイグレーション数が数百を超えてくると rails db:create db:schema:load だけで 数分かかる ということ。 「schema:load だからすぐ終わるでしょ?」と思いきや、テーブル数が多いとそうでもないんですよね。 解決策: MySQL のデータディレクトリごと S3 にキャッシュ 発想を変えて、 マイグレーション済みの MySQL データディレクトリ ( /var/lib/mysql ) をまるごと S3 にキャッシュ することにしました。要は「マイグレーション済みの DB をそのまま持ってくれば、マイグレーション自体を省略できるよね」という作戦です。 仕組みの全体像 【キャッシュの生成】 【キャッシュの利用】 master ブランチ feature ブランチ db/migrate/** 変更 テストジョブ起動 or 毎日定時 │ │ ▼ ▼ S3 からキャッシュをリストア MySQL 起動 → ./tmp/mysql_data に展開 │ │ ▼ ▼ rails db:create db:migrate MySQL 起動(データマウント済み) │ │ ▼ ▼ ./tmp/mysql_data を S3 に保存 rails db:migrate(差分のみ) │ ▼ テスト実行 キャッシュの生成: master で定期的に焼き直す master ブランチでマイグレーションファイルが変更されたとき、または毎日定時に、専用のワークフローがキャッシュを更新します。 # .github/workflows/update-migration-cache.yml on : push : branches : [ master ] paths : - 'db/migrate/**' - 'db/schema.rb' - '.github/workflows/update-migration-cache.yml' - '.github/actions/migration-hash/**' schedule : - cron : '0 2 * * *' # 毎日 UTC 2:00(JST 11:00)に実行 workflow_dispatch : # 手動実行も可能 やっていることはシンプルです。 MySQL コンテナを起動(データディレクトリを ./tmp/mysql_data にマウント) rails db:create db:migrate でフルマイグレーション実行 ./tmp/mysql_data をまるごと S3 にアップロード - name : Run database migration run : bundle exec rails db:create db:migrate - name : Save migration cache uses : runs-on/cache/save@v4.2.3-r2 env : RUNS_ON_S3_BUCKET_CACHE : your-gha-cache-bucket with : path : ./tmp/mysql_data key : test-${{ runner.os }}-${{ runner.arch }}-mysql${{ steps.migration-hash.outputs.mysql_version }}-${{ runner.environment }}-db-migration-${{ steps.migration-hash.outputs.hash }} キャッシュキーの設計: 何をキーに含めるかが大事 キャッシュキーには地味に気を使っています。 test-Linux-X64-mysql8.0.28-self-hosted-db-migration-<db/schema.rb のハッシュ> │ │ │ │ │ OS ARCH MySQL Ver ランナー環境 スキーマハッシュ ポイントは db/schema.rb のハッシュを含めていること。 マイグレーションの内容が変われば schema.rb も変わる ので、自動的に新しいキャッシュが生成されます。MySQL バージョンやアーキテクチャもキーに入れているのは、バイナリ非互換でハマらないための保険です(一度やらかしました……)。 キャッシュの利用: Composite Action で再利用しやすく キャッシュの利用ロジックは Composite Action に切り出して、RSpec だけでなく Steep(型チェック)など他のワークフローからも使い回しています。 # .github/actions/setup-mysql/action.yml - name : Create MySQL data directory run : mkdir -p ./tmp/mysql_data - name : Restore migration cache id : cache-hit-check uses : runs-on/cache/restore@v4.2.3-r2 env : RUNS_ON_S3_BUCKET_CACHE : your-gha-cache-bucket with : path : ./tmp/mysql_data key : test-${{ runner.os }}-${{ runner.arch }}-mysql${{ mysql_version }}-${{ runner.environment }}-db-migration-${{ hash }} - name : Start MySQL service with docker compose run : docker compose -f compose.ci.yml up -d mysql8 Docker Compose では、リストアしたデータディレクトリをそのままボリュームマウントします。 # compose.ci.yml services : mysql8 : volumes : - ./tmp/mysql_data:/var/lib/mysql MySQL が起動すると、キャッシュ内のデータファイルがそのまま認識されるので、 マイグレーション済みのデータベースが即座に使える 状態になります。 テストジョブでの分岐: キャッシュがあれば差分だけ 各テストジョブでは、キャッシュがヒットしたかどうかで処理を分岐しています。 - name : RSpec run : | if [ "${{ steps.setup-mysql.outputs.cache_hit }}" == "true" ] ; then echo "Using cached migration data, running incremental migration" bundle exec rails db:migrate # ← ブランチ固有の差分だけ else echo "No cache found, running schema load" bundle exec rails db:create db:schema:load fi キャッシュヒット時 : master のマイグレーション済みデータが復元されているので、 db:migrate で差分だけ適用。たいていは数秒で終わります キャッシュミス時 : MySQL バージョンアップ直後などキャッシュがない場合は db:schema:load にフォールバック この仕組みのおかげで、並列のテストジョブそれぞれで数分かかっていた DB セットアップが数秒になりました。体感で一番効果が大きかった施策かもしれません。 3. CI 用 MySQL のパフォーマンスチューニング 課題: デフォルト設定の MySQL が意外とボトルネック テスト環境の MySQL をデフォルト設定のまま使っていたのですが、ある日ふと気づきました。テストでは各テストケースごとに BEGIN / ROLLBACK やテーブルのクリーンアップが走るので、 書き込みが尋常じゃない量になっている んですよね。 デフォルト設定だと、コミットのたびにディスクへの fsync が走ります。本番では安全のために必要ですが、テスト環境では……正直、オーバースペックです。 解決策: テスト環境に限定して、耐久性よりパフォーマンスを優先する CI 専用の compose.ci.yml で、 データ耐久性を思い切って犠牲にして、書き込みパフォーマンスを最大化 しました。 # compose.ci.yml services : mysql8 : image : ${MYSQL_IMAGE} command : > mysqld --innodb-flush-log-at-trx-commit=0 --sync-binlog=0 --skip-innodb-doublewrite environment : MYSQL_ALLOW_EMPTY_PASSWORD : "yes" ports : - "3306:3306" volumes : - ./tmp/mysql_data:/var/lib/mysql 各パラメータの解説 innodb-flush-log-at-trx-commit=0 InnoDB のログ書き込み動作を制御するパラメータです。 値 動作 用途 1(デフォルト) コミットのたびにログをディスクに fsync 本番環境(ACID 完全準拠) 2 コミットのたびに OS バッファに書き込み、 fsync は毎秒 レプリカなど 0 ログの書き込みも fsync も毎秒のバッチ処理 テスト環境 テストで 1 秒以内にクラッシュリカバリが必要な場面はないので、 0 にして コミットごとの fsync オーバーヘッドを完全に排除 しています。 sync-binlog=0 バイナリログ(レプリケーション用)の同期タイミングです。 値 動作 1(デフォルト) コミットごとにバイナリログを fsync 0 OS のファイルシステムキャッシュに任せる テスト環境ではレプリケーションを使わないので、バイナリログを sync_binlog=0 にし、同期を OS のキャッシュに任せることでコミットごとの fsync を省いています。 skip-innodb-doublewrite InnoDB の doublewrite バッファを無効化します。これは書き込み途中のクラッシュに備えて全ページを 2 回書く安全機構なのですが、テスト環境では不要です。無効化すれば 書き込み I/O が大幅に減ります 。 注意: 本番では絶対にやらないでください 念のため書いておきますが、上記の設定は データの耐久性・整合性を犠牲にしています 。 innodb-flush-log-at-trx-commit=0 : クラッシュで最大 1 秒分のトランザクションが消える sync-binlog=0 : クラッシュでバイナリログが不完全になる可能性 skip-innodb-doublewrite : 部分書き込みでデータ破損のリスク テスト環境は「テストが通ればデータは捨てる」使い捨ての世界なので、これらのリスクは許容しています。くれぐれも本番には適用しないように! まとめ 施策 何をキャッシュ/最適化しているか 効果 S3 キャッシュ vendor/bundle (Gem パッケージ) ダウンロード高速化・容量制限の解消 マイグレーションキャッシュ マイグレーション済み MySQL データ DB セットアップ時間を数分→数秒に MySQL チューニング fsync・doublewrite の無効化 テスト中の書き込み I/O を削減 CI の高速化に近道はなくて、結局はボトルネックを一つずつ潰していくしかありません。 DB のデータ耐久性は不要なので無効化する。マイグレーションは毎回ゼロからやる必要がないのでキャッシュする。キャッシュの保存先は、ネットワーク的に近い場所に置く。こうした「当たり前だけど意外とやっていない」割り切りが、大きな高速化につながりました。 同じような課題を抱えるチームの参考になれば嬉しいです。
こんにちは、株式会社タイミーでMLOpsエンジニアをしているKYです。普段はMLプラットフォームの構築・運用を担当しています。 実務の中でコンテナイメージのサプライチェーンセキュリティ強化を進めており、その一環として Docker 社が提供する「Docker Hardened Images(DHI)」の実装を辿る機会がありました。 その際、実際の定義ファイルを見て、少し驚きました。コンテナのビルド定義といえば「Dockerfile」が当たり前だと思っていたのですが、DHI の定義はなんと YAML で書かれていたのです。 「なぜ Dockerfile ではないのか?」と定義の読み方を追いかけていくうちに、BuildKit のアーキテクチャに行き着きました。この記事では、DHI の仕組みを通じて、私たちが普段の Dockerfile 運用で押さえるべきポイントを再確認したいと思います。 ビルド定義の主役は「Frontend」 BuildKit は、「定義を解釈する部分(Frontend)」と「実際にビルドを実行する部分(Backend)」に分離しています。Frontend は、入力(Dockerfile や YAML)を BuildKit の中間表現(LLB)に変換する役割を持ちます。 ここで鍵になるのが、ファイル先頭のコメント行 # syntax=... です。BuildKit はまずこの1行を読み、どの Frontend で後続を解釈するかを決めます。つまり、Docker 公式が推奨しているのに見落とされがちな以下の1行は、単なるコメントではなく「このファイルは公式の Dockerfile Frontend で解釈してほしい」という宣言です。 # syntax=docker/dockerfile:1 一方で DHI の定義ファイルを開くと、YAML の1行目に次の指定があります。 # syntax=dhi.io/build:2-debian13 YAML も # をコメントとして扱うため、BuildKit から見れば「 # syntax= から始まるビルド定義」という意味で入口は同じ。その後の中身を YAML として解釈するのは、差し替えられた DHI Frontend の仕事 というわけです。 DHI は何をしているのか:YAML をコンパイルする DHI の定義ファイル(YAML)は、 RUN apt-get... のようにといった手順を重ねるのではなく、「最終的に何を入れるか」という状態を宣言します。 【DHI の YAML 定義例(実際の定義ファイルからの抜粋)】 # syntax=dhi.io/build:2-debian13 name : Debian 13 Base image : dhi.io/debian-base variant : runtime platforms : - linux/amd64 - linux/arm64 dates : release : "2025-08-09" end-of-life : "2028-08-09" contents : packages : - '!libelogind0' - '!mawk' - '!original-awk' - base-files - bash - ca-certificates - coreutils # ... 以下、ベースに含めるパッケージの列挙が続く accounts : run-as : nonroot users : - name : nonroot uid : 65532 gid : 65532 cmd : - /bin/bash いくつかのフィールドに注目してみます。 contents.packages : !mawk のように ! プレフィックスを付けると「明示的に含めない」パッケージを宣言できます。削除手順を書くのではなく、最初から「入れない」と表明する点が Dockerfile との大きな違いです。 accounts.run-as: nonroot : 実行ユーザーを非 root に固定する宣言で、Dockerfile の USER 命令に相当します。Dockerfile のように RUN useradd ... といったユーザ作成手順を書く必要はなく、「誰で動かすか」という状態だけが残る点が特徴です。 dates.end-of-life : イメージのライフサイクル終了日まで定義に含まれており、運用上の管理情報もビルド定義の一部として扱われています。 このように、DHI の YAML は「どう作るか」ではなく「何が入っていて、誰が動かすか」を宣言しています。そしてここで重要なのは、 BuildKit が YAML を直接ビルドしているわけではない という点です。 DHI の Frontend がこの YAML を読み込んで中間表現(LLB)へコンパイルし、あとは通常通り BuildKit がビルドを実行します。つまり、DHI の YAML は「別言語」ではなく、 Frontend を差し替えて得た “別の入力形式” なのです。 たとえば不要パッケージの除外ひとつとっても、Dockerfile では apt-get remove → autoremove → キャッシュ削除と手順を重ねる必要があります。一方、DHI なら - '!mawk' の1行で意図が完結します。手順(How)ではなく意図(What)だけが残るため、セキュリティ監査や再現性の面で有利です。DHI が宣言的定義を採用しているのは、こうした相性の良さがあるからです。 忘れられがちな Dockerfile の公式推奨設定 今後、DHI のような宣言的フロントエンドがすぐに主流になるかは未知数であり、当面は既存の Dockerfile 運用が続くでしょう。 しかし、DHI が示す「Frontend は明示し、選ぶものである」という観点は重要です。まずは Docker 公式が推奨する以下の2行を、忘れずに Dockerfile の先頭へ記述しましょう。 # syntax=docker/dockerfile:1 # check=error=true # syntax=... 使用する Frontend を固定し、手元の環境と CI の違いによるビルド結果の揺れを防ぎます。 # check=error=true BuildKit の静的解析(lint)を強め、警告レベルの記述を CI で弾けるようにします。 これらを習慣づけるだけで、「Frontend を明示し、品質を保つ」文化に確実に近づきます。 まとめ DHI から学べる本質は、 BuildKit は Frontend を自由に差し替えられる という点にあります。この視点を持つと、DHI は単なるセキュアなベースイメージではなく、ビルド定義の抽象度を一段上げる試みとして見えてきます。 「手順を書く」から「状態を宣言する」への移行は、Infrastructure as Code で何度か見てきた流れと重なって見えます。DHI を触ってみて、その発想がコンテナビルドの入力形式にも持ち込まれていることを実感しました。 将来的にビルドのパラダイムがどう変わるにせよ、まずは見逃されがちな # syntax=... と # check=... をきちんと置くこと。タイミーでも Cloud Run / Vertex AI Pipelines の DHI 移行を進める中で、Frontend 指定の差がビルド結果の揺れに直結する場面に何度か遭遇し、この2行の重要性を改めて感じました。DHI がもたらした視点を持ちつつ、足元の運用を公式のベストプラクティスで堅牢にする。これが、現実的で安全なコンテナ運用の第一歩です。 参考文献 Docker Hardened Images - カタログリポジトリ Debian 13 Base 定義ファイル(13.yaml) — 記事中の YAML 定義例の抽出元 Custom Dockerfile syntax - Docker Docs Build hardened images - Docker Docs We're Hiring! サプライチェーンセキュリティや ML 基盤の足回りに興味を持っていただけたなら、ぜひ一緒に働きませんか。タイミーでは、ML プラットフォームの構築・運用やサプライチェーンセキュリティの強化に取り組むエンジニアを募集しています! 少しでも興味を持っていただけましたら、ぜひ以下のリンクから詳細をご覧ください。 MLOpsエンジニア シニアMLOpsエンジニア 募集ポジション一覧
こんにちは、株式会社タイミーで MLOps エンジニアをしている KY です。普段は ML プラットフォームの構築・運用を担当しています。 私たちのチームでは、機械学習エンジニアやデータサイエンティストが開発に集中できるよう、VS Code のリモート開発(Remote SSH および Dev Container)を活用した開発環境を提供しています。本記事では、その中でも 共通 Dev Container Feature によるガードレール にフォーカスし、各チームが自分たちで開発環境を立ち上げられることを前提にしながら、 セキュア・バイ・デフォルト をどう実現しているかをご紹介します。 なぜ Dev Container Feature にガードレールを寄せるのか この記事を書こうと思ったきっかけは、もともと機械学習エンジニアやデータサイエンティスト向けだった開発環境を、データアナリストをはじめとする別職種のメンバーにも広げ始めたことでした。ユーザー層が広がるにつれ、「どこまでを各自の設定に任せ、どこからを仕組みで縛るか」をあらためて考え直す必要が出てきた、というのが出発点です。あわせて、組織として求められるセキュリティレベルも年々高まってきています。 ML プラットフォーム特有の事情として、ユーザーの専門領域が幅広い、という点があります。機械学習エンジニアやデータサイエンティストはモデリングやデータ分析を主戦場としており、依存パッケージの脆弱性管理やコンテナの権限設計といった領域は、本来の業務の中心ではないことが多いです。だからこそ、これらをユーザー個々の習熟度に委ねるのではなく、プラットフォーム側で初期値を配る方針を取りました。 各チームがセルフサービスで開発環境を立ち上げられ、特別な設定をしなくても初期状態でセキュリティのベースラインが担保される 状態を目指しています。推奨パスに乗るだけで安全に進められる、いわゆる「ゴールデンパス」の発想であり、 セキュア・バイ・デフォルト を仕組みで成立させるアプローチです。 この方針を devcontainer.json レベルで素直に表現できる仕組みが Dev Container Feature でした。Feature を1行足すだけで宣言的にガードレールが適用されるため、「各チームが自律的に環境を立ち上げつつ、危険な操作だけは仕組みで塞ぐ」という設計とよく噛み合っています。 共通 Dev Container Feature によるガードレール 私たちの開発環境では、共通化した Dev Container Feature(以下、共通 Feature)を配っています。まず、ベースイメージと Feature の役割は明確に分けています。 Docker Hardened Images(以下、DHI)をベースにした開発用イメージでは、各種開発ツール(Python / uv / gcloud / Claude Code など)をインストールしておきます 。 共通 Feature では、それらツールの設定ファイル配置とガードレール適用のみを担います 。 この前提のもと、各チームの devcontainer.json は以下のようにシンプルで、ベースイメージを指定し、共通 Feature を追加するだけで、後述するガードレールがまとめて適用されます。 { " image ": " asia-northeast1-docker.pkg.dev/<PROJECT>/<CUSTOM_DHI_PATH>:<TAG> ", " features ": { " asia-northeast1-docker.pkg.dev/<PROJECT>/<CUSTOM_FEATURE_PATH>:<TAG> ": {} } } こうしてレイヤーを分けておくと、ツールの入れ物とポリシーの適用が混ざらずに整理されるため、 よりセキュアに締めやすい という体感があります。たとえばポリシー側だけを Renovate で継続的に更新していけるので、イメージの差し替えと独立してセキュリティ設定の追従・レビューを回せます。なお、ベースイメージ側で押さえるべきリスク(OS パッケージの脆弱性など)と、Feature 側で押さえるべきリスク(ツールの権限・設定)をどう切り分けるかといった論点もあります。ただし本記事のスコープ外のため、詳細は割愛します。 この Feature がプロビジョニング時に各種設定ファイルを配置し、ガードレールを自動で効かせます。実際には複数のツール設定を同じ方式で配布していますが、本記事では代表例として AI エージェントの制御を取り上げます。 Claude Code などの AI エージェントの制御 昨今、 Claude Code のような AI コーディングエージェントが普及していますが、無制限の権限を与えると破壊的変更や意図しないデータ送信のリスクがあります。共通 Feature は /etc/claude-code/managed-settings.json を自動生成し、システムレベルで制御を行います。 { " strictKnownMarketplaces ": [ { " source ": " github ", " repo ": " <ORGANIZATION>/<REPOSITORY> " } ] , " allowedMcpServers ": [ { " name ": " <APPROVED_MCP_NAME> ", " command ": " ... " } ] , " permissions ": { " deny ": [ " Bash(sudo:*) ", " Bash(gcloud:*) ", " Read(~/.config/**) " ] } } ※ 実際の設定から一部を抜粋しています。 プラグインマーケットプレイスと MCP サーバーは、社内で承認されたもののみに制限しています(ホワイトリスト形式)。また、 sudo や gcloud などの権限昇格・クラウド操作、 ~/.config/ 配下の機密情報へのアクセスといった危険な操作は、Deny リストでブロックしています。ユーザー側の settings.json では上書きできない managed settings として配置しているため、「うっかり緩めてしまう」ことを構造的に防げます。 Feature に寄せることの嬉しさ これらを共通 Feature として提供していることで、以下のようなメリットが得られています。 各チームの devcontainer.json は Feature を1行足すだけでよく、 セキュリティ設定の知識なしにベースラインを満たせる 。 Feature のバージョンを上げるだけで、 全社的にガードレールを一括更新できる (Renovate で自動 PR される)。 設定の出所が Feature に集約されているため、 監査やレビューの対象が明確 になる。 実際に運用してみると、Renovate の PR を1本マージするだけで全チームの Claude Code 設定が同時に更新されるのは、想像していた以上に運用が軽くなったと感じています。 補足:周辺で効かせている多層防御 共通 Feature だけで全てを押さえようとせず、周辺の仕組みと組み合わせて多層防御を成立させています。ベースイメージには DHI を採用してコンテナ起動時点でのベースラインを引き上げ、ホストとなる Remote SSH 用 VM 側にも同等のポリシーを展開し、依存関係は Dependabot / Renovate で継続的に追従させる、という具合です。 おわりに 今回は、MLOps チームが 共通 Dev Container Feature を使って、ML 開発環境のガードレールをどのように設計・運用しているかをご紹介しました。 振り返ってみると、 ツールは DHI イメージ、設定は共通 Feature、更新は Renovate と責務を分けておくと、それぞれに対するレビューや更新のサイクルを独立して回しやすいのが大きな利点でした。ガードレール自体を作ることよりも、 ガードレールを錆びさせない構造 に落とすことが、各チームの自律性を損なわずにベースラインを引き上げていくうえでの要だったように思います。 参考文献 Claude Code - System settings : /etc/claude-code/managed-settings.json に関する公式ドキュメント Dev Containers - Features : Dev Container Feature の仕様 こうした「セキュア・バイ・デフォルトな ML 開発環境」を、より多くのチームと一緒に磨き込んでいきたいと考えています。 We're Hiring! タイミーでは、ML プラットフォームの構築・運用やセキュアな開発環境の整備に一緒に取り組んでいただけるエンジニアを募集しています! 少しでも興味を持っていただけましたら、ぜひ以下のリンクから詳細をご覧ください。 MLOpsエンジニア シニアMLOpsエンジニア 募集ポジション一覧
1. 自己紹介・経歴 はじめまして、データアナリストのrizumuです。2025年にタイミーに入社しました。 前職ではファッション系のCtoCマーケットプレイスを運営する会社で約4年間データアナリストとして働いていました。UIの分析やクーポン施策の効果検証、顧客セグメントの分析などを担当していました。 2. なぜタイミーを選んだか 転職活動で最も重視したのは、アナリストが多い環境で働きたいという点でした。 前職のチームでは、少人数ならではのスピード感や、幅広い領域を任せてもらえる環境に感謝していました。一方で、「この分析アプローチで良いのか」「他のアナリストはどう考えるのか」と壁打ちをしたい場面では、相談できる相手が限られるもどかしさもありました。分析は一人で完結させようと思えばできてしまう仕事ですが、だからこそ他のアナリストの視点に触れる機会が、自分の成長には欠かせないと感じていました。 一方タイミーは、データを事業の意思決定に活かすことに組織として積極的に投資している姿勢が、選考を通じて伝わってきました。この人数の差は単なる規模の話ではなく、分析の型や議論の文化に触れられる機会が増えることを意味します。ここでならさらにアナリストとしてのスキルを伸ばせると感じたので、タイミーに転職することにしました。 3. 入社して驚いたこと 1つのプロジェクトに複数のアナリストが関わる体制 まず驚いたのは、プロジェクトの体制です。タイミーでは1つのプロジェクトに必ずリードメンバーが一人いて、その上で領域ごとに担当が分かれる形で複数のデータアナリストが関わります。 前職では、基本的に1プロジェクトにつきアナリストは一人、という体制でした。同じチームにアナリストはいても、プロジェクトの深い文脈を理解しているのは担当者だけ。他のメンバーに相談したい場面でも、まずは理解の前提をそろえるために背景共有から始める必要がある場面がありました。 結果として、一人で抱え込んで意思決定する場面が多かったように思います。 タイミーでは、同じプロジェクトに対して最初から共通理解を持った仲間が複数いる。だから「この切り口でいいのか」「この数字の解釈、どう思う?」といった相談を、前提の説明なしにその場で進められます。共通理解を持った相談相手がいる状態は、分析の進めやすさを変えてくれました。 リードメンバーへの相談ハードルの低さ もうひとつ驚いたのが、リードメンバーへの相談のしやすさです。ここでのリードとは、直接の上司ではなく、プロジェクトの中でリードの立ち位置を担うアナリストを指します。Slackで気軽に聞けるのはもちろん、リモート中心の環境なので、「今すぐちょっと相談したい」と思えばGoogle Meetで時間をもらうこともできます。立ち上がり期には、その点に助けられました。 ダッシュボードに求められるクオリティの高さ 想像以上だったのが、ダッシュボードに求められる見やすさ・使いやすさの水準です。データアナリストだけでなく営業など異なる職種の方も使うため、数字が正しいだけでは足りず、誰が見ても意図が伝わり、迷わず使えることが要件になります。ダッシュボード単体で完結する品質が必要、という感覚は業務に入って分かった部分でした。 これらを通して感じるのは、アナリストが多い環境の価値が、日々の相談しやすさやアウトプット基準の高さという形で具体的に現れている、ということです。 4. 半年やってみて、これから 具体的な業務エピソードでいうと、あるプロジェクトで担当したダッシュボード構築の案件が思い浮かびます。ステークホルダーが使い道の想いは持っているものの、ダッシュボードとして何を見るべきかの要件は固まっていない状態からのスタートでした。まず叩き台を作り、リードのデータアナリストからフィードバックをもらいながら要件を詰めていき、最終的には当初の目的に沿った形に仕上がったと評価をいただけました。 このプロジェクトに限らず、目的を起点に形を組み立てていく仕事を経験する中で、半年で一番変わったと感じるのは、以前より目的を強く意識するようになったことです。事業部の業務を十分に理解できていない状態から関わることが多く、目的を掴めていないとアウトプットはすぐにブレてしまう。解像度高く目的を持ち続けることの優先度が、自分の中で上がりました。加えて、自分一人では思いつかない分析の切り口や議論の進め方に日々触れられるのは、データアナリストが多い組織ならではだと実感しています。 今後チャレンジしていきたいのは、AI活用の幅を広げることです。SQL生成などは日常的に使っていますが、最近特に手応えがあるのはダッシュボードのモック作成です。自作のClaudeスキルを使うことでイメージに近いモックが最初から出てきます。一方で実装工程はまだ手作業が中心なので、ここをもっと簡単に進められる仕組みを作っていけたらと感じています。 AI活用の面白さは、個人の業務が速くなるだけにとどまりません。データアナリストが多い組織とAIへの意識の高さが重なると、一人のスキルや得意領域を他のメンバーも扱えるように広げていけます。自分が扱えるスキルやチャレンジできる領域も自然と広がっていく、この循環こそ、この半年で感じているタイミーで働くことの面白さだと思っています。 もし、かつての自分と同じように小規模なデータアナリスト組織で働いていて、次のステージを考えている方や、AIを活用しながらデータアナリストとしての幅を広げていきたいと考えている方がいれば、この記事が何かのきっかけになれば嬉しいです。 We're Hiring! タイミーでは、ともに働くメンバーを募集しています! データアナリストのポジションも募集中です。カジュアル面談も行っていますので、少しでも興味がありましたら、お気軽にご連絡ください。 https://product-recruit.timee.co.jp/
こんにちは。昨年度まで社会人大学院生(修士課程)として学び、無事卒業した Hunachi です 🙌 研究生活の中で、 SICS 2026 と DEIM 2026 に参加し、論文の執筆や発表、ポスター発表をしてきました。 私の研究内容は「Android搭載端末での pKVM 環境を使ったセキュアな声紋認証の実装と評価」です 👀 このブログでは、 私が研究で扱っている pKVM ってなに? どんな研究をしていたのか(ざっくり) 学会に参加したり、論文を書いて発表してみての感想 社会人大学院生をしてみた感想 以上の4 本立てで、私の研究や大学院生活について紹介していきます。 SCISは函館開催でした。その時に食べたごっこ汁 🐟  pKVM ってなに? モバイル端末でも「セキュアな実行環境」が欲しい 最近のスマートフォンでは、生体認証・決済・オンデバイス AI(Gemini Nano など)と、機密性の高い処理を端末上で動かす場面がどんどん増えていますよね。 Android でのセキュアな環境としては 2014 年から Trusty TEE (Trusted Execution Environment)という ARM TrustZone ベースの隔離環境が使われてきました。Android の一般的なアプリが動作する環境( REE: Rich Execution Environment )とは、ハードウェアレベルで分離されたセキュアな環境です。そのため、堅牢なセキュリティを実現できます。 ただし TEE には以下の弱点があります。 利用できるメモリが 数 MB 程度 ととても小さい 開発のハードルがそれなりに高い 端末のベンダーによってセキュリティの質がまちまち 特に利用できるメモリが少ないので、DNN モデルなどを動かすのは大変困難です 😖 pKVM の登場 そこで Android 13 から導入された Android Virtualization Framework(AVF) の中核として、 pKVM(Protected KVM) という仮想化技術が組み込まれました。 ざっくり言うと、 ベースは Linux 由来の KVM (Kernel-based Virtual Machine) そこに「ホスト OS からも触れない VM( Protected VM, pVM )」という概念を載せる 端末の物理メモリ容量いっぱいまで使える隔離環境が手に入る という、Trusty TEE のメモリ制約を解消した比較的新しい技術です 🚀 ちなみに数年前、「 Pixel で root を取らずに Linux(Arch や Ubuntu)が動かせる 」という話題、目にした方もいるんじゃないでしょうか。Danny Lin 氏の Nestbox というアプリで Android 上に Linux VM を立ち上げるものです( 参考記事 )。この基盤になっているのがまさに pKVM で、「ホスト OS から保護された VM」という枠組みを使えば、セキュリティ用途だけでなく汎用的な OS だってホストできてしまう、というのを実証した一例です。 pKVM のアーキテクチャをざっくり ARM のアーキテクチャでは、特権レベルが Exception Level(EL) という階層で分かれています。pKVM 環境に関する階層分けはこのようになっています。 EL2 : pKVM ハイパーバイザ EL1 : Android Host OS と Protected VM EL0 : ユーザアプリケーション EL2 で動く pKVM が ステージ 2 ページテーブル を使って、ホスト OS からの pVM メモリへのアクセスを物理的に遮断します。さらに IOMMU を使うことで、DMA デバイス経由の不正アクセスもブロックしてくれます。 また、pKVM上で動かすプログラムはC/C++で書く必要がありますが、TEE向けアプリの開発に比べれば容易です。 セキュアな環境を成り立たせる仕組み pKVM(AVF)の凄いところは、ただメモリを隔離するだけじゃない点です。 pvmfw (Protected VM Firmware)がペイロードの署名を検証して改ざん検知 DICE (Device Identifier Composition Engine)プロトコルで pVM ごとのシークレットを導出 DICEで導出したシークレットからsealing secretを生成し、さらにsealing keyを作成して永続データなどを暗号化 pVM 終了時にはハイパーバイザがメモリページをゼロクリアして残留防止 つまり、コードの正当性 → 起動時のシークレット → 永続データ → 終了時の残留防止 まで一貫してハイパーバイザがケアしてくれる、という設計です。 そして 2025 年 8 月、Google が pKVM で SESIP Level 5 認証を取得したと発表しました 🎉 SESIP(Security Evaluation Standard for IoT Platforms)は IoT デバイス向けセキュリティ評価基準で、Level 5 は最高レベルです。 大規模消費者向けに展開されるソフトウェアセキュリティシステムとして取得したのは世界初 で、最新かつかなりセキュアな技術であることがわかります( Google Online Security Blog )。 私の研究をざっくり やったこと ここからは自分の研究をかなりざっくり紹介します。 タイトルは「 Google Tensor 搭載端末の pKVM におけるセキュアな音声処理および声紋認証の実装手法と課題の検討 」です。 論文はこちらから読めます 👉 DEIM2026 3D-01 すごく簡単に言うと、 (pKVM環境)上で話者識別のDNNモデルを動かし、実用可能な処理速度で動作する声紋認証システムアプリを実現 pKVM のメモリアクセス特性を細かく測定 提案システムの認証精度・処理時間・pKVMのVM 起動時間などを多角的に評価 を行った論文です。 そしてありがたいことに、この発表で DEIM 2026 学生プレゼンテーション賞 をいただきました 🎉 一緒に研究を進めてくれた共著の先生方、コメントをくださった皆さん、本当にありがとうございました 🙇 まだまだ改善の余地がたくさんある研究内容ですが、興味のある方は論文を読んでもらえると嬉しいです 🙇 学会の感想 SICS に参加した感想 SICSは、以前は暗号系の発表が多かったようですが、最近は傾向が変わってきたようです。セキュリティ関連の発表では、高レイヤの話も多く見られました。特にLLMのセキュリティや研究方法に関する講演や発表が印象的でした。最先端のLLMの研究をしている日本人研究者もいることや、LLMのセキュリティの研究がどこまで進んでいるかの話を聞くことができ、面白かったです。 DEIM に参加した感想 たくさんの学生さんが参加している学会で、色々な研究の発表やポスター発表を見ることができました。特に土日にリモート開催だったので、社会人の私にとって大変嬉しかったです。LINEヤフーさんのDBの話なども興味深く聞かせていただきました。 最近の研究は、やはりLLM関連が多く、自分も研究でLLMも扱えるよう、ある程度は詳しくならないといけないと思いました。 論文執筆・発表・ポスター発表をしてみた感想 学部時代の研究をそのまま続けなかったこともあり、成果が出せる研究テーマにたどり着くまで時間がかかり、とても大変でした。一方で、先生方の助言やAIの活用により、先行研究や最新技術の調査を効率化できました。その結果、成果を出せてよかったです。 また論文を執筆するにあたり、慣れない部分については、AIに手助けしてもらいながら執筆しました。4年前の学部時代や高専時代に論文を書いた時と比べて、LaTeXのエラーに悩まされる時間や、誤字脱字の修正にかかる時間が、ほぼゼロになりました。本当に楽な時代になったなと感じます。 発表では厳しめの質問をいただくこともありましたが、それ以上に嬉しいこともありました。似た研究をしている方が少ないにもかかわらず、特にDEIMでは私の研究に興味を持って質問してくださる方が多く、とても嬉しかったです。 人に自分の研究内容を伝えることは、社会人におけるプレゼンテーションを行う際にも活かせるなと思いました。 社会人大学院生(修士課程)をしてみた感想 大学の教授やD進している同期、夫の家事サポートがあったからこそ、卒業できました。関係者の皆さんに感謝しかありません。 人におすすめできるかというと、とても忙しい生活スタイルになるため、研究が趣味な人以外にはおすすめしにくいです。ただ、AIの活用で調査や文章執筆が容易になった今の時代だからこそ、「チャレンジは可能だ」と思います。 私の感じたメリット・デメリット メリットは、金銭的な問題で困りにくいことです。いろいろな理由があり、猫と暮らしている自分には働かないという選択肢がなかったため、社会人学生を選びました。働きつつ学生でいることを許してくれた大学の教授には感謝しかありません。そのおかげで猫と暮らしつつ学費も安定して払うことができました。 デメリットは以下のとおりです。 大学以外のことをするプライベートな時間がかなり少なくなること 研究に時間を費やす必要があるのはもちろんのこと、学会や授業の参加で有給が消費されます 仕事や大学が忙しい時期には睡眠時間以外はパソコンの前にいる、というような不健康な生活が日常になること 学生らしい生活ができないこと 私の場合は、大学に行く時間が取れず在宅で研究を行なっていた関係で、友人と研究室でおしゃべりしたり、飲み会や合宿への参加などはできませんでした。 また私は、学部時代に大学院の授業単位を取得できる制度を活用していたため、大きな問題はありませんでした。ただし、大学や単位の取得状況によっては、授業のために有給を使う必要が出てくるかもしれません。さらに、大学生らしい生活が送れないのはもったいないと感じるため、個人的には可能であれば通常の大学院生として通うほうがよいと思います。 ※ 私の大学生活のほとんどはコロナでオンラインだった関係で大学生活をまともにしたことがないので意見が偏っている可能性もあります。 ただ、事情があり社会人になる必要がある人やすでに社会人の方で、研究をしたい・続けたい人は十分頑張ってみる価値があると思うので応援しています 🚩 おわりに 引き続きpKVMや研究関連の勉強は続けようと思っています 🧑‍🎓 最後まで読んでくださってありがとうございました!