🕌

AWS CDK で SageMaker の サーバレス推論エンドポイントを作成する

に公開

AWS CDK で SageMaker の サーバレス推論エンドポイントを作成する

はじめに

CDK も SageMaker もさっぱりわからない状態から、なんとなく触れるようになるまでの備忘録です。
Lambda から SageMaker のリアルタイムエンドポイントを呼び出して利用できるようにすることを目指します。

AWS CDK

AWS Cloud Development Kit

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html

AWSのリソースをソースコードで管理できるようになります。
CloudFormation を通してデプロイを行うため、リソースの確認・破棄、エラー時のロールバックなどが簡単に行えます。

Terraform と比較される記事をよく見かけました。

Terraform

  • HashiCorp が提供するオープンソースツール
  • AWS CLI を利用してリソースを操作
  • 細かい制御が可能 で、デプロイ速度も速い
  • 記述量が多くなりがち

AWS CDK

  • AWS が提供するフレームワーク
  • CloudFormation を経由してリソースを作成
  • コード量は少なくシンプル に書ける
  • デプロイ速度は遅くなりやすい

SageMaker

Amazon SageMaker

https://aws.amazon.com/jp/sagemaker/

機械学習モデルの構築・学習・デプロイなどを一貫して行えるサービスです。
様々な機能が提供されていますが、今回は Hugging Face で公開されているモデルをそのまま使ってサーバレス推論エンドポイントを作成します。

環境

  • wsl
  • aws cli と aws cdk がインストール済み
  • AWS アカウントの認証情報が設定済み
$ aws --version
aws-cli/2.25.13 Python/3.12.9 Linux/5.15.167.4-microsoft-standard-WSL2 exe/x86_64.ubuntu.24
$ cdk --version
2.1020.2 (build cf35f57)
$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ******************** shared-credentials-file    
secret_key     ******************** shared-credentials-file    
    region           ap-northeast-1      config-file    ~/.aws/config

CDK のプロジェクトを作成する

CDK の基本的な操作については、こちらのワークショップで一通り触りました。

https://catalog.workshops.aws/typescript-and-cdk-for-beginner/ja-JP/40-cdk-introduction

ワークスペースの作成とプロジェクトの初期化を行います。
言語は typescript を指定します。
他の言語も使用できますが、 TypeScript で書かれた事例や記事が多いので、よほど強いこだわりがなければ TypeScript でいいみたいです。

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/languages.html

$ mkdir cdk-sagemaker-training
$ cd cdk-sagemaker-training/
$ cdk init --language typescript
Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build`   compile typescript to js
* `npm run watch`   watch for changes and compile
* `npm run test`    perform the jest unit tests
* `npx cdk deploy`  deploy this stack to your default AWS account/region
* `npx cdk diff`    compare deployed stack with current state
* `npx cdk synth`   emits the synthesized CloudFormation template

Executing npm install...
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
 All done!

NOTICES         (What's this? https://github.com/aws/aws-cdk/wiki/CLI-Notices)

34892   CDK CLI will collect telemetry data on command usage starting at version 2.1100.0 (unless opted out)

        Overview: We do not collect customer content and we anonymize the
                  telemetry we do collect. See the attached issue for more
                  information on what data is collected, why, and how to
                  opt-out. Telemetry will NOT be collected for any CDK CLI
                  version prior to version 2.1100.0 - regardless of
                  opt-in/out. You can also preview the telemetry we will start
                  collecting by logging it to a local file, by adding
                  `--unstable=telemetry --telemetry-file=my/local/file` to any
                  `cdk` command.

        Affected versions: cli: ^2.0.0

        More information at: https://github.com/aws/aws-cdk/issues/34892


If you don’t want to see a notice anymore, use "cdk acknowledge <id>". For example, "cdk acknowledge 34892".

作成が完了しました。
バージョンの警告は今回無視します。
最後に CDK CLI の通知が出ています。
毎回出るとうっとうしいので cdk acknowledge 34892 を実行し、このワークスペースでは非表示になるようにします。(この設定は cdk.context.json に追記されます。)

$ tree -I 'node_modules' --dirsfirst
.
├── bin
│   └── cdk-sagemaker-training.ts
├── lib
│   └── cdk-sagemaker-training-stack.ts
├── test
│   └── cdk-sagemaker-training.test.ts
├── README.md
├── cdk.context.json
├── cdk.json
├── jest.config.js
├── package-lock.json
├── package.json
└── tsconfig.json

上記のようなディレクトリが作成されました。( node_modules は割愛します)
バージョンが異なると生成物が異なるようで、古い記事では全く違うものが生成されていました。

bin/cdk-sagemaker-training.ts

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CdkSagemakerTrainingStack } from '../lib/cdk-sagemaker-training-stack';

const app = new cdk.App();
new CdkSagemakerTrainingStack(app, 'CdkSagemakerTrainingStack', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});

最上位で呼ばれるファイルです。
一般的には bin フォルダの中にはこのファイルが1つだけ置かれるみたいです。

ここでは cdk.App のインスタンスを作成し、それに紐づくスタックのインスタンスを作成します。
スタックのインスタンス1つにつき、CloudFormation のスタックが1つ生成されます。

lib/cdk-sagemaker-training-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

export class CdkSagemakerTrainingStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'CdkSagemakerTrainingQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}

スタックを定義するためのファイルです。
cdk.Stack を継承したクラスを定義します。
この中に AWS のリソースを定義していきます。

test/cdk-sagemaker-training.test.ts

テストコードを書くことができます。
今回は使用しません。

cdk.json

CDK の設定ファイルです。
アプリの起動方法、フラグ、設定値などを記述します。
リソースを作成する際の初期値や、スタックに渡す値を設定できるようです。

例えば以下の設定にすると、すべての新規 S3 バケットのパブリックアクセスがデフォルトでブロックされます。

{
  "context": {
    "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true
  }
}

詳細は公式ドキュメントで確認できます

https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.cx_api/README.html

デプロイしてみる

最小限の状態でデプロイを行います。

まず bin/cdk-sagemaker-training.ts の env を設定している1行のコメントを解除します。
こうすることで、スタックの作成時に、AWS CLI のデフォルトのアカウント情報が参照されます。

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CdkSagemakerTrainingStack } from '../lib/cdk-sagemaker-training-stack';

const app = new cdk.App();
new CdkSagemakerTrainingStack(app, 'CdkSagemakerTrainingStack', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});

lib/cdk-sagemaker-training-stack.ts の queue を作成しているコメントと import コメントを解除します。
これで1つの AWS SQS が作成されるはずです。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sqs from 'aws-cdk-lib/aws-sqs';

export class CdkSagemakerTrainingStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // example resource
    const queue = new sqs.Queue(this, 'CdkSagemakerTrainingQueue', {
      visibilityTimeout: cdk.Duration.seconds(300)
    });
  }
}

CDK を動かす下準備として cdk bootstrap を実行します。
これを実行すると CDK を動かすために必要な S3 Bucket や IAM Role が作成されます。
この環境では既に作成済みなのでその旨が出力されました。

$ cdk bootstrap
  Bootstrapping environment aws://***/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
  Environment aws://***/ap-northeast-1 bootstrapped (no changes).

cdk deploy を使ってデプロイします。
cdk deploy を実行すると以下の処理が行われます。

  • コードをもとに CloudFormation テンプレートを作成
  • デプロイ済みの既存のスタックがあれば、作成されたテンプレートとの差分を表示
  • CloudFormation をデプロイ

個別の処理を実行するコマンドもあります。

テンプレートの作成のみを行いたい場合 → cdk synth
デプロイ済みの既存のスタックとローカルのテンプレートの差分を確認したい場合 → cdk diff

詳細は cdk で確認できます。

$ cdk deploy

✨  Synthesis time: 19.86s

CdkSagemakerTrainingStack: start: Building CdkSagemakerTrainingStack Template
CdkSagemakerTrainingStack: success: Built CdkSagemakerTrainingStack Template
CdkSagemakerTrainingStack: start: Publishing CdkSagemakerTrainingStack Template (***-ap-northeast-1)
CdkSagemakerTrainingStack: success: Published CdkSagemakerTrainingStack Template (***-ap-northeast-1)
CdkSagemakerTrainingStack: deploying... [1/1]
CdkSagemakerTrainingStack: creating CloudFormation changeset...

 ✅  CdkSagemakerTrainingStack

✨  Deployment time: 46.48s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:***:stack/CdkSagemakerTrainingStack/***

✨  Total time: 66.35s

完了しました。
ワークスペースに生成された cdk.out を確認します。
ここにはソースをもとに作成された CloudFormation のテンプレートが格納されます。

例えば、cdk.out/CdkSagemakerTrainingStack.template.json の中には、 sqs を作成するための記述があります。

{
 "Resources": {
  "CdkSagemakerTrainingQueue842D76D3": {
   "Type": "AWS::SQS::Queue",
   "Properties": {
    "VisibilityTimeout": 300
   },
   "UpdateReplacePolicy": "Delete",
   "DeletionPolicy": "Delete",
   "Metadata": {
    "aws:cdk:path": "CdkSagemakerTrainingStack/CdkSagemakerTrainingQueue/Resource"
   }
  },
  ・・・
 }
}

実行されたスタックを CLI から確認してみます。

$ aws cloudformation describe-stacks --query "Stacks[?StackName=='CdkSagemakerTrainingStack']"
[
    {
        "StackName": "CdkSagemakerTrainingStack",
        ・・・
        "StackStatus": "CREATE_COMPLETE",
        ・・・
    }
]

SQS が作成されていることも確認します。

$ aws sqs list-queues
{
    "QueueUrls": [
        "https://sqs.ap-northeast-1.amazonaws.com/***/CdkSagemakerTrainingStack-CdkSagemakerTrainingQueue***"
    ]
}

AWS のコンソールからも確認できます。

動くことを確認したので、SQSの記載を削除しデプロイします。

lib/cdk-sagemaker-training-stack.ts のクラスの中身をすべて削除します。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSagemakerTrainingStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
  }
}
$ cdk deploy
・・・
  Total time: 84.98s

aws sqs list-queues を実行し、先ほど作った SQS が表示されないことを確認します。

SageMaker 推論エンドポイントの作成

lib/cdk-sagemaker-training-stack.ts を以下のように書き換えます。

cdk-sagemaker-training-stack.ts
import * as cdk from 'aws-cdk-lib';
import { aws_iam, aws_sagemaker } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSagemakerTrainingStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 実行ロール
    const executionRole = new aws_iam.Role(this, 'SagemakerExecutionRole', {
      assumedBy: new aws_iam.ServicePrincipal('sagemaker.amazonaws.com'),
      managedPolicies: [
        aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'),
        aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess'),
      ],
    });

    // 推論コンテナ 環境変数
    const containerEnvironment: { [key: string]: string } = {
      HF_MODEL_ID: "intfloat/multilingual-e5-base",
      HUGGINGFACE_HUB_CACHE: '/opt/ml/tmp',
    }

    // 推論コンテナ イメージURI
    const imageUri = "354813040037.dkr.ecr.ap-northeast-1.amazonaws.com/tei-cpu:2.0.1-tei1.2.3-cpu-py310-ubuntu22.04"

    // 推論コンテナ 設定
    const container: aws_sagemaker.CfnModel.ContainerDefinitionProperty = {
      environment: containerEnvironment,
      image: imageUri,
    };

    // 推論モデル
    const model = new aws_sagemaker.CfnModel(this, 'TextEmbeddingModel', {
      executionRoleArn: executionRole.roleArn,
      primaryContainer: container,
    });
    model.node.addDependency(executionRole);
    
    // 推論エンドポイント 設定
    const endpointConfiguration = new aws_sagemaker.CfnEndpointConfig(this, 'TextEmbeddingEndpointConfig', {
      productionVariants: [
        {
          variantName: "AllTraffic",
          modelName: model.attrModelName,
          serverlessConfig: {
            maxConcurrency: 10,
            memorySizeInMb: 6144
          }
        }
      ]
    });
    endpointConfiguration.node.addDependency(model);

    // 推論エンドポイント
    const endpoint = new aws_sagemaker.CfnEndpoint(this, 'TextEmbeddingEndpoint', {
      endpointConfigName: endpointConfiguration.attrEndpointConfigName,
      endpointName: 'TextEmbedding',
    });
    endpoint.node.addDependency(endpointConfiguration);
  }
}
    // 実行ロール
    const executionRole = new aws_iam.Role(this, 'SagemakerExecutionRole', {
      assumedBy: new aws_iam.ServicePrincipal('sagemaker.amazonaws.com'),
      managedPolicies: [
        aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'),
        aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess'),
      ],
    });

モデルの作成や実行を行うロールを作成します。
Sagemaker と ECR の権限を付与します。

    // 推論コンテナ 環境変数
    const containerEnvironment: { [key: string]: string } = {
      HF_MODEL_ID: "intfloat/multilingual-e5-base",
      HUGGINGFACE_HUB_CACHE: '/opt/ml/tmp',
    }

    // 推論コンテナ イメージURI
    const imageUri = "354813040037.dkr.ecr.ap-northeast-1.amazonaws.com/tei-cpu:2.0.1-tei1.2.3-cpu-py310-ubuntu22.04"

    // 推論コンテナ 設定
    const container: aws_sagemaker.CfnModel.ContainerDefinitionProperty = {
      environment: containerEnvironment,
      image: imageUri,
    };

    // 推論モデル
    const model = new aws_sagemaker.CfnModel(this, 'TextEmbeddingModel', {
      executionRoleArn: executionRole.roleArn,
      primaryContainer: container,
    });
    model.node.addDependency(executionRole);

ここでは推論で使うモデルやコンテナを指定します。
今回は日本語の文字列の埋め込みを行うエンドポイントを作成しようと思います。

モデルは intfloat/multilingual-e5-base を使いました。

https://huggingface.co/intfloat/multilingual-e5-base

コンテナは、Hugging Face が公開している、Text Embeddings Inference(TEI) を使用します。

https://huggingface.co/docs/text-embeddings-inference/index
https://github.com/huggingface/text-embeddings-inference

TEI はスペックに応じた複数のコンテナイメージを公開しています。
インスタンスタイプに応じたイメージを選択する必要があります。
SDKを使うと簡単にイメージURIが取得できます。

from sagemaker.huggingface import get_huggingface_llm_image_uri

# retrieve the image uri based on instance type
def get_image_uri(instance_type):
  key = "huggingface-tei" if instance_type.startswith("ml.g") or instance_type.startswith("ml.p") else "huggingface-tei-cpu"
  return get_huggingface_llm_image_uri(key, version="1.2.3")

https://huggingface.co/blog/sagemaker-huggingface-embedding

モデルによっては対応していない場合があるので注意が必要です。
このコンテナは Rust を用いて書かれているので、日本語の形態素解析に mecab 、 fugashi といった python のモジュールが追加で必要なモデルの場合などは利用が難しいです。

https://huggingface.co/docs/text-embeddings-inference/supported_models

他にも aws が公開しているコンテナを使う方法や、カスタムイメージを使う方法もあります。

https://docs.aws.amazon.com/sagemaker/latest/dg-ecr-paths/ecr-ap-northeast-1.html?utm_source=chatgpt.com

  • HF_MODEL_ID: "intfloat/multilingual-e5-base",
    モデルを指定します。
  • HUGGINGFACE_HUB_CACHE: '/opt/ml/tmp',
    コンテナ内でキャッシュを保存するパスを変更します。書きこみ権限の関係でデフォルトの設定だとエラーになります。
  • model.node.addDependency(executionRole);
    依存関係の指定。executionRole が作成されてから model を作成します。
    // 推論エンドポイント 設定
    const endpointConfiguration = new aws_sagemaker.CfnEndpointConfig(this, 'TextEmbeddingEndpointConfig', {
      productionVariants: [
        {
          variantName: "AllTraffic",
          modelName: model.attrModelName,
          serverlessConfig: {
            maxConcurrency: 10,
            memorySizeInMb: 6144
          }
        }
      ]
    });
    endpointConfiguration.node.addDependency(model);

    // 推論エンドポイント
    const endpoint = new aws_sagemaker.CfnEndpoint(this, 'TextEmbeddingEndpoint', {
      endpointConfigName: endpointConfiguration.attrEndpointConfigName,
      endpointName: 'TextEmbedding',
    });
    endpoint.node.addDependency(endpointConfiguration);

モデルをもとにエンドポイントを作成します。

  • variantName: "AllTraffic",
    慣例名。1つのエンドポイントに複数のモデル(バリアント)を紐づける場合には一意になるように任意の値を設定します。今回は1つしか紐づけないので AllTraffic で問題ありません。
  • serverlessConfig
    サーバレスエンドポイントにする場合に指定します。
    もしサーバレスではない普通のインスタンスのエンドポイントを作成する場合はこの代わりに、 InstanceTypeInitialInstanceCount を設定する必要があります。
    maxConcurrency: 最大同時実行数
    memorySizeInMb: メモリサイズ(MB)

cdk deploy で実行します。
完了まで数分かかりました。

$ cdk deploy

・・・

✨  Total time: 200.96s

作成されたエンドポイントを実際に動かして確認してみます。

$ jq -nc --arg t 'CDKとSageMakerの練習' '{inputs:$t}' > payload.json
$ aws sagemaker-runtime invoke-endpoint \
  --endpoint-name TextEmbedding \
  --content-type application/json \
  --accept application/json \
  --body fileb://payload.json \
  output.json
# サーバレスエンドポイントのためコールドスタートが発生し、初回実行時は数十秒かかります。
{
    "ContentType": "application/json",
    "InvokedProductionVariant": "AllTraffic"
}
$ cat output.json | jq .
[
  [
    0.014250721,
    0.040402036,
    -0.018678946,
・・・

ベクトルが作成されました。

Lambdaから呼び出して使う

2単語の類似度を比較する Lambda を作成します。

Lambda の処理を記述するためのファイルを作成します。

$ mkdir lambda
$ touch ./lambda/handler.ts

追加のモジュールをインストールします。

$ npm i @aws-sdk/client-sagemaker-runtime

added 84 packages, and audited 419 packages in 7s

40 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities


$ npm i -D @types/aws-lambda esbuild

added 3 packages, and audited 422 packages in 4s

40 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

lambda/handler.ts に以下を記述します。

handler.ts
import { Handler } from "aws-lambda";
import { SageMakerRuntimeClient, InvokeEndpointCommand } from "@aws-sdk/client-sagemaker-runtime";

// SageMaker クライアント
const client = new SageMakerRuntimeClient();

// コサイン類似度計算
function cosineSimilarity(vecA: number[], vecB: number[]): number {
  const dot = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
  const normA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
  const normB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
  return dot / (normA * normB);
}

// SageMakerへ推論リクエスト
async function getTextEmbeddingsBatch(texts: string[]): Promise<number[][]> {
  const inputs = texts.map(t => `query: ${t}`); // multilingual-e5 が指定するフォーマットに合わせる
  const command = new InvokeEndpointCommand({
    EndpointName: process.env.ENDPOINT_NAME!,
    ContentType: "application/json",
    Accept: "application/json",
    Body: JSON.stringify({ inputs }),
  });
  const res = await client.send(command);

  const payload = await res.Body?.transformToString();
  if (!payload) throw new Error("レスポンスが空です");

  const parsed = JSON.parse(payload);
  if (!Array.isArray(parsed) || !Array.isArray(parsed[0])) {
    throw new Error("レスポンスが [[...]] 形式ではありません");
  }
  return parsed as number[][];
}

// Lambda ハンドラ
export const handler: Handler = async (event) => {
  const body = typeof event.body === "string" ? JSON.parse(event.body) : event;
  const { word1, word2 } = body;

  if (!word1 || !word2) {
    return { statusCode: 400, body: "word1 と word2 は必須項目です" };
  }

  // 単語の埋め込みと類似度計算
  const [vec1, vec2] = await getTextEmbeddingsBatch([word1, word2]);
  const similarity = cosineSimilarity(vec1, vec2);

  return {
    statusCode: 200,
    body: JSON.stringify({ similarity }),
  };
};

lib/cdk-sagemaker-training-stack.ts に以下を追加します。

cdk-sagemaker-training-stack.ts
import path from 'path';

    ・・・

    // Lambdaの作成
    const fn = new NodejsFunction(this, "EmbedLambda", {
      entry: path.join(__dirname, "..", "lambda", "handler.ts"),
      runtime: aws_lambda.Runtime.NODEJS_20_X,
      memorySize: 512,
      timeout: cdk.Duration.minutes(5),
      bundling: {
        forceDockerBundling: false,
      },
      environment: {
        ENDPOINT_NAME: endpoint.attrEndpointName,
      },
    });

    // Lambdaのロググループをスタック削除時に削除
    fn.logGroup.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY)

    // Lambdaの実行ロールにSageMakerエンドポイントの呼び出し権限を付与
    fn.addToRolePolicy(
      new aws_iam.PolicyStatement({
        actions: ["sagemaker:InvokeEndpoint"],
        resources: [
          cdk.Stack.of(this).formatArn({
            service: "sagemaker",
            resource: "endpoint",
            resourceName: endpoint.attrEndpointName,
          }),
        ],
      })
    );

    // 関数URL
    const fnUrl = fn.addFunctionUrl({
      authType: aws_lambda.FunctionUrlAuthType.NONE,
      cors: {
        allowedOrigins: ["*"],
        allowedMethods: [aws_lambda.HttpMethod.ALL],
      },
    });

    // 関数URLを出力
    new cdk.CfnOutput(this, "EmbedLambdaUrl", {
      value: fnUrl.url,
    });

再度デプロイします。

$ cdk deploy

・・・

Outputs:
CdkSagemakerTrainingStack.EmbedLambdaUrl = https://***.lambda-url.ap-northeast-1.on.aws/
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:***:stack/CdkSagemakerTrainingStack/***

  Total time: 31.61s

作成された関数URLが出力されます。
いくつか試してみます。

$ curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"word1": "朝からずっと天気が崩れていて洗濯物が干せなくて困っている。", "word2": "災害級の強い雨が明日の夕方から降るらしい。"}' \
  https://***.lambda-url.ap-northeast-1.on.aws/ \
  -w "\n"
{"similarity":0.888046462909359}

$ curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"word1": "朝からずっと天気が崩れていて洗濯物が干せなくて困っている。", "word2": "雲ひとつない青空が広がり散歩するのに気持ちの良い天気だ。"}' \
  https://***.lambda-url.ap-northeast-1.on.aws/ \
  -w "\n"
{"similarity":0.8557268751866666}

$ curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"word1": "朝からずっと天気が崩れていて洗濯物が干せなくて困っている。", "word2": "AIの進化によって多くの仕事が自動化される一方で、新しい産業や職業も生まれている。"}' \
  https://***.lambda-url.ap-northeast-1.on.aws/ \
  -w "\n"
{"similarity":0.7852773422952674}

それらしい類似度が出てくれました。
サーバエラーが出た時は、aws コンソールの Lambda のテストタブからテストを行うと、詳細なエラーログが確認できます。

リソースの削除

cdk destroy を実行してスタックを削除します。

$ cdk destroy
Are you sure you want to delete: CdkSagemakerTrainingStack (y/n)? y
CdkSagemakerTrainingStack: destroying... [1/1]

  CdkSagemakerTrainingStack: destroyed

完了しました。コンソールからも確認します

ほとんどのリソースは自動で削除されますが、一部のリソースは削除されずに残ることがあります。
ロググループや S3 の Bucket が残りがちです。

https://dev.classmethod.jp/articles/resources-remain-though-cdk-constructs-are-deleted/

今回は Lambda のロググループをスタックの削除と同時に削除する設定にしていたので、全てのリソースが削除されました。
残ったリソースがある場合は手動で削除します。

おわりに

CloudFormation のテンプレート直書きと比べて、記述量の少なさとコード補完が使えることに感動しました。
リソースをコンストラクトとして切り出し、複数ファイルに分割して整理することもできるみたいですが、いまいち使いこなせていません。

SageMaker については本来の強みを全く生かせていないので他の機能も試してみたいです。

北日本コンピューターサービス

Discussion