TypeScript + Parcel で、JavaScriptをバンドルしてLambda Functionをデプロイしてみた

はじめに

CX事業本部@札幌の佐藤です。最近は案件で使用することもあって、TypeScript、Node.js、その他の周辺ツールについて再勉強中です。TypeScriptを使うにあたって、トランスパイルやモジュールバンドラの知識が疎かったため、Parcelというツールを実際に触ってみました。

概要

Lambda FunctionはネイティブではTypeScriptをサポートしていないため、TypeScript標準のtscwebpack などを使って、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に値が格納されていることを確認できました。うまく動いていそうです。

参考