実践!AWS CDK #33 RDS Stack
はじめに
スタック分割リファクタリング最終回!
RDS スタックの実装です。
前回の記事はこちら。
実装
追加するファイルは以下の通り。
├── lib │ ├── resource │ │ ├── rds-database.ts │ │ ├── rds-parameter-group.ts │ │ └── rds-subnet-group.ts │ └── stack │ └── rds-stack.ts ├── test │ └── stack │ └── rds-stack.test.ts
こちらもマネジメントコンソールの画面に合わせたリソースの分割となっています。
サブネットグループとパラメータグループは ElastiCache
にも存在するため、将来を見据えてこちらのファイル名やクラス名には rds
のプレフィックスを付けることにしました。
RDS のスタッククラスの実装はこちら。
import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { RdsDatabase } from '../resource/rds-database'; import { RdsParameterGroup } from '../resource/rds-parameter-group'; import { RdsSubnetGroup } from '../resource/rds-subnet-group'; import { Ec2Stack } from './ec2-stack'; import { IamStack } from './iam-stack'; import { SecretsManagerStack } from './secrets-manager-stack'; import { VpcStack } from './vpc-stack'; export class RdsStack extends Stack { constructor( scope: Construct, id: string, vpcStack: VpcStack, iamStack: IamStack, ec2Stack: Ec2Stack, secretsManagerStack: SecretsManagerStack, props?: StackProps ) { super(scope, id, props); // Subnet Group const subnetGroup = new RdsSubnetGroup(this, vpcStack.subnet); // Parameter Group const parameterGroup = new RdsParameterGroup(this); // Database new RdsDatabase( this, subnetGroup, parameterGroup, secretsManagerStack.secret, ec2Stack.securityGroup, iamStack.role ); } }
必要なスタックリソースをパラメーターで受け取り、RDS の各リソースを生成しています。
RdsSubnetGroup
クラスの実装はこちら。
import { CfnDBSubnetGroup } from "aws-cdk-lib/aws-rds"; import { Construct } from "constructs"; import { BaseResource } from "./abstract/base-resouce"; import { Subnet } from "./subnet"; export class RdsSubnetGroup extends BaseResource { public readonly subnetGroup: CfnDBSubnetGroup; constructor(scope: Construct, subnet: Subnet) { super(); this.subnetGroup = new CfnDBSubnetGroup(scope, 'RdsDbSubnetGroup', { dbSubnetGroupDescription: 'Subnet Group for RDS', subnetIds: [subnet.db1a.ref, subnet.db1c.ref], dbSubnetGroupName: this.createResourceName(scope, 'rds-sng') }); } }
RdsParameterGroup
クラスの実装はこちら。
import { CfnDBClusterParameterGroup, CfnDBParameterGroup } from "aws-cdk-lib/aws-rds"; import { Construct } from "constructs"; import { BaseResource } from "./abstract/base-resouce"; export class RdsParameterGroup extends BaseResource { public readonly cluster: CfnDBClusterParameterGroup; public readonly instance: CfnDBParameterGroup; constructor(scope: Construct) { super(); this.cluster = new CfnDBClusterParameterGroup(scope, 'RdsDbClusterParameterGroup', { description: 'Cluster Parameter Group for RDS', family: 'aurora-mysql5.7', parameters: { time_zone: 'UTC' } }); this.instance = new CfnDBParameterGroup(scope, 'RdsDbParameterGroup', { description: 'Parameter Group for RDS', family: 'aurora-mysql5.7' }); } }
RdsDatabase
クラスの実装はこちら。
import { CfnDBCluster, CfnDBInstance } from "aws-cdk-lib/aws-rds"; import { Construct } from "constructs"; import { BaseResource } from "./abstract/base-resouce"; import { RdsParameterGroup } from "./rds-parameter-group"; import { RdsSubnetGroup } from "./rds-subnet-group"; import { Role } from "./role"; import { OSecretKey, Secret } from "./secret"; import { SecurityGroup } from "./security-group"; interface InstanceInfo { readonly id: string; readonly availabilityZone: string; readonly preferredMaintenanceWindow: string; readonly resourceName: string; } export class RdsDatabase extends BaseResource { private static readonly engine = 'aurora-mysql'; private static readonly databaseName = 'devio'; private static readonly dbInstanceClass = 'db.r5.large'; private readonly instances: InstanceInfo[] = [ { id: 'RdsDbInstance1a', availabilityZone: 'ap-northeast-1a', preferredMaintenanceWindow: 'sun:20:00-sun:20:30', resourceName: 'rds-instance-1a' }, { id: 'RdsDbInstance1c', availabilityZone: 'ap-northeast-1c', preferredMaintenanceWindow: 'sun:20:30-sun:21:00', resourceName: 'rds-instance-1c' } ]; constructor( scope: Construct, subnetGroup: RdsSubnetGroup, parameterGroup: RdsParameterGroup, secret: Secret, securityGroup: SecurityGroup, role: Role ) { super(); // DB Cluster const cluster = new CfnDBCluster(scope, 'RdsDbCluster', { engine: RdsDatabase.engine, backupRetentionPeriod: 7, databaseName: RdsDatabase.databaseName, dbClusterIdentifier: this.createResourceName(scope, 'rds-cluster'), dbClusterParameterGroupName: parameterGroup.cluster.ref, dbSubnetGroupName: subnetGroup.subnetGroup.ref, enableCloudwatchLogsExports: ['error'], engineMode: 'provisioned', engineVersion: '5.7.mysql_aurora.2.10.0', masterUserPassword: Secret.getDynamicReference(secret.rdsCluster, OSecretKey.MasterUserPassword), masterUsername: Secret.getDynamicReference(secret.rdsCluster, OSecretKey.MasterUsername), port: 3306, preferredBackupWindow: '19:00-19:30', preferredMaintenanceWindow: 'sun:19:30-sun:20:00', storageEncrypted: true, vpcSecurityGroupIds: [securityGroup.rds.attrGroupId] }); // DB Instance for (const instanceInfo of this.instances) { this.createInstance(scope, instanceInfo, cluster, subnetGroup, parameterGroup, role); } } private createInstance( scope: Construct, instanceInfo: InstanceInfo, cluster: CfnDBCluster, subnetGroup: RdsSubnetGroup, parameterGroup: RdsParameterGroup, role: Role ): CfnDBInstance { const instance = new CfnDBInstance(scope, instanceInfo.id, { dbInstanceClass: RdsDatabase.dbInstanceClass, autoMinorVersionUpgrade: false, availabilityZone: instanceInfo.availabilityZone, dbClusterIdentifier: cluster.ref, dbInstanceIdentifier: this.createResourceName(scope, instanceInfo.resourceName), dbParameterGroupName: parameterGroup.instance.ref, dbSubnetGroupName: subnetGroup.subnetGroup.ref, enablePerformanceInsights: true, engine: RdsDatabase.engine, monitoringInterval: 60, monitoringRoleArn: role.rds.attrArn, performanceInsightsRetentionPeriod: 7, preferredMaintenanceWindow: instanceInfo.preferredMaintenanceWindow, }); return instance; } }
クラスターとインスタンスの生成処理を行います。
メインのスタッククラスの実装はこちら。
import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { Ec2Stack } from './stack/ec2-stack'; import { IamStack } from './stack/iam-stack'; import { RdsStack } from './stack/rds-stack'; import { SecretsManagerStack } from './stack/secrets-manager-stack'; import { VpcStack } from './stack/vpc-stack'; export class DevioStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // VPC Stack const vpcStack = new VpcStack(scope, 'VpcStack', { stackName: this.createStackName(scope, 'vpc') }); // IAM Stack const iamStack = new IamStack(scope, 'IamStack', { stackName: this.createStackName(scope, 'iam') }); // EC2 Stack const ec2Stack = new Ec2Stack(scope, 'Ec2Stack', vpcStack, iamStack, { stackName: this.createStackName(scope, 'ec2') }); // Secrets Manager Stack const secretsManagerStack = new SecretsManagerStack(scope, 'SecretsManagerStack', { stackName: this.createStackName(scope, 'secrets-manager') }); // RDS Stack new RdsStack( scope, 'RdsStack', vpcStack, iamStack, ec2Stack, secretsManagerStack, { stackName: this.createStackName(scope, 'rds') } ); } private createStackName(scope: Construct, originalName: string): string { const systemName = scope.node.tryGetContext('systemName'); const envType = scope.node.tryGetContext('envType'); const stackNamePrefix = `${systemName}-${envType}-stack-`; return `${stackNamePrefix}${originalName}`; } }
ハイライト部分を追記しています。
ちなみにリファクタリング前の実装はこちら。
import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { Vpc } from './resource/vpc'; import { Subnet } from './resource/subnet'; import { InternetGateway } from './resource/internetGateway'; import { ElasticIp } from './resource/elasticIp'; import { NatGateway } from './resource/natGateway'; import { RouteTable } from './resource/routeTable'; import { NetworkAcl } from './resource/networkAcl'; import { IamRole } from './resource/iamRole'; import { SecurityGroup } from './resource/securityGroup'; import { Ec2 } from './resource/ec2'; import { Alb } from './resource/alb'; import { SecretsManager } from './resource/secretsManager'; import { Rds } from './resource/rds'; export class DevioStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // VPC const vpc = new Vpc(); vpc.createResources(this); // Subnet const subnet = new Subnet(vpc.vpc); subnet.createResources(this); // Internet Gateway const internetGateway = new InternetGateway(vpc.vpc); internetGateway.createResources(this); // Elastic IP const elasticIp = new ElasticIp(); elasticIp.createResources(this); // NAT Gateway const natGateway = new NatGateway( subnet.public1a, subnet.public1c, elasticIp.ngw1a, elasticIp.ngw1c ); natGateway.createResources(this); // Route Table const routeTable = new RouteTable( vpc.vpc, subnet.public1a, subnet.public1c, subnet.app1a, subnet.app1c, subnet.db1a, subnet.db1c, internetGateway.igw, natGateway.ngw1a, natGateway.ngw1c ); routeTable.createResources(this); // Network ACL const networkAcl = new NetworkAcl( vpc.vpc, subnet.public1a, subnet.public1c, subnet.app1a, subnet.app1c, subnet.db1a, subnet.db1c ); networkAcl.createResources(this); // IAM Role const iamRole = new IamRole(); iamRole.createResources(this); // Security Group const securityGroup = new SecurityGroup(vpc.vpc); securityGroup.createResources(this); // EC2 const ec2 = new Ec2( subnet.app1a, subnet.app1c, iamRole.instanceProfileEc2, securityGroup.ec2 ); ec2.createResources(this); // ALB const alb = new Alb( vpc.vpc, subnet.public1a, subnet.public1c, securityGroup.alb, ec2.instance1a, ec2.instance1c ); alb.createResources(this); // Secrets Manager const secretsManager = new SecretsManager(); secretsManager.createResources(this); // RDS const rds = new Rds( subnet.db1a, subnet.db1c, securityGroup.rds, secretsManager.secretRdsCluster, iamRole.rds ); rds.createResources(this); } }
各リソースをスタッククラスで一階層まとめたことによってだいぶスッキリしましたね。
テスト
テストコードはこちら。
import { App } from 'aws-cdk-lib'; import { Match, Template } from 'aws-cdk-lib/assertions'; import { Ec2Stack } from '../../lib/stack/ec2-stack'; import { IamStack } from '../../lib/stack/iam-stack'; import { RdsStack } from '../../lib/stack/rds-stack'; import { SecretsManagerStack } from '../../lib/stack/secrets-manager-stack'; import { VpcStack } from '../../lib/stack/vpc-stack'; test('Rds Stack', () => { const app = new App({ context: { 'systemName': 'devio', 'envType': 'stg' } }); const vpcStack = new VpcStack(app, 'VpcStack'); const iamStack = new IamStack(app, 'IamStack'); const ec2Stack = new Ec2Stack(app, 'Ec2Stack', vpcStack, iamStack); const secretsManagerStack = new SecretsManagerStack(app, 'SecretsManagerStack'); const rdsStack = new RdsStack(app, 'RdsStack', vpcStack, iamStack, ec2Stack, secretsManagerStack); const template = Template.fromStack(rdsStack); // Subnet Group template.resourceCountIs('AWS::RDS::DBSubnetGroup', 1); template.hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'Subnet Group for RDS', SubnetIds: Match.anyValue(), DBSubnetGroupName: 'devio-stg-rds-sng' }); // Parameter Group template.resourceCountIs('AWS::RDS::DBClusterParameterGroup', 1); template.hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Description: 'Cluster Parameter Group for RDS', Family: 'aurora-mysql5.7', Parameters: { time_zone: 'UTC' } }); template.resourceCountIs('AWS::RDS::DBParameterGroup', 1); template.hasResourceProperties('AWS::RDS::DBParameterGroup', { Description: 'Parameter Group for RDS', Family: 'aurora-mysql5.7' }); // DB Cluster template.resourceCountIs('AWS::RDS::DBCluster', 1); template.hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-mysql', BackupRetentionPeriod: 7, DatabaseName: 'devio', DBClusterIdentifier: 'devio-stg-rds-cluster', DBClusterParameterGroupName: Match.anyValue(), DBSubnetGroupName: Match.anyValue(), EnableCloudwatchLogsExports: ['error'], EngineMode: 'provisioned', EngineVersion: '5.7.mysql_aurora.2.10.0', MasterUsername: Match.anyValue(), MasterUserPassword: Match.anyValue(), Port: 3306, PreferredBackupWindow: '19:00-19:30', PreferredMaintenanceWindow: 'sun:19:30-sun:20:00', StorageEncrypted: true, VpcSecurityGroupIds: Match.anyValue() }); // DB Instance template.resourceCountIs('AWS::RDS::DBInstance', 2); template.hasResourceProperties('AWS::RDS::DBInstance', { DBInstanceClass: 'db.r5.large', AutoMinorVersionUpgrade: false, AvailabilityZone: 'ap-northeast-1a', DBClusterIdentifier: Match.anyValue(), DBInstanceIdentifier: 'devio-stg-rds-instance-1a', DBParameterGroupName: Match.anyValue(), DBSubnetGroupName: Match.anyValue(), EnablePerformanceInsights: true, Engine: 'aurora-mysql', MonitoringInterval: 60, MonitoringRoleArn: Match.anyValue(), PerformanceInsightsRetentionPeriod: 7, PreferredMaintenanceWindow: 'sun:20:00-sun:20:30', }); template.hasResourceProperties('AWS::RDS::DBInstance', { DBInstanceClass: 'db.r5.large', AutoMinorVersionUpgrade: false, AvailabilityZone: 'ap-northeast-1c', DBClusterIdentifier: Match.anyValue(), DBInstanceIdentifier: 'devio-stg-rds-instance-1c', DBParameterGroupName: Match.anyValue(), DBSubnetGroupName: Match.anyValue(), EnablePerformanceInsights: true, Engine: 'aurora-mysql', MonitoringInterval: 60, MonitoringRoleArn: Match.anyValue(), PerformanceInsightsRetentionPeriod: 7, PreferredMaintenanceWindow: 'sun:20:30-sun:21:00', }); });
依存関係があるスタッククラスも生成しなければいけないこのやり方はなんとかならないものだろうか。
MySQL に関する問題
EC2 インスタンスから RDS への接続確認を行ったところ、EC2 インスタンスに MySQL クライアントがインストールされていないことが発覚しました。
EC2 のユーザーデータは以下の通りです。
#!/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
調査したところハイライト部分のクライアントインストールコマンドで以下のエラーが出力されていました。
The GPG keys listed for the "MySQL 5.7 Community Server" repository are already installed but they are not correct for this package. Check that the correct key URLs are configured for this repository. Failing package is: mysql-community-libs-compat-5.7.37-1.el7.x86_64 GPG Keys are configured as: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
どうやら MySQL 8.0.28 がリリースされた 2022/01/18 から GnuPG ビルドキー
というものが更新されたようで、その時点から私が書いた従来のユーザーデータではクライアントがインストールできていなかったようですね、すみません。
新たなキーをインポートすることでこの問題は解消されます。
Note
The GnuPG build key used to sign MySQL downloadable packages was updated with the MySQL 8.0.28 release. To avoid key verification errors when upgrading to MySQL 8.0.28 or higher, import the new GnuPG key:
rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022
インストールコマンドの直前に新たなキーのインポートコマンドを追加しました。
#!/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 rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 sudo yum -y install mysql-community-client
これで MySQL クライアントのインストールが成功し、RDS に接続することもできました。
GitHub
今回のソースコードは コチラ です。
おわりに
今回でスタック分割リファクタイングは完了です。以降はこの構成をベースに実装・構築し、改善点などを見つけていきたいと考えています。本シリーズは一旦ここで終了となりますが、気が向いたときには新たなリソースに関する記事を書いていこうと思います。
今までありがとうございました。
さようなら。