TypeScript + Parcel で、JavaScriptをバンドルしてLambda Functionをデプロイしてみた
はじめに
CX事業本部@札幌の佐藤です。最近は案件で使用することもあって、TypeScript、Node.js、その他の周辺ツールについて再勉強中です。TypeScriptを使うにあたって、トランスパイルやモジュールバンドラの知識が疎かったため、Parcelというツールを実際に触ってみました。
概要
Lambda FunctionはネイティブではTypeScriptをサポートしていないため、TypeScript標準のtsc
や webpack
などを使って、Nodejsにトランスパイルしてからデプロイする必要があります。JavaScriptのモジュールバンドラとしてはwebpackが有名ですが、webpack.config.js
のような設定ファイルを書く必要があるため、設定ファイル肥大化、属人化の問題があるなどが巷では言われていました。そうしたなか、Parcelという設定ファイル不要でモジュールバンドルができるツールが出てきました。
Parcel とは
Parcel とはJavascriptのモジュールバンドラの一種です。webpack との大きな違いは、webpack.config.js
などのような設定ファイルが必要ない点です。設定ファイルを書かなくても、Parcelのcliを叩くだけで簡単に使うことができます。webpackに比べて機能がシンプルで、学習コストが少ない点が魅力の一つだと思います。プロジェクトでシンプルにTypeScriptをトランスパイルして、モジュールバンドルしてくれればいいんだ!というような場合には、採用してみても良いかもしれません。私は今回初めて使ってみましたが、特に迷うこともなく、すごく簡単に使えました。
サンプルリポジトリ
以下のリポジトリに今回のサンプルリポジトリを作っています。Lambdaのサンプルコードは弊社和田が作成したtsasというコマンドラインツールで、Lambdaのサンプルプロジェクトを作る機能があるためそれを使っています。
https://github.com/briete/blog-parcel-typescript-lambda
TypeScript + Parcelを使って、NodejsにトランスパイルしてLambdaにデプロイする
デプロイツールは、SAMでもCloudFormationでもなんでも良いですが、今回はAWS CDKを使っていきます。AWS CDKはTypeScriptの雛形プロジェクトを作るのにも使えるのでとても便利です。
プロジェクトの作成
AWS CDKを使って、TypeScriptプロジェクトの雛形を作成します。
mkdir parcel-typescript-lambda-sample cd parcel-typescript-lambda-sample cdk init app --language=typescript
Parcelをインストールする
npmでプロジェクトにparcelをインストールします。インストールする際は、parcel
ではなくて、parcel-bundler
なので注意します。
npm install --save-dev parcel-bundler
インストールできたら、プロジェクト直下のpackage.json
に以下を追加します。
{ "name": "parcel-typescript-lambda-sample", "version": "0.1.0", "bin": { "parcel-typescript-lambda-sample": "bin/parcel-typescript-lambda-sample.js" }, "scripts": { "build": "tsc", "test": "jest", "cdk": "cdk", "parcel:build": "parcel build --target node --out-file index.js --no-source-maps 'src/lambda/handlers/**/*'" }, "devDependencies": { "@aws-cdk/assert": "^1.17.1", "@types/jest": "^24.0.22", "@types/node": "10.17.5", "aws-cdk": "^1.17.1", "jest": "^24.9.0", "parcel-bundler": "^1.12.4", "ts-jest": "^24.1.0", "ts-node": "^8.1.0", "typescript": "~3.7.2" }, "dependencies": { "@aws-cdk/core": "^1.17.1", "source-map-support": "^0.5.16" } }
設定ファイルなどは必要ないので、これだけでOKです。すごく簡単ですね。設定を変更する場合は、Parcelの実行時に適宜オプションを指定していく形になります。
src/
配下でLambda FunctionのTypeScriptコードを書く
ここは今回は割愛します。
TypeScriptをトランスパイルする
さきほど、package.json
にnpm scriptsを追加したので、parcel:build
を実行します。TypeScriptをトランスパイルして、JavaScriptファイルに変換します。プロジェクト直下のdist/
フォルダ配下にモジュールバンドルされた結果のindex.js
ファイルが作成されます。
npm run parcel:build
実際には以下の、コマンドが実行されています。
parcel build --target node --out-file index.js --no-source-maps 'src/lambda/handlers/**/*'
parcel build --target node
: TypeScriptをNodejsにトランスパイルします。
--out-file index.js
: index.jsとしてdist/以下に作成されます。
--no-source-maps
: ソースマップは必要ないため、除外します。
src/lamnda/handlers/**/*
: Lambdaにデプロイするため、モジュールバンドルする対象をLambdaのハンドラファイルとします。ここは適宜、ハンドラファイルがあるディレクトリを指定するか、単体のファイルを指定することができます。
実行すると、dist/
フォルダ配下にindex.js
が作成されました。トランスパイル後のコードが肥大化してしまうため、node_modules
はバンドルの対象にはしませんでした。node_modules
については、Lambda Layerを使用する方針です。
Lambda Layersにnode_modulesをデプロイ
Lambda Layersへnode_modulesをデプロイするためには、フォルダ構成がnodejs/node_modules
になるようにデプロイする必要があります。そのためプロジェクトに新しくフォルダを作成して、そこにnode_modulesを含めます。Layerのデプロイのパス指定はlayer_modules/
配下を指定します。
mkdir -p layer_modules/nodejs cp package.json layer_modules npm install --production --prefix layer_modules/nodejs
Lambdaをデプロイする
CDKでデプロイします。Lambdaのアセットを指定するところに、dist/
配下のトランスパイル後のjsファイルを指定します。ハンドラはindex.handler
となります。
import cdk = require('@aws-cdk/core'); import lambda = require('@aws-cdk/aws-lambda'); import dynamodb = require('@aws-cdk/aws-dynamodb'); export class ParcelTypescriptLambdaSampleStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // Lambda Layerの作成 const sampleLambdaLayer = new lambda.LayerVersion(this, 'sampleLayer', { compatibleRuntimes: [lambda.Runtime.NODEJS_10_X], // 先ほど作った、layer_modules配下を指定します。 code: lambda.AssetCode.fromAsset('layer_modules') }); // DynamoDBテーブルの作成 const sampleDynamoTable = new dynamodb.Table(this, 'sampleDynamoDBTable', { partitionKey: { name: 'greetingId', type: dynamodb.AttributeType.STRING, }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST }); // Lambda Functionの作成 const sampleFunction = new lambda.Function(this, 'sample', { runtime: lambda.Runtime.NODEJS_10_X, // トランスパイル後のJSファイルを指定します。 code: lambda.Code.fromAsset('dist'), handler: 'index.handler', environment: { GREETING_TABLE_NAME: sampleDynamoTable.tableName }, layers: [sampleLambdaLayer] }); sampleDynamoTable.grantFullAccess(sampleFunction) } }
npm run build cdk deploy
デプロイできました。
Lambdaを実行
デプロイできたので、LambdaをCLIで実行してみます。
$ aws lambda invoke --function-name hogehoge --payload '{ "name": "cm-satonaoya" }' outputfile.txt { "ExecutedVersion": "$LATEST", "StatusCode": 200 } $ aws dynamodb scan --table-name hogehoge { "Count": 1, "Items": [ { "greetingId": { "S": "2e0f08e3-cec6-4175-8dc1-e9f005d6c643" }, "description": { "S": "my first message." }, "title": { "S": "hello, cm-satonaoya" } } ], "ScannedCount": 1, "ConsumedCapacity": null }
DynamoDBに値が格納されていることを確認できました。うまく動いていそうです。