Claude CodeでMLflowのaliasを付け替え、本番昇格を安全に任せる
MLflow Model Registryのchampion・challenger aliasをClaude Codeに付け替えさせる手順。登録と検査は自動で通し、本番aliasの変更だけ確認に残す権限設計を書きます。
MLflow Model Registryでモデルを本番に出すとは、champion のようなaliasを新しいバージョンへ付け替えることです。この付け替えをClaude Codeに任せるなら、「どこまで自動で通し、どこで人が止めるか」を権限ルールで先に決めておく必要があります。
結論を先に書きます。aliasの操作はMLflowのPython APIで行うため、Bashの権限ルールからは中身が見えません。そこで操作を役割別の3本のスクリプトに割り、登録と検査は allow、本番aliasの変更だけ ask に置きます。評価指標が基準を超えたときだけ昇格できるよう、検査スクリプトの結果を昇格スクリプトが確かめる構成にします。
実験のrunを比べる手順はClaude CodeでMLflowの実験管理を組み込むで扱っています。この記事は、その先にあるレジストリ側の運用が主題です。トレースを使った評価はClaudeにMLflow MCPを繋いでトレース調査と評価ループを回すにあります。
aliasは「本番が指す名前付きポインタ」
Model Aliasは、登録モデルの特定バージョンに付ける、変更可能な名前付きの参照です。MyModel のバージョン1に champion を付ければ、models:/MyModel@champion というURIでそのバージョンを指せます。
本番の推論側はこのURIだけを読みます。aliasを別のバージョンへ付け替えれば、推論側のコードを触らずに配信モデルが切り替わります。MLflowのドキュメントは、この性質を「デプロイと推論ワークロードの切り離し」と説明しています。
つまり昇格の実体は、1回の set_registered_model_alias 呼び出しです。軽い操作ですが、本番の挙動が変わる点で影響は大きくなります。
古い資料にある Staging や Production のステージ遷移は、MLflow 2.9.0で非推奨になりました。置き換え先がaliasとモデルバージョンのタグです。この記事の構成もaliasを前提にしています。
この記事で使う2つのalias
challenger
学習後に登録した候補バージョンを指します。Claudeが自由に付け替えてよい側です。
champion
本番トラフィックが読むバージョンを指します。付け替えは人の確認を通します。
aliasの名前は固定の予約語ではありません。ドキュメントの例が champion を使うので、それに揃えています。チームで別の名前を使っているなら、スクリプトの定数を置き換えてください。Claudeのモデル名でaliasとバージョン固定のどちらを選ぶかという別の問いは、Claudeモデルの引退で自動化が止まる日で扱っています。
操作を3本のスクリプトに割る
Claude Codeの権限ルールは、Bashコマンドの文字列に対して効きます。python -c "client.set_registered_model_alias(...)" のような一行コードでは、読み取りなのか本番の付け替えなのか、ルールの側から区別できません。
区別できる形にするには、操作ごとにスクリプトの名前を分けます。
| スクリプト | やること | 権限 |
|---|---|---|
register_candidate.py | やることrunのモデルを登録し、challenger を付ける | 権限allow |
check_promotion.py | やること指標を比べ、バージョンのタグに結果を書く | 権限allow |
promote_champion.py | やること検査済みなら champion を付け替える | 権限ask |
3本とも scripts/mlflow/ に置きます。ファイル名がそのまま権限ルールの境界になるので、名前は用途が一目で分かるものにしておきます。
登録してchallengerを付ける
学習が終わったrunのモデルを、レジストリに登録します。mlflow.register_model() は、同名の登録モデルが無ければ新規作成してバージョン1を作り、あれば新しいバージョンを作ります。
# scripts/mlflow/register_candidate.py
import argparse
import mlflow
from mlflow import MlflowClient
p = argparse.ArgumentParser()
p.add_argument("--run-id", required=True)
p.add_argument("--name", required=True)
p.add_argument("--artifact-path", default="model")
a = p.parse_args()
mv = mlflow.register_model(f"runs:/{a.run_id}/{a.artifact_path}", a.name)
MlflowClient().set_registered_model_alias(a.name, "challenger", mv.version)
print(f"registered {a.name} v{mv.version} -> @challenger")このスクリプトが触るのは challenger だけです。誤って何度実行しても、本番が読むaliasには影響しません。だから allow に置けます。
指標が基準を超えたときだけ承認タグを付ける
検査スクリプトは、challenger と champion が指すバージョンを、それぞれの元のrunの指標で比べます。基準は「現行より min-gain 以上良いこと」です。
# scripts/mlflow/check_promotion.py
import argparse
import sys
from mlflow import MlflowClient
p = argparse.ArgumentParser()
p.add_argument("--name", required=True)
p.add_argument("--metric", default="val_auc")
p.add_argument("--min-gain", type=float, default=0.005)
a = p.parse_args()
client = MlflowClient()
def load(alias):
mv = client.get_model_version_by_alias(a.name, alias)
return mv, client.get_run(mv.run_id).data.metrics[a.metric]
cand, cand_score = load("challenger")
champ, champ_score = load("champion")
ok = cand_score >= champ_score + a.min_gain
client.set_model_version_tag(
a.name, cand.version, "validation_status",
"approved" if ok else "rejected",
)
print(f"{a.metric}: challenger v{cand.version}={cand_score:.4f} "
f"champion v{champ.version}={champ_score:.4f} -> "
f"{'approved' if ok else 'rejected'}")
sys.exit(0 if ok else 1)validation_status は、MLflowのドキュメントがモデルバージョンのタグの例に使っている名前です。終了コードも承認の有無に合わせて0か1にしてあるので、Claudeは出力の1行と終了コードだけで次の動作を決められます。
指標名は val_auc を既定にしました。実験側で記録している指標名に合わせ、CLAUDE.mdにも書いておくと、セッションをまたいでも同じ指標で比べられます。
本番aliasの付け替えは確認を残す
昇格スクリプトは、承認タグを確かめてから champion を付け替えます。付け替える前の champion のバージョンも出力に残し、戻すときの手がかりにします。
# scripts/mlflow/promote_champion.py
import argparse
import sys
from mlflow import MlflowClient
p = argparse.ArgumentParser()
p.add_argument("--name", required=True)
a = p.parse_args()
client = MlflowClient()
cand = client.get_model_version_by_alias(a.name, "challenger")
if cand.tags.get("validation_status") != "approved":
sys.exit("validation_status が approved ではないため中止します")
prev = client.get_model_version_by_alias(a.name, "champion")
client.set_registered_model_alias(a.name, "champion", cand.version)
print(f"champion: v{prev.version} -> v{cand.version}")ロールバックは、出力に出た旧バージョンへ同じ set_registered_model_alias を呼び直すだけです。ドキュメントにあるとおり、aliasは別のバージョンへ何度でも付け替えられます。
初回は champion がまだ無く、get_model_version_by_alias が例外になります。最初の champion だけは人が付けるか、スクリプトに初回用の分岐を足してください。
権限ルールで境界を引く
スクリプトの名前を決めたら、.claude/settings.json に3つの役割を書きます。
{
"permissions": {
"allow": [
"Bash(python scripts/mlflow/register_candidate.py *)",
"Bash(python scripts/mlflow/check_promotion.py *)"
],
"ask": [
"Bash(python scripts/mlflow/promote_champion.py *)"
],
"deny": [
"Bash(python -c *)"
]
}
}評価の順番はdeny、ask、allowです。最初に当たったものが結果を決め、ルールの具体性で順番が変わることはありません。同じコマンドにallowとaskの両方が当たるときは、askが勝って確認が出ます。
Bashルールの * は、空白を含む任意の文字列に当たります。末尾の * の前にある空白もルールの一部なので、promote_champion.py の後ろに付く --name などの引数だけを許し、別のファイル名は許しません。
連結コマンドでも確認は外れない
Claudeは check_promotion.py ... && promote_champion.py ... のように、検査と昇格を一行にまとめて実行することがあります。
Claude Codeはシェルの演算子を解釈します。&&、||、;、| などで区切られた各コマンドを個別に見て、allowで通すにはすべてが許可されている必要があります。一方、askとdenyは、どれか1つのサブコマンドが当たれば効きます。
つまり検査が通ったあとに続けて昇格を呼んでも、昇格のところで必ず確認が出ます。公式の説明では、askルールはauto modeでも確認を出します。確認を省いて昇格まで自動化したくなったときは、ruleを緩めるのではなく、昇格スクリプト側の検査条件を見直すのが筋です。
Bashルールは境界の全部ではない
ここで1つ、限界を押さえておきます。Bashルールが見るのは、Claudeが書いたコマンドの文字列です。同じプログラムを別の形で呼ぶ書き方までは止められません。
公式も、denyやaskのルールは「Claudeが普通に出す形を止めるもので、プログラムの周りのセキュリティ境界ではない」と書いています。例えば git push の拒否は git -C . push を止めません。
この記事の構成に当てはめると、python -c を拒否しても、別名のスクリプトを自分で書いて実行する経路は残ります。ルールを補う手が2つあります。
ルールの穴を補う2つの手
PreToolUseフック
実行前にコマンドの全文を自前のロジックで検査できます。終了コード2で止まるので、
scripts/mlflow/以外のPythonスクリプトの実行を拒否する、といった判定を書けます。MLflow側の権限
MLflow Authenticationを使うと、登録モデル単位で権限を設定できます。環境ごとに登録モデルを分け、Claudeが使う資格情報には本番側の書き込みを渡さない運用が取れます。
後者は、Claude Codeの設定とは独立した最後の砦です。トラッキングサーバーの認証を有効にしている環境では、まずこちらを検討する価値があります。
Claudeへの頼み方とCLAUDE.mdの規約
権限を設計したら、Claudeに渡す手順をCLAUDE.mdに書きます。権限ルールは「何ができるか」を決めるだけなので、「どの順で呼ぶか」は文章で渡します。
## モデル昇格(MLflow Model Registry)
- 学習後は register_candidate.py で challenger に登録する
- 昇格の前に必ず check_promotion.py を実行し、approved が出たときだけ進む
- rejected のときは昇格を試みず、指標の差を報告する
- champion の付け替えは promote_champion.py だけで行う(確認が出る)
- 指標名は val_auc。登録モデル名は churn-modelそのうえで、依頼は「run abc123をchurn-modelに登録して、基準を超えたら昇格して」と一文で足ります。Claudeは登録と検査を確認なしで進め、昇格の直前で止まります。画面に出るコマンドと検査の出力1行を見て、人が承認するかを決める流れです。
CLAUDE.mdの記述は、Claudeが何を試みるかを方向づけるだけです。許可と拒否はClaude Code側のルールが強制するので、規約と権限の両方を書く意味があります。
つまずきやすい点
昇格の自動化で踏みやすい点を、症状で並べます。
championが無くて検査が例外になる: 初回の昇格では比較相手がいません。最初のchampionは人が付けるか、初回用の分岐を足します。- 指標が
metricsに無い:get_run(...).data.metrics[...]は、実験側でlog_metricした名前だけを持ちます。学習スクリプトとスクリプトで名前を揃えてください。 - 推論側が古いバージョンのまま: 推論コードが
models:/name@championではなく、固定のバージョン番号を読んでいると、aliasを付け替えても配信は変わりません。 - タグの承認を信用しすぎる:
validation_statusはタグなので、誰でも書き換えられます。昇格スクリプトの検査は誤操作を防ぐもので、改ざん対策ではありません。
aliasでなく登録モデルを分けて昇格する方法
もう1つの方式として、環境ごとに登録モデルを分け、copy_model_version() で昇格する形があります。例えば regression-model-staging の candidate を、regression-model-production の最新バージョンとしてコピーします。
client.copy_model_version(
src_model_uri="models:/regression-model-staging@candidate",
dst_name="regression-model-production",
)この方式は、MLflow Authenticationで登録モデルごとに権限を分ける設計と相性が良くなります。Claudeにはステージング側の書き込みだけを渡し、本番側のコピーは別の資格情報を持つ人やCIに任せる形です。
どちらを選ぶかは、本番の権限をClaude Codeのルールで守るか、MLflowの認証で守るかの違いと言えます。サーバー側で権限を分けられるなら後者のほうが、Claudeの設定の外で効く分だけ強固です。分けられない環境では、この記事のスクリプトとaskルールの組み合わせが実用的な線になります。
まとめ
昇格を任せる設計の核は、スクリプトの名前で権限の境界を引くことでした。登録と検査は allow、champion の付け替えだけを ask に置けば、日常の反復は止まらず、本番が変わる瞬間にだけ人の目が入ります。
ただしBashルールは文字列の照合で、完全な境界ではありません。トラッキングサーバーの認証を使えるなら、本番側の書き込みをClaudeの資格情報から外すところまで進めると、設計が閉じます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。