
CloudShell上のDuckDBからVPC Peeringで別アカウントのRDS for MySQLに接続してみた
はじめに
データ事業本部ビッグデータチームの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保持アカウントからBastionアカウントへDMSやS3 Exportなどを用いてデータを連携する場合に、データソース(今回はRDS)とターゲット(今回はS3)の差分比較の手段として使うパターンがあると思います。そのような場合に、DuckDBでは両方と接続ができるのでExcept句で簡単に差分比較ができます。このDuckDBの使用方法は以下のブログで紹介されていたものを参考にしました。今回は簡易的な検証のため、手動で生成したcsvをS3に配置することにします。
実装
今回の実装コードについては、Github上に格納してあるのでご確認いただければと思います。
@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
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が必要ですのでそちらも定義しています。
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
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
をデプロイします。
データ準備
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
ログインできたら以下の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したものと一致しています。
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を設定します。
1. VPC Peering接続作成
BastionアカウントでVPC Peering接続を作成します。
- 接続名: 任意の名前
- ピアリング接続するローカルVPC: Bastionアカウント側のVPC
- ピアリング接続するもう一方のVPC: 別のアカウント。アカウントIDはRDS保持アカウントのID
- VPC-ID(アクセプタ): RDS保持アカウントのVPC ID
2. VPC Peeringリクエストの承認
RDS保持アカウントで承認の保留中
状態となっているリクエストを承認します。
3. RDS保持アカウントのルートテーブル、Security Group更新
RDS保持アカウントで作成したPrivate subnetに紐づいているルートテーブルのルートを編集
から更新します。
- 送信先: 172.17.0.0/16 (BastionアカウントVPCのCIDR)
- ターゲット: ピアリングID
続いてRDSが使用しているSecurity GroupのインバウンドルールにBastion CloudShellのSecurity Groupの許可を追加します。
- タイプ: MySQL/Aurora
- ソース: Bastion CloudShellのSecurity Group ID
- 説明: for-bastion-cloudshell
4. Bastionアカウントのルートテーブル更新
今度はBastion保持アカウントで作成したPrivate subnetに紐づいているルートテーブルのルートを編集から更新します。
送信先: 172.16.0.0/16 (RDS保持アカウントVPCのCIDR)
ターゲット: ピアリングID
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;
で抜けておきます。
DuckDBでのデータ比較
それではDuckDB経由でデータ差分を確認したいと思います。
DuckDBインストール
まずはDuckDBをインストールします。
公式に記載のある通りcurlコマンドでインストールします。
curl https://install.duckdb.org | sh
duckdbへはduckdb
と入力することでセッションに入れます。
MySQLアクセス
次にDuckDBのMySQL拡張機能をinstall、secret情報を設定し、データを参照してみます。
-- 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;
S3アクセス
次にDuckDBのhttpfs拡張機能をinstall、secret情報を設定し、データを参照してみます。
INSTALL httpfs;
LOAD httpfs;
CREATE SECRET(
TYPE S3,
PROVIDER CREDENTIAL_CHAIN
);
SELECT * FROM read_csv('s3://<YOUR_S3_BUCKET>/products.csv', header=true);
Except句で差分比較
最後に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件で出力されます。
最後に
DuckDBがGlue Data Catalogにも対応するようになると、このような方法でデータ比較することが増えそうな気がしますので、引き続き楽しみにしたいと思います。