「rsync」を用いてデータ移行をする際のシンボリックリンク動作の検証まとめ
はじめに
みなさんこんにちは、クラウド事業本部コンサルティング部の浅野です。
「rsync」コマンドを用いてファイルサーバー内のデータを移行する際、データにシンボリックリンクが含まれているとrsyncのオプション選択によってFSx上のデータ構造が変わり、移行後の運用に影響します。
本記事ではEC2ファイルサーバーのデータをFSxへ移行するシチュエーションにてrsync -a(シンボリックリンクを保持)とrsync -aL(実体としてコピー)の挙動の違いと、それぞれがアプリケーション側にどう影響するかを検証します。
検証シナリオ
今回は以下のように既存ファイルサーバーEC2を FSx for OpenZFSに移行するシチュエーションを想定しています。

| 役割 | ホスト |
|---|---|
| アプリEC2(クライアント) | app-ec2 |
| ファイルサーバーEC2(サーバー) | fileserver-ec2 |
| 移行先 | FSx for OpenZFS |
シチュエーションとして、ファイルサーバーEC2は/srcをNFSエクスポートしており、アプリEC2はこれを同じく/srcにマウントして使用しているとします。
ファイルサーバーのデータ構成は以下の通りです。
/src/data/sample.html ← 実データ
/src/current → /src/data への参照
このようにしていると例えば新バージョンのデータ/src/data_v2を用意したとき、シンボリックリンクをln -sfn /src/data_v2 /src/currentで更新するだけでアプリ側のアクセスパス(/src/current/sample.html)を変えずに参照するデータを切り替えられます。ロールバックも同様にリンクを元に戻すだけです。
バージョン管理などでアプリケーション側のパスを変更したくないので、実データへのシンボリックリンクを使うパターンはよく見られます。
このようなシンボリックリンクを含むデータをFSxへ移行するとき、rsyncのオプションによってコピー結果が異なります。今回はそれぞれのパターン別に検証して注意点を述べます。
構成
今回は上記構成図に必要な下記のリソースをCDKを用いて用意しました。
- VPC
- CIDR: 192.168.0.0/24
- サブネット: プライベートサブネット×1
- VPCエンドポイント
- SSM・SSM Messages(インターフェース型)
- S3(ゲートウェイ型)
- セキュリティグループ×3
- アプリEC2用・ファイルサーバーEC2用・FSx用
- EC2(アプリ)
- インスタンスタイプ: t3.micro
- OS: Amazon Linux 2023
- 管理: SSM Session Manager
- EC2(移行前ファイルサーバー)
- インスタンスタイプ: t3.micro
- OS: Amazon Linux 2023
- 管理: SSM Session Manager
- FSx for OpenZFS(移行後ファイルサーバー)
- デプロイメントタイプ: SINGLE_AZ_2
- ストレージ容量: 64GB
- スループット: 160 MB/s
- 圧縮: LZ4
CDKスタック
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as fsx from 'aws-cdk-lib/aws-fsx';
import * as iam from 'aws-cdk-lib/aws-iam';
export class DemoFsxOpenzfsEc2MigrationStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC
const vpc = new ec2.Vpc(this, 'FsxMigrationVpc', {
ipAddresses: ec2.IpAddresses.cidr('192.168.0.0/24'),
maxAzs: 1,
natGateways: 0,
restrictDefaultSecurityGroup: false,
subnetConfiguration: [
{
cidrMask: 26,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
// VPCエンドポイント用セキュリティグループ
const vpcEndpointSg = new ec2.SecurityGroup(this, 'VpcEndpointSg', {
vpc,
description: 'Security group for VPC Endpoints',
allowAllOutbound: true,
});
vpcEndpointSg.addIngressRule(
ec2.Peer.ipv4(vpc.vpcCidrBlock),
ec2.Port.tcp(443),
'Allow HTTPS from VPC'
);
// SSM用VPCエンドポイント(SSM Agent 3.3.40.0以降は ssm と ssmmessages の2つでOK)
vpc.addInterfaceEndpoint('SsmEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SSM,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [vpcEndpointSg],
});
vpc.addInterfaceEndpoint('SsmMessagesEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [vpcEndpointSg],
});
// S3ゲートウェイエンドポイント
vpc.addGatewayEndpoint('S3Endpoint', {
service: ec2.GatewayVpcEndpointAwsService.S3,
subnets: [{ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }],
});
// アプリEC2用セキュリティグループ
const appEc2Sg = new ec2.SecurityGroup(this, 'AppEc2Sg', {
vpc,
securityGroupName: 'app-ec2-sg',
description: 'Security group for App EC2',
allowAllOutbound: true,
});
// EC2ファイルサーバー用セキュリティグループ
const ec2FileServerSg = new ec2.SecurityGroup(this, 'Ec2FileServerSg', {
vpc,
securityGroupName: 'ec2-fileserver-sg',
description: 'Security group for EC2 File Server',
allowAllOutbound: true,
});
// アプリEC2からファイルサーバーEC2へのNFSアクセス許可
ec2FileServerSg.addIngressRule(appEc2Sg, ec2.Port.tcp(111), 'NFS RPC');
ec2FileServerSg.addIngressRule(appEc2Sg, ec2.Port.tcp(2049), 'NFS');
ec2FileServerSg.addIngressRule(appEc2Sg, ec2.Port.tcpRange(20001, 20003), 'NFS mount/status/lock');
// FSx用セキュリティグループ
const fsxSg = new ec2.SecurityGroup(this, 'FsxOpenzfsSg', {
vpc,
securityGroupName: 'fsx-openzfs-sg',
description: 'Security group for FSx OpenZFS',
allowAllOutbound: false,
});
// ファイルサーバーEC2からFSxへのNFSアクセス許可(rsync用)
fsxSg.addIngressRule(ec2FileServerSg, ec2.Port.tcp(111), 'NFS RPC');
fsxSg.addIngressRule(ec2FileServerSg, ec2.Port.tcp(2049), 'NFS');
fsxSg.addIngressRule(ec2FileServerSg, ec2.Port.tcpRange(20001, 20003), 'NFS mount/status/lock');
// アプリEC2からFSxへのNFSアクセス許可(移行後マウント用)
fsxSg.addIngressRule(appEc2Sg, ec2.Port.tcp(111), 'NFS RPC');
fsxSg.addIngressRule(appEc2Sg, ec2.Port.tcp(2049), 'NFS');
fsxSg.addIngressRule(appEc2Sg, ec2.Port.tcpRange(20001, 20003), 'NFS mount/status/lock');
// アプリEC2(Amazon Linux 2023)
new ec2.Instance(this, 'AppEc2', {
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
securityGroup: appEc2Sg,
role: new iam.Role(this, 'AppEc2Role', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
],
}),
blockDevices: [
{
deviceName: '/dev/xvda',
volume: ec2.BlockDeviceVolume.ebs(20, {
volumeType: ec2.EbsDeviceVolumeType.GP3,
}),
},
],
});
// EC2ファイルサーバー(Amazon Linux 2023)
new ec2.Instance(this, 'Ec2FileServer', {
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
securityGroup: ec2FileServerSg,
role: new iam.Role(this, 'Ec2FileServerRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
],
}),
blockDevices: [
{
deviceName: '/dev/xvda',
volume: ec2.BlockDeviceVolume.ebs(20, {
volumeType: ec2.EbsDeviceVolumeType.GP3,
}),
},
],
});
// FSx for OpenZFS
new fsx.CfnFileSystem(this, 'FsxOpenzfs', {
fileSystemType: 'OPENZFS',
storageCapacity: 64,
subnetIds: [vpc.isolatedSubnets[0].subnetId],
securityGroupIds: [fsxSg.securityGroupId],
openZfsConfiguration: {
deploymentType: 'SINGLE_AZ_2',
throughputCapacity: 160,
rootVolumeConfiguration: {
dataCompressionType: 'LZ4',
nfsExports: [
{
clientConfigurations: [
{
clients: '192.168.0.0/24',
options: ['rw','async', 'crossmnt', 'no_root_squash'],
},
],
},
],
},
automaticBackupRetentionDays: 0,
},
tags: [
{
key: 'Name',
value: 'fsx-openzfs-migration',
},
],
});
}
}
やってみた
それでは、CDKをデプロイしてリソースがプロビジョニングされたところから始めます。
テストデータ作成
ファイルサーバーEC2にSSM接続を行い、テストデータとシンボリックリンクを作成します。
$ sudo mkdir -p /src/data
$ sudo chmod -R 777 /src/data
# sample.html を/src/data/ に配置
$ echo "<html><body>hello</body></html>" | sudo tee /src/data/sample.html
<html><body>hello</body></html>
# /src/current -> /src/data を指すシンボリックリンクを作成
$ sudo ln -s /src/data /src/current
$ ls -la /src/
total 0
drwxr-xr-x. 3 root root 33 Mar 5 09:47 .
dr-xr-xr-x. 19 root root 248 Mar 5 09:46 ..
lrwxrwxrwx. 1 root root 9 Mar 5 09:47 current -> /src/data
drwxrwxrwx. 2 root root 25 Mar 5 09:46 data
続いてファイルサーバーEC2にNFSエクスポートの設定を行います。
# NFSサーバーの起動
$ sudo systemctl enable --now nfs-server
Created symlink /etc/systemd/system/multi-user.target.wants/nfs-server.service → /usr/lib/systemd/system/nfs-server.service.
# `/etc/exports`に`/src`のNFSエクスポート設定を追加
$ echo '/src *(rw,async,no_root_squash)' | sudo tee -a /etc/exports
/src *(rw,async,no_root_squash)
# エクスポート設定の反映・確認
$ sudo exportfs -a
$ sudo exportfs -v
/src <world>(async,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)
これにてファイルサーバーEC2側でのシンボリックリンクの用意とNFSエクスポートの設定が完了しました。
移行前の動作確認
続いてアプリEC2にSSM接続し、上記でNFSエクスポートしたファイルサーバーEC2の/srcを同じパスでマウントします。
# アプリEC2側に`/src`を作成
$ sudo mkdir -p /src
# `/src`をファイルサーバーEC2の`/src`にマウント
$ sudo mount -t nfs {ファイルサーバーEC2のプライベートIPアドレス}:/src /src
# マウント後のデータ確認(ファイルサーバーEC2内のデータが確認できる)
$ ls -la /src/
total 0
drwxr-xr-x. 3 root root 33 Mar 5 09:47 .
dr-xr-xr-x. 19 root root 248 Mar 5 09:49 ..
lrwxrwxrwx. 1 root root 9 Mar 5 09:47 current -> /src/data
drwxrwxrwx. 2 root root 25 Mar 5 09:46 data
$ cat /src/current/sample.html
<html><body>hello</body></html>
上記一連のコマンド実行から/src/current/sample.htmlにアクセスするとシンボリックリンク経由で/src/data/sample.htmlが正しく参照できました。
次にこの状態でデータをFSxへ移行します。
FSxへのデータ移行と動作確認
まず、データを移行するためにファイルサーバーEC2に戻り新ファイルサーバーであるFSxをマウントします。
# マウント用ディレクトリ作成
$ sudo mkdir -p /mnt/fsx
# (EC2側) /mnt/fsx -> (FSx側) /fsx/ としてマウント
$ sudo mount -t nfs -o nfsvers=4.1 {FSxのDNS名}:/fsx/ /mnt/fsx/
# マウント確認
$ df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 4.0M 0 4.0M 0% /dev
tmpfs 459M 0 459M 0% /dev/shm
tmpfs 184M 416K 183M 1% /run
/dev/nvme0n1p1 20G 1.8G 19G 9% /
tmpfs 459M 0 459M 0% /tmp
/dev/nvme0n1p128 10M 1.3M 8.7M 13% /boot/efi
fs-088804fbf13ecf7c7.fsx.ap-northeast-1.amazonaws.com:/fsx 64G 0 64G 0% /mnt/fsx
rsync -a でコピーした場合
シンボリックリンクをリンクのままFSxへ移行します。
# シンボリックリンクをリンクとして移行
$ sudo rsync -a /src/ /mnt/fsx/
# 移行後のディレクトリ内のデータ確認
$ ls -la /mnt/fsx/
total 2
drwxr-xr-x. 3 root root 4 Mar 5 09:47 .
drwxr-xr-x. 3 root root 17 Mar 5 09:52 ..
lrwxrwxrwx. 1 root root 9 Mar 5 09:47 current -> /src/data
drwxrwxrwx. 2 root root 3 Mar 5 09:46 data
このようにシンボリックリンクcurrent -> /src/dataとしてFSxにコピーされています。
(-aオプションがシンボリックリンクをリンクのままコピーさせている要因ではなく、-Lをつけていないのでデフォルト動作でリンクのままコピーされる挙動になっています。)
続いてアプリEC2にSSM接続し、マウント先をFSxへ切り替えます。
# 現在のマウント状況を確認 (ファイルサーバーEC2にマウントされている)
$ df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 4.0M 0 4.0M 0% /dev
tmpfs 459M 0 459M 0% /dev/shm
tmpfs 184M 408K 183M 1% /run
/dev/nvme0n1p1 20G 1.8G 19G 9% /
tmpfs 459M 0 459M 0% /tmp
/dev/nvme0n1p128 10M 1.3M 8.7M 13% /boot/efi
{ファイルサーバーEC2のプライベートIP}:/src 20G 1.8G 19G 9% /src
# 既存のファイルサーバーEC2マウントを解除してFSxのルートボリューム`/fsx`を`/src`にマウント(アプリEC2側は元のパスと同じ)
$ sudo umount /src
$ sudo mount -t nfs -o nfsvers=4.1 {FSxのDNS名}:/fsx/ /src
# 参照したいパスにアクセスすると移行前と同じように確認できる
$ cat /src/current/sample.html
<html><body>hello</body></html>
アプリEC2側は元のパスと同じ/srcにマウントしたことでシンボリックリンクcurrent -> /src/dataが解決でき、正常にアクセスできたことが確認できました。
ここでのポイントはEC2マウントしていた時と同じパス(/src)にクライアント側のパスを合わせて新規ファイルサーバーもマウントする必要があるということです。
FSxをマウントする際にアプリEC2側のパスも/srcに合わせないとシンボリックリンクの参照先が消えてしまい、アプリケーションの動作に影響してしまいます。
例えば「FSxのルートボリュームのエクスポートパスが/fsxなので、マウント先も/fsxにしてしまおう!」とすると以下のようにリンクが機能しなくなります。
# 先ほどマウントした`/src`のマウントを一度解除
$ sudo umount /src
# FSxをアプリEC2の`/src`ではなく`/fsx`としてマウントしてしまった場合
$ sudo mkdir -p /fsx
$ sudo mount -t nfs -o nfsvers=4.1 {FSxのDNS名}:/fsx/ /fsx
# アプリが使っていた元のパスで確認すると参照できない
$ cat /src/current/sample.html
cat: /src/current/sample.html: No such file or directory
上記のようにFSxにコピーされたシンボリックリンクはcurrent -> /src/dataのままで/src/dataを指しています。しかし/srcがどこにもマウントされなくなってしまったためリンクが参照できなくなりました。
よってrsync -aでコピーした場合は、アプリEC2のマウントポイントを移行前と同じパス(/src)に揃えないとシンボリックリンクが参照できなくなります。
rsync -aL でコピーした場合
-Lオプションを付与すると、シンボリックリンクをコピー時点で解決し、リンクではなく参照している実体データとしてFSxへ移行してくれます。
まずアプリEC2の既存マウントを全て解除します。
# /src と /fsx がマウントされていれば解除
$ sudo umount /src
$ sudo umount /fsx
続いてファイルサーバーEC2に戻り、FSxのデータをクリアしてからrsync -aLを実行します。
# rsync -a でコピーしたデータを削除してクリーンな状態にする
$ sudo rm -rf /mnt/fsx/current /mnt/fsx/data
# `-L`オプションをつけて再度移行
$ sudo rsync -aL /src/ /mnt/fsx/
$ ls -la /mnt/fsx/
total 2
drwxr-xr-x. 4 root root 4 Mar 5 09:47 .
drwxr-xr-x. 3 root root 17 Mar 5 09:52 ..
drwxrwxrwx. 2 root root 3 Mar 5 09:46 current # シンボリックリンクではなく実際のデータとして移行されている
drwxrwxrwx. 2 root root 3 Mar 5 09:46 data
currentは/src/dataへのシンボリックリンクではなく実ディレクトリとしてコピーされています。
続いてアプリEC2のマウント先をFSxに切り替えます。これを実行すると-aオプションの時とは異なり、アプリEC2のマウントパスが/fsxとしてもアクセスできます。
$ sudo mount -t nfs -o nfsvers=4.1 {FSxのDNS名}:/fsx/ /fsx
$ cat /fsx/current/sample.html
<html><body>hello</body></html>
このようにcurrentは実ディレクトリとしてコピーされているため、マウント先のパスに関係なくアクセスできます。
では、-aLで全部移行すればいいんじゃないの?と思うかもしれませんが、移行後にcurrentを新しいバージョンに張り替えるなどシンボリックリンクがアプリにある前提で運用している場合は問題が起きます。
アプリEC2(FSxが/fsxにマウント済み)にて以下のコマンドを実行します。
# データをバージョンアップしてシンボリックリンクを張り替えたい
$ sudo ln -sfn /fsx/data_v2 /fsx/current
# currentの状態を確認
$ ls -la /fsx/
total 2
drwxr-xr-x 3 root root 25 Mar 5 09:47 .
drwxrwxrwx 2 root root 2 Mar 5 09:47 ..
drwxrwxrwx 2 root root 38 Mar 5 10:05 current # 実ディレクトリのまま変わっていない
drwxrwxrwx 2 root root 25 Mar 5 09:46 data
# currentの中を確認
$ ls -la /fsx/current/
total 2
drwxrwxrwx 2 root root 38 Mar 5 10:05 .
drwxr-xr-x 3 root root 25 Mar 5 09:47 ..
lrwxrwxrwx 1 root root 13 Mar 5 10:05 data_v2 -> /fsx/data_v2 # 意図せずにcurrentの中にリンクが作られてしまっている
-rw-r--r-- 1 root root 32 Mar 5 09:46 sample.html
このようにrsync -aLで移行した後にシンボリックリンクを誤っていじるとcurrent自体がシンボリックリンクに置き換わるのではなく、currentディレクトリの中にdata_v2 -> /fsx/data_v2というリンクが作られてしまうみたいな問題が起きるので、注意してください。
まとめ
| rsync -a | rsync -aL | |
|---|---|---|
| FSxへのコピー結果 | シンボリックリンクを保持 | 実体としてコピー |
| マウントポイント | 元のパス(/src)に揃える必要あり |
自由に設定できる |
| 移行後のシンボリックリンク張り替え | できる | できなくなる |
移行後もシンボリックリンクを張り替える運用が続くならrsync -aを選び、アプリEC2のマウントポイントを移行前と同じパスに揃える。
シンボリックリンク操作が不要であればrsync -aLで移行すると新規ファイルサーバーのマウントポイント設定が旧ファイルサーバーのパスを意識せずに自由に変更できます。
ただし、運用としては 「シンボリックリンクはリンクのまま移行してマウントパスを適切に合わせる」 方が余計なことを考える必要がなくなり楽だと思います。
最後に
本記事では、シンボリックリンクを含むデータをファイルサーバーEC2からFSx for OpenZFSへrsyncで移行する際の挙動を検証しました。
rsync -aとrsync -aLでファイルサーバー上のデータ構造が変わり、それぞれマウントポイントの設定やその後の運用に異なる影響があることが確認できました。
細かい話ですがこの記事がどなたかの参考になれば幸いです。今回は以上です。






