TypeScript+webpack+AWS CDKで開発環境を構築してLambdaをデプロイしてみた

TypeScriptでLambdaのコードを記述してwebpackでトランスパイル・バンドルする手順を紹介します。TypeScriptで開発環境を構築して運用していくのに良い選択だと思います。LambdaなどをTypeScriptで継続して開発する予定の場合は参考にしてください。
2020.06.23

はじめに

CX事業本部@東京の佐藤智樹です。

今回はTypeScriptでLambdaのコードを記述してwebpackでトランスパイル・バンドルする手順を紹介します。LambdaのデプロイにはAWS CDKを使用します。普段使っている方法で別記事で引用しようと思ったのですが見当たらなかったので個別の記事にしました。

この記事を読めばAWS CDKとTypeScriptで継続して開発する際に便利な環境が構築できるようになります。今回の記事で作成した内容を土台としてTypeScriptでサーバーレスアプリーケーションなどの構築にも活用できます。普段使用している設定を入れているので慣れたら適宜好みに合わせて変更してください。

実行環境

項目 バージョン
OS Mac OS Catalina
AWS CDK 1.46.0
TypeScript 3.7.2
yarn 1.21.1
webpack 4.43.0

webpackを使う利点

TypeScriptは現状そのままだとLambdaで実行できないのでJavaScriptへトランスパイルする必要があります。単なるコードの変換だけでなく依存関係のあるライブラリもバンドルしてデプロイする必要があります。

Lambda Layerにnode_modulesを追加する方法などもありますが、node_modulesに入っている余分なファイルもアップロードされます。webpackであればバンドルする際に、実行で必要なライブラリだけ選別してまとめることができるなどバンドル設定を細かく指定ができたり色々利点があります。

webpackの詳細については公式ドキュメントか以下の記事が詳細に書かれているのでおすすめです。自分も公式ドキュメントと以下の記事をみてパラメータなどを理解しました。

実行環境の整備

本章では実行環境に必要なコマンドラインツールなどをインストールします。

AWS CDK、yarnのインストール

まずnpmを使用してCDKのCLIをインストールします。npmをインストールしていない場合は、brewなどを使用してインストールしてください。

$ npm install -g aws-cdk

次に今回はnpmの代わりにyarnでパッケージを管理するので、yarnをインストールしてください。

$ brew install yarn

CDKでプロジェクトを作成

以下のコマンドでcdkのプロジェクトを作成できます。

$ cdk init --language typescript

またnpmはこの後使用しないのでnpm用のファイルは以下のコマンドで削除します。

$ rm package-lock.json

yarnでwebpackとトランスパイルに関連するライブラリを追加

以下のコマンドでyarnを使用して依存関係のあるライブラリをインポート後、webpackに必要な設定とTypeScriptをJavaScriptにトランスパイルするためのts-loaderを追加します。デプロイ時には不要な設定なので、yarn addの際には-Dを付けてデプロイ時には含まれないようにします。

yarn install
yarn add -D webpack webpack-cli webpack-node-externals ts-loader

webpackとTypeScriptを設定

webpack.config.jsを使用してwebpackのバンドル設定を記述します。 本来であればwebpack initコマンドでconfigファイルを生成したかったのですが、うまく動かなかったので今回はtouchで空ファイルを作成します。

$ touch webpack.config.js

作成したファイルを以下のように編集します。コメント欄は簡単な解説のため記載しているので適宜削除してください。コメントで記述していない内容は最初に紹介したwebpackの記事でほとんど紹介されています。

webpackのentry内で指定している./src/lambda/handlers/handler.tsのファイルを元に依存関係などがバンドルされてdistフォルダ配下にJavaScriptファイルが作成されます。

webpack.config.js

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'development',
  target: 'node',
  entry: {
    handler: path.resolve(
      __dirname,
      './src/lambda/handlers/handler.ts',
    ),
  },
  // 依存ライブラリをデプロイ対象とするか設定(対象はpackage.json参照)
  // devDependencies:開発時に必要なライブラリを入れる
  // dependencies:実行時に必要なライブラリを入れる
  externals: [
    nodeExternals({
      modulesFromFile: {
        exclude: ['dependencies'],
        include: ['devDependencies'],
      },
    }),
  ],
  output: {
    filename: '[name]/index.js',
    path: path.resolve(__dirname, 'dist'),
    libraryTarget: 'commonjs2',
  },
  // 変換後ソースと変換前ソースの関連付け
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        // ローダーが処理対象とするファイルを設定
        test: /\.ts$/,
        // 先ほど追加したts-loaderを設定
        use: [
          {
            loader: 'ts-loader',
          },
        ],
      },
    ],
  },
  // import時のファイル指定で拡張子を外す
  // https://webpack.js.org/configuration/module/#ruleresolve
  resolve: {
    extensions: ['.ts', '.js'],
  },
};

次にTypeScriptの設定を変更します。変更する内容は型定義ファイルをtsファイルと同じ場所に出力させないだけなので飛ばしても問題ないです。

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "lib": ["es2018"],
    "declaration": false,  ←ここだけfalseに変更
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": false,
    "inlineSourceMap": true,
    "inlineSources": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "typeRoots": ["./node_modules/@types"]
  },
  "exclude": ["cdk.out"]
}

最後にデプロイなどのコマンドを簡略化するためscript配下の内容を変更します。buildwebpackdeploycdk deployに設定します。項目の記載がない場合追加してください。

{
  "name": "lambda-unit",
  "version": "0.1.0",
  "bin": {
    "lambda-unit": "bin/lambda-unit.js"
  },
  "scripts": {
    "build": "webpack",
    "deploy": "cdk deploy",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
  "devDependencies": {
    "@aws-cdk/assert": "1.46.0",
    "@types/jest": "^25.2.1",
    "@types/node": "10.17.5",
    "@webpack-cli/info": "^0.2.0",
    "aws-cdk": "1.45.0",
    "jest": "^25.5.0",
    "ts-jest": "^25.3.1",
    "ts-loader": "^7.0.5",
    "ts-node": "^8.1.0",
    "typescript": "~3.7.2",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12",
    "webpack-node-externals": "^1.7.2"
  },
  "dependencies": {
    "@aws-cdk/core": "1.46.0",
    "source-map-support": "^0.5.16"
  }
}

Lambdaを実装

Lambdaの実装を追加します。webpackの設定で指定した./src/lambda/handlers/handler.tsを作成します。まずはデプロイするためのディレクトリを作成します。

$ mkdir -p src/lambda/handlers

次にLambdaのソースを作成します。内容はデプロイのテストだけできれば良いので簡易なものにしています。

handler.ts

export interface TestEvent {
    id: number;
    eventValue?: string;
  }
  
  export async function handler(event: TestEvent): Promise<TestEvent | void> {
    console.log(JSON.stringify(event));
    return event;
  }

CDKの設定

デプロイするためにCDKのライブラリ追加と設定変更を行います。まずはLambdaを使用するためにyarnでcdkのライブラリを追加します。

$ yarn add "@aws-cdk/aws-lambda"

次にLambdaをデプロイするための設定をCDKのスタックファイルに記述します。今回はシンプルにLambdaの追加のみを行います。スタック名はlambda-unitというフォルダでプロジェクトを作成したのでそれが反映されています。

lib/lambda-unit-stack.ts

import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';

export class LambdaUnitStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new lambda.Function(
      this,
      'testFunction',
      {
        // webpackでバンドルしたファイルを設定
        code: lambda.Code.fromAsset(`dist/handler`),
        functionName: `test-handler`,
        handler: 'index.handler',
        runtime: lambda.Runtime.NODEJS_12_X,
        timeout: cdk.Duration.seconds(10),
      },
    );
  }
}

以上で事前の設定は完了です。

webpackの実行とAWS環境へのデプロイ

まず以下のコマンドでwebpackを実行してソースをバンドルします。

$ yarn build

するとdistフォルダが自動で作成され、dist/handlerフォルダの中にトランスパイルされたindex.jsファイルが作成されます。

dist/handler/index.js

(中略)
/***/ "./src/lambda/handlers/handler.ts":
/*!****************************************!*\
  !*** ./src/lambda/handlers/handler.ts ***!
  \****************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

Object.defineProperty(exports, "__esModule", { value: true });
async function handler(event) {
    console.log(JSON.stringify(event));
    return event;
}
exports.handler = handler;
(中略)

次にCDKでデプロイを行っていきます。まず最初にaws-cliを使用できる状態にしてからS3バケットの作成などを以下のコマンドで実行します。

$ cdk bootstrap
 ⏳  Bootstrapping environment aws://XXXXXXXXXXXX/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
 0/3 | 0:00:35 | CREATE_IN_PROGRESS   | AWS::S3::Bucket       | StagingBucket 
 0/3 | 0:00:38 | CREATE_IN_PROGRESS   | AWS::S3::Bucket       | StagingBucket Resource creation Initiated
 1/3 | 0:01:00 | CREATE_COMPLETE      | AWS::S3::Bucket       | StagingBucket 
 1/3 | 0:01:03 | CREATE_IN_PROGRESS   | AWS::S3::BucketPolicy | StagingBucketPolicy 
 1/3 | 0:01:05 | CREATE_IN_PROGRESS   | AWS::S3::BucketPolicy | StagingBucketPolicy Resource creation Initiated
 2/3 | 0:01:05 | CREATE_COMPLETE      | AWS::S3::BucketPolicy | StagingBucketPolicy 
 3/3 | 0:01:06 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit 
 ✅  Environment aws://XXXXXXXXXXXX/ap-northeast-1 bootstrapped.
✨  Done in 46.59s.

次にいよいよ以下のコマンドでデプロイを行います。一度IAMリソースの更新で確認がでるのでyを押して、問題なければ以下のようにデプロイが完了します。

$ yarn deploy 
~
IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐
│   │ Resource                        │ Effect │ Action         │ Principal                    │ Condition │
├───┼─────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${testFunction/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴─────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────┬──────────────────────────────────────────────────────────────────────────┐
│   │ Resource                    │ Managed Policy ARN                                                       │
├───┼─────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ + │ ${testFunction/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecuti │
│   │                             │ onRole                                                                   │
└───┴─────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘
~
Do you wish to deploy these changes (y/n)? y
LambdaUnitStack: deploying...
[0%] start: Publishing 93ee72a91afad5a3251db04b4367b5c7195b59350b289b13e96fd78ed999999:current
[100%] success: Published 93ee72a91afad5a3251db04b4367b5c7195b59350b289b13e96fd78ed1999999:current
LambdaUnitStack: creating CloudFormation changeset...
 0/4 | 0:02:09 | CREATE_IN_PROGRESS   | AWS::IAM::Role        | testFunction/ServiceRole (testFunctionServiceRoleFEC29B6F) 
 0/4 | 0:02:09 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata 
 0/4 | 0:02:10 | CREATE_IN_PROGRESS   | AWS::IAM::Role        | testFunction/ServiceRole (testFunctionServiceRoleFEC29B6F) Resource creation Initiated
 0/4 | 0:02:11 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata Resource creation Initiated
 1/4 | 0:02:11 | CREATE_COMPLETE      | AWS::CDK::Metadata    | CDKMetadata 
 2/4 | 0:02:27 | CREATE_COMPLETE      | AWS::IAM::Role        | testFunction/ServiceRole (testFunctionServiceRoleFEC29B6F) 
 2/4 | 0:02:30 | CREATE_IN_PROGRESS   | AWS::Lambda::Function | testFunction (testFunction483F4CBA) 
 2/4 | 0:02:31 | CREATE_IN_PROGRESS   | AWS::Lambda::Function | testFunction (testFunction483F4CBA) Resource creation Initiated
 3/4 | 0:02:32 | CREATE_COMPLETE      | AWS::Lambda::Function | testFunction (testFunction483F4CBA) 
 4/4 | 0:02:34 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | LambdaUnitStack 

 ✅  LambdaUnitStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/LambdaUnitStack/99999999-b3d0-11ea-ad3a-999999999999
✨  Done in 73.86s.

上記のような表示がでればデプロイは問題なく完了です。念のためAWSのWebコンソールで状態を確認します。先ほどトランスパイルしたJavaScriptのコードがweb上でも確認できます。

念のためデフォルトでテストイベントを作成してテストしたところ問題なく動作することが確認できました。

感想

webpack使うだけの記事がなさそうだったので紹介しました。単発で書き捨てる開発なら単純にトランスパイルでも良いですが、ライブラリの更新など継続的な開発を意識した場合はこのような設定をとって開発が行われています。

自分も普段は出来合いの設定を使っていたので、最小限必要な設定が分かり勉強になりました。他にも色々方法はありますが結構使いやすいかと思いますのでよかったら試してみてください。