これは、なにをしたくて書いたもの?
TypeScriptでAWS Lambda関数を書いてみようかなと思いまして。
LocalStackにデプロイして、動かしてみるところまでやってみました。
考え方?
TypeScriptを使った例は、こちらにも出てくるのですが。
Lambda 関数の作成と使用 - AWS SDK for JavaScript
https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javascriptv3/example_code/lambda
肝心の、アップロードするzipファイルの作り方が書いてありません。
どうやら、TypeScriptファイルのビルド後にpackage.json
(およびpackage-lock.json
)をコピー+npm install
して
zipファイルを作るという流れになるみたいです。
では、やってみましょう。
環境
今回の環境は、こちら。
LocalStack。
$ localstack --version 0.12.19.3
起動。
$ LAMBDA_EXECUTOR=docker-reuse localstack start
$ awslocal --version aws-cli/2.3.3 Python/3.8.8 Linux/5.4.0-89-generic exe/x86_64.ubuntu.20 prompt/off
AWS LambdaのサポートしているNode.jsのバージョンで、現時点で最新なのはv14のようなので
ローカルもv14のNode.jsを使っておくことにしました。
$ node -v v14.18.1 $ npm -v 6.14.15
TypeScriptのバージョン。一応、Prettierも使っています。
$ npx tsc --version Version 4.4.4 $ npx prettier --version 2.4.1
tsconfig.json
は、このように設定。
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "baseUrl": "./src", "outDir": "dist", "strict": true, "noImplicitAny": true, "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "sourceMap": true, "skipLibCheck": true }, "include": [ "src" ] }
.prettierrc.json
については、特に中身はありません。
.prettierrc.json
{}
お題
単に文字列を返すだけの関数を作成してもなんなので、LocalStackのAmazon S3にアクセスする関数を作成することに
しましょう。
準備
必要なライブラリをインストールします。
型宣言。
$ npm i -D @types/node @aws-sdk/types
Amazon S3にアクセスするためのライブラリ。
$ npm i @aws-sdk/client-s3
SDK for JavaScript をインストールする - AWS SDK for JavaScript
APIドキュメントは、こちら。
v3では、各サービスごとにクライアントをインストールする形態みたいです。
package.json
での依存関係は、こうなりました。
"devDependencies": { "@aws-sdk/types": "^3.38.0", "@types/node": "^16.11.6", "prettier": "2.4.1", "typescript": "^4.4.4" }, "dependencies": { "@aws-sdk/client-s3": "^3.39.0" }
AWS Lambda関数からアクセスする、S3バケットの作成。
$ awslocal s3 mb s3://my-bucket
これで準備は完了です。
ソースコードを作成する
では、ソースコードを作成します。
src/index.ts
import { S3 } from "@aws-sdk/client-s3"; const localstackEndpoint = `http://${process.env["LOCALSTACK_HOSTNAME"]}:${process.env["EDGE_PORT"]}`; const s3 = new S3({ endpoint: localstackEndpoint, forcePathStyle: true }); export async function handler(event: any, context: any): Promise<object> { const requestMessage = event.message; const now = new Date().toISOString(); const bodyMessage = `[${now}] ${requestMessage}`; const putResult = await s3.putObject({ Bucket: "my-bucket", Key: "my-file", Body: bodyMessage, }); return { message: bodyMessage, }; }
ポイントですが、LocalStack内で起動するAWS Lambda関数でAmazon S3やAWS DynamoDBにアクセスする際には、
LOCALSTACK_HOSTNAME
とEDGE_PORT
という環境変数を使うと良さそうです。
特にこのことが書かれているのは、LOCALSTACK_HOSTNAME
ですね。
LocalStack / Debugging Configurations
あと、Amazon S3にアクセスする際にはforcePathStyle
をtrue
にしておかないと、うまくアクセスできません。
S3 | S3 Client - AWS SDK for JavaScript v3
const localstackEndpoint = `http://${process.env["LOCALSTACK_HOSTNAME"]}:${process.env["EDGE_PORT"]}`; const s3 = new S3({ endpoint: localstackEndpoint, forcePathStyle: true });
S3#putObject
に渡す引数が最初APIドキュメントからはよくわからなかったのですが…
PutObjectCommandInput | S3 Client - AWS SDK for JavaScript v3
よく見るとこう書いてあったので
This interface extends from PutObjectRequest interface. There are more parameters than Body defined in PutObjectRequest
こちらも合わせてみるのが正解だと…。
PutObjectRequest | S3 Client - AWS SDK for JavaScript v3
APIドキュメントの読み方は、まだちょっと慣れない感じがしますね。
あと、handlerのany
が気持ち悪い気がしますが、これはオマケで補足します。
※特にAWS SDK v2の場合
export async function handler(event: any, context: any): Promise<object> {
ビルドとパッケージングは、こんなステップで実行しました。
まずはTypeScriptファイルをビルド。今回の設定だと、dist
ディレクトリ内に出力されます。
$ npx tsc
TypeScriptファイルのビルドが終わったら、dist
ディレクトリにpackage.json
とpackage-lock.json
をコピー。
$ cp package*.json dist
dist
ディレクトリ内で、dependencies
の依存関係のみインストール。
$ cd dist $ npm i --production
ビルドされた.js
ファイルとnode_modules
ディレクトリを、zipにまとめます。
$ zip -r function.zip index.js node_modules
元のディレクトリに戻って
$ cd ..
最後にawslocal lambda create-function
でデプロイです。
.zip ファイルアーカイブで Node.js Lambda 関数をデプロイする - AWS Lambda
$ awslocal lambda create-function \ --function-name my-typescript-function \ --zip-file fileb://dist/function.zip \ --handler index.handler \ --runtime nodejs12.x \ --role test-role
更新と削除は、こちらですね。
$ awslocal lambda update-function-code \ --function-name my-typescript-function \ --zip-file fileb://dist/function.zip $ awslocal lambda delete-function --function-name my-typescript-function
デプロイしたAmazon Lambda関数を呼び出してみます。
$ awslocal lambda invoke --function-name my-typescript-function --payload '{"message": "Hello World!!"}' --cli-binary-format raw-in-base64-out result.json
結果。
result.json
{"message":"[2021-11-03T13:31:26.607Z] Hello World!!"}
日付の部分は、実行する度に変化します。
$ awslocal s3 cp s3://my-bucket/my-file - [2021-11-03T13:31:26.607Z] Hello World!!
OKですね。
オマケ:scriptsでpackage.jsonをコピーするようにする
package.json
でのzipパッケージングの定義は、このようにまとめました。
"scripts": { "build": "tsc", "clean": "rm -rf dist", "format": "prettier --write src", "lambda-package": "npm run build && cp package*.json dist/ && cd dist && npm i --production && zip -r function.zip *" },
zipファイルにまとめるところは、だいぶざっくりになっていますが。
こちらのコマンドでビルドすると
$ npm run lambda-package
dist/function.zip
ができるのでデプロイすればOKです。
オマケ: aws-lambda型宣言
hanlder関数の引数がany
になっているのがちょっと気持ち悪いところですが、
export async function handler(event: any, context: any): Promise<object> {
この型宣言は@types/aws-lambda
で追加できます。
$ npm i -D @types/aws-lambda
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/README.md
ぱっと見るとaws-lambda
というパッケージの型宣言に見えるのですが、こちらはCLIツールだったりします。
README.md
にはCLIツールであるaws-lambda
とは無関係なことが書かれています。
Types helpful for implementing handlers in the AWS Lambda NodeJS runtimes, the handler interface and types for AWS-defined trigger sources.
まとめ
TypeScriptでAWS Lambda関数を作成して、LocalStackにデプロイしてみました。
割と困ったのは、TypeScriptでAWS Lambda関数をどう宣言したらいいのかがわからなくて、いろいろ見た結果
引数をany
にすることにしたのですが。
こちらにあるソースコードだと、型を省略していたのでそういうものかもしれません…。
https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javascriptv3/example_code/lambda/src
あとは、AWS SDK for JavaScript v3のAPIドキュメントがなかなか読めなくて困りました…。
まあ、いろいろとっかかりにはなったので良しとしましょう。