AWS CDK - Node.js 18 TypeScript での Lambda 実装

# 前提条件

- IaC は CDK（CDK v2）
- Lambda のランタイムは Node.js 18（拡張子は.mjs）
- Lambda の実装は TypeScript（AWS SDK for JavaScript v3）

# ファイル 構成

`cdk init`で自動生成される構成から多少変更している。
[実践！AWS CDK #7 ファイル分割](https://dev.classmethod.jp/articles/cdk-practice-7-split-file/)を参考に、AWS リソースごとにリソース定義ファイルを分割している。
Lambda のコンパイル用に別途`tsconfig.json`を用意する。

```
  ├─ src/
  │   ├─ lib/
  │   │   ├─ api-gateway/
  │   │   │      ├─ handlers/
  │   │   │      │     └─ sample-api/
  │   │   │      │            └─ index.ts  // Lambdaコード
  │   │   │      │
  │   │   │      └─ resource.ts  // API Gateway + API GatewayがトリガのLambda定義
  │   │   │
  │   │   ├─ sample-stack.ts
  │   │   └─ tsconfig.json  // Lambdaのコンパイルに使用するtsconfig
  │   │
  │   └─ main.ts
  │
  ├─ .gitignore
  ├─ .npmignore
  ├─ cdk.json
  ├─ jest.config.js
  ├─ package.json
  ├─ package-lock.json
  └─ tsconfig.json  // CDKコンパイルに使用するtsconfig
```

# 実装

## tsconfig.json

基本的に`cdk init`で自動生成されたものそのまま

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["es2020"],
    "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"],
    "esModuleInterop": true
  },
  "exclude": ["node_modules", "cdk.out"]
}
```

## src/lib/tsconfig.json

上記の`tsconfig.json`をベースにするため、`extends`で指定。
`ts`を`mjs`にコンパイルするため、`module`に`ES2022`を指定。

```src/lib/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  }
}
```

## src/main.ts

```typescript:src/main.ts
import * as cdk from 'aws-cdk-lib';
import { SampleStack } from './lib/sample-stack';

const app = new cdk.App();
new SampleStack(app, 'SampleStack');
```

## src/lib/sample-stack.ts

```typescript:src/lib/sample-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

import { ApiGateway } from './api-gateway/resource';

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

    const environment = 'develop';

    const apiGateway = new ApiGateway({
      environment,
    });
    apiGateway.createResources(this);
  }
}

```

## src/lib/api-gateway/resource.ts

Lambda のランタイムに Node.js 18 を指定。
Lambda の bundling オプション`tsconfig`で`src/lib/tsconfig.json`を指定。

```typescript:resource.ts
import * as path from 'path';

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

interface Parameters {
  environment: string;
}

export class ApiGateway {
  private _parameters: Parameters;

  constructor(parameters: Parameters) {
    this._parameters = parameters;
  }

  public createResources(scope: Construct) {
    const runtime = cdk.aws_lambda.Runtime.NODEJS_18_X;

    const bundling = {
      minify: true,
      sourceMap: true,
      externalModules: ['@aws-sdk/*'],
      tsconfig: path.join(__dirname, '../tsconfig.json'),
      format: cdk.aws_lambda_nodejs.OutputFormat.ESM,
    };

    const lambdaBasicRole = new cdk.aws_iam.Role(scope, 'lambda-basic-role', {
      assumedBy: new cdk.aws_iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')],
    });

    const sampleFunction = new cdk.aws_lambda_nodejs.NodejsFunction(scope, 'sample-function', {
      functionName: `${this._parameters.environment}_sampleFunction`,
      entry: path.join(__dirname, './handlers/sample-api/index.ts'),
      handler: 'handler',
      runtime,
      memorySize: 1024,
      role: lambdaBasicRole,
      bundling,
    });
  }
}
```

## src/lib/api-gateway/handlers/sample-api/index.ts

```typescript:src/lib/api-gateway/handlers/sample-api/index.ts
export const handler = async event => {
  console.log(event.path);
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ type: 'sample' }),
  };
};

```

# 参考

- [[アップデート] AWS Lambda が Node.js 18 をサポートしました](https://dev.classmethod.jp/articles/aws-lambda-support-node-js-18/)
- [実践！AWS CDK #7 ファイル分割](https://dev.classmethod.jp/articles/cdk-practice-7-split-file/)