AWS CDKでAPI Gatewayのアクセスログの送信先にFirehoseストリームを設定する

## はじめに
今回は AWS CDKを使い以下を行ったため、そのソースコードのご紹介です。
* <b>API GatewayのアクセスログをS3へ保存するFirehoseストリームの作成</b>
* <b>上記Firehoseストリームを送信先とするAPI Gateway APIの作成</b>

言語はTypeScirptになります。

[前回投稿内容](https://qiita.com/nao-United92/items/c9456a37f30ec7e1940b)と照らし合わせながらご紹介していきます。

## 目次
1. [必要となるAWSリソース🔨](#必要となるawsリソース)
2. [IAMロールの作成](#iamロールの作成)
3. [S3バケットの作成](#s3バケットの作成)
4. [Firehoseストリームの作成](#firehoseストリームの作成)
5. [API Gateway APIの作成](#api-gateway-apiの作成)
6. [まとめ📝](#まとめ)
7. [参考文献🌟](#参考文献)

## 必要となるAWSリソース🔨
今回のゴールである、API Gatewayのアクセスログの送信先にFirehoseストリームを設定を行う上で、必須となるAWSリソースは以下です。
* IAMロール：Firehoseストリームが必要とする許可を設定
* S3バケット：Firehoseストリームが配信するログの記録先
* Firehoseストリーム：API Gateway アクセスログの送信先
* API Gateway API：アクセスログの送信先としてFirehoseストリームを設定


## IAMロールの作成
まずは、IAMロールの作成です。
Firehoseストリームの[サービスアクセス](https://qiita.com/nao-United92/items/c9456a37f30ec7e1940b#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%82%92%E7%B7%A8%E9%9B%86)で設定するロールになります。

```typescript
  // Create Role
  const firehoseDeliveryRole = new Role(scope, `AmazonApigatewayMyApiFirehoseDeliveryRole`, {
    roleName: `AmazonApigatewayMyApiFirehoseDeliveryRole`,
    assumedBy: new ServicePrincipal('firehose.amazonaws.com'),
    inlinePolicies: {
      ecsServicePolicy: new PolicyDocument({
        statements: [
          new PolicyStatement({
            actions: [
              's3:AbortMultipartUpload',
              's3:GetBucketLocation',
              's3:GetObject',
              's3:ListBucket',
              's3:ListBucketMultipartUploads',
              's3:PutObject',
            ],
            resources: [`arn:aws:s3:::amazon-apigateway-my-api*`],
          }),
          new PolicyStatement({
            actions: ['lambda:InvokeFunction'],
            resources: [`arn:aws:lambda:${scope.region}:${scope.account}:function:FirehoseTransformFunction`],
          }),
        ],
      }),
    },
  });
```

### S3バケットの作成
続いて、S3バケットの作成です。

```typescript
  // Create S3
  const logS3 = new Bucket(scope, `AmazonApigatewayMyApiLogS3`, {
    accessControl: BucketAccessControl.PRIVATE,
    blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
    bucketName: `amazon-apigateway-my-api-log-ap-northeast-1`,
    encryption: BucketEncryption.S3_MANAGED,
    removalPolicy: RemovalPolicy.DESTROY,
  });
  cdk.Tags.of(logS3).add('Name', `amazon-apigateway-my-api-log-ap-northeast-1`);
  cdk.Tags.of(logS3).add('Role', 'S3');
```

### Firehoseストリームの作成
続いて、Firehoseストリームの作成です。
Firehoseストリームの設定では、以下3つの内容を指します。
* [詳細設定](https://qiita.com/nao-United92/items/c9456a37f30ec7e1940b#%E9%80%81%E4%BF%A1%E5%85%88%E3%81%AE%E8%A8%AD%E5%AE%9A)
* [レコードを編集、変換、および転換](https://qiita.com/nao-United92/items/c9456a37f30ec7e1940b#%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E7%B7%A8%E9%9B%86%E5%A4%89%E6%8F%9B%E3%81%8A%E3%82%88%E3%81%B3%E8%BB%A2%E6%8F%9B)
* [送信先の設定](https://qiita.com/nao-United92/items/c9456a37f30ec7e1940b#%E9%80%81%E4%BF%A1%E5%85%88%E3%81%AE%E8%A8%AD%E5%AE%9A)

※`レコードを編集、変換、および転換`はCloudWatchLogsへのログ記録で必要な設定で、CDKだと[processingConfiguration](https://docs.aws.amazon.com/ja_jp/firehose/latest/APIReference/API_ProcessingConfiguration.html)を指しますが、<b>今回は直接Firehoseストリーム > S3となるため定義なしとしています。</b>

```typescript
  // Create KinesisDeliveryStream
  const logDeliveryStream = new CfnDeliveryStream(scope, `AmazonApigatewayMyApiLogStream`, {
    deliveryStreamName: `amazon-apigateway-my-api-log-stream`,
    deliveryStreamType: 'DirectPut',
    extendedS3DestinationConfiguration: {
      bucketArn: logS3.bucketArn,
      bufferingHints: {
        intervalInSeconds: 900,
        sizeInMBs: 50,
      },
      compressionFormat: 'UNCOMPRESSED',
      prefix: 'apigateway/',
      roleArn: firehoseDeliveryRole.roleArn,
    },
  });
```

### API Gateway APIの作成
最後に、API Gateway APIの作成です。
`MyApi`はAPI名です。
`accessLogDestination: new FirehoseLogDestination()`で、アクセスログの送信先として上記で作成したFirehoseストリーム(`logDeliveryStream`)を設定しています。

```typescript
// Create ApiGateway Api
const myApi = new RestApi(this, 'MyApi', {
  restApiName: 'myApi',
  defaultCorsPreflightOptions: {
    allowOrigins: Cors.ALL_METHODS,
        allowMethods: ['GET', 'OPTIONS'],
        statusCode: 204,
      },
      deployOptions: {
        stageName: this.apiGatewayStage,
        accessLogDestination: new FirehoseLogDestination(logDeliveryStream),
        accessLogFormat: AccessLogFormat.jsonWithStandardFields(),
        metricsEnabled: true,
        loggingLevel: MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        tracingEnabled: true,
  },
});
```

## まとめ📝
これまでの処理をまとめると以下のような感じです。
可読性向上のために以下を行いましたが、ファイル分割しても良いかもです( ^ω^ )

* Firehoseストリームの作成処理をメソッド化
* ログ名(logName)・ゾーン名(zoneName)は引数渡しで共通化

```typescript
import { RemovalPolicy } from 'aws-cdk-lib';
import * as cdk from 'aws-cdk-lib';
import {
  AccessLogFormat,
  FirehoseLogDestination,
  MethodLoggingLevel,
  RestApi,
} from 'aws-cdk-lib/aws-apigateway';
import { CfnDeliveryStream } from 'aws-cdk-lib/aws-kinesisfirehose';
import { PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { BlockPublicAccess, Bucket, BucketAccessControl, BucketEncryption } from 'aws-cdk-lib/aws-s3';

// ApiGateway アクセスログ配信用Firehoseストリーム作成
const ApiGWFirehoseStream: CfnDeliveryStream = createApiGWAccessLogResource(this, 'amazon-apigateway-my-api', zoneName);

// Create ApiGateway Api
const myApi = new RestApi(this, 'MyApi', {
  restApiName: 'myApi',
  defaultCorsPreflightOptions: {
    allowOrigins: Cors.ALL_METHODS,
        allowMethods: ['GET', 'OPTIONS'],
        statusCode: 204,
      },
      deployOptions: {
        stageName: this.apiGatewayStage,
        accessLogDestination: new FirehoseLogDestination(ApiGWFirehoseStream),
        accessLogFormat: AccessLogFormat.jsonWithStandardFields(),
        metricsEnabled: true,
        loggingLevel: MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        tracingEnabled: true,
  },
});

/**
 * ApiGatewayのアクセスログをS3へ保存するためのFirehoseストリームを作成
 *
 * @remarks
 *
 * @param scope - スコープ
 * @param logName - ログ名
 * @param zoneName - DNSZoneName
 * @returns Firehoseストリーム
 */
export function createApiGWAccessLogResource(scope: cdk.Stack, logName: string, zoneName: string): CfnDeliveryStream {
  // Snake形からCamel形に変換
  const logNameCamel = snakeToCamel(logName);

  // Create Role
  const firehoseDeliveryRole = new Role(scope, `${logNameCamel}FirehoseDeliveryRole`, {
    roleName: `${logNameCamel}FirehoseDeliveryRole`,
    assumedBy: new ServicePrincipal('firehose.amazonaws.com'),
    inlinePolicies: {
      ecsServicePolicy: new PolicyDocument({
        statements: [
          new PolicyStatement({
            actions: [
              's3:AbortMultipartUpload',
              's3:GetBucketLocation',
              's3:GetObject',
              's3:ListBucket',
              's3:ListBucketMultipartUploads',
              's3:PutObject',
            ],
            resources: [`arn:aws:s3:::${logName}*`],
          }),
          new PolicyStatement({
            actions: ['lambda:InvokeFunction'],
            resources: [`arn:aws:lambda:${scope.region}:${scope.account}:function:FirehoseTransformFunction`],
          }),
        ],
      }),
    },
  });

  // Create S3
  const logS3 = new Bucket(scope, `${logNameCamel}LogS3`, {
    accessControl: BucketAccessControl.PRIVATE,
    blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
    bucketName: `${logName}-log-${zoneName}`,
    encryption: BucketEncryption.S3_MANAGED,
    removalPolicy: RemovalPolicy.DESTROY,
  });
  cdk.Tags.of(logS3).add('Name', `${logName}-log-${zoneName}`);
  cdk.Tags.of(logS3).add('Role', 'S3');

  // Create KinesisDeliveryStream
  const logDeliveryStream = new CfnDeliveryStream(scope, `${logNameCamel}LogStream`, {
    deliveryStreamName: `${logName}-log-stream`,
    deliveryStreamType: 'DirectPut',
    extendedS3DestinationConfiguration: {
      bucketArn: logS3.bucketArn,
      bufferingHints: {
        intervalInSeconds: 900,
        sizeInMBs: 50,
      },
      compressionFormat: 'UNCOMPRESSED',
      prefix: 'apigateway/',
      roleArn: firehoseDeliveryRole.roleArn,
    },
  });

  return logDeliveryStream;
}

/**
 * Snake > Camel形に変換する
 * @remarks
 * 例：`amazon-apigateway-my-api`は`AmazonApigatewayMyApi`に変換される
 * @param str - 変換したい文字列
 *
 * @returns 変換後結果
 */
export function snakeToCamel(str: string): string {
  str = str.replace(/([-_]\w)/g, (g) => g[1].toUpperCase());
  return str.charAt(0).toUpperCase() + str.slice(1);
}
```

## 参考文献🌟
* [ProcessingConfiguration](https://docs.aws.amazon.com/ja_jp/firehose/latest/APIReference/API_ProcessingConfiguration.html)
* [FirehoseLogDestination](https://construct-hub-testing.dev-tools.aws.dev/packages/aws-cdk-lib/v/2.175.1/api/FirehoseLogDestination?lang=typescript&submodule=aws_apigateway)