【AWS Lambda使い方ガイド】AWS CLIから簡単なランタイムNodejs v18対応のLambdaをデプロイする手順


※ 当ページには【広告/PR】を含む場合があります。
2023/06/15
【AWS Lambda使い方ガイド】Lambdaのハンドラーに課金対象外の内部タイマーをひっそりと仕込んでみる
【AWS Lambda使い方ガイド】AWS CLIから関数URLを設定してLambdaをAPIとして公開する
蛸壺の技術ブログ|AWS CLIから簡単なランタイムNodejs v18対応のLambdaをデプロイする手順

現在、AWS LambdaのNodejsのランタイムで推奨されるバージョンは
「v16以降」となっています。

さらに、昨年の2022年11月には
「v18」が正式にサポートを開始されたこともあり、今後徐々にnode v18を意識したコードに置き換える準備も必要です。

参考|AWS Lambda が Node.js 18 のサポートを追加

v18でこれまでと大きく違うことは、デフォルトのハンドラーコードが
「CommonJS(CJS)からES Module(ESM)」へとモダナイゼーションしたことです。

実際にLambdaのハンドラを生成してみると分かりますが、これまでの
index.jsという名前から、index.mjsに変わっています。

ということで、typescriptの開発者としても嬉しい、
esbuildなどのパフォーマンスの高いトランスパイラ全盛時代に移り変わる時期に来ております。

ここではAWS Lambdaを利用する基礎中の基礎であるAWS CLIを使った簡単な使い方を、
esbuildを使ってトランスパイルしたESMベースのハンドラで動作確認してみようとするまでの手順を紹介してみます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

前置き

ここではAWS CLIがあらかじめ使える状態であること前提で話を進めます。

もし
AWS CLIのインストール方法や、利用方法がわからない場合には、AWSの公式リファレンスをみたり、別の詳しいサイトで紹介されている資料をご覧ください。

AWS公式ドキュメンテーション ~ AWS Command Line Interface とはどのようなものですか。

Lambda実行ロールの準備

Lambdaを新規作成する場合、必要最低限のIAMロールが一つ必要です。

参考|Lambda 実行ロール

通常はIAMダッシュボードから作成するか、管理者にロールの付与を依頼しましょう。

以下は適当に作った
lambda_basic_executionというIAM権限ロールです。

合同会社タコスキングダム|蛸壺の技術ブログ

AWS CLIからのLambdaのデプロイにはロールのARN値の設定が必要になります。

ちなみにARNの確認にもCLIコマンドを使うのが便利です。

            
            $ aws iam list-roles | grep Arn
#...ARNのリストが表示される
            "Arn": "arn:aws:iam::************:role/<Lambdaに割り当てる実行権限ロール>",
            #...

#もしくはjqコマンドが使える場合
$ aws iam list-roles | jq '.Roles[].Arn'
#...ARNのリストが表示される
"arn:aws:iam::************:role/<Lambdaに割り当てる実行権限ロール>"
#...
        

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

Lambdaのプロジェクト作成 〜 esbuildでハンドラのビルドする

ここからは簡単なnodejsプロジェクトを作成し、esbuildでハンドラを出力する手順までを説明していきます。

参考|esbuild - An extremely fast bundler for the web

nodejsプロジェクトのリソースを作成

適当なフォルダに移動し、必要最低限のリソースファイルを新規作成していきます。

            
            $ touch package.json index.ts
        
とりあえずpackage.jsonは必要最低限以下のようにしておきます。

            
            {
  "name": "my-lambda",
  "version": "0.0.1",
  "description": "To execise to use AWS lambda with nodejs18."
}
        
package.jsonを生成するだけなら、

            
            $ yarn init
        
でもOKです。

また今回はtypescriptでソースコードを作成するので、以下のようにインストールを済ませておきましょう。

            
            $ yarn add typescript @types/node tslib -D
        
typescriptの初期化(tsconfig.json)を行います。

            
            $ yarn tsc --init
        
これでtsconfig.jsonも出力されました。

ひとまずtypescriptの準備はこれでOKです。

トランスパイラをtscからesbuildに乗り換え

tscでもESMターゲットの出力も可能ではありますが、最近ではよりビルド・バンドルの高速化が出来る・
「esbuild」を使う機会も増えてきていると思います。

インストールも簡単で、

            
            $ yarn add esbuild -D
$ touch esbuild.config.js
        
で利用可能になります。

また通常、
esbuildコマンドの引数でもビルドオプションの指定が可能ですが、オプションが長くなっていくと手動でタイプするのが面倒なので、ここでは最初からesbuild.config.jsを作っておきましょう。

            
            import esbuild from 'esbuild';

esbuild.build({
    entryPoints: [
        './iindex.ts',
    ],
    outdir: 'build',
    outExtension: { '.js': '.mjs' },
    bundle: true,
    minify: true,
    platform: 'node',
    sourcemap: false,
    target: ['node18'],
    plugins: [],
    external: [],
}).catch(() => process.exit(1));
        
で、package.jsonも更新しておきます。

            
            {
    "name": "my-lambda",
    "version": "0.0.1",
    "description": "To execise to use AWS lambda with nodejs18.",
    "scripts": {
        "build": "rm -rf build && node esbuild.config.js"
    },
    "type": "module",
    "devDependencies": {
        "tslib": "^2.4.1",
        "@types/node": "^20.0.0",
        "esbuild": "^0.18.2",
        "typescript": "^4.9.4"
    }
}
        
今回はesbuildで出力するので必然的にESMのプロジェクトとして認識されるので、必要はないですが、標準のtscでトランスパイルする場合、"type": "module",の項目追加が必要です。

esbuild:external指定でnodeパッケージ依存性を制御する

ここでは出番がありませんが、esbuildを使ってAWS Lambdaハンドラを作成する際には、
esbuild:external指定のどこか頭の片隅においておいたほうが良いでしょう。

ローカルの開発環境でインストールしているnodeでは動いても、AWS Lambdaのランタイムとして動作してるnodeでは正常に実行されないものがあり、esbuildでのビルド後のESMコード(mjs)から動作しないモジュールの依存性を取り除く必要があります。

例えばシェルコマンドがnodejs側から呼び出すことのできる
「shelljs」や、常駐デーモンとしてプロセスの稼働状態を監視・管理できる「pm2」などがあります。

こういった通常のAWS Lambdaでは動かないタイプのモジュールは
externalであらかじめ指定しておくことでバンドルから除外することが可能です。

            
            //...中略
esbuild.build({
    //...
    external: [
        'shelljs',
        'pm2'
    ],
}).catch(() => process.exit(1));
        
なお、除外したパッケージは、起動先のnodejs環境に別途グローバルインストールやローカルインストールする必要があることもお忘れなく。

esbuildでindex.tsをビルドする

まだ
index.tsが空っぽのままでしたので、以下のようにとりあえず一番簡単なソースコードを試してみましょう。

            
            export const handler = async (event: any, context: any) => {
    const response: any = { statusCode: 200, body: '' };
    try {
        response.body = {message: 'HELLO, NODEJS V18!!!'};
        return response;
    } catch (error) {
        response.statusCode = 500;
        response.body = `ERROR: ${error}`;
        return response;
    }
}
        
これまでのLambdaハンドラの作法とは少し違ってexport const handler...の出だしがESMの用法となっているのが分かります。

では早速、esbuildをトライしてみます。

            
            $ yarn build
$ ls -la build/
-rw-r--r--    1 node     node           655 Jun 14 07:45 index.mjs
        
ビルド後は非常にコンパクトなindex.mjs(中身はESMファイル)が出力されていればOKです。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

AWS CLIでLambdaをデプロイしてみる

今回のようにアップロードするリソースが小さいサイズで収まる場合には特に必要ないのですが、10MB超えてきたあたりから、ファイルを個別アップロードするのが転送速度的にも厳しくなってきます。

ということで、ここでは
index.mjsファイルたった一つなのですが、最初からZIPでリソースを一つに固めた上で、まとめてデプロイする方法を取ります。

ビルドしたソースを圧縮

適当に
tmpフォルダを作って、そこに一時ファイルとしてzipした圧縮ファイルを保存します。

zipコマンドで
buildフォルダ以下のリリースファイルをすべて圧縮し、tmpへ保存しておきます。

            
            $ mkdir tmp
$ zip -rj tmp/my-function.zip build/*
        

zip圧縮で-j(junk)オプションがないと困った話

個人的に少しハマったのですが、当初は何も考えずに、とりあえず
-rオプション(指定したディレクトリを再帰的に圧縮する)を指定して、

            
            $ zip -r tmp/my-function.zip build/*
        
としていました。

-rオプションだけでは、圧縮後のファイルの中身は、フォルダの階層もそのまま保持されて、buildフォルダの構造までそのまま維持されていることが分かります。

            
            $ zipinfo -1 my-function.zip
build/index.mjs
        
実はこの状態でLambdaにデプロイしてしまうと、Lambdaがbuildフォルダに入っているindex.mjsを見つけられません。

このまま
CloudWatchのログ等を確認すると、

            
            2023-06-14T11:14:24.790Z undefined ERROR Uncaught Exception
{
    "errorType": "Runtime.ImportModuleError",
    "errorMessage": "Error: Cannot find module 'index'\nRequire stack:\n- /var/runtime/index.mjs",
    "stack": [
        "Runtime.ImportModuleError: Error: Cannot find module 'index'",
        "Require stack:",
        "- /var/runtime/index.mjs",
        "    at _loadUserApp (file:///var/runtime/index.mjs:997:17)",
        "    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1032:21)",
        "    at async start (file:///var/runtime/index.mjs:1195:23)",
        "    at async file:///var/runtime/index.mjs:1201:1"
    ]
}
        
というように、index.mjsを正しく読み込めないエラー・「Cannot find module 'index'」が発生してしまいます。

ということで、
buildフォルダごと圧縮されるのが余計ですので、ここでは応急処置として、ファイルの階層構造を無視してくれる-j(junk)オプションを指定することで、buildフォルダの位置を記録せずにトップのディレクトリにファイルを展開してくれるようにできます。

            
            $ zip -rj tmp/my-function.zip build/*
        
確認すると、

            
            $ zipinfo -1 my-function.zip
index.mjs
        
となり、buildフォルダからひん剥かれたリリースコードだけが圧縮されています。

すべてのリソースを一つにバンドルしてくれる
esbuildでは、junkオプションのほうが相性が良い場合があります。

ただし、Lambdaをzipしてアップロードするのに
node_modulesフォルダなどの階層構造がどうしても必要な場合には、もう少しzipコマンドのオプションを丁寧に取り扱う必要があるでしょう。

ともかく、zipでリソースを固めてデプロイする際には、圧縮ファイル内のフォルダ構造にも気を遣う必要があります。

AWS CLIからLambdaへデプロイ

以上で、デプロイの準備が整いましたので、いよいよAWS CLIを使ってリモートへデプロイしてみましょう。

初回のデプロイには、以下のコマンドを利用します。

            
            $ aws lambda create-function \
    --function-name [Lambdaの関数名] \
    --runtime nodejs18.x \
    --zip-file fileb://tmp/my-function.zip \
    --role [Lambdaに割り当てる実行権限ロールのARN] \
    --handler index.handler
        
問題なくデプロイされていれば、Lambdaのリストで表示されていると思います。

            
            $ aws lambda list-functions | grep FunctionName
#...
            "FunctionName": "Lambdaの関数名",
#...
        
さらにデプロイしたLambdaがちゃんと動作するかinvokeサブコマンドを使って確認してみましょう。

            
            $ aws lambda invoke \
    --function-name [Lambdaの関数名] \
    --payload '{"key1":"value1","key2":"value2","key3": "value3"}' \
    response.json
#👇レスポンス
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
        
コード・200が返って来ているので、ひとまずはエラーなくレスポンスが返っていることが確認できます。

また実行後に正常にLambadからレスポンスが返れば、
response.jsonにレスポンスの中身(body)が書き込まれて保存もされていることが分かります。

ちなみにここでは
--payloadオプション(Eventに渡すパラメータ)はなくても良いですが、とりあえず良く使うので付けておきます。

デプロイしたLambdaを更新する場合

最初のデプロイには
create-functionサブコマンドを使いましたが、これ以降のソースコード更新はupdate-function-codeサブコマンドを使います。

例えばソースコードを書き換えて、ビルドした後で、

            
            $ aws lambda update-function-code \
    --function-name [Lambdaの関数名] \
    --zip-file fileb://tmp/my-function.zip
        
とすることで、Lambdaが更新されます。

デプロイしたLambdaを抹消する

なお要らなくなったLambdaは以下のコマンドで一発削除することができます。

            
            $ aws lambda delete-function \
    --function-name [Lambdaの関数名]
        
要らなくなったLamdbaが名前を指定するだけで抹消できるので、後片付けにはとても便利です。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

まとめ

以上、ここではESMベースのソースコードと、最新のLambdaランタイム・Nodejs v18を念頭に、AWS CLIからデプロイするまでの一連の過程を説明してみました。

デプロイしたLambdaの動作確認はあくまで
lambda invokeサブコマンドを使っただけでしたが、最近追加されて注目を浴びている・「関数URL」までは説明していませんでした。

参考|Lambda 関数 URL - AWS公式ドキュメンテーション

この
「関数URL」はちょうど一年前くらいにリリースされた、比較的新しいLambdaの機能です。これまでLambda単体ではWeb APIに出来なかったので、APIGatewayを中継せざるを得なかったのが、この「関数URL」の登場によって、LambdaだけでWeb APIを叩けるようになったです!

この関数URLの詳しい説明とAWS CLIからの設定方法について、
次回の記事で解説していこうかと思います。

参考サイト

Webページでスクロールを禁止する - JavaScript プログラミング

addEventListener for keydown on Canvas

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集