Перейти к содержанию
Telegram
Публикация AiManual

Больше никаких ссылок в Slack: как встроить SageMaker MLflow в корпоративный портал с SSO

Пошаговое руководство, как встроить SageMaker MLflow в корпоративный портал с SSO через iframe и presigned URLs. Избавляемся от ручной раздачи ссылок для ML-ком

Коротко

Что будет в материале

  1. 01

    Проклятие presigned URL (и почему это не твоя вина)

  2. 02

    Как это работает в правильном мире (архитектура)

  3. 03

    Строим: пошаговый план без магии

  4. 04

    Подводные камни, на которых я обжёгся

Ты senior DevOps, твоя ML-команда выросла до 20 человек. Каждый раз, когда нужно глянуть эксперимент в MLflow, кто-то просит ссылку. Ты генерируешь presigned URL через AWS Console, кидаешь в Slack, через час ссылка истекает. И всё по новой. Знакомая боль?

💡
В 2026 году SageMaker MLflow Tracking Server — зрелая фича, но управление доступом через presigned URLs всё ещё напоминает костыль. Пора это automate.

Решение — встроить MLflow прямо в ваш внутренний портал, который уже привязан к корпоративному SSO. Никакой ручной раздачи ссылок. Один iframe, динамическая генерация presigned URL на лету, полный контроль доступа.

Проклятие presigned URL (и почему это не твоя вина)

Когда Amazon анонсировал нативный MLflow в SageMaker, все радовались. Интеграция с эксперимент-трекингом из коробки, Managed MLflow Tracking Server — красота. Но архитектура доступа построена на presigned URLs: AWS генерирует короткоживущую ссылку на UI сервера, которая валидна от 1 до 12 часов.

Проблема в масштабировании:

  • Ссылку нужно каждый раз обновлять.
  • Нельзя встроить в корпоративный портал — браузер блокирует кросс-доменные запросы.
  • Интеграция с SSO (Okta, Azure AD, Cognito) отсутствует.

Звучит логично, но на практике: аналитик просыпается, открывает портал, а там пустой iframe. Ссылка протухла. Никто не хочет писать бота для обновления presigned URL каждые 8 часов.

Как это работает в правильном мире (архитектура)

Нам нужен middleware, который берёт на себя генерацию presigned URL на основе сессии пользователя из SSO. Схема простая:

  1. Пользователь логинится в корпоративном портале через SSO (например, Cognito).
  2. Портал получает JWT токен.
  3. Фронтенд вызывает Lambda/API Gateway endpoint, передавая JWT.
  4. Lambda проверяет права (через IAM или SSO groups), генерирует новый presigned URL для MLflow Tracking Server.
  5. Портал вставляет iframe с этим URL.
  6. Каждые N минут фронтенд обновляет ссылку через фоновый запрос.

⚠️ Важно: presigned URL генерируется от имени IAM-роли Lambda, а не пользователя. Чтобы разграничить права (один видит только свои эксперименты, другой — все), нужно использовать STS AssumeRole с тегированием или отдельные MLflow Tracking Servers под каждую команду.

Строим: пошаговый план без магии

1 Создаём SageMaker MLflow Tracking Server

Если у тебя ещё нет — создай через AWS Console или CloudFormation. На 2026 рекомендую версию MLflow 2.18.x (последняя стабильная с поддержкой embed режима). Убедись, что стоит флаг AllowPresignedUrlEmbedding=true — это новая опция, появившаяся ещё в 2025.

aws sagemaker create-mlflow-tracking-server \
    --tracking-server-name mlflow-embed-demo \
    --mlflow-version 2.18.0 \
    --artifact-store-uri s3://mlflow-artifacts-123 \
    --role-arn arn:aws:iam::123456789012:role/SageMakerMLflowRole \
    --allow-presigned-url-embedding

После создания получишь TrackingServerArn. Запомни — он понадобится для генерации presigned URL.

2 Lambda-функция для генерации ссылок

Создаём Python Lambda с ролью, у которой есть права sagemaker:CreatePresignedMlflowTrackingServerUrl. Вот правильный код (никаких устаревших SDK — используем boto3 1.36+):

import boto3
import os
import json
from datetime import datetime, timezone

sagemaker = boto3.client('sagemaker')

def lambda_handler(event, context):
    # Получаем user из JWT (проверка делается на API Gateway level)
    claims = event['requestContext']['authorizer']['claims']
    user = claims.get('cognito:username', 'unknown')

    # Генерация presigned URL на 4 часа
    response = sagemaker.create_presigned_mlflow_tracking_server_url(
        TrackingServerName=os.environ['TRACKING_SERVER_NAME'],
        SessionExpirationDurationInSeconds=14400  # 4h
    )

    url = response['AuthorizedUrl']

    # Логируем для аудита
    print(f"[{datetime.now(timezone.utc).isoformat()}] User {user} generated URL")

    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': os.environ['ALLOWED_ORIGIN']
        },
        'body': json.dumps({'url': url})
    }

Обрати внимание: SessionExpirationDurationInSeconds — максимальное значение 43200 (12 часов). Для embedded приложений рекомендую 14400 (4 часа) и обновлять по таймеру.

3 API Gateway с авторизатором Cognito

Создаём HTTP API (или REST), интегрируем с Lambda. Включаем авторизатор Cognito — JWT токены. Важно: в настройках CORS укажи origin твоего портала (не звездочку, если хочешь безопасно).

# cloudformation template fragment
ApiGatewayApi:
  Type: AWS::ApiGatewayV2::Api
  Properties:
    Name: mlflow-presigned-api
    ProtocolType: HTTP
    CorsConfiguration:
      AllowOrigins: ["https://portal.mycompany.com"]
      AllowMethods: ["GET", "OPTIONS"]
      AllowHeaders: ["authorization", "content-type"]

Authorizer:
  Type: AWS::ApiGatewayV2::Authorizer
  Properties:
    JwtConfiguration:
      Audience: ["your-cognito-app-client-id"]
      Issuer: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPoolId}"

4 Фронтенд: iframe с refresh

В корпоративном портале (React, Vue, Angular — неважно) создаём компонент. Пример на React с хуком:

import { useState, useEffect, useRef } from 'react';

const MLflowEmbed = () => {
  const [url, setUrl] = useState(null);
  const [error, setError] = useState(null);
  const intervalRef = useRef(null);

  const fetchUrl = async () => {
    try {
      const token = await getIdToken(); // из Cognito SDK
      const res = await fetch('https://api.portal.com/mlflow-url', {
        headers: { 'Authorization': `Bearer ${token}` }
      });
      const data = await res.json();
      setUrl(data.url);
      setError(null);
    } catch (err) {
      setError('Не удалось получить доступ к MLflow. Обновите страницу.');
    }
  };

  useEffect(() => {
    fetchUrl();
    // Обновляем ссылку каждые 3 часа (10800 секунд)
    intervalRef.current = setInterval(fetchUrl, 10800000);
    return () => clearInterval(intervalRef.current);
  }, []);

  if (error) return 
{error}
; if (!url) return
Загрузка...
; return (