aws-cdkでVPCを作ってRDSと踏み台サーバを設定したメモ

## 概要

[Amazon Auroraの開始方法][*1]と[CDK Patternsを参考にしてRDS Proxyを利用したAPIをデプロイしてみた](https://dev.classmethod.jp/articles/cdk-rds-proxy-api-go/)をみながら、CDK v2を使って試してみる。今回の作成範囲は、RDSと踏み台のみとする。
なお、RDSを作ってもたもたしていると50円くらいかかる可能性がある。
踏み台のポートフォワーディングについては[別記事](https://qiita.com/hibohiboo/items/e043ff7ab4e343e786fc)とした。

[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/tree/id/2)


## 今回作成するアーキテクチャ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/c3dabb80-fd2d-e1bf-f896-4de0f0799fa5.png)



## 環境
* Windows 10
* aws cli,awscli-session-managerをインストール済
* コマンドはpowershellを使用

<details><div>

今回は、chocolateyで以下のようにインストール済

```
cinst -y awscli awscli-session-manager
```

インストール後はbashやvscodeの再起動推奨。

[検証](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html#install-plugin-verify)

```
 session-manager-plugin
```


</div></details>

## VPCの準備

> DBクラスターを作成する前に、Virtual Private Cloud (VPC) が必要です。

とあるので、まずはVPCの設定を行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/4e7aa3a3-1f34-355c-a130-e3ef3a316509.png)

CDKのスタックは以下。
まずはRDSのセキュリティグループのインバウンドルールは全て受け入れる状態にしておく。
接続が確認できたら閉じる予定。

```ts
import { Aspects, Stack, StackProps, Tag, Tags } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { CfnInternetGateway, CfnRoute, CfnRouteTable, CfnVPCGatewayAttachment, Peer, Port, PrivateSubnet, PrivateSubnetProps, PublicSubnet, PublicSubnetProps, RouterType, SecurityGroup, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { SubnetGroup } from 'aws-cdk-lib/aws-rds';

interface VpcStackProps extends StackProps {
  subnetGroupName: string
}
export class VpcStack extends Stack {
  constructor(scope: Construct, id: string, props: VpcStackProps) {
    super(scope, id, { ...props, subnetGroupName: undefined } as StackProps);
    const cidr = '10.0.0.0/16';
    const vpc = new Vpc(this, 'VPC', {
      cidr,
      natGateways: 0, // デフォルトは1。
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "public-subnet",
          subnetType: SubnetType.PUBLIC
        },
      ], // デフォルトはAZごとに１つのパブリックサブネットと１つのプライベートサブネット
      maxAzs: 3 // 3を指定すると、 public: 10.0.0.0 - 10.0.0.2 の3つ分のcidrBlockが使われる。
    });
    Tags.of(vpc).add('Name', 'vpc');

    // プライベートSubnet（RDS用）
    const privateSubnetProps: PrivateSubnetProps[] = [
      { availabilityZone: 'ap-northeast-1a', vpcId: vpc.vpcId, cidrBlock: '10.0.3.0/24' },
      // ap-northeast-1b は使えない
      { availabilityZone: 'ap-northeast-1c', vpcId: vpc.vpcId, cidrBlock: '10.0.4.0/24' },
      { availabilityZone: 'ap-northeast-1d', vpcId: vpc.vpcId, cidrBlock: '10.0.5.0/24' },
    ]

    const subnets = privateSubnetProps.map((prop, i) => {
      const subnet = new PrivateSubnet(this, `MyPrivateSubnet${i}`, prop);
      Tags.of(subnet).add('Name', `private-subnet-${i}`);
      Tags.of(subnet).add('aws-cdk:subnet-type', SubnetType.PRIVATE_ISOLATED);
      return subnet
    });

    //------------------ Aurora用の設定 ----------------------------------
    const securityGroupPrivate = new SecurityGroup(this, 'SecurityGroupForPrivateSubnets', {
      vpc,
      description: 'seburity group for Aurora and vpc lambda'
    })
    Tags.of(securityGroupPrivate).add('Name', 'SecurityGroupForPrivateSubnets');
    securityGroupPrivate.addIngressRule(Peer.ipv4(cidr), Port.allTcp());

    const subnetGroupForAurora = new SubnetGroup(this, 'SubnetGroupForAurora', {
      vpc,
      vpcSubnets: { subnets },
      description: 'subnet group for Aurora db',
      subnetGroupName: props.subnetGroupName
    });
    Tags.of(subnetGroupForAurora).add('Name', 'SubnetGroupForAurora');

    //------------------ 踏み台用の設定 ----------------------------------
    const securityGroupPublic = new SecurityGroup(this, 'SecurityGroupForPublicSubnets', {
      vpc,
      description: 'seburity group for bastion'
    })
    Tags.of(securityGroupPublic).add('Name', 'SecurityGroupForPublicSubnets');

    //------------------ 共通設定 ----------------------------------
    // 作成したリソース全てにタグをつける
    Aspects.of(this).add(new Tag('Stack', id));

  }
}
```

```
Security Group Changes
┌───┬──────────────────────────────────────────┬─────┬─────────────┬─────────────────┐
│   │ Group                                    │ Dir │ Protocol    │ Peer            │
├───┼──────────────────────────────────────────┼─────┼─────────────┼─────────────────┤
│ + │ ${SecurityGroupForPrivateSubnts.GroupId} │ In  │ TCP 0-65535 │ 10.0.0.0/16     │
│ + │ ${SecurityGroupForPrivateSubnts.GroupId} │ Out │ Everything  │ Everyone (IPv4) │
└───┴──────────────────────────────────────────┴─────┴─────────────┴─────────────────┘
```

なお、設定値はdotenvを使って`.env`ファイルに環境変数として設定している。

```ts:cdk/bin/vpc.ts
#!/usr/bin/env node
import 'source-map-support/register';
import { VpcStack } from '../lib/vpc-stack';
import { App } from 'aws-cdk-lib';
import { config } from 'dotenv'
config();
if (!process.env.SUBNET_GROUP_NAME) throw Error('please set environment variable SUBNET_GROUP_NAME')

const app = new App();

new VpcStack(app, 'VpcStack', {
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
  subnetGroupName: process.env.SUBNET_GROUP_NAME,
});
```

```config:cdk/.env
SUBNET_GROUP_NAME=SubnetGroupForAurora
```

## RDSのPostgresのバージョン確認

最新版のバージョンが使えるか確認。

```
aws rds describe-orderable-db-instance-options --engine aurora-postgresql --engine-version 13.4 --query 'OrderableDBInstanceOptions[].[DBInstanceClass,StorageType,Engine,EngineVersion]' --output table --region ap-northeast-1 '
```

[AuroraPostgresのRDSProxyはバージョン10と11のみサポート](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html)しているので、13はRDSProxyを使おうと思うと使えない。


## RDSの設定

VPCの設定値を控えておいて、環境変数を通して設定するようにしている。

Postgresを使うにあたって、インスタンスのバージョンに制限があるので注意。`t2.micro`では使用できない。
RDSのAdminユーザのパスワードなどはSecretを経由して設定を行う。

```ts
import { Aspects, RemovalPolicy, SecretValue, Stack, StackProps, Tag, Tags } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { InstanceClass, InstanceSize, InstanceType, Peer, Port, PrivateSubnet, PrivateSubnetProps, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2';
import { AuroraPostgresEngineVersion, Credentials, DatabaseCluster, DatabaseClusterEngine, DatabaseProxy, ParameterGroup, ProxyTarget, SubnetGroup } from 'aws-cdk-lib/aws-rds';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';

interface AuroraStackProps extends StackProps {
  vpcId: string
  sgId: string
  subnetGroupName: string
  dbSecretName: string
  dbAdminName: string
  dbUserPassword: string
}

export class AuroraStack extends Stack {
  constructor(scope: Construct, id: string, props: AuroraStackProps) {
    // デフォルトのpropsとの意図しない競合を防ぐため、自前で設定したプロパティを削除して親に渡す
    const superProps = {
      ...props, vpcId: undefined, sgId: undefined, subnetName: undefined
      , dbSecretName: undefined, dbAdminName: undefined, dbUserPassword: undefined

    } as StackProps
    super(scope, id, superProps);

    const vpc = Vpc.fromLookup(this, 'Vpc', { vpcId: props.vpcId })
    const securityGroup = SecurityGroup.fromLookupById(this, 'SecurityGroup', props.sgId);
    // subnetGroupNameはlowecaseで作成される
    const subnetGroup = SubnetGroup.fromSubnetGroupName(this, 'SubnetGroup', props.subnetGroupName.toLowerCase());

    const secret = new Secret(this, 'DBCredentioalSecret', {
      secretName: props.dbSecretName,
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ username: props.dbAdminName }),
        excludePunctuation: true, // '、/、"、@、スペースはpostgresのパスワードに利用できないので除外
        includeSpace: false,
        generateStringKey: 'password'
      }
    })
    Tags.of(secret).add('Name', props.dbSecretName);

    const cluster = new DatabaseCluster(this, 'clusterForAurora', {
      // LTSのバージョンを選択.RDSProxyは10と11のみのサポート 2021.12.10
      engine: DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.VER_11_9 }),
      removalPolicy: RemovalPolicy.DESTROY, // 本番運用だと消しちゃだめだと思う
      defaultDatabaseName: 'postgres',
      instanceProps: {
        vpc,
        securityGroups: [securityGroup],
        // postgresを使える最安値 (2021.12.10)
        instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM),
      },
      instances: 1,
      subnetGroup,
      // parameterGroup,
      credentials: Credentials.fromSecret(secret)
    });

    // 作成したリソース全てにタグをつける
    Aspects.of(this).add(new Tag('Stack', id));
  }
}
```



## 踏み台サーバの設定
RDSをVPCのPrivateSubnet内に作成したため、アクセスは踏み台経由で行うものとする。

cdkの[BastionHostLinux](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.BastionHostLinux.html)クラスを使うと踏み台用のSSM設定がされた状態でEC2を立てることができる。
今回のCDKで作成したpublicサブネットでは、[参照先](https://dev.classmethod.jp/articles/cdk-rds-proxy-api-go/)のように`subnetSelection: {subnetType: ec2.SubnetType.PUBLIC,}`と指定すると、,` There are no 'Public' subnet groups in this VPC. Available types: Isolated`のエラーが表示されてしまう。
そのため、subnetのidを指定して設定している。
また、RDS用のSecretへの読み取り権限を付与している。

```ts
import { , Stack, StackProps, Tags } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { BastionHostLinux, InstanceType, PublicSubnet, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';

interface BastionStackProps extends StackProps {
  vpcId: string
  sgId: string
  dbSecretName: string
  publicSubnetId: string
}

export class BastionStack extends Stack {
  constructor(scope: Construct, id: string, props: BastionStackProps) {
    // デフォルトのpropsとの意図しない競合を防ぐため、自前で設定したプロパティを削除して親に渡す
    const superProps = {
      ...props, vpcId: undefined, sgId: undefined, subnetName: undefined
      , dbSecretName: undefined, dbAdminName: undefined, dbUserPassword: undefined

    } as StackProps
    super(scope, id, superProps);

    const vpc = Vpc.fromLookup(this, 'Vpc', { vpcId: props.vpcId })
    const bastionGroup = SecurityGroup.fromLookupById(this, 'SecurityGroup', props.sgId);

    // パブリックサブネットに踏み台サーバを配置する
    const subnet = PublicSubnet.fromPublicSubnetAttributes(this, 'PublicSubnet', {
      subnetId: props.publicSubnetId,
      availabilityZone: 'ap-northeast-1a'
    });
    const host = new BastionHostLinux(this, 'BastionHost', {
      vpc,
      instanceType: InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      securityGroup: bastionGroup,
      subnetSelection: {
        availabilityZones: [subnet.availabilityZone],
        subnets: [subnet]
      }
    });
    Tags.of(host).add('Name', 'BastionLinux');
    host.instance.addUserData(
      'yum -y update',
      'yum install -y postgresql jq', // postgres 9.2がインストールされる。 RDSのpostgres11よりも古いので、より完璧を目指すならpostgres11をインストールしたい
      // DB接続用のbash作成
      "echo '#!/bin/bash' > /usr/bin/dbaccess.sh",
      `echo 'secret=$(aws secretsmanager get-secret-value --region ap-northeast-1 --secret-id ${props.dbSecretName} | jq .SecretString | jq fromjson)' >> /usr/bin/dbaccess.sh`,
      "echo 'user=$(echo $secret | jq -r .username)' >> /usr/bin/dbaccess.sh",
      "echo 'password=$(echo $secret | jq -r .password)' >> /usr/bin/dbaccess.sh",
      "echo 'endpoint=$(echo $secret | jq -r .host)' >> /usr/bin/dbaccess.sh",
      "echo 'PGPASSWORD=$password psql -h $endpoint -U $user -d postgres' >> /usr/bin/dbaccess.sh",
      "chmod 755 /usr/bin/dbaccess.sh"
    );

    // 認証情報へのアクセス許可
    Secret.fromSecretNameV2(this, 'RDSSecret', props.dbSecretName).grantRead(host);
  }
}
```

```
 ✅  BastionStack

Outputs:
BastionStack.BastionHostBastionHostxxxx = i-hoge
```

## SSMを通した接続

SSMで接続するので、まずはコンソールでSession Managerのページを開き、セッションの開始を行う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/082bedb2-c340-feff-dc2f-a1fb39847d9f.png)

powershellから接続を行う。

```
aws ssm start-session --target i-hoge
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/b7c30239-5e2f-092b-1da3-98250ed7ab31.png)
踏み台サーバにログインできるところまで確認。

つぎに、踏み台サーバからRDSへアクセスを行う。psqlを使用する。
接続情報は、踏み台サーバに最初からインストールされているaws-cliを使って取得する。

```
sh-4.2$ secret=$(aws secretsmanager get-secret-value --region ap-northeast-1 --secret-id db-secrets | jq .SecretString | jq fromjson)
sh-4.2$ user=$(echo $secret | jq -r .username)
sh-4.2$ password=$(echo $secret | jq -r .password)
sh-4.2$ endpoint=$(echo $secret | jq -r .host)
sh-4.2$ PGPASSWORD=$password psql -h $endpoint -U $user -d postgres
```

試しにpostgresqlのユーザを確認。

```sql
select usename from pg_user; -- ユーザを確認するSQL
\q --  psqlを終了
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/6fdd39ba-ac24-985a-7a19-eb8c03396b34.png)

shellファイルにして簡単に接続できるようにしてみた。

```
./dbaccess.sh
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/630a5994-0579-858f-6ba2-d46abdd781b5.png)

## ポートの絞り込み
現在の設定では、VPC内からのアクセスは全てのポートで受け入れる設定になっている。
まずはその設定を削除する。

```diff:cdk/lib/vpc-stack.ts
-    securityGroupPrivate.addIngressRule(Peer.ipv4(cidr), Port.allTcp());
```

`./dbaccess.sh`を実行し、アクセスができなくなっていることを確認。


```
psql: could not connect to server: Connection timed out
        Is the server running on host "aurorastack-clusterforauroradbhoge.cluster-fuga.ap-northeast-1.rds.amazonaws.com" (10.0.x.xx) and accepting
        TCP/IP connections on port 5432?
```

つぎに、RDSの接続ポートのみ開く設定を行う。


```diff:cdk/lib/vpc-stack.ts
+ securityGroupPrivate.addIngressRule(securityGroupPublic, Port.tcp(5432));
```

`./dbaccess.sh`を実行し、アクセスができることを確認。

## 料金
1時間くらいで0.16$取られている。こまめに勘定してますね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/d4df61d8-a22e-9925-0462-9a9d3fa0b48d.png)


## 参考
### 公式
[api reference - ec2 - VPC](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html)
[NAT Gateway - 料金](https://aws.amazon.com/jp/vpc/pricing/)
[Amazon Aurora Getting Started][*1]
[長期サポート](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Updates.LTS.html)
[Amazon Aurora DB インスタンスのインスタンスクラスを変更またはスケーリングするときにエラーが発生したのはなぜですか？](https://aws.amazon.com/jp/premiumsupport/knowledge-center/aurora-mysql-instance-class-error/)
[AuroraPostgresのRDSProxyはバージョン10と11のみサポート](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html)
[RDS インスタンスに接続するように Lambda 関数を設定する方法を教えてください。](https://aws.amazon.com/jp/premiumsupport/knowledge-center/connect-lambda-to-an-rds-instance/)
[BastionHostLinux ](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.BastionHostLinux.html)
[AWS Systems Manager 経由で SSH トンネルを使用してプライベート VPC リソースにアクセスするにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/systems-manager-ssh-vpc-resources/)

### そのほか

[aws-cdkのv2が来たのでドキュメントを読んでみたメモ](https://qiita.com/hibohiboo/items/1a6dec3bbcf941ea4d9e)
[【AWS CDK】VPC コンストラクタでまとめて作成したサブネットの CIDR ブロックでセキュリティグループの設定をする方法](https://dev.classmethod.jp/articles/aws-cdk-vpc-subnet-securitygroup-cidr/)
[CDK(TypeScript)で自分好みのVPCを作りたい](https://blue21neo.blogspot.com/2021/04/cdktypescriptvpc.html)
[AWSCDKでVPCをデプロイする](https://qiita.com/is_ryo/items/66dfe6c4b6dda4bd1eeb)
[実践！AWS CDK #2 VPC](https://dev.classmethod.jp/articles/cdk-practice-2-vpc/)
[実践！AWS CDK #19 Secrets Manager](https://dev.classmethod.jp/articles/cdk-practice-19-secrets-manager/)
[実践！AWS CDK #22 RDS クラスター](https://dev.classmethod.jp/articles/cdk-practice-22-rds-cluster/)
[実践！AWS CDK #23 RDS インスタンス](https://dev.classmethod.jp/articles/cdk-practice-23-rds-instance/)
[aws-cdkでAurora Postgresqlを起動するために試行錯誤した結果](https://com4dc.hatenablog.com/entry/2020/03/06/161409)
[ AWS CDKを使用してPostgres12データベースクラスタをプロビジョニングできません](https://www.fixes.pub/program/726515.html)
[CDK Patternsを参考にしてRDS Proxyを利用したAPIをデプロイしてみた](https://dev.classmethod.jp/articles/cdk-rds-proxy-api-go/)
[[AWS SAM] Amazon RDS Proxy をつかって Amazon Aurora MySQL に接続するサーバーレスアプリケーションを構築する](https://www.seeds-std.co.jp/blog/creators/2021-08-13-002824/)
[Node.jsからPostgreSQLへコネクションプールを使った接続](https://qiita.com/sa9ra4ma/items/6d61ae046daeadde68e2)
[今から始めるLambda⑦「LambdaからRDS(Aurora)に接続する 後編」](https://zenn.dev/nekoniki/articles/70fe311d3e46d6)
[［Node.js + PostgreSQL］Node.js アプリから PostgreSQL DB に接続](https://qiita.com/tanakadaichi_1989/items/15e22cf9c9926e316166)
[`Node-Postgres` を使ったときに、試行錯誤してできたPostgresクラス](https://qiita.com/masaks/items/3ee1b5a06a95315a7ae7)
[VSCodeからPostgreSQLへ接続してSQL発行まで](https://obenkyolab.com/?p=1280)
[CDKで作成した踏み台サーバにAWS SSM Session Manager と AWS Instance Connect を利用してSSH接続する](https://qiita.com/dairappa/items/96d55e929cce22ba90a5)
[AWS CDK で3つ以上の AZ に Subnet を作成するにはアカウントとリージョンを必ず指定する必要があります](https://dev.classmethod.jp/articles/create-subnet-in-three-or-more-azs/)
[初めてのAWS Session Manager（SSM）](https://qiita.com/comefigo/items/b705325d082018ab2348)
[yumでPostgreSQLをインストールしてみよう](https://lets.postgresql.jp/documents/tutorial/yum/yum)
[PostgreSQLの起動・接続・終了・停止コマンドとよく使うpsqlコマンド](https://hirooooo-lab.com/development/postgresql-command-tips/)
[Session Managerを使ってEC2の先にあるRDSに接続してみた](https://dev.classmethod.jp/articles/try-port-forwarding-with-ssm/)

[*1]:https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/CHAP_GettingStartedAurora.html