AWS CDK L2 Constructに引数を追加する対応をステップごとに解説

# はじめに
AWS CDKへのOSSコントリビューションの一環としてL2 Constructへの引数追加対応を実施しました。PR提出までに色々と学ぶことが多かったので、記事にまとめておきます。

https://github.com/aws/aws-cdk/pull/34198

# 前提知識：L1 ConstructとL2 Construct
AWS CDKにはL1 Construct, L2 Constructというものがあります。
（L3 Constructもありますが本記事では登場しないため割愛します）

L1 ConstructはCloufFormationのスタックと一対一で対応します。ソースコード的には、aws-cdk-libパッケージの中に*.generated.tsというファイルが自動生成されておりこのファイルの中にCfn*というクラスがあります。`Cfn`で始まるこのクラスをL1 Constructと呼びます。

例えばAPI GatewayのL1 Constructである`CfnRestApi`クラスは以下の通りです。

```ts:apigateway.generated.ts
/**
 * The `AWS::ApiGateway::RestApi` resource creates a REST API.
 *
 * For more information, see [restapi:create](https://docs.aws.amazon.com/apigateway/latest/api/API_CreateRestApi.html) in the *Amazon API Gateway REST API Reference* .
 *
 * > On January 1, 2016, the Swagger Specification was donated to the [OpenAPI initiative](https://docs.aws.amazon.com/https://www.openapis.org/) , becoming the foundation of the OpenAPI Specification.
 *
 * @cloudformationResource AWS::ApiGateway::RestApi
 * @stability external
 * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-restapi.html
 */
export class CfnRestApi extends cdk.CfnResource implements cdk.IInspectable, cdk.ITaggable {
```
JSDocに`* @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-restapi.html`と記載されている通り、CloudFormationの`AWS::ApiGateway::RestApi`というスタックと対応しています。

一方、L2 ConstructはL1を抽象化したものです。L1との違いは、便利なメソッドを持っていたりコンストラクタ引数に独自の型が定義されていたりなどしている点です。素のL1は使いにくいところがあるのでL2で使いやすくしたようなイメージです。

例えばAPI GatewayのL2 Constructはrestapi.tsのRestApiクラスです。

```ts:restapi.ts
/**
 * Represents a REST API in Amazon API Gateway.
 *
 * Use `addResource` and `addMethod` to configure the API model.
 *
 * By default, the API will automatically be deployed and accessible from a
 * public endpoint.
 */
export class RestApi extends RestApiBase {

// (中略)

constructor(scope: Construct, id: string, props: RestApiProps = { }) {
    super(scope, id, props);
```

ここで注目すべき点はconstructorの引数にRestApiPropsクラスが利用されている点です。RestApiPropsの実装は以下です。

```ts
export interface RestApiProps extends RestApiOptions {

  /**
   * The list of binary media mime-types that are supported by the RestApi
   * resource, such as "image/png" or "application/octet-stream"
   *
   * @default - RestApi supports only UTF-8-encoded text payloads.
   */
  readonly binaryMediaTypes?: string[];

  /**
   * A nullable integer that is used to enable compression (with non-negative
   * between 0 and 10485760 (10M) bytes, inclusive) or disable compression
   * (when undefined) on an API. When compression is enabled, compression or
   * decompression is not applied on the payload if the payload size is
   * smaller than this value. Setting it to zero allows compression for any
   * payload size.
   *
   * @default - Compression is disabled.
   * @deprecated - superseded by `minCompressionSize`
   */
  readonly minimumCompressionSize?: number;

  /**
   * A Size(in bytes, kibibytes, mebibytes etc) that is used to enable compression (with non-negative
   * between 0 and 10485760 (10M) bytes, inclusive) or disable compression
   * (when undefined) on an API. When compression is enabled, compression or
   * decompression is not applied on the payload if the payload size is
   * smaller than this value. Setting it to zero allows compression for any
   * payload size.
   *
   * @default - Compression is disabled.
   */
  readonly minCompressionSize?: Size;

  /**
   * The ID of the API Gateway RestApi resource that you want to clone.
   *
   * @default - None.
   */
  readonly cloneFrom?: IRestApi;

  /**
   * The source of the API key for metering requests according to a usage
   * plan.
   *
   * @default - Metering is disabled.
   */
  readonly apiKeySourceType?: ApiKeySourceType;
}
```

引数にSizeやIRestApiなどの独自の型を採用しています。これにより、constructorの利用者が引数に想定外の値を渡すリスクが減少し、型を用いた安全な実装が可能になります。

より詳細な情報はCDKのBlackbeltが参考になります。
参考：https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_AWS-CDK-Basic-1-Overview_0731_v1.pdf

# Propsクラスに引数を追加する
AWSのサービスアップデートに応じてL1 Constructのconstructor引数は自動で追加されていきます。L2 Constructのconstructor引数もL1に追従して追加される必要がありますが、L2の引数は自動では追加されないため人の手による引数追加対応が必要です。

冒頭で紹介した私のPRでは、API GatewayのL2 Constructに`mode`という引数を追加しました。SpecRestApiPropsクラスにmodeを追加しています。

```diff:restapi.ts
export interface SpecRestApiProps extends RestApiBaseProps {
  /**
   * An OpenAPI definition compatible with API Gateway.
   * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-import-api.html
   */
  readonly apiDefinition: ApiDefinition;
  /**
   * A Size(in bytes, kibibytes, mebibytes etc) that is used to enable compression (with non-negative
   * between 0 and 10485760 (10M) bytes, inclusive) or disable compression
   * (when undefined) on an API. When compression is enabled, compression or
   * decompression is not applied on the payload if the payload size is
   * smaller than this value. Setting it to zero allows compression for any
   * payload size.
   *
   * @default - Compression is disabled.
   */
  readonly minCompressionSize?: Size;

+   /**
+    * The Mode that determines how API Gateway handles resource updates.
+    *
+    * Valid values are `overwrite` or `merge`.
+    *
+    * For `overwrite`, the new API definition replaces the existing one.
+    * The existing API identifier remains unchanged.
+    *
+    * For `merge`, the new API definition is merged with the existing API.
+    *
+    * If you don't specify this property, a default value is chosen:
+    * - For REST APIs created before March 29, 2021, the default is `overwrite`
+    * - For REST APIs created after March 29, 2021, the new API definition takes precedence, but any container types such as endpoint configurations and binary media types are merged with the existing API.
+    *
+    * Use the default mode to define top-level RestApi properties in addition to using OpenAPI.
+    * Generally, it's preferred to use API Gateway's OpenAPI extensions to model these properties.
+    *
+    * @default - `merge` for REST APIs created after March 29, 2021, otherwise `overwrite`
+    */
+   readonly mode?: RestApiMode;
+ }
```

SpecRestApiPropsに引数を追加したので、SpecRestApiのconstructorも修正します。

```diff:restapi.ts
  constructor(scope: Construct, id: string, props: SpecRestApiProps) {
    super(scope, id, props);
    // Enhanced CDK Analytics Telemetry
    addConstructMetadata(this, props);
    const apiDefConfig = props.apiDefinition.bind(this);
    this.resourcePolicy = props.policy;
    const resource = new CfnRestApi(this, 'Resource', {
      name: this.restApiName,
      policy: Lazy.any({ produce: () => this.resourcePolicy }),
      failOnWarnings: props.failOnWarnings,
      minimumCompressionSize: props.minCompressionSize?.toBytes(),
      body: apiDefConfig.inlineDefinition ?? undefined,
      bodyS3Location: apiDefConfig.inlineDefinition ? undefined : apiDefConfig.s3Location,
      endpointConfiguration: this._configureEndpoints(props),
      parameters: props.parameters,
      disableExecuteApiEndpoint: props.disableExecuteApiEndpoint,
+     mode: props.mode,
    });
```

:::note info
L2 Constructに不足している引数を探す時は以下のツールがおすすめです。
https://d1upnzw71mlot9.cloudfront.net/
:::

# Unit test/Integration testを修正
テストコードも修正が必要です。
Unit testはrestapi.test.ts、Integration testはinteg.spec-restapi.tsにあるのでそれぞれ修正します。

```ts:restapi.test.ts
  test.each([
    [apigw.RestApiMode.OVERWRITE, 'overwrite'],
    [apigw.RestApiMode.MERGE, 'merge'],
    [undefined, undefined],
  ])('mode property is set (%s)', (mode, expectedMode) => {
    // WHEN
    const api = new apigw.SpecRestApi(stack, 'api', {
      apiDefinition: apigw.ApiDefinition.fromInline({ foo: 'bar' }),
      mode,
    });

    api.root.addMethod('GET');

    // THEN
    Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', {
      Name: 'api',
      Mode: expectedMode ?? Match.absent(),
    });
  });
```

```ts:integ.spec-restapi.ts
class Test extends cdk.Stack {
  constructor(scope: cdk.App, id: string) {
    super(scope, id);
    const api = new apigateway.SpecRestApi(this, 'my-api', {
      apiDefinition: apigateway.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')),
      disableExecuteApiEndpoint: true,
      minCompressionSize: Size.bytes(1024),
      retainDeployments: true,
      cloudWatchRole: true,
      deployOptions: {
        cacheClusterEnabled: true,
        stageName: 'beta',
        description: 'beta stage',
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
        methodOptions: {
          '/api/appliances/GET': {
            cachingEnabled: true,
          },
        },
      },
      mode: apigateway.RestApiMode.MERGE, //この１行を追加
    });
```

# README.mdを修正
引数追加のPRのタイトルは`feat`で始まることになります。featのPRはREADME.mdの修正も必要となります。今回はAPI GatewayのL2 Constructを修正したため、packages/aws-cdk-lib/aws-apigateway/README.mdを修正します。

このREADMEはCDK API Referenceの[aws-cdk-lib.aws_apigatewayのOverview](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway-readme.html)と対応しています。RosettaというツールがREADMEからリファレンスを生成しているようです。

# PR提出・マージ
ここまでできたらPRを提出し、レビューを受けてマージしてもらいましょう。

# さいごに
以上です。L1とL2の概念理解に苦労しましたが概念さえわかればあとはコードを書くだけ、という感じでしたね。コーディングの量は多くなかったです。

また、CDKコントリビューション界隈は初学者向けの資料が本当に充実しているため非常に助かりました。参考になった資料を以下に貼っておきます。

https://jaws-ug-cdk.github.io/cdk-conf-2024-contribute-workshop/

https://speakerdeck.com/badmintoncryer/cdkkontoribiyutonozui-chu-nobi-woyue-eyou-jian-dan-issuenojian-tukefang