【AWS CDK】LambdaをTypeScriptで動かす環境を構築する（その3：AWSリソースにアクセスする（AWS SDK for JavaScript v3を使う）  ）

## はじめに  
本ブログは、2025/02/21（金）に開催された「JAWS-UG CDK支部#19 クラスメソッドコラボ回」における私のLT「CDKでカスタムランタイムを作成して、Lambdaをnode.js23+TypeScriptで動かしてみた」の詳細資料になります。

https://jawsug-cdk.connpass.com/event/343974/
  
LTの発表資料は、下記で公開しています。（上記Connpassページにもリンクがあります）  

https://speakerdeck.com/smt7174/cdkdekasutamurantaimuwozuo-cheng-site-lambdawonode-dot-js23-plus-typescriptdedong-kasitemita 
  
なお今回は下記の構成で、何回かに分けて投稿します。  
  
1. [カスタムランタイム作成](https://makky12.hatenablog.com/entry/2025/03/10/120500)   ※前々回の記事
1. AWS CDKでの実装＆TypeScriptで動作させる   ※前回の記事  
1. AWSリソースにアクセスする（AWS SDK for JavaScript v3を使う）   ※今回はこれ  
  
なお最終回となる今回は「S3バケットに保存したファイルの内容を読み込む」という処理をカスタムランタイムにて実施します。  

ちなみにLambdaのソースコードは以下の通りで、型importに ```type``` を記載すること以外は通常のLambda関数と全く同じです。  
    
```typescript
import type { LambdaFunctionURLEvent, LambdaFunctionURLResult } from 'aws-lambda/trigger/lambda-function-url'
import { S3, GetObjectCommand } from '@aws-sdk/client-s3'

export const handler = async function (event: LambdaFunctionURLEvent) {
  
  // 各環境変数（リージョン、バケット名、キー名）は事前にCDKで定義しておく
  const s3 = new S3({
    region: process.env.DEPLOY_REGION
  });
  
  const getObjectCommand = new GetObjectCommand({
    Bucket: process.env.S3_BUCKET_NAME
    Key: process.env.S3_KEY_NAME
  });
  
  const response = await s3.send(getObjectCommand);
  const output = await response.Body?.transformToString();
  
  const result: LambdaFunctionURLResult = {
    statusCode: 200,
    body: JSON.stringify({
      output
    }),
  };
  
  return result;
};
```
  
### 組み込みAWS SDK for JavaScript v3は使えない  
まず前提として、Lambda標準で組み込まれているAWS SDK for JavaScript v3（以下「AWS SDK」）は使えません。  
  
組み込みAWS SDKは当然ながらAWSで用意されているランタイム（node.js ver22など）に組み込みされているものであり、今回はカスタムランタイムを用意するのでそれは使えません。  
  
というわけで、AWS SDKを自分で用意する必要があります。  
  
### Lambda LayerでOK...そう思ってた時期が、俺にもありました  
となれば、AWS SDKをLambdaレイヤーとして作成すればOKと思っていました。（CDKソースは下記）   
  
```typescript
const S3_KEY_NAME = 'xxx';
const S3_BUCKET_NAME = 'yyy';
sonst DEPLOY_REGION = 'us-east-1'
  
// AWS SDK用のLambdaレイヤーを定義
const nodeModulesLayer = new lambda.LayerVersion(this, 'NodeModulesLayer', {
  code: lambda.Code.fromAsset(path.join(__dirname, '../node-modules-layer')),
  layerVersionName: 'node-modules',
  removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
  compatibleRuntimes: [
    lambda.Runtime.PROVIDED_AL2023,
  ],
  compatibleArchitectures: [
    lambda.Architecture.X86_64,
  ]
});
  
// Lambda関数定義
const sampleFunction = new lambda.Function(this, "Node23SampleFunction", {
  functionName: 'Node23SampleFunction',
  runtime: lambda.Runtime.PROVIDED_AL2023,
  architecture: lambda.Architecture.X86_64,
  // node23RuntimeLayerはカスタムランタイムのLambdaレイヤー 
  layers: [node23RuntimeLayer, nodeModulesLayer],
  code: lambda.Code.fromAsset(path.join(__dirname, '../lambda')),
  environment: {
    S3_KEY_NAME,
    S3_BUCKET_NAME,
    DEPLOY_REGION,
  }
  
  // 読み込み先のS3バケットを定義＆権限設定
  const sampleBucket = new s3.Bucket(this, 'SampleBucket', {
      bucketName: S3_BUCKET_NAME,
      autoDeleteObjects: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });
  
    sampleBucket.grantRead(sampleFunction);  
});
```
  
が、すでにネタバレしている通り、それだとAWS SDKをうまくimportできず ```Cannot find package '@aws-sdk/client-s3' imported from /var/task/index.ts``` エラーとなってしまいました。   
  
これに関しては、下記公式サイトを参考にいろいろ調査して試行錯誤したのですが[^1]、最終的にはどれもうまくいきませんでした...（「事前にパスをLambda側に認識させるとうまくいく」という情報も頂いたのですが、いろいろ試してもうまくいかず...）  

[^1]:例えば「フォルダ名＆パスをいろいろ変えてみる」「デプロイパッケージにtsconfig.jsonを含め、そこでtypesを指定する」」「importでフォルダを絶対パスで直指定する」「環境変数を自分で設定する」「カスタムランタイムでnpm installする」など))、
  
- [レイヤーコンテンツのパッケージング](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/packaging-layers.html)

これに関して、もしご存じの方がいたら教えて頂けるとありがたいです。  
  
### 最終的な方法  
Lambdaレイヤーではうまくいかなかったので、最終的に「デプロイパッケージにnode_modulesを含める」という方法で対処しました。 
   
先ほどLambda関数の定義で ```code: lambda.Code.fromAsset(path.join(__dirname, '../lambda'))``` という設定がありましたが、この「lambda」フォルダに「node_modules」フォルダをコピペしてデプロイすれば、今度はちゃんと実行結果が「200 OK」となり、下記レスポンスが返ってきます。（本記事では書いてませんが、読み込んだファイルの内容は下記の通り「This is aws-cdk-event-19-sample.txt desuyo.」というテキストです）
  
> {
  "statusCode": 200,  
  "body": "{\"output\":\"This is aws-cdk-event-19-sample.txt desuyo.\\n\"}"
}  
    
ということで、今回の目的である「S3バケットに保存したファイルの内容を読み込む（AWSリソースにアクセスする）」は達成できました！   
   
### いろいろ問題が...  
ただ、「AWSリソースにアクセスする」ことはできましたが、この方法だと問題がいろいろあります。  
  
とりあえずぱっと思いつくだけでも、下記2点があげられます。  
  
#### デプロイパッケージのサイズが大きい（=デプロイが遅い）
今回の方法だと、「node_modules」フォルダ全体をデプロイパッケージに組み込むため、デプロイパッケージのサイズが大きくなりがちです。  
そのため、デプロイパッケージのサイズが大きくなる分、デプロイも遅くなりがちです。  
  
#### 250MB制限に引っかかる  
Lambda関数やLambdaレイヤーのデプロイパッケージには「上限250MBまで」という上限があります。  
  
【参考】：[Lambda クォータ](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html)  
  
そのため、AWS SDKのあれもこれも追加...とやっていると、すぐに上限に引っかかってデプロイできなくなってしまいます。[^2]

[^2]:実際、DynamoDB関係のモジュールを追加したら、250MB制限に引っかかってデプロイできませんでした))  
  
なおコンテナイメージなら10GBまでOKなのですが、コンテナ使うならカスタムランタイムは初めから使わないでしょうし...
  
### まとめ  
というわけで、3回にわたって記載した「【AWS CDK】LambdaをTypeScriptで動かす環境を構築する」ですが、これで以上となります。  
  
今回、あえて「LambdaをTypeScriptで動かす」ためにカスタムランタイムを作成しましたが、なかなかnode,jsのカスタムランタイムを動かすのが大変でした。（特にAWS SDKを使用するのが）  
Lambdaにはnode.jsランタイムが標準で用意されていますので、よほどのことがない限りそれを利用するのが無難だな...と思いました。（あるいはコンテナイメージを使用するか）  
  
ただ、おそらく今年の夏くらいにnode.js ver24（TypeScriptをサポートしたLTSバージョン）がリリースされ、そのLambdaランタイムも登場するはずなので、もしかしたらその頃に何か進展があるかも？と期待しつつ、今回は終了したいと思います。
    
### 告知  
5/23(金)～5/24(土)に東京・ベルサール神田で実施される「TS Kaigi 2025」にて、「AWS LambdaをTypeScriptで動かして分かった、Node.jsのTypeScriptサポートの利点と課題」登壇させていただくこととなりました。（日時は5/23(金) 15:50 ~ 16:20 の予定です）  
  
https://2025.tskaigi.org/
  
ちょうどこのブログで扱っている「【AWS CDK】LambdaをTypeScriptで動かす環境を構築する」で分かったNode.jsのTypeScriptサポートを触ってみてわかったことをお話しさせていただきますので、当日はよろしくお願いいたします。  
  
それでは、今回はこの辺で。