CLOVER🍀

That was when it all began.

TypeScriptで作成したAWS Lambda関数を、LocalStackにデプロイしてみる

これは、なにをしたくて書いたもの?

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

AWS CLI+LocalStack。

$ 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のようなので

Lambda ランタイム - AWS Lambda

ローカルも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

AWS SDKは、v3を使うことにしました。

SDK for JavaScript をインストールする - AWS SDK for JavaScript

APIドキュメントは、こちら。

AWS SDK for JavaScript v3

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関数を呼び出してみます。

同期呼び出し - AWS 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!!"}

日付の部分は、実行する度に変化します。

Amazon S3バケットの中身も確認してみましょう。

$ 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.

Unrelated to the npm package aws-lambda, a CLI tool.

まとめ

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ドキュメントがなかなか読めなくて困りました…。

まあ、いろいろとっかかりにはなったので良しとしましょう。