【AWS CDK】LambdaをTypeScriptで書きたい

【AWS CDK】LambdaをTypeScriptで書きたい
この記事をシェアする

こんにちは、クラウドビルダーズのきむです。

LambdaをTypeScriptで書きたい!!!

Lambdaを実装する時はPythonで記述することが多かったのですが、AWS CDKを使っているとTypeScriptで書きたい欲が強くなってきました。今回はAWS CDKを使ってTypeScriptで実装したLambdaをデプロイする方法を紹介します。

やること

  • LambdaをTypeScriptで記述する
  • node_modulesをLambda Layerに含める
  • 自作関数もLambdaLayerに含める
  • AWS CDKでデプロイ

フォルダ構成

最終的なフォルダ構成は下記のとおりです。

.
├── README.md
├── bin
│   └── my-cdk.ts
├── deploy.sh
├── dist
├── cdk.json
├── jest.config.js
├── lambda 
│   ├── lambda-layer
│   │   ├── index.ts
│   │   ├── now
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── package-lock.json 
│   ├── package.json
│   ├── tsconfig.json
│   └── src
│       └── index.ts
├── lib
│   └── my-cdk-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
│   └── my-cdk.test.ts
└── tsconfig.json

Lambdaの実装

まずはLambdaを実装していきます。

ソースコードを作成する前にプロジェクトルートのtsconfig.jsonに手を加えます。

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["es2020", "dom"],
    "declaration": true,
    "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"],
    "outDir": "dist/nodejs/node_modules/lambda-layer",
    "paths": {
      "lambda-layer": ["./lambda/lambda-layer"] ★追加
    }
  },
  "exclude": ["node_modules", "cdk.out"]
}

自作関数は~/lambda/lambda-layer/に格納するのですが、ローカル環境とLambda環境の両環境でライブラリ解決ができるように”paths”を設定しています。

~/lambda/lambda-layer/にもtsconfig.jsonを配置します。

プロジェクトルートの設定を継承して一部設定を上書きしています。

{
  "compilerOptions": {
    "declaration": false,
    "inlineSourceMap": false,
    "inlineSources": false
  },
  "extends": "../../tsconfig.json"
}

~/lambda/src/にデプロイするLambdaを配置します。

import { Handler } from 'aws-lambda';
import { now } from 'lambda-layer';

export const handler: Handler = async (event, context) => {
  const currentTIme = now();

  return currentTIme;
};

自作関数nowを呼び出してその結果を返すだけの単純な関数です。

nowの実装は下記のとおりです。

import * as dayjs from 'dayjs';
import * as utc from 'dayjs/plugin/utc';
import * as timezone from 'dayjs/plugin/timezone';

export const now = () => {
  dayjs.extend(utc);
  dayjs.extend(timezone);

  const currentTime = dayjs().tz('Asia/Tokyo');
  return currentTime.format('YYYY-MM-DD HH:mm:ss');
};

dayjsを使用して現在の日本時刻を返す関数です。

作成した関数を~/lambda/lambda-layer/now/に配置します。

次に~/lambda/lambda-layer/にindex.tsを作成して、nowをexportすることでメインのソースコードからimport { now } from ‘lambda-layer’でimportできるようにています。

export { now } from './now';

Lambda Layerにnode_modulesを含めるために~/lambda/にpackage.jsonとpackage-lock.jsonを配置します。package.jsonの中身は下記のとおりです。

{
  "name": "lambda",
  "version": "1.0.0",
  "dependencies": {
    "dayjs": "^1.11.12"
  }
}

~/lambda/lambda-layer/でbuildを実行するのでここにもpackage-jsonを配置します。

{
  "name": "lambda-layer",
  "version": "1.0.0",
  "scripts": {
    "build": "tsc"
  }
}

CDKの実装

AWS CDKでLambda用のIAMロール、Lambda、Lambda Layerを作成していきます。

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';

import { MyCDKStack } from '../lib/my-cdk-stack';

import 'source-map-support/register';

interface EnvConfig {
  account?: string;
  region?: string;
}

const app = new cdk.App();

const env: EnvConfig = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
};

new MyCDKStack(app, `MyCDKStack`, {
  env,
});
import * as cdk from 'aws-cdk-lib';

import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';

import { Construct } from 'constructs';

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

    const role = new iam.Role(this, 'LambdaRole', {
      roleName: 'lambda-role',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'service-role/AWSLambdaBasicExecutionRole'
        ),
      ],
    });

    const layer = new lambda.LayerVersion(this, 'Layer', {
      layerVersionName: 'common-layer',
      code: lambda.Code.fromAsset('dist'),
      compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
      compatibleArchitectures: [lambda.Architecture.X86_64],
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    new nodejs.NodejsFunction(this, 'Lambda', {
      functionName: 'test-lambda',
      runtime: lambda.Runtime.NODEJS_20_X,
      entry: 'lambda/src/index.ts',
      memorySize: 128,
      timeout: cdk.Duration.seconds(3),
      role,
      layers: [layer],
      bundling: {
        externalModules: ['*'], // Layerに含まれるライブラリをbundleから全て除外
        format: nodejs.OutputFormat.ESM, // ESMフォーマットでコードをスッキリに
      },
    });
  }
}

ポイントはLambdaのbundlingオプションです。

bundlingの有無でコンパイル後のパッケージサイズやコードの可読性が大きく変わります。

bundlingオプション無し

externalModules: [‘*’]のみ

externalModules: [‘*’] + format: nodejs.OutputFormat.ESM

externalModules: [‘*’] + format: nodejs.OutputFormat.ESMを使用するとパッケージサイズも小さくなりbundle後のコードがかなり見やすくなります。

Lambdaはコンテナイメージを使わない場合のパッケージサイズは250 MBまでとなっているので積極的に活用していきたいです。

デプロイ

それではデプロイしてみましょう。

コンパイルからcdk deployまでの一連の処理をdeploy.shに定義しています。

TypeScriptで実装したLambda Layer用のソースコードはJavaScriptにコンパイルする必要があるので~/lambda/lambda-layer/でnpm run buildを実行します。

コンパイル後のjsファイルは~/dist/nodejs/node_modules/lambda-layer/に配置します。

#!/bin/bash
set -e

rm -rf dist                                                  # 古いフォルダを削除
mkdir -p dist/nodejs/node_modules                            # フォルダ作成
cp lambda/package.json lambda/package-lock.json dist/nodejs/ # package.jsonとpackage-lock.jsonを移動

pushd dist/nodejs # ~/dist/nodejs/でnpm ciを実行
npm ci
popd

pushd lambda/lambda-layer # ~/lambda/lambda-layer/でbuildを実行
npm run build
popd

npm run cdk:deploy # プロジェクトルートでcdk deploy

deploy.shに実行権限を付与してdeploy.shを実行すればデプロイ完了です。

さいごに

Lambda Layerをローカル環境とLambda環境の両環境でライブラリ解決できるようにするために頭を悩ませていましたが、結果から見たらtsconfig.jsonに少し手を加えるだけで簡単に行うことができました。

AWS CDKならTypeScriptで実行したLambdaも簡単にデプロイすることができますのでぜひお試しください。

この記事をシェアする
著者:きむ
フェスが好きなエンジニア