Digital Reactor
機械学習

小規模チームのMLOps最小構成:Composerを入れず、Cloud Run jobsで4つの要件を満たす

小規模チームのMLOps最小構成:Composerを入れず、Cloud Run jobsで4つの要件を満たす

はじめに

MLOpsの解説は、大きなチームと大きなトラフィックを前提にしたものが多く、feature storeもオーケストレータも監視基盤も最初から揃える構成になりがちです。それを数人のチームがそのまま入れると、動かすより維持するほうに手を取られ、半年後には誰も触らないまま放置されます。

小規模のMLOps設計で軸になるのは、規模によらず保証すべき4点を最小のコストで満たし、常時課金と維持工数を避けることです。この記事は、その4点をクラウドに依存しない形で挙げ、実際に組んで動かした最小構成を示します。対象はバッチ推論です。オンライン推論、リアルタイム特徴量、LLMのサービングは対象から外します。これらは秒単位の応答や常時稼働が要件になり、設計の勘所がまるごと変わるためです。

対象読者:

  • モデルは作ったが、運用に載せきれていないスタートアップのCTO・データサイエンティスト
  • MLOpsを入れたいが、フル構成の維持コストに二の足を踏んでいる方
  • 外注先の運用設計を評価したい事業側の方

記事のポイント:

  • 規模によらず外せない4要件(データのバージョンの特定・再現性・出所・分布監視)を先に決め、そこから逆算します
  • Cloud Composerのような常時課金サービスを入れず、Cloud Run jobs+Schedulerで同じ要件を満たす構成を、実際に動かして示します
  • cron運用が壊れる場所(冪等性・リトライ・バックフィル)と、後から部品を足す/捨てる順番まで扱います

大きな構成をそのまま入れると、数人では回らない

典型的なフル構成には、feature store、ワークフローのオーケストレータ、モデルレジストリ、監視基盤、自動再学習が並びます。どれも大きな運用では効きますが、小規模で問題になるのは初期構築より維持のほうです。オーケストレータのバージョン追従、権限設計の見直し、障害の切り分けといった作業が、動かし続けるかぎり毎月発生し、チームが小さいほど1人あたりの負担が重くなります。

自動化するかどうかは、感覚ではなく大小で決められます。自動化で浮く工数は、およそ「頻度 × 1回あたりの所要時間 × 期間」です。かかるコストは「構築 + 維持 × 期間」です。頻度が低ければ、前者はいつまでも後者に追いつきません。

損益分岐の模式図。自動化は初期構築ぶんだけ高い位置から始まり、維持で緩やかに増える。手作業は頻度ぶんの傾きで増え、高頻度なら早く自動化と交わるが、低頻度では期間内に交わらない。

図の高頻度の手作業(赤)は、7〜8か月で自動化のコストを追い越します。ここまで来れば自動化が得です。一方、低頻度の手作業(緑)は運用期間の終わりまで自動化と交わりません。この場合、手作業を意図的に残すのが正しい設計です。月に1回しか動かさない処理のために、常時動くオーケストレータを入れるのは、維持工数を払い続けるだけになります。

以前、立ち上げ期のチームにオーケストレータとfeature storeを一式入れたことがあります。狙いは将来の拡張でしたが、実際にはDAGの定義とワークフローエンジンのバージョン上げに毎月手が取られ、肝心のモデル更新は月に1回どまりでした。半年たつ頃には誰もその基盤を触らなくなり、結局スケジューラ起動のジョブに戻しました。将来のために早めに入れた道具が、その将来が来る前に維持コストだけを積み上げます。小規模のチームでは、これが繰り返し起こります。

GCPの場合 マネージドサービスを使うと、自前で構築する手間は料金に肩代わりされます。ただし維持の工数(権限設計、バージョン追従、障害の切り分け)は自分たちに残ります。さらに小規模では、課金形態が常時か実行ごとかで損益がほぼ決まります。Cloud Composerは待機中も環境が動き続けるため、タスクを1つも実行しなくても最小構成で月数万円規模(2026年7月時点、継続稼働の小規模環境で概算)。対してCloud Run jobs+Cloud Schedulerは実行ごと課金で、月数回の学習なら月数百円未満のオーダーです。以降のGCP側の判断は、この桁差から決まります。(AWSなら常時課金のMWAA ≒ Composer、実行ごとのFargateタスク ≒ Cloud Run jobs)

規模によらず外せない四つ

入れるものを「何を自動化するか」で選ぶと、便利そうなものを足しすぎます。「何が保証されていないと障害になるか」で選ぶと、最小限に絞れます。規模によらず外せないのは次の4点です。

  1. データのバージョンが特定できる。 再学習で精度が落ちたとき、データが変わったのかコードが変わったのかを切り分けられること。
  2. 学習が再現できる。 本番のモデルを、同じ入力からもう一度作り直せること。
  3. 本番モデルの出所が追える。 いまどのモデルが動いているかを、誰かの記憶ではなく記録で断言できること。
  4. 入力・出力の分布の変化に気づける。 静かな劣化を、半年後ではなくその週に検知できること。

4番目の監視は、怠ったときの損失がいちばん大きくなります。あるモデルで、精度が少しずつ落ちているのに誰も気づかず、四半期のレビューで初めて発覚しました。原因は、入力データの上流仕様が変わり、ある特徴量の分布がずれていたことでした。モデルもコードも触っていないのに精度が落ちたため、原因の切り分けに時間がかかりました。もし分布監視が1つでも入っていれば、ずれた週にアラートが鳴り、調査は数時間で済んだはずです。

保証すべきこと起きる問題最小の実現手段
データのバージョンの特定データとコードのどちらが変わったか切り分けられない日付分割パス+読んだデータの内容ハッシュを記録
学習の再現本番モデルを作り直せないコンテナ・seed・ライブラリ版を固定して記録
モデルの出所どのモデルが動いているか誰も断言できない成果物に metadata.json を同梱、ポインタで現行を指す
分布の監視静かに劣化し、気づくのが遅れる基準分布とのずれをPSI(Population Stability Index)で週次監視

GCPの場合 いずれもストレージとクエリの従量課金だけで実現でき、数千件規模のデータなら月数百円のオーダーに収まります(2026年7月時点)。

  • データのバージョンの特定:GCSのオブジェクトバージョニングと日付分割パス。学習時に読んだオブジェクトの generation番号 を記録します。BigQuery起点ならタイムトラベル(既定7日、それ以上はスナップショット)で断面を固定します。
  • 再現性:コンテナをArtifact Registryに置き、タグではなく digestで固定 して記録します。タグは動くので出所になりません。
  • 出所:成果物をGCSに置き、metadata.json(git hash、data generation、image digest、seed)を同梱します。
  • 分布監視:基準統計をBigQueryのテーブルに保存し、スケジュールドクエリでPSIを計算、Cloud Monitoringのアラートで通知します。 (AWSなら S3バージョニング/ECRのdigest/S3+メタデータ/Athena+EventBridge)

この規模では入れないもの(と、入れる条件)

入れないものは「不要」なのではなく、「この規模ではコストが価値を超える」だけです。だから、それぞれに再導入のしきい値を添えておき、越えた時点で入れます。

  • feature store:特徴量を複数チーム・複数モデルで共有するようになったら、あるいはオンライン推論で低遅延の特徴量提供が要るようになったら。
  • 専用オーケストレータ:ステップが10を超え、依存の分岐が生まれ、部分再実行が日常になったら。
  • 自動再学習:週次以上で再学習し、データ品質のゲートを自動化できてから。順序が逆だと、汚れたデータで自動的にモデルを悪くします。
  • 常時稼働のオンライン推論基盤:秒単位の応答が要件になったら。バッチで足りるうちは要りません。
  • 実験管理SaaS:試行が数百件に達し、手作業で目的の実験を探せなくなったら。

「それなら最初からフル構成で組んだほうが、あとで移行しなくて済むのでは」という反論は当然あります。これには次の節で答えます。先に結論だけ言えば、最小構成の価値は各部品を独立に捨てられる点にあり、最初にフルで固めると、合わなかったときに捨てる判断のほうが重い負担になります。

GCPの場合

  • Cloud Composerは最初に入れません。 常時稼働で月数万円規模に加え、Airflowの追従という維持工数がかかります。代わりはCloud Run jobs+Cloud Schedulerです。
  • Vertex AI Pipelinesも急ぎません。 実行ごと課金でComposerより小規模向きですが、KFPでパイプラインを記述するコストが増えます。ステップ単位のキャッシュや部分再実行が欲しくなってからで十分です。
  • Vertex AI Feature Store は、BigQueryのテーブル関数で特徴量の定義を一元化すれば当面は代替できます。
  • Vertex AI Model Monitoring も、スケジュールドクエリで自前実装できます。モデルとエンドポイントが増えてから移せばよいです。
  • GKE はこの規模では選びません。運用の面倒がクラスタごと増えます。 常時課金型(Composer、常時起動のエンドポイント、GKE)が、小規模で最初に見るべきコスト項目です。

実際に組んだ最小構成

構成はひとつながりです。データ取得から前処理、学習、成果物の保存、バッチ推論、分布監視までを1本の流れにし、1リポジトリ・1コンテナに収めてスケジューラから起動します。

最小構成のアーキテクチャ。Schedulerが日付を渡してパイプラインを起動し、成果物はcurrent.jsonで現行を指し、実験ログはruns.jsonlに1行ずつ、バッチ推論のあとに週次PSIで監視してアラートを出す。

クラウドに依存しない要点は、次の5つです。

  • git hash、データのバージョン、seed、ライブラリ版を run_metadata としてまとめ、成果物に同梱します。
  • 成果物はタイムスタンプ付きのディレクトリに置き、current ポインタの書き換えで現行を切り替えます。ロールバックはポインタを戻すだけです。
  • 実験ログは1試行1行で追記し、あとで検索・集計できる最小の形にしておきます。
  • 分布監視は、学習時に固定した基準の分布と各週の分布のずれを、PSI(分布のずれを1つの数値にまとめた指標)として週次で見ます。
  • ジョブは冪等に作ります。

cron運用は冪等性・リトライ・バックフィルで壊れる

スケジューラ起動の運用で障害の原因になりやすいのは、冪等性・リトライ・バックフィルの3点です。順に見ます。

まず冪等性です。ジョブは「同じ日付の引数で再実行しても結果が同じ」になるように作ります。出力先を日付キーにして上書きすれば、二重起動や再送があっても結果は重複しません。私の構成では推論の出力を pred_YYYYMMDD.npz のように日付で固定し、同じ日付なら同じ場所へ書き直します。

リトライは、この冪等性とセットで初めて安全になります。ネットワークや上流の遅延といった一時的な失敗は自動リトライで吸収し、それでも駄目なときだけ通知します。同じ日付で何度流しても結果が変わらないので、再試行の副作用を気にせずに済みます。

バックフィルは、過去のある日を後から流し直す作業です。上流データの修正、監視の見落とし、初回の取り込み漏れといった場面で必ず必要になります。日付を引数で渡すだけで流し直せる形にしておけば、これは特別な作業になりません。逆に実行日時が暗黙に「今日」へ固定されていると、過去を再実行するたびにコードへ手を入れることになり、その手直しが障害の入り口になります。

この構成が4要件をどう満たすか

先に挙げた4要件と、構成の各部品の対応を並べます。

保証すべきことこの構成での満たし方
データのバージョンの特定日付分割パスに書き、読んだ内容のハッシュを metadata.json に記録
学習の再現seed・ライブラリ版・コンテナを固定し、run_metadata に残す
モデルの出所成果物に metadata.json を同梱、current.json が現行を指す
分布の監視学習時の基準統計に対する週次PSIをしきい値で監視

要点になるコードだけ挙げます。現行モデルをポインタ経由で読むロード関数、PSIを計算するBigQueryのSQL、実験ログの1行のスキーマです。デプロイのコマンド類は付随リポジトリのREADMEに置きました。

現行モデルは、current.json が指すディレクトリから読みます。切り替えもロールバックも、このポインタを書き換えるだけです。

def load_current_model():
    ptr = json.loads((MODEL_DIR / "current.json").read_text())
    sub = ptr["active"]                     # 例: "train_20260105"
    coef = np.load(MODEL_DIR / sub / "model.npz")
    clf = LogisticRegression()
    clf.classes_ = np.array([0, 1])
    clf.coef_, clf.intercept_ = coef["coef"], coef["intercept"]
    return clf, sub

分布監視は、学習時に固定した基準分布と当日の分布を突き合わせ、ビンごとの割合の乖離をPSIとしてまとめます。BigQueryのスケジュールドクエリで日次に回し、しきい値を超えた特徴量だけをアラートに送ります。

SELECT feature,
       SUM((actual_ratio - expected_ratio)
           * LN(actual_ratio / GREATEST(expected_ratio, 1e-4))) AS psi
FROM joined            -- 基準分布 expected と当日分布 actual を結合済み
GROUP BY feature
HAVING psi > 0.25      -- 要対応のしきい値を超えた特徴量だけ通知

実験ログは1試行1行のJSONです。学習も推論も同じスキーマで追記するので、あとから「いつ・どのデータのバージョンで・どのモデルが動いたか」を1つのテーブルで追えます。

{"kind": "train", "run_date": "2026-01-05", "git_hash": "1e7cbed",
 "data_version": "0717d6a344ef", "seed": 42,
 "image_digest": "sha256:...", "artifact": "train_20260105", "n_train": 4000}

分布がずれたときの挙動を例で確かめる

典型的な例として、入力分布が途中でずれるデータでこの構成を回してみます。学習は週0の基準分布で行い、以降は毎週バッチ推論しながらPSIを見ます。

週次PSIの推移。8週目に入力分布をずらすと、PSIはそれまでの0.01未満から立ち上がり、9週で警戒線(0.10)、10週で要対応線(0.25)を越える。

分布をずらす前の8週間、PSIは0.01を下回って動きませんでした。8週目にずらし始めると翌週から立ち上がり、10週目に要対応のしきい値(0.25)を越えてアラートが出ました。四半期のレビューを待たず、ずれた週のうちに気づけます。推論ジョブは日付キーで冪等に作ったので、同じ日を二度流しても出力は重複しませんでした。現行モデルの切り替えとロールバックは、current.json の1回の書き換えで済みました。

分布がずれると、モデルの精度だけでなく、判定に使う閾値の最適点も古くなります。その閾値を金額で引き直す考え方は、精度改善の事業価値を見積もる記事で扱っています。監視でずれを捕まえたら閾値を引き直す、という形で対にして回します。

費用は、実行ごと課金の料金モデルから見積もっています。この規模(数千件/日の推論、週次の学習)をCloud Run jobs+Cloud Schedulerで回すと、実行時間が短いため月数百円未満のオーダーに収まります(2026年7月時点、東京リージョン想定)。常時課金のオーケストレータを入れた場合の月数万円規模と比べると、桁が2つ違います。

GCPの場合

  • リポジトリは4点セット:Dockerfiletrain.pypredict.pymonitor.sql
  • Cloud BuildでビルドしてArtifact Registryへ。digestを記録します。
  • 学習・推論はCloud Run jobs。タスクタイムアウトはCPUなら最大7日まで延ばせるので、多少長い学習も収まります。ただしGPUを使う場合のタイムアウトは1時間まで(2026年7月時点)。数千件規模の短時間ならCloud RunのGPU(1インスタンス1GPU)で足り、長時間のGPU学習が要るならVertex AI custom trainingを選びます。GPUはVertex一択、と決め打たないほうがよいところです。
  • 起動はCloud Scheduler → Cloud Run jobs。日付をペイロードで渡すのが冪等性とバックフィルの実装点です。
  • 成果物は gs://…/models/{ts}/current.json の書き換えで切替とロールバック。
  • 実験ログはBigQueryの runs テーブル。metadata.json と同じスキーマにします。
  • 監視はスケジュールドクエリでPSIを計算し、しきい値超過をCloud Monitoringのアラートへ。通知先はSlack等のwebhookまで書きます。メール通知は埋もれて見られません。
  • 認証情報が要る場合はSecret Managerに置きます。
  • 権限はジョブごとに専用のサービスアカウントで最小権限に(学習SAはGCS書込+BQ読取、推論SAはBQ書込)。小規模でも最初にやります。障害が起きたときの影響範囲が、サービスアカウント単位で確定するからです。
  • IaCはこの規模でもTerraformを入れます。リソースが少ないうちに始めるほうが、あとから足すより安く済みます。最近は生成AIでTerraformの記述自体が速く書けるようになり、導入のハードルはさらに下がりました。「この規模では入れないもの」の原則に対する、意図的な例外です。

最初に壊れる箇所は、捨てやすく作ってあるか

最小構成は、いつか手狭になります。大事なのは、手狭になったときに部品を独立に足せて、古い自前実装を捨てられることです。経験上、壊れる(手狭になる)順番はだいたい決まっています。

  1. 学習と推論で特徴量が食い違う。学習で使った特徴量の作り方と、推論での作り方がずれる。
  2. ジョブの依存が複雑になる。分岐や部分再実行が増える。
  3. 実験ログが検索できなくなる。試行が増えてJSONの追記では追えない。
  4. 学習が1台のマシンに収まらなくなる。
  5. モデル数が増え、metadata.json の追跡が破綻する。
兆候追加するものその時点で捨ててよい自前実装
学習・推論で特徴量がずれる特徴量の共通化、やがてfeature store各スクリプトに散った特徴量生成
ジョブ依存の分岐・部分再実行ワークフローのオーケストレータSchedulerの直列起動
実験ログを検索できない実験管理ツールruns テーブルへの追記
学習が1台に収まらない分散・専用の学習基盤単一コンテナの学習
モデル数の増加で追跡が破綻モデルレジストリcurrent.json とメタデータ同梱

最小構成の値打ちは、安さそのものより、各部品を独立に捨てられることにあります。最初にフルスタックを固めてしまうと、合わなかった部品を抜くときに、絡んだ依存ごとほどく必要が出て、そこでかえって大きな代償を払うことになります。

GCPの場合

  1. 特徴量の乖離 → BigQueryのテーブル関数で定義を一元化 → さらにVertex AI Feature Store。
  2. ジョブ依存の複雑化 → Vertex AI Pipelines(実行ごと課金を維持)→ SLAや外部連携が絡んだらComposer。
  3. 実験ログ → Vertex AI Experiments、またはBigQueryのまま可視化を足す。
  4. 学習が1台に収まらない → Vertex AI custom training(コスト重視ならSpot VMも)。
  5. モデル数の増加 → Vertex AI Model Registry+Model Monitoring。 どの段階でも、移した先で不要になる自前実装をセットで捨てます。

まとめ

小規模のMLOpsで押さえるのは、保証すべき4点(データのバージョンの特定、再現性、出所、分布監視)を最小コストで満たすこと、常時課金と維持工数を避けること、各部品を独立に捨てられるように作ることです。常時課金のオーケストレータを入れる代わりにCloud Run jobs+Schedulerで組めば、費用は月数万円規模から月数百円未満のオーダーへ、桁で下がります。

もっとも、この構成が合わない場面もあります。秒単位の応答が要るオンライン推論、規制でマネージドな監査証跡が求められる領域、あるいはモデルとエンドポイントが数十を超える規模などです。ここまで来れば、常時課金を払ってでもマネージドに寄せる判断が正しくなります。自分の現場がまだその手前にいるなら、やることはひとつです。いま動いているモデルについて、データのバージョン・再現性・出所・分布監視の4点が記録で追えるかを点検し、追えない項目から順に、最小の手段で1つずつ埋めていくことです。

関連記事

← 技術ブログ一覧へ