AWS CDKを用いたRedshift Serverless構築の注意点

Redshift Serverlessが登場し、最小ベースキャパシティが8RPUとなったことで、より安価に利用できるようになりました。Redshiftの機能は他のDBに比べて豊富なので、使いこなすことができれば非常に便利です。

# Redshift Serverlessの構築

Redshift ServerlessをCDKで構築する際に、いくつかはまりどころがあったので解説します。
まず、Redshift serverlessを構築するサブネットは、3AZ以上である必要があります。

https://docs.aws.amazon.com/ja_jp/redshift/latest/mgmt/serverless-usage-considerations.html

ec2.VpcのMaxAZsプロパティを使って、3と指定したところ、以下のエラーとなりました。
おそらく、MaxAZsでは、サブネットの数が3以上であることが保証されないからだと思います。

```
There aren?t enough free IP addresses in subnets to allow this operation. Make sure that there are at least 13 free IP addresses in 3 subnets.
Each subnet should be in a different Availability Zone.
```

というわけで、以下のようにavailabilityZonesを明示しています。

```typescript:./lib/vpc-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class VpcStack extends cdk.Stack {
  public readonly vpc: ec2.Vpc;

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    this.vpc = new ec2.Vpc(this, 'MyVPC', {
      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
      // Redshiftの要件でAZが3つ以上のため(MaxAZsではエラーになる)
      availabilityZones: ['ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d'],
      natGateways: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });
  }
}
```

Redshift Serverlessでは大量のIPアドレスが必要でしたが、(1AZあたり)3つに削減されたようです。ただし、Enhanced VPC Routing (EVR)を使わない場合とのことで、enhancedVpcRoutingをfalseにしています。

https://aws.amazon.com/jp/about-aws/whats-new/2025/02/amazon-redshift-serverless-ip-address-3-per-subnet/

また、最小のRPUが8となったとのことで、baseCapacity/maxCapacityともに8としています。

https://dev.classmethod.jp/articles/20230310-amazon-redshift-rpu-8/

```typescript:./lib/redshift-stack.ts
// lib/redshift-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as redshiftserverless from 'aws-cdk-lib/aws-redshiftserverless';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';

export interface RedshiftStackProps extends cdk.StackProps {
  vpc: ec2.Vpc;
}

export class RedshiftStack extends cdk.Stack {
  public readonly workgroupName: string;
  public readonly namespaceName: string;

  constructor(scope: Construct, id: string, props: RedshiftStackProps) {
    super(scope, id, props);

    // Secrets Manager で Redshift 管理者パスワードを生成・管理
    const redshiftAdminSecret = new secretsmanager.Secret(this, 'RedshiftAdminSecret', {
      secretName: 'redshiftAdminSecret',
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ username: 'admin' }),
        generateStringKey: 'password',
        excludePunctuation: true,
        includeSpace: false,
        passwordLength: 32,
      },
    });

    // Redshift Serverless Namespace の作成
    const namespace = new redshiftserverless.CfnNamespace(this, 'RedshiftNamespace', {
      namespaceName: 'myredshiftnamespace',
      adminUserPassword: redshiftAdminSecret.secretValueFromJson('password').toString(),
      adminUsername: 'admin',
      dbName: 'dev',
    });

    // VPC のプライベートサブネットからサブネットIDを取得
    const subnetIds = props.vpc.privateSubnets.map(subnet => subnet.subnetId);

    // Redshift Serverless Workgroup の作成
    const workgroup = new redshiftserverless.CfnWorkgroup(this, 'RedshiftWorkgroup', {
      workgroupName: 'myredshiftworkgroup',
      namespaceName: namespace.namespaceName!,
      enhancedVpcRouting: false,
      baseCapacity: 8,
      maxCapacity: 8,
      subnetIds: subnetIds,
      publiclyAccessible: false,
    });

    workgroup.addDependency(namespace);

    this.workgroupName = workgroup.workgroupName!;
    this.namespaceName = namespace.namespaceName!;

    // Redshift の COPY 用ロールを作成（必要な S3 読み取り権限などを付与）
    const redshiftCopyRole = new iam.Role(this, 'RedshiftCopyRole', {
      assumedBy: new iam.ServicePrincipal('redshift.amazonaws.com'),
      inlinePolicies: {
        S3ReadAccess: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ['s3:GetObject', 's3:ListBucket'],
              resources: ['*'],
            }),
          ],
        }),
      },
    });

    // COPY 用ロールの ARN を出力
    new cdk.CfnOutput(this, 'RedshiftRoleOutput', {
      value: redshiftCopyRole.roleArn,
      exportName: `${this.stackName}-RedshiftRoleArn`,
    });
  }
}
```

エントリーポイントは通常通り作成します。

```typescript:./bin/app.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
import { RedshiftStack } from '../lib/redshift-stack';

const app = new cdk.App();

// VPC スタック
const vpcStack = new VpcStack(app, 'VpcStack');

// Redshift スタック (VPC を渡す)
const redshiftStack = new RedshiftStack(app, 'RedshiftStack', {
  vpc: vpcStack.vpc,
});

app.synth();
```

cdk.json、package.json、tsconfig.jsonなどを作成し、以下のようにデプロイします。

```
npm install
npx cdk bootstrap
npx cdk deploy --all
```