TECH PLAY

タイミー

タイミー の技術ブログ

294

はじめに こんにちは! タイミーで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や研究関連の勉強は続けようと思っています 🧑‍🎓 最後まで読んでくださってありがとうございました!
こんにちは。タイミーのデータエンジニアリング部 DSグループでMLOpsを担当しているYukitomoです。 私たちのチームでは多くのPythonアプリをモノレポで管理していますが、Dependabotによる依存関係更新PRが多すぎることが運用課題でした。本記事では、Renovateへの移行によって「更新PRの粒度と数をコントロールできる運用」を実現するまでの設計判断と、Python + uv環境特有の注意点を共有します。 この記事の想定読者 Pythonのモノレポ環境で、複数のアプリケーションやライブラリを運用している方 Dependabotが生成する大量の更新PRの対応に疲弊しており、運用を効率化したい方 Renovateへの移行を検討している、または導入したが設定(packageRules)のベストプラクティスに悩んでいる方 パッケージマネージャーに uv を採用している(または検討している)方 要約(TL;DR:この記事でわかること) 本記事では、Python + uv環境でRenovateを運用する際の課題とその解決策(新しすぎるパッケージの除外設定、Google Cloud WIFにおけるブランチ名の文字数制限の回避など)を整理し、実践的なrenovate.json5の設定ノウハウを解説します。 背景 近年はサプライチェーン攻撃が現実的なリスクになっており、Trivyの侵害以降も Python モジュールや JS ライブラリを狙った攻撃が継続して観測されています。PyPI など外部エコシステムに依存する以上、これまで以上に「依存関係をどう安全に運用するか」を真面目に考える必要があります。 一方で、依存関係を「安全に」保つには、継続的にアップデートを回し続ける必要があります。ここで次の課題になるのが、運用対象が増えたときに更新対応のコストがスケールしてしまう点です。 私たちもDependabot運用の効率化を進めてきましたが *1 、アプリごとにパッケージ管理へ移行した結果、モノレポ内のpyproject.tomlが増えました。2026年5月時点では、DSグループだけでも約70のPythonアプリケーション/ライブラリを扱っています。Dependabotは脆弱性の検知とPR作成を行ってくれる一方で、依存関係ごとにPRが分割されます。そのため、対象が増えるほど対応コストが急増します。 そこでこの課題を解決するため、Renovateを導入し「更新をまとめて扱える運用」へ切り替える方針にしました。本記事では、公式ドキュメントや公開されている設定例を参考にしつつ、私たちが重視した設定ポイントを整理します。 この記事の前提 言語: Python 依存関係ファイル: pyproject.toml / uv.lock 動作環境: GitHub & Google Cloud 目的: Renovateで「脆弱性対応」と「定常アップデート」を破綻なく回す(PRの数と粒度をコントロールする) 設定ファイル(.renovaterc.json5) 設計方針 私たちが設定で重視したのは以下の3点です。 PRの粒度をコントロールする — patch / minor / vulnerability を適切にグルーピングし、PRの本数を削減する サプライチェーンリスクを軽減する — 公開直後のバージョンを即座に採用しない 小さく始める — まず許可リスト方式で必要な更新だけを有効化し、段階的に広げる 全体像 以下が設定ファイルの抜粋です(各設定の詳細は後述)。 // .renovaterc.json5 より一部抜粋 { extends : [ " config:best-practices " ] , minimumReleaseAge : " N days ", lockFileMaintenance : { enabled : true , branchTopic : " lfm ", // GCP WIF 127-byte limitに対応するためブランチ名を省略 minimumReleaseAgeBehaviour : " timestamp - optional " // 一時的な対応 } , vulnerabilityAlerts : { groupName : " maintenance ", groupSlug : " maint ", minimumReleaseAge : " 14 days " , } , packageRules : [ // packageFileDirをブランチ名に含めつつGCP WIF 127-byte limitに対応するためブランチ名を省略 { matchFileNames : [ " base_containers/base/** " ] , additionalBranchPrefix : " {{{replace 'base_containers/base/' 'b_b/' packageFileDir}}}/ " , } , // packageRuleを一旦無効化 { matchPackageNames : [ " ** " ] , enabled : false } , // グルーピング ---------------------------------------------------- // ルール 1: { matchUpdateTypes : [ " patch " ] , enabled : true , groupName : " maintenance ", groupSlug : " maint " , } , // ルール 2: { matchUpdateTypes : [ " minor " ] , matchJsonata : [ " isVulnerabilityAlert = false " ] enabled : true , groupName : " minor updates ", groupSlug : " minor ", dependencyDashboardApproval : true } , // ルール 3: { matchPackageNames : [ " ** " ] , matchJsonata : [ " isVulnerabilityAlert = true " ] enabled : true , // この2つは vulnerabilityAlerts で設定した値で上書きされます groupName : " maintenance ", groupSlug : " maint " , } , // マイナーレベルでの破壊的な更新の抑制 ただし脆弱性対応を除く { matchJsonata : [ " isBreaking = true and not(isVulnerabilityAlert) " ] , enabled : false , }, ] , // バージョンの更新 bumpVersions : [ { bumpType : " patch ", filePatterns : [ " {{packageFileDir}}/pyproject.toml " ] , matchStrings : [ " version \\ s*= \\ s* \" (?<version>[^ \" ]+) \" " ] } , { bumpType : " patch ", filePatterns : [ " {{packageFileDir}}/uv.lock " ] , matchStrings : [ " name = \" [^ \" ]+ \"\\ nversion = \" (?<version>[^ \" ]+) \"\\ nsource = \\ { (?:editable|virtual) = \"\\ . \" \\ } " ] } ] } 設定項目の説明 config:best-practices を土台にする Renovateは設定可能な項目が多く、ゼロから組むと必ず設定が肥大化します。そこでまずは extends: ["config:best-practices"] をベースにして、一般的に安全側なデフォルトを取り込みました。 ベース設定を取り込んだうえで、運用上の要所(PRの粒度、セキュリティ例外、ロックファイルの扱い)だけを packageRules で上書きしています。 minimumReleaseAge (新しすぎるリリースを避ける) サプライチェーン観点では「出たばかりのバージョンを即座に拾う」ことが常に正解とは限りません。そこで minimumReleaseAge を設定し、公開直後のバージョンを一定期間は自動採用しないようにしています。 ここで一つ注意点があります。 minimumReleaseAge が効くのは、Renovateが pyproject.toml のバージョン指定を直接書き換える通常の更新(Standard Update)だけです。 一方、 pyproject.toml には記載されず uv.lock にだけ現れる間接依存(依存パッケージがさらに依存しているパッケージ)の更新は、Renovateの lockFileMaintenance が担当します。 lockFileMaintenance は内部で uv lock を実行しますが、現時点ではRenovateから uv lock に --exclude-newer 等のオプションを渡す手段がありません *2 。つまり、間接依存に対しては minimumReleaseAge による「新しすぎるバージョンの除外」が効かないのです。 そこで、uv自身が持つ exclude-newer 機能で補完しています。各 pyproject.toml に以下のように記述することで、 uv lock 実行時にuv側で公開直後のバージョンを除外します *3 。 # pyproject.toml [tool.uv] exclude-newer = "n days ago" # uv 0.10+ で相対日付指定が可能 # uv.lock (uv lock 実行時、以下のように変換されて保存されます) [options] exclude-newer = "2026-04-07T08:39:48.633055Z" exclude-newer-span = "PnD" この二重構成により、直接依存は Renovate の minimumReleaseAge 、間接依存は uv の exclude-newer でそれぞれカバーし、すべての依存パッケージに対して「新しすぎるバージョンを即座に採用しない」制約を適用しています。 lockFileMaintenance (Google Cloud Workload Identity Federationの制約対策とminimumReleaseAgeBehaviourの調整) 明示的に有効化します。また、プライベートなモジュールを独自のArtifactoryやNexusなどのパッケージインデックスに配置・利用することはよくあると思います。我々はGoogle Cloudを利用しており、プライベートなパッケージは Google Artifact Registry に保管しています。この場合、RenovateからGoogle Cloudにアクセスが発生し、Workload Identity Federation (WIF) を使う構成だと、subject claimにブランチ名が入ります *4 。ここに127 bytes制約があり、Renovateが生成するブランチ名が長いと認証に失敗します。対策として、作成するブランチ名を短縮化するため、 branchTopic の値を短縮しています(デフォルトでは “lock-file-maintenance”)。 この値を調整して通常の更新とブランチ名を一緒にし、PRを一緒にできないかと試したのですが、 Grouping lockfile maintenance with other update types is not supported というエラーメッセージが出たため断念しました。 minimumReleaseAgeBehaviour は、本来はデフォルト値の "timestamp-required" のほうが自然です。ただし、(*2)のrenovate のPRがマージされるまでは "timestamp-optional" にしておかないと、 lockFileMaintenance のPRがRenovate botの承認待ちになってしまうため注意してください。 vulnerabilityAlerts AIエージェントの助けを借りてRenovateのコードを確認し、試行錯誤する中で気づいたのですが、脆弱性対応に関する一部の設定は、packageRules で指定してもグローバルの vulnerabilityAlerts の値で上書きされます。具体的には、後述するグルーピングルール3の内容や、前項の minimumReleaseAge の設定です。 packageRules 以下、設定の意図をダイジェスト順に説明します。 1) packageFileDirをブランチ名に追加 複数のモジュールの更新を集約するために packageFileDir を additionalBranchPrefix に利用しています。モノレポにおけるadditionalBranchPrefixの一般化( packageFileDir 由来のprefixを使う等)の詳細は別記事 *5 に委ねます。lockFileMaintenanceの項と同様、ブランチ名が長くなるため、Google Cloud Workload Identity Federation (WIF) の制約対策のためにブランチ名を短縮しています。 2) いったん全ルールを無効化して「許可リスト方式」にする { matchPackageNames: ["*"], enabled: false } 最初に全パッケージを enabled:false に落としてから、必要な更新だけを後続ルールで enabled:true に戻しています。Defaultを使いこなす方が安全とは思うのですが、まず最初は小さく、自分達でコントロールできる範囲から始めようとしてこのような設定としました。 3) PRのグルーピング(patch / minor / vulnerability) 別記事(*5)にもありますが依存関係更新の運用コストは「PRの数」と「レビューのコンテキスト切り替え」で決まるので、グルーピングが最重要です。 グルーピングルール1: patch更新: まとめて1本(メンテナンス枠) グルーピングルール2: minor更新: まとめて1本(ただし dependencyDashboardApproval:true で人間の許可待ちにする) グルーピングルール3: vulnerability: patchと同じグループに合流させ、優先して処理できるようにする Minor更新は、まずはダッシュボードで確認する運用にします。運用がうまく回りそうであれば、将来的にpatch groupingに合流させる想定です。 また、renovateのconfigは後ろの条件が前の条件を上書きするため、グルーピングルール3は最後に配置する必要があります。4)で扱う isBreaking に関する packageRule も同様に、後ろに配置してください。 4) 0.y.z系の“実質breaking”を抑止する SemVer上はminorでも、 0.y.z のようにリリースされていない場合、minor更新がbreakingになり得ます。そこで isBreaking = true を検知した更新は原則止めています。 ここは「通常アップデートは保守的に、ただし脆弱性対応は止めない」方針にしたいため、脆弱性アラート( isVulnerabilityAlert:true )にはこの抑止が効かないようにしています(=脆弱性があるものはIsBreakingでも検出させる)。 bumpVersions RenovateをGitHubで動作させるには、1) GitHub Actionsとして renovatebot/github-action を利用する方法と、2) 作成元のMend社が提供するMend Renovate Appを利用する方法があります。設定が簡単なことからタイミーでは後者を利用しているのですが、それ故の制約もあり、 postUpgradeTasks のように任意のコマンドを実行するようなことができず、コマンド実行できれば簡単なversionの更新もそのままでは実現できません *6 。解決策として、 bumpVersions を使い、正規表現で pyproject.toml と uv.lock の両方を同時に更新しています。なおbumpVersionsは オフィシャルドキュメントの最初にはpackageRulesの外の記述例があるのですが、マッチ表現と組み合わせpackageRulesの中に記述することもできます *7 。上記のサンプルではpackageRulesの外で記述していますが、我々はlockFileMaintenanceとそれ以外でbump up の方法を一部変えるため、 matchUpdateTypes を lockFileMaintenance とそれ以外で分離し、packageRulesの中に両方を記述しています。 // 応用例: lockFileMaintenanceと通常の更新を別々に記述する場合 (packageRulesの中に記述する) packageRules : [   : { matchUpdateTypes : [ " lockFileMaintenance " ] , bumpVersions : [ .. ] } , { matchUpdateTypes : [ " major ", " minor ", ... ] , bumpVersions : [ .. ] } ] まとめ Dependabotの「依存ごとにPRが分割される」性質は、対象が増えるほど脆弱性対応の運用コストを押し上げる。 Renovateは packageRules を適切に設定することで、上記の運用コストの削減を行うことができる。 Python + uv の場合、 lockFileMaintenance のオプションとして指定できない exclude-newer については pyproject.toml に直接記述することで問題を回避できる。 Mend Renovate Appを利用する場合においても、bumpVersionsを利用することで pyproject.toml / uv.lock それぞれのversionの更新を実現できる。 We’re Hiring! 現在、タイミーでは、データサイエンスやエンジニアリングの分野で、共に成長し、革新を推し進めてくれる新たなチームメンバーを積極的に探しています!  現在募集中のポジションは こちら です! また、気軽な雰囲気での カジュアル面談 も随時行っておりますので、ぜひお気軽にエントリーしてください。↓ 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう! References *1 : https://tech.timee.co.jp/entry/2024/10/15/101953 *2 : uv lock コマンド自体は --exclude-newer オプションを持つのですが、Renovateからこのオプションを2026年5月13日現在渡せていません。他のパッケージマネージャーに関しても同様でIssueとして登録されており、uvに関しては既にPRも出ているようですがリリースはまだされていません。 https://github.com/renovatebot/renovate/issues/41652 *3 : 0.10より古いバージョンのuvでも exclude-newerは記述できるのですが ”N days ago” のような相対的な評価がこのバージョンから記述できるようになりメンテナンスが非常に楽になっています。 https://github.com/astral-sh/uv/releases/tag/0.10.0 *4 : https://docs.cloud.google.com/iam/docs/troubleshooting-workload-identity-federation#error-google-subject-too-long *5 : 近日公開予定 *6 : uvを利用する場合、 pyproject.toml / uv.lock に記述された自身のversionの更新はuv version --bump patchのように実現できます。 *7 : https://docs.renovatebot.com/configuration-options/#bumpversions
はじめに こんにちは、タイミーでエンジニアをしている徳富( @yannkazu1 )です。 クラウドネイティブ会議2026 で発表された「 ペアーズ本番環境でのcgroup-aware化との死闘録 」がめちゃくちゃ面白かったので、自分の手でも体感したくなりました。 GoのGOMAXPROCSがコンテナのCPU制限を無視するって、実際に見るとどうなるのか? 過剰並列のスループット低下って、数字で見るとどのくらいインパクトがあるのか? スロットリングとスレッド数の関係を自分の目でたしかめたい! 自分で動かして数字を見ないと腑に落ちないタイプなので、 ローカルのMac環境で全部再現してみました。 発表の要約 ペアーズのバックエンド pairs-main はGo製でAmazon EKS上で稼働。48コアのNodeで limits.cpu: 5000m (5コア)のPodが動いていたが、 GoのGOMAXPROCSがデフォルトで48 (=Node全体のコア数)になっていた。これにより以下の問題が発生: 過剰並列 : 5コアしか使えないのに48スレッドが走る → Goスケジューラのオーバーヘッド増大 CPUスロットリング : cgroupのクォータ(CPU時間の上限)をスレッドが共食い → 全スレッドが同時に停止 監視の死角 : CPU使用率は正常に見えるが、実際はスロットリングで断続的に停止 同じ問題がHAProxy( nbthread=48 、CPU制限1コア)でも発生していた。 これらをcgroup-awareな設定(GOMAXPROCS=5, nbthread=1)に修正したところ、大幅に改善した、という話でした。 用語の整理 ここから先で出てくる「コア」「GOMAXPROCS」「クォータ」「スロットリング」あたりがピンと来なくても大丈夫です。記事全体で繰り返し登場するので、最初にざっくり整理しておきます(すでに馴染みがある方はスキップでOK)。 CPUコア・プロセス・スレッド 用語 ざっくりした意味 CPUコア 計算を実行する物理的な実体。1コア = 同時に1つの処理を進められる プロセス 動いているプログラム1つ分の単位 スレッド プロセス内で実際にCPUに割り当てられる作業の単位。1プロセスは複数スレッドを持てる ざっくり言うと、 コアの数 = 同時に進められるスレッドの数の上限 です。8コアのCPUなら、ある一瞬に進行できるのは最大8スレッドまで。それ以上のスレッドを立ち上げた場合は、OSが順番にコアを割り当て直しながら回します(= コンテキストスイッチ)。 コンテナと cgroup 用語 ざっくりした意味 コンテナ 同じサーバー上で複数のアプリを互いに干渉しないように動かす仕組み(Docker や Kubernetes の中身)。実体はホストのカーネルをそのまま使う 「namespaces で見える範囲を、cgroup で使える量を制限したプロセス(群)」 にすぎず、VM のように専用カーネルを持つわけではない cgroup (Control Groups) Linuxカーネルの機能で「このプロセス群はCPUをここまで・メモリはここまで」と上限を設定する仕組み CPU制限 「このコンテナはCPU 1コア分まで」のような上限設定。実体は cgroup の cpu.max ファイル コンテナの「CPU 0.5コアまで」という設定は、Linuxカーネルが cgroup を通じて「100msのうち50msまでしかCPUを使わせない」という形で強制します。この 100msの枠を「ピリオド」、その中で使ってよい時間量を「クォータ」 と呼びます( cpu.max: 50000 100000 なら「100msのうち50ms使える = 0.5コア相当」)。 CFS スケジューラ Linux のデフォルトの CPU スケジューラを CFS(Completely Fair Scheduler) と呼びます。先ほどの「ピリオド」「クォータ」は、CFS が持つ 帯域制御(Bandwidth Controller) という機能の用語で、cgroup の cpu.max の値を実際にスレッドへ適用する(=クォータを使い切ったら停止させる)のはこの CFS の仕事です。 つまり「cgroup が制限値を持ち、CFS がそれを実施する」という分担関係。後の実験で出てくる nr_periods (CFS が時間を区切る単位の総数)や nr_throttled (CFS が停止させたピリオドの数)も、この CFS 帯域制御の統計を見ています。 Goroutine と GOMAXPROCS(Go特有の話) 用語 ざっくりした意味 goroutine Goの軽量スレッド。OSスレッドより遥かに軽く、1プロセスで数万〜数百万個立ち上げられる OSスレッド OSが実際にCPUにスケジュールするスレッド。コアを取り合うのはこちら GOMAXPROCS Goランタイムが同時に走らせるOSスレッドの数の上限。デフォルトはホストのCPUコア数 goroutine を何万個立ち上げても、Goランタイムは GOMAXPROCS 個の OSスレッドの上にそれらを多重化して実行します。つまり同時に CPU を握っているのは最大でも GOMAXPROCS 個。この割り当てを管理するのが Goスケジューラ です。 ポイントは、 コンテナのCPU制限が下がってもデフォルトの GOMAXPROCS はホストのCPU数のまま ということ。これがそもそも今回のテーマで、後の実験でその挙動を実際に確かめます。 過剰並列 CPU 制限よりも多くのスレッド(や goroutine、ワーカー)を同時に走らせている状態 を指します。たとえば 5 コア相当の CPU 制限に対して GOMAXPROCS=48 なら、約 9.6 倍の過剰並列。実際に走れるのは制限分のスレッドだけなので、残りはスケジューラの上で順番待ちをしつつ、共有クォータを早食いし合うことになります。 Go の GOMAXPROCS に限った話ではなく、HAProxy の nbthread 、Nginx の worker_processes 、Puma の workers など、 「並列数のデフォルトがホスト CPU 数に依存する」設定はすべて同じ構造で過剰並列を起こします 。 CPUスロットリング cgroupでCPU 0.5コア分に制限されたコンテナが、たくさんのスレッドでCPUを一気に使おうとすると、Linuxカーネルが 「クォータを使い切ったので、次のピリオドまで全スレッド一時停止」 と強制的にブロックします。これが CPUスロットリング です。 スロットリングが頻発すると、レスポンスが断続的に止まったり、スループットが落ちたりします。その結果、「なぜか遅延がスパイクする」原因になっているケースが多いです。発生状況は /sys/fs/cgroup/cpu.stat に出力されており、本記事では以下の3指標を追います: nr_periods : スケジューラの計測単位(ピリオド = 100ms)の総数 nr_throttled : そのうちスロットリングが起きたピリオドの数(回数) throttled_usec : スロットリングで実際にCPUが止められた累積時間(マイクロ秒) 「回数」だけでなく「 累積停止時間 」も見るのが重要だ、というのが発表の山場の一つで、後の実験3でその違いがハッキリ出ます。 Thundering Herd スロットリングで停止していた全スレッドが、 次のピリオドのリセットで一斉に走り出し、また一瞬でクォータを食い潰して同時に止まる 、というサイクルが繰り返される状態を 「Thundering Herd(雷鳴の群れ)」 と呼びます。元はソケット accept など I/O 文脈の用語ですが、cgroup の帯域制御下でも同じ構造の問題が起きます。スレッド数が多いほど被害が大きくなるのは、ここに端を発しています。実験4でその挙動を観察します。 cgroup-aware プログラムやライブラリが cgroup の制限( cpu.max など)を自分で読み取り、その値に合わせて並列度を調整する 設計のことを 「cgroup-aware」 と呼びます。Go 1.25 以降のランタイムや uber-go/automaxprocs は cgroup-aware に GOMAXPROCS を設定します。逆に Go 1.24 以前のように cgroup を見ずにホストの CPU 数だけ見る挙動は「cgroup-aware ではない」状態で、今回の過剰並列はそこから生まれています。 この記事で検証すること # 検証テーマ 発表でのポイント 1 GOMAXPROCSのデフォルト値 コンテナのCPU Limitを無視してホストのCPU数になる 2 過剰並列のパフォーマンス影響 GOMAXPROCSが大きすぎるとスループットが低下する 3 CPUスロットリングの発生 スレッド数が多いほどクォータを早く消費し、停止時間が増える 4 スレッド数とスロットリングの相関 スレッド数に比例して throttled_usec が増加する 1. ローカル環境構築(Mac) 前提条件 macOS (Apple Silicon / Intel 両対応) Docker Desktop がインストール済み なぜDockerで検証できるのか cgroup(Control Groups)は Linuxカーネルの機能 で、macOS 自体には存在しません。しかし Docker Desktop は内部で Linux VM を動かしており、コンテナはその Linux 上で動作します。 ┌─────────────────────────────────────────────┐ │ macOS │ │ ┌────────────────────────────────────────┐ │ │ │ Docker Desktop (Linux VM) │ │ │ │ ┌──────────────────────────────────┐ │ │ │ │ │ コンテナ │ │ │ │ │ │ /sys/fs/cgroup/cpu.max ← ここ! │ │ │ │ │ │ /sys/fs/cgroup/cpu.stat │ │ │ │ │ └──────────────────────────────────┘ │ │ │ └────────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ Docker の --cpus フラグは Kubernetes の limits.cpu と同じく cgroup の cpu.max に変換されます。つまり Kubernetes と同じ仕組みをローカルで再現 できます。 Docker Kubernetes cgroup v2 --cpus=0.5 limits.cpu: 500m cpu.max: 50000 100000 --cpus=1.0 limits.cpu: 1000m cpu.max: 100000 100000 --cpus=5.0 limits.cpu: 5000m cpu.max: 500000 100000 セットアップ手順 Step 1: Docker Desktop のインストール Docker Desktop for Mac からインストール。 docker --version # Docker version 27.x.x, build xxxxxxx Step 2: 検証用 Go アプリケーション 本記事の検証コードは以下のリポジトリにまとめています: hirosi1900day/cgroup-throttling-lab git clone https://github.com/hirosi1900day/cgroup-throttling-lab.git cd cgroup-throttling-lab 3つのモードを持つGoアプリケーションを書きました。 モード 用途 info GOMAXPROCSの値とcgroupの設定を表示 benchmark CPU負荷をかけてスループットを計測 throttle-demo CPU負荷をかけてスロットリングの Before/After を表示 コード解説 各パートを順に見ていきます。 1. CPU負荷を発生させる関数 // cpuIntensiveWork はCPU負荷をかける計算処理 // 平方根と三角関数を1万回ループし、意図的にCPUを使い切る func cpuIntensiveWork() float64 { result := 0.0 for i := 0 ; i < 10000 ; i++ { result += math.Sqrt( float64 (i)) * math.Sin( float64 (i)) } return result } この関数が実験の要です。 math.Sqrt と math.Sin の計算を1万回繰り返すことで、 純粋なCPU負荷 を発生させます。I/O待ちが一切ないので、GOMAXPROCS(=ワーカースレッド数)の影響がダイレクトに現れます。 2. infoモード — GoランタイムとcgroupのCPU設定を表示 func showRuntimeInfo() { // runtime.GOMAXPROCS(0) は「現在の値を返し、変更しない」 // ← これがコンテナのCPU制限と一致しているかがポイント fmt.Printf( "GOMAXPROCS: %d \n " , runtime.GOMAXPROCS( 0 )) fmt.Printf( "NumCPU: %d \n " , runtime.NumCPU()) // --- cgroup のCPU制限を直接読む --- // /sys/fs/cgroup/cpu.max は cgroup v2 のCPU制限ファイル // 中身は "クォータ ピリオド" の形式(例: "100000 100000") // Kubernetes の limits.cpu や Docker の --cpus がここに反映される if data, err := os.ReadFile( "/sys/fs/cgroup/cpu.max" ); err == nil { fmt.Printf( "cpu.max: %s" , string (data)) } // /sys/fs/cgroup/cpu.stat はCPUスロットリングの統計情報 // nr_periods: CFSスケジューラのピリオド(100ms)の総数 // nr_throttled: スロットリングが発生したピリオドの数 // throttled_usec: スロットリングでCPUが停止した累積時間(μs) if data, err := os.ReadFile( "/sys/fs/cgroup/cpu.stat" ); err == nil { fmt.Printf( "cpu.stat: \n %s" , string (data)) } } このモードでは、 GoランタイムがcgroupのCPU制限を認識しているか を見ます。Go 1.24以前では、 GOMAXPROCS がホストのCPU数のままなのが確認できるはずです。 3. benchmarkモード — スループットの計測 func runBenchmark() { // 環境変数でベンチマーク時間とgoroutine数を制御可能にしている duration := 10 * time.Second // BENCH_DURATION で変更可 goroutines := 100 // BENCH_GOROUTINES で変更可 // --- ここからが計測のコア --- var totalOps atomic.Int64 // goroutine間で安全にカウントを共有 var wg sync.WaitGroup // 全goroutineの完了を待つ // タイマーで終了を通知するチャネル done := make ( chan struct {}) go func () { <-time.After(duration) close (done) // ← 全goroutineに「終了」を伝える }() // goroutines個のgoroutineを起動し、それぞれが独立にCPU負荷をかける // これらのgoroutineは GOMAXPROCS 個のワーカースレッドに // Goスケジューラによって割り当てられる for i := 0 ; i < goroutines; i++ { wg.Add( 1 ) go func () { defer wg.Done() localOps := int64 ( 0 ) // goroutineローカルでカウント(競合を避ける) for { select { case <-done: totalOps.Add(localOps) // 最後にまとめて加算 return default : cpuIntensiveWork() // CPU負荷をかけ続ける localOps++ } } }() } wg.Wait() // Ops/sec = 単位時間あたりの処理回数 // この値が高いほどスループットが良い opsPerSec := float64 (totalOps.Load()) / elapsed.Seconds() } 100個のgoroutineが cpuIntensiveWork() を呼び続け、それらがGOMAXPROCS個のOSスレッド上でスケジュールされる構造。CPU制限がある環境では、スレッドが多いほどcgroupのクォータを早く使い切る、スロットリングでスループットが落ちるわけです。 (脱線)ベンチマークコードの工夫 — キャッシュコヒーレンシの話 cgroup の検証とは直接関係ないですが、このベンチマークコードには「計測自体が結果を歪めないための工夫」が入っています。せっかくなので解説します。 select + default でノンブロッキングに終了チェックしつつCPU処理を回し続ける、というのはGoの定番パターンなので軽く触れるだけにして、本題はカウンタの設計です。 localOps := int64(0) // goroutineローカル(普通のint) for { select { case <-done: totalOps.Add(localOps) // ← 終了時に1度だけatomic操作 return default: cpuIntensiveWork() localOps++ // ← 普通のインクリメント。超高速 } } ループ内では localOps++ (普通の int インクリメント)だけを使い、終了時に1度だけ totalOps.Add(localOps) ( atomic 操作)で合算しています。 「毎回 totalOps.Add(1) でいいのでは?」と思うかもしれませんが、それだと100個の goroutine が同じメモリアドレスに毎ループ書き込み合い、 キャッシュコヒーレンシ(Cache Coherency) のオーバーヘッドで性能が大きく落ちます。 キャッシュコヒーレンシとは まず前提として、CPUがデータにアクセスする仕組みを整理しておきます。 CPU のメモリ階層 CPUが変数やデータを読み書きするとき、毎回メインメモリ(DRAM)まで取りに行くのは遅すぎます。そこで CPUは メモリ階層(Memory Hierarchy) という多段のキャッシュ構造を持っています: ┌─────────────────────────────────────────────────┐ │ CPU コア │ │ ┌───────────┐ │ │ │ レジスタ │ ← 最速(~0.3ns)、数十〜数百個 │ │ └─────┬─────┘ │ │ ┌─────┴─────┐ │ │ │ L1 キャッシュ│ ← 32〜64KB / コア、~1ns │ │ │ (データ+命令)│ │ │ └─────┬─────┘ │ │ ┌─────┴─────┐ │ │ │ L2 キャッシュ│ ← 256KB〜1MB / コア、~3-10ns │ │ └─────┬─────┘ │ │ │ ┌──────────┐ │ │ │ │ TLB │ ← 仮想→物理アドレス │ │ │ │ │ 変換のキャッシュ │ │ │ └──────────┘ │ └────────┼────────────────────────────────────────┘ ┌─────┴─────┐ │ L3 キャッシュ│ ← 数MB〜数十MB、全コア共有、~10-30ns └─────┬─────┘ ┌─────┴──────────────────┐ │ メインメモリ(DRAM) │ ← 数GB〜数百GB、~50-100ns └─────┬──────────────────┘ ┌─────┴──────────────────┐ │ ストレージ(SSD/HDD) │ ← ~10,000ns (SSD) 〜 10,000,000ns (HDD) └───────────────────────┘ 階層 容量 レイテンシ 特徴 レジスタ 数百バイト ~0.3ns CPUが直接演算する場所 L1キャッシュ 32〜64KB/コア ~1ns データ用と命令用に分離。コアごとに専有 L2キャッシュ 256KB〜1MB/コア ~3-10ns コアごとに専有(アーキテクチャによる) L3キャッシュ 数MB〜数十MB ~10-30ns 全コア共有 。ここがコア間のデータの橋渡し TLB 数百〜数千エントリ ~1ns(ヒット時) 仮想アドレス→物理アドレスの変換キャッシュ メインメモリ 数GB〜 ~50-100ns L1の50〜100倍遅い CPUが localOps++ を実行するとき、その変数がレジスタや L1 にあれば 1ns 以下で完了します。しかし L1 にない(キャッシュミス)と L2 → L3 → メインメモリと順にたどる必要があり、最悪100nsかかる。 L1ヒットとメインメモリアクセスでは約100倍の速度差 があるわけです。 TLB(Translation Lookaside Buffer) は少し役割が違って、仮想メモリのアドレス変換を高速化するキャッシュです。プロセスが使うメモリアドレス(仮想アドレス)を実際の物理アドレスに変換するにはページテーブルを引く必要がありますが、毎回引くとメモリアクセスが2倍になるので、よく使う変換結果を TLB にキャッシュしています。TLB ミスが発生すると ページテーブルウォーク が走り、数十nsの追加コストがかかります。goroutine が大量のスタックやヒープを使うワークロードでは、TLB ミスもパフォーマンスに効いてきます。 この前提を踏まえると、マルチコアでのキャッシュ一貫性がなぜ重要かがわかります。 キャッシュコヒーレンシ問題 マルチコアCPUでは、各コアが独自の L1/L2キャッシュ を持っています。あるコアが変数を更新すると、他のコアが持つ同じ変数のキャッシュラインは 古い値(stale) になります。これを放置すると各コアが異なる値を見てしまうため、ハードウェアレベルで一貫性を保つ仕組みが必要です。これが キャッシュコヒーレンシプロトコル (代表的なものに MESI プロトコル )です。 MESI プロトコルでは、キャッシュラインは以下の4状態を遷移します: 状態 意味 M odified 自コアだけが変更済みの値を持つ E xclusive 自コアだけが持っているが、メモリと同じ値 S hared 複数コアが同じ値を持っている(読み取り専用) I nvalid 他コアが更新したので、このキャッシュラインは無効 atomic 変数への書き込みが発生すると: 書き込むコアがキャッシュラインの 排他的所有権 を要求 他の全コアの同じキャッシュラインが Invalid に変わる(無効化) 次にそのコアが同じ変数にアクセスすると、 キャッシュミス → メモリ(or 他コアのキャッシュ)から再取得 これが毎ループ・100 goroutine で発生すると: [NG] 毎回 atomic(キャッシュラインのピンポン) コア1: totalOps.Add(1) → キャッシュライン取得 (Exclusive) → 値を更新 (Modified) → 他の全コアのキャッシュラインが Invalid に コア2: totalOps.Add(1) → Invalid なので再取得が必要(キャッシュミス!) → コア1から転送 → Exclusive → Modified → 他の全コアのキャッシュラインが Invalid に コア3: totalOps.Add(1) → また Invalid → また再取得...(以下ピンポン状態) → 実際のCPU計算ではなく、キャッシュの同期にCPU時間が消える このキャッシュラインの奪い合いは 「キャッシュラインバウンシング」 や 「false sharing」 (同じキャッシュラインに別の変数が乗っている場合)とも呼ばれ、マルチスレッドプログラミングの有名なパフォーマンス落とし穴です。 一方、ローカルカウンタなら: [OK] ローカルカウンタ + 最後に1回だけ atomic コア1: localOps++ → 自コアのレジスタ or L1キャッシュだけ。他コアに影響なし コア2: localOps++ → 同上。各goroutineが独立したメモリを触る ... (終了時だけ totalOps.Add → atomic 操作は10秒間で合計たった100回) 「 ベンチマークそのもののコストでベンチマーク結果が歪む 」のを防ぐテクニックです。cgroup のスロットリングを正確に測るなら、計測のオーバーヘッドは極力削っておきたい。 4. throttle-demoモード — スロットリングの観測 func runThrottleDemo() { // GOMAXPROCS個のワーカーを起動(= OSスレッド数と一致させる) numWorkers := runtime.GOMAXPROCS( 0 ) // Before: スロットリング前の統計を記録 // cpu.stat の nr_throttled, throttled_usec を確認 fmt.Println( "--- Before ---" ) readCgroupStat() // numWorkers個のgoroutineでCPU負荷をかける // GOMAXPROCS=8 なら8本、GOMAXPROCS=1 なら1本 // → スレッド数の違いがスロットリングにどう影響するかを観測 for i := 0 ; i < numWorkers; i++ { go func () { for { cpuIntensiveWork() // 全スレッドでCPU全開 } }() } // 5秒間 CPU負荷をかけた後... // After: スロットリング後の統計を記録 // Before との差分が「この5秒間で発生したスロットリング」 fmt.Println( "--- After ---" ) readCgroupStat() } GOMAXPROCS の値がそのままワーカー数になります。GOMAXPROCS=8 なら8スレッドが同時にCPUを使おうとするので、共有クォータを一瞬で食い潰します。Before/After の throttled_usec の差分で、 実際にどれだけCPUが止められたか がわかります。 Dockerfile # ビルドステージ: Go 1.24 でコンパイル FROM golang:1.24-bookworm AS builder WORKDIR /app COPY go.mod ./ COPY main.go ./ RUN go build -o /app/cgroup-bench . # 実行ステージ: 軽量なイメージで実行 FROM debian:bookworm-slim COPY --from=builder /app/cgroup-bench /usr/local/bin/cgroup-bench ENTRYPOINT ["cgroup-bench"] CMD ["info"] Go バージョンについて Go の最新安定版 : 1.26.3(2026年5月時点) container-aware GOMAXPROCS が導入されたバージョン : Go 1.25 本記事で使うバージョン : Go 1.24(1.25直前の最終版) Go 1.25以降ではランタイムがcgroupの cpu.max を自動で読み取り、GOMAXPROCSをCPU制限に合わせて設定します。今回は 問題が発生していた当時の挙動を再現 するため、あえてGo 1.24を使用しています。 main.go 全文(クリックで展開) ```go package main import ( "encoding/json" "fmt" "math" "os" "runtime" "strconv" "sync" "sync/atomic" "time" ) type Result struct { GOMAXPROCS int `json:"gomaxprocs"` NumCPU int `json:"num_cpu"` CPULimit string `json:"cpu_limit"` Duration time.Duration `json:"duration_ns"` DurationStr string `json:"duration"` TotalOps int64 `json:"total_ops"` OpsPerSec float64 `json:"ops_per_sec"` GoroutineCount int `json:"goroutine_count"` } func cpuIntensiveWork() float64 { result := 0.0 for i := 0; i < 10000; i++ { result += math.Sqrt(float64(i)) * math.Sin(float64(i)) } return result } func main() { mode := "benchmark" if len(os.Args) > 1 { mode = os.Args[1] } switch mode { case "benchmark": runBenchmark() case "info": showRuntimeInfo() case "throttle-demo": runThrottleDemo() } } func showRuntimeInfo() { fmt.Println("=== Go Runtime Information ===") fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) fmt.Printf("NumCPU: %d\n", runtime.NumCPU()) fmt.Printf("GOVERSION: %s\n", runtime.Version()) envGOMAXPROCS := os.Getenv("GOMAXPROCS") if envGOMAXPROCS == "" { fmt.Println("ENV GOMAXPROCS: (not set — using default)") } else { fmt.Printf("ENV GOMAXPROCS: %s\n", envGOMAXPROCS) } fmt.Println("\n=== cgroup CPU Information ===") if data, err := os.ReadFile("/sys/fs/cgroup/cpu.max"); err == nil { fmt.Printf("cpu.max: %s", string(data)) } if data, err := os.ReadFile("/sys/fs/cgroup/cpu.weight"); err == nil { fmt.Printf("cpu.weight: %s", string(data)) } if data, err := os.ReadFile("/sys/fs/cgroup/cpu.stat"); err == nil { fmt.Printf("cpu.stat:\n%s", string(data)) } } func runBenchmark() { duration := 10 * time.Second if d := os.Getenv("BENCH_DURATION"); d != "" { if parsed, err := time.ParseDuration(d); err == nil { duration = parsed } } goroutines := 100 if g := os.Getenv("BENCH_GOROUTINES"); g != "" { if parsed, err := strconv.Atoi(g); err == nil { goroutines = parsed } } maxprocs := runtime.GOMAXPROCS(0) var totalOps atomic.Int64 var wg sync.WaitGroup done := make(chan struct{}) go func() { <-time.After(duration) close(done) }() start := time.Now() for i := 0; i < goroutines; i++ { wg.Add(1) go func() { defer wg.Done() localOps := int64(0) for { select { case <-done: totalOps.Add(localOps) return default: cpuIntensiveWork() localOps++ } } }() } wg.Wait() elapsed := time.Since(start) ops := totalOps.Load() opsPerSec := float64(ops) / elapsed.Seconds() fmt.Printf("GOMAXPROCS=%d Ops/sec=%.2f Total=%d\n", maxprocs, opsPerSec, ops) jsonData, _ := json.Marshal(Result{ GOMAXPROCS: maxprocs, OpsPerSec: opsPerSec, TotalOps: ops, }) fmt.Printf("JSON: %s\n", string(jsonData)) if data, err := os.ReadFile("/sys/fs/cgroup/cpu.stat"); err == nil { fmt.Printf("\ncpu.stat:\n%s", string(data)) } } func runThrottleDemo() { fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) fmt.Println("\n--- Before ---") if data, err := os.ReadFile("/sys/fs/cgroup/cpu.stat"); err == nil { fmt.Printf("%s", string(data)) } numWorkers := runtime.GOMAXPROCS(0) duration := 5 * time.Second if d := os.Getenv("DEMO_DURATION"); d != "" { if parsed, err := time.ParseDuration(d); err == nil { duration = parsed } } var wg sync.WaitGroup stop := make(chan struct{}) go func() { <-time.After(duration) close(stop) }() for i := 0; i < numWorkers; i++ { wg.Add(1) go func() { defer wg.Done() for { select { case <-stop: return default: cpuIntensiveWork() } } }() } wg.Wait() fmt.Println("\n--- After ---") if data, err := os.ReadFile("/sys/fs/cgroup/cpu.stat"); err == nil { fmt.Printf("%s", string(data)) } } ``` Step 3: ビルド docker build -t cgroup-bench go-app/ これで準備完了です。 2. 実験と結果 検証環境: - macOS(Apple Silicon) - Docker Desktop - Docker VM: 11コア (ここがKubernetesの「48コアNode」に相当) 実験1: GOMAXPROCS はコンテナの CPU 制限を無視する 何を確認するか 発表では、コンテナの limits.cpu: 5000m に対して GOMAXPROCS が 48(ノードのコア数)になっていたことが、問題の発端でした。まずは、 GoランタイムがcgroupのCPU制限を見ていない という状態をローカルで確認します。 実行コマンド # A: CPU制限なし docker run --rm cgroup-bench info # B: CPU制限 1コア docker run --rm --cpus=1.0 cgroup-bench info # C: CPU制限 0.5コア docker run --rm --cpus=0.5 cgroup-bench info 実際の結果 A: CPU制限なし === Go Runtime Information === GOMAXPROCS: 11 ← Docker VMの全CPUコア数 NumCPU: 11 GOVERSION: go1.24.13 ENV GOMAXPROCS: (not set — using default) === cgroup CPU Information === cpu.max: max 100000 ← "max" = 上限なし B: CPU制限 1コア( --cpus=1.0 ) === Go Runtime Information === GOMAXPROCS: 11 ← 制限をかけたのに11のまま! NumCPU: 11 GOVERSION: go1.24.13 ENV GOMAXPROCS: (not set — using default) === cgroup CPU Information === cpu.max: 100000 100000 ← cgroupには1コア分の制限が正しく設定されている C: CPU制限 0.5コア( --cpus=0.5 ) === Go Runtime Information === GOMAXPROCS: 11 ← まだ11のまま! NumCPU: 11 === cgroup CPU Information === cpu.max: 50000 100000 ← cgroupには0.5コア分の制限が設定されている 結果を見てみる CPU制限 cpu.max(cgroup) GOMAXPROCS 過剰並列の倍率 なし max 100000 (無制限) 11 - 1コア 100000 100000 11 11倍 0.5コア 50000 100000 11 22倍 完全に無視してます。cgroupには cpu.max として正しくCPU制限が設定されているのに、 Go 1.24のランタイムは一切見ていない 。GOMAXPROCSは常にホスト(Docker VM)のCPU数=11がデフォルト。 発表の本番環境では48コアNodeで limits.cpu: 5000m だったので、 GOMAXPROCS=48(約10倍の過剰並列) が起きていた。ローカルでも同じ構造の問題を再現できました。 cpu.max の読み方 : クォータ ピリオド の形式。ピリオド(デフォルト100ms=100000μs)のうち、クォータ分だけCPUを使える。 50000 100000 なら「100msのうち50ms使用可能 = 0.5コア分」。 実験2: 過剰並列はスループットを低下させる 何を確認するか 発表では GOMAXPROCS を48→5に変えたらスループットが大幅改善、Goスケジューラの CPU使用率が50%以上減ったとのこと。同じ体験をローカルでも数字で見てみます。 実行コマンド CPU制限1コアの環境で、100個のgoroutineを10秒間走らせます。変えるのはGOMAXPROCSだけ。 # GOMAXPROCS=1(CPU制限に一致 = 適切) docker run --rm --cpus=1.0 \ -e GOMAXPROCS=1 -e BENCH_DURATION=10s -e BENCH_GOROUTINES=100 \ cgroup-bench benchmark # GOMAXPROCS=8(CPU制限の8倍 = 過剰並列) docker run --rm --cpus=1.0 \ -e GOMAXPROCS=8 -e BENCH_DURATION=10s -e BENCH_GOROUTINES=100 \ cgroup-bench benchmark 実際の結果 GOMAXPROCS=1(適切な並列数): GOMAXPROCS=1 Ops/sec=21503.63 Total=215525 cpu.stat: nr_periods 101 nr_throttled 56 throttled_usec 43791 GOMAXPROCS=8(過剰並列): GOMAXPROCS=8 Ops/sec=6832.54 Total=68646 cpu.stat: nr_periods 102 nr_throttled 101 throttled_usec 70703432 結果を見てみる 指標 GOMAXPROCS=1 GOMAXPROCS=8 差分 Ops/sec(スループット) 21,504 6,833 68.2% 低下 nr_throttled / nr_periods 56/101 (55%) 101/102 ( 99% ) ほぼ全ピリオドで停止 throttled_usec(累積停止時間) 43,791μs (0.04秒) 70,703,432μs (70.7秒) 1,614倍 正直、ここまで差が出るとは思っていませんでした。 GOMAXPROCS を1→8にするだけで、 スループットが約3分の1に落ちる 10秒間のテストで 累計70.7秒ものCPU停止 (8スレッドが各約8.8秒ずつ止まった計算) スロットリング率99% — ほぼ毎ピリオドで全スレッドが止められている 発表で説明されていた「 クォータをスレッドが共食いする 」現象そのものです。 ┌──────── 1ピリオド (100ms) ────────┐ GOMAXPROCS=1 の場合: [████████ 実行 ████████][░░ 停止 ░░] ← 1スレッドで穏やかに使う GOMAXPROCS=8 の場合: [█ 8スレッド一斉実行 █][░░░░░░░░░░░░░░░░░░░░░░ 長時間停止 ░░░░░░░░░░░░░░░░░░░░░░] ↑ クォータ枯渇 ↑ 全スレッドが同時にスロットリング 実験3: スロットリングの深刻度はスレッド数で変わる 何を確認するか 発表で「 時間も見れば、ピリオドの%が同じでも深刻度の違いが分かる 」と指摘されていました。これ、実際に nr_throttled (回数)は同じくらいなのに throttled_usec (停止時間)には大きな差が出るということなので、自分の目で見てみます。 実行コマンド CPU制限0.5コア(かなり厳しい制限)でGOMAXPROCS=8 vs 1 を比較。 # 過剰並列(GOMAXPROCS=8, CPU=0.5コア) docker run --rm --cpus=0.5 -e GOMAXPROCS=8 -e DEMO_DURATION=5s cgroup-bench throttle-demo # 適切な並列(GOMAXPROCS=1, CPU=0.5コア) docker run --rm --cpus=0.5 -e GOMAXPROCS=1 -e DEMO_DURATION=5s cgroup-bench throttle-demo 実際の結果 GOMAXPROCS=8(過剰並列): --- After --- nr_periods 52 nr_throttled 51 ← 98%のピリオドでスロットリング throttled_usec 39039180 ← 39秒のCPU停止 GOMAXPROCS=1(適切): --- After --- nr_periods 51 nr_throttled 50 ← 98%のピリオドでスロットリング(ほぼ同じ!) throttled_usec 2644221 ← 2.6秒のCPU停止 結果を見てみる 指標 GOMAXPROCS=8 GOMAXPROCS=1 差分 nr_throttled / nr_periods 51/52 (98%) 50/51 (98%) ほぼ同じ throttled_usec 39,039,180μs (39秒) 2,644,221μs (2.6秒) 14.8倍 数字を自分で並べてみて、初めて深刻さがわかりました。 nr_throttled の割合(スロットリング率)だけ見るとどっちも98%で全く同じに見えます。でも throttled_usec (実際の停止時間)には14.8倍もの差がある。 これが発表で言われていた「CPU使用率だけでは気づけない」「監視の死角」の正体です。 なぜCPU使用率では見えないのか ここをもう少し掘り下げます。実はこの実験、 どちらのケースもCPU使用率は約100% と表示されます。「え、GOMAXPROCS=8 のほうが遅いのにCPU使用率は同じ?」と思うかもしれませんが、これにはカラクリがあります。 CPU使用率の計算式は本質的にこうです: $$ \text{CPU使用率} = \frac{\text{消費したCPU時間}}{\text{割り当てクォータ}} $$ 今回の実験では --cpus=0.5 なので、1ピリオド(100ms)あたりのクォータは 50ms です。 GOMAXPROCS=1 GOMAXPROCS=8 クォータ 50ms / period 50ms / period 消費CPU時間 50ms(使い切る) 50ms(使い切る) CPU使用率 ≈100% ≈100% 消費ペース 1スレッド × 50ms = 50msかけて徐々に 8スレッド × 6.25ms = 約6msで一気に 残りの時間 50ms間は停止(穏やか) 94ms間 全スレッド凍結 どちらもクォータ50msを使い切るので、CPU使用率は同じ100%です。しかし 消費のペースがまるで違います 。 1ピリオド(100ms)の内訳: GOMAXPROCS=1: |███████████████████████████░░░░░░░░░░░░░░░░░░░| ← 1スレッドで50ms実行 →← 50ms 待機 → CPU使用率: 50/50 = 100% レイテンシ: 安定 GOMAXPROCS=8: |████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░| ←6ms→←───────── 94ms 全スレッド凍結 ──────────→ CPU使用率: 50/50 = 100% レイテンシ: スパイク発生 GOMAXPROCS=1 は1スレッドで50msを穏やかに消費するので、処理は途切れつつも比較的均等に進みます。一方 GOMAXPROCS=8 は8スレッドが同時にCPUを要求するため、わずか約6msでクォータを食い尽くし、 残りの94msは全スレッドが完全に凍結 します。 つまりCPU使用率100%の裏で起きていることが全く異なるのに、 集約メトリクスではその違いが消えてしまう 。これが「監視の死角」の本質です。 まとめると: nr_throttled率が同じでも、 8スレッドが同時に止まる のと 1スレッドだけ止まる のでは深刻度がまるで違う CPU使用率は クォータを消費した量 しか示さず、 消費のペース(バースト性)を一切反映しない throttled_usec を合わせて監視しないと、スロットリングの実態はつかめない 実験4: スレッド数と停止時間の相関 何を確認するか スレッド数を段階的に増やしたとき、 throttled_usec が比例して増えるのか。発表スライドの「 クォータはスレッド間で共有 」「 スレッドが多いほど早く消費 」という説明を、グラフで体感してみます。 実行コマンド for MAXPROCS in 1 2 4 8 16; do echo "--- GOMAXPROCS=$MAXPROCS ---" docker run --rm --cpus=1.0 -e GOMAXPROCS=$MAXPROCS -e DEMO_DURATION=5s \ cgroup-bench throttle-demo 2>&1 \ | grep -E "nr_periods|nr_throttled|throttled_usec" | tail -3 echo "" done 実際の結果 --- GOMAXPROCS=1 --- nr_periods 51 nr_throttled 20 throttled_usec 21885 --- GOMAXPROCS=2 --- nr_periods 51 nr_throttled 50 throttled_usec 5075001 --- GOMAXPROCS=4 --- nr_periods 51 nr_throttled 50 throttled_usec 15053306 --- GOMAXPROCS=8 --- nr_periods 52 nr_throttled 51 throttled_usec 35847841 --- GOMAXPROCS=16 --- nr_periods 51 nr_throttled 51 throttled_usec 50338309 結果を見てみる GOMAXPROCS nr_throttled スロットリング率 throttled_usec 累積停止時間 1 20 / 51 39% 21,885 0.02秒 2 50 / 51 98% 5,075,001 5.1秒 4 50 / 51 98% 15,053,306 15.1秒 8 51 / 52 98% 35,847,841 35.8秒 16 51 / 51 100% 50,338,309 50.3秒 停止時間 (秒) 50 ┤ ● GOMAXPROCS=16 │ ╱ 40 ┤ ╱ │ ╱ 35 ┤ ● ╱ GOMAXPROCS=8 │ ╱ ╱ 20 ┤ ╱ │ ╱ 15 ┤ ● ╱ GOMAXPROCS=4 │ ╱ ╱ 10 ┤ ╱ │ ╱ 5 ┤ ● ╱ GOMAXPROCS=2 │ ╲╱ 0 ┤● GOMAXPROCS=1 └──┬──┬──┬──┬──┬──┬── 1 2 4 8 12 16 GOMAXPROCS GOMAXPROCS=1〜8の範囲ではほぼ線形に比例しています。 GOMAXPROCS=1 → 0.02秒(ほぼ停止なし) GOMAXPROCS=16 → 50.3秒(5秒のテストで累計50秒分の停止!) ただし GOMAXPROCS=16 では、同時にCPUを使えるスレッド数が Docker VM の物理CPU数(11コア)で頭打ちになるため、純粋な線形モデルの予測(75秒)より低い50.3秒に飽和しています。16スレッド中、同時に実行できるのは最大11スレッドなので、停止時間は $\min(n, 11) \times P - Q$ に近づきます。 GOMAXPROCS=8 以下では物理CPU数の制約を受けないため、きれいに $n \times P - Q$ の線形モデルと一致しています(8スレッド時の予測35秒 vs 実測35.8秒)。 発表でいう Thundering Herd 問題 そのもので、クォータリセットで全スレッドが一斉に再開 → 共有クォータを瞬殺 → 全スレッド同時停止、のサイクルが繰り返される。 3. 考察: なぜスレッドを増やすと「遅くなる」のか 実験4の結果を改めて見ると、 throttled_usec がスレッド数にほぼ比例して増えています。「スレッドを増やすほど損をする」って、直感に反しますが、CFS 帯域制御の仕組みから数式で説明できます。 CFS 帯域制御の数理 — クォータ消費のモデル cgroup の CPU 制限は「1ピリオド(100ms)あたり $Q$ だけ CPU を使える」というクォータ制です。 --cpus=1.0 なら $Q = 100\text{ms}$ です。 $n$ 本のスレッドが同時にフル稼働すると、CPU 時間は $n$ 倍の速度で消費されます。つまり: クォータ枯渇までの時間 : $\frac{Q}{n}$ 残りのピリオド : 全 $n$ スレッドが同時に停止 1スレッドあたりの停止時間 : $\text{ピリオド} - \frac{Q}{n}$ 累積停止時間 (= throttled_usec ): $n \times \left(\text{ピリオド} - \frac{Q}{n}\right)$ -cpus=1.0 ($Q = 100\text{ms}$、ピリオド $= 100\text{ms}$)で $n = 8$ の場合: クォータ枯渇: $\frac{100}{8} = 12.5\text{ms}$ で使い切る 各スレッドの停止: $100 - 12.5 = 87.5\text{ms}$ 1ピリオドあたりの累積停止: $8 \times 87.5 = 700\text{ms}$ 5秒間(50ピリオド)なら $50 \times 700\text{ms} = 35\text{秒}$。実験4の GOMAXPROCS=8 の結果(35.8秒)とほぼ一致します。 USL で見るとスループット悪化も説明がつく この現象をスケーリング法則の観点から見ると、Neil Gunther の USL(Universal Scalability Law) が当てはまります: $$ S(n) = \frac{n}{1 + \alpha(n-1) + \beta \cdot n(n-1)} $$ パラメータ 意味 cgroup 環境での具体例 $\alpha$ 競合 — 直列化ペナルティ Go スケジューラのロック競合、ランキュー管理 $\beta$ 一貫性 — スレッド間の協調コスト CFS クォータの共有消費 + 全スレッド一斉停止 USL で効いてくるのは $\beta$ の項です。$\beta \cdot n(n-1)$ は $n 2 $ オーダーで増大するため、ある閾値を超えるとスループットが ピークから減少に転じます 。実験2の「GOMAXPROCS=8 で 68% 低下」は、この retrograde(逆行)領域に入った結果です。 スループット ↑ ● ピーク(GOMAXPROCS=1) │ ╱╲ │ ╱ ╲ ← USL の retrograde 領域 │ ╱ ╲ │╱ ╲ ● GOMAXPROCS=8(68%低下) └──────────→ スレッド数 cgroup制限下では「スレッド1本」がピーク。 増やすほどクォータの奪い合いで損をする。 通常の並列プログラミングでは「コア数まではスケールする」のが常識ですが、cgroup でリソースが制限された環境では スレッド1本がすでに最適解 という直感に反する結果になる。CPU 時間の総量が固定されたゼロサム環境なので、スレッドを増やすほど「クォータの奪い合い → 一斉停止 → 再開 → また枯渇」のサイクルが重くなるだけ。 「I/O 待ちがある場合はどうなのか?」 ここまでの実験は cpuIntensiveWork() による 純粋な CPU バウンド処理 です。「I/O 待ちがあるならスレッドを増やしたほうがいいんじゃ?」と思いますよね。一般論としてはその通りで、スレッドが I/O で待っている間は CPU クォータを消費しないので、CPU 数より多いスレッドが有効な場面はあります。 ただし Go の場合は話が別 です。Goランタイムには以下の仕組みがあるので、GOMAXPROCS を I/O のために増やす必要は基本的にないです: I/O の種類 Goランタイムの挙動 GOMAXPROCS への影響 ネットワーク I/O netpoller が非同期処理。goroutine は待つが OS スレッドはブロックしない 影響なし ブロッキング syscall (ファイル I/O, CGO 等) ランタイムが GOMAXPROCS とは 別に追加の OS スレッドを自動生成 影響なし つまり Go では、ネットワーク I/O は goroutine レベルで多重化され、ブロッキング I/O は GOMAXPROCS の枠外で処理される。GOMAXPROCS が制御するのは CPU を実際に使うスレッドの数 だけなので、I/O の多寡に関わらず GOMAXPROCS = CPU 制限 が正解です。 DB クエリや API 呼び出しを大量に行う Web サービスでも、GOMAXPROCS=5(CPU 制限に一致)で大幅に改善した事例があるのは、この仕組みがあるからです。 一方、Go 以外のランタイムでは、それぞれ事情が違うので整理しておきます: ランタイム 並列の仕組み cgroup の影響 Java スレッドプール(ForkJoinPool 等)で並列化。 Runtime.availableProcessors() の値を基準にプールサイズが決まることが多い スレッドプールサイズを CPU limit より大きい値にするとスロットリング発生 Node.js メインスレッドはシングル。 UV_THREADPOOL_SIZE (デフォルト4)で fs/dns 等のブロッキング I/O を処理。CPU バウンド処理は worker_threads で並列化 worker_threads の数を CPU limit より大きい値にするとスロットリング発生 Ruby (CRuby) GVL(Global VM Lock)があるため、スレッドを増やしても CPU バウンド処理は並列実行されない 。Puma 等の Web サーバーは workers (fork によるマルチプロセス)で並列化 Puma の workers を CPU limit より大きい値にするとスロットリング発生 4. 発表の内容をローカルで再現できたか? 全検証結果まとめ # 発表のポイント ローカル検証の結果 再現 1 GOMAXPROCSはcgroupのCPU制限を考慮しない(Go 1.24以前) --cpus=0.5 でも GOMAXPROCS=11(ホストCPU数)のまま 再現 2 limits.cpu は cgroup の cpu.max (クォータ/ピリオド)に変換される --cpus=0.5 → cpu.max: 50000 100000 を確認 再現 3 過剰並列はスループットを低下させる GOMAXPROCS 1→8 で Ops/sec が 68.2% 低下 (21,504 → 6,833) 再現 4 クォータはスレッド間で共有され、多いほど早く消費される スレッド数と throttled_usec がほぼ線形に比例 再現 5 スロットリング時は全スレッドが同時に停止する GOMAXPROCS=16で5秒間に累計50.3秒分の停止を確認 再現 6 CPU使用率だけではスロットリングに気づけない nr_throttled 率は同じ98%でも throttled_usec に14.8倍の差 再現 7 GOMAXPROCSをCPU制限に合わせると改善する GOMAXPROCS=1 で停止時間が 1/1,614 に改善 再現 すべて手元で再現できました。 Docker と Go 1.24 だけでここまで体験できるのは、やってみてよかったと素直に思います。 個人的に印象に残った数字 比較 過剰並列の場合 適切な並列の場合 倍率 Ops/sec(スループット) 6,833 21,504 3.1倍の差 throttled_usec(停止時間) 70.7秒 0.04秒 1,614倍の差 GOMAXPROCS=16の停止時間 50.3秒 - 5秒のテストで50秒停止 本番環境との対応関係 発表 ローカル検証 48コアNode 11コア Docker VM limits.cpu: 5000m --cpus=0.5 GOMAXPROCS=48(デフォルト) GOMAXPROCS=11(デフォルト) 過剰並列倍率: 約10倍 過剰並列倍率: 最大22倍 /sys/fs/cgroup/cpu.max 同じパス(Docker内Linux) cpu.stat の nr_throttled 同じメトリクス 本番ではさらにHAProxy( nbthread=48 , CPU制限1コア = 48倍の過剰並列 )でも同じ問題が起きていたそうで、Goに限った話ではないということがわかります。 まとめ 1. 並列設定を cgroup-aware にする GOMAXPROCS に限らず、 コンテナ内で動くすべてのプロセスの並列設定 は確認したほうがいいです。 ソフトウェア 並列設定 対処 Go GOMAXPROCS Go 1.25+ で自動対応 / 1.24以前は uber-go/automaxprocs Ruby (Puma) WEB_CONCURRENCY / workers CPU制限に合わせて明示指定。cgroup非対応の auto 設定に注意 Java スレッドプールサイズ JDK 10+ は availableProcessors() が cgroup 認識。ライブラリ側も要確認 Node.js worker_threads 数 CPU バウンド処理の並列数を CPU 制限に合わせる HAProxy nbthread 手動でCPU制限に合わせて設定 Nginx worker_processes auto はcgroup非対応の場合あり、明示指定が安全 2. スロットリングを監視する CPU使用率だけじゃなくて、 スロットリングのメトリクスもセットで見る 。これを怠ると実験3で見たような死角にハマります。 メトリクス Linux Datadog 停止時間 throttled_usec kubernetes.cpu.cfs.throttled.seconds 停止ピリオド数 nr_throttled kubernetes.cpu.cfs.throttled.periods 3. throttled_usec まで見る 今回の実験を通して一番の収穫は、 nr_throttled の割合が同じ 98% でも、 throttled_usec に 14.8倍の差がある と自分の手で確認できたこと。スロットリング率だけ見ても、 実際にどれだけ止まっているかは見えない 。 CPUをもっと知りたくなった方へ — 個人的なおすすめ本 今回の検証を通して「もっとCPUの中身を理解したくなった」という方に、個人的に強くおすすめしたい一冊があります。 「プログラマーのためのCPU入門 — CPUは如何にしてソフトウェアを高速に実行するか」 パイプライン、スーパースカラ、分岐予測、キャッシュ、メモリオーダリングといった、 普段は意識しないけれど性能に直結するCPU内部のメカニズム が、プログラマーの目線で一通り整理されている本です。本記事の脱線で触れたキャッシュコヒーレンシまわりも、この本を読むとより腑に落ちると思います。 「なぜこのコードは速いのか/遅いのか」を、ハードウェア寄りの視点から考えられるようになる本なので、cgroup の挙動の先を覗いてみたい方にぴったりです。 参考 ペアーズ本番環境でのcgroup-aware化との死闘録(発表スライド) — 本記事のベースとなった発表 クラウドネイティブ会議2026 セッションページ Container-aware GOMAXPROCS | Go 1.25 Release Notes uber-go/automaxprocs — Go 1.24以前で使えるcgroup-aware GOMAXPROCS Kubernetes CPU limits and requests: A deep dive | Datadog 検証コード 本記事の検証に使ったコードは以下のリポジトリにあります: git clone https://github.com/hirosi1900day/cgroup-throttling-lab.git cd cgroup-throttling-lab docker build -t cgroup-bench go-app/ ./scripts/run_experiments.sh # 全実験を一括実行
株式会社タイミーでモバイルエンジニア (Android / iOS) をしている、みかみです。介護領域のグロース施策を中心に、AB テストや分析、マーケティングとの連携などにも取り組んでいます。 2026年4月16日に開催された RevenueCat App Growth Annual Tokyo 2026 (以下 RAGA) に参加してきました。 アプリ成長、サブスクリプション、AI をテーマにしたセッションの中から、個人的に印象に残った話をいくつか紹介します。 RAGA について RAGA (RevenueCat App Growth Annual) は、 RevenueCat が主催する「モバイルアプリ成長」をテーマにしたグローバルカンファレンスです。RevenueCat は、モバイルアプリのサブスク収益化プラットフォームを提供している会社です。 カンファレンスでは「 AI・サブスクリプション・アプリ成長 」をテーマに、プロダクト戦略やユーザー獲得、マネタイゼーション設計といったトピックが扱われていました。登壇者は Notion、ElevenLabs、Speak、YAMAP、SmartNews、Mirrativ、NOT A HOTEL といった国内外のアプリ企業の実践者で、経営層・CPO・CTO クラスが多かったのが特徴的でした。加えて RevenueCat Co-founder / CTO による基調講演もありました。 参加した目的 RAGA に参加したのは、アプリグロースに取り組む他社の現場の話を直接聞ける機会だったからです。日々の業務では、アプリの機能開発だけでなく、AB テスト・分析・マーケ連携にも取り組んでいます。エンジニアリングの先にあるグロース領域への関心も、最近広がってきていました。RAGA で扱われるトピックは、まさに自分の関心と重なる内容でした。 もうひとつ、AI に聞けばなんでも答えてくれる時代だからこそ、現場で一次情報に触れる機会を大事にしたいという考えもありました。後からまとめ記事や録画で追える情報も多いですが、一日を通して様々な実践者の話を続けて聞ける経験は、その場に行かないと得られないと思ったからです。 印象に残ったセッション RAGA は2トラック構成で、世界でアプリを成長させたリーダーが集う「 Global Track 」と、日本市場の実践者や世界進出を目指す起業家が登壇する「 Japan Track 」がありました。自分が参加した中で特に印象に残ったセッションを紹介します。 1. 原点回帰:iモードから現代のアプリ経済へ RevenueCat 共同創業者兼 CTO の Miguel Carranza さんによる基調講演では、日本のモバイル市場の歴史を起点に、現代のアプリ経済が直面する変化が語られました。基調講演の内容や RAGA Tokyo 全体の様子は ProductZine の詳細レポート に詳しいので、ここでは個人的に印象に残った点を中心に紹介します。 冒頭で印象的だったのは、現代のアプリ経済で当たり前になっている概念の多くが日本発だった、という視点です。1999 年の i-mode、絵文字、LINE など、日本のモバイル文化が今のアプリ経済につながっているという話から始まりました。 同時に、日本市場の大きさも具体的な数字で示されました。 日本は世界 3 位のアプリ市場 ユーザー一人あたりの年間支出は $166 2027 年にはスマホユーザーが人口の 94% に達する見込み 一方で、印象に残ったのは「市場が大きい」という話だけではありません。AI によってアプリを作るハードルが下がった結果、過去 4 年間で アプリの供給は7 倍に増え、市場には明確な分断が起きているそうです。上位 25% のアプリは収益を 80% 以上伸ばす一方で、下位 25% は 33% 減少しているという話もありました。 この話を聞いて、アプリ市場は「作れば伸びる」市場ではなくなってきているのだと感じました。AI によって作る力が広がるほど、作れること自体の価値は相対的に下がり、継続して使われる体験をどれだけ設計できるかがより重要になっていくのだと思います。 最後に取り上げられていた 「AI パラドックス」 も、その流れとつながる話でした。AI を組み込んだアプリは初期コンバージョンが強い一方で、解約が約 30% 早く、継続率に課題を抱えるケースが多いそうです。AI の新しさで一度使ってもらうことはできても、継続的な価値に落とし込めなければ LTV にはつながらない、という指摘として受け取りました。 アプリ市場というと、これまで漠然と「大きそうだな」くらいに捉えていましたが、今回の話を聞いて、規模の大きさ以上に競争の中身が変わってきていることを実感しました。ペイウォール設計やトライアル期間、継続率改善といった細部への投資が差を生むという話は、日々のグロース施策を考えるうえでもかなり示唆的でした。 2. 広告視点で考えるサブスクハックネタお披露目会 Repro の中野竜太郎さんと Alethne の坂本翔也さんが、 Adjust の高橋将平さんのモデレートで議論したパネルディスカッションです。広告運用・計測・クリエイティブ最適化など、アプリ成長を広告の視点から見つめてきた登壇者ならではの、現場感の強い話が詰まったセッションでした。 冒頭で出てきたフレーズが強烈です。 "CPI (Cost per install) だけ見る投資はナンセンス。" "エンゲージメントの時代じゃない、アクティベーションの時代だ。" 登壇者たちが共通して強調していたのは、 広告で獲得したユーザーの8割は Day 0 で離脱する という前提です。離脱を防ぐ鍵になるのが 「アハ体験」 、つまりユーザーがアプリの価値を実感する瞬間の設計です。リワードで引っ張ってくるのではなく、自社サービスのコア体験そのものをゲーム化していくのが大事だ、という主張でした。 もう一つ参考になったのが「マジックナンバー」と最適化トリガーの設計の話です。トライアル開始を成果イベントにするのは弱く、「7日間トライアルで辞める気のユーザーは2時間で消える」というデータがあるそうです。そこで、「2時間以上滞在したユーザー」や「特定アクションを N 回実行したユーザー」をマジックナンバーに設定したほうが、広告の最適化トリガーとしても成果が出やすいとのことでした。 AB テストや分析に取り組んでいる身として、「何をイベントとして計測するか」の解像度を一段上げるヒントになる話でした。単に画面遷移を計測するのではなく、ユーザーがそのアプリの価値を実感したシグナルとしてイベントを設計する、という視点が新鮮でした。 3. 激動の時代、日本発メガベンチャーはどのように世界で勝ちに行くのか SmartNews のホン・ランドンさんと Mirrativ の赤川隼一さんによるパネルディスカッションです。コミスマの坂本達夫さんがモデレーターを務めたこの回が、自分にとっては強く印象に残ったセッションでした。プロダクト・グロース・経営の観点から、AI の進化と激化するグローバル市場にどう立ち向かうかが議論されました。 特に印象に残ったのが、グローバル展開の戦略の話です。展開する国の数によって取るべき戦略は変わるそうです。1 カ国に絞るなら徹底した現地化が有効、多国展開なら同一プロダクトを広げて手応えのある市場に集中投資する、という対比でした。いずれにせよ、個別市場の局所最適ではなく、会社全体の収益最大化を基準に手段を選ぶべきだ、という話が腑に落ちました。 セッションの締めくくり、ランドンさんからの「AI 時代に変わるもの・変わらないものは?」という問いに対する赤川さんの答えが、一日で最も刺さった言葉でした。 "変わるもの = ほぼ全部。アンラーニングとスピードが勝負。変わらないもの = 現地現物。" 「優秀な PM は必ず現地でユーザーが迷う姿を観察する」という話でした。AI で機能開発のスピードが上がる時代だからこそ、ただ機能開発を進めるだけではなく、問いを立てる力や、現場やユーザーに直接聞きにいくことの重要性は、むしろ増しているように感じます。 おわりに 参加してみて改めて感じたのは、AI 時代だからといって仕事が 華やか になっているわけではない、ということでした。 各セッションで語られていたのは、AI を使いこなす派手な事例というよりも、その手前にある 地道な計測や粘り強い観察、ユーザーの一次の声を拾い続ける姿勢 だったと感じます。今回語られていた事例のどれもが、こうした地道さの積み重ねの上にあるということを、当たり前のようでいて改めて強く感じました。 AI でプロダクトの作り方は変わっていきますが、アプリを成長させるために向き合う対象は変わらず、むしろ AI が広がるほど、ユーザーをどれだけ深く読めるかがアプリの差として出やすくなっていくのかもしれない、と感じた一日でした。 カンファレンス後のアフターパーティーも独特でした。ライブや DJ セットを交えた構成で、日中のセッション合間にもスタンダップコメディが入るなど、国内の他のカンファレンスと比べても振り切り方が際立っていて、一日を通して印象的でした。
はじめに 株式会社タイミーでデータアナリストをしている平野です。 タイミーキャリアプラス (以下、タイキャリ)という新規事業に、リードDAとして半年あまり伴走してきました。 タイキャリは、タイミーの スキマバイトのデータを起点に、長期就業(正社員採用)を支援する サービスです。タイミーで得た就業実績・バッジを"職務経歴書"として活用できるのが特徴で、2024年2月に開始されています。 新規事業DAの面白さ 0→1のフェーズで、データと事業を一緒に育てていく感覚 を味わえる仕事です。 自分の分析が 事業の意思決定に直結 する瞬間が日常的にある 事業責任者・GM・PdMと机を並べて、 事業の方向づけにデータで参加できる データ基盤・モニタリング・分析テーマを ゼロから設計 できる この記事で書くこと 一方で、新規事業ならではの難しさもあり、半年の間に"知っていれば避けられた"失敗 を数多く経験しました。同時に 再現可能な"型"が存在するとも確信しています。 本記事では、私が踏んだ落とし穴とそこから抽出したアクションを 時系列で整理 します。これから新規事業に入るDAの参考になれば嬉しいです。 Part 1: 新規事業DA特有の3つの難しさ 💡 難しさはPart 2の予防アクション、Part 3の原則とセットで読むと"克服可能な型"として見えてきます。 私が実際に踏んだ落とし穴を3つ共有します。どれも 新規事業DAが共通して直面しうる構造的な問題 で、「誰かが悪い」ではなく、立ち上げ期に起きやすい"あるある"です。DA側がどう動くと摩擦や手戻りを減らせるか、という観点で整理します。 ① 統制構造の有無で難易度が桁違いに変わる DAへの依頼を集約する"要望窓口"(bizops的人材)がいるかどうかで、伴走の難易度が大きく変わります。立ち上げ初期は役割や導線が固まりきっていないため、集約は"自然発生"しないことも多いです。 集約役がいる事業 : 要望が一本化、定義の議論は1対1で済む 窓口体制が立ち上がる前の事業 : DA側が複数の現場と直接やり取りし、 定義確認を個別に進める必要が出てくる 人材紹介領域はbizops担当の方がいてスムーズだった一方、DR領域では 複数の現場と直接やり取りしながら同じ定義の議論を繰り返す ことになりました。 ② DA介入前にプロダクト開発が進んでしまう プロダクト駆動型の新規事業では、スピード重視で DAが合流する前に実装が固まる ことが起きやすく、後から計測設計を整える負担が大きくなります。 DRサービスでは、私が伴走を始めた時点で、 計測設計にDA観点を反映する場がない 状態でした。一気通貫のファネル分析のため、 計測設計の整理を提案する ところから始める必要があったのです。 新規事業の速度感を考えれば自然なこと。だからこそ DA側から早期に合流する関係性を作りに行く ことが重要だと痛感しました。 ③ DA側が組織横断の優先順位付けの場を提案できていない 立ち上げ期は、各担当者が自分の領域の進捗を重視するのは自然(むしろ健全)です。一方で、全体の優先順位を議論する場は用意されていないことも多く、 DA側から立ち上げないと、横断の判断軸を持って動くのが難しくなる と感じました。 私も最初の数ヶ月は会議体を提案できず、 腰を据えるべきテーマに集中しきれない期間 を過ごしました。 Part 2: 時系列アクションリスト 新規事業にDAが入るとき、いつ何をやるべきか を時系列で整理します。各アクションに「なぜそう思ったか」のエピソードを添えました。 🔵 Day 0: アサイン前の意思決定 アクション 内容と効くポイント 🧗 DAをディープダイブ(兼務させない) 新規事業は定義変更・指標追加が高頻度で、半コミットでは事業理解が追いつかない。 特にリードアナリストは他案件と兼務させない ことが、後の伴走の質を大きく左右する 💡 最初からディープダイブ前提でアサインされたことが、信頼関係構築 → 中長期伴走パートナー化につながりました。 🟢 Week 1-4: 関係性構築・PJ立ち上げ 🎯 特に効いた3つ : 関係者ヒアリング / 要望窓口の確認 / ログ設計レビュー合流(プロダクト駆動型) アクション 内容と効くポイント 📋 関係者ヒアリング 事業責任者・GM・現場メンバーに事業の目的・課題・関心事をひとりひとり丁寧に聞く。 この時間投資が後の伴走の土台 になるため、急ぎたい気持ちを抑えても話を聴く価値がある 💬 依頼チャンネル合意 Slack・依頼フォーム等を事業側と合意して「どこに・どの形式で依頼を投げるか」を明確化。受け手のオペが回り、依頼の取りこぼしが減る 🔁 週次定例の継続参加 事業の解像度を大きく上げる手段。 ただし参加だけでは何も変わらない 。「この定例から何のアクションを生むか」を常に自問する姿勢が大事 🚪 要望窓口の確認/提案 集約役不在のままだとPart 1 ①の状況に陥りやすい。いない場合は、GMや事業部のミドルマネージャーに 集約役を担ってもらう依頼導線の設計 をDA側から提案 🔍 ログ設計レビュー合流 プロダクト仕様検討・ログ設計レビューの場にDAが入れるよう、 開発開始前から合流 しておく。これを逃すとPart 1 ②の罠にはまる 📚 ナレッジのGit管理 AIコーディングツール(Claude Code・Cursor・Devin等)が参照できる形でナレッジを蓄積。 分析の壁打ち・クエリ作成の工数が劇的に削減 されるため、アサイン初期の最優先投資 ⚠️ 前任期に「定例参加だけでアクションに繋がらない」時期がありました。 参加は手段、目的ではない 。 🟡 Month 1-3: 目先の困りごと解決期(信頼残高を貯める) 🎯 特に効いた3つ : 爆速対応+問い返しの両立 / 要件定義を向き合いと一緒に言語化 / 基盤整備のタイミング評価 アクション 内容と効くポイント ⚡ 爆速対応 + 「なぜ/必要性/定義」の問い返し 初動の爆速対応は信頼関係構築の重要戦略。「このDAは価値がある」と認識されると、後のポジション変更が楽になる。 ただし依頼対応に留まり続けると"依頼を捌く人"として固定 されるため、「なぜやるか/本当に必要か/定義は適切か」を毎回問い返す姿勢を早期から取り入れる。 信頼関係構築と「言うべきことを言う」は両立可能 ✏️ 要件定義は向き合いと一緒にテキスト化 ダッシュボードやレポート作成の依頼時、「どの目的で、どの観点で、なぜ見るのか」を 向き合いと一緒に書き残せるレベル で整理。これを省くと要件が固まる前に着手することになり、双方にとって手戻りが発生しやすい 🏗️ データ基盤整備のタイミングを常に評価 早すぎても空振り、遅すぎても移行コストが跳ね上がるのが基盤整備の難しさ。 スプシ主体のモニタリングが積み上がってから着手すると、現行オペとの二重管理で身動きが取りにくくなる 🎁 外注保守カット案件は中長期運用主体を確認 「外注の保守費用削減のためにDA側で巻き取る」相談は、一度立ち止まって 中長期の運用主体を一緒に確認したい 類の案件。自分が異動・退職したときの移管コストが高く、長期の保守タスクとして残り続けやすい。 特にdbt等が絡むものは要注意 💡 私はこの切り替えが遅れ、後から上流に意見を出し始めた際に向き合いとの期待値調整が必要になりました。 ⚠️ 整備のタイミングをDA側から提案しきれず、暫定的な参照・運用が一部残っています。 軽いうちに議論を持ち出す ことが重要。 🟠 Month 3-6: 中長期伴走パートナーへのギアチェンジ 🎯 特に効いた3つ : 事業責任者+GM+bizops週次MTG立ち上げ / OKR策定シンクロ / 能動的価値発揮へシフト アクション 内容と効くポイント 🗓️ 事業責任者+GM+bizops 週次MTG立ち上げ 組織全体で優先順位を棚卸しする会議体をDA側から提案して立ち上げると、 横断の判断軸を持って動けるようになる 。この会議体は、2事業目が入ったときの司令塔にもなる 🎯 OKR策定タイミングにシンクロ 期初のOKR策定に合流して、事業側と「何を課題と捉えるか」をすり合わせ。これをやると、その後の依頼について 目的を事前にすり合わせた状態 で動けるようになる 🔀 GM経由のコミュニケーションフロー 現場の複数メンバーからは似た観点の依頼がそれぞれ届くため、個別にすり合わせていると工数が膨らむ。 GM(またはbizops・集約役)経由のフローに整える と、定義の議論が一つの場に集約され、双方のコミュニケーションコストが抑えられる 🎤 能動的価値発揮へシフト 受け身で依頼を捌くフェーズから、 DA起点で価値を提案するフェーズ へ。例: 現場担当者(CA等)へのインタビュー設計で定性課題を抽出 / 事業責任者・GMが意思決定に使う経営ダッシュボードをDA起点で設計・提案 💡 ここからが 新規事業DAの本領発揮ゾーン 。事業の意思決定に直接関われるようになり、 自分の分析・提案が事業の方向づけにつながる手触り感 を強く感じられます。 🔴 2事業目が追加されるタイミング 🎯 特に効いた3つ : 熱量シグナル検知→能動介入 / ログ設計レビュー最優先 / 要望窓口の最初からの設計 アクション 内容と効くポイント 🌡️ 熱量シグナル検知 → 能動介入 新しい領域が熱を帯びる瞬間(目標変更・予算変更等)を DAが能動的にキャッチして動く ことが重要。待っていると後手に回り、Part 1 ②の罠(介入前に実装が固まる)にはまる 🔍 ログ設計レビューを最優先 プロダクト駆動型事業なら必須。 Part 1 ②を2度繰り返さない ためにも、最初期から計測設計のレビューに入る関係性を作りに行く 🚪 要望窓口の体制を最初から設計 1事業目の学び(GM経由フロー)を即座に転用。人材紹介領域で「GM経由フロー」が効くと学んだので、DR領域では 初期から意識的にGM経由フローを設計 した 🗓️ 横断の優先順位会議体を再設計 2つ目以降の事業が入るタイミングを 会議体再設計のトリガー にする。1事業目向けの会議体を拡張して、横断優先順位を扱える形に変えていく 💡 DR領域で目標が跳ね上がる動きを検知し、自分から事業責任者+PdMに働きかけて週次定例への参加を勝ち取りました。 これがなければ今の伴走体制は立ち上がっていません 。 Part 3: 全体を貫く2つの原則 これまでの話は 2つの原則 に集約できます。 原則① 未来を見据えた動きをせよ 「今が楽に回るから」で意思決定しない。 半年後のオペレーション・組織・関係性を想像して、初動で先手を打ちます。 データ基盤整備のタイミングを早めに見定める 初期から事業責任者との接点を作る KPIや優先順位の構造を、組織が大きくなる前に整理する 「目先の困りごと解決 → 中長期伴走パートナー」の移行を意図的にデザインする 新規事業は成長スピードが速く、 "今困っていないこと"が半年後に大きなコスト として返ってくることが頻繁に起きます。 原則② 統制構造(要望窓口)を早期に組め 現場にbizops相当の集約役がいるかで、新規事業DAの難易度は桁違いに変わります。 いなければDA側から「誰が集約役を担うか」を提案するのが、DAの重要な仕事です。 統制構造が組めると、依頼フロー・優先順位付け・コミュニケーション工数の すべてが好転 。逆にここが組めないと、どんなに優秀なDAでも個別対応だけで手が回りきらなくなります。 おわりに 落とし穴を中心に書きましたが、新規事業のDA伴走は半年経った今、 自分のキャリアで最も学びと手触り感のあるフェーズ だと感じています。「新規事業DA、ちょっと面白そう」と感じてもらえたら本望です。 最後に改めて、本記事の失敗エピソードはすべて、 私自身(DA)の動きで改善できる余地について書いたもの であり、事業側の皆さんを批判する意図はありません。 急成長する新規事業の中で常に最善を尽くしてくださっているCareer Plus部の皆さんと、前任のDA・bizopsの方々に深く感謝 しています。 この役回りに関する再現可能なノウハウはまだ少ない領域です。 多くのDAが同じ試行錯誤を繰り返している 現状が変わるきっかけになれば嬉しいです。 最後に、タイミーでは一緒に働くメンバーを募集してます!ご興味があればぜひお話しましょう! プロダクト採用サイトTOP カジュアル面談申込は こちら
こんにちは。株式会社タイミーのバックエンドエンジニアの神山( @ dak2 )です。 2026/4/22 から 24 まで函館で開催された RubyKaigi 2026 に参加し、Day 2 に登壇しました。 タイミーでは、世界中で開催される技術系カンファレンスに無制限で参加できる「 Kaigi Pass 」という制度を活用し、8名が現地でカンファレンスに参加してきました。 登壇内容や参加セッションで得た学びは各レポートにまとめています。気になった方はご覧いただけると幸いです。読者の皆様の今後の学びの参考になれば嬉しいです。 tech.timee.co.jp tech.timee.co.jp tech.timee.co.jp Road To RubyKaigi 2026 地震や飛行機の遅延などもありましたね。皆様の中にも、移動に戸惑った方いらっしゃったのではないでしょうか。 色々大変でしたが、参加者の方々が無事到着されたのを X(旧Twitter)で見て、一安心したのを覚えています。 印象に残ったセッション A Faster FFI rubykaigi.org 自分が作っている gem で Ruby と Rust を使っており、FFI 周りが気になっていたので聞きました。 FFIは、引数の数の確認や型変換(アンボックス)を実行時に行う必要があるため、C拡張に比べてオーバーヘッドが大きいようです。また、ピュアなRubyで書かれたJITコンパイラのFJITはC拡張より速いものの、CRubyの内部データ構造に強く依存していました。そのため仕様変更に弱く、実用的ではなかったとのことです。 FJIT これですね。 fjit.rb · GitHub これに加え、アーロンは hacks gem というものを作っていて、そこから CRuby の 構造体を引っ張ってきて FJIT で使っているみたい。AST をダンプして構造体の名前を元にデータ構造を抽出、メモリのレイアウト情報を計算して Ruby のハッシュに格納して返してるみたいですね。 面白いなー。すごい力技だw FJIT はこのハッシュに依存しているから実用的ではなかったとのこと。 新たな解決策として ffx という gem を作ったみたいです。 Ruby の値を C に変換してネイティブ関数を呼ぶ impl 関数と、実際のコードへジャンプするトランポリン関数を生成。トランポリン関数を生成する際に、アセンブリに impl 関数への jump 命令を書いておき、その次のメモリ領域にマジックマーカーや型情報、パラメータなどのメタデータを書き込んでおく。 通常の実行時には impl 関数へ jump するので、型情報などは読み込まれないけど、ZJIT でのコンパイル時にはマジックマーカーを検知してメタデータを読み取って最適化に使うとのこと。 このハックは単純にすごいなと思いました。感心しながら聞いていたのを覚えています。勉強になります。個人的に印象に残るセッションでした。 Lightning-Fast Method Calls with Ruby 4.1 ZJIT rubykaigi.org speakerdeck.com 行く末が気になる ZJIT ですね。速さは正義ということで、とても期待しています。そこで、このセッションを聞きに行きました。 今の ZJIT はメソッド呼び出し時に全てのパラメータを一度メモリにロードしてしまう課題があるみたいですね。 バックトレースや例外処理、ローカル変数読み込みなどでスタック上に正確なメタデータを残しておく必要があるためとのこと。 これに対し、 Lightweight Frames が提案されていました。 メタデータの書き込みを遅延させ、書き込むフィールドを「JIT Return」の1つなど最小限に限定しつつ、メソッドコール時のメモリライトを削減すると。ただ、ローカル変数の処理や例外時の longjmp などをどうにかしていかないといけないみたいですね。帰ってきてから ZJIT の内部を読んでみています。 Matz Keynote なんか Matz が一番楽しそうだったなあと思う Keynote でした。Ruby という言語を作ってなお、まだ作りたいものがある。AI と一緒に作った Spinel を発表している Matz が楽しそうだった。AI Slop とか色々ありますが、作りたいものを作るって最高ですよね。最高にクールでした。非常にインスピレーションを受けた Keynote でした。 matz リツイート 登壇 登壇時の写真 special thanks to @ginkouno rubykaigi.org speakerdeck.com AI 時代の Ruby では、NoMethodError を静的に解析できたら良いのではないか、という内容で登壇しました。登壇は想像していた何倍も得るものがありました。 きっかけは「日常の疑問」だった このトークの種は、普段の業務で感じた引っかかりでした。型アノテーション付きの Ruby とそうでない Ruby を行き来していると、どうしても型チェックの遅さが気になります。一方で、AI Agent のコーディング能力が一気に上がったことで、「そもそも型をちゃんと書いていく ROI ってどうなんだっけ?」という疑問が、頭の片隅から離れませんでした。 この「気になり」を寝かせていたところ、CFP が1週間延長になった当日、サウナで急に像を結びました。よく「サウナで整うと閃く」と言いますが、実際は逆で、 普段から問題意識を温めていたからこそ、たまたま緩んだ瞬間に繋がったのかな と思っています。整うどころではなくなり、速攻で帰りました。その後、寝る間も惜しんでアイディアを形にし、CFP を提出したのが懐かしいです。 「型ありの Ruby と型なしの Ruby、両方を日常的に書いている自分だからこそ立てられる問い」だと思えたことが、提出のモチベーションになりました。自分の業務の中の違和感は、思っている以上にトークの種になるんだなと。 gem を作って「持論」を形にした セッション内で発表した Method-Ray という gem は、コアロジックに Rust を用いています。命名は X-Ray から着想を得ていて、個人的にも気に入っています。「こうすればできるのでは」という仮説をもとに設計し、最小限で動くものを形にし、gem として公開しました。僕の持論やスタンスを gem に込めた上で CFP を提出しました。 株式会社mov の @pjocprac さんが型関連のセッション内容をまとめてくださっており、僕の発表内容にも触れてくださっているので、詳細に興味があればぜひご覧ください。 RubyKaigiで型まわりの内容をまとめたブログ書きました! RubyKaigi 2026 型まわり4セッション聴講メモ — AI コーディング時代の Ruby の型 #RubyKaigi https://t.co/HZB5PJW7z0 — Takeshi Watanabe (@pjocprac) 2026年4月27日 登壇して初めて得られた3つのもの 発表後、いろんな方に声をかけていただきました。「ここの型解析どうしてる?」「なんでそう思ったんですか?」「英語話せるんですか?」などたくさんお声がけいただき嬉しかったです。 自分の盲点を埋めるフィードバック 質問内容を受けて「自分の gem もこう直さないといけないな」と気づきが連鎖的に出てきました。一人だと気づきにくい視点に気づかせてくれるというのは良い機会だなと。 自分のスタンスを認識する 今回はスタンスを取った発表をしたのですが、それに対していろんな意見をもらえました。賛否両論あると思いますし、いろんな意見があると思いますが、同時に自分の立ち位置もはっきりしました。スタンスを取った発表をして良かったなと思っています。 アイデアの昇華 @okuramasafumi さんとは、いわゆる廊下の話で、「テストが十分速ければ AI が PDCA を回しやすくなって、それは型解析の近似になっているのでは」という方向性を議論しました。登壇内容を、登壇の外で次のテーマへと押し進めてもらえる感覚があり、これは登壇したからこそ発生した会話なんだと思うと、良い機会に恵まれたなと嬉しく思いました。 次にやりたいこと 今後は、 Method-Ray の解析範囲を広げたいのと、RBS の Rust crate の利便性を高めたいですね。自分の gem で RBS のロードを Rust crate から行いたいなと思っています。また、廊下での会話の流れもあって、高速化にも気持ちが出てきています。登壇を経て、自分の中の「次にやること」のリストが、行く前より大きくなっています。 終わりに 今年の Kaigi も最高でした。運営の方々、本当に素敵な機会をありがとうございました。いろんな方と知り合えましたし、思考を深められました。ぼんやりと次にやりたいことが見えてきました。 次の RubyKaigi は宮崎 ですね。次も楽しんで行きましょう!(帰りに青森、秋田に旅行したんですが、それ含め諸々個人ブログで感情を書こうと思います)
はじめに はじめまして、プラットフォームエンジニアリング本部に所属している徳富( @yannKazu1 )です。 みなさん、サプライチェーン攻撃って気にしてますか? npm パッケージの乗っ取り( ua-parser-js 事件 )、GitHub Actions の改ざん( tj-actions/changed-files 事件 )、依存パッケージへのバックドア混入( xz-utils 事件 )……。ここ数年、OSS を取り巻くセキュリティの前提がガラッと変わってきています。正直、「いつ・どこから仕掛けられるかわからない」状況です。 しかもサプライチェーン攻撃って、攻撃側のコストが低いわりに被害範囲が広いのが厄介なんですよね。 そんなわけで、ECS Fargate 環境におけるサプライチェーン攻撃対策を整理してみようと思ったのですが、いきなり全部を洗い出そうとしてもカオスになるだけ。何かいいフレームワークはないかな……と探していたところ、Kubernetes の 4C セキュリティモデル(Cloud → Cluster → Container → Code) の考え方がそのまま使えそうだったので、これをベースにチェックシート的に整理してみました。 「うちの環境だとどこが手薄いんだろう?」を考えるときの参考にしてもらえればと思います。 おことわり: これをやれば完璧!というものではないです。あくまで「見通しよく整理するための道具」として 4C モデルを借りているだけなので、実際にどこまでやるかは環境やリスク許容度に応じて取捨選択してください。 整理に使う 2 つの軸 軸 1:4C セキュリティモデル —「どこを守るか」 Kubernetes の公式ドキュメントで紹介されている、クラウドネイティブセキュリティを 4 つの同心円レイヤー で捉えるモデルです。 参考: クラウドネイティブセキュリティの概要 | Kubernetes ┌─────────────────────────────────────────┐ │ Cloud(クラウド基盤) │ │ ┌─────────────────────────────────────┐ │ │ │ Cluster(オーケストレーター) │ │ │ │ ┌─────────────────────────────────┐ │ │ │ │ │ Container(コンテナランタイム) │ │ │ │ │ │ ┌─────────────────────────────┐ │ │ │ │ │ │ │ Code(アプリケーション) │ │ │ │ │ │ │ └─────────────────────────────┘ │ │ │ │ │ └─────────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ │ └─────────────────────────────────────────┘ ポイントは 各レイヤーが外側のレイヤーの上に構築されている ということ。どれだけアプリのコードを堅牢にしても、基盤レイヤーのセキュリティが低い水準では守りきれません。だからこそ、特定のレイヤーだけに頼るのではなく、すべてのレイヤーを固める 多層防御(Defense in Depth) が基本方針になります。 ECS Fargate への読み替えはこんな感じです。 4C レイヤー K8s での意味 ECS Fargate での対応 サプライチェーン攻撃での主な攻撃面 Cloud クラウド基盤 AWS アカウント・IAM・ネットワーク IAM キー漏洩、ECR への不正 push Cluster オーケストレーター ECS クラスター・CI/CD パイプライン CI/CD アクションの改ざん、パイプライン侵入 Container コンテナランタイム Docker イメージ・Fargate タスク ベースイメージの汚染、OS パッケージへのバックドア混入、実行時の不正プロセス Code アプリケーションコード ソースコード・依存パッケージ パッケージ乗っ取り、typosquatting、悪意ある PR 軸 2:対策の目的 —「何のために守るか」 4C モデルは「どこを守るか」を整理するフレームワークですが、それだけだと対策が偏りがちです。そこでもうひとつ、 「何のために守るか」 という軸を加えてみます。今回は、セキュリティ対策を以下の 4 つの目的に分類して整理してみました。 目的 説明 考え方 🛡 予防(Prevention) 攻撃を未然に防ぐ そもそも悪いものを入れさせない 🔍 検知(Detection) 攻撃や脆弱性を発見する 入り込んだ・紛れ込んだことに気づく 🧱 封じ込め(Containment) 侵入後の被害を最小化する やられても被害を広げさせない 🔎 調査(Investigation) 何が起きたかを追跡する 事後に原因と影響範囲を特定する よくある落とし穴は 「予防」ばかりに意識が向いて、他が手薄になる こと。完璧な予防は不可能なので、入り込まれた後にどう気づいて・どう被害を抑えて・どう調べるか、まで含めて考えるのが多層防御の本質です。 この記事の構成 本記事では 目的(予防・検知・封じ込め・調査)を大項目 にして、それぞれの中で 4C のどのレイヤーに対する対策か を整理していきます。 🛡 予防(Prevention)— そもそも入れさせない 攻撃を未然に防ぐための対策です。「入口を塞ぐ」イメージですね。 Cloud:VPC Endpoint 概要: AWS サービスへの通信をインターネットを経由せずに VPC 内で完結させる。 防げる攻撃: 侵害されたタスクからの外部 AWS アカウントへのデータ持ち出し (Endpoint Policy で自社アカウントに限定) マルウェア感染後の C2 通信・情報送信 (Egress 全遮断下でも AWS サービスは利用可能) 漏洩した IAM 認証情報による外部からの不正アクセス (バケットポリシーで aws:SourceVpce を指定) 設定のポイント: S3 Gateway Endpoint(無料)は必須 ECR、SSM、Secrets Manager、CloudWatch Logs 用の Interface Endpoint を検討 Endpoint Policy で aws:PrincipalAccount を制限 リソース側ポリシーで aws:SourceVpce を指定 Cluster:CI/CD パイプラインのハードニング 概要: GitHub Actions など CI/CD で使うサードパーティアクションを、改ざんされない形で固定する。 防げる攻撃: GitHub Actions の改ざん (tj-actions/changed-files 事件のように、人気アクションのリポジトリが侵害されてタグが書き換えられるケース) バージョンタグの上書きによる 意図しないコードの実行 設定のポイント: GitHub Actions は通常 uses: actions/checkout@v4 のようにタグやブランチで指定しますが、これらは 後から書き換え可能 です。tj-actions/changed-files 事件(2025 年 3 月)では、攻撃者がメンテナーの認証情報を侵害し、既存タグを悪意あるコミットに向け直すことで、汚染されたアクションを使う CI でシークレットがビルドログに書き出されるという被害が広範囲に発生しました。一方、 commit SHA でピンニングしていたユーザーは影響を受けませんでした (侵害期間中に対象 SHA へ更新していなければ)。 対策として、 commit SHA でピンニングする のが推奨されます。 # Before(タグ指定 - 書き換えられる可能性あり) - uses : actions/checkout@v4 # After(commit SHA でピンニング - 改ざんされない) - uses : actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 Dependabot や Renovate Bot で SHA の自動更新を設定 permissions: で各ジョブの GITHUB_TOKEN 権限を最小化 OIDC を使った AWS 認証に切り替え、長期クレデンシャルを廃止 パブリックリポジトリでは特に注意:ビルドログが公開されるため、ログ経由のシークレット漏洩のインパクトが大きい サプライチェーン攻撃との関連: これは Container レイヤーの digest ピンニングと同じ思想です。「同じ名前で違うものを掴まされる」攻撃を防ぐには、暗号学的なハッシュで内容を固定するのが基本になります。 Cluster:Secrets の安全な注入 概要: DB パスワードや API キー等の機密情報を、コードへの直書きではなく SSM Parameter Store や Secrets Manager から注入する。 防げる攻撃: ソースコードやコンテナイメージへのシークレット埋め込み を防止 Git リポジトリの漏洩時に クレデンシャルが直接露出するリスク を排除 KMS 暗号化により、AWS アカウントが侵害されても 暗号化キーなしでは復号不可能 設定のポイント: " secrets ": [ { " name ": " DATABASE_PASSWORD ", " valueFrom ": " /fargate/myapp/database-password " } ] Cluster:ECS Exec の制御 概要: ECS Exec(コンテナへの対話的アクセス)を必要時以外は無効化する。 防げる攻撃: IAM クレデンシャルが漏洩した場合の コンテナへの直接侵入 本番コンテナへの 不正なコマンド実行 設定のポイント: ECS Exec の制御は、サービス (または RunTask 呼び出し)レベル と IAM レベル の二重で行うのが確実です。 enableExecuteCommand はタスク定義のフィールドではなく、サービス(CreateService / UpdateService)または RunTask のパラメータです。 サービス定義(または RunTask 呼び出し)で無効化 : enableExecuteCommand = false を明示。そもそもサービス側で受け付けない状態にしておく。AWS CLI でも aws ecs update-service --no-enable-execute-command のように切り替え可能です。 IAM で ecs:ExecuteCommand を Deny :ECS Exec 専用の API なので、これを Deny するのが最も直接的。なお ECS Exec は内部的に SSM Session Manager の通信レイヤー(ssmmessages API)を利用するため、より厳密に制御したい場合は、タスクロールに ssmmessages:* 系の権限が紛れ込んでいないかも確認する 必要時のみ一時的に有効化する運用フロー :踏み台用の専用サービスを別途用意し、調査時のみそちらを起動する運用が安全。 補足: 後述の封じ込めセクションで紹介する readonlyRootFilesystem: true と ECS Exec は 両立しない 点に注意してください。SSM agent がコンテナファイルシステムへの書き込みを必要とするため、ルートファイルシステムを読み取り専用にすると ECS Exec が動きません(AWS 公式ドキュメントでも明記されています)。本番では readonlyRootFilesystem: true + ECS Exec 無効化、調査用の踏み台サービスでは ECS Exec 有効化、と用途で使い分けるのが現実的です。 Container:ベースイメージの digest ピンニング 概要: Dockerfile のベースイメージ指定を、タグだけでなく digest(SHA256 ハッシュ)で固定する。 防げる攻撃: ベースイメージのタグ上書き攻撃 (同一タグに悪意あるイメージを push) レジストリが侵害された場合の イメージ改ざんの検知 設定例: # Before(タグのみ) FROM ruby:3.3.0-bookworm # After(digest ピンニング) FROM ruby:3.3.0-bookworm@sha256:2e1e76e5b2... 運用のポイント: Dependabot や Renovate Bot で digest の自動更新を設定する digest がまだ付いていないイメージに digest を自動付与する pinning 自体は、現時点では Renovate のほうが運用面で先行 ( pinDigests プリセット)。Dependabot 側でも 2026 年 2 月に PR #14071 が dependabot-core 本体にマージされ、 docker_pin_digests という experiment flag として実装されました。ただし experiment flag は Dependabot サービス側で段階的に展開されるため、GitHub.com の Dependabot で「タグだけのイメージに digest を新規付与する挙動」がデフォルトで使えるかはタイミング次第です(フラグの有効化状況によっては自分の環境で効かないこともある点に注意)。 現時点で「すでに digest 付き」のイメージに対する digest 更新は Dependabot でも問題なくできます。タグだけで運用しているイメージを一括で digest ピン化したい、もしくは version と digest の同時更新・グルーピングや自動マージ条件など細かい制御をしたい場合は、Renovate を選ぶのが堅実です。 サプライチェーン攻撃との関連: Docker Hub のアカウントが侵害されて、同一タグに悪意あるイメージが push されることがあります。digest ピンニングをしておけば、たとえタグが上書きされても意図しないイメージを引っ張ってくることがなくなります。 Container:ECR イミュータブルタグ 概要: ECR リポジトリのタグを上書き不可(IMMUTABLE)に設定する。サプライチェーン攻撃対策の観点では 基本的に有効化しておくべき 設定です。 防げる攻撃: 既存タグへの 悪意あるイメージの上書き CI/CD パイプラインが侵害された際の イメージ差し替え 設定のポイント: ECR のタグ mutability 設定は、 2025 年 7 月 23 日のアップデート で以下の 4 モードに拡張されました。 モード 挙動 MUTABLE すべてのタグを上書き可(従来) IMMUTABLE すべてのタグを上書き不可(従来) MUTABLE_WITH_EXCLUSION デフォルト mutable、特定タグのみ immutable IMMUTABLE_WITH_EXCLUSION デフォルト immutable、特定タグ(例: latest )のみ mutable これにより、たとえば「 本番用の v1.2.3 のようなセマンティックタグや git SHA タグは IMMUTABLE で固めつつ、 latest だけは上書き可能にしておきたい 」という現実的な要件にも対応できるようになりました。 IMMUTABLE 化(または IMMUTABLE_WITH_EXCLUSION )すると同じタグでの再 push ができなくなるため、デプロイフローを以下のいずれかに合わせる必要があります。 タグを毎回ユニークにする : repo:v1.0.1 、 repo:gitsha-abc1234 のように、デプロイのたびに別タグを振る。既存のタグベースのデプロイフローを大きく変えずに済むので導入しやすい。 digest ベースのデプロイに移行する :ecspresso 等で image: repo:tag → image: repo@sha256:xxxxx に変更。改ざん検知の観点でもより堅牢で、究極的にはこちらが望ましい。 IMMUTABLE_WITH_EXCLUSION を活用する : latest のような可変運用が必要なタグだけを除外し、それ以外のタグは IMMUTABLE で固める。 まずは (1) のタグユニーク化で IMMUTABLE 化(必要に応じて (3) で latest を除外) から始めて、余裕があれば (2) の digest ベースに進化させていく、というステップ方式が現実的です。 注意: 現在 MUTABLE で運用している場合、デプロイ失敗時のイメージ再 push に依存していないか事前に確認してください。CI のリトライ等でタグの上書きが発生する構成だと、IMMUTABLE 化で詰まります。 Container:イメージ署名(AWS Signer / ECR Managed Signing) 概要: CI でビルドしたイメージに暗号署名を付与し、デプロイ時に署名を検証する。 防げる攻撃: ECR リポジトリが侵害された場合の 未署名イメージのデプロイ CI パイプラインをバイパスした 不正なイメージの注入 「CI でビルドされたイメージと本番で動くイメージが同一である」ことの 暗号学的な保証 実現方法: ECS には EKS の admission controller のような署名検証機構が長らくネイティブには用意されていませんでしたが、近年は AWS 側でも仕組みが整ってきています。代表的なパターンは以下の通り。 (1) 署名フェーズ : (a) Amazon ECR Managed Signing を使う(2025 年 11 月 21 日 GA、AWS の推奨アプローチ) :ECR にシグニングルールを登録しておくと、push 時に ECR が AWS Signer を呼んで自動で署名してくれる。クライアント側に Notation を入れる必要がなく、署名キーやその証明書ライフサイクルも AWS が完全マネージドで管理する。新規導入ならまずこちらを検討するのが筋。 (b) 自前で notation sign を実行(manual signing) :CI のビルドステップでイメージ push 後に Notation CLI + AWS Signer プラグインで署名する。署名タイミングや対象を細かく制御したい場合に選択。プラットフォーム ID は Notation-OCI-SHA384-ECDSA 。 いずれの場合も 署名キー自体は AWS Signer が完全マネージドで管理するため、利用者が KMS キー等を別途用意する必要はありません 。 (2) 検証フェーズ : (a) ECS Service Lifecycle Hook(PRE_SCALE_UP)で Lambda を呼び、 notation verify を実行 :ECS のサービスデプロイ時にイメージ署名を検証し、失敗時はデプロイをブロックできる。 現在 AWS が公開している ECS 向け公式パターンであり、 BLOCK_ON_FAILURE (厳格モード)と LOG_ON_FAILURE (監査のみモード)の両方をサポート 。 2025 年 7 月 18 日の ECS ネイティブ Blue/Green デプロイメント GA 以降、PRE_SCALE_UP hook は rolling update 戦略でも利用可能 (rolling update で使えるのは PRE_SCALE_UP のみで、それ以外の hook は Blue/Green 専用)。 (b) EventBridge → Lambda で push 直後に検証 :監査・アラート用途。Lifecycle Hook と違ってデプロイ自体はブロックできない非同期パターン。 (c) CodePipeline / CI のデプロイ前ステップで notation verify を実行 :パイプラインの中でブロックする方式。 注意: ECR イミュータブルタグと組み合わせると効果が高まります(タグの差し替え + 署名なしイメージのデプロイの両方を防げる)。Lifecycle Hook ベースの検証はデプロイをブロックできるため、本気の防御として AWS が現在推奨している実装パターンです。実装工数はそれなりにかかるので、リスク許容度と相談して導入判断するのがよいでしょう。まずは EventBridge 経由のアラート運用(監査)から始めて、本格運用で Lifecycle Hook に移行する、というステップでも問題ありません。 EKS との対比(参考): EKS の場合は Kubernetes admission controller(Kyverno、OPA Gatekeeper、Connaisseur 等)を使って、Pod 起動前に署名を検証するのが定石です。ECS Service Lifecycle Hook はこれに相当する仕組みを ECS 側で提供する位置づけと考えると理解しやすいです。 🔍 検知(Detection)— 入り込んだことに気づく 予防を突破された場合に、異常や脆弱性を発見するための対策です。 Cloud:GuardDuty + ECS Runtime Monitoring 概要: AWS 環境全体の脅威検知サービス。ECS Runtime Monitoring を有効にすると、Fargate タスク内の不審な振る舞いをリアルタイムで検知できる(ECS Fargate 向けは re:Invent 2023 で一般提供開始)。 防げる攻撃: コンテナ内での クリプトマイニングプロセスの実行 C2(Command & Control)サーバーへの通信 コンテナ内での リバースシェルの起動 既知のマルウェアバイナリの実行 設定のポイント: Fargate の場合、セキュリティエージェントは GuardDuty が自動でサイドカーとしてデプロイするため、タスク定義の変更は不要 Organizations の委任管理者から一括有効化が可能 サプライチェーン攻撃との関連: 汚染された npm パッケージや gem が実行時にこっそりマイニングスクリプトを起動したり、バックドアが C2 サーバーに接続したり……といったケースをリアルタイムで検知してくれます。「入り込まれた後」の最後の砦ですね。 Cloud:Security Hub(セキュリティポスチャ管理) 概要: AWS Foundational Security Best Practices (FSBP) などのセキュリティ標準に基づき、設定の逸脱を検出する。 防げる攻撃: セキュリティグループの過剰な公開、暗号化の欠如など、 設定ミスに起因する攻撃面の拡大 を防止 Inspector や GuardDuty の検出結果を一元的に集約し、 見逃しを減らす Container:イメージスキャン(CI + ECR Enhanced Scanning) 概要: コンテナイメージに含まれる脆弱性を、CI のビルド時とレジストリの両方でスキャンする。サプライチェーン攻撃対策の観点では OS パッケージレイヤーへの攻撃(xz-utils 事件のような)を捕まえるための要 となる対策です。 防げる攻撃: 既知の脆弱性を持つ OS パッケージ・言語ライブラリ がプロダクションに到達するのを検知 xz-utils 事件のように OS パッケージレベルでバックドアが混入したケース (CVE 公開後)を検知 アプリ側の依存パッケージマネージャー(Bundler / npm 等)では拾えない、 OS レイヤーの脆弱性 をカバー サプライチェーン防御の基本は 複数のポイントでチェックする ことです。CI でのビルド時スキャンとレジストリでの継続スキャンを組み合わせて、異なるタイミングで網をかけるのが効果的です。 CI ビルド時スキャン → レジストリスキャン(継続) → ランタイム監視 (Trivy / Inspector 等) (ECR Enhanced Scanning) (GuardDuty) デプロイ前にブロック 新規 CVE も自動で再スキャン 実行時の異常検知 CI でのスキャン 代表的なアプローチは大きく 2 系統あります。 (a) OSS スキャナ(Trivy / Grype 等) docker build 後にスキャンを実行し、脆弱性が閾値を超えたらデプロイをブロック セットアップが軽く、外部 API への依存もないので導入ハードルが低い Dockerfile の Linting(hadolint 等)も合わせて入れると効果的 (b) Amazon Inspector SBOM Generator (sbomgen) + Inspector Scan API AWS 公式アプローチ。 sbomgen でイメージから CycloneDX 形式の SBOM を生成し、Inspector Scan API に投げて脆弱性スキャンを実行 GitHub Actions なら AWS 公式の aws-actions/vulnerability-scan-github-action-for-amazon-inspector が使える。SBOM 生成からスキャン、結果のサマリー表示までワンステップで完結 Jenkins 用のプラグインも公式提供あり Inspector を既に有効化している環境なら、ECR Enhanced Scanning と 同じ脆弱性データベース・同じ判定基準 で CI 段階から検査できるのがメリット 生成された SBOM をアーティファクトとして保存しておけば、調査セクションで触れる SBOM 管理にもそのまま流用可能 CI で push 前に止められる のがこのレイヤーの最大の価値(シフトレフト)です。AWS 中心の構成なら (b) が一気通貫で扱いやすく、ツール選択の自由度を取りたいなら (a) という選び方になります。 ECR Enhanced Scanning(Inspector 統合) ECR push 時 + 新規 CVE 公開時に自動で再スキャン( CONTINUOUS_SCAN ) OS パッケージに加えて言語ライブラリも対象 Security Hub にネイティブ統合されるため、検出結果を一元管理できる Basic Scan Enhanced Scan (Inspector) 対象 OS パッケージのみ OS パッケージ + 言語ライブラリ(gem, npm, pip 等) スキャン頻度 push 時のみ push 時 + 新規 CVE 公開時(CONTINUOUS_SCAN) Security Hub 統合 なし あり 組織一括管理 なし あり(Organizations 委任管理者から) xz-utils 事件との関連: xz-utilsのような OS パッケージレベルのバックドアは、アプリケーションの依存パッケージマネージャー(Bundler / npm)レベルの SCA では検知できません。 コンテナイメージスキャンの OS パッケージレイヤー で拾うのが正しい守備範囲です。なお、xz-utils のバックドアはビルドプロセス中に難読化されたペイロードを段階的に展開する極めて巧妙な手口で、CVE 公開「前」の検知は実質的に困難でした。だからこそ、CVE 公開後にどれだけ早く対応できるかが勝負になります( CONTINUOUS_SCAN のような新規 CVE への自動再スキャンが効いてくるのはこのため)。 Code:SCA(ソフトウェア構成分析) 概要: アプリケーションが依存するパッケージ(Bundler の gem、npm のパッケージ等)の既知脆弱性および悪意あるバージョンへの依存を検出し、更新を促す。 アプリケーション依存パッケージレイヤーのサプライチェーン攻撃対策の中核 となる対策です。 防げる攻撃: 既知の脆弱性を持つアプリ依存パッケージ への依存を早期発見 パッケージの乗っ取り (ua-parser-js 事件、event-stream 事件のような)で注入された悪意あるバージョンへの自動更新を抑止 typosquatting (正規パッケージに似た名前の悪意あるパッケージ)への気づき ツール例: Dependabot(GitHub ネイティブ、Bundler / npm / Docker 対応) Renovate Bot(digest ピン対応、細かい設定が可能) GitHub Advanced Security の Dependency Review 設定のポイント: パッケージマネージャー(Bundler、npm)と GitHub Actions の両方を対象に daily または weekly でスキャン versioning-strategy の設定で意図しないメジャーバージョンアップを防止(npm なら lockfile-only 、Bundler なら lockfile-only 相当の制約をかける) 更新を即座に取り込まない :新バージョンに悪意が混入していた場合に備え、リリースから一定期間(例:3〜7 日)寝かせてからマージするポリシーも検討に値する 守備範囲の整理: SCA は アプリの依存パッケージマネージャー(Bundler / npm 等)の領域 が対象です。OS パッケージ(apt / yum / apk)レベルの脆弱性は SCA ではなく、前述のコンテナイメージスキャンが守備範囲になります。 ua-parser-js のような npm パッケージ乗っ取りは SCA、xz-utils のような OS パッケージへのバックドア混入はイメージスキャン、と分けて考えるとスッキリします。 Code:SAST(静的アプリケーションセキュリティテスト) 概要: ソースコードを静的に解析し、セキュリティ上の問題を検出する。 位置づけの注意: SAST はサプライチェーン攻撃そのものへの直接対策ではなく、 自前コードの脆弱性検出ツール です。ただし、攻撃者が悪意ある PR を送り込んできた場合(社外コントリビューターからの PR や、内部アカウントが乗っ取られた場合)に、危険なコードパターンを検出できる可能性があります。多層防御の一環として位置づけてください。 防げる攻撃: SQL インジェクション、XSS、CSRF 等の コーディングレベルの脆弱性 フレームワーク固有の危険なパターン(Rails なら Brakeman、Node.js なら ESLint Security Plugin 等) 悪意ある PR に含まれる 明らかに怪しいコードパターン 設定のポイント: CI で全 PR に対して実行し、マージ前に検出 言語・フレームワーク固有のツールを選定 🧱 封じ込め(Containment)— やられても被害を広げさせない 予防も検知もすり抜けて侵入された場合に、被害の範囲を最小限にするための対策です。「入られた前提」で考えるのがポイント。 Cloud:ネットワーク制御(Egress 制限) 概要: コンテナからのアウトバウンド通信を必要最小限に制限する。侵害が起きた後にデータ持ち出しや C2 通信を成立させにくくする、封じ込めとしての効果が大きい。 防げる攻撃: ランタイム侵害後の C2 サーバーへの通信をネットワークレベルで遮断 悪意あるパッケージや侵害されたコンテナによる外部へのデータ送信をブロック xz-utils 事件のようなバックドアが仮にコンテナ内に入り込んでも、外部との通信路を断つことで攻撃者の活動を阻害 設定のポイント: AWS Network Firewall:FQDN ベースのアウトバウンド許可リスト方式で必要なドメインのみ通す Route 53 DNS Firewall:悪意あるドメインへの DNS クエリをブロック Security Group:NAT Gateway 経由のアウトバウンドを最小化し、不要なポート・宛先を塞ぐ Cluster:IAM ロール分離(タスクロール / 実行ロール) 概要: ECS の実行ロール(ECR pull、ログ出力等)とタスクロール(アプリケーションが使う権限)を分離する。最小権限の原則そのものなので予防の性質も持ちますが、真価を発揮するのは侵害が起きた後—— 横移動の範囲を制限する封じ込めとしての効果が大きい ため、ここで扱います。 防げる攻撃: アプリケーションが侵害された場合でも、 ECR へのイメージ push や他タスクへの影響を防止 権限の最小化により、 ラテラルムーブメント(横移動)の範囲を制限 設定のポイント: 実行ロール :ECR pull、CloudWatch Logs、SSM パラメータ取得のみ タスクロール :アプリケーションが実際に必要とする権限のみ ssm:GetParameters の Resource は ではなくパラメータパス(例: /fargate/myapp/* )で制限 タスクロールに ecr:PutImage 等の書き込み権限が紛れ込んでいないか定期確認 Container:コンテナハードニング コンテナが侵害された後の被害を最小限にするための設定群です。まさに「封じ込め」の代表格。 readonlyRootFilesystem 概要: コンテナのルートファイルシステムを読み取り専用にする。 防げる攻撃: コンテナ侵害時の マルウェアのファイルシステムへの書き込み・永続化 攻撃ツールの 追加ダウンロードと配置 Web シェルの設置 設定例: { " containerDefinitions ": [ { " name ": " app ", " readonlyRootFilesystem ": true , " mountPoints ": [ { " sourceVolume ": " tmp ", " containerPath ": " /tmp " } , { " sourceVolume ": " app-tmp ", " containerPath ": " /app/tmp " } ] } ] , " volumes ": [ { " name ": " tmp " } , { " name ": " app-tmp " } ] } 注意 1: アプリケーションによっては /tmp や /app/tmp 、 /app/log 等への書き込みが必要です。tmpfs ボリュームをマウントして書き込み先を確保してください。 注意 2: readonlyRootFilesystem: true は ECS Exec と両立しません 。SSM agent がコンテナファイルシステムへの書き込みを必要とするためです( AWS 公式ドキュメント で明記)。本番タスクは readonlyRootFilesystem: true + ECS Exec 無効化、調査用の踏み台サービスは readonlyRootFilesystem: false + ECS Exec 有効化、と用途で分けるのが現実的です。 Linux capabilities の DROP ALL 概要: コンテナに付与される Linux capabilities をすべて削除する。 防げる攻撃: コンテナ内からの 不要な特権操作 を制限 権限昇格(setuid バイナリ経由、カーネル脆弱性等)を試みた際の 被害を抑制 USER ディレクティブが何らかの理由で無視・上書きされた場合の 保険 設定例: { " linuxParameters ": { " initProcessEnabled ": true , " capabilities ": { " drop ": [ " ALL " ] } } } 補足: Fargate では元々 capability の add は不可でしたが、プラットフォームバージョン 1.4.0 以降、 SYS_PTRACE の 1 つだけは追加可能 になりました(Sysdig Falco のような観測ツール用途のために 2020 年に解禁された経緯がある)。一方、 drop は問題なく可能 です。EC2 launch type ではすべての capability が利用できますが、Fargate で add できるのは依然として SYS_PTRACE のみという制約があります。非 root で実行していれば実質的な効果は限定的ですが、多層防御(Defense in Depth)の観点から「保険として入れておく」温度感で設定しておくと安心です。 非 root 実行 概要: コンテナプロセスを root 以外のユーザーで実行する。 防げる攻撃: コンテナ侵害時の ホストへのエスケープリスクを低減 ファイルシステムやプロセスへの不正操作の範囲を制限 設定方法: Dockerfile で USER app:1000 のように非 root ユーザーを指定 サイドカー(fluent-bit 等)も可能な限り非 root 化 🔎 調査(Investigation)— 何が起きたかを追える状態にする インシデントが起きた後に「何が起きたか」「影響範囲はどこまでか」を追跡するための対策です。地味ですが、ここが抜けていると事後対応でめちゃくちゃ苦労します。 Cloud:CloudTrail(監査ログ) 概要: AWS API コールの監査ログを記録・保全する。 防げる攻撃・実現できること: IAM クレデンシャルが漏洩した場合の 事後調査(フォレンジック) が可能になる 「誰が・いつ・どの API を叩いたか」を追跡でき、不正な ECR push や IAM 変更を特定できる ECS のコントロールプレーン操作(クラスター / タスク定義 / サービスの作成・更新・削除など)も AWS API コール経由で行われるため CloudTrail でカバーされる 。Cluster 層で「誰が悪意あるタスク定義を登録したか」「サービスが書き換えられたのはいつか」を追えるのはここ S3 Object Lock でログの改ざん・削除を防止 CloudTrail ログファイルの整合性の検証(CloudTrail log file integrity validation)でログ自体が改ざん・削除されていないことを事後検証できる 。CloudTrail が 1 時間ごとに、その間に配信されたログファイルのハッシュを含むダイジェストファイルを RSA で署名して S3 に配置するので、 aws cloudtrail validate-logs で「保管されているログが配信時のものと一致するか」を検証できる 設定のポイント: 組織トレイルでマルチリージョン・全管理イベントを記録 S3 Object Lock(GOVERNANCE モード以上)でログの不変性を担保 ログファイルの検証(Log file validation)を有効化 (コンソール作成時のオプション、CLI なら --enable-log-file-validation )。S3 Object Lock が「改ざんさせない」予防策、ログファイルの整合性の検証が「改ざんされていないことを示す」検出策で、両方揃えると証跡としての信頼性が一段上がる Advanced Event Selectors で S3 データイベントも記録 サプライチェーン攻撃との関連: たとえば攻撃者が CI/CD の認証情報を奪って ECR にイメージを push した場合、CloudTrail がなければ「いつ・どのアカウントから push されたか」すら追えません。同様に、奪った認証情報で RegisterTaskDefinition や UpdateService を叩かれた場合も、CloudTrail があれば Cluster 層での改ざんを追跡できます。防御だけでなく「何が起きたか調べられる状態にしておく」のも大事です。 Cloud:VPC フローログ / DNS クエリログ(ネットワークレベルの追跡) 概要: VPC 内の通信メタデータと DNS 名前解決を記録し、「どこから・どこへ・何が通信したか」を追跡可能にする。 実現できること: 侵害されたタスクが どこに通信していたか (C2 サーバー、不審な外部エンドポイント、想定外の内部リソース)を特定できる VPC フローログは ECS タスクの ENI 単位で取得可能で、 タスクごとの通信を追跡 できる( ECS 向けの VPC フローログ機能 ) VPC フローログと Route 53 Resolver クエリログを組み合わせて分析することで、「いつ・どのドメインに対して通信したか」まで踏み込んだ調査ができる 。フローログ単体だと宛先 IP しか分からず、CDN やクラウドサービス相手だと「結局どこと話していたのか」が判然としない。クエリログで名前解決の履歴を突き合わせることで、IP → ドメインのマッピングが復元でき、不審な通信先の正体を特定しやすくなる マルウェアが DGA(ドメイン生成アルゴリズム)で生成したドメインへ問い合わせた痕跡など、フローログだけでは見えない挙動も DNS クエリログ側で捕捉できる 設定のポイント: VPC フローログは S3 / CloudWatch Logs に出力。長期保管なら S3 + Object Lock カスタムフォーマットで pkt-srcaddr / pkt-dstaddr を含めると、NAT 越しでも実際の送信元・宛先が分かる Route 53 Resolver クエリログを VPC に紐づけておけば、タスクからの DNS 問い合わせも記録される サプライチェーン攻撃との関連: 悪意ある依存パッケージが侵入した場合、最終的に外部 C2 への通信を試みるケースが多いです。CloudTrail は API コールの記録なので、こうした データプレーン上の通信 までは追えません。VPC フローログ + DNS クエリログを揃えて組み合わせ分析できる状態にしておけば、「侵害されたタスクがどのドメインを名前解決し、その IP に対して実際にどれだけのトラフィックを流したか」まで一連の流れで再構成でき、情報流出先や C2 ドメインの特定が一気に現実的になります。 Code:SBOM(ソフトウェア部品表) 概要: アプリケーションが依存するすべてのパッケージとそのバージョンを一覧化する。 実現できること: インシデント発生時に 「何のバージョンの何が動いていたか」 を即座に特定 新たな CVE が公開された際に 影響範囲を迅速に判断 xz-utils 事件のように 依存関係に紛れ込んだバックドア が後から発覚した場合の影響調査を効率化 実現方法: CI 段階で sbomgen を使う:検知セクションで触れた aws-actions/vulnerability-scan-github-action-for-amazon-inspector 等は、副産物として CycloneDX 形式の SBOM を出力する。脆弱性スキャンと SBOM 保存が同時にできるので一石二鳥 Amazon Inspector の SBOM エクスポート機能 を使うと、Inspector でスキャン済みのリソース(ECR イメージ等)の SBOM を CycloneDX または SPDX 形式で S3 にエクスポート できる。リアルタイム生成ではなく一括エクスポート方式なので、定期実行ジョブとして仕込んでおくのが現実的 いずれの場合も、イメージのバージョン(できれば digest)と SBOM を紐づけて保管 しておくのがポイント。インシデント時に「あの時動いていたバージョン」の SBOM をすぐ参照できるようにする サプライチェーン攻撃との関連: 「うちで動いてるイメージに xz-utils の脆弱バージョンって入ってたっけ?」を即答できる状態を作っておく、というのが SBOM の本質です。インシデント時に手作業で全イメージを掘り返すのは現実的ではないので、平時から仕組み化しておきましょう。SBOM は OS パッケージとアプリ依存パッケージの両方をカバーするため、「SCA で見える範囲」と「イメージスキャンで見える範囲」を横断して影響調査できる のが強みです まとめ 正直、セキュリティ対策って「どこまでやればいいの?」が永遠の問いだと思うんですが、まずはこのマトリクスで現状を棚卸しして「ここが手薄いな」と見えるようにするだけでも一歩前進です。 この記事が、ECS Fargate 環境のサプライチェーン攻撃対策を考える際のチェックシートとして使ってもらえれば嬉しいです。 参考資料 クラウドネイティブセキュリティの概要 | Kubernetes Amazon GuardDuty ECS Runtime Monitoring Amazon Inspector ECR scanning Amazon Inspector SBOM Generator (sbomgen) Integrating Amazon Inspector scans into your CI/CD pipeline aws-actions/vulnerability-scan-github-action-for-amazon-inspector Amazon Inspector SBOM export AWS Signer for container images Streamline container image signatures with Amazon ECR managed signing Amazon ECR now supports managed container image signing (2025-11-21) Extending deployment pipelines with Amazon ECS blue green deployments and lifecycle hooks Container image signing and verification using AWS Signer for Amazon ECS and AWS Fargate Amazon ECR now supports exceptions to tag immutability (2025-07-23) Preventing image tags from being overwritten in Amazon ECR ECS タスク定義 - linuxParameters KernelCapabilities - Amazon ECS Fargate security best practices in Amazon ECS Monitor Amazon ECS containers with ECS Exec (readonlyRootFilesystem との非互換について) Security hardening for GitHub Actions GHSA-mrrh-fwg8-r2c3: tj-actions/changed-files supply chain compromise CISA: Supply Chain Compromise of Third-Party tj-actions/changed-files (CVE-2025-30066) and reviewdog/action-setup (CVE-2025-30154) CVE-2024-3094: XZ Utils Backdoor Renovate Docker Presets Dependabot: Add digest pinning when updating Docker image tags (#14065 / PR #14071, 2026-02-04 マージ済み)
はじめに タイミーでは、世界中で開催される技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度を活用し、8名がRubyKaigi 2026 in 函館に現地参加しました。 また今年はDay2に、タイミーから @ dak2 さんが "No Types Needed, Just Callable Method Check" というタイトルで登壇しました。 本レポートでは、参加したエンジニアが注目したセッションごとに、ポイントや得た知見をまとめてご紹介します。 各セッションごとに内容を整理し、参加者自身の視点から学びや気づきをまとめています。読者の皆様にとって、今後の学びの参考になれば幸いです。 Surviving Black Friday: 329 billion requests with Falcon! rubykaigi.org Shopify社のSamuel Williamsさん、Marc-André Cournoyerさん、Josh TeeterさんがFalconを導入することでブラックフライデー・サイバーマンデー(BFCM)期間の合計3,290億リクエストを乗り切ったお話でした。 自身は普段Webのバックエンドエンジニアとしてユーザートラフィックのパフォーマンス課題について関心を持っており、その流れでFalconや基礎技術のAsyncにも興味を持っていました。 Falconの開発者であるSamuelさんから、本番稼働中の高トラフィックなプロダクトにFalconを導入する際のコストや注意点、そして得られる効果を聞けると期待して、このセッションを聴講しました。 前提として、FalconとはAsyncベースのRack互換Webサーバーです。 AsyncとはRubyの軽量スレッドであるFiberを利用した非同期I/Oフレームワーク(ノンブロッキングI/O)です。 github.com Falconは単一プロセスの中で複数のHTTPリクエストに対応するFiberを割り当てます。 FiberでI/Oが発生すると、処理を別のFiberに移すことで、大量のリクエストの処理を実現します。 OSネイティブのプロセスやスレッドではなく、Fiberで並行処理を実現するためメモリ効率が良く、PumaやUnicornと比較してランニングコストが低いという特徴があります。 セッションではFalconのアーキテクチャの解説と導入に伴う段階的なスケールテストを通して多くの課題を解決していった様子を発表されていました。 取り組みの結果、従前のUnicornよりもスループット・コアあたりのリクエスト数・レイテンシが改善し、冒頭にもあったBFCM期間の3,290億リクエストを捌き切りました。 まず最初に自分が抱いた感想として、プロダクトの課題を解決しつつOSSコミュニティへの還元を忘れないという点に感銘を受けました。 導入の過程でFalconに対して行われた改善は誰もが利用できるようになっています。 Falconを実際のプロダクトに導入し、入念なテストを重ねてブラッシュアップしたうえで、個社最適化にとどめずOSSとして誰もが使える形で公開し、Rubyコミュニティ全体へ還元する姿勢に、エンジニアとしてとても憧れました。 また、発表の中でスケールテストの重要性について強くお話をされていたのが印象的でした。 最近の自身の関心ごとの一つとして、以下にテストをしやすい環境を作るか・本番に近い環境でテストができるかというものがありました。 Shopify社は「Genghis」というツールを用いてこれを行っているそうなのですが、詳しい情報が見つけられなかったのでご存知の方がいれば教えていただけると嬉しいです。 また、同社のテックブログを読むと「Game Day」と称してスケールテスト以外にもカオスエンジニアリングや障害のシミュレーションなどかなり大規模で入念なテストを行っていたことがわかります。 shopify.engineering ユーザー増加やキャンペーン施策によるトラフィックの増加は、パフォーマンスだけではなくランニングコストという面においても重大な関心事です。そうした状況で、Falconは有効な選択肢の一つだと感じました。 実際に触ってみて他のRack互換アプリケーションサーバーとどのような違いがあるか試してみたくなったので、まずは自身のローカルのRailsでFalconを動かしてみようと思います。 FalconのRuby on Railsの導入に関しては2025年のKaigi on RailsのキーノートでSamuelさんが発表しておりこちらもおすすめです。 kaigionrails.org 志賀( @akitoshiga ) Practical TypeProf: Lessons from Analyzing Optcarrot speakerdeck.com @mametter さんによる、Rubyの型解析ツールTypeProfを、Ruby製8ビットマシンシミュレータであるOptcarrotに適用し、偽陽性の型エラーをゼロにするまでに何をしたのかという話でした。 TypeProfは、最小限の型注釈でエラー表示、定義ジャンプ、補完を提供するエディタ支援ツールです。型検査機にはSteepやSorbetがありますが、TypeProfはそれらとは異なり、型注釈をほとんど書かずにコードから型を推論します。 偽陽性の型エラーの原因は、実行時の不変条件が伝わらず、コード上nilになり得る型に対するメソッド呼び出しによるもの、動的メソッドで解析自体が難しいものなどがありましたが、本発表ではそれらをどう見つけどう直したかというものをAIの活用も含めてお話しされていました。 原因調査の中で、AIがキーポイントの調査において有効であることが語られました。実装全体の把握やその中でも特に多く呼び出される処理の特定は人間が調査するにはコストが高く不向きであると思うので、その部分をAIが十分に代替できるというのは素晴らしいなと思いました。 一方で、RBSでの型付けはAI任せだと雑になりがちなので一部は自分の手で直したと語られていました。まだまだAIは万能ではなく、人間が有効に使えているかどうかを判断する必要があり、場合によっては人の手で修正することが必要であるというメッセージとして受け取りました。 話の最後には 同氏が以前公開された記事 を踏まえ、型を書かないRubyがAIコーディングとの相性が良い可能性について触れており、その中で型の役割についてもガードレールとして機能するとされていたが、定量効果については慎重に評価すべきではないだろうか、としていました。 今後の展望としては、まずは人間向けに改善を進めていき、AIの使い勝手が安定してきた頃にAIエージェントを助けるためのツールとしての改良も検討しているとしていました。 RubyがAIコーディングと相性が良いかもしれないという示唆は、Rubyをメインで使っている者として、今後も使っていく価値のある言語である可能性を示されたような安心がありました。一方で動的言語であるが故に実行時エラーの懸念が常に付き纏いますが、そこを少ないコストで軽減できるツールとしてTypeProfの進化には期待したいです。 Rubyにおける型は、人によって意見が分かれると思います。個人的には、「型はあるに越したことはない。しかし、自分でわざわざ型注釈は書きたくない」という意見です。TypeProfは僕のようなRubyistにとって理想のツールとなる可能性があります。 RubyKaigi後、早速TypeProfを試してみようと思いましたが、RBSでモジュールエイリアスを定義しているとエラーによりTypeProfが起動しない問題に遭遇したので、PRを作成しました。 github.com 今後も自分にできる範囲でTypeProfの進化に貢献していきたいです。 rhiroe( @buta_botti ) Ruby Releases Ruby rubykaigi.org @hsbt さんによるRubyのリリースを支えるRubyによる仕組みづくりについての発表でした。 今回の発表で印象的だったのは、数値で示されたリリースプロセスの進化と「徹底してRubyを使い込むこと」でした。 かつてRubyのセキュリティリリース現場では、最大4つの安定版ブランチにパッチを当てるために複数名のコミッターが5〜6時間に及ぶ深夜作業をされていたそうです。インフラ構成管理やリリーススクリプト、各種自動化ツールをRubyで揃えて改善を進めたことで、それが今や1名が1時間程度の作業で完了できる体制になったといいます。自動化による効率性向上に加えて、緊急性が高いセキュリティパッチ対応におけるRubyコミュニティのアジリティが底上げされたと感じました。 更に、リリースの頻度についても、2022年の年間9回から2024年の15回へと約1.5倍に加速しています。hsbtさん自身が、Ruby 4.0以降は「2週間に1回の頻度でリリースする」という目標を立てられているといい、既にRuby 4.0リリース以降から10回のリリースが実現できているようです。テスト基盤はGitHub ActionsとRuby CIの2系統で運用され、GitHub Actionsには約120のworkflowが存在するなど、幅広いプラットフォーム・ビルドパターン・コンフィギュレーションを継続的に検証できる体制が紹介されていました。 特に、開発版Rubyにおけるパフォーマンス向上やメモリ削減などの成果を現行の安定バージョンに還元するバックポートの仕組みや、OIDC連携によるTrusted Publisher、Sigstoreを用いた署名など、Bundlerを含むエコシステム全体に最新のセキュリティ標準が組み込まれていくことが紹介され、Rubyエコシステムの揺るぎない技術的信頼を感じました。 RubyによるRubyのためのリリースプロセスの改善が、あらゆるRubyistへの良質なユーザー体験を形作っているのだと感じられる発表でした。 江田( @edy629s ) Lightning-Fast Method Calls with Ruby 4.1 ZJIT speakerdeck.com こちらのセッションでは今後のRubyの進化には欠かすことのできないZJITに関する内容でした。 特にメソッドコールに関する詳細なアプローチについて話されていました。 このセッションは今回の会場で一番広いホールで行われたのですが、そのほとんどの座席が埋まっているという状態で始まりました。 これはRubyにおけるZJITの注目度の高さが伺えますし、ShopifyのJIT開発チームでもYJITの頃から活躍されているk0kubun氏のトークであることからも多くの人を集めたセッションとなったのだと思います。 セッションの内容はメソッドコールにおけるRuby内部のアプローチについてRuby VMとYJIT、そしてZJITについて比較を行いながら話されていました。 まず最初にZJITのメソッドコール最適化の方法はいかにmemory writeを減らすかということに主眼をおいていることがわかりました。 メソッドコールはコードの実行においては多くの割合を占めるものであり、ここの部分での最適化の積み重ねが最終的に大きな改善となりうる箇所です。 本セッションではmemoryに加えてregisterの活用とそれぞれの活用順序を組み合わせることで、YJITと比べてもより高効率な最適化を目指したことを個々のステップを丁寧に説明されていました。 私はこのセッションにおいて、情報工学において誰もが習うであろうmemory, registerの組み合わせがいかに重要かを改めて認識することになりました。 CRubyという巨大な処理系においてもコンピューターの基礎となるアーキテクチャを駆使することで改善を積み重ねられるという事実に、ZJITという大きな取り組みの中にも基礎ありき、という重要さを再認識させられました。 関口亮一 ( @ryopeko ) Blazing-fast Code Indexing for Smarter Ruby Tools rubykaigi.org この発表では Rubydex という Rust 製の Ruby Code Indexer が紹介されていました。RubyLSP や Tapioca に統合することで最大10倍の高速化と2倍のメモリ削減を実現したという内容でした。 また、Ruby ツールのための統一的なコードインデックス基盤としてのビジョンも示されていました。Shopify の Ruby DX チームが9名関わっていることから、Rubydexに対するShopifyの本気度がうかがえます。 個人的に RubyKaigi で最も注目していたのはこの Rubydex でした。というのも、これからの時代の言語選定において、大きな要素となるのがトークン効率の高さだと考えているからです。 mame さんによる Ruby はトークン効率が高い言語なのではないか という記事が話題を呼びましたが、私は言語自体のトークン効率と同程度もしくはそれ以上にトークン効率を高める補助ツールの存在が重要なのではないかと考えています。 AI エージェントにコードベースを調査させると、Grep → Read → また Grep…というループが延々と続き、トークンを湯水のごとく消費します。人間はこんなことはせず、エディタのコードジャンプや「このコードはこのあたりのファイルにあるはずだ」と当たりをつけて調べます。これと同じことを AI エージェントが行えるようになればトークン効率は劇的に改善するはずです。 実際にタイミーのモノリス Rails で Rubydex MCP を試したところ、簡単なベンチマークではトークン消費量を3〜4割削減できました。 tech.timee.co.jp トークン効率の改善としては、Claude Code の LSP サポートはまさにそのための機能だと思いますが、Rubydex は LSP 以外でも活用できます。Rubydex を基盤として、コードを静的解析してデッドコードを検出し、自動で削除・改善するワークフローを組むことができるかもしれませんし、AI によるコードレビュー時に「この Pull Request で変更したメソッドの全呼び出し元」をインプットとして与えることでシステム全体への影響をより詳しく検証できるようになるかもしれません。Rubydex は Ruby が “AI 時代にとっても最高の言語” であるための重要な技術基盤になると確信しています。 Rust 実装なのでコントリビュートの敷居が高いというのは正直なところなのですが、自分でできることを探して貢献したいと思っています。 新谷( @euglena1215 ) Matz Keynote rubykaigi.org 「Claude Codeでこんなの作ってみました」という話は最近よく目にしますが、今年のキーノートではRubyの生みの親であるMatzがそんな話をしだして自分は終始テンションが上がりっぱなしでした。ここ数年のMatzキーノートの中でも一番印象に残る内容だったかもしれません。 最近のMatzはあえて自分ではコードを書かない制約を課してClaude Codeで様々なプロジェクトに取り組んでいるといいます。自身の日常的な課題を解決するためにRSSリーダーをRuby on Railsで作ったり、組み込み向けRubyであるmrubyの実装の改善をしたり、そして極めつけは今回の目玉、RubyのAOT(Ahead of Time)コンパイラであるSpinelを開発したりと驚くべきスピード感で具体的なアウトプットを次々と生み出しています。 同日の『Ruby Committers and the World』では後継者問題も話題にのぼりましたが、そんな懸念をよそに新しい技術に目を輝かせ、誰よりも楽しそうにプロダクトを作り続けるMatzの姿には非常に感銘を受けました。 また、リーナス・トーバルズがLinuxとGitという世界的に利用されているプロダクトを複数世に送り出していることに触れ、自分もRuby以外でもう一発当てたいと話していたのも良かったです。Spinelがそのひとつになるかはわかりませんが、彼の手からまた新しい何かが生まれてくるんじゃないかという期待を抱かずにはいられない、最高のキーノートでした。 須貝( @sugaishun ) おわりに RubyKaigi 2026は、Rubyコミュニティの熱量と、言語としての更なる可能性を肌で感じられる3日間でした。 スピーカーの発表内容や企業ブースでの会話、DrinkUpイベントなどを通じて得た繋がりは、単なる情報交換に留まらず、新たな技術的な挑戦への大きな原動力となっています。 タイミーはRubyコミュニティの一員としてこれからも技術への探究心を燃やし続けていきます。 また来年、宮崎で開催されるRubyKaigi 2027でお会いしましょう!
1. はじめに DRE(Data Reliability Engineering)グループ のつざきです。タイミーのデータエンジニアリング部で、BigQuery / dbt / Cloud Composer / Looker といったデータ基盤の開発・運用をしています。 DREチームでは 2026 年 2 月から、AWS が提唱する AI-DLC(AI-Driven Development Life Cycle)というワークフローを運用しています。きっかけは、 1 月末に AWS 主催の研修「Unicorn Gym」で3 日間 AI-DLC を体験したことでした。 AI-DLC 自体とタイミー全体への波及は同部の橋本さんが、Operations フェーズ(リリース後の検証)の独自構築については同じ DRE G の chanyou さんが、それぞれまとめています。 3日間のUnicorn Gymが1ヶ月で組織を変えた —— データで見るAI-DLC導入の波及効果 (橋本さん) 「リリース後」に向き合うAI駆動開発の実践 (chanyou さん) 本記事はこれらの続編的な位置づけで、「DREチーム が Inception と Construction フェーズで何を実装・運用しているか」に絞って書きます。 対象読者 : AI-DLC を個人ではなく、チーム(モブ)で運用したい開発/データ基盤チーム この記事の目的 : 公式の想定(単一プロジェクト/個人運用)を、複数リポジトリ・リモートモブ前提に翻訳した実装パターンを共有する 扱わないこと : Operations フェーズの詳細、全社展開の話、AI-DLC の一般解説 TL;DR DREチームは 2026 年 2 月から AI-DLC を運用中 実装 : Workspace + CLAUDE.md 読み替え、Intent 単位の運用 モブ : 1 日 3 ~ 4 時間のフルリモートモブ。狙いは「フロー効率(承認ゲートで止まらない)」「キーパーソンに頼らない(新基盤導入や新メンバー受け入れに効く)」「AI 出力の欠陥を集合知で減らす」の 3 つ 3 ヶ月の結果 : Intent 完了が月 14〜17 件で推移、PR 数は維持、サイクルタイムに劇的な変化は見えず 記事の立ち位置 : 公式に書かれていない実装の隙間(Mob、複数リポジトリ、パス読み替え等)を自分たちで翻訳した事例として記録 2. AI-DLC をざっくり AI-DLC の全体像は既出記事に譲り、本記事で後から使う概念だけ押さえておきます。 本記事での用語の使い方 Intent : 1 つのゴール(例: あるデータソースを BigQuery で使えるようにする) Unit : Intent を疎結合に分解した作業単位(DDD の Subdomain 相当。例: Terraform 追加、DAG 実装など) Ritual : モブでの儀式的な作業(後述の Mob Elaboration / Mob Programming / Mob Testing) Workspace : ドキュメントとルールを置く専用リポジトリ フェーズと成果物の階層 AI-DLC には 3 つのフェーズがあります。 Inception : 要件分析・設計 Construction : 実装・テスト Operations : デプロイ・監視 3 つの Mob Ritual 各フェーズには対応する儀式(Ritual)が定義されています。 Mob Elaboration (Inception): 要件分析・分解を全員で Mob Programming (Construction): 実装を全員で Mob Testing (Construction): テストを全員で いずれも、公式推奨は「物理集合 + 共有スクリーン + ファシリテーター」です。 Human Oversight = Loss Function AI-DLC は AI が実行主体、人間は各ステップで検証・承認する構造です。公式ペーパーの表現が印象的で: "Each step serves as a strategic decision point where human oversight functions like a 'loss function' - catching and correcting errors early before they snowball downstream." 機械学習の損失関数のように、人間のレビューが早期にエラーを補正する、というメタファーです。後の章でモブワークの話をするときに効いてきます。 3. 公式ドキュメントに書かれていない実装ギャップ chanyou さんの記事では、awslabs/aidlc-workflows リポジトリで Operations フェーズがプレースホルダになっている話が出てきます。実は Inception と Construction の側にも、公式の文書と実装の間にいくつかのギャップがあります。 awslabs/aidlc-workflows の構成 原典の awslabs/aidlc-workflows は MIT-0 ライセンスで公開されている、マークダウンのルールファイル群です。 aidlc-rules/ ├── aws-aidlc-rules/ │ └── core-workflow.md # ワークフロー本体 └── aws-aidlc-rule-details/ ├── common/ # 共通ルール ├── inception/ # Inception 詳細 ├── construction/ # Construction 詳細 └── operations/ # プレースホルダ ギャップ 1: ルール実装に Mob の記述がない AI-DLC 公式ペーパーでは Mob Elaboration / Mob Programming / Mob Testing が中核の儀式として定義されています。しかし原典のルールファイル群を mob で grep してもヒットしません。実装部分は「個人と AI エージェントが 1 対 1 で対話しながら承認ゲートを通す」構造になっており、Mob は想定されていない書き方です。 ギャップ 2: 公式チュートリアルは個人開発の例 AWS 公式ブログの実践記事 Building with AI-DLC using Amazon Q Developer のサンプルは、単一 HTML ファイルの川渡りパズルを個人で作る例だけで、モブで回す実演は出てきません。 ギャップ 3: 複数リポジトリの扱いが明確でない 公式は単一プロジェクト前提です。データチームのように「1 つの機能を作るのに複数リポジトリにまたがる」ケースへの具体的な示唆はほぼありません。 理念と実装の翻訳が必要 つまり、公式ペーパーに書かれた「Mob ワーク」や「複数チームでの協調」を実際に動かすには、自分たちで翻訳する必要があります。DRE では、各ギャップに対応する形で次のように対処しています。 ギャップ 1(Mob がルールにない) → モブでの意思決定を組み込み(章 6) ギャップ 2(単一 Intent 想定) → Workspace + CLAUDE.md 読み替え(章 4) ギャップ 3(複数リポジトリが薄い) → 複数リポジトリを 1 Intent でまとめる(章 5) 次章から具体に入ります。 4. DRE の実装: Workspace + CLAUDE.md 読み替え AI-DLC を Claude Code で回すために、DRE では次の構成にしています。 全体像(先に結論) ルール階層 : aidlc-rules/ (上流)→ .claude/rules/ (上書き)→ CLAUDE.md (入口) パス読み替え : aidlc-docs/requirements.md を aidlc-docs/intents/<YYYY-MM>/<intent_name>/inception/requirements.md に読み替え Intent 箱 : Intent ごとに独立したディレクトリ( intents/<YYYY-MM>/<intent_name>/ ) 状態管理 : aidlc-state.md に Status と Code Repositories を記録 スキル化 : Intent ライフサイクルを Claude Code のスキルで操作 以下、理由と詳細を順に見ていきます。 なぜこの構成なのか awslabs のリポジトリは単一プロジェクト・単一 Intent 前提で書かれていて、1 つの aidlc-docs/ ディレクトリに成果物を蓄積する想定になっています。 一方で DRE は、Intent という単位で開発を進めていて、完了した Intent もそのまま保存しています(後述しますが 2026 年 3 月は 14 件の Intent が完了しました)。Intent ごとに独立したディレクトリが必要になるので、パス読み替えが不可欠です。 ルール階層(継承構造) aidlc-rules/ : awslabs/aidlc-workflows の中身をそのまま取り込む。手動変更禁止、 /aidlc-rules-update スキルで上流追従 .claude/rules/ : プロジェクト固有のルール。aidlc-rules のオーバーライドや追加ルールを置く CLAUDE.md : エントリポイント。プロジェクト概要とディレクトリ規則を最小限に記述 上流は変えない。プロジェクト固有の振る舞いは派生側で足す。オブジェクト指向の継承に近い発想です。 [入口] CLAUDE.md ├─ 参照: aidlc-rules/ # 上流(awslabs 同期、変更禁止) └─ 参照: .claude/rules/ # 派生(DRE 固有、オーバーライド+追加) パス読み替えの例 awslabs のルールは、成果物の置き場として aidlc-docs/ というパスを前提に書かれています。DRE ではこれを Intent ごとのディレクトリに読み替えます。 公式: aidlc-docs/requirements.md DRE: aidlc-docs/intents/<YYYY-MM>/<intent_name>/inception/requirements.md この読み替えは .claude/rules/aidlc-workflow.md に定義してあり、Claude Code が実行時に解釈します。ルール本体(aidlc-rules/)は触らずに、パスだけ派生側で書き換える構成です。 Intent ディレクトリの構造 1 つの Intent のディレクトリはこういう構造です。 aidlc-docs/intents/<YYYY-MM>/<intent_name>/ ├── intent.md # Intent の目的・受け入れ基準 ├── aidlc-state.md # Intent の状態管理 ├── audit.md # 監査ログ ├── inception/ │ ├── requirements.md │ ├── stories.md │ └── ... └── construction/ └── <unit_name>/ ├── functional-design.md ├── code-generation.md └── ... aidlc-state.md のカスタマイズ Intent の進捗追跡に使う aidlc-state.md は、公式テンプレートをベースに少し拡張しています。 Status : OPEN / SUSPEND / CLOSED の 3 値を追加 Assignee : 担当者 Code Repositories : 複数のコードリポジトリのブランチ状態を記録 この Code Repositories セクションが次の章(複数リポジトリ運用)の鍵になります。 スキル化 Intent のライフサイクル管理は Claude Code のスキルとして定義しています。 /aidlc-intent-start : 新規 Intent 開始 /aidlc-intent-continue : 既存 Intent の再開 /aidlc-intent-save : 作業内容を PR 化してマージ /aidlc-rules-update : 上流(awslabs)への追従 chanyou さんの記事では /inception のように AI-DLC のワークフローそのものを呼び出すスキルが紹介されています。一方、DRE では「Intent というライフサイクルの入れ物」をスキル側で担う構成にしています。どちらも awslabs のルールに乗りつつ、スキルで扱う粒度が違う、という関係です。 5. 複数リポジトリを 1 Intent でまとめる DRE のようなデータ基盤チームでは、1 つの機能を作るのに複数のリポジトリが絡みます。 典型的なワーク 例えば「ある外部 SaaS のデータを BigQuery に自動転送するパイプラインを構築する」といった Intent だと、以下のようなリポジトリにまたがる変更が必要になります。 GCP Terraform リポジトリ : IAM やデータセットの定義 Composer インフラリポジトリ : Cloud Composer や Secret Manager の Terraform Composer DAG リポジトリ : Cloud Run Job と Airflow DAG のコード dbt リポジトリ : staging モデル これを 1 つの Intent としてまとめます。まず Inception フェーズで全体の要件・設計を固め、その後 Construction フェーズで各リポジトリに Unit を切って進めます。例えば DRE の 2026 年 2 月に動かしたあるパイプライン構築 Intent では、4 ユニット・60 ドキュメント・6 PR で完了しました(規模感の一例として)。 ブランチ戦略の 2 階建て ドキュメントとコードで別々のブランチ戦略を使い分けています。 Workspace リポ : session/<intent_name>/<hex> という短命ブランチ。スキル呼び出し単位で切って都度 main にマージ コードリポ : feature/<intent_name> という長命ブランチ。Intent が完了するまで維持 Workspace 側はドキュメントの進捗を小さくマージして積み上げ、コードリポ側は実装が揃ったタイミングで main に入れる、という二層構造です。 aidlc-state.md に Code Repositories を記録 1 つの Intent が複数リポジトリに触るので、どのリポのどのブランチで作業しているかを aidlc-state.md に記録しておきます。 ## Code Repositories - < dbt-repo > (feature/ < intent _name> ) - < composer-dag-repo > (feature/ < intent _name> ) - < composer-infra-repo > (feature/ < intent _name> ) - < gcp-terraform-repo > (feature/ < intent _name> ) Intent を再開するときも、Claude Code がどのブランチをチェックアウトすればよいか即判断できます。 横断ドキュメントとして蓄積される Inception で作る requirements.md や application-design.md は、複数リポジトリにまたがる機能の「横断的な設計書」になります。公式ペーパーではこうした成果物を "semantically rich context memory" と呼んでいて、AI がライフサイクル全体で参照する知識として機能します。 「1 機能 = 複数リポジトリ」というデータチーム特有の性質と、AI-DLC のコンテキストメモリの思想が、意外とうまくかみ合った部分です。 並列 Unit と audit.md 分散 モブとは別に、1 つの Intent の中で独立した複数 Unit を並列処理で進めるケースもあります。これは、Worktree で複数の Claude Code Agent を同時起動する方式です。このとき地味に困ったのが audit.md の Git コンフリクトです。並列の Agent が 1 つの audit.md に書き込もうとして衝突します。 対策として、 audit.md は Intent レベルのマイルストーンのみ記録する役割に限定し、Unit 内の詳細ログは construction/<unit>/audit.md に分散する運用にしました。このルールは .claude/rules/parallel-unit-audit.md に定義しています。 6. モブでの意思決定: なぜモブにしたか DREチームではメンバー 5〜6 名でモブワークを組み、1 日 3 ~ 4 時間をこれに充てています。全員フルリモートのため、公式ペーパー推奨の「物理集合 + 共有スクリーン」ではなく、Google Meet を接続して画面共有しながら進めています。本章では「なぜモブにしたか」と「どう使い分けているか」を DRE 視点で書きます。 3 つの狙い モブを採用する狙いは、マーク・パール『モブプログラミング・ベストプラクティス』で挙げられている利点と重なります。特に以下の 3 つが今の DRE に効いています。 フロー効率(Loss function を強化する) 章 2 で触れた通り、AI-DLC の各ステップには人間の検証・承認ゲートがあります("human oversight functions like a 'loss function'")。この承認が詰まるとフロー全体が止まる構造です。 AI の確認質問(clarifying questions)に即答できる人がその場にいないと、承認ゲートで数時間〜半日止まることもあります。Intent 単位で開発を進める AI-DLC では、並行して複数の Intent を抱えるより、少数に集中する方がリードタイムが短くなります。 少人数のDREチームでモブをやると、WIP は 1〜2 Intent に絞られ、意思決定の待ち時間がほぼゼロになります。モブは AI-DLC の Loss function を強化する実装とも言えます。ただし、外部チームへの依存がある場合はこの限りではなく、PR レビュー待ちや情報提供待ちになることはあります。 キーパーソンに頼らない Cloud Composer の統合や TROCCO から dlt への移行など、DRE は新しい基盤への切り替えを多く抱えています。こうした新基盤は「誰か一人だけが仕組みを分かっている」状態になりがちで、障害対応や次の意思決定がその 1 人に依存するリスクがあります。 モブで設計判断を進めると、全員が同時に基盤の意図を理解していきます。ドキュメントで読むのと、設計を議論しながら作るのとでは、後の理解度の深さが違います。 加えて、Intent の議論は Claude Code の audit.md に自然と記録されていくので、後から加入したメンバーが「なぜこの設計にしたか」を追えるようになります。口頭の議論を議事録に起こす手間が、AI-DLC の運用中に自動で払われる形です。 AI の出力の欠陥を減らす モブで AI の出力をその場でレビューすると、一人では見逃しがちな欠陥が減ります。厳密に比較計測したわけではないのですが、集合知がうまく効いている実感はあります。 AI の提案に対して「そこは業務仕様的にこう違う」「その構成だと監視が弱くなる」「別の選択肢の方が運用が楽」といった指摘が即座に入るので、Intent 完了後に気づく修正が減っていると感じます。 使い分け: 全員モブ / 2 分割 / 個人 タスクの性質に応じて、モブの粒度を変えています。 粒度 想定シーン 全員モブ(チーム全員) Intent の Inception、新基盤の設計判断、障害対応 2 分割モブ(2 ~ 3 人 × 2) 複数 Unit を並列で進められる Construction 個人ワーク 既知パターンの実装、軽微なドキュメント整備 判断基準は「不確実性」と「依存関係」です。不確実性が高いものは全員モブ。独立した Unit に切れるなら 2 分割。どちらも低い軽微な作業は個人。 公式ペーパーでも Mob Elaboration(Inception 相当)は必須、Mob Programming(Construction 相当)は分岐可能、と書き分けられていて、DRE の使い分けもほぼこの原則通りです。 リモートモブの実装と未解決の課題 公式ペーパーが想定するモブは「物理集合 + 共有スクリーン + ファシリテーター」ですが、DRE は全員フルリモートなので、ここを読み替える必要がありました。現在の構成は Google Meet + 画面共有 + 各メンバーのローカルエディタ(VS Code / Zed / Vim など人により様々)+ Claude Code です。 タイピスト交代 タイピスト交代は時間交代式(30 分サイクル)で、現タイピストが /aidlc-intent-save スキルで作業を保存・マージし、次のタイピストが自分のマシンで /aidlc-intent-continue で引き継ぎます。 試して撤退したもの VS Code Live Share : タイピスト交代のスイッチングコストを下げる狙いで試しましたが、ターミナル共有はできても接続環境によって表示が崩れ、肝心の Claude Code 拡張機能自体は Live Share で共有できなかったため断念しました タイマーの Claude Code スキル化 : タイピスト交代を時間で促すスキルを試作したものの、時間ベースで AI セッションに割り込む仕組みが安定せず、一旦撤退。今は Google Meet のタイマー機能で運用しています バットマン 『モブプログラミング・ベストプラクティス』に登場する「バットマン」(モブの外で外部からの問い合わせや割り込みに対応するメンバー)に倣い、その日の障害対応当番はタイピストに入れない運用にしています。データ基盤の障害対応はアラート監視と並行処理が必要で、モブに入ると集中を妨げるためです。 未解決の課題: スイッチングコスト 『モブプログラミング・ベストプラクティス』では 10 分ごとの交代が目安として紹介されていますが、DRE では現状 30 分が限界です。AI-DLC を始めた当初は 1 時間以上かかっていたのを、保存・再開処理の高速化や Google Meet のタイマーで短縮してきました。それでも、各メンバーが自分のマシンで作業するスタイルでは、保存・再開を速くしても、セッションを次の人のマシンに同期し直すスイッチングコストが残ります。10 分にはまだ遠いのが現状で、ここはリモートモブの構成上の制約として残っています。 7. 3 ヶ月の数字 AI-DLC を運用してみて、何が数字で変わったかを見ていきます。 計測の方針 2025 年 10 月〜 2026 年 4 月の PR を集計しました(4 月は 4/24 時点)。対象は、DREチームのメンバー(5〜6 名程度)が author の PR に限定しています。なお、ドキュメント中心の Workspace リポは /aidlc-intent-save で作られる即マージ PR が多く数字を歪めるため、集計は コードリポジトリのみ にしています。また、他チーム調整が必要で構造的に長い PR は、上位 5% を外れ値として除外しています。 Intent 完了数 一番わかりやすい変化は、Intent 単位で開発を進められるようになったことです。 月 完了 Intent ドキュメント成果物 2026-02 0(初月、進行中 1) 60 2026-03 14 263 2026-04(4/24 時点) 17 215 2 月は AI-DLC 導入初月で Intent の完了は 3 月にずれ込みました。3 月は 14 件完了、ドキュメントは 263 ファイルが積み上がりました。4 月は月途中(4/24 時点)で既に 17 件の Intent が完了しています。 PR 数 コードリポジトリのマージ済み PR 数(DRE メンバー author 分)です。 月 コードリポ PR 2025-11 36 2025-12 24 2026-01 43 2026-02 41 2026-03 74 2026-04(4/24 時点) 58 1 日 3 ~ 4 時間をモブに充てているので、「モブで時間を使っている分、実装量は減るのでは?」という懸念もありましたが、少なくとも PR 数の面では減っていません。2026-03 で 74 件、4 月は月途中で既に 58 件のペースです。 PR サイクルタイム DRE メンバーの PR サイクルタイム(作成〜マージ)です。外れ値除外後の値です。 月 PR 数 中央値 P90 2025-10 19 1.5h 64.9h 2025-11 36 10.6h 104.7h 2025-12 24 21.2h 94.5h 2026-01 43 2.1h 89.1h 2026-02 41 6.5h 137.5h 2026-03 74 2.6h 96.5h 2026-04(4/24 時点) 58 2.2h 89.6h 月ごとのばらつきが大きく、AI-DLC 導入前後で劇的な改善があるとは言いにくい数字です。ただ、モブで 1 日 3 ~ 4 時間を使いつつ中央値 2〜3 時間台で安定しているので、モブによる時間投入に見合う速度は維持できているとは言えそうです。 注記 この期間の数字を AI-DLC の効果だけで説明するのは慎重にしたいところです。大型案件の時期的な偏りや、メンバーの稼働割合など、他の要因も混ざっています。 それでも、Intent 単位で開発を進める仕組みが 2 月から稼働し、3 月に 14 件の Intent 完了まで回せるようになったのは定量的な変化です。PR サイクルタイムの劇的改善は見えていませんが、モブで使う時間分の生産性ロスが起きていない点は、少なくともマイナスの兆候は出ていないと言えます。 何が効いていそうか(仮説) PR サイクルタイムが変わっていない一方で Intent 完了数は増えている、という組み合わせから、いくつか仮説を立てられます(断定はできません)。 WIP の削減 : 少数の Intent にチーム全員が集中することで、リードタイムのばらつきが抑えられている可能性 レビュー待ちの削減 : モブで合意してから PR を作るので、PR 段階での非同期レビュー待ちが実質なくなっている(中央値を押し下げる方向) 外部依存が P90 を支配 : P90 は 60〜140h 台で大きく変わっていません。他チームとの調整や権限待ちで止まる PR が混ざっているためと思われ、ここは AI-DLC 単独では改善しにくい部分 記事執筆時点での仮説として記録しておきます。 8. やれていないこと 3 ヶ月で骨格はできた感覚がありますが、まだうまく回せていない領域もあります。 Operations フェーズ : DRE では /aidlc-ops-incident という障害対応スキルに留まっています。chanyou さんの記事で紹介されている Operations ワークフローを取り込むのが次のステップです ハーネスエンジニアリング寄りの自動化 : コードレビューはモブで全員でやっていますが、AI にコードをレビューさせる仕組み、Claude Code の Hooks / Agents の活用、AI 出力の評価ループなど、人間介入を減らす方向(いわゆるハーネスエンジニアリング)の仕組みはまだ薄いです。モブの検証密度は保ちつつ、自動化できる部分はそちらに寄せていきたいと考えています 本記事で触れなかった工夫 本記事は Inception と Construction フェーズの実装に絞ったため、以下のような工夫は紙面の都合で触れていません。続編で書く予定です。 多角的レビュー : コンテキスト分離型のサブエージェントで Inception / Construction 成果物をレビューする仕組み Knowledge Base 体系 : Intent 横断の仕様・運用ノウハウを knowledge-base/ に蓄積し、Intent 完了時に差分提案するルール 他チームとの擦り合わせルールの統合 : 連携する別チームとの合意事項を AI-DLC ワークフローに組み込み、PR 差分の自動準拠レビューも用意 効果計測・導入促進 : 月次効果計測レポートの自動生成と GitHub Discussions 投稿、社内全体の AI-DLC 導入状況レポート 運用ガードレール : 本番環境への破壊的操作を一律禁止するルール、bash コマンド実行規約 9. おわりに 3 ヶ月運用して現時点で落ち着いている構成を書き残しました。完成形ではないですが、この方向で続けるメリットはあると感じています。 公式ドキュメントには書かれていない実装の隙間を埋める例として、同じようにデータチームで AI-DLC を運用している方の参考になれば嬉しいです。 参考 AI-Driven Development Life Cycle: Reimagining Software Engineering — AI-DLC の理念を紹介した AWS DevOps Blog AI-DLC Method Definition(Raja SP, AWS)— https://prod.d13rzhkk8cj2z0.amplifyapp.com/ — 本記事で引用した "Human oversight as loss function" 等の出典 awslabs/aidlc-workflows — AI-DLC ワークフローの原典リポジトリ(MIT-0) Building with AI-DLC using Amazon Q Developer — Amazon Q Developer を使った実践ガイド マーク・パール『モブプログラミング・ベストプラクティス — ソフトウェアの品質と生産性をチームで高める』オライリー・ジャパン タイミーのデータエンジニアリング部 DRE G では、こうしたデータ基盤の構築と AI-DLC の運用に取り組んでいます。興味がある方は、以下よりプロダクト採用サイトをご覧いただけますので、ぜひカジュアル面談などお申込みください! 株式会社タイミー | プロダクト採用サイト
こんにちは、タイミーでSRE業務を担当している徳富(@yannKazu1)です。 先日、函館で開催されたRubyKaigi 2026に参加してきました。Ruby本体やパーサ、GC、JITといった「言語の中身」を深掘りするカンファレンスなので、普段アプリケーションコードよりインフラ寄りの仕事をしている自分が行って楽しめるのか、という気持ちも少しありました。ですが、結果としてとても楽しめたので、感想を書いておきます。 SREが行っても普通に楽しめた 普段の仕事はRailsアプリケーションを安定して動かしたり、スケールさせたり、観測したりすることが中心です。Ruby本体にコミットするわけでも、パーサを書くわけでもないので、専門外の話も多いだろうと思っていました。 実際、聞いていて全部の細部までは追えないセッションもありました。それでも、 普段ブラックボックスとして扱っているGCやランタイムの中身が、作っている人の口から語られる 他社のRails運用をやっている人たちと直接話せる このあたりだけでも参加する価値を感じました。 Day 1のブースが意外と良かった 参加されたことがある方ならわかると思いますが、Day 2以降はブース巡りが意外と慌ただしくなります。スタンプラリーで人が流れたり、セッションの合間で時間が限られていたり。 その点、 Day 1はスタンプラリーがまだ始まっていないので、ブースが比較的空いていてゆっくり話せる時間帯 でした。立ち寄っても順番待ちがほとんどなく、エンジニアの方とじっくり話せます。 RubyKaigiにはほぼ例外なくRailsを本番運用している会社さんがスポンサーとして出展しているので、SREとしては「他社さんのアーキテクチャや困りごとを直接聞ける場」として、これがかなり面白かったです。 Railsで完結する vs クラウドネイティブに振り切る 複数の会社さんと話して興味深かったのが、「Railsの中で完結させるか、AWSのマネージドサービスに切り出すか」の判断基準が会社によって全然違うことです。 Railsはよくできていて、ActiveJob + Sidekiq、ActiveStorage、ActionCableなどを組み合わせれば、大抵のユースケースはRailsの世界の中で完結します。わざわざクラウドネイティブなマネージドサービスに切り出さなくても、運用負荷を抑えながら回せるケースは多い。 一方で、「ジョブの遅延が事業KPIに直結するので、マネージドサービスに切り出して水平スケールを確実にしている」と話す会社さんもあれば、逆に「今のスタックで十分捌けているし、Rubyエンジニアが運用できる構成に揃えたほうが組織的に強い」と話す会社さんもありました。 技術選定の基準として挙がっていたのは、ざっくりこんな観点です。 スパイク耐性が事業上クリティカルかどうか 運用するチームのスキルセットと採用市場 コールドスタートを許容できるワークロードか RubyKaigiは登壇者も来場者もアプリケーションエンジニアが中心です。そのため、「Sidekiqのままいくか、それとも切り出すか」といったテーマひとつとっても、「アプリケーション側からどう見えているか、何が嬉しいかつらいか」という視点で語られていたのが印象的でした。 普段、SRE系のイベントで「インフラ側の都合」としてこの手の話を聞くことが多い私にとっては、この視点の対比が非常に新鮮に感じられました。 「正解は一つじゃない」と頭ではわかっていても、SRE目線とアプリ目線では同じ意思決定でも見えている景色が違います。両方の視点を持っておくことが大事だなと、改めて感じました。 AI活用の温度感 もう一つ、多くのブースでも話題になったのが AI活用 です。プロダクトへの組み込み、社内開発フローへの導入、営業・カスタマーサポートへの活用と、レイヤーごとに状況が違っていて面白かったです。 特に印象に残ったのが、SmartBankさんのブースで展示されていた「スマートバンクで働くAI Agentたち」のポスターでした。アプリユーザー向けの「 ワンバンフレンズ 」(家計を読み解いて気づきを届けるAIアシスタント)、社内メンバー向けの「 Ask! ワンバン 」(自然言語で社内データを検索・分析する分析AI)、そして開発者向けの「 Guardie 」(エラーや異常を検知してログ・コード・変更履歴を横断して原因特定を支援する番犬AI)という三本立てで、 ユーザー向け / 社内向け / 開発者向けの3レイヤーに対してそれぞれAIエージェントを配置している のがすごく整理されていて印象的でした。 特にGuardieはSRE視点でめちゃくちゃ刺さりました。「2時間覚悟していた調査が10分で終わった」という社内の声が紹介されていて、これは障害対応における MTTR(平均復旧時間)を本質的に短縮しに行っている 事例だなと。エラー検知 → ログ・コード・変更履歴を横断した原因特定までをAIに任せる、というのはこれから我々も作っていこうと思っていた仕組みが普通に動いていて、刺激を受けました。 ブース担当の方とは「どこまでをAIに任せて、どこから人間がやるべきか」「誤検知や暴走への安全装置をどう設計しているか」みたいな話までできて、こういう一次情報が聞けるのもRubyKaigiならではでした。 気になった話は、その後のアフターパーティでさらに深掘りできました。資料には載らない現場のリアルな知見が交換される場として、ブース + アフターパーティの組み合わせは、セッションと同じくらい価値があったと感じます。 参加したセッションを日ごとに振り返る ここからは、自分が参加して特に印象に残ったセッションを日ごとに紹介していきます。 Day 1: Exploring RuboCop with MCP (Koichi ITO さん) 1日目に聴いたのが、RuboCopコアチーム・MCP Ruby SDKチームメンバーのKoichi ITOさんによる、 RuboCopとMCP(Model Context Protocol) を組み合わせる試みについてのセッションです。 これまでRuboCopは「人間」または「他のプログラム(CIなど)」をきっかけに実行されてきました。そこにAI時代になって、 AIエージェントという新しい実行のきっかけ が登場した、というのが導入の話です。生成AIとリンター/フォーマッターをどう組み合わせるか、Rubyで実装されたMCPサーバーをエージェントの隣で走らせるとどうなるか、という内容でした。 技術的には、 MCP SDKの構造(サーバーとクライアントそれぞれのSDKがあること) トランスポート層として stdio と Streamable HTTP の2種類があり、用途で使い分けること HTTPトランスポートではセッション管理が肝になり、Pumaのようなスレッドモデル + シングルプロセス構成だと素直に動くが、複数プロセス・複数ホストに横断するとセッションの保持が難しくなること ただし Stateless Mode ( stateless: true )を使えば、Pumaの複数ワーカーやUnicornのような複数プロセス構成にも対応できること。ただしこれは リクエストごとに新しいtransportインスタンスが生成されるためMCP-Session-Idを共有できない という制約とのトレードオフであり、「セッション保持を諦める代わりに複数ワーカー/プロセスでもスケールできる」という割り切り あたりが特に勉強になりました。特にセッション管理の話は、MCPサーバーをWebアプリケーションとして本番に乗せようとすると、ロードバランサーやスケールアウトとの兼ね合いが出てくるという点で実践的でした。 ステートフル/ステートレスをどう使い分けるか は、これから考えないといけないテーマになりそうです。 セッションの締めくくりで「LLMの 確率的な性質 を決定的なツールに組み込むことで、これまでの決定的なツールとは違う未来が描ける」という話があって、そこも印象的でした。MCPの サンプリング (サーバーがクライアント経由でLLM補完を要求する仕組み)や、 Elicitation (実行中にユーザーへ追加情報を問い合わせる仕組み)といった機能は、ツールの形そのものを変えそうな予感があります。 スライド: https://speakerdeck.com/koic/exploring-rubocop-with-mcp Day 2: Chasing Real-Time Observability for CRuby (Shintaro Otsuka さん) 2日目で一番テンションが上がったのがこのセッション。CRubyの実行状態を リアルタイムに3D可視化する というツール「rrtrace」の話でした。 普通のプロファイラはサンプリングベースで、後から集計して結果を見る形が多いですが、このツールは「いまこの瞬間にCRubyの中で何が起きているか」を、複数スレッドのスタック状態として3次元空間にレンダリングしながら見せる、というアプローチです。デモを見せてもらった時、IRBに入力するたびにスタックが積み上がっていく様子がリアルタイムで見えて、純粋に「すごいものを見ている」という感覚になりました。 技術的なポイントとしては、 計測側のC拡張は軽量に保つ設計 で、イベントの収集と転送に特化している イベントは TracePoint API ( CALL / RETURN / INTERNAL_GC_ENTER / INTERNAL_GC_EXIT )や内部のスレッドイベント( INTERNAL_THREAD_EVENT 、こちらはWindowsでは利用不可)から収集し、timestamp(60bit) + event type(4bit) + method id/thread id(64bit)の合計16バイトの構造体に統一 C拡張(計測側)とビジュアライザプロセスの間は OS管理の共有メモリ上のリングバッファ で受け渡し ビジュアライザ側はCRubyのコアを使っていない別プロセスなので、可視化処理が重くてもCRubyの実行を直接ブロックしない スタックシミュレーションが重いため、 Parallel Scanアルゴリズム で並列化している という設計でした。ただし、スライドのベンチマーク結果を見ると、rrtrace有効時は 関数呼び出しスループットがplain CRubyの17%程度 (73,417,127 → 12,760,131 calls/s、約5.9倍遅くなる)、 Railsサーバーのrpsもplain CRubyの55%程度 (203.19 → 110.84 rps)になるとのことで、 計測のオーバーヘッド自体は「小さくない(not small)」 とスライドでも明記されていました。TracePointフックのコストが支配的で、ここは今後の課題とのことです。 それでも、「 重い処理を別プロセスに全部寄せる 」というアーキテクチャの考え方は面白かったです。計測側はできるだけ軽く保って、分析・可視化は別のリソースでやる。この割り切りが設計をシンプルにしていて、きれいだなと思いました。 「現代のマシンは10コア以上あるのが普通で、CRubyが1コアで動くなら残りのコアを観測やビジュアライズに自由に使える」という、リソースの捉え方の話も新鮮でした。GVLがある世界での観測ツールの設計思想として、納得感が強かったです。 スライド: https://speakerdeck.com/whitegreen/chasing-real-time-observability-for-cruby Day 3: The Less-Told Story of Socket Timeouts (Misaki Shioi さん) 3日目に聴いたのがこれ。ソケットライブラリのタイムアウトの 歴史と内部実装 を、Issue/Commitを参照しながらRuby 4.0までの流れに沿って解説していくセッションでした。タイトルからして気になっていたけど、期待以上の内容でした。 Socket.tcp / TCPSocket.new には、 resolv_timeout (名前解決のタイムアウト) connect_timeout (接続のタイムアウト) そしてRuby 4.0で追加された open_timeout (全体のタイムアウト) の3種類があります。「この3つがなぜ必要で、どういう順番で導入され、どんな歴史的な紆余曲折があったのか」を、Issue/Commitを参照しながら丁寧に追っていく構成でした。 特に印象的だったのが、 まず Socket.tcp に connect_timeout が導入され、続いて Addrinfo.getaddrinfo への timeout および Socket.tcp への resolv_timeout が追加されたこと resolv_timeout と connect_timeout を両方指定しても、全体のタイムアウト時間は制御できない (複数アドレスに対して逐次接続を試行するため、合計時間が想定より長くなりうる) これらの問題を解決するために、Ruby 4.0で 全体時間を管理する open_timeout が追加された という話の流れです。普段、HTTPクライアントの open_timeout / read_timeout / write_timeout を「だいたいこのくらい」で設定しがちですが、その下のレイヤーでは名前解決と接続が並行で走っていて、 タイムアウトの組み合わせによっては想定と全然違う挙動になる ということを、改めて意識させられました。 また、歴史的な経緯として特に面白かったのが、 名前解決の中断可能化(interruptible)をめぐる話 です。 getaddrinfo(3) はブロッキング呼び出しで、 Ctrl+C でも中断できないという問題が長年ありました。これを解決するアプローチとして、まず 2020年1月に「 Addrinfo.getaddrinfo が timeout をサポートする」提案が行われ、そのパッチで Addrinfo.getaddrinfo に timeout 、 Socket.tcp に resolv_timeout が追加されると同時に、内部的に「GNU拡張の getaddrinfo_a(3) が利用可能ならそれを使う」実装が入りました ( getaddrinfo_a(3) はワーカースレッドで非同期に名前解決を行う仕組み)。その後 2020年8月には「Make Socket.getaddrinfo interruptible」がマージされ、 Socket.getaddrinfo の内部もこの getaddrinfo_a(3) を使うように利用範囲が拡張 、さらに 2020年9月には TCPSocket.new にも resolv_timeout / connect_timeout が追加 され、名前解決を中断可能にする方向で進められました。 ところがその後、 Rails ActiveJobの統合テストが失敗するようになった という報告が入り、調査の結果、 fork後の子プロセスで getaddrinfo_a(3) を呼び出すとハングする ことが判明します。 getaddrinfo_a(3) は内部で再利用可能なワーカースレッドを保持しているのですが、forkでコピーされる子プロセスにはワーカースレッドが存在しないにもかかわらず内部状態は「ワーカースレッドが待機中」のままになっており、これによってデッドロックが発生する、という仕組みでした。 2ヶ月以上の調査と回避策の検討を経て、最終的には getaddrinfo_a(3) の導入自体が撤回 され、関連変更もrevertされました。その代わり、後に別アプローチとして 「名前解決ごとに専用のpthreadを立てて getaddrinfo(3) を実行する」方式 (mameさん提案、 ruby/ruby#8695 )が採用され、こちらは rsock_getaddrinfo 内に実装されることで、内部的に rsock_getaddrinfo を呼んでいる Addrinfo.getaddrinfo や Socket.getaddrinfo を含む幅広いメソッド で名前解決のブロッキング問題が解消された、という流れです。 外部API連携で「タイムアウトを設定したはずなのにハングする」「 connect_timeout を短くしたのに、複数アドレスがあるホストで合計時間が想定の何倍もかかる」みたいな経験がある人は少なくないと思いますが、まさにあれの背景にある話でした。タイムアウト設計の見直しや、Ruby 4.0以降は open_timeout を積極的に使っていくこと、テストでタイムアウト周りの挙動を確認しておくことなど、すぐに持ち帰れる学びがいくつもありました。 スライド: https://speakerdeck.com/coe401_/the-less-told-story-of-socket-timeouts リモートワーク時代の副次効果 もう一つ書いておきたいのが 社内メンバーとの関係性 の話です。 弊社エンジニアはリモートワーク中心で、普段の業務だと開発チームの全員と毎日話すわけではありません。SlackやZoomでは話すけれど、雑談ベースで「最近どう?」みたいな会話になりにくい人もいます。 それが、RubyKaigiで3日間一緒に過ごすと一気に距離が縮まります。一緒にセッションを聞いて、休憩中に「今のどう思った?」と話して、夜は飲みに行って、移動中に雑談する。この3日間の密度は、リモートでの数ヶ月分のコミュニケーションに相当するんじゃないかと思います。 おわりに RubyKaigiは「Ruby本体に関わっている人たちのお祭り」という側面が強いカンファレンスですが、Rubyを本番で動かしている側の人間にとっても十分に楽しめる場でした。SREとしても、ランタイムの理解が深まったり、他社の運用知見をもらえたり、社内の関係性が深まったりと、副次効果を含めて満足度の高い3日間でした。 普段Railsを動かしているSREの方や、これからRubyKaigiどうしようかなと迷っている方の参考になれば嬉しいです。
※ 2026年4月時点の情報です こんにちは、データアナリティクス部のkoyoです。2024年1月に「 データアナリストの一日 」という記事を書きました。あれから2年が経ち、分析の進め方がかなり変わったので、改めてお伝えできればと思います。 この記事で紹介するのは、AIへのプロンプトの工夫ではありません。AIが正しく動き続けるための環境を自分で設計した話です。 Before / After — 変わったのは「認知負荷の配分」 2024年の朝はこんな感じでした。Slackの通知を上から順に読んで、未読チャンネルを巡回して、カレンダーを確認して、「あ、あのスレッドに返信できていなかった」と気づく。情報を集めること自体に時間と集中力を使っていました。 2026年の朝は違います。出社するとSlack DMにブリーフィングが届いています。自分がやることは、それを読んで判断し、返信するだけ。 変わったのは作業の速さではなく、認知負荷の配分です。「何を見るべきか」を考える必要がなくなった分、「見たものに対してどう判断するか」に集中できるようになりました。 昨年からAIエージェント(Claude Code)に本格的に向き合ってきました。個人でも、データ収集・分析パイプラインの構築や、育児・家事を含めた日常オペレーションの自動化など、生活のあらゆる場面でAIとの協働を重ねてきました。 データの収集・加工・判断支援という一連の流れをAIと一緒に設計・運用する経験を積む中で、「この考え方は分析業務にそのまま適用できる」という手応えを得ました。それを業務環境に展開したのが、これからご紹介する仕組みです。 朝のブリーフィング — 8つの視点で1日を俯瞰する 毎朝、ブリーフィングが自動生成され、Slack DMに届く仕組みを構築しています。Claude Codeの /loop 機能(cronのようにコマンドを定期実行するスケジュール機能)を使い、毎朝決まった時間に実行される設計です。 カレンダーAPI、Slack API、Notion API、Google Tasks APIを横断して情報を収集し、8つの視点で1日を俯瞰できるブリーフィングにまとめます。この仕組みは既製品ではなく、API連携スクリプト、収集ロジック、検証ルール、Slackメッセージの整形まで自分で設計・実装しました。 朝のブリーフィング自動生成フロー 📅 朝ブリーフィング ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. 今日の予定 ← カレンダーAPI連携 2. 要対応 ← Slack未返信検出 + TODO期限 3. チーム動向 ← 所属チャンネルの横断要約 4. 注目チャンネル ← 担当プロジェクト関連の要約 5. 依頼更新 ← Notionの対応依頼 + チーム連絡の更新 6. ナレッジ鮮度 ← 知識ベースの最終更新チェック 7. 目標進捗 ← 四半期個人目標のリマインド 8. TODO追加提案 ← 全セクション横断の見落とし検知 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ → Slack DMに自動送信 ブリーフィングのSlack DMスクリーンショット ブリーフィングの3つの工夫 ① Slackの確認漏れ防止 直近3日間の自分宛スレッドを取得し、最終発言者が自分でなければ「未返信候補」として検出します。ただ、スレッド返信ではなく別のメッセージで対応済みのケースもあります。そこで、同チャンネルの同日付近にある自分の発言をクロスチェックし、「対応済みなのに未返信と誤検知する」ケースを排除する仕組みにしています。 これだけで「あのスレッドに返信できていなかった」が大幅に減りました。 ② 複数ツールの文脈を自動で横断する ただ情報を集めるだけなら、各ツールを開けば済む話です。このブリーフィングの価値は、人間が毎朝手作業で確認するには現実的でない量の情報を、構造化して届ける設計にあると思っています。 複数のSlackチャンネルを同時に監視し、チームの動向と担当プロジェクトの最新状況を毎朝要約します。分析依頼については、Slackの通知だけでなくNotionの依頼ページの中身まで参照します。そのうえで、自分の担当領域に合致するものを自動で判定します。会議予定にはNotionの議事録リンクやSlackの関連スレッドを自動付与する設計です。 「この会議って何の話だっけ?」「この依頼は自分が拾うべき?」を自分で調べに行く時間がなくなりました。 ③ TODO提案で見落としを防ぐ ブリーフィングの最後に、「TODO化すべきだがまだ登録されていない項目」を提案する仕組みを組み込んでいます。そのために、複数の情報ソースを優先順位付きで横断します。自分が「あとで対応する」と保存したSlackスレッド、未アサインの分析依頼、自分宛の未返信スレッド、全社向けの対応依頼 — これらを順にチェックし、既存TODOのタイトルと照合して重複を除外した上で提案します。 各提案には、「なぜTODO化すべきか」の判断理由を付与する設計です。提案の前には必ずスレッド本文を読み、タイトルだけでは判断しないルールも組み込んでいます。さらに、過去にタイトルだけで誤った提案をしてしまった経験から、このルールを追加しました。 曜日に応じて変わる情報収集 ブリーフィングは毎日同じではありません。月曜日にはナレッジの参照目次を最新状態に更新し、月初にはデータ基盤に加わった変更点をまとめて取得し、週明けにはデータ基盤の週次変更サマリーが新しい情報として表示されます。業務のリズムに合わせて情報収集の範囲が自動で変わる設計にしています。 ブリーフィングを支えるナレッジ基盤 ブリーフィングが正確に動くのは、AIが参照できるナレッジベースがあるからです。 ブリーフィングを生成する中で、AIは毎朝いくつもの判断をしています。たとえば: 「この分析依頼は自分が拾うべきか?」 → 自分の担当テーマの定義を参照 「このナレッジは古くなっていないか?」 → 各ファイルの最終検証日を参照 「この未返信スレッドは本当に未対応か?」 → クロススレッド対応の判定ルールを参照 「この社内用語は何を指しているのか?」 → 部門横断の用語集を参照 これらの判断を一つひとつ仕込んでおくのではなく、「判断に必要な情報」をAIがいつでも参照できる形で整備しておくのがナレッジ基盤の役割です。 目的別にディレクトリを分け、全体では12カテゴリ・約250ファイルを蓄積しています。 knowledge/ ├── business-logic/ ← 担当領域の定義・用語・判定ルール ├── collaboration/ ← コミュニケーション運用ルール ├── data-dictionary/ ← データ基盤の構造 └── sql-patterns/ ← 分析で使う設計パターン・検証テンプレ 最初から整備されていたわけではなく、日々の業務の中で少しずつ蓄積してきました。最初は空でも大丈夫です。使うことで育っていきます。 ブリーフィングが毎朝正確に届くようになって初めて、「判断の材料をAIが自律的に参照できる状態」こそがこの仕組みの土台なのだと実感しました。同じ考え方は、日常のクエリ作成や資料作成など別の業務にも応用しています。 設計思想 — AIを信頼できる同僚にする3つの原則 この仕組みを作る中で、AIとの協働に大切だと感じた原則が3つあります。 AIを信頼できる同僚にする3つの原則 ① 推測禁止 — 知らないことは調べる ブリーフィングでは、自分宛の未返信スレッドを毎朝検出しています。「このスレッドは未返信か?」を判定するとき、安易に「最終発言者が自分でなければ未返信」と推測すると、同チャンネル内の別メッセージで対応済みのケースを誤検知してしまいます。AIが推測で結論を出すと、毎朝同じ誤通知が届き続ける — これが一番厄介です。「知らないなら調べる、調べていなければ使わない」をルールに組み込むことで、この誤検知は大きく減りました。 ② 検証付き実行 — 作ったら検証してから報告する 未返信候補を検出したあと、同チャンネル内で自分が別メッセージで対応済みでないかを必ずクロスチェックしています。ブリーフィングの各セクションも、出力前に整合性を検証するステップを必ず挟んでいます。「動いたから正しい」ではなく、「検証したから正しい」を積み重ねていく考え方です。 ③ ソース付き情報 — 出所のない情報は存在しないのと同じ ブリーフィングの全項目にソースリンクを必須にしています。「どこかで見た気がする」ではなく、リンクを辿れば原文にたどり着ける。これがAIの出力を信頼できる理由です。 仕組みがあるからAIの出力を信頼できる。信頼できるから判断に集中できる。同じ3原則は、クエリ作成や資料作成にもそのまま当てはまる考え方でした。 変わったこと・まだ変われないこと 変わったこと 朝の情報確認が5分で完了するようになりました。Slackの返信漏れも大幅に減りました。一番大きいのは、「自分から情報を見に行く」から「情報が届く」に変わったこと。その分、判断と行動に使える時間が増えました。 これから変わりたいこと 情報収集と検証をAIに任せられるようになった分、DAとしてより価値の高い仕事に時間を使えるようになってきました。たとえば、事業課題の構造化や仮説の設計、ステークホルダーとの対話などです。ただ、まだその変化の途中にいます。 一番の課題は、この仕組みがまだ個人最適にとどまっていること。チーム全体で活用できる形にしていくのは、今後の挑戦です。 まとめ 2年前は「データアナリストの一日」を自分で全部やっていました。今は、朝の準備が完了した状態で1日を始められる環境を設計しました。 AIの能力は日々進化していますが、それだけでは業務の質は変わらないと思っています。AIが正しく動くためのナレッジや、出力を信頼するためのルール、見落としを防ぐための検証など、こうした「環境」を人間が設計して初めて、AIは信頼できる同僚になる。逆に言えば、環境を設計する力がこれからのデータアナリストに求められるスキルなのかもしれません。 自分はこういう形を選びましたが、やり方は人それぞれだと思います。もし興味があれば、まずは普段使っているテーブル定義を1つ、Markdownに書き出してAIに参照させてみるところから試してみてください。推測で書かれたクエリとの違いに気づくと、面白いと思います。 AIの社会実装や企業での本格導入がさらに進んでいく中で、こうした運用のあり方も磨きをかけながら形を変えていくと思います。そのときにまた、続編を書けたらいいなと思っています。 環境設計という視点が、どなたかの次の一歩のヒントになれば嬉しいです。 We're Hiring! タイミーでは、ともに働くメンバーを募集しています! データアナリストのポジションも募集中です。カジュアル面談も行っていますので、少しでも興味がありましたら、お気軽にご連絡ください。 データ | 採用情報 |株式会社タイミー
こんにちは、タイミーのバックエンド/Webフロント基盤チーム マネージャーの新谷( @euglena1215 )です。 先日開催された RubyKaigi 2026 に参加してきました。その中で特に気になったのが、Shopify の Alexandre Terrasa さんによる「 Blazing-fast Code Indexing for Smarter Ruby Tools 」という発表です。 この発表では rubydex という Rust 製の Ruby Code Indexer が紹介されていました。RubyLSP や Tapioca に統合することで最大10倍の高速化と2倍のメモリ削減を実現したという内容でした。また、Ruby ツールのための統一的なコードインデックス基盤としてのビジョンも示されていました。Shopify の Ruby DX チームが9名関わっているということで、Shopify への rubydex への本気度が伺えます。 発表の中では、Experimental ながら MCP サーバー(rubydex-mcp) も提供されていることが紹介されていました。Claude Code などの AI アシスタントからセマンティックにコードベースを検索できるようになっています。 AI Coding Agent にコードベースを調査させると、 Grep → Read → また Grep …というループが延々と続き、トークンがみるみる消費されていきます。rubydex-mcp を使えば、クラス定義やリファレンス、継承ツリーといった構造的な情報を、1回の MCP ツール呼び出しで取得できます。そのため、このループを大幅に削減できそうです。 実際にどの程度効果があるのか、タイミーのバックエンドリポジトリで定量的に検証してみました。 rubydex とは rubydex は Shopify が開発した Ruby Code Indexer です。 github.com Rust 製の Indexer がコードベースを解析し、MCP(Model Context Protocol)サーバーとして以下のツールを提供します。 ツール 機能 search_declarations 名前によるファジー検索(クラス、モジュール、メソッド、定数) get_declaration 完全修飾名による定義情報の取得(ドキュメント、祖先チェーン、メンバー) find_constant_references 定数の参照箇所をコードベース全体から検索 get_descendants クラス/モジュールの継承ツリーを取得 get_file_declarations ファイル内の構造一覧 codebase_stats コードベース全体の統計情報 通常、AI Coding Agent がコードベースを調査する際は grep や find でファイルを探し、 Read で中身を読み、また grep で次のファイルを探し…というループを繰り返します。rubydex を使うと、このループの多くが1回の MCP ツール呼び出しで完結できる可能性があります。 検証方法 概要 claude -p (Claude Code の非インタラクティブモード)を使い、rubydex あり/なし の2条件で同じプロンプトを実行し、トークン消費量と回答品質を比較しました。 検証環境 ツール:Claude Code CLI ( claude -p ) モデル:claude-sonnet-4-6 rubydex_mcp バージョン:0.1.0 対象リポジトリ:タイミーのバックエンド(モジュラーモノリス、70パッケージ) 条件の制御 外部要因を排除するため、以下の共通オプションを使用しました。 claude -p "<prompt>" \\ --output-format json \\ # トークン使用量を含む JSON 出力 --model sonnet \\ # モデル固定 --bare \\ # hooks, CLAUDE.md 等を無効化 --strict-mcp-config \\ # .mcp.json を無視し引数の MCP 設定のみ使用 --mcp-config <config> \\ # 条件別の MCP 設定 --tools "Read,Bash,Edit" \\ --no-session-persistence --bare で CLAUDE.md の自動読み込み等の副作用を排除し、 --strict-mcp-config により MCP サーバーの有効/無効を制御しています。これにより、2条件間の差は rubydex の有無のみになります。 プロンプト rubydex のツール群が効果を発揮しそうな質問を5種類用意しました。 ID プロンプト class_hierarchy XXXモデルのクラス継承チェーンと、includeしているモジュールの一覧を教えてください。それぞれのモジュールがどのファイルで定義されているかも含めてください。 find_references YYYモデルがコードベース全体でどこから参照されているか調査してください。参照元をコントローラ、モデル、サービス等のカテゴリ別に分類して一覧にしてください。 descendants ApplicationRecordを継承しているクラスの一覧を取得し、packs/ディレクトリ配下のパッケージごとに何個のモデルが存在するか集計してください。上位10パッケージを表示してください。 method_investigation XXXモデルに定義されているpublicインスタンスメソッドのうち、名前に'status'を含むものをすべてリストアップしてください。各メソッドの定義場所(ファイルパスと行番号)と、そのメソッドが何をしているかの簡単な説明を付けてください。 codebase_overview このRailsプロジェクトのpacks/ディレクトリ配下のパッケージ構成を調査してください。各パッケージに含まれるモデル数、主要なクラス名を一覧にし、パッケージ間の依存関係で特に密結合なものがあれば指摘してください。 各条件 × 各プロンプトで5回ずつ、合計50回実行しました。LLM の出力は非決定的なので、複数回実行して平均を取ることでばらつきの影響を軽減しています。 結果 トークン消費量 プロンプト rubydex なし(平均トークン) rubydex あり(平均トークン) 変化率 class_hierarchy 144,076 85,958 -40.3% codebase_overview 1,187,722 664,761 -44.0% descendants 46,501 165,795 +256.5% find_references 770,404 332,369 -56.9% method_investigation 986,840 636,411 -35.5% 5つ中4つのプロンプトで 35〜57% のトークン削減 を達成しました 🎉 コスト・速度 プロンプト コスト変化 実行時間変化 ターン数変化 class_hierarchy -16.7% -48.6% -35.4% codebase_overview -34.3% -19.6% -13.7% descendants +294.6% +261.0% +62.7% find_references -33.5% -19.0% -11.9% method_investigation -30.7% -50.1% -38.1% ターン数(エージェントのツール呼び出し回数)の削減がトークン削減の主因です。rubydex のセマンティック検索が Grep → Read の繰り返しを置き換えることで、エージェントループが短縮されています。 回答品質(LLM-as-a-Judge) トークンが減っても回答品質が下がっては意味がありません。別の Claude セッションを立ち上げ両条件の回答を渡し、正確性・網羅性・有用性の3観点で5点満点のスコアリングを行いました。 プロンプト rubydex なし(平均) rubydex あり(平均) 差分 class_hierarchy 4.33 4.00 -0.33 codebase_overview 4.33 4.00 -0.33 descendants 3.00 4.67 +1.67 find_references 4.00 4.00 0.00 method_investigation 3.33 4.33 +1.00 平均 3.80 4.20 +0.40 回答品質はむしろ改善しています 👏 Judge のコメントから見えた傾向を簡単にまとめます。 rubydex ありで改善した点(正確性) rubydex なしの場合、grep の結果をもとに関連しそうなクラスやメソッドを補足情報として列挙する傾向があった。目視では誤情報は確認できなかったが、裏取りが不十分な状態で情報を出しているため信頼性の判断がしにくい( method_investigation など) rubydex ありではインデックスに基づいた情報を返すため、出力の根拠が明確になっている rubydex なしが勝った点(網羅性・有用性) grep ベースの調査は探索範囲が広いため、rubydex が返さないカテゴリ(Policy、Mailer 等)の参照元も拾えていた( find_references ) rubydex なしの回答はファイルのフルパスや構造図を含むなど、開発者がすぐに使える形に整理されている傾向があった( class_hierarchy , codebase_overview ) 総じて、 rubydex ありは正確性で優位、rubydex なしは網羅性で優位 という傾向が見られました。また、いくつかのプロンプトの回答を目視でも確認しましたが、rubydex ありで明らかにおかしな内容を回答しているケースは見られませんでした。 descendants が悪化したケース 唯一、 descendants プロンプトではトークンが +256.5% と大幅に悪化しました。 このプロンプトは「ApplicationRecord の子孫クラスをパッケージ別に集計する」という内容です。rubydex の get_descendants ツールは全子孫を忠実に返します。(我々の環境では346クラス)一方、rubydex なしの場合は grep -r "< ApplicationRecord" のような検索で主要なものだけを拾うため、結果的にトークンが少なく済んでいました。 つまり、 大量の結果を返すような網羅的な検索では、rubydex がかえってトークンを増やす ケースがあります。ただし、品質面では rubydex ありの方が +1.67pt と最も大きく改善しており、正確性とのトレードオフと言えます。 まとめ 5つ中4つのプロンプトで 35〜57% のトークン削減 と 17〜34% のコスト削減 を達成しつつ、回答品質は LLM-as-a-Judge の評価で同等以上(5点満点で 3.80 → 4.20)でした。特に「クラスの参照元を探す」「メソッドの定義場所を調べる」といった構造的な検索タスクで効果が顕著です。 一方、全件取得のような網羅的検索ではトークンが増えるケースもあり、万能ではありません。rubydex が提供するツールの特性を理解した上で導入すると、より効果的に活用できそうです。 また、現状 rubydex-mcp を利用するには Rust ツールチェーン(cargo)が必要で、ソースからのビルドが求められます。開発環境の依存が増えることになるため、チーム全員が使える形に整備するかどうかはもう少し見極めたいところです。とはいえ、トークン35〜57%削減という効果は十分に大きく、rubydex-mcp への期待はとても高まる結果となりました。 検証の制約 今回の検証にはいくつかの制約があります。結果を解釈する際の参考にしてください。 --bare モードでの検証 : CLAUDE.md 等が無効化されているため、通常利用時とはベースのトークン消費量が異なります キャッシュの影響 : 実行順序や間隔によってキャッシュヒット率が変わるため、コスト比較は参考値です 回答品質の評価 : LLM-as-a-Judge による自動評価のみで、正解データとの突合は行っていません プロンプトの偏り : rubydex が得意そうなタスクを選んでいるため、実際の利用での改善幅はこれより小さくなる可能性があります
はじめに こんにちは、株式会社タイミーでデータサイエンティストをしている藤井です。 普段は推薦システムの改善を担当しています。 早速ですが、皆さんは推薦モデルの改善実験を月に何本回せていますか? 仮説を立てて、実装し、実験し、結果を整理し、次を考える。 1サイクル回すだけでも、相応の負荷がかかります。片手間でサイクル数を増やすのは簡単ではありません。 しかし、もし「仮説を立てる」から「結果を整理する」までを AI が担えるとしたら? 実際に AI の案から改善が生まれています。しかも、人間が担うのは方針の選択、コードレビュー、実験の実行に絞れています。 では、実際にどれだけ回せて、どれだけ当たるのか? 人間が思いつかない切り口は出てくるのか? 私たちはそれを確かめるために、Claude Code の Skill 機能を使った半自動の実験プロセスを組み、実際に回してみましたので、紹介したいと思います。 先に結論 AI が出した改善案は 13件で、そのうち実験まで進めたのは 8件でした。 改善が確認できたのは 3件、横ばいが 2件、下落が 3件です。 打率だけを見ると突出して高いわけではありません。 ただ、人間が手を動かしたのは方針の選択、コードレビューと実験の実行だけで、それ以外は AI が担っています。通常業務と並行しながら、このサイクル数を回せたこと自体が、この取り組みのいちばんの成果でした。 モデル改善は 1回ごとの改善幅だけでなく、試行回数を増やせるかどうかが効いてくる領域です。 片手間で回せる仕組みがあれば、改善の累積速度が変わります。 解決したかった課題 AI に推薦モデルの改善案を聞くだけなら、チャットでもできます。 しかし、それを継続的な実験プロセスとして回そうとすると、運用上のボトルネックがいくつか出てきます。 長期記憶がない セッションが切れるたびに AI は過去の実験を忘れます。 同じ失敗を繰り返すリスクがあるだけでなく、過去の失敗を踏まえて次の方向を絞る、改善が出た方向を深掘りするといった、蓄積を活かした提案ができません。 コンテキストの無駄遣い 毎回生のログや大量のファイルを読ませると、トークンを消費するだけで、期待したほど精度も上がりません。 必要な情報を構造化して渡す仕組みがないと、コストだけが膨らみます。 これらを解決するために、Claude Code の Skill 機能を使って半自動の実験プロセスを組みました。 Skill と記憶の設計 このプロセスには 2種類の記憶があります。 knowledge(長期記憶) 過去の実験記録を構造化した Markdown ファイルとして蓄積するフォルダです。 各 Skill はここを読み、過去の試行を把握した状態から動きます。 実験結果はサマリとして圧縮されて書き戻されるので、サイクルを重ねてもトークン消費が膨らみにくい設計です。 scratch(作業記憶) サイクル途中の方針メモや実装の下書きなど、一時的な情報を置く場所です。 長期記憶に残すほどではないが、セッション内では参照したい情報がここに入ります。 また、AI のコンテキストウィンドウが圧縮された場合でも、ファイルとして残っていれば再読み込みで復元できるため、意図しないコンテキスト消失への備えにもなっています。 Skill Skill 自体には、参照すべきファイルパスやテーブル定義、コーディングルールに加えて、実験コストの前提、安全性チェックの観点、実装原則、過去実験との重複を避けるための自問自答リストなどを埋め込んでいます。 これにより、毎回ゼロから指示しなくても、各フェーズで必要な文脈と制約が揃った状態で AI が動けます。 また、長期記憶と作業記憶はリポジトリ上に存在するため、Cursor など別の AI ツールからも同じ情報を参照でき、Claude Code の提案を独立に検証することも可能です。 半自動実験プロセスの仕組み このプロセスは、上記の Skill と記憶の仕組みを使って構築しています。 1サイクルの流れ AI がテーブル定義やコーディングルールを確認する AI が過去の実験記録を読み、現状を把握する AI が次に試す改善案を複数提案する 人間が方針を選ぶ AI が実装する AI が変更の安全性を確認する AI が実施予定の内容を記録する 人間が実験を実行する AI が結果を記録に反映する 人間が手を動かすのは、方針の選択、コードレビュー、実験の実行だけです。 定義の確認、過去実験の整理、提案、実装、安全性チェック、記録は主に AI が担います。 実験結果 個別の実験内容の詳細は割愛し、ここでは改善幅の傾向のみを共有します。 13件の提案のうち、実験に進めなかった 5件を除いた 8件の結果です。 実験 主要な機械学習指標の改善幅(複数指標の範囲) A +2〜+7% B +0.5〜+3% C +1〜+3% D ±2%以内 E ±2%以内 F -3〜-7% G -3〜-6% H -35〜-20% 学び 以下はあくまで運用を通じた感想であり、厳密に検証された結論ではありません。 提案の方向性 変更が小さくなりがちな傾向がある。 AI に自走させると、実験結果の正確性を担保しやすい方向、つまり対照実験がしやすい最小限の変更に寄りやすい傾向がありました。 指示を入れると質が変わる。 「小さい改善ではなく構造ごと変える改善を考えてほしい」と明示的に伝えたところ、論文の知識を参照した鋭い提案が複数出てきました。AI の提案の質は、渡す制約や方向付けに強く依存します。 既存手法の非自明な応用が出てくる。 たとえば、DIN(Deep Interest Network)の target-aware attention を two-tower モデルに持ち込む提案がありました。two-tower では推論時に候補アイテムが不明なためそのまま適用できませんが、AI は「学習時だけ正例を attention query として使い、推論時はフォールバックする」という変形を考えました。この切り口自体、推薦チーム内では出ていなかったもので私たちには非自明でした。当然、学習と推論の不一致(train-serve skew)がリスクになりますが、提案自体にそのリスクと失敗した場合に何がわかるかが含まれていました。成功の保証はなく、失敗する可能性は高そうですが、失敗しても学びが得られる実験設計になっています。また、仮にこの方式がそのまま機能しなくても、事前学習フェーズでのみ target-aware に学習させるといった派生が考えられ、アイデアの種として意味のある提案でした。 壁打ち相手としては十分実用的だった。 厳密な比較をしたわけではありませんが、少なくとも今回の運用では、AI が出す提案は人間の壁打ち相手として十分実用的だと感じました。場面によっては、自分たちだけではすぐに出なかった切り口が出てくることもありました。 蓄積と学習 最も鋭い提案は最後に出てきた。 偶然の可能性はありますが、サイクルを重ねて過去実験の蓄積が増えたタイミングで、最も構造的な提案が出ています。蓄積が提案の質に寄与している可能性は否定できません。 過去の失敗を踏まえた推論が出てくる。 AI が提案を出す際に、「過去にこの実験は失敗したので、こういう可能性がある。だからこちらの方向を試してみましょう」といった推論のログを出してくることがよくありました。蓄積された記憶を参照しながら提案理由を組み立てている様子が見て取れます。 運用コスト 人間の作業時間の大半はバグ対応。 実験が問題なく動く回では、人間の仕事は方針を選び、コードをレビューし、実験を実行することに絞られます。一方でバグが出ると調査・修正・再実行に手を取られ、体感で人間の作業時間の 8〜9割はバグ起因でした。逆にいえば、バグが出た回を無理に立て直さず次の実験に進めば、手数自体はさらに増やせる可能性があります。実装にバグが出た実験案も、提案自体は knowledge に記録しておけば、AI のコーディング能力が向上した時点で低コストに再挑戦できます。 現時点でまだ分かっていないこと サンプルサイズが不足している。 この半自動改善プロセスを運用し始めたのは最近であり、実験数は 8 件です。ここから得られた傾向が一般化できるかは、まだわかりません。 長期記憶の効果は未検証。 長期記憶なしのフローと比較した実験は行っていません。蓄積が提案の質に寄与している可能性は示唆されますが、長期記憶が本当に効いているのか、それとも同じ品質の提案が記憶なしでも出るのかは、現時点では検証できていません。 まとめ このプロセスの価値は、AI が良い改善案を出すことそのものではなく、試行の回転数を上げられることにあります。 13件の提案から 8件を実験し、3件の改善を得る。個々の実験の改善幅は小さくても、改善を積み重ねれば累積的な効果は大きくなります。 つまり、モデル改善は打率ではなく打席数が効いてくる可能性が高い。この取り組みの価値は、試行錯誤を片手間でも継続して回せるようになる点にあります。 長期記憶の効果や AI の提案精度については、まだ言い切れることは多くありません。 ただ、少なくとも「AI に改善案を出させて回す」というサイクル自体は、実用的に機能しています。今後はサンプルを増やしながら、このプロセス自体の改善も続けていきます。 こうした推薦改善の試行錯誤や、評価・運用の仕組みづくりに興味がある方は、ぜひ以下もご覧ください。 We’re hiring! 現在、タイミーではデータサイエンスやエンジニアリングの分野で、共に成長し、革新を推し進めてくれる新たなチームメンバーを積極的に探しています! データ | 採用情報 |株式会社タイミー また、気軽な雰囲気での カジュアル面談 も随時行っておりますので、ぜひお気軽にエントリーしてください。↓ hrmos.co hrmos.co hrmos.co Reference Skillを作成するにあたっては、Y Combinator の Garry Tan さんによる gstack リポジトリ(MITライセンス)を大いに参考にしました。 GitHub - garrytan/gstack: Use Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA · GitHub なお、本記事を書いている途中に、AI に継続的に作業させる方向の動きがいくつか出ていました。今回の取り組みと直接の関係はありませんが、同じ方向性の事例としてメモ的に置いておきます。 Automated Alignment Researchers: Using large language models to scale scalable oversight (Anthropic, 2026/04/14)― 9 体の Claude Opus 4.6 を自律的なアライメント研究者として走らせた実験。複数 AI に役割分担させて探索を回す、という方向の一例。 Introducing routines in Claude Code (Anthropic, 2026/04/14)― Claude Code にスケジュール実行や webhook トリガーで動かせる routines が追加。今回は Skill で手動起動していますが、こうした仕組みに載せれば定期的な提案・記録反映まで自動化できそうです。
はじめに こんにちは。タイミー プロダクトエンジニアの津守です。今年1月にタイミーに入社し、気づけば早3ヶ月が経ちました。 この記事では、入社して数ヶ月働いて感じた「AIツールがオンボーディングプロセスをどう変えたか」という体験をまとめます。技術的な深掘りというより、新しい環境に飛び込んだエンジニアの個人的な気づきとして読んでもらえると嬉しいです。 従来のオンボーディングプロセスのイメージ 新しい会社に入ったとき、多くのエンジニアは最初の数週間を ドキュメントを読む。コードベースを読む。アーキテクチャを把握する。チームの開発スタイルを理解する。そして、ある程度理解できたと感じてから、ようやく手を動かし始める。 というような順序で過ごしてきたと思います。 言ってみれば「インプット先行」の学習スタイルです。この期間は"情報を受け取る期間"と暗黙的に認識されていることが多く、アウトプットは後回しになりがちでした。チームへの貢献よりも、まず「追いつくこと」が優先されるイメージです。 AI活用によって感じた変化 入社して最も強く感じたのは、この「順序」が変わったということです。 AIツールを使うことで、コードベースへの理解が浅い段階でも、まず動くものを作ることが現実的になりました。実際Cursor や Claude Code を主体に実装を進めると、知識のギャップをAIがある程度埋めてくれます。おかげで、入社初期からチームメンバーやステークホルダーのレビューやフィードバックを素早く受け取ることができました。 実際、チームメンバーやステークホルダーからのフィードバックやレビューには、基礎的な知識だけでなく、「タイミーではこうやってる」といった暗黙のコンテキストが含まれており、ドキュメントから得られる知識に加えて、より多くの背景情報を得られる場面があります。 開発アプローチとして広く受け入れられている アジャイル では、「いかに早くステークホルダーがレビューできる状態を作るか」が重要な論点のひとつです。これは、個人が環境に適応するうえでも重要な要件だと思います。完璧な理解を待ってから動くのではなく、まず動くものを見せてフィードバックをもらう。そのサイクルを素早く回すことが、結果的に最も効率的な学習を生み出し、属する組織で求められる行動を起こせるようになるために重要と考えています。 適切に学習サイクルが回るための条件 ただ、このアウトプット先行の学習サイクルが機能するには、2つの環境が整っている必要があると感じました。どちらもAIツールの登場で新たに生まれた課題ではなく、組織の開発体験として元々重要だったものですが、AIツールの発達によってその重要性はさらに増しています。 1. AIが適切なアウトプットを出せる環境 AIが出すアウトプットの質は与えるコンテキストに大きく左右されます。学習サイクルの中でフィードバックを通じて暗黙のコンテキストを受け取れるとはいえ、そもそも明示的に言語化されているほうが望ましく、AIのアウトプットの精度も上がります。 実際、タイミーではバックエンド開発Handbookを通じて、開発プロセスを明示的に言語化する動きがあります。設計・実装・運用にまたがるガイドラインが体系的にまとめられており、さらにそれがAIエージェントのスキルとして提供されています(詳しくは新谷さんの記事「 バックエンド開発Handbookを届けるために ― AI時代の知の高速道路を敷く 」をご覧ください)。 Handbookが生まれた背景のひとつには、メンバーの増加やAIツールの進化により、バックエンド以外のエンジニアが越境してコードを書く機会が増えたという事情があります。ただ実際に使ってみて感じたのは、暗黙知をまだ何も持っていない入社直後のメンバーにこそ、そのインパクトが大きいということです。「そもそも何を知らないかもわからない」状態でも、AIがHandbookに沿ったアウトプットを出してくれることは、単なる品質担保以上の意味を持ちます。 自分がHandbookを意識していなくても、AIがガイドラインに沿った設計や実装を提案してくれる——「気づいたらタイミー流の書き方になっていた」という感覚は、入社して間もない時期にとても心強いものでした。 2. フィードバックをもらえる環境・体制 もうひとつの前提条件は、フィードバックの環境です。どんなに早くアウトプットを出しても、適切なフィードバックが返ってこなければ学習サイクルは止まります。 この3ヶ月間は、チームメンバーやステークホルダーから丁寧なフィードバックをもらえる環境にあり、そのおかげで学習が加速しました。特に、PRベースのコードレビューは厳密に行われており、そこでの指摘やディスカッションがとても多くの学びになりました。 ただ、3ヶ月を通じて感じたのは、現状ではフィードバックの質が運用や状況によってばらつきが出やすい状態にあるということです。体制として担保されているというよりは、チームメンバーの意識や余裕に左右される面があり、持続的に機能させるには仕組みとしての設計が必要だと感じています。 もうひとつ、AIがアウトプットを加速させることで生まれるトレードオフも見えてきました。学ぶ側が早く動けるようになった分、レビューする側が見るべきPRの量も増えます。学習サイクルが速くなった恩恵が、レビュワーの負荷という形で偏在してしまうわけです。 また、PRベースのレビューは厳密に行われている一方で、暗黙知やコンテキストを深く共有したうえでのフィードバックを、どう生み出すかという問いも残ります。これらに対してモブプログラミングやペアプログラミングのような形式は有効な解のひとつだと思っていて、後からまとめてレビューするコストを分散させながら、よりリッチなフィードバックを生みやすい構造だと感じています。 持続的に相互フィードバックが行われる開発体制をどう設計するか——これはまだ答えの出ていない問いですが、AIが学習サイクルを高速化させるほど、フィードバックを循環させる体制の重要性は増していくと感じています。 おわりに 3ヶ月を振り返ると、AIは入社直後の学習の「量」を変えたのではなく、「順序」を変えた、というのが一番しっくりくる表現です。 以前なら「理解してから作る」だったものが、「作りながら理解する」に変わりました。この変化は、入社直後という時期をより能動的に過ごす後押しをしてくれると感じています。 ただし、そのサイクルを本当に機能させるには、AIが適切なアウトプットを出せる環境と、フィードバックを届ける体制という、人間側の設計が不可欠だと感じました。Handbookをはじめとした環境を整えてくれたチームには、改めて感謝しています。 同じように新しい環境に飛び込んでいる方や、新しいメンバーを迎える立場の方に、少しでも参考になれば嬉しいです。
こんにちは。タイミーでプロダクトエンジニアをしている福島(taishi)と大竹(otake)です。 EMConf JP 2026が3月4日に開催されました。 2026.emconf.jp タイミーは今年、EMConf JP 2026のスポンサーをさせていただきました。 タイミーには、世界中で開催されているすべての技術カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。今回はこの制度を使って参加しました。 詳細は以下をご覧ください。 productpr.timee.co.jp EMConf はエンジニアリングマネージャー(EM)向けのカンファレンスですが、私たちのようなマネジメントをしていないメンバーにとっても学びの多いイベントでした。 本記事では、印象に残ったセッションをいくつかピックアップしてご紹介します。 冒険する組織のつくりかた 著書「冒険する組織のつくりかた」を執筆された、株式会社MIMIGURIの安斎勇樹さんによるセッション。メンバーの興味傾向を把握し、目標と個人の動機を接続する場をデザインするための具体的なアプローチや、思考のフレームワークが紹介されていました。 目標のマネジメント:SMARTからALIVEへ 目標設定において、管理側の論理である「SMART」と、取り組む側の視点である「ALIVE」を両立させることが重要です。 SMARTの法則 : 業務を精緻に遂行させるための指標(Specific, Measurable, Achievable, Relevant, Time-bound) ALIVEの法則 : メンバーが前向きな意味を感じられる指標 Adaptive(適応) : 変化に適応し、将来役立つ能力を身につけている安心感 Learningful(学習) : 学びの機会となる Interesting(興味) : 好奇心をそそる Visionary(未来) : 未来を見据える Experimental(実験) : 実験的な試みである さらに重要なのは、目標設定の「前」と「後」のプロセスです。目標を単に提示するのではなく、メンバーの内発的動機と目標を「ミート」させることが肝心です。 設定するまで : ヒアリングで意見を踏まえ、参加型デザインで一緒に考える 設定したあと : リーダーがストーリーテリングで意図を語り、ダイアログ(対話)で取り組む意味を共有する 興味のマネジメント:8つの「活動スタイル」 個人の「興味のツボ」を把握することで、目標にALIVEな要素を組み込めます。興味は「ヒト」か「コト」か、そして「どのレンズ(役割)で見るか」の組み合わせで8タイプに分類されます。 興味のレンズ ヒトに興味がある コトに興味がある 創造 新しいコミュニティやカルチャーを生み出したい 新しいプロダクトやビジネスモデルを創出したい 解明 人間の心理や集団の力学を明らかにしたい 現象のデータを分析し法則性を明らかにしたい 介入 人やチームに寄り添い、変化や成長を支援したい 現場の課題に働きかけ、状況を改善・解決したい 運用 秩序や制度を維持し、公平に運営したい 手続きを正しく回し、品質を保ちたい コメント これまで「SMARTの法則」に則った目標設定は意識していましたが、それは管理する側の視点でした。そのため、取り組む側のモチベーションの観点が不足しうる、という指摘が興味深かったです。SMARTとALIVE両方の観点を取り入れることで、メンバーが主体性をもって取り組めるようになり、結果的に目標達成の確度が上がります。生成AIの急速な進歩で目まぐるしく変化する世の中だからこそ、モチベーションを高く保ち、腰を据えて取り組める目標設定のアプローチとして、私も心がけてみようと思いました。(taishi) 数年後のキャリアプランを立てることに難しさを感じていましたが、「今この瞬間の興味(好奇心)」を起点にするというアプローチは非常に腹落ちしました。技術トレンドの移り変わりが激しいからこそ、無理に未来を固定するのではなく、自分の「好き」や「気になる」を組織の課題と接続し、適応しながら進んでいけるよう、前向きな気持ちで業務に取り組んでいきたいと感じました。(otake) 「ストレッチゾーンに挑戦し続ける」ことって難しくないですか? speakerdeck.com 成長には、現状のスキルで難なくこなせるコンフォートゾーンを抜け出し、適度な負荷がかかるストレッチゾーンに身を置き続けることが不可欠です。しかし、実際には「今の自分に最適な挑戦が不明」「日々の忙しさによる自然消滅」「外部要因による阻害」といった要因で、ストレッチゾーンに居続けることが難しい場合があります。本セッションでは、これらを克服するための環境設計が示されました。 現状分析 : Will/Can/Mustのフレームワークに加え、具体的な事象を問い(Why)で抽象化し、新たなアクション(How)に繋げる「具体と抽象の往復」が重要 目標設定 : コンフォートゾーンの誘惑を断ち切るための武器として、具体的で測定可能な「SMART」だけでなく、チームで共有・可視化され野心的な「FAST」な目標設定を活用する 仕組み化 : マネージャーの支援前提ではなく、権限委譲やシステム思考(因果ループ図など)を用いて、組織として挑戦が推奨される構造を作る コメント 特に印象的だったのは、スナッキング(簡単で達成感はあるが学びが少ない仕事)の誘惑という概念です。忙しいときほど慣れた仕事に逃げてしまいがちですが、それを防ぐために自らFASTな目標を掲げ、周囲に宣言することで、意識的にストレッチゾーンに身を置く工夫を取り入れたいと感じました。 また、SMARTな目標は達成までの道のりが具体的にイメージできる反面、大きな成長につながる目標が生まれにくい側面もあります。これまでチームで目標を共有しても、協力体制が生まれたり切磋琢磨する状態になったりしにくかったため、その点でもFASTな目標設定を取り入れてみたいと思いました。(otake) 「事業目線」の正体 〜3つのフェーズのCTO経験から見えてきた、EMが持つべき視点 speakerdeck.com EMが持つべき「事業目線」を、3つのステップで具体化したセッションです。 Lv.1 数字を知る : 自組織に関わる数字(売上、MAU等)を把握し、事業予算の構造を因数分解して、自分のエンジニアリング組織がどこに作用しているかを理解する Lv.2 お客さまと隣接組織を知る : 数字の裏にある「なぜそうなっているか」を知るために、お客さまの声(生の声)を聞き、経理・営業・CSといった他部門の力学(大切にしていること)を理解する Lv.3 戦略に反映する : 得られた知見をエンジニアリング戦略や組織の仕組み(ダッシュボード化や研修等)に落とし込み、「明日」の大きな問題を解決するために「今日」何をすべきかを決める。 コメント 事業目線という言葉の解像度が劇的に上がったセッションでした。特に隣接組織の構造を知ることで、自分の開発が他部署のオペレーションにどう影響するかまで想像を膨らませるという視点は、シニアなエンジニアを目指す上で欠かせないものだと痛感しました。また、何のために開発しているのかを改めて考えてみて、価値を最大化できるように日々の行動を変えてみたいと思いました。まずは自分のチームに関わる数字を言えるようにするという小さな一歩から始めてみます(otake) 技術的負債の泥沼から組織を救う3つの転換点 speakerdeck.com 著書「アーキテクチャモダナイゼーション 組織とビジネスの未来を設計する」の翻訳を担当された、株式会社スリーシェイクのnwiizoさんによるセッション。技術的負債は「技術」の問題ではなく、組織構造やプロセスの問題が技術的な問題として表出したものだと捉え、モダナイゼーションを推進する手法が紹介されていました。最初に組織に学習する構造を持たせ(転換点1)、次にどこに集中投資するかを意思決定者の言語で語り(転換点2)、最後に不確実性を受け入れつつ小さく始め、学習するサイクルを作る(転換点3)。そのための手法や考え方について詳細に解説いただきました。 転換点1:学ぶ力(組織に学習する構造を持たせる) 組織が自律的にシステムとビジネスの構造を理解し、学び続ける状態を作る段階です。AMET(Architecture Modernization Enabling Team)を触媒として、イベントストーミングやワードリーマッピングなどの手法を活用しつつ、チームが自律的に学習を続けられるようになるまで支援します。 転換点2:語る力(意思決定者の言語で投資判断を促す) 技術的な課題を「コードが汚い」といった技術者の言葉ではなく、経営や事業の成長を阻害する「ビジネスリスク」として翻訳する段階です。Core Domain Chartで自社のドメインを「差別化度」と「複雑性」の2軸で整理した上で、それを事業の選択肢の制約として提示することで、経営層が自分の判断軸で技術投資を評価できる構造を作ることが重要です。 転換点3:始める力(不確実性を受け入れ、小さくサイクルを回す) 成果を出しながら段階的に変革を進める段階です。1つのバリューストリームで小さく始め、3〜6ヶ月以内にモダナイゼーションの第一歩となる成果を出すことを目指します。逆コンウェイ戦略に基づき、理想のアーキテクチャに合わせて組織構造も同時にデザインします。 コメント モダナイゼーションにおける具体的なステップと実践的な手法が紹介されており、とても良質なセッションでした。変革の推進力を高めるだけでなく、変化を阻む摩擦を取り除く重要性とアプローチについても語られており、現場目線で参考になりました。「モダナイゼーションで最も難しいのは着手すること。2番目に難しいのは勢いを維持すること」という言葉も印象に残りました。チームがいかに強い意志をもって持続可能な変化を続けられるかが最重要だと感じています。著書「アーキテクチャモダナイゼーション 組織とビジネスの未来を設計する」もぜひ読んでみたいです。(taishi) まとめ 今回のEMConf JP 2026では、エンジニアリング組織における幅広いテーマが取り上げられていました。 メンバーの視点でも、今この瞬間の興味を起点にした成長戦略、ストレッチゾーンに身を置くための仕組みづくり、事業の数字や隣接組織への解像度を上げることなど、自身の成長とチームへの貢献を加速させるヒントが詰まっていました。 EMConfという名前ではありますが、エンジニアリングに関わるすべての人にとって価値のあるイベントだと感じています。 得られた知見を日々の業務に活かして、個人と組織の両方で成長できるように努力したいと思います!