CloudShell上のDuckDBからVPC Peeringで別アカウントのRDS for MySQLに接続してみた

CloudShell上のDuckDBからVPC Peeringで別アカウントのRDS for MySQLに接続してみた

Clock Icon2025.05.05

はじめに

データ事業本部ビッグデータチームのkasamaです。
今回はVPC CloudShell上にDuckDBをインストールし、VPC Peering経由で別アカウントのRDS for MySQLに接続したいと思います。

前提

今回実現したい構成は以下になります。
RDS保持アカウントとBastionアカウントをVPC Peeringで繋ぎ、BastionアカウントのCloudShellにinstallしたDuckDBからRDSへアクセスします。事前にRDSと同じデータのcsvをS3に格納し、DuckDBでRDSデータとS3データに差分が無いか確認します。

rds-duckdb

例えば、RDS保持アカウントからBastionアカウントへDMSやS3 Exportなどを用いてデータを連携する場合に、データソース(今回はRDS)とターゲット(今回はS3)の差分比較の手段として使うパターンがあると思います。そのような場合に、DuckDBでは両方と接続ができるのでExcept句で簡単に差分比較ができます。このDuckDBの使用方法は以下のブログで紹介されていたものを参考にしました。今回は簡易的な検証のため、手動で生成したcsvをS3に配置することにします。
https://qiita.com/ogi-iii/items/db631f04e71f112c85b4#データベース移行期間中の新旧テーブルデータ比較
https://tech.timee.co.jp/entry/data-quality-check-with-duckdb

実装

今回の実装コードについては、Github上に格納してあるのでご確認いただければと思います。

https://github.com/cm-yoshikikasama/blog_code/tree/main/55_rds_to_duckdb

@55_rds_to_duckdb % tree
.
├── bastion_account_vpc.yaml
├── mysql_account_rds.yaml
├── mysql_account_vpc.yaml
├── products.csv
└── README.md

1 directory, 5 files
bastion_account_vpc.yaml
bastion_account_vpc.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: cm-kasama-bastion-vpc, private subnet, NAT Gateway, route table

Resources:
  BastionVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.17.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: cm-kasama-bastion-vpc

  # パブリックサブネット(NAT Gateway配置用)
  BastionSubnetPublic1A:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref BastionVPC
      CidrBlock: 172.17.10.0/24
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: cm-kasama-bastion-vpc-subnet-public1-ap-northeast-1a

  # プライベートサブネット(CloudShell用)
  BastionSubnetPrivate1A:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref BastionVPC
      CidrBlock: 172.17.20.0/24
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: cm-kasama-bastion-vpc-subnet-private1-ap-northeast-1a

  # IGW
  BastionInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: cm-kasama-bastion-vpc-igw

  BastionVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref BastionVPC
      InternetGatewayId: !Ref BastionInternetGateway

  # Elastic IP for NAT Gateway
  BastionEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  # NAT Gateway(パブリックサブネットに配置)
  BastionNatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId: !Ref BastionSubnetPublic1A
      AllocationId: !GetAtt BastionEIP.AllocationId
      Tags:
        - Key: Name
          Value: cm-kasama-bastion-natgw

  # パブリックサブネット用ルートテーブル(0.0.0.0/0 → IGW)
  BastionRouteTablePublic:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref BastionVPC
      Tags:
        - Key: Name
          Value: cm-kasama-bastion-vpc-rtb-public

  BastionRoutePublicInternet:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref BastionRouteTablePublic
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref BastionInternetGateway

  BastionSubnetRouteTableAssocPublic1A:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref BastionSubnetPublic1A
      RouteTableId: !Ref BastionRouteTablePublic

  # プライベートサブネット用ルートテーブル(0.0.0.0/0 → NAT Gateway)
  BastionRouteTablePrivate:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref BastionVPC
      Tags:
        - Key: Name
          Value: cm-kasama-bastion-vpc-rtb-private

  BastionRoutePrivateNatGateway:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref BastionRouteTablePrivate
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref BastionNatGateway

  BastionSubnetRouteTableAssocPrivate1A:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref BastionSubnetPrivate1A
      RouteTableId: !Ref BastionRouteTablePrivate

  # CloudShell用SG
  CloudShellRDSAccessSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: cm-kasama-cloudshell-rds-access-sg
      VpcId: !Ref BastionVPC
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic
      Tags:
        - Key: Name
          Value: cm-kasama-cloudshell-rds-access-sg

Bastionアカウントで必要なVPC等を定義しています。VPC Peeringは後ほど手動で設定するので定義していません。VPC CloudShellからInternetにアクセスするにはNAT gateawayが必要ですのでそちらも定義しています。

https://zenn.dev/skakimoto/articles/2024-10-30-aws-cloud-shell-private-ping

mysql_account_vpc.yaml
mysql_account_vpc.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: cm-kasama-dev-vpc, private subnets, RDS subnet group, and RDS security group

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.16.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: cm-kasama-dev-vpc

  SubnetPrivate1A:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 172.16.1.0/24
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: cm-kasama-dev-subnet-private1-ap-northeast-1a

  SubnetPrivate2C:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 172.16.2.0/24
      AvailabilityZone: !Select [ 2, !GetAZs '' ]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: cm-kasama-dev-subnet-private2-ap-northeast-1c

  RouteTablePrivate:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: cm-kasama-dev-rtb-private

  SubnetRouteTableAssoc1A:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate1A
      RouteTableId: !Ref RouteTablePrivate

  SubnetRouteTableAssoc2C:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate2C
      RouteTableId: !Ref RouteTablePrivate

Outputs:
  VPCId:
    Value: !Ref VPC
    Export:
      Name: cm-kasama-vpc-id

  SubnetPrivate1AId:
    Value: !Ref SubnetPrivate1A
    Export:
      Name: cm-kasama-subnet-private1a-id

  SubnetPrivate2CId:
    Value: !Ref SubnetPrivate2C
    Export:
      Name: cm-kasama-subnet-private2c-id

RDS保持アカウント側用のVPC等を定義しています。こちらもVPC Peeringについては後ほど手動で設定します。

mysql_account_rds.yaml
mysql_account_rds.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: RDS MySQL resources for account

Resources:
  RDSMySQLSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: cm-kasama-rds-mysql-sg
      VpcId: !ImportValue cm-kasama-vpc-id
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !Ref CloudShellRDSAccessSG
          Description: Allow MySQL traffic from CloudShell VPC Environment
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic
      Tags:
        - Key: Name
          Value: cm-kasama-rds-mysql-sg

  CloudShellRDSAccessSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: cm-kasama-mysql-account-cloudshell-sg
      VpcId: !ImportValue cm-kasama-vpc-id
      # インバウンドルールなし
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic
      Tags:
        - Key: Name
          Value: cm-kasama-mysql-account-cloudshell-sg

  RDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Subnet group for RDS MySQL
      SubnetIds:
        - !ImportValue cm-kasama-subnet-private1a-id
        - !ImportValue cm-kasama-subnet-private2c-id
      DBSubnetGroupName: cm-kasama-mysql-subnet-group

  RDSInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: cm-kasama-mysql
      Engine: mysql
      EngineVersion: 8.0.40
      DBInstanceClass: db.t4g.micro
      AllocatedStorage: 20
      StorageType: gp2
      MasterUsername: admin
      ManageMasterUserPassword: true
      VPCSecurityGroups:
        - !Ref RDSMySQLSG
      DBSubnetGroupName: !Ref RDSSubnetGroup
      MultiAZ: false
      PubliclyAccessible: false
      BackupRetentionPeriod: 7
      DeletionProtection: false
      StorageEncrypted: true
      KmsKeyId: alias/aws/rds
      EnableIAMDatabaseAuthentication: false
      Tags:
        - Key: Name
          Value: cm-kasama-mysql

RDSとRDSにデータをCloudShellから挿入するためのSecurityGroupを定義しています。

デプロイ

それでは先ほど定義したyamlファイルをデプロイしていきます。こちらはaws cliコマンドでも良いですが、私はAWS Management ConsoleからCloudFormationスタックの新規作成で作成しました。

依存関係があるため、mysql_account_vpc.yamlをデプロイしてからmysql_account_rds.yamlをデプロイします。

Screenshot 2025-05-05 at 16.44.41
Screenshot 2025-05-05 at 16.44.59

データ準備

RDSとS3データを比較するためにデータを準備します。

RDSデータ挿入

まずRDSにデータを挿入します。RDS保持アカウントのCloudShellを起動させ、作成したVPC、Subnet、CloudShell用Security GroupでVPC Environmentを作成します。
次に以下のコマンドでMySQLに接続します。RDSエンドポイントは、AWS Management ConsoleのRDS画面より確認できる~rds.amazonaws.comという値で、ユーザー名とパスワードは、今回の実装ではSecret Manager管理なので、こちらもAWS Management Consoleのシークレットの値より取得できます。

mysql -h <RDSエンドポイント> -u <ユーザー名> -p

Screenshot 2025-05-05 at 16.56.02

ログインできたら以下のSQLを順に実行し、データを挿入します。

CREATE DATABASE sample_db;

CREATE TABLE sample_db.products (
  id INT,
  name VARCHAR(255),
  category VARCHAR(255),
  price INT,
  stock INT,
  description TEXT,
  created_at VARCHAR(32)
);

INSERT INTO sample_db.products (id, name, category, price, stock, description, created_at) VALUES
(1, 'MacBook Pro', 'Electronics', 1999, 50, 'Apple MacBook Pro with M2 chip', '2025-05-04 12:21:11'),
(2, 'iPhone 15', 'Mobile', 999, 100, 'Latest iPhone model with 128GB storage', '2025-05-04 12:21:11'),
(3, 'AirPods Pro', 'Audio', 249, 200, 'Wireless noise-cancelling earbuds', '2025-05-04 12:21:11'),
(4, 'iPad Air', 'Tablet', 599, 75, '10.9-inch display with 64GB storage', '2025-05-04 12:21:11'),
(5, 'Apple Watch', 'Wearable', 399, 120, 'Series 8 with health monitoring features', '2025-05-04 12:21:11'),
(6, 'Magic Mouse', 'Accessories', 79, 150, 'Wireless mouse for Mac', '2025-05-04 12:21:11'),
(7, 'Magic Keyboard', 'Accessories', 99, 100, 'Wireless keyboard with numeric keypad', '2025-05-04 12:21:11'),
(8, 'HomePod Mini', 'Smart Home', 99, 80, 'Smart speaker with Siri', '2025-05-04 12:21:11'),
(9, 'AirTag', 'Accessories', 29, 300, 'Item tracker', '2025-05-04 12:21:11'),
(10, 'Apple TV 4K', 'Entertainment', 179, 60, 'Streaming device with 4K HDR support', '2025-05-04 12:21:11'),
(11, 'MacBook Air M3', 'Electronics', 1299, 75, 'Apple MacBook Air with M3 chip and 13-inch display', '2025-05-04 12:21:11'),
(12, 'MacBook Air M4', 'Electronics', 1299, 75, 'Apple MacBook Air with M4 chip and 13-inch display', '2025-05-04 12:21:11'),
(13, 'MacBook air 2028', 'Electronics', 2499, 30, 'Latest generation MacBook Pro with enhanced performance and display.', '2025-05-04 12:21:11');

CSVデータをS3に配置

以下のcsvファイルをBastionアカウントの任意のS3 Bucketに配置します。データの内容はRDSにInsertしたものと一致しています。

products.csv
id,name,category,price,stock,description,created_at
1,MacBook Pro,Electronics,1999,50,Apple MacBook Pro with M2 chip,2025-05-04 12:21:11
2,iPhone 15,Mobile,999,100,Latest iPhone model with 128GB storage,2025-05-04 12:21:11
3,AirPods Pro,Audio,249,200,Wireless noise-cancelling earbuds,2025-05-04 12:21:11
4,iPad Air,Tablet,599,75,10.9-inch display with 64GB storage,2025-05-04 12:21:11
5,Apple Watch,Wearable,399,120,Series 8 with health monitoring features,2025-05-04 12:21:11
6,Magic Mouse,Accessories,79,150,Wireless mouse for Mac,2025-05-04 12:21:11
7,Magic Keyboard,Accessories,99,100,Wireless keyboard with numeric keypad,2025-05-04 12:21:11
8,HomePod Mini,Smart Home,99,80,Smart speaker with Siri,2025-05-04 12:21:11
9,AirTag,Accessories,29,300,Item tracker,2025-05-04 12:21:11
10,Apple TV 4K,Entertainment,179,60,Streaming device with 4K HDR support,2025-05-04 12:21:11
11,MacBook Air M3,Electronics,1299,75,Apple MacBook Air with M3 chip and 13-inch display,2025-05-04 12:21:11
12,MacBook Air M4,Electronics,1299,75,Apple MacBook Air with M4 chip and 13-inch display,2025-05-04 12:21:11
13,MacBook air 2028,Electronics,2499,30,Latest generation MacBook Pro with enhanced performance and display.,2025-05-04 12:21:11

VPC Peering設定

以下の記事を参考にしながらVPC Peeringを設定します。
https://docs.aws.amazon.com/ja_jp/vpc/latest/peering/create-vpc-peering-connection.html

1. VPC Peering接続作成

BastionアカウントでVPC Peering接続を作成します。

  • 接続名: 任意の名前
  • ピアリング接続するローカルVPC: Bastionアカウント側のVPC
  • ピアリング接続するもう一方のVPC: 別のアカウント。アカウントIDはRDS保持アカウントのID
  • VPC-ID(アクセプタ): RDS保持アカウントのVPC ID
    Screenshot 2025-05-05 at 18.25.01

2. VPC Peeringリクエストの承認

RDS保持アカウントで承認の保留中状態となっているリクエストを承認します。

Screenshot 2025-05-05 at 17.14.12

3. RDS保持アカウントのルートテーブル、Security Group更新

RDS保持アカウントで作成したPrivate subnetに紐づいているルートテーブルのルートを編集から更新します。

  • 送信先: 172.17.0.0/16 (BastionアカウントVPCのCIDR)
  • ターゲット: ピアリングID

Screenshot 2025-05-05 at 17.18.31

続いてRDSが使用しているSecurity GroupのインバウンドルールにBastion CloudShellのSecurity Groupの許可を追加します。

  • タイプ: MySQL/Aurora
  • ソース: Bastion CloudShellのSecurity Group ID
  • 説明: for-bastion-cloudshell

Screenshot 2025-05-05 at 17.22.49

4. Bastionアカウントのルートテーブル更新

今度はBastion保持アカウントで作成したPrivate subnetに紐づいているルートテーブルのルートを編集から更新します。

送信先: 172.16.0.0/16 (RDS保持アカウントVPCのCIDR)
ターゲット: ピアリングID

Screenshot 2025-05-05 at 17.31.16

5. BastionアカウントからRDS接続確認

BastionアカウントからRDSへの接続確認を最後に行います。BastionアカウントのCloudShellを起動させ、Bastion VPC、Private Subnet、CloudShell用Security GroupでVPC Environmentを作成します。
作成したら以下のコマンドでMySQLに接続します。設定値は先ほどと同様です。

mysql -h <RDSエンドポイント> -u <ユーザー名> -p

ここで問題なく接続できている場合はVPC Peering経由でアクセスできています。次の作業のためにMySQLのセッションはexit;で抜けておきます。

Screenshot 2025-05-05 at 17.35.31

DuckDBでのデータ比較

それではDuckDB経由でデータ差分を確認したいと思います。

DuckDBインストール

まずはDuckDBをインストールします。

公式に記載のある通りcurlコマンドでインストールします。
https://duckdb.org/docs/installation/?version=stable&environment=cli&platform=macos&download_method=direct

curl https://install.duckdb.org | sh

duckdbへはduckdbと入力することでセッションに入れます。

Screenshot 2025-05-05 at 17.42.04

MySQLアクセス

次にDuckDBのMySQL拡張機能をinstall、secret情報を設定し、データを参照してみます。
https://duckdb.org/docs/stable/extensions/mysql

-- MySQL拡張をインストール
INSTALL mysql;
LOAD mysql;

CREATE SECRET my_mysql_secret (
    TYPE mysql,
    HOST '<RDSエンドポイント>',
    PORT 3306,
    DATABASE 'sample_db',
    USER '<ユーザー名>',
    PASSWORD '<パスワード>'
);
ATTACH '' AS rdsdb (TYPE mysql, SECRET my_mysql_secret);

-- データをSELECT
SELECT * FROM rdsdb.products;

Screenshot 2025-05-05 at 17.47.30

S3アクセス

次にDuckDBのhttpfs拡張機能をinstall、secret情報を設定し、データを参照してみます。
https://duckdb.org/docs/stable/extensions/httpfs/s3api.html

INSTALL httpfs;
LOAD httpfs;
CREATE SECRET(
    TYPE S3,
    PROVIDER CREDENTIAL_CHAIN
);

SELECT * FROM read_csv('s3://<YOUR_S3_BUCKET>/products.csv', header=true);

Screenshot 2025-05-05 at 17.52.27

Except句で差分比較

最後にExcept句を活用してデータを比較します。
https://duckdb.org/docs/stable/sql/query_syntax/setops.html#except

SELECT * FROM rdsdb.products
EXCEPT
SELECT * FROM read_csv('s3://<YOUR_S3_BUCKET>/products.csv', header=true);

SELECT * FROM read_csv('s3://<YOUR_S3_BUCKET>/products.csv', header=true)
EXCEPT
SELECT * FROM rdsdb.products;

データは完全一致しているので0件で出力されます。
Screenshot 2025-05-05 at 18.34.03

最後に

DuckDBがGlue Data Catalogにも対応するようになると、このような方法でデータ比較することが増えそうな気がしますので、引き続き楽しみにしたいと思います。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.