Timee Product Team Blog

タイミー開発者ブログ

複数リポジトリのTerraformワークフローを共通基盤化して運用をスケールさせた話

はじめに

こんにちは、タイミーでPlatform Engineerをしている近藤です。

タイミーでは、インフラ管理においてTerraformを積極的に活用しています。 当初はAWSリソースの管理が中心でしたが、事業や組織の拡大に伴い、管理対象は多岐にわたるようになりました。

現在では、以下のような様々な用途でTerraformリポジトリが運用されています。

  • AWSインフラ: メインとなるサービス基盤の構築・運用
  • GCPインフラ: BigQueryなどのデータ分析基盤や、特定のGoogle Cloudリソース管理
  • SaaSアカウント管理: GitHubやDatadogなどのユーザー・権限管理
  • その他: Elastic Cloudなどの専用リソース管理

このように、クラウドプロバイダーも用途も異なる複数のリポジトリが存在する状況において、私たちは「Terraformワークフローの共通化」に取り組みました。

共通化前の課題

リポジトリが増えるにつれて、開発・運用チームは次のような課題に直面していました。

  1. ワークフローの実装と機能のバラつき リポジトリごとにCI/CDの設定がコピペと独自改変を繰り返して作られていたため、「あるリポジトリではLintが走るが、別のリポジトリでは走らない」「あるリポジトリではSlack通知が飛ぶが、別のリポジトリでは通知自体がない」といった不整合が起きていました。
  2. メンテナンスコストの増大 新しいセキュリティチェックツールの導入や、Terraformのバージョンアップに伴うCI修正を行おうとすると、全てのリポジトリに対して個別に修正PRを送る必要がありました。
  3. 新規リポジトリ作成への心理的ハードル 「リポジトリを分けたほうが責務が明確になる」と分かっていても、「またあのCI設定をコピーしてメンテナンスするのか」という負担が頭をよぎり、既存のリポジトリに無理やりリソースを追加してしまうケースがありました。

結果として、ワークフローの維持管理が大変そうで、新しい用途での活用を躊躇してしまうという状態になっていました。

解決策:共通Terraformワークフロー基盤の開発

これらの課題を解決するために、社内のTerraformリポジトリで利用するためのCI/CDワークフローを共通化し、単一のプロダクトとして管理する共通Terraformワークフロー基盤を開発しました。

安全かつ自動化された配布の仕組み

共通化にあたっては、変更が全リポジトリに波及するリスクを考慮し、安全に開発・配布できる仕組みを構築しました。

  1. サンドボックス用リポジトリでの開発 まず、Terraform用の実験リポジトリで機能追加や修正を行います。ここで実際にTerraformを動かし、動作を検証します。
  2. 共通ワークフロー基盤リポジトリへの自動PR サンドボックスでの検証が完了して main ブランチにマージされると、自動的に共通Terraformワークフロー基盤リポジトリに対してPull Requestが作成されます。
  3. 各リポジトリへの自動配布 共通Terraformワークフロー基盤側でPRをマージすると、その変更が対象となる全ての実利用リポジトリ(AWS用、GCP用、SaaS用など)に対して自動的に配布されます。

このサイクルにより、開発者は安心して共通基盤を改善でき、利用者も常に検証済みの最新ワークフローを享受できるようになりました。

同期ワークフローの実装

上記の配布フローは、GitHub Actions と GitHub App を使った同期ワークフローによって実現されています。具体的な実装を見ていきましょう。

同期ワークフローの全体像

name: Sync Workflow

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  # 配布先リポジトリ一覧を設定ファイルから読み込み
  load_repositories:
    outputs:
      repositories: ${{ steps.load.outputs.repositories }}
    steps:
      - uses: actions/checkout@v4
      - id: load
        run: |
          REPOS=$(yq -o=json '.repositories' sync-repositories.yaml | jq -c)
          echo "repositories=$REPOS" >> "$GITHUB_OUTPUT"

  # 各リポジトリに対してマトリクス実行で同期
  sync_workflow:
    needs: load_repositories
    strategy:
      matrix:
        include: ${{ fromJson(needs.load_repositories.outputs.repositories) }}
    steps:
      # GitHub Appで配布先リポジトリ用のトークンを発行
      - uses: actions/create-github-app-token@v2
        id: app-token
        with:
          app-id: ${{ vars.BOT_APP_ID }}
          private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
          owner: ${{ matrix.owner }}
          repositories: ${{ matrix.name }}

      # ソースと配布先の両方をチェックアウト
      - uses: actions/checkout@v4
        with:
          repository: ${{ matrix.owner }}/${{ matrix.name }}
          token: ${{ steps.app-token.outputs.token }}
          path: target-repo

      # sync-files.yaml に基づいてファイルをコピー
      - name: Copy workflow files
        run: .github/scripts/sync-copy-files.sh

      # 差分があればPRを作成
      - name: Create PR if changed
        run: .github/scripts/sync-create-pr.sh
        env:
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
  

配布先リポジトリの設定(sync-repositories.yaml)

配布先となるリポジトリは、設定ファイルで管理します。共通基盤リポジトリでは、実利用リポジトリすべてを配布先として定義しています。

# 共通基盤リポジトリの sync-repositories.yaml
repositories:
  - owner: your-org
    name: repo-a  # 利用リポジトリA
  - owner: your-org
    name: repo-b  # 利用リポジトリB
  - owner: your-org
    name: repo-c  # 利用リポジトリC
  # ... 他の利用リポジトリ

一方、サンドボックスリポジトリでは、共通基盤リポジトリのみを配布先として定義します。

# サンドボックスの sync-repositories.yaml
repositories:
  - owner: your-org
    name: workflow-base  # 共通基盤リポジトリのみ

同期対象ファイルの設定(sync-files.yaml)

同期するファイルと除外するファイルは、設定ファイルで明確に管理します。

# .github/config/tf/sync-files.yaml
# 同期対象のファイル・ディレクトリ
files:
  - .github/config/tf/        # 共通設定ファイル
  - .github/workflows/        # ワークフローファイル
  - .github/actions/tf_*      # カスタムアクション
# 除外対象(リポジトリ固有の設定)
exclude:
  - .github/workflows/tf_sync_workflow.yml  # 同期WF自体
  - .github/config/tf/sync-files.yaml       # 同期設定
  - .github/config/tf/sync-repositories.yaml
  - .github/config/tf/.env.local            # ローカル環境変数
  - .github/config/tf/dirs.json             # 対象ディレクトリ一覧
  - .github/config/tf/filters.local.yaml    # ローカルフィルタ
  - .github/config/tf/.tflint.hcl           # tflint設定
  - .github/config/tf/trivy.yaml            # trivy設定

このように、共通化すべきファイルと、リポジトリ固有であるべきファイルを明確に分離することで、柔軟性を保ちながら一貫性を維持しています。

設定ファイルによる振る舞いの制御

共通ワークフローの振る舞いは、階層化された設定ファイルによって柔軟に制御できます。

設定ファイルの階層構造

.github/config/tf/
├── .env                 # グローバル設定(共通)
├── .env.local           # リポジトリ固有の設定
├── dirs.json            # 対象ディレクトリ一覧
├── filters.yaml         # グローバル変更検出フィルタ
├── filters.local.yaml   # ローカル変更検出フィルタ
├── .tflint.hcl          # tflint設定
└── trivy.yaml           # trivy設定
envs/
├── production/
│   └── .env             # ディレクトリ固有の設定
└── staging/
    └── .env

グローバル設定(.env)

全リポジトリで共通の設定です。同期によって配布されます。主な設定項目は以下の通りです。

  • Terraform実行設定: 並列数(TF_MAX_PARALLEL_JOBS)、CLI引数(TF_CLI_ARGS_plan)など
  • Git設定: コミット時のユーザー名・メールアドレス
  • Slack通知設定: ステータスごとの色・メッセージ・絵文字・アイコンを SLACK_STATUS_{STATUS}_{PROPERTY} 形式で定義

Slack通知の設定はデータとして定義し、ワークフロー側ではシェルの間接変数展開を使ってジョブのステータス(success / failure / cancelled / maintenance)に応じた値を動的に参照する設計にしています。

リポジトリ固有・ディレクトリ固有の設定

グローバル設定を上書きする設定は、以下の2箇所で定義できます。

  • .env.local: リポジトリ全体に適用される設定(同期の除外対象)
  • envs/{env}/.env: 特定のディレクトリにのみ適用される設定

主な設定項目:

  • TF_MAX_PARALLEL_JOBS: APIレート制限を考慮した並列数の上書き
  • AWS_ROLE_TO_ASSUME / GCP_WORKLOAD_IDENTITY_PROVIDER: 認証設定
  • SLACK_CHANNEL_ID / SLACK_MENTION_NAME: 通知先チャンネルとメンション

対象ディレクトリ設定(dirs.json)

terraform plan/apply の対象ディレクトリを定義します。

[
  "envs/production",
  "envs/staging",
  "envs/development"
]

変更検出フィルタ(filters.yaml)

共通モジュールや設定ファイルの変更時に全ディレクトリを再実行するためのグローバルフィルタです。

# .github/config/tf/filters.yaml(共通)
terraform_trigger:
  - modules/
  - .github/config/tf/
  - .github/actions/tf_*/**
  - .github/workflows/tf_*
  - .github/workflows/*tf**
  - .terraform-version
# .github/config/tf/filters.local.yaml(リポジトリ固有)
terraform_trigger:
  - shared/  # リポジトリ固有の共有モジュール

認証処理の共通化(tf_init アクション)

認証処理はComposite Actionとして共通化し、AWS OIDC / GCP Workload Identity Federation の両方に対応しています。

# .github/actions/tf_init/action.yml
name: Terraform Init
inputs:
  directory:
    required: true
  credentials:
    required: false

runs:
  using: composite
  steps:
    # リポジトリ固有のパッチがあれば適用
    - name: Check if tf_patch exists
      id: check-patch
      shell: bash
      run: |
        if [ -d ".github/actions/tf_patch" ]; then
          echo "exists=true" >> "$GITHUB_OUTPUT"
        fi

    - name: Apply patches
      if: steps.check-patch.outputs.exists == 'true'
      uses: ./.github/actions/tf_patch
      with:
        directory: ${{ inputs.directory }}

    # Terraformバージョンのセットアップ
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3
      with:
        terraform_wrapper: false
        terraform_version: ${{ steps.var.outputs.terraform-version }}

    # 環境変数の読み込み(グローバル → ローカル → ディレクトリ固有)
    - name: Load environment variables
      shell: bash
      run: |
        for env_file in \
          ".github/config/tf/.env" \
          ".github/config/tf/.env.local" \
          "${{ inputs.directory }}/.env"; do
          if [ -f "$env_file" ]; then
            envsubst < "$env_file" >> "$GITHUB_ENV"
          fi
        done

    # AWS OIDC認証(設定がある場合)
    - name: Configure AWS credentials
      if: env.AWS_ROLE_TO_ASSUME != ''
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-region: ${{ env.AWS_REGION }}
        role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }}

    # GCP Workload Identity認証(設定がある場合)
    - name: Authenticate to Google Cloud
      if: env.GCP_WORKLOAD_IDENTITY_PROVIDER != ''
      uses: google-github-actions/auth@v2
      with:
        workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
        service_account: ${{ env.GCP_SERVICE_ACCOUNT }}

    # Terraform init
    - name: Terraform init
      shell: bash
      run: |
        terraform -chdir=${{ inputs.directory }} init

導入成果

運用面での改善

共通基盤を1箇所修正するだけで、全リポジトリに改善が行き渡るようになりました。セキュリティスキャンの追加やツールのバージョンアップも、一括で適用可能です。

また、どのリポジトリを触っても、「PRを出せばPlanが走り、結果がコメントされる」「マージすればApplyされる」という同じ挙動が保証されるようになりました。これにより、エンジニアが新しいリポジトリを触る際の学習コストが下がりました。

技術的に実現できたこと

共通Terraformワークフロー基盤では、インフラ運用に必要な仕組みを次のような形でまとめて提供しています。

  • Terraform実行パイプラインの共通化: PRごとに terraform plan、mainマージ時に terraform apply を自動実行し、複数環境をマトリクス実行しながら並列数を制御。
  • 実行の安全性とレートリミット対策: ワークフローのコンカレンシーやキャンセルポリシーを設定し、不要な再実行を避けつつ、GitHub API やクラウドプロバイダAPIのレートリミットにかからないように実行を制御。
  • 品質・セキュリティチェックの標準化: terraform fmt / validate / tflint / trivy に加え、各種Lintを一括で走らせ、結果をPRレビューとしてフィードバック。
  • ドリフト検出と自動PR: 定期的なPlanで実環境とのドリフトを検出し、差分があれば自動でPRを作成・管理。
  • PRレビュー体験の強化: Plan/Apply結果や診断結果に加え、AIベースのPRエージェントによる要約・レビューコメント生成でレビュー負荷を軽減。
  • 認証と権限管理の一貫性: AWS OIDC / GCP Workload Identity Federation とアクセスキー認証の両方に対応し、plan / apply ごとに適切な認証情報を選択できるよう統一。

設計時に意識したこと

Terraformワークフローを「単なるCI設定」ではなく、継続的に拡張・改善していける共通基盤にするために、次のような設計方針と実装上の工夫を取り入れています。

1つのソースから複数リポジトリへ安全に配布できること

共通ワークフローの更新がそのまま全リポジトリに波及するため、必ずサンドボックスで検証してから共通基盤に取り込む二段階フローにしています。その上で、GitHub App を使ったトークン発行と同期専用ワークフローにより、「人手でコピペせずに、レビュー済みの変更だけを各リポジトリへ配布する」という流れを自動化しました。

配布方法としては、reusable workflow を使って直接呼び出す案も検討しました。しかし、ユースケースが社内限定であること、配布時の差分をPRとして確認できたほうがレビューしやすいこと、呼び出し元ワークフロー側の設定も含めて将来的に変更する可能性があることから、GitHub App で各リポジトリにPRを作成する方式を選びました。

設定ファイルで振る舞いを切り替えられること

Terraform の実行対象やトリガーとなるファイルパターンなどは、ワークフロー内にベタ書きせず、グローバル設定とリポジトリごとの設定ファイルをマージして解決する構造にしています。これにより、「共通のベースラインは維持しつつ、プロジェクトごとに監視対象や実行頻度だけ変える」といった調整をコード変更なしで行えるようにしました。

認証・権限まわりをワークフロー側で肩代わりすること

AWS OIDC / GCP Workload Identity Federation によるキーレス認証と、従来のアクセスキー認証の両方をラップし、plan / apply などのアクションごとに適切な権限を選び分けられるようにしています。個々のリポジトリでは「どのロール/サービスアカウントを使うか」だけを意識すればよく、認証フローそのものの実装は共通基盤に閉じ込めています。

共通ワークフローと各リポジトリの実装を柔軟につなげられること

ワークフローの中で、Terraformファイルに対して小さなパッチを当てる専用アクションtf_patchを用意しています。

例えば、各リポジトリの providers.tf には、過去の経緯からローカル開発用のAWS認証設定(プロファイル指定やロール引き受け設定)が含まれています。CI環境ではOIDC認証を使用するためこれらの設定は不要であり、むしろ干渉してしまいます。tf_patch アクションは terraform initの前にこれらの行を自動的に削除し、どのリポジトリでも同じOIDC認証の前提で共通ワークフローを実行できるようにしています。

このように、共通ワークフローと各リポジトリ固有の実装の間をつなぐアダプタとして機能させることで、既存コードへの変更は最小限に抑えたまま共通化を進められるようにしています。

現状の課題

共通化によって多くのメリットを得られましたが、運用を続ける中でいくつかの改善点も見えてきました。

同期除外対象の管理

現在、同期対象から除外するファイルは sync-files.yaml にファイル名を個別で定義しています。しかし、除外対象が増えるにつれてこのリストの管理が煩雑になってきました。ディレクトリ構造を整理し、「共通化するもの」と「リポジトリ固有のもの」を明確にディレクトリで分離することで、除外設定をシンプルにできる余地があります。

ファイルの差分管理

同期ワークフローは「ソースリポジトリにあるファイルを配布先にコピーする」という動作をしますが、ソースリポジトリでファイルを削除・移動した場合に、配布先の古いファイルを自動で削除するような仕組みにはなっていません。そのため、ファイル構成を変更した際には、配布先リポジトリに不要なファイルが残ってしまうことがあります。この問題も、上述のディレクトリ構造の見直しによって対応できると考えています。現状では、AIコーディングエージェントの Devin を活用して、不要になったファイルのお掃除PRを作成してもらうなど、人力での対応を行っています。

おわりに

共通Terraformワークフロー基盤によって、インフラや各種 SaaS アカウント管理のワークフローは一貫性を持って運用できるようになり、個々のリポジトリごとに CI を実装・メンテナンスする負担は大きく減りました。そのうえで、開発者は「リポジトリごとの Terraform の中身」に集中しやすくなり、ワークフロー自体は共通基盤として継続的に改善していける状態になっています。

また、この記事で紹介したのと同じような考え方で、アプリケーションのモノレポに対するデプロイワークフローも共通化しています。こちらについても、機会があれば別のエントリとして詳しく紹介できればと思います。

Rails MVC を抽象化して捉える - 一貫性のあるクラス設計のために

こんにちは、タイミーでバックエンドのテックリードをしている新谷(@euglena1215)です。

Rails アプリケーションの開発をしていると、fat になってしまった Sidekiq worker や、ドメインロジックらしき実装が書かれている Serializer に遭遇したことがあるのではないでしょうか。

この記事では、MVC アーキテクチャを一段階抽象化して捉えることで、Rails のさまざまなレイヤーに対して一貫したプラクティスを適用する考え方を紹介します。ある程度 Rails を触ったことのあるエンジニアが感覚的に理解している概念を、改めて言語化したものです。

MVC の役割を改めて整理する

まず、MVC の各レイヤーの役割を整理しておきましょう。

  • Model: ビジネスロジック、データ、ルールそのものを表現し、データの保存・取得を行う
  • View: データをユーザーに返却するための適切な形で変換する
  • Controller: 外部からの入力を受け取り、Model を操作しデータの取得・更新を行い、View で変換したデータを外部へ出力する

ここで重要なのは、MVC を「Model, View, Controller という三つのディレクトリ」として捉えるのではなく、「三つの責務」として抽象的に捉えることです。

「実質〇〇」という視点

一番分かりやすい例は、JSON の組み立てを行う Serializer です。Serializer は app/serializers ディレクトリに配置されますが、その責務は「データを適切な形に変換してユーザーに返却する」ことであり、これは実質的に View です。

この「実質〇〇」という視点でタイミーの Rails アプリケーションの各レイヤーを分類してみましょう。

実質 Model

ディレクトリ 説明
app/models Model そのもの
app/policies 特に認可にフォーカスしたもの
app/validators 特にバリデーションにフォーカスしたもの

Policy や Validator は、ビジネスルールを表現するという点で、 Model の責務を担っています。

実質 Controller

ディレクトリ 説明
app/controllers Web API リクエストにおけるエントリポイント
app/workers 非同期処理におけるエントリポイント
app/mailers メール送信におけるエントリポイント
lib/tasks rake タスクにおけるエントリポイント
app/services エントリポイントから括り出された処理
app/forms エントリポイントから括り出された処理

ポイントは、Service*1 や Form*2 がエントリポイントではないものの、Controller に実装されるべき処理を括り出したものであるという点です。そのため、実質的には Controller と捉えるのが妥当です。

実質 View

ディレクトリ 説明
app/views HTML やメール文面の組み立て
app/serializers JSON の組み立て

Serializer を View と捉えることで、View に関するプラクティスを Serializer にも適用できるようになります。

この視点のメリット

「〇〇は実質的に Model / View / Controller である」という感覚を持つことで、MVC の一般的なプラクティスを MVC 以外のレイヤーにも適用できます。

例えば、以下のようなセルフチェックが可能になります。

一般的な MVC のプラクティス チェックリスト

  • 実質 Controller は十分に薄く、ドメインロジックや表示用ロジックが混ざっていないか?
  • ドメインロジックは実質 Model に寄せているか?
  • 表示のためのデータ加工は実質 View に寄せているか?
  • 実質 Model は実質 Controller に依存していないか?
  • 実質 Model は実質 View に依存していないか?

具体例

例えば、「Service クラスが肥大化している」という問題があったとします。

Service は実質 Controller なので、「Controller が肥大化している」と言い換えられます。Controller が肥大化する原因は、ドメインロジックや表示用ロジックが混入していることが多いです。

したがって、解決策は以下のようになります。

  • ドメインロジック → Model(または Policy, Validator)に移動
  • 表示用ロジック → View(または Serializer)に移動

このように、MVC のプラクティスを適用することで、自然と適切な設計に導かれます。

まとめ

  • MVC は「三つのディレクトリ」ではなく「三つの責務」として捉える
  • Rails の各レイヤーを「実質 Model / View / Controller」で分類することで、一般的な MVC プラクティスを適用できる
  • Service は実質 Controller、Serializer は実質 View、Policy は実質 Model
  • この視点を持つことで、一貫性のあるクラス設計が可能になる

日々のコードレビューや設計判断の際に、「これは実質どのレイヤーか?」と問いかけてみてください。

*1:タイミーでは、Service クラスをController や Sidekiq worker から呼ぶものと位置付け、Model からは呼び出されないようなルールにしています。詳細は https://tech.timee.co.jp/entry/2024/01/29/170000 をご覧ください。

*2:タイミーでは、Form クラスにおける明確なルールはないものの、特定のエンドポイントにおける処理を括り出したものという位置付けの使い方をしていることが多いため、こういった整理をしています。

データサイエンティストの最適配置を求めて:ストリームアラインドチームからコンプリケイティッド・サブシステムチームへの再配置

こんにちは、タイミーのデータサイエンスグループでマネージャーをしている菊地です。

本記事では、タイミーのデータサイエンス組織が直面した「認知負荷」や「優先順位」の課題に対し、チームトポロジーの考え方を取り入れて、どのように体制を見直したかを紹介します。

具体的には、データサイエンティストをストリームアラインドチームから「コンプリケイティッド・サブシステムチーム」へと再配置した背景と、その後のチーム間連携を円滑にするための「プロトコル」の設計・運用についてお話しします。

はじめに

タイミーの開発組織ではチームトポロジーに基づいた組織運営を行っており、ストリームアラインドチーム、プラットフォームチーム、イネイブリングチーム、コンプリケイティッド・サブシステムチームといったチームタイプに分かれて、日々の開発業務を行っています。

チームトポロジー 価値あるソフトウェアをすばやく届ける適応型組織設計

tech.timee.co.jp

データサイエンスを用いた機能開発の初期フェーズでは、その立ち上げを軌道に乗せるために、データサイエンティストがストリームアラインドチームに所属し、プロダクト開発と並走する体制をとっていました。データサイエンティストがドメイン知識を深く理解し、ビジネス課題に対して即応性の高い分析やモデル構築を行う必要があったからです。

しかし、事業が急成長し、プロダクトが大規模化・複雑化するとともに、サブシステムとして運用しうるものが一定程度できてくるにつれて、「ストリームアラインドチームにデータサイエンティストが同居する」体制の限界が見え始めてきました。

そこで、高度な専門性が求められる特定の領域(審査システムや推薦システムなど)において、データサイエンティストをストリームアラインドチームから切り出し、コンプリケイティッド・サブシステムチームとして再編成する組織リデザインを行いました。

以下では、その背景にあった具体的な課題と解決策、そして運用で定着しつつある具体的な連携プロトコルについて紹介します。

課題:ストリームアラインドチームにおけるデータサイエンティストの認知負荷と専門性のコンフリクト

以前まで特定のプロダクト領域では、データサイエンティストがストリームアラインドチームの一員として活動していました。この体制には「ドメイン知識の獲得」や「デリバリーのスピード」というメリットがあった一方で、運用を続ける中で以下のような課題が顕在化してきました。

1. 認知負荷の高まり

ストリームアラインドチームの取り組みは、ユーザーへの価値提供に集中し、高速にイテレーションを回すことが求められます。一方、高度なML/LLMシステムの開発・運用には、論文調査や実験設計、長期的な精度改善といった、通常の開発サイクルとは異なる時間軸と専門知識が必要です。

ストリームアラインドチームにおける日々のユーザーへの価値提供と高度なMLモデルの研究開発を、同時に同一チーム内で担うことは、脳のスイッチングコストが非常に高く、認知負荷の限界を超えつつありました。結果として、「機能は作れるが、モデルの精度改善やアーキテクチャの刷新に手が回らない」という状況が生まれ始めていました。

2. 優先順位の競合

ストリームアラインドチームは、その時々のプロダクトゴールに向かってバックログの優先順位を決定します。一方で、ML/LLMシステムには「一度作って終わり」ではなく、データ傾向の変化への追随や継続的な精度モニタリングといった恒常的な改善が不可欠です。しかし、こうしたタスクは直近のゴールには直接結びつかないことが多く、結果として構造的に優先順位を上げにくいという課題がありました。

例えば、MLモデルの定期的な再学習パイプラインの整備や、将来の施策に向けた技術検証(PoC)、論文調査などは、短期的にはユーザー価値に直結しにくいため、スプリントの中で後回しにされがちでした。さらには、正式なタスクとしてバックログに載せることを諦め、メンバーが個人的に時間を捻出して整備を行うといった「隠れた稼働」が発生してしまうこともありました。これにより、データサイエンティストの活動が見えにくくなったり、キャリア成長の機会が損なわれたり、技術的負債が蓄積するといった懸念がありました。

解決策:チームトポロジーに基づくコンプリケイティッド・サブシステムチームの組成

これらの課題を解決するため、チームトポロジーの概念におけるコンプリケイティッド・サブシステムチームとして、対象領域ごとに専門チームを新設しました。

コンプリケイティッド・サブシステムチームの役割は、「高度な専門知識を必要とする複雑なサブシステムの管理に責任を持ち、その複雑さを他のチームから隠蔽すること」です。

これにより、データサイエンティストはコンプリケイティッド・サブシステムチームに所属し、複雑なサブシステムの開発・運用に集中できるようになります。同時に、ストリームアラインドチームは「中身の複雑なロジック」を意識することなく、提供されたAPIを利用するだけで高度な機能をユーザーに届けることができるようになります。

Before

flowchart TB
    subgraph SAT1 ["🔹 Stream-aligned Team"]
        direction TB
        DS1("📊 Data Scientist"):::ds
        Others1("👥 Other Members<br>(PdM, Designer, Eng, Scrum Master, etc.)"):::others
    end

    %% Styles
    classDef ds fill:#fff9c4,stroke:#fbc02d,color:black,stroke-width:2px,rx:5,ry:5
    classDef others fill:#ffffff,stroke:#bdbdbd,color:black,stroke-width:1px,rx:5,ry:5
    
    style SAT1 fill:#f8f9fa,stroke:#dee2e6,stroke-width:2px,rx:10,ry:10

After

flowchart TB
    subgraph SAT2 ["🔹 Stream-aligned Team"]
        direction TB
        Members2("👥 Team Members<br>(PdM, Designer, Eng, Scrum Master, etc.)"):::others
    end

    subgraph CSub ["✨ Complicated Subsystem Team ✨"]
        direction TB
        DS2("📊 Data Scientist"):::ds
    end

    CSub == "API (X-as-a-Service)" ===> SAT2

    %% Styles
    classDef ds fill:#fff9c4,stroke:#fbc02d,color:black,stroke-width:4px,rx:10,ry:10
    classDef others fill:#ffffff,stroke:#bdbdbd,color:black,stroke-width:1px,rx:5,ry:5
    
    style SAT2 fill:#f8f9fa,stroke:#dee2e6,stroke-width:2px,rx:10,ry:10
    style CSub fill:#fff3e0,stroke:#ef6c00,stroke-width:3px,stroke-dasharray: 5 5,rx:10,ry:10
    
    linkStyle 0 stroke:#ef6c00,stroke-width:3px,color:#ef6c00

インタラクションモードとプロトコルの明文化

組織を分けることで最も懸念されるのが、「サイロ化」と「連携コストの増大」です。これを防ぐため、基本的にはX-as-a-Serviceモードを採用しつつ、状況に応じて コラボレーションモードを使い分ける運用としています。

基本方針:X-as-a-Service

日常的な開発・運用においては、コンプリケイティッド・サブシステムチームが提供するサービス(API、ドキュメント等)をストリームアラインドチームがセルフサービスで利用する形をとります。

ストリームアラインドチームとの連携境界における、コンプリケイティッド・サブシステムチームの主な責務は以下の通りです。

  • 安定したAPIの提供と、SLA/SLOの維持。
  • 専門的なロジックの隠蔽と、利用しやすいインターフェースの設計。
  • ドキュメントの整備(ストリームアラインドチームが自律的に利用できるようにする)。

ストリームアラインドチームは、いちいちコンプリケイティッド・サブシステムチームに「これどうなってますか?」と確認することなく、APIドキュメントを参照して開発を進めることができ、開発スピードを維持できます。

もちろん、API仕様書だけを投げ合うのが非効率な場面(新規機能開発や大きな変更時など)では、同期的な会議や一時的なスクラムイベントへの参加などを通じて、柔軟にコラボレーションを行っています。基本は疎結合(X-as-a-Service)を保ちつつ、必要な時には密に連携できる柔軟性を持たせています。

プロトコルの明文化

「X-as-a-Service」や「コラボレーション」といったモードを定義しても、実際の現場では「このタスクはどっちの責務?」「今はどっちのモードで動くべき?」といった迷いが生じがちです。

そこで、これらの実行を補完する役割として、特に密接に関わっているストリームアラインドチームとコンプリケイティッド・サブシステムチームの間で「チーム間連携プロトコル」というドキュメントを作成し、合意形成を行いました。

このドキュメントには、主に以下のような内容を記載しています。

  • 各チームのミッションと責務の定義: ストリームアラインドチームとコンプリケイティッド・サブシステムチームがそれぞれ何に責任を持つかを明記。
  • 自律的に実行できる活動リスト: 連携や確認なしで進めて良いタスクを具体的にリスト化(例:公開API仕様内でのリファクタリングなど)。
  • 連携が必要なケースとフロー: コミュニケーションが必要な具体的なトリガーと、その際の推奨連絡手段。
  • エスカレーションパス: 解決しない場合の判断フロー。

こうした具体的な「期待値調整」をドキュメント化しておくことで、日々のコミュニケーションコストを削減し、迷いを減らすことができています。

おわりに

組織構造は、その時点でのビジネスフェーズと技術的な複雑さに応じて進化させる必要があります。かつてはデータサイエンス的な要素を含んだ機能開発を行うため、ストリームアラインドチームにデータサイエンティストが在籍する体制をとっていましたが、フェーズの変化に伴い、コンプリケイティッド・サブシステムチームとして切り出す形がより適した状態へと変わりました。

今回のリデザインにより、データサイエンティストはより専門性を発揮しやすく、ストリームアラインドチームはよりユーザー価値に向き合いやすい体制が整いつつあります。特に、データサイエンティストからは「技術的な深掘りがしやすくなった」「コンテキストスイッチが減った」という声が上がっており、組織としての健全性も向上しています。

この事例が、拡大するエンジニアリング組織におけるチーム設計の参考になれば幸いです。

We’re Hiring!

タイミーではデータサイエンティストをはじめ、一緒に働くメンバーを募集しています!

カジュアル面談も行なっておりますので、興味のある方はぜひお気軽にお申し込みください!

https://product-recruit.timee.co.jp/

タイミーMLOpsエンジニアが描く、LLM基盤の夢妄想

はじめに

この記事はTimee Product Advent Calendar 2025の25日目の記事です。

MLOpsエンジニアのtomoppiです。データエンジニアリング部 データサイエンスグループ(以下DSG)に所属し、ML/LLM基盤の構築・改善に取り組んでいます。

2024年10月にタイミーへジョインし、気がつけば1年あまりが経ちました。2025年はLLM/LLMOpsに奔走した1年で、PoCを実施した施策の本番導入や、LLMの可観測性・プロンプトマネジメントに取り組み、走りながら考え、考えながら走る日々でした。

そんな中で、ふと思うようになりました。「2026年、タイミーのLLM基盤はどうあるべきか?」

本記事は、その問いに対する私なりの答え——というより、夢と妄想です。この1年で見えてきた課題と、それを乗り越えるための構想を、できるだけ具体的に描いてみます。

2025年の振り返り

個人的な振り返り

まずは、私がこの1年取り組んできたことについて、簡単に触れさせていただきたいと思います。

年初時点では、社内でいくつかのLLM関連PoCが動いているフェーズで、まだMLOpsとしてのLLMへの関わりは薄い状態でした。一方で、DSG内ではLLMへの注目が高まり、キャッチアップを目的としたLLM勉強会が始まりました(この勉強会は現在も継続しています)。

その流れで、2月に「LLMOpsとは何か」をテーマに勉強会を開催し、評価手法やプロンプト管理、モニタリングなど、従来のMLOpsとは異なる論点をDSGで共有しました。

3月に入ると、PoC段階だったLLM機能を本番導入する動きが増え、「どう設計すれば安全かつ現実的に運用できるか」を一から詰めていくことになりました。この過程で得られた知見・経験は、LLMアプリケーション向けProduction Readiness Checklist執筆の動機になりました。

LLMOpsツール導入の検討も並行して行い、10月頃、LLMOps基盤としてDatadog LLM Observabilityの本格運用を開始しました。

「なんとなく動いている」から「ちゃんと運用できている」状態へ、徐々に進めている感覚がありました。

また、Datadog社からのお声がけで、Datadog LLM Observabilityを題材とした登壇の機会もいただき、実りの多い1年となりました。

LLMOpsとTeam Topologies

MLOpsチームは、Team Topologiesで言うCollaboration Modeで、データサイエンティストチーム(as Complicated Subsystem Team)やプロダクトエンジニア(as Stream-aligned team)と密に連携してきました。

AI施策ごとにMLOpsエンジニアがプロジェクトに参画し、初期の壁打ちからアーキテクチャ設計、実装まで伴走してきました。この進め方は、ナレッジが少なく不確実性が高い状況で、知見を素早く蓄積し、開発のアジリティを確保するうえで非常に有効でした。

一方で、この体制は長期的には持続しないことも見えてきました。

「AIを使いたい」という熱量が全社的に高まるほど、インフラやガバナンス整備待ちの「行列」が生まれ、結果としてビジネスのスピードを落としてしまいます。

同時に、LLM利用が拡大するにつれ、プロンプトインジェクションをはじめとするセキュリティ対策やコスト最適化、利用モデルの透明化といったガバナンスが、より重要になってくると考えています。

その規模になると、個別対応をずっと続けることは現実的ではありません。

2025年末の課題

整理すると、現状の課題は次の3つです。

  • Agility(俊敏性の欠如)
    • 検証環境構築のリードタイムが長く、高速な検証を阻害する状況になっています。
    • LLM開発では「評価データの準備」「プロンプト変更→評価→改善の反復」「モデルの切り替え」「コスト見積(トークン/レイテンシ/単価)」など、環境以外にも反復のボトルネックが多く、施策の立ち上げや改善の速度を落としてしまいます。
  • Scalability(拡張性の限界)
    • LLM施策の需要増に対しMLOpsチームの供給体制が追いつかず、組織全体でのLLM施策がスケールしません。
    • ボトルネックが「技術(インフラ)」ではなく「人(MLOpsチーム)」になっていることは、構造的な制約であり、このままではスケール上の限界に直面します。
  • Governance(統制の分散/部分最適)
    • 施策ごとに個別最適な判断・実装が積み上がり、環境や運用がサイロ化します。その結果、コスト配賦や利用実態の把握、セキュリティ対策、監査対応などがチームごとにバラバラになり、全社としての管理が難しくなっています。

2026年に目指したいタイミーのLLMOps

Collaboration ModeからX-as-a-Serviceへ

前述したような課題を乗り越え、LLM活用を全社的にスケールさせるために、2026年はMLOpsチームが個別のプロジェクトに入り込むのではなく、「セルフサービスで使えるプラットフォーム」を提供することを目指しています。Team Topologiesで言うX-as-a-Serviceへの移行です。

プロダクトチームが自律的にLLM機能を開発・運用できる基盤——これをUnified LLM Platformと呼ぶことにします。

「The Art of AI Maturity」とPlatform Engineering Maturity Modelへの接続

とはいえ、「本当にUnified LLM Platformとしての共通基盤は必要なのか?」という問いには答えておきたいところです。戦略的なフレームワークを参照しながら、その必要性を整理してみます。 そのフレームワークとして、The Art of AI MaturityPlatform Engineering Maturity Modelを参照していきます。

The Art of AI Maturityから見たUnified LLM Platformの必要性

The Art of AI Maturityは、Accentureが提唱するAI成熟度を評価するフレームワークで、17のKey Capabilitiesを定義しています。タイミーでは、このフレームワークを全社的に参照しています。

Unified LLM Platformの構築は、以下の8つのKey Capabilitiesに対する実行施策となります。

Key Capability Unified LLM Platformとの関係
#3. Proactive vs Reactive 先行投資によるプラットフォーム整備により、市場ニーズ発生後の追随ではなく、先回りでAIオポチュニティを創出
#4. Readily Available AI/ML Tools セルフサービス化されたLLM開発環境により、開発者が環境準備で待たされない状態を実現
#5. Readily Available Developer Networks 統一インターフェースやオープンなアーキテクチャにより、OSSコミュニティとの連携を促進
#6. Build vs Buy 競争優位を生むコア領域(LLM features)は内製、基盤部分でのUtilityはSaaS/PaaS/OSSを活用するポートフォリオ戦略を実現
#7. Platform & Technology 推論基盤(マルチプロバイダー対応、統一API)、ガードレール(DLP/ポリシー/ツール実行制御)、可観測性(トレーシング/メトリクス)、評価、コスト管理をプラットフォームとして標準化し、安全で再現性のある運用を実現
#13. Innovation Culture Embedded 探索の自由度を高めるプラットフォームにより、日常業務に実験・学習サイクルを組み込む文化を促進
#16. Responsible AI 技術的統制(PIIマスキング、監査ログなど)により、倫理・法規制に適合したRAIプロセスを実現
#17. Responsible AI — Change RAI体制の継続的強化を、プラットフォームレベルで自動化・標準化

Platform Engineering Maturity Modelから見たUnified LLM Platformの必要性

次に、Platform Engineering Maturity Modelを参照してみます。 Platform Engineering Maturity Modelとは、プラットフォームエンジニアリングを5つの観点(Adoption、Interfaces、Investment、Measurement、Operations)で評価するフレームワークです。

これまでプロジェクト単位でのスピードを優先してきたため、プラットフォームとしての成熟度は「レベル1(暫定的)」であり、部分的に「レベル2(戦略的)」という評価になります。

0→1の立ち上げ期においては、この「都度対応」が最も機動力を発揮しました。しかし、1→10、10→100へとスケールさせるこれからのフェーズにおいては、かつての最適解(レベル1)が最大のアンチパターンになりつつあります。「レベル1だからダメ」なのではなく、「レベル1のやり方で戦えるフェーズは終わった」と認識しています。

2025年の振り返りで述べた課題や体制を踏まえると、現在のタイミーのLLMOpsは、各観点で以下のような状態にあると考えられます:

観点 現状
Investment(投資) プロジェクトごとの個別対応が基本で、MLOpsチームの部分的なタスクとして遂行している状況です。
Adoption(採用) LLMが必要なチームが散在する社内ナレッジをもとに施策を検討し、MLOpsチームも都度個別対応を行っていたため、標準的なプラクティスやポリシーが不十分な状態です。
Interfaces(インターフェース) これまではMLOpsエンジニアが伴走し、プロジェクトごとに最適な構成を提供してきました。部分的に共通化できている箇所もありますが、セルフサービスで利用できる統一インターフェースは整っていない状況です。
Measurement(測定) LLM基盤として指標は定義できておらず、収集もできていない状況です。
Operations(運用) これまではプロジェクトごとのリクエストに応じた柔軟な対応で素早く価値を届けてきましたが、モデルの利用状況や、更新対応は個別管理になっており、LLMライフサイクルを中央集権的に管理できていない状態です。

2026年に目指すのは、各観点でレベル3(スケーラブル)への移行です。これはチャレンジングな目標ですが、Unified LLM Platformの構築により実現を目指します:

観点 実現内容
Investment MLOpsチームがPlatform Teamとして明確な役割を持ち、LLM Platformのロードマップに基づく計画的な投資を実行していきます。
Adoption 「Golden Paths over Cages」 の方針により、「強制」ではなく「便利だから使われる」プラットフォームを実現。ドキュメント整備とオンボーディング(利用開始までの手順・ガイド)を含めたSelf-service化を推進し、プロダクトチームが自律的にLLM機能を開発・運用できる状態を目指します。
Interfaces 統一されたエンドポイントとOpenAI互換の単一API仕様を提供。認証・認可はインフラ層で透過的に実行され、開発者は意識不要。Observability by Defaultにより、追加実装なしで全リクエストのトレース・レイテンシ・コストを可視化し、セルフサービスで利用できるようにします。
Measurement LLM基盤のSuccess Metrics(標準指標)を定義し、必要な計装(テレメトリ/ログ/利用データ)を組み込みます。定期レビューにより、利用状況と改善効果を可視化し、意思決定と改善サイクルを回せる状態にします。
Operations どのサービスがどのモデルを利用しているのかを中央集権で管理し、モデルの非推奨対応や新規モデルのロールアウト統制を取れる状態にします。加えて、Migration手順、責任分界(責任共有モデル)、ロールバック戦略を標準プロセスとして整備します。

実現に向けたエンジニアリング

ここからは、どのようにUnified LLM Platformを実現するのか、エンジニアリング観点で整理していきます。

「探索の自由度」・「運用時の信頼性」・「技術的統制」

プラットフォームを設計するにあたり、特に重視したい要求を3つに整理しました。

1. 探索の自由度(Freedom of Exploration)

  • 「データサイエンティストやプロダクトエンジニアが、面倒な環境構築なしに、アイデアを即座に試せる環境」 を実現したいです。

LLM開発は「試行錯誤」がすべてです。エンジニアだけでなく、PMやビジネスサイドのメンバーもプロンプトを直接触れられることが、開発速度に直結します。

この要求を満たすため、以下の要素が重要です。

  • モデルの切り替え(Model Agnostic)
    • 「Geminiで試したけど、AnthropicやBedrockだとどうなる?」をコード変更なしで、設定の切り替えだけで素早く試せること。
  • 統一インターフェース
    • プロバイダーごとに異なるAPI仕様(OpenAI / Azure / Bedrock / Vertex AIなど)を意識せず、統一されたSDK/APIで呼び出せること。
  • プロンプトのバージョン管理とプレイグラウンド
    • Gitのように「v1.2のプロンプト」と「v1.3のプロンプト」をGUI上で比較実行できる環境。エンジニアに依頼せずとも、画面上でパラメータを調整して挙動を確認できること。
  • 評価駆動開発(Eval-driven Development)のための環境
    • オフライン評価における実験管理やLLM-as-a-judgeなどを、Day1から行えること。
  • Open-weight Modelsの活用
    • 必要なユースケースでは、GKE上のvLLMなどでOpen-weight Modelsを扱える選択肢を残すこと。

2. 運用時の信頼性(Operational Reliability)

  • 「確率的なLLMを、確定的なエンタープライズシステムとして振る舞わせるための安定化機構」 が必要です。

PoCでは許されても、本番環境では「APIが落ちていました」「遅すぎます」は許されません。

この要求を満たすため、以下の要素が重要と考えています。

  • リトライとフォールバック(Fallbacks)
    • プロバイダー障害時に別経路へ切り替えたり、高性能モデルがタイムアウトしたら高速なモデルで再試行したりする冗長化構成。
    • ただし、フォールバックは「評価で許容できる」と確認できた組み合わせに限定。(挙動差分が大きいと品質事故につながるため)。
  • スマートキャッシング(Caching)
    • 同一リクエストにはキャッシュから応答し、レイテンシ短縮とコスト削減を両立。将来的にSemantic Cacheも検討。
  • レート制限と公平性
    • 一部のヘビーユーザーがトークンを使いすぎて他のサービスに影響を与えないよう制御できること。
  • オブザーバビリティ
    • 開発者がモニタリング環境の構築を意識せず、モデルの性能やリソース使用状況を確認できること。

3. 技術的統制(Technical Governance)

「組織としてリスクをコントロールし、野良AIや青天井のコストを防ぐためのガードレール」を整備したいです。

この要求を満たすため、以下の要素が重要と考えています。

  • PII/機密情報のマスキング (Redaction)
    • クレジットカード番号、メールアドレスなどがプロンプトに含まれていたら、LLMに送信する前に自動でマスキングする機能。
  • プロンプトインジェクション対策(Prompt Injection Mitigation)
    • 入力/出力のフィルタリング(疑わしい指示の検知・遮断、システムプロンプトや機密情報の漏洩防止など)と、コンテンツポリシー(許可/禁止の基準)をプラットフォーム側で標準化する。
    • Tool Use(外部API呼び出し、DB参照、チケット発行など)を行う場合は、ツールのAllowlist化・引数検証・権限境界の設計などにより「実行制御」を行い、必要に応じてサンドボックス(隔離環境)で安全に実行する。
  • 認証方式の統一化
    • 全てのLLMリクエストをLLM Gatewayを通すことで、認証方式を統一化し、プロジェクト固有で認証部分を行う必要性をなくす。
  • コストの予算管理 (Cost Budgeting)
    • 「プロジェクトAは月額◯◯円まで」といったキャップを設定し、超過しそうになったらアラートを出す仕組み。
  • 監査ログ (Audit Logs)
    • 「誰が」「いつ」「どんなプロンプトを」「どのモデルに」投げたか追跡可能にすること。
    • ここは強い統制になる一方、ログの取り扱い(PIIの扱い、保持期間、閲覧権限、暗号化、マスキング後のみ保存など) も同時に設計対象。

プラットフォームの構成イメージ

これらの要件を同時に満たすために、AI Gateway(LLM Gateway) というアーキテクチャパターンを中心に据えることを検討しています。

イメージ図

アプリケーションとLLMプロバイダーの間に「ゲートウェイ」を挟むことで、開発者は自由にAPIを呼び出せます。一方で、裏側では自動的にログが取られ、セキュリティチェックが走り(統制)、キャッシュやリトライが効く(信頼性)という構成です。

設計原則

プラットフォームを構築・運用していく上で、以下の原則を大事にしたいと考えています。これらは Platform EngineeringセキュリティSRE / ObservabilityLLMOps の各分野から学んだものです。

原則 概要
Platform as a Product プラットフォームを「社内向けプロダクト」として捉え、利用者(プロダクトチーム)の体験を最優先に設計する。「インフラを提供する人」ではなく「開発者という顧客にプロダクトを届ける人」としてのマインドセットを持つ
TVP(Thinnest Viable Platform) 初期スコープは「ガバナンスと可観測性」を解決する純粋なLLM APIプロキシに絞る。RAGやTool Useは後から。まずは「安全で、誰が使ったか、いくらかかったかがわかるAPI」を全社に民主化する
Golden Path の提供 「推奨される使い方」を明確にし、その道を歩めば自然とベストプラクティスに沿える状態を作る。自由度は残しつつも、迷わないための道標を用意する
Secure by Default LLMはプロンプトインジェクションやPII流出など従来とは異なるリスクがあるため、ガードレールをデフォルトで有効にした状態を標準とする。「セキュリティを意識しなくても安全」な状態を作る
Observable by Default LLMは確率的に動作するため、入出力の追跡なしには改善のポイントが見えない。特別な設定なしで自動的にトレーシングとメトリクス収集が行われる状態を目指す
Eval Driven(評価駆動) LLM開発ではリグレッションが頻繁に発生するため、「感覚」ではなく「評価」で改善を確認する。評価データセットの整備、CI/CDへの評価組み込み、LLM-as-a-Judgeによる自動スコアリングを開発フローに組み込む

アーキテクチャ設計

現在、初期検証で構築しているアーキテクチャは下図のようになっています。

技術選定

AI Gateway

まずはAI Gateway(LLM Proxy)の技術選定から着手しました。ここで満たしたい要件は、ざっくり言うと次のとおりです。

  • マルチプロバイダー対応: Vertex AI / AWS Bedrock / GKE上のvLLM(セルフホスト)を同じ入口で扱えること
  • 認証: LLMプロバイダー側へのキーレス認証(OIDCなど)と、クライアント向けのVirtual API Key
  • トラフィック制御と信頼性: リクエスト数・トークン数ベースのレート制限、フォールバック、ストリーミング
  • 統一インターフェース: OpenAI互換APIなどのスキーマ正規化、Embeddingsのサポート
  • オブザーバビリティとコスト: OpenTelemetry連携、コスト追跡(チーム/キー単位の配賦)、(将来的な)セマンティックキャッシュ
  • ガードレール: PII/DLPのマスキング(Redaction)を含む入力/出力のフィルタリングに加え、プロンプトインジェクション対策(ポリシー、検知/遮断、必要に応じたTool Useの実行制御)

10個強のツール(LiteLLM Proxy / Envoy AI Gateway / APISIX / Kong / Portkey.ai など)を調査した結果、現時点では LiteLLM Proxy が要件を最も素直に満たせそう、という結論になりました。

一方で、Envoy AI Gateway もKubernetesネイティブで魅力的です。将来的に本格的な本番運用(GitOps前提の運用、より厳格なトラフィック制御、低レイテンシ)に寄せるなら有力候補です。しかし現段階では、初速を優先して LiteLLM +(認証を担う)Envoy Sidecarの構成から始めるのが現実的だと感じています。

LLM Observability Tool

現在利用しているDatadog LLM Observabilityは、プラットフォームでも引き続き中核の可観測性基盤として活用する想定です。AI Gateway 経由の呼び出しを起点に、レイテンシやエラー、トークン使用量などを一貫して可視化し、運用改善や評価(Eval Driven)に繋げます。

Envoy Sidecar vs Service Mesh

Cloud Runのマルチコンテナ機能で、Envoy → Backend にする必要があるかどうかについては、代替案としてService Mesh(Istio・Cloud Service Mesh)が考えられます。

しかし、現状Service Mesh構成ではなく、この取り組みのために0からService Meshを構築することは、TVPの考え方に反します。そのため、Envoy Sidecarを採用することにしました。

AWS → GCPのキーレス認証

クロスクラウド連携で悩ましいのが、 AWS → GCP の認証です。従来の Workload Identity Federation だと、GCP側のSTSを呼び出してアクセストークンを取得する必要があり、実装・運用の負担が大きくなりがちです。

そこで最近登場した、AWS IAM Outbound Identity Federation を使って、AWS STSが発行するJWTを、Cloud Run前段のEnvoyで直接検証する方式を採用することにしました。

おわりに

ここまで読んでいただきありがとうございます。

この構想には多くの不確実性があり、自由と統制のトレードオフとも向き合い続けることになると思います。「夢妄想」と銘打ったとおり、すべてが計画どおりに進む保証はありません。

それでも、「安全に、再現性を持ってLLMを活用できる状態をつくる」という軸だけはブレずに、2026年はタイミーのLLM基盤を前に進めていきたいと思います。

来年のアドベントカレンダーで、「あの夢妄想、現実になりました」と報告できることを目指して。

タイミーのMLOps・LLMOpsって面白くなりそうと思ってもらえた方はぜひ、お話ししましょう!

プロダクト採用サイトTOP

カジュアル面談申込はこちら

「超人EM」へのアンチテーゼ - チームで立ち向かうためのエンジニアリングマネジメント -

はじめに

この記事はTimee Product Advent Calendar 2025の24日目の記事です。

株式会社タイミーでVPoEをつとめております、赤澤 a.k.a chango(@go0517go)です! 「ええやん!」が口癖なので、社内では「VP of Eeyan」なんていじられ呼ばれたりもしています。

さて、アドベントカレンダーも佳境の24日目。 今回は、私が以前登壇した内容をベースにしつつ、来年の「EMConf JP 2026」にProposalとして提出していたテーマのうちの1つである「チームで立ち向かうエンジニアリングマネジメント」についてお話しします。 Engineering Managementに関するテーマや内容は自分にとっても深く、私たちも常に試行錯誤しながら探索し続けている領域です。 今回のアドベントカレンダーでは、その「触り」となるエッセンスや、根底にある思想にフォーカスして書きたいと思います。

なお、こちらのProposalは残念ながら採択されませんでした涙…。ですが、タイミーはEMConf JP 2026のプラチナスポンサーとして協賛しております! 弊社のCTOである山口 a.k.a ZIGOROu(@zigorou)がスポンサーセッションで登壇しますので、タイミーの技術戦略やエンジニアリング組織論に興味がある方は、ぜひ会場でZIGOROuのセッションを聞いてください!

それでは、急拡大する事業とプロダクト開発の中で私たちが描いた、「診断医」としてのエンジニアリングマネジメントについてお話しします。

前提:アウトカムに向き合う「プロダクトエンジニア」と組織構造

EMの役割についてお話しする前段階として、そもそもタイミーのエンジニアリング組織がどのような組織構造で、どのようなエンジニア像をもって開発しているかをお話しさせてください。

① 「チームトポロジー」に基づくストリームアラインドチーム

タイミーの開発組織は、『チームトポロジー』の考え方に基づき、特定の価値を提供する継続的な流れ(バリューストリーム)を対象とした「ストリームアラインドチーム」で構成されています。

これは、エンジニアだけでなくPdMやデザイナーを含む6±2名程度の「職能横断型組織」であり、DiscoveryからDelivery、つまり価値の探索と検証から実際の開発、そして運用までを一気通貫で担い、高速に仮説検証のサイクルを回すことを目的としています。

タイミーにおけるストリームアラインドチーム

② 「プロダクトエンジニア」という在り方

そして、そこで働くエンジニアに求めているのは、単に機能を作るだけの役割ではありません。私たちは「プロダクトエンジニア」という在り方を掲げています。 これは、「プロダクトの提供価値を継続的に高めることでお客様の課題を解決するエンジニア」と定義しており、全員がアウトカム(顧客への価値提供)に向き合うことを前提としています。

  • 提供価値向上: 機能を作ることではなく、顧客課題を解決することに責任を持つ。
  • 越境性: 自分の専門領域に閉じず、ドメインや役割を越えて知識を追求する。
  • オーナーシップ: 「How(どう作るか)」だけでなく、「Why(なぜ作るか)」「What(何を作るか)」の段階から関与する。

③ 2sidesの顧客に向き合う複合ドメインのプロダクト

さらに重要なのが、私たちが向き合っているプロダクトの特異性です。 タイミーは「ワーカー(働き手)」と「クライアント(事業者)」という異なる2つの顧客(2sides)に価値を提供するプラットフォームであり、その裏側は複数のビジネスドメインが密接に連携する複合システムとなっています。 「スキマバイトのマッチングアプリ」として、UI/UXとともに想起いただくことが多いと思いますが、実はそのバックグラウンドには極めて広範な機能群が存在しています。 例えば、いくつか代表的な機能要素を見ても、QRコードによる出退勤、銀行API連携を伴う給与計算・振込処理、そして法令対応を含む労務管理など、これらすべてがなめらかな雇用体験と良いマッチングを実現するために不可欠な要素として統合されています。

つまり、タイミーのEMが管掌するのは、「職能横断で自律的に動き、Why/Whatから考え、これら複雑な複合ドメインのアウトカムにコミットするチーム」なのです。

2sidesの顧客に向き合う複合ドメインのプロダクト

なぜ私たちは「超人EM」を組織の前提とするのをやめたのか

さて、ここからが本題です。 上記のような複雑かつ高水準なチームを率いるエンジニアリングマネージャー(EM)には、どのような能力が求められるでしょうか? 一般的に語られる「EMの4つのマネジメント領域」を見てみましょう。

  • Technology: 技術的負債の解消、アーキテクチャ設計、生産性向上
  • People: 採用、育成、評価、目標設定、メンタリング
  • Process: 開発プロセスの最適化、アジャイルの実践
  • Product: 顧客課題の発見、ドメイン知識、法要件の理解

知らず知らずのうちに、これら全てを一人で高い水準でリードできる「超人(スーパーマン)EM」を、理想像として求めてしまってはいないでしょうか?

もちろん、各マネジメント領域においてプレイヤー性を高め続けることを、個人のキャリアとして追い求めることは重要であり、私自身もそうありたいと願っています。

しかし、同時に組織としてその超人的存在を前提としすぎると「ひずみ」が拡大してしまいます。

全4領域で常に100点のプレイングができる人材を求めることは、現実には極めて困難であり、個人の負荷を過剰にします。その結果、採用基準も高止まりし、適切な人材が集まらなくなる。そうして、事業の急成長にエンジニアリング組織とそのマネジメントが追いつかないという、組織的な「ひずみ」が生み出されるのです。

なぜ、知らず知らずのうちに、そこまでの理想を求めてしまうのでしょうか?

そもそもEMという職種は役割定義が曖昧になりがちな上に、タイミーの場合は多様なバックグラウンドを持つEMが中途入社で多く参画いただく状況でした(私自身もその一人です)。だからこそ、「この混沌を個人の力で解決してほしい」という期待が先行し、採用要件や期待値の中に、無意識のうちにこの「スーパーマン像」を投影してしまうケースがあると考えています。 私たち自身も過去を振り返り、そのような判断があったと振り返っています。

かといって、現実解として特定の領域、例えば「People Management」だけに単純に絞り込むこともできません。 顧客課題を深く理解するProductの視点、そして最適な実装を判断するTechnologyの視点。これらを欠いたままでは、「エンジニアリング組織」のマネージャーとして本質的な課題解決ができないからです。

同時に、一人の人間にすべての領域のリードを求めれば、マネジメントの人材不足はより顕著になります。

そもそも、「あれもこれも全部やる」というのは、戦略ではありません。それは単なる「願望(Wish List)」です。 『良い戦略、悪い戦略』の著者、リチャード・P・ロメルトが指摘するように、戦略の本質は「何をやらないか」を選択することにあります。

タイミーのエンジニアリング組織では、「超人EM」という個人の頑張りに過度に依存する構造から脱却し、組織として持続可能な仕組みを作る必要があると判断しました。

役割のアップデート:「実行」から「診断」へ

「全てをやる」という願望(Wish List)から離れたとき、私たちの目の前に残った問いは明確です。

「では、限られたリソースで最大のアウトカムを出すために、今『何をするか』を誰が決めるのか?」

複雑で混沌とした状況の中で、無数にある課題から本質的な「急所」を見極め、チームの力をそこに集中させる。 自分で手を動かして全てを解決するのではなく(もちろんそれも重要な手段です)、「何を解くべきか」を正しく定義することこそが、この規模の組織におけるEMの最大の提供価値ではないか。

そう考えたとき、私たちの指針となったのが、戦略論の名著『良い戦略、悪い戦略』にある「戦略のカーネル」という概念でした。 これはタイミーのプロダクト開発組織においてしばしば用いられる、あらゆる戦略や方針を検討する際の「骨格」となっているフレームワークであり、私自身もタイミーにジョインした後、CTOのZIGOROuから注入された代表的なエッセンスです笑。

【参考:戦略のカーネルとは】

  • 診断 (Diagnosis): 状況を単純化し、取り組むべき課題の「核心」を見極める。
  • 基本方針 (Guiding Policy): 診断された課題に対処するための、全体的なアプローチ。
  • 一貫した行動 (Coherent Actions): 方針に基づき、リソースを集中させた具体的なアクション。

私たちは今、これをベースに、EMの役割を「プレイヤーとして全てを実行すること」から、「チームが実行できるように『診断』し設計すること」へとアップデートしようとしています。

構造①:診断 (Diagnosis) 〜 核心を見極める 〜

EMの第一の責務は、Technology、People、Process、Productの全領域において、「何が問題の『核心(Kernel)』なのか」を高い解像度で見極めることだと考えています。

単に「課題リスト」を作るのではありません。「開発スピードが遅い」という現象があったとき、その真因は技術的負債なのか、意思決定フローなのか、それともメンバーの心理的安全性なのか。 無数にある課題の中から、そこさえ解けば状況が好転する「急所」を見抜くこと。この診断をエンジニアリングの現場で適切に行えることをEMの重要な役割だと考えております。

構造②:基本方針と行動 (Guiding Policy & Actions) 〜 「埋め方」のデザイン 〜

診断によって「何が足りないか(Gap)」が明確になれば、次はそれをどう埋めるか。 ここで重要なのは、「基本方針(Guiding Policy)」を定め、それに基づいた「一貫した行動(Coherent Actions)」に落とし込むことです。

その「埋め方」の選択肢は、決して「EMが頑張る」ことだけではありません。チームの状況に合わせて、最適な打ち手をデザインする必要があります。

  • プレイヤーとしてEMが補う(「オレが3人分になる...」)
  • メンバーのケイパビリティ獲得を支援する(育成・学習機会の提供)
  • 組織構造やリチーミングで解決する(チーム構成の変更)
  • 外部からケイパビリティを獲得する(採用・外部の組織や企業との連携)

重要なのは、自分がスーパーマンになることではなく、診断結果に基づき、最適な「方針」を選び取り、迷いなく「行動」に移すことです。

逆に言えば、「自分はやらない(チームや仕組みに任せる)」と決める勇気を持つことでもあります。

個人の万能さに過度に依存するのではなく、チームとしての総力を最大化する道を選ぶ。これが今取り組んでいる(そして試行錯誤し悩みながら進んでいる!)私たちのエンジニアリングマネジメントです。

「個のスパイク」を組織のポートフォリオにする

「診断医」モデルにおいては、EM個人の「弱み」は個人としては、今後補強していく点であると同時に、組織としては「補い合うべき余白」になります。 私たちは、各EMのバックグラウンド(エンジニア、PdM、アジャイルコーチ・スクラムマスターなど)による「スパイク(突出した強み)」を歓迎し、それを個人のスキルではなく「組織のアセット」として考えようとしています。

  • Tech-oriented EM: 技術的な診断や意思決定の「構造知」を言語化し、他のEMに共有する。
  • Product-oriented EM: 複雑なドメインや法要件の読み解き方を形式知化し、組織全体のProduct理解を底上げする。
  • Process-oriented EM (Engineering Operations Manager): 開発フローやデリバリーのボトルネックを可視化し、チーム間の学習速度と成果再現性を高める。AI活用やリチーミング設計など、継続的改善のサイクルを仕組み化。

もちろん、現実的な運用においては、「チームが取り組む課題」と「EMの強み」が必ずしもパズルのように完璧にマッチするとは限りません。 特に現在のタイミーでは、EMが目標設定や評価も担う組織上の上長も兼ねています。心理的安全性や長期的な育成の観点からも、プロジェクトの都合だけで上長を高頻度に変えることは望ましくありません。

そこで重要になるのが、「EMs(EMチーム)間での相互補完」です。 担当チームの課題に対して、そのEMのケイパビリティが不足している場合、他の得意なEMがクロスで入って支援する。個人の不足を「個人の努力」だけで埋めるのではなく、「組織(EMs)の総力」でフォローする体制を構築しています。

さらに現在はテックリードなどの役割をあらためて定義、各領域のマネジメントをより委譲・分散していく体制についても、CTOや私(VPoE)、そして現場のEMたちと議論し、進行しています。

ここはまだまだ議論や方針を定めている最中で、まさにこれから推進していきたい「組織の伸び代」でもあります。

一人でフルスタックを目指すのではなく、EMチーム全体、そして組織全体でポートフォリオを組み、相互に診断を支援し合う。

これこそが、複雑化・巨大化する組織に対する唯一の対抗策だと信じています。

仕組みで支える:AI活用と評価制度

この新しいEM像を考え方で終わらせないために、いくつかの「仕組み」での実装に現在取り組んでいます。

① AI・LLMによる「診断力」のブースト

全領域のキャッチアップを人間の脳だけで行うには限界があります。そこで私たちはAIを「診断アシスタント」としてフル活用しています。

  • コードベースの把握: DevinやCursorを用い、技術的な仕様や依存関係を高速に把握する。
  • 組織状態のモニタリング: Slackのpublicな業務チャンネルでの重要なトピックや会話のサマリ、チーム全体のGitHubのアクティビティ傾向など、客観的な数値指標を参考にエンゲージメントの変化の予兆(診断材料)を提示する。

② キャリアラダーとコンピテンシーの再設計

「スーパーマン」を求めない以上、評価基準も変える必要があります。 ここが今まさに私たちが挑戦している最前線なのですが、EMのキャリアラダーにおいて、「個人の実行能力(どれだけコードが書けるか、どれだけ仕様に詳しいか)」の評価軸をさらに拡張して、以下の能力を重視するコンピテンシー設計へとシフトを進めています。

  • 診断の正確さ: 課題の核心(Kernel)をどれだけ深く捉えられているか。
  • チームへの移譲・設計能力: 成果を特定の個人に依存させず、再現性のある仕組みとして設計・実装できているか。
  • 相互補完: 自身の強みを組織知として還元し、他EMを支援できているか。

「個人のスキル」ではなく「組織の機能」としてEMを定義し直す。この評価制度の設計こそが、超人EMへのアンチテーゼを完遂させる最後のピースだと考えています。

まとめ:Engineering Managementを「個人のスキル」から「組織の機能」へ

急拡大する組織と複雑なドメインに向き合う中で、私たちがたどり着いたエンジニアリングマネジメントの現在地は、以下の4点に集約されます。

  • 役割の再定義

EMに求めることは、「自らが全領域を実行すること(超人的になること)」ではありません。「チーム全体で全領域を実行できるように、状況を『診断』し『設計』すること」です。

  • 強さの源泉は「構造化」

個人の馬力に頼るのではなく、「診断 → 基本方針 → 行動」のサイクルを回し、課題解決を構造化できることこそがEMの強さであると考えています。

  • 「個のスパイク」を組織の武器に

全方位完璧である必要はありません。EM個人の「スパイク(突出した専門性)」を歓迎し、足りない部分はEMチーム全体で相互補完する設計を進めています。

  • 「個人のスキル」から「組織の機能」へ

Engineering Managementを、属人化した個人のスキルではなく、再現性のある「組織の機能」へと昇華させること。これが、私たちの現在進行形の挑戦です。

この新しいEM像の実装はまだ道半ばです。だからこそ、私たちと一緒にこの挑戦を楽しんでくださる仲間を求めています!

未来のエンジニアリングマネジメントのあり方を一緒に描いていただけませんか?

私たちは今、これまでのエンジニアリングマネジメントを構造や仕組みで再現性持って継続していくというチャレンジの最中です。 「EMの仕事は負荷が高くて辛い」「一人の個人に求められる要素が多すぎる」という課題に対して、「チームで戦う」「診断と設計で価値を出す」というスタイルを確立したい。

タイミーではエンジニアポジションを全方位で全力拡大採用中です!そして今回お話しさせていただいたEngineering Managerポジションも特に注力しているポジションです。

採用ポジション:Engineering Manager

少し話聞いてみようかな、くらいの気軽さで私たちはとても嬉しいです、ぜひ気軽にお話しさせてください!明日25日の記事もお楽しみに!

プロダクト採用サイトTOP

カジュアル面談申込はこちら

2値化されたアウトカムをそのまま効果測定していいのか

はじめに

この記事はTimee Product Advent Calendar 2025の23日目の記事です。

背景

営業やマーケティングの現場では、連続変数を特定のしきい値で区切って2値のKPIとして扱うケースがあります。例えば、「月の購入金額が1万円を超えた顧客を『ロイヤル顧客』と定義する」といったケースです。ここで、このKPIに対する施策の効果測定を行う際は、「ロイヤル顧客への転換率」をアウトカムに設定し、ABテストで群間の差を検定するのが最もシンプルなアプローチとして挙げられます。

図1: Geminiもそう思います(2025/12/17, Gemini 3 Pro)

連続的な現象を2値データとして観察した場合は、連続的な潜在変数および誤差項のモデリングと、閾値を基準とした2値化によって定式化できます。例えば、誤差項にロジスティック分布を仮定した場合にはロジットモデル(ロジスティック累積分布関数)に従います[1][2]。本ケースもこれと同様に考え、施策効果の分布の推定が可能です。ただし、ビジネス上の定義としてはわかりやすいこの2値化処理は、同時に本来持っていた情報を捨てることになり、効果測定における検出力が低下する(正規分布に従う変数を中央値でカットした場合、同じ検出力を得るためにはサンプルサイズが1.5倍必要[3])と報告されています。

このようなケースではもとの連続量も併せてモニタリングすることが推奨されますが、本稿では連続量のままモデリングする手法の有効性検証に焦点を当てます。 具体的には、2値化されたアウトカムを2値として扱う場合と、連続量分布を推定してから事後的にKPIを計算する場合とで、効果測定の正確さがどう変わるかを比較します。

実験

1.分布の仮定が正しい一回試行での比較

仮想のECサービスを考え、「売上が1万円以上」をロイヤル顧客とみなし、顧客のロイヤル率を改善する施策をABテストした状況を想定してサンプルデータを作成しました。

具体的には、「非購入者(0円)」と「購入者(対数正規分布に従う売上)」が混在する売上分布を仮定し、以下のパラメータp, μ, σのもと、各グループで500顧客分ずつ作成しました。

パラメータ Control Treatment
購入率 p 0.50 0.53
対数平均 μ 8.0 8.20
対数標準偏差 σ 1.0 1.0

このとき、各サンプルデータが従う真の統計と施策効果は以下のようになります。

指標 Control Treatment 効果
期待売上 ¥2,457 ¥3,182 +¥724
ロイヤル率 5.65% 8.28% +2.62pt

次に、サンプルデータに対して以下の2つの手法でロイヤル顧客率に対する効果を推定し、真の施策効果を基準として比較しました。

  1. アウトカム2値化: 売上が1万円以上か否かのフラグを立て、その比率の差を直接比較する。
  2. アウトカム連続+ラベル事後計算: 真の尤度関数を用いて売上金額の分布を推定し、事後分布からロイヤル顧客の定義である「売上が1万円以上」を超える確率を計算して差を比較する。

2.分布の仮定が正しい複数回試行での比較

データが従う分布のパラメータを以下の範囲でランダムに変化させて作成しなおし、その他は1の設定を踏襲した実験を100回繰り返しました。そのうえで、2つの手法間で測定される効果の正確さを比較しました。

項目
購入率 p 0.6〜0.8
購入率 p への効果 +0.02〜+0.10
対数平均 μ 7.9〜8.2
対数平均 μ への効果 +0.05〜+0.20
対数標準偏差 σ 0.7〜1.1

3.分布の仮定が誤っている複数回試行での比較

2.と同様の手順でサンプルデータを20回作成し、各データに対して3通りの尤度関数(対数正規分布・ガンマ分布・パレート分布)を用いた効果測定をそれぞれ実施しました。そのうえで、各尤度関数間で測定した効果の正確さを比較しました。

結果

1.分布の仮定が正しい一回試行での比較

2つの手法間での結果の差を図2に示します。

図2: 2値化しないほうが分布の山が真値に近く、信用区間の幅も狭い。

2つの手法はどちらも真の効果をほぼ捉えていますが、「アウトカム連続+ラベル事後計算」手法(グラフ橙色)は真の効果である+2.62pt付近に鋭い分布でピークがあり、真値に近い効果を比較的高い確信度で捉えました。

2.分布の仮定が正しい複数回試行での比較

2つの手法を繰り返したとき、測定した効果の真値に対する差の比較を図3に、分布の幅の広さの差を図4に示します。

図3: 2値化しないほうが平均的に分布の山が真値に近い。

図4: 2値化しないほうが平均的に信用区間の幅が狭い。

「アウトカム連続+ラベル事後計算」手法のほうが平均的に真値に近い効果を推定しました。

3.分布の仮定が誤っている複数回試行での比較

「アウトカム連続+ラベル事後計算」手法について、尤度関数を変えたとき測定した効果の真値に対する差の変化を図5に示します。

図5: 尤度関数の当てはまりが悪いほど真の効果に対するバイアスが増加する。

尤度関数として真の分布と異なる分布を仮定した場合、当てはまりが悪いほど測定した効果が真値から離れる傾向があり、いずれも「アウトカム2値化」手法と比較して正確さが悪化しました。

結論

  • 連続量を2値化したKPIに対しては、Beta-Binomialモデルを用いた2値KPIに対する効果測定を、複雑な分布仮定を必要としないロバストなベースラインとして採用できます。
  • より高い検出力を追求したい場合、連続量のまま分布をモデリングして効果測定し、事後分布からKPIを再計算するアプローチが有効な選択肢となりえます。 ただし誤った分布を仮定すると逆効果となるため、WAICによるモデル選定や閾値周辺の適合度の目視確認など、厳密なモデル選定が必要です。

参考文献

[1] Greene, W. H. (2003). Econometric Analysis (5th ed.), pp. 665-669.

[2] C. M. ビショップ (2007). パターン認識と機械学習 上 (元田 浩 他 訳).

[3] Cohen, J. (1983). The cost of dichotomization. Applied Psychological Measurement, 7(3).

おわりに

タイミーではデータ分析を通じてプロダクトの成長を支えるメンバーを募集しています。ご興味のある方はぜひお話ししましょう!

プロダクト採用サイトTOP

カジュアル面談申込はこちら

元神主QAエンジニアが語る「祈り」と「品質保証」そして「システムを鎮める技術」

こんにちは。タイミーQAエンジニアの矢尻です。 この記事は Timee Advent Calendar 2025 シリーズ1の23日目の記事です。

「アドベントカレンダー」といえばキリスト教の待降節ですが、実は私、元神主(神職)というちょっと変わった経歴を持っています。

「異教徒がアドベントカレンダーを書いていいのか?」と心配される方もいるかもしれません。しかし、日本の神道には「八百万(やおよろず)の神」という、あらゆるものに神性を見出す懐の深い概念があります。クリスマスも正月も祝う日本人の精神性において、このテックブログもまた、技術の神々に感謝を捧げる「祭り」の一環と捉えれば、何ら問題はありません(ほんとに?)。

さて、今日はそんな異色の経歴を持つ私が、神社祭式とソフトウェア品質保証(QA)の驚くべき共通点」について、少しマニアックに語りたいと思います。

神社は「巨大なレガシーシステム」

神職からエンジニアに転身して最初に感じたこと。それは「やっていることが本質的に同じだ」という衝撃でした。

神社には「祭式(さいしき)」という厳格なプロトコルが存在し、「式次第」というプログラムがあり、「祝詞(のりと)」という仕様書が存在します。これらが緻密に相互作用し、バグ(不作法)なく執り行われることで、初めて「祭り」というシステムが正常稼働(デプロイ)されます。

私がQAエンジニアとして日々システムに祈っている「平穏」は、神主時代に祈っていた「安寧」と、構造的に全く同じものなのです。

1. 「祓(はら)へ」と「デバッグ」:ケガレを除去するリファクタリング

神道には「ケガレ(穢れ/気枯れ)」という概念があります。これは罪や穢れ、気力が枯渇した状態を指します。これを元の清浄な状態(あるべき姿)に戻すのが「祓(はら)へ」です。

システム開発における「バグ」や「技術的負債」、あるいは「不安定なテストデータ」。これらはまさにシステムにおける「ケガレ」です。

  • 修祓(しゅばつ): 祭儀の最初に行うお祓いは、本番環境(祭場)へ移行する前の初期化処理スモークテストです。
  • 大祓(おおはらえ): 定期的に蓄積した罪穢れを祓う行事は、定期的なリファクタリングそのものです。

「祓へ」によって場をクリーンにしなければ、どんなに素晴らしい祝詞(機能)も神様(ステークホルダー)には届きません。QAがバグを見つけ出し修正を促す行為は、システムを「清浄」に保つための神聖なプロセスなのです。

2. 祭式をコードで理解する

神社の儀式はブラックボックスに見えますが、実は非常に堅牢なアーキテクチャで設計されています。 ここでは、神職が奉仕する「家内安全祈願祭」を、あえてエンジニアの皆様に馴染み深いコードに置き換えて紹介したいと思います。

身体的プロトコル:「進左退右」というコーディング規約

コードに入る前に、マニアックな仕様の話をさせてください。 神職が祭場を歩くとき、「進左退右(しんさたいゆう)」という厳格なルールがあります。「進むときは左足から、退くときは右足から」という決まりです。

「どっちでもいいだろ」と思われるかもしれませんが、これは「オペレーションミスによる障害」を防ぐための物理的な制約です。 上位(神前)に近い足を軸にすることで、体の向きが安定し、神主同士がぶつかったり(コンフリクト)、転んだり(クラッシュ)、神様に背中を向けたり(要求定義からの逸脱)するリスクを最小化しています。祭式とは、人間の身体動作に実装されたコーディング規約なのです。

Rubyによる式次第の実装

では、祭りの進行表である「式次第」をRubyのコードに落とし込みます。 原文のプロセスとコードのロジックが完全に1対1で対応している点にご注目ください。

【原文】家内安全祈願祭 式次第

  • 参進(さんしん):祭場へ入る
  • 修祓(しゅばつ):心身を祓い清める
  • 献饌(けんせん):お供え物を捧げる
  • 祝詞奏上(のりとそうじょう):願い事を申し上げる
  • 玉串拝礼(たまぐしはいれい):拝礼する
  • 撤饌(てっせん):お供え物を下げる
  • 退下(たいげ):祭場から退く

【コード】KanaiAnzenService.rb

class KanaiAnzenService
  include ShintoProtocols

  # 祭事は不可分な一連の処理であるため、トランザクションとして扱う
  def execute(devotee:)
    ActiveRecord::Base.transaction do
      begin
        # 1. 参進(さんしん)
        # ※Linter: 進左退右プロトコルに準拠すること
        enter_shrine(devotee, step_strategy: :shin_sa_tai_u)

        # 2. 修祓(しゅばつ):初期化・クリーンアップ
        # ※ここが最重要。穢れ(Bug/Dirty Data)が残っていると後の処理は全て無効
        unless perform_purification(target: devotee, level: :strict)
          raise UncleannessError, "祓い清めが不十分です。儀式を続行できません。"
        end

        # 3. 献饌(けんせん):リソースロード
        # 依存関係順(米→酒→塩→水)にロードしないとエラーになる
        offer_shinsen(type: :sake_and_food, order: :strict)

        # 4. 祝詞奏上(のりとそうじょう):コアロジック実行
        # 言霊パラメータを神界へ送信
        invoke_norito(type: :kanai_anzen, target: devotee)

        # 5. 玉串拝礼(たまぐしはいれい):ユーザーインタラクション
        # ユーザー(参拝者)自身によるコミット操作
        guide_tamagushi_worship(devotee)

        # 6. 撤饌(てっせん):リソース解放
        withdraw_shinsen

      rescue UncleannessError => e
        # 異常系:儀式の中断。再浄化(Fix & Re-test)が必要
        handle_kegare(e)
        raise e
      ensure
        # 7. 退下(たいげ):プロセス終了
        # 成功失敗に関わらず、最後は退室する
        exit_shrine
      end
    end
  end
end

transaction ブロックで囲んでいるのがポイントです。お祓いに失敗したまま祝詞だけ読んでも、それは儀式として成立しません(コミットされません)。実際の祭りでもロールバック(やり直し)されます。ACID特性が求められるのです。

3. 祝詞の仕様定義:Gherkin

次に、祭儀の核となる「祝詞(のりと)」です。 祝詞は、「誰が」「どういう状態で」「何をすれば」「どうなるか」を宣言するものです。これは、振る舞い駆動開発(BDD)における Gherkin記法 そのものです。

「家内安全」の祝詞の原文と、それをGherkinに翻訳したものを並べてみます。

【原文・Gherkin対応】

Feature: 家内安全 (Family Safety)
  In order to 平穏無事な生活を送るため
  As a 氏子/崇敬者
  I want to 神の加護を得て災いを防ぐ

  Scenario: 祓い清められた状態で祈願を行う

    # 【原文】掛けまくも畏き(かけまくもかしこき)XX神社の大前(おおまえ)に
    # 【意味】言葉にするのも畏れ多い、XX神社の神様の御前にて
    Given ターゲット環境は "XX神社本番環境" である

    # 【原文】宮司 氏名 恐み恐みも白す(かしこみかしこみもまおす)
    # 【意味】宮司の[氏名]が、謹んで申し上げます
      And 実行ユーザー(Actor)は "認定神職" である

    # 【原文】此の氏子(うじこ)◯◯の家族(やから)一同
    # 【意味】この氏子である[住所][氏名]の家族全員が
      And 対象オブジェクトは "氏子[住所][氏名]のFamilyクラス全インスタンス" である

    # 【原文】過ち犯す事無く 災い触れる事無く
    # 【意味】過ちを犯すことなく、災難に遭うこともなく
    When "HumanError" および "ExternalDisaster" イベントが発生しない条件を適用し

    # 【原文】夜の守り 日の守りに守り恵み給い
    # 【意味】昼も夜も守り恵んでいただき
      And "24/365" の監視体制(Protection)を有効化し

    # 【原文】平らけく安らけく 孫の代に至るまで
    # 【意味】平穏無事に、子孫の代に至るまで
    Then システム稼働状況は "平らけく安らけく(Stable)" であること
      And データ永続性は "孫の代(LongTerm)" まで保証されること

    # 【原文】家門(いえかど)高く広く 立ち栄えしめ給へと 恐み恐みも白す
    # 【意味】家がますます繁栄しますようにと、謹んで申し上げます
      And "家門(BusinessValue)" は右肩上がりにスケールすること

「言霊(ことだま)」とは、言葉にしたことが現実になるという信仰ですが、エンジニアリングにおいても「コード(言語)にしたことが実際の挙動になる」という点は全く同じです。 正確な言葉(コード)で記述しなければ、システムは意図通りに動きません。

4. 式年遷宮という名の「大規模リプレース」

エンジニアの世界では、大規模なシステムリプレースのことを冗談交じりに「式年遷宮(しきねんせんぐう)」と呼ぶことがあります。 伊勢神宮で20年に一度行われる、社殿を新しく建て替える一大プロジェクトのことですね。

実はこれ、神道的な観点でも意図・目的が完全に一致しています。 式年遷宮の真の目的は、建物を新しくすることだけではありません。「宮大工の知識と技術の継承」こそが本質です。 20年というサイクルは、ベテランから若手へ技術を継承するのに最適な期間(世代交代の期間)として設計されています。

システムも同じです。どんなに堅牢なコードも、それを保守する技術者がいなくなれば(属人化すれば)いずれ朽ち果てます。 コードを書き直し、技術スタックを新しくすることは、システムという「神」を若返らせ、技術という「伝統」を次世代へ継承するための、聖なる営みなのです。

結びに代えて:「平らけく安らけく」の祈りを込めて

神道の祝詞によく登場する「平(たい)らけく安(やす)らけく」という言葉があります。 これは「波風が立たず、平穏であること」を願う言葉です。

私がQAエンジニアとしてリリース直前に願うこと。それは新機能の華々しい成功よりも先に、「システムがクラッシュせず、ユーザーにとって当たり前の日常(平穏)が守られますように」という祈りです。

しかし、神主の祈りは「神頼み」ではありません。 掃除をし、場を整え、身を清め(テストをし)、作法を極めた(バグを修正した)上で、最後の最後に捧げるのが祈りです。 「人事を尽くして天命を待つ」。このプロセスこそが品質保証の本質であり、それは「祭り」の準備と何ら変わりがないのです。

今年も残りわずかとなりました。 アドベントカレンダーという「祭り」を通じて、皆様と知見(?)を共有できたことを嬉しく思います。

来る新玉(あらたま)の年が、皆様そして皆様の開発するシステムにとって、バグなき清浄なものであり、弥栄(いやさか)に栄え、平らけく安らけくあらんことを、心よりご祈念申し上げます。

タイミーでは一緒に働く仲間を募集しています。ご興味があればぜひお話ししましょう!

プロダクト採用サイトTOP

カジュアル面談申込はこちら