☁️

AWS SDK for JavaScriptのv2,v3の違いでチュートリアルから詰まりまくった件

2024/04/03に公開

はじめに

AWSの学習がてら、以下の「基本的なウェブアプリケーションを構築する」チュートリアルを試していたら、30分で完了どころか3時間ほど溶かしたので、備忘録として書いておく。
https://aws.amazon.com/jp/getting-started/hands-on/build-web-app-s3-lambda-api-gateway-dynamodb/

詰まった点

主に詰まったのは「Lesson4 データテーブルを作成する」項目における、Lambda関数の記述である。
「Lesson2 サーバレス関数を構築する」でLambda関数をPython、JavaScript、Javaのいずれかで作成するのだが、その時にJavaScriptでNode.js 20.xを選択して作成したのが敗因。

ガイドではNode.js 16.xを選択しているため、チュートリアルのデモスクリプトのLambda関数で使用している「AWS SDK for JavaScript」がv2だったのである。そして、Node.js 18.x以上からはSDKがv3になっており、両者は記述方法が異なるため、「デモのスクリプトをコピペしたらエラーが出まくるのだが」状態になった。
(そもそもNode.jsに明るくないので、「このエラーはなんで出るんや?」状態から始まり、SDKのバージョンの違いという原因にたどり着くまでに結構な時間を要した。
そして、原因が分かった後「ほなv3やとどない書けばええんや?」を1個ずつ調べたので、かなりの時間がかかった。)

当初のコード(v2)

Lesson4で記述されているLambda関数のデモコード。

// Include the AWS SDK module
const AWS = require('aws-sdk');
// Instantiate a DynamoDB document client with the SDK
let dynamodb = new AWS.DynamoDB.DocumentClient();
// Use built-in module to get current date & time
let date = new Date();
// Store date and time in human-readable format in a variable
let now = date.toISOString();
// Define handler function, the entry point to our code for the Lambda service
// We receive the object that triggers the function as a parameter
exports.handler = async (event) => {
    // Extract values from event and format as strings
    let name = JSON.stringify(`Hello from Lambda, ${event.firstName} ${event.lastName}`);
    // Create JSON object with parameters for DynamoDB and store in a variable
    let params = {
        TableName:'HelloWorldDatabase',
        Item: {
            'ID': name,
            'LatestGreetingTime': now
        }
    };
    // Using await, make sure object writes to DynamoDB table before continuing execution
    await dynamodb.put(params).promise();
    // Create a JSON object with our response and store it in a constant
    const response = {
        statusCode: 200,
        body: name
    };
    // Return the response constant
    return response;
};

出てきたエラー色々

1個のエラーが解消する→デプロイ・テスト実行→別のエラーが出るの繰り返しだった。

エラー1:exports is not defined in ES module scope

Lesson2のラムダ関数作成・実行時に失敗したのでエラー名で検索した。
Lambda ランタイム Node.js 18 実行時「exports is not defined in ES module scope」のエラーが発生します。の記事で

ES モジュール方式のファイルに、CommonJS モジュール方式のコードを記述しているのが原因です。ファイル拡張子を「.mjs」から「.js」に変更いただくか、ES モジュール方式のコードに修正してください。

という回答を見て、「.js」に拡張子を変更した。
(そのためLesson4で別のエラーが出てくる羽目になった)

エラー2:[time out after 3.00 seconds]

初期設定だとテスト時に3秒以上かかるとタイムアウトのエラーになってしまう。
Qiita:【AWS】Lambdaでtime out after 3.00 secondsが出たときの対処法を見て、Lambdaのtimeoutを30秒に変更した。

エラー3:[Cannot find package 'aws-sdk' imported from /var/task/index.mjs]

これが一番詰まったエラー。エラー名で検索をかけて探しているうちに以下の記事辺りにたどり着き、原因がバージョンの違いによるものだと察した。

以下のガイド辺りをみて、const AWS = require('aws-sdk');
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";に変更した。

エラー4: [SyntaxError: Cannot use import statement outside a module]

Lesson2で拡張子を.mjsから.jsに変更し、v3のimport文を使ったため出たエラー。
Qiita: javascriptで外部モジュールのimportが出来ないを見て拡張子を「.mjs」に戻した。

エラー5: [dynamodb is not a function]

最初の処理をimport文に変更したのと、そもそもv2仕様の書き方だったので、その次の処理(インスタンス化)も通らなくなった。
以下のガイドを見て、let dynamodb = new AWS.DynamoDB.DocumentClient();
const dynamoDbClient = new DynamoDBClient({});に変更した。

newを使っていてJavaのコンストラクタみたいな書き方だなとか思っていたが、コンストラクタの生成ではないようだ。

エラー6:[exports is not defined in ES module scope](再び)

拡張子を.mjsに戻したのでエラー1に戻ってきた。ESモジュール方式のコードに修正する方のアプローチで検証。
CommonJS module Handlerだと「exports.handler = async function (event, context) {」という書き方になるが、ES module Handlerだと「export const handler = async (event, context) => {」という書き方になる。
AWS Lambda 開発者ガイド:Node.js の AWS Lambda 関数ハンドラーの説明が丁寧だった。

エラー7:[docClient.put(...).promise is not a function]

await(非同期処理)の記述方法の差異によるエラー(変数名を変えただけだと当然動かなかった)。ここまで来るとエラーが返される行数が一気に進んで(=正常に動いている行が増えて)、「もう少しや!」となっていた。
当該のエラー行をコメントアウトして実行すると、200のレスポンスが返ってきたので「これ解決したら動く!」と安堵した。

以下の記事がかなり丁寧に書いていたので、v2とv3の記述方法を比較しながら修正。

PutCommandメソッドを使用したいので、import { DynamoDBDocument, PutCommand } from "@aws-sdk/lib-dynamodb"でlib-dynamodbをインポートし、
const docClient = DynamoDBDocument.from(dynamoDbClient); でインスタンス化を行った後、
当該行のawait dynamodb.put(params).promise();await docClient.send(new PutCommand(params));に変更した。

ちなみに今回はaws-sdk使用時にimport文で記述したが、
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); という書き方でもOKとのこと。

AWS SDK v2 から v3 (JavaScript) への移行に関する注意事項 - 役立つクイックガイド

最終的に動いたコード(v3)

見よう見まねでv3仕様に書きかえて動いたコードは以下。

// Include the AWS SDK module
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument, PutCommand } from "@aws-sdk/lib-dynamodb"
// Instantiate a DynamoDB document client with the SDK
const dynamoDbClient = new DynamoDBClient({});
const docClient = DynamoDBDocument.from(dynamoDbClient); 

// Use built-in module to get current date & time
let date = new Date();
// Store date and time in human-readable format in a variable
let now = date.toISOString();

// Define handler function, the entry point to our code for the Lambda service
// We receive the object that triggers the function as a parameter
export const handler = async (event) => {
    // Extract values from event and format as strings
    let name = JSON.stringify(`Hello from Lambda, ${event.firstName} ${event.lastName}`);
    
    // Create JSON object with parameters for DynamoDB and store in a variable
    let params = {
        TableName:'HelloWorldDatabaseKnt',
        Item: {
            'ID': name,
            'LatestGreetingTime': now
        }
    };
    // Using await, make sure object writes to DynamoDB table before continuing execution
    await docClient.send(new PutCommand(params));
    
    // Create a JSON object with our response and store it in a constant
    const response = {
        statusCode: 200,
        body: name
    };
    // Return the response constant
    return response;  
};

その後のレッスンは順調に進み、「Call API」を押下するとAPI Gateway 経由で Lambda 関数を呼び出すことができるようになった。

レッスン完了時の画面
レッスン完了時のAPI

雑感

node.js v16.xを大人しく選択するか、関数を作り直せばさくっと動いたのでは?とも思う。ただこの手のはエラーを調べて頭を抱えながら寄り道するのが楽しいのである。デプロイ→テストを繰り返し、無事に200が返ってきたときはすごくうれしかった。
コンソール画面の情報が古い程度なら画面を探してすぐに解決するが、バージョンの違いによる記述方法の差異は中々たどり着くのが難しかった。
エラー文をコピペしてググることや、公式のガイドを見ること、公式の説明でぴんと来ない場合はQiitaやZenn等他ブログも見てみることは大事だと思った。

Discussion