
【夏休みの自由研究リレー】 Snowflakeの外部関数からSageMaker AIの推論エンドポイントにデプロイしたChronos-2に時系列予測をリクエストする仕組みを作ってみた
こんにちは、データ事業本部の鈴木です。
当記事はクラスメソッドの有志による『夏休みの自由研究リレー』 第7回のエントリです。
このブログリレーの企画は、普段からクラウドやAIを追いかけているメンバーによって、「やってみた」だけではなく「作ってみた」や「調査/研究してみた」もアウトプットしてみようという企画です。
新たな知見になることは勿論、アイデアを自社やコミュニティの発展に寄与できればと考えておりますので、お付き合い頂けますと幸いです。
では、さっそくいってみましょう。今回の記事は『Snowflakeの外部関数からSageMaker AIの推論エンドポイントにデプロイしたChronos-2に時系列予測をリクエストする仕組みを作ってみた』です。
今回作ってみた仕組みについて
Chronos-2 は、ゼロショットで時系列予測ができる Amazon の時系列基盤モデルです。単変量だけでなく、多変量や共変量付きの予測にも対応しています。
Chronos-2 は、AutoGluon-Cloud また SageMaker JumpStart から SageMaker AI の推論エンドポイントをインフラとしてデプロイして利用できます。
AWS ではSageMaker AI の推論エンドポイントにデプロイするだけで非常に便利に使えるようになっていますが、 Snowflake に入っているデータも処理できるようにしてみたいなと思ったので、以下のように外部関数から Lambda 関数経由でリクエストする構成を調べて作ってみました。

Snowflake から AWS へのリクエストは、以下のように Snowflake の外部関数を使いました。
構築してみた
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 \
<プロファイル名>

2. 推論エンドポイントのデプロイ
続いて、先に Chronos-2 を推論エンドポイントにデプロイしておきました。
SageMaker Studio のコンソールから JumpStart を開いて chronos-2 を検索・選択しました。

Deployボタンを押しました。

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

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

エンドポイント名はカスタムの名前を設定したものの、デプロイ日時を含めたエンドポイント名になっているようでした。
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 関数をデプロイできました。

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_ARN と API_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 で紹介されているサンプルデータをダウンロードしました。
CHRONOS2_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;

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

予測も入力データを投入しただけですが、良い感じにできていますね!
最後に
以上、『夏休みの自由研究リレー』の第7回のエントリで、 Snowflake の外部関数から SageMaker AI の推論エンドポイントにデプロイした Chronos-2 に時系列予測をリクエストする仕組みを作ってみたご紹介でした。
なお、今回 API Gateway の REST API と Lambda の統合を使っているため、 Lambda がリクエストした推論時間があまり長いとタイムアウトになるなど制約があります。
もしこのような課題がある場合は、 Snowflake から S3 にデータをエクスポートできるため、非同期な推論の仕組みにするなど工夫が必要そうです。
また、今回はリージョンエンドポイントで試してみましたが、よりセキュアにするためにプライベートエンドポイントを使うことも可能な想定ですので、気になる方はご確認ください。
次回もこの記事に関連して、私が Chronos-2 を SageMaker AI 推論エンドポイントにデプロイする方法を調査したためご紹介します。












