実践!AWS CDK #23 RDS インスタンス

題字・息子たち
2021.07.29

はじめに

今回は最後のリソース、RDS の DB インスタンスを作成します。
前回作成した DB クラスターに紐付けます。

前回の記事はこちら。

AWS 構成図

今回でこの構成が完成します。

1

2 台の DB インスタンスを立ち上げ、そのうち 1 台が Aurora レプリカとなります。

設計

プロパティは以下の通り。

devio-stg-rds-instance-1a

項目
Availability Zone ap-northeast-1a
エンジン Aurora MySQL
DB インスタンスクラス db.r5.large
DB クラスター ID devio-stg-rds-cluster
サブネットグループ devio-stg-rds-sng
パラメータグループ 前々回作ったやつ
モニタリング詳細度 60 秒
モニタリングロール devio-stg-role-rds
パフォーマンスインサイト 有効
パフォーマンスインサイト保持期間 7 日
マイナーバージョン自動アップグレード 無効
メンテナンスウィンドウ 月曜 5:00-5:30 JST(日曜 20:00-20:30 UTC)

devio-stg-rds-instance-1c

項目
Availability Zone ap-northeast-1c
エンジン Aurora MySQL
DB インスタンスクラス db.r5.large
DB クラスター ID devio-stg-rds-cluster
サブネットグループ devio-stg-rds-sng
パラメータグループ 前々回作ったやつ
モニタリング詳細度 60 秒
モニタリングロール devio-stg-role-rds
パフォーマンスインサイト 有効
パフォーマンスインサイト保持期間 7 日
マイナーバージョン自動アップグレード 無効
メンテナンスウィンドウ 月曜 5:30-6:00 JST(日曜 20:30-21:00 UTC)

2 つのインスタンスで異なる部分は AZ とメンテナンスウィンドウくらいです。
メンテナンスウィンドウに関してはクラスターとインスタンスそれぞれで 30 分ずつ時間をずらしました。

実装

RDS に関する処理を行うクラスに、ハイライト部分を追記しました。

lib/resource/rds.ts

import * as cdk from '@aws-cdk/core';
import { CfnDBSubnetGroup, CfnDBClusterParameterGroup, CfnDBParameterGroup, CfnDBCluster, CfnDBInstance } from '@aws-cdk/aws-rds';
import { CfnSubnet, CfnSecurityGroup } from '@aws-cdk/aws-ec2';
import { CfnSecret } from '@aws-cdk/aws-secretsmanager';
import { CfnRole } from '@aws-cdk/aws-iam';
import { Resource } from './abstract/resource';
import { SecretsManager, OSecretKey } from './secretsManager';

interface InstanceInfo {
    readonly id: string;
    readonly availabilityZone: string;
    readonly preferredMaintenanceWindow: string;
    readonly resourceName: string;
    readonly assign: (instance: CfnDBInstance) => void;
}

export class Rds extends Resource {
    public dbCluster: CfnDBCluster;
    public dbInstance1a: CfnDBInstance;
    public dbInstance1c: CfnDBInstance;

    private static readonly engine = 'aurora-mysql';
    private static readonly databaseName = 'devio';
    private static readonly dbInstanceClass = 'db.r5.large';
    private readonly subnetDb1a: CfnSubnet;
    private readonly subnetDb1c: CfnSubnet;
    private readonly securityGroupRds: CfnSecurityGroup;
    private readonly secretRdsCluster: CfnSecret;
    private readonly iamRoleRds: CfnRole;
    private readonly instances: InstanceInfo[] = [
        {
            id: 'RdsDbInstance1a',
            availabilityZone: 'ap-northeast-1a',
            preferredMaintenanceWindow: 'sun:20:00-sun:20:30',
            resourceName: 'rds-instance-1a',
            assign: instance => this.dbInstance1a = instance
        },
        {
            id: 'RdsDbInstance1c',
            availabilityZone: 'ap-northeast-1c',
            preferredMaintenanceWindow: 'sun:20:30-sun:21:00',
            resourceName: 'rds-instance-1c',
            assign: instance => this.dbInstance1c = instance
        }
    ];

    constructor(
        subnetDb1a: CfnSubnet,
        subnetDb1c: CfnSubnet,
        securityGroupRds: CfnSecurityGroup,
        secretRdsCluster: CfnSecret,
        iamRoleRds: CfnRole
    ) {
        super();
        this.subnetDb1a = subnetDb1a;
        this.subnetDb1c = subnetDb1c;
        this.securityGroupRds = securityGroupRds;
        this.secretRdsCluster = secretRdsCluster;
        this.iamRoleRds = iamRoleRds;
    };

    createResources(scope: cdk.Construct) {
        const subnetGroup = this.createSubnetGroup(scope);
        const clusterParameterGroup = this.createClusterParameterGroup(scope);
        const parameterGroup = this.createParameterGroup(scope);
        this.dbCluster = this.createCluster(scope, subnetGroup, clusterParameterGroup);

        for (const instanceInfo of this.instances) {
            const instance = this.createInstance(scope, instanceInfo, this.dbCluster, subnetGroup, parameterGroup);
            instanceInfo.assign(instance);
        }
    }

    private createSubnetGroup(scope: cdk.Construct): CfnDBSubnetGroup {
        const subnetGroup = new CfnDBSubnetGroup(scope, 'RdsDbSubnetGroup', {
            dbSubnetGroupDescription: 'Subnet Group for RDS',
            subnetIds: [this.subnetDb1a.ref, this.subnetDb1c.ref],
            dbSubnetGroupName: this.createResourceName(scope, 'rds-sng')
        });

        return subnetGroup;
    }

    private createClusterParameterGroup(scope: cdk.Construct): CfnDBClusterParameterGroup {
        const clusterParameterGroup = new CfnDBClusterParameterGroup(scope, 'RdsDbClusterParameterGroup', {
            description: 'Cluster Parameter Group for RDS',
            family: 'aurora-mysql5.7',
            parameters: { time_zone: 'UTC' }
        });

        return clusterParameterGroup;
    }

    private createParameterGroup(scope: cdk.Construct): CfnDBParameterGroup {
        const parameterGroup = new CfnDBParameterGroup(scope, 'RdsDbParameterGroup', {
            description: 'Parameter Group for RDS',
            family: 'aurora-mysql5.7'
        });

        return parameterGroup;
    }

    private createCluster(scope: cdk.Construct, subnetGroup: CfnDBSubnetGroup, clusterParameterGroup: CfnDBClusterParameterGroup): CfnDBCluster {
        const cluster = new CfnDBCluster(scope, 'RdsDbCluster', {
            engine: Rds.engine,
            backupRetentionPeriod: 7,
            databaseName: Rds.databaseName,
            dbClusterIdentifier: this.createResourceName(scope, 'rds-cluster'),
            dbClusterParameterGroupName: clusterParameterGroup.ref,
            dbSubnetGroupName: subnetGroup.ref,
            enableCloudwatchLogsExports: ['error'],
            engineMode: 'provisioned',
            engineVersion: '5.7.mysql_aurora.2.10.0',
            masterUserPassword: SecretsManager.getDynamicReference(this.secretRdsCluster, OSecretKey.MasterUserPassword),
            masterUsername: SecretsManager.getDynamicReference(this.secretRdsCluster, OSecretKey.MasterUsername),
            port: 3306,
            preferredBackupWindow: '19:00-19:30',
            preferredMaintenanceWindow: 'sun:19:30-sun:20:00',
            storageEncrypted: true,
            vpcSecurityGroupIds: [this.securityGroupRds.attrGroupId]
        });

        return cluster;
    }

    private createInstance(scope: cdk.Construct, instanceInfo: InstanceInfo, cluster: CfnDBCluster, subnetGroup: CfnDBSubnetGroup, parameterGroup: CfnDBParameterGroup): CfnDBInstance {
        const instance = new CfnDBInstance(scope, instanceInfo.id, {
            dbInstanceClass: Rds.dbInstanceClass,
            autoMinorVersionUpgrade: false,
            availabilityZone: instanceInfo.availabilityZone,
            dbClusterIdentifier: cluster.ref,
            dbInstanceIdentifier: this.createResourceName(scope, instanceInfo.resourceName),
            dbParameterGroupName: parameterGroup.ref,
            dbSubnetGroupName: subnetGroup.ref,
            enablePerformanceInsights: true,
            engine: Rds.engine,
            monitoringInterval: 60,
            monitoringRoleArn: this.iamRoleRds.attrArn,
            performanceInsightsRetentionPeriod: 7,
            preferredMaintenanceWindow: instanceInfo.preferredMaintenanceWindow,
        });

        return instance;
    }
}

特に複雑なことはしておらず。いつものようにリソースを作成するためのメソッドを追加しています。

メインのプログラムはこちら。
ハイライト部分を追記しました。

lib/devio-stack.ts

~ 省略 ~

export class DevioStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    ~ 省略 ~

    // RDS
    const rds = new Rds(
      subnet.db1a,
      subnet.db1c,
      securityGroup.rds,
      secretsManager.secretRdsCluster,
      iamRole.rds
    );
    rds.createResources(this);
  }
}

モニタリングロールを Rds クラスに渡すためにコンストラクタにパラメータを追加しました。

テスト

テストコードはこちら。
ハイライト部分を追記しました。

test/resource/rds.test.ts

import { expect, countResources, haveResource, anything } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import * as Devio from '../../lib/devio-stack';

test('Rds', () => {
    const app = new cdk.App();
    const stack = new Devio.DevioStack(app, 'DevioStack');

    expect(stack).to(countResources('AWS::RDS::DBSubnetGroup', 1));
    expect(stack).to(haveResource('AWS::RDS::DBSubnetGroup', {
        DBSubnetGroupDescription: 'Subnet Group for RDS',
        SubnetIds: anything(),
        DBSubnetGroupName: 'undefined-undefined-rds-sng'
    }));

    expect(stack).to(countResources('AWS::RDS::DBClusterParameterGroup', 1));
    expect(stack).to(haveResource('AWS::RDS::DBClusterParameterGroup', {
        Description: 'Cluster Parameter Group for RDS',
        Family: 'aurora-mysql5.7',
        Parameters: { time_zone: 'UTC' }
    }));

    expect(stack).to(countResources('AWS::RDS::DBParameterGroup', 1));
    expect(stack).to(haveResource('AWS::RDS::DBParameterGroup', {
        Description: 'Parameter Group for RDS',
        Family: 'aurora-mysql5.7'
    }));

    expect(stack).to(countResources('AWS::RDS::DBCluster', 1));
    expect(stack).to(haveResource('AWS::RDS::DBCluster', {
        Engine: 'aurora-mysql',
        BackupRetentionPeriod: 7,
        DatabaseName: 'devio',
        DBClusterIdentifier: 'undefined-undefined-rds-cluster',
        DBClusterParameterGroupName: anything(),
        DBSubnetGroupName: anything(),
        EnableCloudwatchLogsExports: ['error'],
        EngineMode: 'provisioned',
        EngineVersion: '5.7.mysql_aurora.2.10.0',
        MasterUsername: anything(),
        MasterUserPassword: anything(),
        Port: 3306,
        PreferredBackupWindow: '19:00-19:30',
        PreferredMaintenanceWindow: 'sun:19:30-sun:20:00',
        StorageEncrypted: true,
        VpcSecurityGroupIds: anything()
    }));

    expect(stack).to(countResources('AWS::RDS::DBInstance', 2));
    expect(stack).to(haveResource('AWS::RDS::DBInstance', {
        DBInstanceClass: 'db.r5.large',
        AutoMinorVersionUpgrade: false,
        AvailabilityZone: 'ap-northeast-1a',
        DBClusterIdentifier: anything(),
        DBInstanceIdentifier: 'undefined-undefined-rds-instance-1a',
        DBParameterGroupName: anything(),
        DBSubnetGroupName: anything(),
        EnablePerformanceInsights: true,
        Engine: 'aurora-mysql',
        MonitoringInterval: 60,
        MonitoringRoleArn: anything(),
        PerformanceInsightsRetentionPeriod: 7,
        PreferredMaintenanceWindow: 'sun:20:00-sun:20:30',
    }));
    expect(stack).to(haveResource('AWS::RDS::DBInstance', {
        DBInstanceClass: 'db.r5.large',
        AutoMinorVersionUpgrade: false,
        AvailabilityZone: 'ap-northeast-1c',
        DBClusterIdentifier: anything(),
        DBInstanceIdentifier: 'undefined-undefined-rds-instance-1c',
        DBParameterGroupName: anything(),
        DBSubnetGroupName: anything(),
        EnablePerformanceInsights: true,
        Engine: 'aurora-mysql',
        MonitoringInterval: 60,
        MonitoringRoleArn: anything(),
        PerformanceInsightsRetentionPeriod: 7,
        PreferredMaintenanceWindow: 'sun:20:30-sun:21:00',
    }));
});

以下を確認しています。

  • DB インスタンスのリソースが 2 つあること
  • 各リソースのプロパティが正しいこと

確認

マネジメントコンソール上でリソースを確認してみましょう。

2

前回作成したクラスター配下に、今回作成したインスタンス 2 台が紐付いています。

前回は 作成中 だった各エンドポイントのステータスも 利用可能 に。

3

パフォーマンスインサイトも確認可能です。

4

5

続いて(少し長くなりますが)EC2 インスタンスからの疎通確認を行います。

MySQL クライアントのインストール

SSM のセッションマネージャーから EC2 にログインし、公式のインストール手順 を参考に MySQL のクライアントをインストールします。

以下のコマンドを実行し、MySQL の yum リポジトリをシステムのリポジトリリストに追加します。

$ sudo yum -y install https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm

6

次に現在設定されているサブリポジトリのバージョンを確認します。

$ yum repolist all | grep mysql

7

現状バージョン 8.0 が有効化されているので、こちらを無効化し 5.7 を有効化します。

$ sudo yum-config-manager --disable mysql80-community
$ sudo yum-config-manager --enable mysql57-community

再度ステータスの確認。

$ yum repolist all | grep mysql

8

MySQL クライアントをインストールします。

$ sudo yum -y install mysql-community-client

9

インストールできました。

10

ここまでの MySQL クライアントインストール処理も EC2 の UserData に追加しておきます。

lib/script/ec2/userData.sh

#!/bin/bash

# Apache のインストール
sudo yum -y install httpd
sudo systemctl enable httpd
sudo systemctl start httpd

# MySQL クライアントのインストール
sudo yum -y install https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm
sudo yum-config-manager --disable mysql80-community
sudo yum-config-manager --enable mysql57-community
sudo yum -y install mysql-community-client

RDS インスタンスへの接続

クラスター(ライター)エンドポイントをターゲットに以下のコマンドを実行します。(ここでのクラスターエンドポイントは devio-stg-rds-cluster.cluster-cndvsdcsdzja.ap-northeast-1.rds.amazonaws.com

パスワードは Secrets Manager に保存されているものを入力します。

$ mysql -h devio-stg-rds-cluster.cluster-cndvsdcsdzja.ap-northeast-1.rds.amazonaws.com -P 3306 -u admin -p

11

接続できました!

クラスター作成時に指定した DB devio も作成されています。

12

DB deviouser テーブルを作成してみます。

$ create table devio.user (id int, name varchar(8));

テーブルの作成(書き込み)も OK ですね。

13

14

なお RDS への接続時にリーダーエンドポイントを指定した場合、書き込み処理は行なえません。その名の通り、書き込み処理を行う場合はクラスター(ライター)エンドポイントを、読み込み処理のみを行う場合はリーダーエンドポイントを指定するようにしましょう。(ここでのリーダーエンドポイントは devio-stg-rds-cluster.cluster-ro-cndvsdcsdzja.ap-northeast-1.rds.amazonaws.com

15

CloudFormation 版

今回のコードを CFn で書くと以下のようになります。

RdsDbInstance1a:
  Type: AWS::RDS::DBInstance
  Properties:
    DBInstanceClass: db.r5.large
    AutoMinorVersionUpgrade: false
    AvailabilityZone: ap-northeast-1a
    DBClusterIdentifier:
      Ref: RdsDbCluster
    DBInstanceIdentifier: devio-stg-rds-instance-1a
    DBParameterGroupName:
      Ref: RdsDbParameterGroup
    DBSubnetGroupName:
      Ref: RdsDbSubnetGroup
    EnablePerformanceInsights: true
    Engine: aurora-mysql
    MonitoringInterval: 60
    MonitoringRoleArn:
      Fn::GetAtt:
        - RoleRds
        - Arn
    PerformanceInsightsRetentionPeriod: 7
    PreferredMaintenanceWindow: sun:20:00-sun:20:30
RdsDbInstance1c:
  Type: AWS::RDS::DBInstance
  Properties:
    DBInstanceClass: db.r5.large
    AutoMinorVersionUpgrade: false
    AvailabilityZone: ap-northeast-1c
    DBClusterIdentifier:
      Ref: RdsDbCluster
    DBInstanceIdentifier: devio-stg-rds-instance-1c
    DBParameterGroupName:
      Ref: RdsDbParameterGroup
    DBSubnetGroupName:
      Ref: RdsDbSubnetGroup
    EnablePerformanceInsights: true
    Engine: aurora-mysql
    MonitoringInterval: 60
    MonitoringRoleArn:
      Fn::GetAtt:
        - RoleRds
        - Arn
    PerformanceInsightsRetentionPeriod: 7
    PreferredMaintenanceWindow: sun:20:30-sun:21:00

GitHub

今回のソースコードは コチラ です。

おわりに

今回で当初目標としていた構成を AWS CDK ですべて実装することができました!

プログラムを書きながら学べたことがとても多く、非常に充実した取り組みでした。
本シリーズはこの回をもって 一部完 とさせていただきます。

以降はもう少しのんびりとした周期で、構成の拡張AWS CDK の小ワザ などをお伝えしていきたいと思います。

ありがとうございました。

リンク