概要
本記事では、AuroraMySQLからCloudSQLへインターナル通信にてレプリケーションする方法を紹介します。GCPのネットワーク周りで結構ハマりましたので参考になればと思います。
手順
1. AWSのVPC並びにGCPのネットワークの作成
まず、AWSとGCPそれぞれでVPCが無いと始まらないのでそれらの作成を行います。なお本記事ではAWS側をCloudFormationで、GCP側をTerrafromにて記述しています。
- AWS
Resources:
EC2VPC:
Type: 'AWS::EC2::VPC'
Properties:
CidrBlock: !Ref VPCCidrBlock
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: 'default'
Tags:
- Key: 'Name'
Value: !Sub ${AWS::StackName}
- Key: 'Scope'
Value: !Ref GlobalEnvironment
EC2DHCPOptions:
Type: 'AWS::EC2::DHCPOptions'
Properties:
DomainName: !Sub '${GlobalEnvironment}.${GlobalPrefix}.internal ${AWS::Region}.compute.internal'
DomainNameServers:
- 'AmazonProvidedDNS'
Tags:
- Key: 'Name'
Value: !Sub ${AWS::StackName}-dhcp-options
- Key: 'Scope'
Value: !Ref GlobalEnvironment
EC2VPCDHCPOptionsAssociation:
Type: 'AWS::EC2::VPCDHCPOptionsAssociation'
Properties:
DhcpOptionsId: !Ref EC2DHCPOptions
VpcId: !Ref EC2VPC
- GCP
// VPC
resource "google_compute_network" "vpc" {
name = "vpc"
auto_create_subnetworks = false
routing_mode = "GLOBAL"
}
2. AWS側サブネットとAuroraの準備
続いてprimaryとなるAurora MySQLの作成を行います。レプリケーションができるように以下のRDSのパラメータグループのオプションを指定しています。
Parameters:
binlog_format: 'ROW'
gtid-mode: 'ON'
enforce_gtid_consistency: 'ON'
以下がサブネットとAurora作成のためのテンプレートです。
Resources:
# Subnet
EC2SubnetPrivateAZ1:
Type: 'AWS::EC2::Subnet'
Properties:
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: !Ref AWS::Region
CidrBlock: !Ref IPCidrBlockPrivateAZ1
MapPublicIpOnLaunch: false
Tags:
- Key: 'Name'
Value: !Sub ${GlobalPrefix}_${GlobalEnvironment}_private_az1
- Key: 'Scope'
Value: !Ref GlobalEnvironment
VpcId: !Ref EC2VPC
EC2RouteTablePrivateAZ1:
Type: 'AWS::EC2::RouteTable'
Properties:
VpcId: !Ref EC2VPC
Tags:
- Key: 'Name'
Value: !Sub ${GlobalPrefix}_${GlobalEnvironment}_route_table_private_az1
- Key: 'Scope'
Value: !Ref GlobalEnvironment
EC2SubnetRouteTableAssociationPrivateAZ1:
Type: 'AWS::EC2::SubnetRouteTableAssociation'
Properties:
RouteTableId: !Ref EC2RouteTablePrivateAZ1
SubnetId: !Ref EC2SubnetPrivateAZ1
EC2RoutePrivateAZ1NatGateway:
Type: 'AWS::EC2::Route'
Properties:
DestinationCidrBlock: '0.0.0.0/0'
NatGatewayId: !Ref EC2NatGatewayAZ1
RouteTableId: !Ref EC2RouteTablePrivateAZ1
## AZ2
EC2SubnetPrivateAZ2:
Type: 'AWS::EC2::Subnet'
Properties:
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: !Ref AWS::Region
CidrBlock: !Ref IPCidrBlockPrivateAZ2
MapPublicIpOnLaunch: false
Tags:
- Key: 'Name'
Value: !Sub ${GlobalPrefix}_${GlobalEnvironment}_private_az2
- Key: 'Scope'
Value: !Ref GlobalEnvironment
VpcId: !Ref EC2VPC
EC2RouteTablePrivateAZ2:
Type: 'AWS::EC2::RouteTable'
Properties:
VpcId: !Ref EC2VPC
Tags:
- Key: 'Name'
Value: !Sub ${GlobalPrefix}_${GlobalEnvironment}_route_table_private_az2
- Key: 'Scope'
Value: !Ref GlobalEnvironment
EC2SubnetRouteTableAssociationPrivateAZ2:
Type: 'AWS::EC2::SubnetRouteTableAssociation'
Properties:
RouteTableId: !Ref EC2RouteTablePrivateAZ2
SubnetId: !Ref EC2SubnetPrivateAZ2
EC2RoutePrivateAZ2NatGateway:
Type: 'AWS::EC2::Route'
Properties:
DestinationCidrBlock: '0.0.0.0/0'
NatGatewayId: !Ref EC2NatGatewayAZ2
RouteTableId: !Ref EC2RouteTablePrivateAZ2
# RDS
IAMRoleRDSEnhancedMonitoring:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Service: 'monitoring.rds.amazonaws.com'
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole'
EC2SecurityGroupDatabase:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: !Sub '${AWS::StackName}-database'
SecurityGroupIngress:
- SourceSecurityGroupId: !Ref MyHomeIp
FromPort: 3306
ToPort: 3306
IpProtocol: 'tcp'
Tags:
- Key: 'Name'
Value: !Sub '${AWS::StackName}-database'
- Key: 'Scope'
Value: !Ref GlobalEnvironment
VpcId: !Ref EC2VPC
RDSDBClusterParameterGroup:
Type: 'AWS::RDS::DBClusterParameterGroup'
Properties:
Description: !Sub 'cluster parameter group for ${AWS::StackName}'
Family: 'aurora-mysql5.7'
Parameters:
binlog_format: 'ROW'
gtid-mode: 'ON'
enforce_gtid_consistency: 'ON'
Tags:
- Key: 'Scope'
Value: !Ref GlobalEnvironment
RDSDBParameterGroup:
Type: 'AWS::RDS::DBParameterGroup'
Properties:
Description: !Sub 'parameter group for ${AWS::StackName}'
Family: 'aurora-mysql5.7'
Tags:
- Key: 'Scope'
Value: !Ref GlobalEnvironment
RDSDBSubnetGroup:
Type: 'AWS::RDS::DBSubnetGroup'
Properties:
DBSubnetGroupDescription: !Sub 'db subnet group for ${AWS::StackName}'
SubnetIds:
- !Ref EC2SubnetPrivateAZ1
- !Ref EC2SubnetPrivateAZ2
Tags:
- Key: 'Scope'
Value: !Ref GlobalEnvironment
RDSDBCluster:
Type: 'AWS::RDS::DBCluster'
DeletionPolicy: 'Snapshot'
Properties:
AvailabilityZones:
- !Select [0, !GetAZs '']
- !Select [1, !GetAZs '']
BackupRetentionPeriod: 35
DBClusterParameterGroupName: !Ref RDSDBClusterParameterGroup
DBSubnetGroupName: !Ref RDSDBSubnetGroup
Engine: 'aurora-mysql'
EngineVersion: '5.7'
KmsKeyId: 'alias/aws/rds'
MasterUsername: 'root'
MasterUserPassword: 'password'
Port: 3306
PreferredBackupWindow: '19:00-19:30' # JST 05:00-05:30
PreferredMaintenanceWindow: 'Thu:20:00-Thu:20:30' # JST FRI 04:00-04:30
StorageEncrypted: true
Tags:
- Key: 'Scope'
Value: !Ref GlobalEnvironment
VpcSecurityGroupIds:
- !Ref EC2SecurityGroupDatabase
RDSDBInstanceFirst:
Type: 'AWS::RDS::DBInstance'
DeletionPolicy: 'Delete'
Properties:
AllowMajorVersionUpgrade: false
AutoMinorVersionUpgrade: false
AvailabilityZone: !Select [0, !GetAZs '']
CopyTagsToSnapshot: true
DBClusterIdentifier: !Ref RDSDBCluster
DBInstanceClass: 'db.r5.large'
Engine: 'aurora-mysql'
MonitoringInterval: 15
MonitoringRoleArn: !GetAtt IAMRoleRDSEnhancedMonitoring.Arn
PreferredMaintenanceWindow: 'Thu:20:00-Thu:20:30' # JST FRI 04:00-04:30
PubliclyAccessible: false
Tags:
- Key: 'Scope'
Value: !Ref GlobalEnvironment
DBParameterGroupName: !Ref RDSDBParameterGroup
RDSDBInstanceSecond:
Type: 'AWS::RDS::DBInstance'
DeletionPolicy: 'Delete'
Properties:
AllowMajorVersionUpgrade: false
AutoMinorVersionUpgrade: false
AvailabilityZone: !Select [1, !GetAZs '']
CopyTagsToSnapshot: true
DBClusterIdentifier: !Ref RDSDBCluster
DBInstanceClass: 'db.r5.large'
Engine: 'aurora-mysql'
MonitoringInterval: 15
MonitoringRoleArn: !GetAtt IAMRoleRDSEnhancedMonitoring.Arn
PreferredMaintenanceWindow: 'Thu:20:00-Thu:20:30' # JST FRI 04:00-04:30
PubliclyAccessible: false
Tags:
- Key: 'Scope'
Value: !Ref GlobalEnvironment
DBParameterGroupName: !Ref RDSDBParameterGroup
4. AWSとGCPをプライベート接続
AWSとGCPを専用線で結びます。AWSのDirectConnectとGCPのInterConnectを利用することでそれが可能です。今回は個人レベルではそれらを用意することは不可能なのでVPNで代用します。AWSとGCPでVPNを結ぶ方法は以下の記事をご参照ください。
5. GCPのgoogle-managed-services-vpcのCIDRを指定する
CloudSQLはgoogle-managed-services-vpcと言うgoogleのマネージドVPCに作成されます。そこにVPCピアリングする形で、自分たちで作ったネットワークと接続します。そのため、自分たちで作ったサブネットの中にCloudSQLを作成するといったことができません。しかし、CloudSQLのIPがどのCIDRから決まるのかがわかっていないと、AWS側のセキュリティグループの許可設定をCloudSQLが立ち上がるたびに設定する必要が出てきてしまいます。
そこでgoogle-managed-services-vpcのアドレスを設定することでgoogle-managed-services-vpcで作られるサブネットのCIDRを指定します。このアドレスを指定することで、指定したIPレンジからCloudSQLのIPが決まります。VPCのPrivate service connectionで、「google-managed-services-vpc-{プロジェクト名}」というNameでCIDRを指定することで設定が可能です。
resource "google_compute_global_address" "private_ip_alloc_google_managed_service" {
name = "google-managed-services-vpc-projectname"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 24
network = google_compute_network.vpc.id
address = "10.165.2.0"
}
6. AWS側で上記で固定したIPレンジからの通信を許可する
次にAWS側でCloudSQLが作られるgoogle-managed-services-vpcのIPレンジからの通信を許可します。通信を許可するために、ルートテーブルとセキュリティグループの設定を行います。
EC2RoutePrivateDatabaseVGWGCPCloudSQLSubentAZ1:
Type: 'AWS::EC2::Route'
Properties:
DestinationCidrBlock: '10.165.2.0/24'
GatewayId: !Ref VGW
RouteTableId: !Ref EC2RouteTablePrivateAZ1
EC2RoutePrivateDatabaseVGWGCPCloudSQLSubentAZ2:
Type: 'AWS::EC2::Route'
Properties:
DestinationCidrBlock: '10.165.2.0/24'
GatewayId: !Ref VGW
RouteTableId: !Ref EC2RouteTablePrivateAZ2
更新
EC2SecurityGroupDatabase:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: !Sub '${AWS::StackName}-database'
SecurityGroupIngress:
- SourceSecurityGroupId: !Ref EC2SecurityGroupAllowSSHFromOffice
FromPort: 3306
ToPort: 3306
IpProtocol: 'tcp'
- CidrIp: !Ref IPGCPPrivateServiceDev
FromPort: 3306
ToPort: 3306
IpProtocol: 'tcp'
- CidrIp: !Ref IPGCPSubnetZozoDataPoolDev
FromPort: 3306
ToPort: 3306
IpProtocol: 'tcp'
- CidrIp: '10.165.2.0/24'
FromPort: 3306
ToPort: 3306
IpProtocol: 'tcp'
Tags:
- Key: 'Name'
Value: !Sub '${AWS::StackName}-database'
- Key: 'Scope'
Value: !Ref GlobalEnvironment
VpcId: !Ref EC2VPC
詳しくは以下の記事をご参照ください。
7. 仮のCloudSQL用の作成
「CloudSQLはgoogle-managed-services-vpcと言うgoogleのマネージドVPCに作成されます。そこにVPCピアリングする形で、自分たちで作ったネットワークと接続します。」と説明しましたが、そのVPCが作られるのは初めてCloudSQLを作成したタイミングになります。
レプリケーション用のインスタンスの作成前にCloudSQLが置かれるVPCを作成しておきたいため一度仮のCloudSQLを立ち上げて起きます。VPCが作られたあとは作ったインスタンスは不要になるため削除します。(別の方法が有りましたら教えて下さい。)
↓が作られる
8. CloudSQLのネットワークにAWSへのルートを伝搬する
CloudSQLからAWSのAurora MySQLへ通信するためには、CloudSQLのVPCへAWSへのルートが設定されている必要があります。そこで、作成されたVPC Peeringの設定から「Export custom routes」を有効にする必要があります。「Export custom routes」を指定することでCloudSQL側のVPCへルートが伝搬されます。また、CloudSQL側のVPC Peeringの設定で「Import custom routes」が有効になっている必要がありますが、これはデフォルトで有効になっているようでした。
9. AWSへCloudSQLへのルートを伝搬させる
今度はCloudSQLへのルートをAWSへ伝搬させます。これは、Cloud Routerの設定に伝搬させたいCIDRをCUSTOM IP RANGESに指定することで伝搬させることができます。
resource "google_compute_router" "router" {
name = "router"
network = google_compute_network.vpc.id
bgp {
asn = var.router_google_asn
advertise_mode = "CUSTOM"
advertised_groups = ["ALL_SUBNETS"]
}
advertised_ip_ranges {
range = "10.165.2.0/24"
}
region = "asia-northeast1"
}
9. AuroraにCNAMEを貼る
CloudSQLのレプリケーションを作成する場合の制約として、host名を60文字以内にする必要があります。ただし、Auroraの書き込みエンドポイントのhost名は大体の場合60文字を超えてしまいます。そこでAuroraの書き込みエンドポイントへCNAMEを貼って、60文字以内のFQDNを設定します。
10. CloudSQLからレプリケーションする
続いて以下の手順に沿ってレプリケーションを行います。
https://cloud.google.com/sql/docs/mysql/replication/replication-from-external
ただしTerraformがv1.1に対応していないため、ここではv1の手順も混ぜながら構築していきます。
https://cloud.google.com/sql/docs/mysql/replication/replication-from-external_v1
ソース表現インスタンスの作成
まず、ソース表現インスタンスの作成を行います。Terraformでは、host名にFQDNが指定できないためAPIを利用します。
{
"name": "replication-bo-test",
"region": "us-central1",
"databaseVersion": "MYSQL_5_7",
"onPremisesConfiguration": {
"hostPort": "hostname:3306"
}
}
ACCESS_TOKEN="$(gcloud auth print-access-token)"
curl --header "Authorization: Bearer ${ACCESS_TOKEN}" --header 'Content-Type: application/json' --data @./external_mysql.json -X POST https://www.googleapis.com/sql/v1beta4/projects/{project-name}/instances
レプリケーション用ユーザの作成と初期ダンプ
続いて、Aurora MySQLにてレプリケーション用のユーザの作成します。また、ダンプファイルを作成しGCSへ保存します。やり方についてはドキュメントをご参照ください。
https://cloud.google.com/sql/docs/mysql/replication/replication-from-external
レプリケーション
最後に上記で作成したソース表現インスタンスに対してレプリケーションの作成を行います。
resource "google_sql_database_instance" "replica-instance" {
name = "replica-instance" # TODO
database_version = "MYSQL_5_7"
master_instance_name = "replication" # TODO
region = "us-central1"
replica_configuration {
failover_target = false
username = "repuser"
password = "password"
dump_file_path = "gs://bucket/dump.sql.gz"
}
settings {
tier = "db-n1-standard-1"
database_flags {
name = "character_set_server"
value = "utf8mb4"
}
ip_configuration {
private_network = google_compute_network.vpc.id
}
}
}
まとめ
以上のようにインターナル通信でAurora MySQLからCloudSQLへレプリする方法について紹介しました。GCPのネットワークは知らないとハマりどころが多いので参考になれば幸いです。
参考
以下の記事を参考にさせていただきました。ありがとうございあました!!