【夏休みの自由研究リレー】 Snowflakeの外部関数からSageMaker AIの推論エンドポイントにデプロイしたChronos-2に時系列予測をリクエストする仕組みを作ってみた

【夏休みの自由研究リレー】 Snowflakeの外部関数からSageMaker AIの推論エンドポイントにデプロイしたChronos-2に時系列予測をリクエストする仕組みを作ってみた

Snowflakeの外部関数を活用して、SageMaker AI推論エンドポイントにデプロイされたAmazonの時系列予測モデル「Chronos-2」に時系列予測をリクエストする仕組みを作ってみました。
2026.07.31

こんにちは、データ事業本部の鈴木です。
当記事はクラスメソッドの有志による『夏休みの自由研究リレー』 第7回のエントリです。
このブログリレーの企画は、普段からクラウドやAIを追いかけているメンバーによって、「やってみた」だけではなく「作ってみた」や「調査/研究してみた」もアウトプットしてみようという企画です。

新たな知見になることは勿論、アイデアを自社やコミュニティの発展に寄与できればと考えておりますので、お付き合い頂けますと幸いです。
では、さっそくいってみましょう。今回の記事は『Snowflakeの外部関数からSageMaker AIの推論エンドポイントにデプロイしたChronos-2に時系列予測をリクエストする仕組みを作ってみた』です。

今回作ってみた仕組みについて

Chronos-2 は、ゼロショットで時系列予測ができる Amazon の時系列基盤モデルです。単変量だけでなく、多変量や共変量付きの予測にも対応しています。

https://www.amazon.science/blog/introducing-chronos-2-from-univariate-to-universal-forecasting
https://huggingface.co/amazon/chronos-2

Chronos-2 は、AutoGluon-Cloud また SageMaker JumpStart から SageMaker AI の推論エンドポイントをインフラとしてデプロイして利用できます。

AWS ではSageMaker AI の推論エンドポイントにデプロイするだけで非常に便利に使えるようになっていますが、 Snowflake に入っているデータも処理できるようにしてみたいなと思ったので、以下のように外部関数から Lambda 関数経由でリクエストする構成を調べて作ってみました。

今回の構成

Snowflake から AWS へのリクエストは、以下のように Snowflake の外部関数を使いました。

https://docs.snowflake.com/ja/sql-reference/external-functions-creating-aws

構築してみた

Chronos-2 向けの SageMaker AI の推論エンドポイントは SageMaker Jumpstart で作成しました。 HuggingFace で推奨と記載されている AutoGluon-Cloud だと、 Lambda 関数も AutoGluon-Cloud を使えるようにする必要があり、ライブラリが重たくなりそうだったので、 boto3 から推論エンドポイントにリクエストを送ることにしました。

1. Lambda パッケージを作成して S3 へ配置

API エンドポイントから実行する Lambda 関数を作成し、S3 バケットへアップロードしました。
S3 バケットは予め作成しておきました。

上記の通り、boto3 で環境変数に設定した名前のエンドポイントにリクエストする実装としました。

Lambda関数の実装例
"""Snowflake external function proxy for a JumpStart Chronos-2 endpoint."""

from __future__ import annotations

import base64
import hashlib
import json
import logging
import os
from typing import Any

import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

SAGEMAKER_ENDPOINT_NAME = os.environ.get("SAGEMAKER_ENDPOINT_NAME", "").strip()
runtime = boto3.client("sagemaker-runtime")

def _snowflake_error(status_code: int, message: str) -> dict[str, Any]:
    body = json.dumps({"data": [], "error": message})
    return {
        "statusCode": status_code,
        "body": body,
        "headers": {"Content-Type": "application/json"},
    }

def _snowflake_success(rows: list[list[Any]]) -> dict[str, Any]:
    body = json.dumps({"data": rows})
    md5digest = hashlib.md5(body.encode("utf-8")).digest()
    return {
        "statusCode": 200,
        "body": body,
        "headers": {
            "Content-Type": "application/json",
            "Content-MD5": base64.b64encode(md5digest).decode("ascii"),
        },
    }

def _parse_event_body(event: dict[str, Any]) -> dict[str, Any]:
    raw_body = event.get("body")
    if raw_body is None:
        raise ValueError("Request body is missing")

    if event.get("isBase64Encoded"):
        raw_body = base64.b64decode(raw_body).decode("utf-8")

    return json.loads(raw_body)

def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
    try:
        if not SAGEMAKER_ENDPOINT_NAME:
            raise ValueError("SAGEMAKER_ENDPOINT_NAME is not set")

        rows = _parse_event_body(event)["data"]
        prediction_length, freq = int(rows[0][4]), str(rows[0][5])

        inputs = []
        for row in rows:
            if int(row[4]) != prediction_length or str(row[5]) != freq:
                raise ValueError("prediction_length and freq must match within a batch")
            inputs.append(
                {
                    "item_id": str(row[1]),
                    "start": str(row[2]),
                    "target": [float(value) for value in row[3]],
                }
            )

        payload = {
            "inputs": inputs,
            "parameters": {
                "prediction_length": prediction_length,
                "freq": freq,
            },
        }
        response = runtime.invoke_endpoint(
            EndpointName=SAGEMAKER_ENDPOINT_NAME,
            ContentType="application/json",
            Accept="application/json",
            Body=json.dumps(payload).encode("utf-8"),
        )
        forecasts = json.loads(response["Body"].read())["predictions"]

        output_rows = []
        for row, input_data, forecast in zip(rows, inputs, forecasts, strict=True):
            forecast.setdefault("item_id", input_data["item_id"])
            output_rows.append([int(row[0]), forecast])

        return _snowflake_success(output_rows)
    except Exception as exc:
        logger.exception("Chronos-2 external function failed")
        return _snowflake_error(400, str(exc))

後に利用する CloudFormation テンプレートと合わせて、以下のように配置しました。

infra
├── cloudformation.yaml
└── lambda
    ├── build.sh
    └── handler.py

以下のように S3 バケットに実装をアップロードしました。

chmod +x infra/lambda/build.sh
./infra/lambda/build.sh  \
    <Lambdaコードアップロード先S3バケット> \
    lambda/chronos2-external-function.zip \
    <プロファイル>

アップロードしたLambda関数コード

2. 推論エンドポイントのデプロイ

続いて、先に Chronos-2 を推論エンドポイントにデプロイしておきました。

SageMaker Studio のコンソールから JumpStart を開いて chronos-2 を検索・選択しました。

chronos-2を選択する

Deployボタンを押しました。

Deployボタン

エンドポイントの設定では、今回はサポートされていた中で一番料金が安いml.c5.xlargeを選びました。より高いスループットを求める場合はインスタンスサイズを上げたり、アクセラレーター付きのインスタンスを選択することも可能でした。
エンドポイント名はカスタムの名前を設定しました。

エンドポイントの設定1

それ以外はデフォルトでデプロイしました。

デプロイしたエンドポイント

エンドポイント名はカスタムの名前を設定したものの、デプロイ日時を含めたエンドポイント名になっているようでした。

3. API Gateway・Lambda関数のデプロイ

以下の CloudFormation テンプレートをデプロイしました。

CloudFormationテンプレート例
AWSTemplateFormatVersion: "2010-09-09"
Description: API Gateway and Lambda proxy for Snowflake external functions calling Chronos-2 on SageMaker.

Parameters:
  SageMakerEndpointName:
    Type: String
    Description: Name of the Chronos-2 realtime endpoint deployed from SageMaker JumpStart.

  LambdaExecutionRoleName:
    Type: String
    AllowedPattern: "^[a-zA-Z0-9+=,.@_-]+$"
    Description: IAM role name used by the Lambda function.

  LambdaCodeS3Bucket:
    Type: String
    Description: S3 bucket containing the Lambda deployment package.

  LambdaCodeS3Key:
    Type: String
    Default: lambda/chronos2-external-function.zip
    Description: S3 object key for the Lambda deployment package.

  apiGatewayStageName:
    Type: String
    AllowedPattern: "^[-a-z0-9]+$"
    Default: ext-func-stage
    Description: API deployment stage.

  lambdaName:
    Type: String
    AllowedPattern: "^[a-zA-Z0-9]+[-a-zA-Z0-9-]+[-a-zA-Z0-9]+$"
    Default: chronos2-ext-func-lambda
    Description: Lambda function name.

  apiGatewayType:
    Type: String
    Default: REGIONAL
    AllowedValues:
      - REGIONAL
      - PRIVATE
    Description: API Gateway endpoint type.

  apiGatewayName:
    Type: String
    AllowedPattern: "^[a-zA-Z0-9]+[-a-zA-Z0-9-]+[-a-zA-Z0-9]+$"
    Default: chronos2-ext-func-api
    Description: API Gateway instance name.

  apiGatewayIAMRoleName:
    Type: String
    AllowedPattern: "^[a-zA-Z0-9]+[-a-zA-Z0-9-]+[-a-zA-Z0-9]+$"
    Description: IAM role assumed by Snowflake for API Gateway invocation.

  sourceVpcId:
    Type: String
    Default: ""
    Description: Snowflake VPC ID. Required only for PRIVATE API Gateway.

Conditions:
  shouldCreateRegionalGateway:
    !Equals [!Ref apiGatewayType, REGIONAL]

Resources:
  apiIAMRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref apiGatewayIAMRoleName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
            Action: sts:AssumeRole

  lambdaExecutionIAMRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref LambdaExecutionRoleName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: InvokeChronos2Endpoint
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - sagemaker:InvokeEndpoint
                Resource: !Sub "arn:aws:sagemaker:${AWS::Region}:${AWS::AccountId}:endpoint/${SageMakerEndpointName}"

  lambdaFunction:
    Type: AWS::Lambda::Function
    DependsOn:
      - lambdaExecutionIAMRole
    Properties:
      FunctionName: !Ref lambdaName
      Description: Snowflake external function proxy for Chronos-2 on SageMaker.
      Runtime: python3.12
      Handler: handler.handler
      Role: !GetAtt lambdaExecutionIAMRole.Arn
      Timeout: 29
      MemorySize: 2048
      Environment:
        Variables:
          SAGEMAKER_ENDPOINT_NAME: !Ref SageMakerEndpointName
      Code:
        S3Bucket: !Ref LambdaCodeS3Bucket
        S3Key: !Ref LambdaCodeS3Key

  apiGateway:
    Type: AWS::ApiGateway::RestApi
    DependsOn:
      - apiIAMRole
    Properties:
      Name: !Ref apiGatewayName
      Description: Snowflake external functions gateway for Chronos-2.
      Policy: !Sub
        - |
          {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Effect": "Allow",
                "Principal": {
                  "AWS": "arn:aws:sts::${AWS::AccountId}:assumed-role/${apiGatewayIAMRoleName}/snowflake"
                },
                "Action": "execute-api:Invoke",
                "Resource": "${resourceArn}",
                "Condition": { ${vpcCondition} }
              }
            ]
          }
        - resourceArn: !Join ["", ["execute-api:/", "*"]]
          vpcCondition: !If
            - shouldCreateRegionalGateway
            - ""
            - !Sub '"StringEquals": { "aws:sourceVpc": "${sourceVpcId}" }'
      EndpointConfiguration:
        Types:
          - !Ref apiGatewayType

  apiResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref apiGateway
      ParentId: !GetAtt
        - apiGateway
        - RootResourceId
      PathPart: chronos2

  apiGatewayRootMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      AuthorizationType: AWS_IAM
      HttpMethod: POST
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt lambdaFunction.Arn
      ResourceId: !Ref apiResource
      RestApiId: !Ref apiGateway

  apiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - apiGatewayRootMethod
    Properties:
      RestApiId: !Ref apiGateway
      StageName: !Ref apiGatewayStageName

  lambdaApiGatewayInvoke:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt lambdaFunction.Arn
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/*/*/*

Outputs:
  ResourceInvocationUrl:
    Description: Resource invocation URL for CREATE EXTERNAL FUNCTION.
    Value: !Sub https://${apiGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}/chronos2

  ApiGatewayIAMRoleArn:
    Description: IAM role ARN for Snowflake CREATE API INTEGRATION.
    Value: !GetAtt apiIAMRole.Arn

  LambdaExecutionRoleArn:
    Description: IAM role ARN used by the Lambda function.
    Value: !GetAtt lambdaExecutionIAMRole.Arn

  LambdaFunctionName:
    Description: Lambda function name.
    Value: !Ref lambdaFunction

AWS CLIで以下のようにデプロイしました。

aws cloudformation deploy \
  --profile <プロファイル> \
  --template-file infra/cloudformation.yaml \
  --stack-name chronos2-snowflake-ext-func \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    SageMakerEndpointName=<デプロイした推論エンドポイント> \
    LambdaExecutionRoleName=<Lambda実行ロール> \
    LambdaCodeS3Bucket=<Lambdaコードアップロード先S3バケット> \
    LambdaCodeS3Key=lambda/chronos2-external-function.zip \
    apiGatewayIAMRoleName=<APIゲートウェイ向けIAMロール>

API ゲートウェイをトリガーとした Lambda 関数をデプロイできました。

デプロイされたLambda関数

4. Snowflake側の準備

続いてSnowflake側の環境の準備をしました。

i. 外部接続の作成

外部接続を作成しました。

USE ROLE ACCOUNTADMIN;

CREATE OR REPLACE API INTEGRATION chronos2_api_integration
  API_PROVIDER = aws_api_gateway
  API_AWS_ROLE_ARN = '<ApiGatewayIAMRoleArn>'
  API_ALLOWED_PREFIXES = ('<ResourceInvocationUrl prefix without /chronos2>')
  ENABLED = TRUE;

-- SYSADMINから使うことを仮定
GRANT USAGE ON INTEGRATION chronos2_api_integration TO ROLE SYSADMIN;

<ResourceInvocationUrl prefix without /chronos2>はCloudFormationで作ったResourceInvocationUrl OUTPUTから/chronos2を取ったものです。

ii. API Gateway 用 IAM ロールの信頼ポリシーの更新

DESC INTEGRATION chronos2_api_integration;API_AWS_IAM_USER_ARNAPI_AWS_EXTERNAL_ID を取得しました。

Snowflake から API Gateway が実行できるように、以下のように API Gateway 用 IAM ロールの信頼ポリシーを更新しました。

#!/usr/bin/env bash
set -euo pipefail

ROLE_NAME="$1"
SNOWFLAKE_IAM_USER_ARN="$2"
SNOWFLAKE_EXTERNAL_ID="$3"
AWS_ACCOUNT_ID="$4"
AWS_PROFILE_NAME="${5:-}"

TRUST_POLICY="$(cat <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "${SNOWFLAKE_IAM_USER_ARN}"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "${SNOWFLAKE_EXTERNAL_ID}"
        }
      }
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::${AWS_ACCOUNT_ID}:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
)"

AWS_ARGS=(iam update-assume-role-policy
  --role-name "${ROLE_NAME}"
  --policy-document "${TRUST_POLICY}"
)

if [[ -n "${AWS_PROFILE_NAME}" ]]; then
  AWS_ARGS+=(--profile "${AWS_PROFILE_NAME}")
fi

aws "${AWS_ARGS[@]}"

echo "Updated trust policy for role ${ROLE_NAME}"

以下のように実行して、信頼ポリシーを更新しました。

chmod +x ./scripts/update_iam_trust.sh
./scripts/update_iam_trust.sh \
  <APIゲートウェイ向けIAMロール> \
  <取得したAPI_AWS_IAM_USER_ARNの> \
  <取得したAPI_AWS_EXTERNAL_IDの> \
  <AWS_ACCOUNT_ID> \
  <プロファイル>

iii. 外部関数の作成

以下のように外部関数を作成しました。

USE ROLE SYSADMIN;
USE DATABASE <database_name>;
USE SCHEMA <schema_name>;

CREATE OR REPLACE EXTERNAL FUNCTION chronos2_forecast(
  item_id VARCHAR,
  start_ts VARCHAR,
  target ARRAY,
  prediction_length NUMBER,
  freq VARCHAR
)
RETURNS VARIANT
API_INTEGRATION = chronos2_api_integration
AS '<ResourceInvocationUrl>';

作成した外部関数

iv. デモデータのアップロードと実行向けの集約

AutoGluon-Cloud のガイドの Zero-shot forecasting で紹介されているサンプルデータをダウンロードしました。

https://auto.gluon.ai/dev/tutorials/timeseries/forecasting-chronos.html

CHRONOS2_RAWテーブルとしてアップロードしました。

アップロードしたRAWデータ

外部関数経由で Chronos-2 に連携するため、以下のように3系列分の集約データを作成しました。
系列ごとに最新の512点をリストに集約し、そのうち3系列を取得しました。

CREATE OR REPLACE TABLE chronos2_demo_series AS
WITH ranked AS (
  SELECT
    item_id,
    timestamp,
    target,
    ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY timestamp DESC) AS rn_desc
  FROM chronos2_raw
),
limited AS (
  SELECT
    item_id,
    timestamp,
    target
  FROM ranked
  WHERE rn_desc <= 512
),
ordered AS (
  SELECT
    item_id,
    timestamp,
    target,
    ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY timestamp ASC) AS rn_asc
  FROM limited
),
aggregated AS (
  SELECT
    item_id,
    MIN(timestamp)::VARCHAR AS start_ts,
    ARRAY_AGG(target) WITHIN GROUP (ORDER BY rn_asc) AS target
  FROM ordered
  GROUP BY item_id
  HAVING COUNT(*) >= 48
)
SELECT *
FROM aggregated
ORDER BY item_id
LIMIT 3;

サンプルデータ

5. 外部関数から推論を実行してみる

作成したデモデータを外部関数経由で推論エンドポイントにリクエストし、推論できることを確認しました。

CREATE OR REPLACE TABLE CHORONOS_2_RESULT
AS
SELECT
  item_id,
  chronos2_forecast(
    item_id,
    start_ts,
    target,
    48,
    '30min'
  ) AS forecast
FROM chronos2_demo_series;

Chronos-2による推論結果

推論結果は CoCo に可視化してもらいました。 ipynb ファイルで直ぐに可視化結果を表示してくれるので非常に便利です。

入力系列と推論結果の可視化

予測も入力データを投入しただけですが、良い感じにできていますね!

最後に

以上、『夏休みの自由研究リレー』の第7回のエントリで、 Snowflake の外部関数から SageMaker AI の推論エンドポイントにデプロイした Chronos-2 に時系列予測をリクエストする仕組みを作ってみたご紹介でした。

なお、今回 API Gateway の REST API と Lambda の統合を使っているため、 Lambda がリクエストした推論時間があまり長いとタイムアウトになるなど制約があります。

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html

もしこのような課題がある場合は、 Snowflake から S3 にデータをエクスポートできるため、非同期な推論の仕組みにするなど工夫が必要そうです。

また、今回はリージョンエンドポイントで試してみましたが、よりセキュアにするためにプライベートエンドポイントを使うことも可能な想定ですので、気になる方はご確認ください。

https://docs.snowflake.com/ja/sql-reference/external-functions-creating-aws-planning#label-external-functions-aws-endpoint-type

次回もこの記事に関連して、私が Chronos-2 を SageMaker AI 推論エンドポイントにデプロイする方法を調査したためご紹介します。


Snowflake World Tour Tokyo 2026に参加しませんか?

Snowflakeの国内最大級イベント「Snowflake World Tour Tokyo」が2026年9月10日(水)・11日(木)にグランドプリンスホテル新高輪にて開催されます。
最新のAI・データ活用事例やライブデモを体感できる無料イベントです。

Snowflake World Tour Tokyoイベントに参加する


Snowflakeの導入支援はクラスメソッドに!

クラスメソッドでは Snowflake の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

Snowflakeの詳細を見る

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事