CDK で 列挙型 SubnetType を利用する際に注意する点

## これは何?
AWS CDKのL3コンストラクトである[aws_ecs_patterns](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html)を試してみて、RDSも必要だなーと思ってRDSを作成した際、サブネット周りのエラー内容になります。
稼働プロジェクト上でのサブネットの追加や参照の仕方についても考察したいと思います。

## 事前準備
CDKプロジェクト上の`lib/test.ts`で、以下のように**ApplicationLoadBalancedFargateService**によってALB + ECS on Fargate環境を構築しました。
ECSクラスターをProtectedサブネット上に置き、Nat Gateway経由でインターネットにアウトバウンドします。

```ts:lib/test.ts
const vpc = new ec2.Vpc(this, `${props.resourceName}-vpc`, {
    vpcName: `${props.resourceName}-vpc-01`,
    ipAddresses: ec2.IpAddresses.cidr(props.vpcCidrValue),
    natGateways: 1,
});

vpc.addInterfaceEndpoint(`${props.resourceName}-edpif-dkr`, { service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER, });
vpc.addInterfaceEndpoint(`${props.resourceName}-edpif-ecr`, { service: ec2.InterfaceVpcEndpointAwsService.ECR, });
vpc.addGatewayEndpoint(`${props.resourceName}-edpgw-s3`, { service: ec2.GatewayVpcEndpointAwsService.S3, });

/* ---------- ECS Cluster ---------- */
const cluster = new ecs.Cluster(this, `${props.resourceName}-ecs-cluster`, {
    vpc: vpc,
});

/* ---------- ECR ---------- */
const ecrNginxRepo = ecr.Repository.fromRepositoryName(this, `${props.resourceName}-ecr-nginx`, `${props.resourceName}/nginx`);

/* ---------- ECS Patterns ---------- */
const ecsServiceAlbFargate = new ecs_patterns.ApplicationLoadBalancedFargateService(this, `${props.resourceName}-ecspatterns`, {
    cluster: cluster,
    taskImageOptions: {
        image: ecs.ContainerImage.fromEcrRepository(ecrNginxRepo),
    },
    taskSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
    },
    assignPublicIp: false,
    deploymentController: {
        type: ecs.DeploymentControllerType.ECS,
    }
});

```

上記を`cdk deploy`コマンドによって環境構築した後、RDSへの接続も検証するため、以下のようにRDSを追加しました。

サブネットタイプは`PRIVATE_ISOLATED`と指定しました。

```ts:lib/test.ts
// 追加
/* ---------- RDS ---------- */
const rdsInstance = new rds.DatabaseInstance(this, `${props.resourceName}-rds-mysql`, {
    vpc: vpc,
    engine: rds.DatabaseInstanceEngine.MARIADB,
    instanceType: new ec2.InstanceType("t3.micro"),           // db.t3.micro
    vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
    storageType: rds.StorageType.GP3,
    multiAz: true,
    allocatedStorage: 20,
    credentials: rds.Credentials.fromGeneratedSecret(
        'mariadb',
        { secretName: `${props.resourceName}/mariadb/pw`, }
    ),
    securityGroups: [sgRds],
});
```

しかし、`cdk synth`にて、以下のようなエラーに遭遇しました。

```bash 
Error: There are no 'Isolated' subnet groups in this VPC. Available types: Deprecated_Private_NAT,Private,Deprecated_Private,Public
    :
    :
```


## エラーの原因
RDS上インスタンスの作成場所を `SubnetType.PRIVATE_ISOLATED` として定義してPRIVATE_ISOLATEDの箇所にインスタンスを作成しようとしたが、そもそもSubnetType上でPRIVATE_ISOLATEDのサブネットが存在せず、エラーになったとのことです。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.SubnetType.html


上記リンクの通り、NatGatewayを作成したことでサブネットタイプがPRIVATE_WITH_EGRESSと定義されていること & PRIVATE_ISOLATEDに該当するサブネットが環境上で存在しないことでこのようなエラーメッセージが発生しました。。。


## 確認方法
すでに作成してしまっている場合、VPCのPropertiesである[publicSubnets](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html#publicsubnets)などで、直接出力することもできます。

```typescript:lib/test.js
console.log(`publicSubnets = ${vpc.publicSubnets}`);
console.log(`privateSubnets = ${vpc.privateSubnets}`);
console.log(`isolatedSubnets = ${vpc.isolatedSubnets}`);
```

上記を追加して`cdk synth`を実行することで, どのタイプのサブネットがあるのかを把握できます。
以下の実行結果は、IsolatedSubnetがない出力結果となります。

```bash:出力結果
publicSubnets = TestStack/test-vpc/PublicSubnet1,TestStack/test-vpc/PublicSubnet2,TestStack/test-vpc/PublicSubnet3
privateSubnets = TestStack/test-vpc/PrivateSubnet1,TestStack/test-vpc/PrivateSubnet2,TestStack/test-vpc/PrivateSubnet3
isolatedSubnets = 
```

また、新規でVPCを作成する場合は、subnetConfiguration を明示的に定義し、PRIVATE_WITH_EGRESS と PRIVATE_ISOLATEDの役割を持つサブネットを2つ作成するようにすればいいかと思います。

```ts:lib/test.ts
const vpc = new ec2.Vpc(this, `test-vpc`, {
    vpcName: `test-vpc-01`,
    ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
    subnetConfiguration: [
        {
            cidrMask: 24,
            name: 'Public',
            subnetType: ec2.SubnetType.PUBLIC,
        },
        {
            cidrMask: 24,
            name: 'Protected',
            subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
        {
            cidrMask: 24,
            name: 'Private',
            subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
    ],
    natGateways: 2,
});
```

## 既存VPC上でのサブネットの追加
上記のやり方での欠点としては、既存のVPCでsubnetConfigurationを定義しようとすると場合によってはReplaceの対象になるため、プロジェクトで稼働中のリソースでのケースでは避けたいアプローチです。

[Vpc](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html)コンストラクトでてっきりaddSubnetのようなメソッドがあるかと思ったらありませんでした。

仕方がないため、以下のようにPrivateSubnetコンストラクトを追記して、IsolatedSubnetを作成します。

```ts:lib/test.ts
    :
const isolatedSubneta = new ec2.PrivateSubnet(this, `${props.resourceName}-subnet-isolated-a`, {
    vpcId: vpc.vpcId,
    availabilityZone: "ap-northeast-1a",
    cidrBlock: "10.0.200.0/24",
});
const isolatedSubnetc = new ec2.PrivateSubnet(this, `${props.resourceName}-subnet-isolated-c`, {
    vpcId: vpc.vpcId,
    availabilityZone: "ap-northeast-1c",
    cidrBlock: "10.0.210.0/24",
});
const isolatedSubnetd = new ec2.PrivateSubnet(this, `${props.resourceName}-subnet-isolated-d`, {
    vpcId: vpc.vpcId,
    availabilityZone: "ap-northeast-1d",
    cidrBlock: "10.0.220.0/24",
});

    :
    :

console.log(`publicSubnets = ${vpc.publicSubnets}`);
console.log(`privateSubnets = ${vpc.privateSubnets}`);
console.log(`isolatedSubnets = ${vpc.isolatedSubnets}`);
```

上記をcdk deployを実行した後、console.log部分の出力結果を見ましたが以下のように、IsolatedSubnetに該当するSubnetTypeはみあたりませんでした。。(何でや。。)
```bash:出力結果
publicSubnets = TestStack/test-vpc/PublicSubnet1,TestStack/test-vpc/PublicSubnet2,TestStack/test-vpc/PublicSubnet3
privateSubnets = TestStack/test-vpc/PrivateSubnet1,TestStack/test-vpc/PrivateSubnet2,TestStack/test-vpc/PrivateSubnet3
isolatedSubnets = 
```

仕方がないため、subnetTypeで指定せず、subnetsで直接サブネットIDを参照するように変更するとうまくRDSの作成をすることができました。

```ts:lib/test.ts
// 追加
/* ---------- RDS ---------- */
const rdsInstance = new rds.DatabaseInstance(this, `${props.resourceName}-rds-mysql`, {
    vpc: vpc,
    engine: rds.DatabaseInstanceEngine.MARIADB,
    instanceType: new ec2.InstanceType("t3.micro"),           // db.t3.micro
    // vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED }, // コメントアウト
    vpcSubnets: { subnets: [isolatedSubneta, isolatedSubnetc, isolatedSubnetd] },
    storageType: rds.StorageType.GP3,
    multiAz: true,
    allocatedStorage: 20,
    credentials: rds.Credentials.fromGeneratedSecret(
        'mariadb',
        { secretName: `${props.resourceName}/mariadb/pw`, }
    ),
    securityGroups: [sgRds],
});
```

## まとめ
列挙型便利だなーと思ったのですが今回のような追加/削除に伴ったケースだと、参照時には列挙型より直接リソースを指定した方が、今回のような事象は回避しやすいのかなとおもいました。

(時間があったら列挙型で途中で追加したサブネットに対しても、参照できるようにしてみたい。。)