インターナル通信でAurora MySQLからCloudSQLへレプリケーションする方法

# 概要

本記事では、AuroraMySQLからCloudSQLへインターナル通信にてレプリケーションする方法を紹介します。GCPのネットワーク周りで結構ハマりましたので参考になればと思います。

# 手順

## 1.  AWSのVPC並びにGCPのネットワークの作成

まず、AWSとGCPそれぞれでVPCが無いと始まらないのでそれらの作成を行います。なお本記事ではAWS側をCloudFormationで、GCP側をTerrafromにて記述しています。

- AWS

```cfn.yml
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

```tf
// VPC
resource "google_compute_network" "vpc" {
  name                    = "vpc"
  auto_create_subnetworks = false
  routing_mode            = "GLOBAL"
}
```

## 2. AWS側サブネットとAuroraの準備

続いてprimaryとなるAurora MySQLの作成を行います。レプリケーションができるように以下のRDSのパラメータグループのオプションを指定しています。

```yml
Parameters:
  binlog_format: 'ROW'
  gtid-mode: 'ON'
  enforce_gtid_consistency: 'ON'
```

以下がサブネットとAurora作成のためのテンプレートです。

```yml
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を結ぶ方法は以下の記事をご参照ください。

https://qiita.com/katsuyan/items/94eb005c06c517c0c84a

## 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を指定することで設定が可能です。

```tf
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"
}
```

![Google_Cloud_Platformのコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74514/8809be84-db21-3dd0-2eeb-c079c2713ea2.png)


## 6. AWS側で上記で固定したIPレンジからの通信を許可する

次にAWS側でCloudSQLが作られるgoogle-managed-services-vpcのIPレンジからの通信を許可します。通信を許可するために、ルートテーブルとセキュリティグループの設定を行います。

```yml
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
```

更新

```yml
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
```

詳しくは以下の記事をご参照ください。

https://qiita.com/shiozaki/items/2aa3c249f0399d6b779e

## 7.  仮のCloudSQL用の作成

「CloudSQLはgoogle-managed-services-vpcと言うgoogleのマネージドVPCに作成されます。そこにVPCピアリングする形で、自分たちで作ったネットワークと接続します。」と説明しましたが、そのVPCが作られるのは初めてCloudSQLを作成したタイミングになります。

レプリケーション用のインスタンスの作成前にCloudSQLが置かれるVPCを作成しておきたいため一度仮のCloudSQLを立ち上げて起きます。VPCが作られたあとは作ったインスタンスは不要になるため削除します。(別の方法が有りましたら教えて下さい。)

![Create_MySQL_instance_-_zozo-datapool-dev_-_Google_Cloud_Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74514/2303f562-0b53-5187-3c54-35fcfcfd05d7.png)


↓が作られる

![Peering_connection_details_–_VPC_network_–_zozo-datapool-dev_–_Google_Cloud_Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74514/d315cb60-345b-bf59-9545-f6db951c6850.png)


## 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に指定することで伝搬させることができます。

```tf
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"
}
```

![Router_details_–_Hybrid_Connectivity_–_zozo-datapool-dev_–_Google_Cloud_Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74514/3d2073ae-6665-efa5-e8b3-5a8f08bd0573.png)


## 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](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](https://cloud.google.com/sql/docs/mysql/replication/replication-from-external_v1?hl=JA)

### ソース表現インスタンスの作成

まず、ソース表現インスタンスの作成を行います。Terraformでは、host名にFQDNが指定できないためAPIを利用します。

```json
{
  "name": "replication-bo-test",
  "region": "us-central1",
  "databaseVersion": "MYSQL_5_7",
  "onPremisesConfiguration": {
    "hostPort": "hostname:3306"
  }
}
```

```sh
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](https://cloud.google.com/sql/docs/mysql/replication/replication-from-external)

### レプリケーション

最後に上記で作成したソース表現インスタンスに対してレプリケーションの作成を行います。

```tf
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のネットワークは知らないとハマりどころが多いので参考になれば幸いです。

# 参考

以下の記事を参考にさせていただきました。ありがとうございあました！！

- [https://qiita.com/hkame/items/9cff6117bc1763ac7810](https://qiita.com/hkame/items/9cff6117bc1763ac7810)
- [https://medium.com/google-cloud-jp/gcp-の細かすぎて伝わらないハイブリッドネットワーキング-14ed12ebe84d](https://medium.com/google-cloud-jp/gcp-%E3%81%AE%E7%B4%B0%E3%81%8B%E3%81%99%E3%81%8E%E3%81%A6%E4%BC%9D%E3%82%8F%E3%82%89%E3%81%AA%E3%81%84%E3%83%8F%E3%82%A4%E3%83%96%E3%83%AA%E3%83%83%E3%83%89%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AD%E3%83%B3%E3%82%B0-14ed12ebe84d)