AWS CDKでリソース作成 第4回: RDS編

こんにちは。 エキサイト株式会社で内定者アルバイトをしています。

今回も、実際のリソースの作成を行いたいと思います。第4回は、RDSです。

前回の記事 tech.excite.co.jp

RDSとは?

Amazon RDSはAmazon Relational Database Serviceの略で、クラウド内でデータベースのセットアップ、運用、およびスケールを簡単に行うことのできるマネージド型サービスの集合体です。MySQLとの互換性を持つAmazon AuroraPostgreSQLとの互換性を持つAmazon AuroraMySQLMariaDBPostgreSQLOracleSQL Serverの7つのエンジンから選択することができます。

要するにクラウド上で簡単に使える関係データベースです。

実際に作成してみる

今回は、2つのデータベースインスタンス(一つがPrimary、もう一つがReplica)からなる、データベースクラスタを作成します。ちなみに、RDSではパフォーマンスと耐久性を向上させるために、一つのDBインスタンスに対してレプリカを(複数)作成することが多いです。Primaryが書き込みも読み出しも行えるソースDBインスタンス、Replicaが読み出しのみ許可されたリードレプリカです。

データベースクラスタの設定は以下の通りです。

項目 設定
エンジン Aurora MySQL
エンジンバージョン 5.7.mysql_aurora.2.10.2
マスターユーザ名 user
スターパスワード password
ポート 3306
メンテナンスウィンドウ wed:20:15-wed:20:45 UTC (GMT)
バックアップウインドウ 19:30-20:00 UTC (GMT)
暗号化 有効
CloudWatch Logsへの出力 スロークエリ

データベースインスタンスの設定は以下の通りです。

項目 Primary Replica
サイズ db.t3.micro db.t3.small
アベイラビリティゾーン ap-northeast-1a ap-northeast-1c

前回までのコード

前回はApplication Load Balancerを作成しました。これまでのコードは以下の通りです。

package com.myorg;

import software.amazon.awscdk.CfnTag;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.services.ec2.CfnInternetGateway;
import software.amazon.awscdk.services.ec2.CfnVPCGatewayAttachment;
import software.amazon.awscdk.services.ec2.IMachineImage;
import software.amazon.awscdk.services.ec2.Instance;
import software.amazon.awscdk.services.ec2.InstanceClass;
import software.amazon.awscdk.services.ec2.InstanceSize;
import software.amazon.awscdk.services.ec2.InstanceType;
import software.amazon.awscdk.services.ec2.MachineImage;
import software.amazon.awscdk.services.ec2.Peer;
import software.amazon.awscdk.services.ec2.Port;
import software.amazon.awscdk.services.ec2.PrivateSubnet;
import software.amazon.awscdk.services.ec2.PublicSubnet;
import software.amazon.awscdk.services.ec2.SecurityGroup;
import software.amazon.awscdk.services.ec2.SubnetSelection;
import software.amazon.awscdk.services.ec2.Vpc;
import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationListener;
import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationLoadBalancer;
import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationProtocol;
import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationProtocolVersion;
import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationTargetGroup;
import software.amazon.awscdk.services.elasticloadbalancingv2.HealthCheck;
import software.amazon.awscdk.services.elasticloadbalancingv2.IpAddressType;
import software.amazon.awscdk.services.elasticloadbalancingv2.ListenerAction;
import software.amazon.awscdk.services.elasticloadbalancingv2.Protocol;
import software.amazon.awscdk.services.elasticloadbalancingv2.TargetType;
import software.amazon.awscdk.services.elasticloadbalancingv2.targets.InstanceTarget;
import software.constructs.Construct;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;

import java.util.List;

public class MyProjectStack extends Stack {
    public MyProjectStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public MyProjectStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);

        Vpc vpc = Vpc.Builder.create(this, "SampleVPC")
                .vpcName("sample-vpc")
                .cidr("cidr")
                .subnetConfiguration(List.of())
                .build();

        // Availability zone: ap-northeast-1a
        PrivateSubnet privateSubnetA = PrivateSubnet.Builder.create(this, "PrivateSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetA = PublicSubnet.Builder.create(this, "PublicSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // Availability zone: ap-northeast-1c
        PrivateSubnet privateSubnetB = PrivateSubnet.Builder.create(this, "PrivateSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetB = PublicSubnet.Builder.create(this, "PublicSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // NAT gatewayの追加
        publicSubnetA.addNatGateway();
        publicSubnetB.addNatGateway();

        // Internet Gateway
        CfnInternetGateway internetGateway = CfnInternetGateway.Builder.create(this, "InternetGateway")
                .tags(List.of(CfnTag.builder()
                        .key("Name")
                        .value("sample-internet-gateway")
                        .build()))
                .build();

        // Internet gatewayをVPCにattachする
        CfnVPCGatewayAttachment.Builder.create(this, "VpcGateAwayAttachment")
                .vpcId(vpc.getVpcId())
                .internetGatewayId(internetGateway.getAttrInternetGatewayId())
                .build();

        SecurityGroup securityGroup = SecurityGroup.Builder.create(this, "SampleSecurityGroup")
                .securityGroupName("sample-security-group")
                .vpc(vpc)
                .build();

        IMachineImage machineImage = MachineImage.latestAmazonLinux();

        Instance instance01 = Instance.Builder.create(this, "ec2-instance-01")
                .instanceName("ec2-instance-01")
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetA)).build())
                .machineImage(machineImage)
                .vpc(vpc)
                .availabilityZone("ap-northeast-1a")
                .instanceType(InstanceType.of(InstanceClass.T2, InstanceSize.SMALL))
                .securityGroup(securityGroup)
                .build();

        Instance instance02 = Instance.Builder.create(this, "ec2-instance-02")
                .instanceName("ec2-instance-02")
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetB)).build())
                .machineImage(machineImage)
                .vpc(vpc)
                .availabilityZone("ap-northeast-1c")
                .instanceType(InstanceType.of(InstanceClass.T2, InstanceSize.SMALL))
                .securityGroup(securityGroup)
                .build();

        // Load Balancer
        // Load Balancer用のSecurity Group
        SecurityGroup securityGroupEC2LB = SecurityGroup.Builder.create(this, "SecurityGroupEC2LoadBalancer")
                .securityGroupName("ec2-lb-security-group")
                .vpc(vpc)
                .description("security-group-of-ec2-load-balancer")
                .allowAllOutbound(true)
                .build();

        securityGroupEC2LB.addIngressRule(Peer.ipv4("0.0.0.0/0"), Port.tcp(80));

        ApplicationLoadBalancer applicationLoadBalancer = ApplicationLoadBalancer.Builder.create(this, "SampleEC2ApplicationLoadBalancer")
                .loadBalancerName("sample-ec2-load-balancer")
                .deletionProtection(false)
                .vpc(vpc)
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetA, privateSubnetB)).build())
                .securityGroup(securityGroupEC2LB)
                .ipAddressType(IpAddressType.IPV4)
                .idleTimeout(Duration.seconds(120))
                .http2Enabled(true)
                .build();

        ApplicationTargetGroup ec2TargetGroup = ApplicationTargetGroup.Builder.create(this, "EC2TargetGroup")
                .targetGroupName("ec2-target-group")
                .targetType(TargetType.INSTANCE)
                .port(80)
                .vpc(vpc)
                .targets(List.of(new InstanceTarget(instance01, 80), new InstanceTarget(instance02, 80)))
                .protocol(ApplicationProtocol.HTTP)
                .protocolVersion(ApplicationProtocolVersion.HTTP1)
                .healthCheck(HealthCheck.builder()
                        .enabled(true)
                        .path("/")
                        .healthyThresholdCount(3)
                        .unhealthyThresholdCount(2)
                        .interval(Duration.seconds(60))
                        .timeout(Duration.seconds(5))
                        .protocol(Protocol.HTTP)
                        .port("80")
                        .build())
                .build();

        ApplicationListener applicationListener = ApplicationListener.Builder.create(this, "ApplicationListener")
                .loadBalancer(applicationLoadBalancer)
                .protocol(ApplicationProtocol.HTTP)
                .port(80)
                .defaultAction(ListenerAction.forward(List.of(ec2TargetGroup)))
                .build();
    }
}

データベースクラスタを作成するコード

        // ①
        SubnetGroup databaseSubnetGroup = SubnetGroup.Builder.create(this, "DatabaseSubnetGroup")
                .vpc(vpc)
                .vpcSubnets(SubnetSelection.builder()
                        .subnets(List.of(privateSubnetA, privateSubnetB))
                        .build())
                .description("subnet-group-for-rds")
                .subnetGroupName("sample-db-subnet-group")
                .build();

        // DBインスタンス用のセキュリティグループ
        SecurityGroup vpcSecurityGroup = SecurityGroup.Builder.create(this, "VPCSecurityGroup")
                .securityGroupName("sample-db-sg")
                .vpc(vpc)
                .allowAllOutbound(true)
                .description("sample_db_sg")
                .build();

        vpcSecurityGroup.addIngressRule(Peer.ipv4("0.0.0.0/0"), Port.tcp(3306));

        // ②
        CfnDBParameterGroup dbParameterGroup = CfnDBParameterGroup.Builder.create(this, "DBParameterGroup")
                .family("aurora-mysql5.7")
                .description("parameter-group-for-sample-db")
                .tags(List.of(CfnTag.builder()
                        .key("Name")
                        .value("sample-db-parameter-group")
                        .build()))
                .parameters(Map.of("slow_query_log", "1"))
                .build();

        CfnDBClusterParameterGroup dbClusterParameterGroup = CfnDBClusterParameterGroup.Builder.create(this, "DBClusterParameterGroup")
                .family("aurora-mysql5.7")
                .description("parameter-group-for-sample-db-cluster")
                .tags(List.of(CfnTag.builder()
                        .key("Name")
                        .value("sample-db-Cluster-parameter-group")
                        .build()))
                .parameters(Map.of("slow_query_log", "1"))
                .build();


        // ③
        CfnDBCluster dbCluster = CfnDBCluster.Builder.create(this, "DBCluster")
                .engine("aurora-mysql")
                .engineMode("provisioned")
                .databaseName("samplecfndb")
                .dbClusterIdentifier("sample-cfn-db-cluster")
                .engineVersion("5.7.mysql_aurora.2.10.2")
                .masterUsername("user")
                .masterUserPassword("password")
                .port(3306)
                .dbClusterParameterGroupName(dbClusterParameterGroup.getRef())
                .preferredMaintenanceWindow("wed:20:15-wed:20:45")
                .preferredBackupWindow("19:30-20:00")
                .storageEncrypted(true)
                .deletionProtection(false)
                .enableIamDatabaseAuthentication(false)
                .vpcSecurityGroupIds(List.of(vpcSecurityGroup.getSecurityGroupId()))
                .dbSubnetGroupName(databaseSubnetGroup.getSubnetGroupName())
                .enableCloudwatchLogsExports(List.of("slowquery"))
                .build();

        // ④
        CfnDBInstance dbInstancePrimary = CfnDBInstance.Builder.create(this, "DBInstancePrimary")
                .dbInstanceClass("db.t3.small")
                .autoMinorVersionUpgrade(false)
                .availabilityZone("ap-northeast-1a")
                .dbClusterIdentifier(dbCluster.getDbClusterIdentifier())
                .dbInstanceIdentifier("sample-cfn-db-01")
                .dbSubnetGroupName(databaseSubnetGroup.getSubnetGroupName())
                .dbParameterGroupName(dbParameterGroup.getRef())
                .engine("aurora-mysql")
                .preferredMaintenanceWindow("wed:19:35-wed:20:05")
                .build();

        dbInstancePrimary.addDependsOn(dbCluster);
 
        CfnDBInstance dbInstanceReplica = CfnDBInstance.Builder.create(this, "DBInstanceReplica")
                .dbInstanceClass("db.t3.medium")
                .autoMinorVersionUpgrade(false)
                .availabilityZone("ap-northeast-1c")
                .sourceDbInstanceIdentifier(dbInstancePrimary.getSourceDbInstanceIdentifier())
                .dbClusterIdentifier(dbCluster.getDbClusterIdentifier())
                .dbInstanceIdentifier("sample-cfn-db-02")
                .dbSubnetGroupName(databaseSubnetGroup.getSubnetGroupName())
                .dbParameterGroupName(dbParameterGroup.getRef())
                .engine("aurora-mysql")
                .preferredMaintenanceWindow("wed:19:35-wed:20:05")
                .build();

        dbInstanceReplica.addDependsOn(dbCluster);
        dbInstanceReplica.addDependsOn(dbInstancePrimary);

        // ⑤
        ScalableTarget scalableTarget = ScalableTarget.Builder.create(this, "SampleDBScalableTarget")
                .maxCapacity(4)
                .minCapacity(1)
                .serviceNamespace(ServiceNamespace.RDS)
                // 何をオートスケールするか
                .scalableDimension("rds:cluster:ReadReplicaCount")
                .resourceId("cluster:" + dbCluster.getDbClusterIdentifier())
                .build();

        TargetTrackingScalingPolicy.Builder.create(this, "TargetTrackingScalingPolicy")
                .policyName("sample-db-autoscaling")
                .scalingTarget(scalableTarget)
                .predefinedMetric(PredefinedMetric.RDS_READER_AVERAGE_CPU_UTILIZATION)
                .targetValue(70)
                .scaleInCooldown(Duration.seconds(120))
                .scaleOutCooldown(Duration.seconds(300))
                .build();

かなり長くなりましたので、以下で各リソースの作成および設定について簡単に解説します。

まず①では、作成するデータベースクラスタおよびデータベースインスタンスに割り当てるサブネットグループを作成しています。第1回で作成したPrivateサブネット二つで構成されたグループを作成しています。

②では、パラメータグループを作成しています。データベースクラスタやデータベースインスタンスのパラメータの値を変更するにはこのパラメータグループを作成します。
データベースクラスタとデータベースインスタンスの両方のパラメータグループで、スロークエリログを出力するためにslow_query_logを1に設定します。
特に、データベースクラスタのパラメータグループのL1コンストラクトであるCfnDBClusterParameterGroupではparameters()に渡せる引数がObject型になっていますが、データベースインスタンスのパラメータグループのL1コンストラクトであるCfnDBParameterGroupと同様にMap型を渡せばOKのようです。

③、④ではデータベースクラスタとデータベースインスタンスを作成しています。dbClusterIdentifier()でデータベースクラスタとデータベースインスタンスを紐づけます。

⑤ではオートスケーリングの設定を行なっています。ScalableTargetでは何をどの程度までオートスケールできるかを指定します。このコードでは、ここで定義したデータベースクラスタのリードレプリカの数を最大4つまで増やせるように設定しています。
また、TargetTrackingScalingPolicyでは何に基づいてどのような詳細設定でオートスケーリングするかを設定します。このコードでは、平均CPU使用率(70%が閾値)をもとにオートスケーリングを行うことを設定しています。

デプロイ

ここまで、記述できたらいつものようにデプロイします。

cdk deploy

エラーなくデプロイできれば成功です。

補足

addDependsOn()メソッドについて

お気づきの方も多いかと思いますが、コード中にはaddDependsOn()メソッドが何度か出現しました。
このメソッドが何をしているかと言いますと、a.addDependsOn(b)でリソースaの作成がリソースbの作成に依存していることを明示的に示すためのメソッドです。

詳細は別の記事で解説していますので、こちらもご覧ください。

tech.excite.co.jp

L2コンストラクトの使用

今回のコードでは、同じデータベースクラスタ内のデータベースインスタンスを異なるサイズにするためにL1コンストラクトを使用しました。
しかし、そのような制約がない場合にはL2コンストラクトを使用して簡単にデータベースを作成することができます。L2コンストラクトを使用したデータベースクラスタの作成については別記事にて紹介しようと思います。

終わりに

今回は、AWS CDKを用いてRDSのデータベースクラスタを作成しました。

では、また次回。

次の記事 tech.excite.co.jp

参考文献