[Summer Vacation Independent Research Relay]
I built a mechanism to request time series forecasting from Chronos-2 deployed to a SageMaker AI inference endpoint via Snowflake external functions

[Summer Vacation Independent Research Relay] I built a mechanism to request time series forecasting from Chronos-2 deployed to a SageMaker AI inference endpoint via Snowflake external functions

I tried building a mechanism to request time series forecasting from Amazon's time series forecasting model "Chronos-2" deployed on a SageMaker AI inference endpoint, utilizing Snowflake's external functions.
2026.07.31

This page has been translated by machine translation. View original

Hello, I'm Suzuki from the Data Business Division.
This article is the 7th entry in the 'Summer Break Independent Research Relay' by volunteer members of Classmethod.
This blog relay is a project by members who regularly follow cloud and AI, aiming to output not just "tried it" but also "built it" and "researched/investigated it".

We hope this will not only provide new insights but also contribute to the development of our company and community, so we appreciate your continued reading.
Now, let's get started. Today's article is: 'Built a system to request time series forecasting from Chronos-2 deployed on a SageMaker AI inference endpoint via Snowflake external functions'.

About the system we built

Chronos-2 is Amazon's time series foundation model capable of zero-shot time series forecasting. It supports not only univariate but also multivariate and covariate-based forecasting.

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

Chronos-2 can be deployed and used with SageMaker AI inference endpoints as infrastructure via AutoGluon-Cloud or SageMaker JumpStart.

While AWS makes it very convenient to use by simply deploying to a SageMaker AI inference endpoint, I wanted to be able to process data stored in Snowflake as well, so I researched and built a configuration that sends requests via Lambda functions from external functions as shown below.

Configuration for this setup

For requests from Snowflake to AWS, I used Snowflake external functions as follows.

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

What we built

The SageMaker AI inference endpoint for Chronos-2 was created with SageMaker JumpStart. With AutoGluon-Cloud, which is listed as recommended on HuggingFace, Lambda functions would need to be configured to use AutoGluon-Cloud as well, which would make the library heavier, so I decided to send requests to the inference endpoint from boto3.

1. Create a Lambda package and place it in S3

I created a Lambda function to be executed from the API endpoint and uploaded it to an S3 bucket.
The S3 bucket was created in advance.

As mentioned above, the implementation uses boto3 to send requests to the endpoint whose name is set as an environment variable.

Lambda function implementation example
"""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))

Together with the CloudFormation template to be used later, the files were placed as follows.

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

The implementation was uploaded to the S3 bucket as follows.

chmod +x infra/lambda/build.sh
./infra/lambda/build.sh  \
    <S3 bucket name for Lambda code upload> \
    lambda/chronos2-external-function.zip \
    <profile name>

Uploaded Lambda function code

2. Deploy the inference endpoint

Next, I deployed Chronos-2 to the inference endpoint first.

I opened JumpStart from the SageMaker Studio console and searched for and selected chronos-2.

Select chronos-2

I pressed the Deploy button.

Deploy button

For the endpoint configuration, I chose ml.c5.xlarge, which was the cheapest among the supported options. For higher throughput, it was also possible to increase the instance size or select an instance with an accelerator.
A custom name was set for the endpoint name.

Endpoint configuration 1

Everything else was deployed with the defaults.

Deployed endpoint

Although a custom name was set for the endpoint name, it appeared that the endpoint name included the deployment date and time.

3. Deploy the API Gateway and Lambda function

I deployed the following CloudFormation template.

CloudFormation template example
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

Deployed using the AWS CLI as follows.

aws cloudformation deploy \
  --profile <profile name> \
  --template-file infra/cloudformation.yaml \
  --stack-name chronos2-snowflake-ext-func \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    SageMakerEndpointName=<deployed inference endpoint name> \
    LambdaExecutionRoleName=<Lambda execution role name> \
    LambdaCodeS3Bucket=<S3 bucket name for Lambda code upload> \
    LambdaCodeS3Key=lambda/chronos2-external-function.zip \
    apiGatewayIAMRoleName=<IAM role name for API Gateway>

Successfully deployed the Lambda function triggered by the API Gateway.

Deployed Lambda function

4. Preparing the Snowflake side

Next, I prepared the Snowflake environment.

i. Create the external integration

Created the external integration.

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;

-- Assuming usage from SYSADMIN
GRANT USAGE ON INTEGRATION chronos2_api_integration TO ROLE SYSADMIN;

<ResourceInvocationUrl prefix without /chronos2> is the ResourceInvocationUrl OUTPUT created by CloudFormation with /chronos2 removed.

ii. Update the trust policy of the IAM role for API Gateway

Retrieved API_AWS_IAM_USER_ARN and API_AWS_EXTERNAL_ID using DESC INTEGRATION chronos2_api_integration;.

Updated the trust policy of the IAM role for API Gateway as follows so that API Gateway can be invoked from Snowflake.

#!/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}"

Executed as follows to update the trust policy.

chmod +x ./scripts/update_iam_trust.sh
./scripts/update_iam_trust.sh \
  <IAM role name for API Gateway> \
  <Retrieved API_AWS_IAM_USER_ARN value> \
  <Retrieved API_AWS_EXTERNAL_ID value> \
  <AWS_ACCOUNT_ID> \
  <profile name>

iii. Create the external function

Created the external function as follows.

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>';

Created external function

iv. Upload demo data and aggregate for execution

Downloaded the sample data introduced in Zero-shot forecasting in the AutoGluon-Cloud guide.

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

Uploaded it as the CHRONOS2_RAW table.

Uploaded RAW data

To pass data to Chronos-2 via the external function, aggregated data for 3 series was created as follows.
The latest 512 points per series were aggregated into a list, and 3 of those series were retrieved.

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;

Sample data

5. Run inference from the external function

Confirmed that inference could be performed by requesting the inference endpoint via the external function using the created demo data.

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;

Inference results from Chronos-2

The inference results were visualized with CoCo. It is very convenient as it immediately displays visualization results in an ipynb file.

Visualization of input series and inference results

The forecasting turned out quite nicely just by feeding in the input data!

In closing

That was the 7th entry in the 'Summer Break Independent Research Relay', introducing the system we built to request time series forecasting from Chronos-2 deployed on a SageMaker AI inference endpoint via Snowflake external functions.

Note that since we are using API Gateway REST API and Lambda integration this time, there are constraints such as timeouts if the inference time requested by Lambda is too long.

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

If such issues arise, since data can be exported from Snowflake to S3, it may be necessary to devise an asynchronous inference mechanism.

Also, this time we tried with a regional endpoint, but it should also be possible to use a private endpoint for greater security, so please check if you are interested.

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

In the next article, related to this one, I will introduce my research on how to deploy Chronos-2 to a SageMaker AI inference endpoint.


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

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

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


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

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

Snowflakeの詳細を見る

Share this article

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

Related articles