SageMaker AI 现在支持托管 MLflow Server,但默认的访问方式要么走 SageMaker Studio 内部,要么依赖 AWS IAM 策略直接暴露端点——两者都不适合直接面向团队内部的非 AWS 用户。这篇文章给出了一套完整方案:React 前端嵌入 MLflow UI,Flask 反向代理自动签注 SigV4 请求,整栈用 CDK 一键部署。下面逐层拆开来看。
整体架构:三层分工
核心思路是把"谁在访问"和"AWS 怎么认证"这两件事拆到不同层:
| 层 | 职责 | 技术 |
|---|---|---|
| 前端门户 | 展示 MLflow UI、团队导航、权限提示 | React SPA |
| 反向代理 | 拦截所有 /mlflow/* 请求,补签 SigV4,转发到 SageMaker MLflow 端点 |
Flask + boto3 |
| SageMaker MLflow | 官方托管,存储实验/模型数据 | SageMaker AI MLflow App |
前端不碰 AWS 凭证,所有签名在代理层完成。这样前端团队只需要关心 UI,后端团队只需要保证代理服务的 IAM 角色有 sagemaker:CreatePresignedMlflowTrackingServerUrl 等权限。
Flask 反向代理:SigV4 签名的关键细节
Flask 层做两件事:
- 接收前端请求——路径前缀
/mlflow/映射到 SageMaker MLflow 端点。 - 签注 SigV4——用 boto3 的
SigV4Auth对每个出站请求的 header 和 query string 重新签名,然后转发。
下面是一个可改造运行的 Flask 代理核心示例:
# app.py — Flask 反向代理核心逻辑(简化版,生产环境需加错误处理与日志)
import os
import requests
from flask import Flask, request, Response
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from boto3 import session
app = Flask(__name__)
# 从环境变量或 CDK 注入
MLFLOW_ENDPOINT = os.environ["MLFLOW_ENDPOINT"] # 例如 https://xxxx.sagemaker.aws/mlflow
REGION = os.environ["AWS_REGION"] # 例如 us-east-1
SERVICE = "sagemaker"
boto_session = session.Session()
credentials = boto_session.get_credentials()
sigv4 = SigV4Auth(credentials, SERVICE, REGION)
def sign_and_forward(method: str, path: str, headers: dict, body: bytes | None) -> Response:
"""对原始请求做 SigV4 签名后转发到 SageMaker MLflow 端点"""
url = f"{MLFLOW_ENDPOINT}{path}"
# 构造 botocore AWSRequest 以便 SigV4Auth 签注
aws_req = AWSRequest(method=method.upper(), url=url, data=body)
# 把前端带来的关键 header 搬过来(排除 host 等)
for k, v in headers.items():
if k.lower() not in ("host", "content-length", "authorization"):
aws_req.headers[k] = v
sigv4.add_auth(aws_req)
# 用签注后的 header 发出真实请求
signed_headers = dict(aws_req.headers)
resp = requests.request(
method=method,
url=url,
headers=signed_headers,
data=body,
stream=True, # 流式转发,MLflow UI 有大文件下载
allow_redirects=False,
)
# 逐 header 透传,但去掉 hop-by-hop header
excluded = {"transfer-encoding", "connection", "content-encoding"}
resp_headers = {
k: v for k, v in resp.raw.headers.items()
if k.lower() not in excluded
}
return Response(resp.content, status=resp.status_code, headers=resp_headers)
@app.route("/mlflow/<path:subpath>", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
def proxy_mlflow(subpath):
path = f"/mlflow/{subpath}"
if request.query_string:
path = f"{path}?{request.query_string.decode()}"
body = request.get_data()
return sign_and_forward(request.method, path, dict(request.headers), body)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
运行前需要改什么:
MLFLOW_ENDPOINT:换成你 SageMaker MLflow App 的真实端点 URL,可从 SageMaker 控制台或aws sagemaker describe-app获取。- IAM 角色:代理服务运行角色需要
sagemaker:CreatePresignedMlflowTrackingServerUrl和对 MLflow 端点的访问权限。 - 生产部署不要用
app.run(),放到 ECS/Fargate 或 Lambda + API Gateway 后面。
React 前端:iframe 嵌入与路径拼接
前端门户的核心就是把 MLflow UI 嵌入 iframe,所有 MLflow 路径指向本地代理而非 AWS 直连:
// MlflowPortal.tsx — React 组件,iframe 嵌入 MLflow
import React from "react";
const PROXY_BASE = "/mlflow"; // 指向 Flask 代理的同域路径
export default function MlflowPortal() {
// MLflow 默认首页是 /mlflow/,通过代理变成 /mlflow/(代理自己再拼上游路径)
const mlflowUrl = `${PROXY_BASE}/`;
return (
<div style={{ width: "100%", height: "calc(100vh - 64px)" }}>
{/* 顶部导航栏可放团队 logo、项目切换等 */}
<iframe
src={mlflowUrl}
title="MLflow Experiment Tracking"
style={{ width: "100%", height: "100%", border: "none" }}
sandbox="allow-same-origin allow-scripts allow-forms"
/>
</div>
);
}
关键点:iframe 的 src 指向 /mlflow/,浏览器请求会先到同域 Flask 代理,代理签注后转发到 SageMaker。前端完全不碰 AWS 签名逻辑。
如果 MLflow 内部链接跳转用了绝对路径(比如 /api/2.0/mlflow/runs/search),需要在代理层做路径重写,或者在 MLflow Server 配置 --artifact-root 和 --host 时把 base path 对齐。实际部署中,Flask 代理可以加一层 response.content 的文本替换来修正 MLflow 返回 HTML 里的硬编码路径。
CDK 部署:一栈搞定
整栈用 CDK 定义,核心资源包括:
- SageMaker MLflow App:
CfnApp或通过aws sagemaker create-app创建。 - Flask 代理:Fargate 服务 + NLB,或者 Lambda + API Gateway(轻量场景)。
- React 前端:S3 + CloudFront(静态托管),或者同 Fargate 服务用 Nginx 混合部署。
- IAM 角色:代理角色带 SigV4 签名权限。
下面是 CDK 核心片段(TypeScript):
// cdk-stack.ts — 关键资源定义(简化版)
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
export class MlflowPortalStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, "Vpc", { maxAzs: 2 });
// ---- Flask 反向代理:Fargate ----
const cluster = new ecs.Cluster(this, "Cluster", { vpc });
const proxyRole = new iam.Role(this, "ProxyRole", {
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
});
proxyRole.addToPolicy(
new iam.PolicyStatement({
actions: [
"sagemaker:CreatePresignedMlflowTrackingServerUrl",
"sagemaker:DescribeMlflowTrackingServer",
],
resources: ["*"], // 生产环境应限定到具体 MLflow Server ARN
})
);
const proxyTask = new ecs.FargateTaskDefinition(this, "ProxyTask", {
taskRole: proxyRole,
});
proxyTask.addContainer("flask-proxy", {
image: ecs.ContainerImage.fromAsset("./flask-proxy"), // 本地 Dockerfile 构建
environment: {
MLFLOW_ENDPOINT: "https://your-mlflow-endpoint.sagemaker.aws/mlflow",
AWS_REGION: this.region,
},
portMappings: [{ containerPort: 8080 }],
});
const proxyService = new ecs.FargateService(this, "ProxyService", {
cluster,
taskDefinition: proxyTask,
publicLoadBalancer: true, // NLB 对外暴露,生产建议加 WAF + 认证
});
// ---- React 前端:S3 + CloudFront ----
const frontendBucket = new s3.Bucket(this, "FrontendBucket", {
websiteIndexDocument: "index.html",
publicReadAccess: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const distribution = new cloudfront.Distribution(this, "FrontendDist", {
defaultBehavior: {
origin: new cloudfront.S3Origin(frontendBucket),
},
additionalBehaviors: {
"/mlflow/*": {
// MLflow 请求走 NLB → Flask 代理
origin: new cloudfront.LoadBalancerOrigin(proxyService.loadBalancer),
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
},
},
});
}
}
部署流程:
# 1. 构建 Flask 代理镜像(CDK fromAsset 会自动 docker build)
cd flask-proxy && docker build -t flask-proxy . && cd ..
# 2. 构建 React 前端并上传到 S3
cd react-frontend && npm run build && cd ..
# CDK 部署后用 aws s3 sync dist/ s3://<bucket-name>/ 上传静态文件
# 3. CDK 部署整栈
cdk bootstrap # 首次需要
cdk deploy # 等约 10-15 分钟(VPC + ECS + CloudFront)
# 4. 验证:浏览器打开 CloudFront 域名,应看到嵌入 MLflow UI 的门户
安全考量与边界
这套架构有几个必须注意的点:
-
代理层是唯一的信任边界——前端用户不持有 AWS 凭证,所有签名在代理完成。这意味着代理服务的 IAM 角色权限必须最小化,只给 MLflow 相关操作,不要附加
s3:*或其他宽泛权限。 -
CloudFront
/mlflow/*路径必须禁缓存——MLflow 的实验数据实时更新,缓存会导致 UI 显示过期数据。上面 CDK 片段已设CACHING_DISABLED。 -
iframe sandbox 属性——
allow-same-origin是必须的(否则 MLflow 内部 AJAX 请求会被浏览器拦截),但不要加allow-top-navigation,防止 MLflow 页面跳转劫持整个门户。 -
MLflow 内部路径重写——如果 MLflow Server 返回的 HTML/JS 里包含指向自身端点的绝对路径,代理需要做文本替换。一种做法是在 Flask 的
sign_and_forward里对text/html类型的 response body 做MLFLOW_ENDPOINT→/mlflow的字符串替换。 -
清理——CDK 销毁时
cdk destroy会删除 ECS、VPC、S3 bucket(如果设了DESTROYremoval policy)。但 SageMaker MLflow App 需要单独删除:
aws sagemaker delete-app \
--domain-id <your-domain-id> \
--app-type MLflowServer \
--app-name <your-mlflow-app-name> \
--user-profile-name <your-user-profile>
什么时候该用这套方案
| 场景 | 建议 |
|---|---|
| 团队全员在 SageMaker Studio 内工作 | 直接用 Studio 内置 MLflow,不需要代理 |
| 需要给非 AWS 用户(数据科学家、PM)看实验追踪 | 这套方案正好解决 |
| 已有内部门户/运维平台,想嵌入 MLflow | Flask 代理可以集成到现有 Nginx/网关后面 |
| 多团队多 MLflow Server | 代理层按路径前缀路由到不同端点,前端加项目切换下拉框 |
核心收益只有一个:让不懂 AWS 的人也能用 MLflow,同时不把 AWS 凭证暴露到前端。如果你的团队已经在 Studio 里工作,没必要加这层复杂度;但如果需要跨团队共享实验追踪面板,这套 React + Flask + SigV4 的组合是目前最干净的方案。