CDKのL2 Constructで指定できないパラメータをL1 Constructで指定する

## 概要

CDK（L2 Construct）でAWSリソースを作っていると指定できないプロパティがあります。

CDKのバージョンを上げれば解消されることもありますが、いつでも最新のCDKが使える状況ではないかもしれません。また、最新化したとしても依然として指定できないケースもあります。

より低レイヤーなL1 Constructを使うことで解決する方法を紹介します。


## 前提

### バージョン情報

* CDK：2.89.0
* TypeScript

## CDKのレイヤーについて

CDKには大きく３つのレイヤーが存在しています。

|レイヤー名|説明|
|:-:|:-:|
|L1|Cfnのテンプレートを直接記述できる。L2よりも細かい指定ができる。|
|L2|普通に書いてたらこのレイヤーを使ってる。|
|L3|本記事とは無関係なので割愛。|

極端な例ですが、L2でS3バケットを作ろうと思った場合以下のように1行書けば作れます。

```typescript
const bucket = new s3.Bucket(this, 'MyFirstBucket');
```

作成したバケットにバケットポリシーをつけたらかったら`addToResourcePolicy()`を使えばいいです。

```typescript
const bucket = new s3.Bucket(this, 'MyBucket');
const result = bucket.addToResourcePolicy(
  new iam.PolicyStatement({
    actions: ['s3:GetObject'],
    resources: [bucket.arnForObjects('file.txt')],
    principals: [new iam.AccountRootPrincipal()],
  })
);
```

L2を使うことでこのように少ないコード量でリソースを定義することができるのは大きなメリットでしょう。

## L2では書けないケース

CDKのバージョンが上がるにつれて少しずつ減ってきていますが、新しいサービスや整備が進んでいないサービスだとL2では書けないことがあります。いくつか例を出しましょう。

### 例１：S3 サーバーアクセスのログ記録

S3にはアクセスログを記録する機能があります。（下図）

![Monosnap test-system-maintenance-v2-339713138572-apne1-dev - S3 バケット | S3 | ap-northeast-1 2024-10-06 16-44-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654639/942e58d3-c9c2-8d6d-093b-a4568d6ea420.png)

ログの保存先としてS3バケットを指定することができますが、設定パラメータの中でログオブジェクトキーの形式を指定することができます。

![Monosnap サーバーアクセスのログ記録を編集 - S3 バケット test-system-maintenance-v2-339713138572-apne1-dev | S3 | ap-northeast-1 2024-10-06 16-48-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654639/6dccf5e9-1a9f-e8c9-c2fb-30335406d0c1.png)

Athenaでの検索を考慮するとパーティション分割されている後者を使いたいところです。最新のCDKだと`targetObjectKeyFormat`が用意されているので問題なく指定できますが、バージョンが`2.89.0`だとこれが指定できません。

![スクリーンショット 2024-10-06 16.54.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654639/53db9c66-3e6e-a52d-55ad-f822ecd2bddf.png)

### 例２：Glue

L2すらないようなケースもあります。

`aws-cdk-lib.aws_glue module`のリファレンスの冒頭には次のような記述があります。

>There are no official hand-written (L2) constructs for this service yet. Here are some suggestions on how to proceed:

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_glue-readme.html

実験的なライブラリとして`@aws-cdk/aws-glue-alpha`が提供されていますのでこちらを利用することで少し書くのが楽になります。

ただし、不足している部分は多いです。Serde parametersの設定などはできませんので例えば先ほどのアクセスログをAthenaで検索するには以下のようなテーブル定義をすればいいのですが、`input.regex`などは指定できないです。

```sql
CREATE EXTERNAL TABLE `s3_access_logs_db.mybucket_logs`(
  `bucketowner` STRING, 
  `bucket_name` STRING, 
  `requestdatetime` STRING, 
  `remoteip` STRING, 
  `requester` STRING, 
  `requestid` STRING, 
  `operation` STRING, 
  `key` STRING, 
  `request_uri` STRING, 
  `httpstatus` STRING, 
  `errorcode` STRING, 
  `bytessent` BIGINT, 
  `objectsize` BIGINT, 
  `totaltime` STRING, 
  `turnaroundtime` STRING, 
  `referrer` STRING, 
  `useragent` STRING, 
  `versionid` STRING, 
  `hostid` STRING, 
  `sigv` STRING, 
  `ciphersuite` STRING, 
  `authtype` STRING, 
  `endpoint` STRING, 
  `tlsversion` STRING,
  `accesspointarn` STRING,
  `aclrequired` STRING)
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.serde2.RegexSerDe' 
WITH SERDEPROPERTIES ( 
  'input.regex'='([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)(?: ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*))?.*$') 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://amzn-s3-demo-bucket1-logs/prefix/'
```

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/using-s3-access-logs-to-identify-requests.html

## 解決策

こういったケースの解決策の一つとしてL1の利用が考えられます。

先の例のケースで具体的に見ていきます。

### 例１：S3 サーバーアクセスのログ記録

```typescript
// バケット作成（これはL2）※ バケット作成時のプロパティは割愛
const bucket = new s3.Bucket(this, 'MyBucket');
// L2で定義したリソースをL1で再定義 
const cfnBucket = bucket.node.defaultChild as CfnBucket;
// L1でのリソース設定
cfnBucket.addPropertyOverride("LoggingConfiguration", {
          TargetObjectKeyFormat: {
            "PartitionedPrefix": {
              "PartitionDateSource": "EventTime" // パーティション分割の基準となる日付はイベント発生日時とする。
            }
          }
```

L1で再定義するところは`{L2リソース}.node.defaultChild as {適切なデータ型}`という形式で取得できます。

`addPropertyOverride()`の中身はCfnのリファレンスを見て頑張って記載します。今回の例だと`TargetObjectKeyFormat`で指定できることはわかっているのでそこを起点に調べてみます。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-targetobjectkeyformat.html

`PartitionedPrefix`が用意されていて、`PartitionDateSource`として`EventTime`か`DeliveryTime`が指定できるそうなのでそのように指定しています。


### 例２：Glue

こっちも同じです
```typescript

(table.node.defaultChild as CfnTable).addPropertyOverride("TableInput", {
        "StorageDescriptor": {
          "SerdeInfo": {
            "Parameters": {
              "input.regex": '([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)(?: ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*))?.*$',
              "serialization.format": '1'
            }
          }
        }
      });
```




プロパティの部分は今までL1を使ってないとしんどいですが、慣れれば難しくないと思います。

## 参考文献

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3-readme.html