AWS CDK - API Gateway（REST）+ Lambda Authorizer

# 前回記事

- [AWS CDK - Node.js 18 TypeScript での Lambda 実装](https://qiita.com/takmot/items/83326c5e3e3213038dde)

# 前提条件

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

# ファイル 構成

[前回](https://qiita.com/takmot/items/83326c5e3e3213038dde)から Lambda Authorizer を追加している。

```
  ├─ src/
  │   ├─ lib/
  │   │   ├─ api-gateway/
  │   │   │      ├─ handlers/
  │   │   │      │     └─ sample-api/
  │   │   │      │            └─ index.ts  // Lambdaコード
  │   │   │      │
  │   │   │      ├─ authorizer.ts  // Lambda Authorizer
  │   │   │      └─ resource.ts  // API Gateway + API GatewayがトリガのLambda定義
  │   │   │
  │   │   ├─ sample-stack.ts
  │   │   └─ tsconfig.json
  │   │
  │   └─ main.ts
  │
  // 省略
  │
  ├─ package.json
  ├─ package-lock.json
  └─ tsconfig.json
```

# 実装

src/main.ts、src/lib/sample-stack.ts の実装は、[前回](https://qiita.com/takmot/items/83326c5e3e3213038dde)参照。

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

Authorizer には[CognitoUserPoolsAuthorizer](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.CognitoUserPoolsAuthorizer.html)、[RequestAuthorizer](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RequestAuthorizer.html)、[TokenAuthorizer](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.TokenAuthorizer.html)等があるが、Lambda Authorizer の場合は、RequestAuthorizer を使用する。
[前回](https://qiita.com/takmot/items/83326c5e3e3213038dde)と同様に Lambda Authorizer も`ts`を`mjs`にコンパイルするため`bundling`オプションを指定。
API のルート、メソッド設定は[Defining APIs](https://docs.aws.amazon.com/cdk/api/v1/docs/aws-apigateway-readme.html#defining-apis)を参考に実装。

```typescript:src/lib/api-gateway/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,
    };

    // API Gateway（REST）
    const restApi = new cdk.aws_apigateway.RestApi(scope, 'rest-api', {
      restApiName: `${this._parameters.environment}_rest-api`,
    });

    // Lambda Authorizer
    const authorizerFunction = new cdk.aws_lambda_nodejs.NodejsFunction(scope, 'authorizer-function', {
      functionName: `${this._parameters.environment}_AuthorizerFunction`,
      entry: path.join(__dirname, './authorizer.ts'),
      handler: 'handler',
      runtime,
      memorySize: 1024,
      bundling,
    });

    authorizerFunction.addPermission('authorizer-function-permission', {
      principal: new cdk.aws_iam.ServicePrincipal('apigateway.amazonaws.com'),
      action: 'lambda:InvokeFunction',
    });

    const lambdaAuthorizer = new cdk.aws_apigateway.RequestAuthorizer(scope, 'Authorizer', {
      handler: authorizerFunction,
    });

    // Lambda Role
    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')],
    });

    // Lambda
    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,
    });

    // API Gatewayからの実行権限を追加
    sampleFunction.addPermission('sample-function-permission', {
      principal: new cdk.aws_iam.ServicePrincipal('apigateway.amazonaws.com'),
      action: 'lambda:InvokeFunction',
    });

    // ルート設定（/api/groups/{groupId}/items/{itemId}） + Lambda Authorizer 指定
    restApi.root
      .addResource('api')
      .addResource('groups')
      .addResource('{groupId}')
      .addResource('items')
      .addResource('{itemId}')
      .addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(sampleFunction), {
        authorizer: lambdaAuthorizer,
      });
  }
  }
}
```

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

[Amazon API Gateway Lambda オーソライザーからの出力](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html)に従った形式で値を返却する。
[Amazon API Gateway Lambda オーソライザーへの入力](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-lambda-authorizer-input.html)に Body は含まれないため、その他要素の検証を実装する。
API タイプが HTTP の場合は、レスポンス形式が異なる。
[HTTP API の AWS Lambda オーソライザーの使用](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.payload-format)

```typescript:src/lib/api-gateway/authorizer.ts
import * as lambda from 'aws-lambda';

export const handler: lambda.APIGatewayRequestAuthorizerHandler = async (
  event: lambda.APIGatewayRequestAuthorizerEvent,
): Promise<lambda.APIGatewayAuthorizerResult> => {
  try {
    // 何かしらの検証処理を実装
    return allowPolicy(event);
  } catch (error) {
    console.log(error);
    return denyAllPolicy();
  }
};

const allowPolicy = (event: lambda.APIGatewayRequestAuthorizerEvent): lambda.APIGatewayAuthorizerResult => {
  return {
    principalId: 'sample-user-id',
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: 'Allow',
          Resource: event.methodArn,
        },
      ],
    },
  };
};

const denyAllPolicy = (): lambda.APIGatewayAuthorizerResult => {
  return {
    principalId: '*',
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: '*',
          Effect: 'Deny',
          Resource: '*',
        },
      ],
    },
  };
};
```

# 参考

- [API Gateway REST API + Cognito User Pool Authorizer + Lambda Function な構成を AWS CDK v2 で構築してみた](https://dev.classmethod.jp/articles/api-gateway-rest-apicognito-user-pool-authorizerlambda-i-tried-building-a-functional-configuration-with-aws-cdk-v2/)
- [CDKv2 で Lambda+API Gateway 作成（テストまで）](https://zenn.dev/t_morooka/articles/2951743c240063)