暗号化したZFSデータをアンロックせずにバックアップする
原文は Michael Lynch により に公開されました。 このブログを購読する
最近、初めて自宅用のTrueNASサーバーを自作しました。個人用や仕事用のデータの大部分をそこに保存しているので、TrueNASとそのファイルシステムであるZFSを最大限に活用する方法を学んでいるところです。
今回は、暗号化されたデータのバックアップについてお話しします。


ZFSの優れた機能の一つに、暗号化されたデータを暗号化されたままバックアップできるというものがあります。厄介なのは、TrueNASはバックアップ先が常に別のTrueNASシステムであることを前提にしている点です。私のように、暗号化したデータを汎用的なクラウドストレージにバックアップしたい場合は、少し工夫が必要です。今回はその方法を紹介します。
なぜ暗号化したデータをバックアップするのか
滅多にアクセスしないものの、暗号化されたデータセットに保存しておきたいファイルがいくつかあります。
以前使っていたSynology NASでは、暗号化されたボリュームをバックアップする方法がありませんでした。データを暗号化すると、ロックを解除するまで一切アクセスできなくなってしまうのです。普段よく使うデータならそれでも問題ありませんが、たまにしかアクセスしないボリュームはどうでしょうか。夜間に実行されるバックアップでは、それらをクラウドストレージに複製できなくなってしまいます。
TrueNASはその点が優れています。データセットが暗号化されロックされていても、フルバックアップや増分バックアップを作成できるのです。めったにアクセスしないデータを、わざわざ復号したままにしておかずにバックアップできる、素晴らしい方法に思えました。
私はクラウドストレージへのバックアップにresticとresticpyを使っているので、resticが暗号化されたZFSバックアップにアクセスできる方法が必要でした。少し試行錯誤し、手作業でbashスクリプトを書く必要がありましたが、なんとか動くようにできました。
TrueNAS経由で暗号化データセットをバックアップしてみる(間違った方法)
自分がやろうとしていることを説明するために、「diary-entries」というデータセットを作成しました。

では、このデータセットにファイルを追加してみましょう。
echo "I enjoy Taylor Swift, but I don't want anyone to know"
> /mnt/pool1/diary-entries/2022-07-05.txt
次に、バックアップを受け取るための「diary-entries-backup」という新しいデータセットを作成する必要があります。すでに暗号化されているバックアップにさらに暗号化を重ねる必要はないので、この新しいデータセットでは暗号化を無効にしました。

これで、「diary-entries」データセットの暗号化されたスナップショットを、暗号化されていない「diary-entries-backup」データセットにバックアップするレプリケーションタスクを設定する準備ができました。あとはresticが「diary-entries-backup」データセットにアクセスし、クラウドストレージへ複製できます。
レプリケーションタスクを作成すると、TrueNASは暗号化されたデータセットをレプリケートしようとしていると警告してきます。問題ありません。これこそ私がやりたいことです。暗号化されたスナップショットを取得し、暗号化されたままクラウドにバックアップしたいのです。

レプリケーションタスクを開始すると……失敗しました。

エラーは次の通りです。
Unable to send encrypted dataset ‘pool1/diary-entries’ to existing unencrypted or unrelated dataset ‘pool1/diary-entries-backup’.
がっかりです!
暗号化されたデータセットを暗号化されていないデータセットにレプリケートすることができません。スナップショット自体が暗号化されてロックされているのなら、それが暗号化されているデータセット上にあろうとなかろうと関係ないはずなのに、少しおかしな気がします。
コマンドラインインターフェースでZFSを使う
TrueNASは基本的にZFSを扱いやすくしたUIです。話を簡単にするために、TrueNASを迂回して、ZFSのより強力なコマンドラインインターフェース(CLI)を直接使うことにしました。
ZFSドキュメントには、データセットをレプリケートするコマンドの例が載っています。
zfs send pool/fs@a | zfs receive poolB/received/fs@a
「@a」は「a」という名前のスナップショットを表すので、「2022-07-05」という名前のスナップショットを取得してみます。
zfs snapshot pool1/diary-entries@2022-07-05
そして「diary-entries」を「diary-entries-backup」にレプリケートしてみます。
$ zfs send pool1/diary-entries@2022-07-05 \
| zfs receive pool1/diary-entries-backup
cannot receive new filesystem stream: destination 'pool1/diary-entries-backup' exists
must specify -F to overwrite it
なるほど、既存のデータセットにはレプリケートできないということですか。では、新しいデータセット名「diary-entries-backup2」を指定してみましょう。
$ zfs send pool1/diary-entries@2022-07-05 \
| zfs receive pool1/diary-entries-backup
warning: cannot send 'pool1/diary-entries@2022-07-05': dataset key must be loaded
cannot receive: failed to read from stream
ということは、「diary-entries」が復号されていないとレプリケートを拒否するということでしょうか。暗号化されたデータセットでもレプリケートできるはずだと思っていたのですが……
改めてZFSドキュメントを見直すと、「--raw」フラグがありました。
-w, --raw暗号化されたデータセットについては、ディスク上に存在する通りのデータを送信します。
では、これを試してみます。
$ zfs send --raw pool1/diary-entries@2022-07-05 \
| zfs receive pool1/diary-entries-backup2
成功しました!
TrueNASのWeb UIに戻って、何が作成されたか確認してみます。

しまった、これは望んでいたものではありませんでした。
ZFSはまた別の暗号化されたデータセットを作成してしまいました。私が欲しいのは、暗号化されていないデータセット上に置かれた、暗号化されたバックアップファイルです。ZFSにはそれを実現する方法がないように思えました。
ひらめき:出力をファイルにリダイレクトできる
ZFSのレプリケーションコマンドを見直していて、あることに気づきました。
$ zfs send --raw pool1/diary-entries@2022-07-05 \
| zfs receive pool1/diary-entries-backup2
「zfs send」コマンドの出力を「zfs receive」コマンドにパイプで渡しています。では、「zfs receive」にパイプで渡す代わりに、単にファイルに書き出したらどうなるでしょうか。
$ zfs send --raw pool1/diary-entries@2022-07-05 \
> /mnt/pool1/diary-entries-backup/snapshot@2022-07-05
お、うまくいきました!暗号化されていない「diary-entries-backup」データセット上に、24KBのバックアップファイルを作成できました。
$ du -h /mnt/pool1/diary-entries-backup/*
24K /mnt/pool1/diary-entries-backup/snapshot@2022-07-05
暗号化されていないデータセット上にバックアップがファイルとして存在するようになったので、resticは他のファイルと同じようにクラウドへバックアップできます。
ただ、その前に、このバックアップから「zfs receive」コマンドを使って「diary-entries」のデータを復元できるか試してみます。
$ zfs receive pool1/diary-entries-backup3 \
< /mnt/pool1/diary-entries-backup/snapshot@2022-07-05
成功し、プール内に新しいデータセットが作成されました。

いよいよ真実の時です。もし「diary-entries-backup3」を「diary-entries」と同じパスワードで復号でき、同じデータが含まれていれば、ファイル「diary-entries-backup/snapshot@2022-07-05」がスナップショット「2022-07-05」時点の「diary-entries」データセットの完全なバックアップであることが証明されます。
そこで、「diary-entries-backup3」を同じパスワードで復号し、中身を確認してみます。
$ cat /mnt/pool1/diary-entries-backup3/2022-07-05.txt
I enjoy Taylor Swift, but I don’t want anyone to know
やった!うまくいきました。
暗号化されたデータセットを一度もアンロックすることなく、暗号化されたバックアップファイルを正常に作成できるようになりました。
増分バックアップの作成
この方法でバックアップしようと考えているデータセットの一つに、スクリーンキャストの動画キャプチャ用があります。このデータセットは現在12GBあり、今後も増えていく見込みです。毎日バックアップを行うとなると、毎日12GBもの新しいファイルができるのは避けたいところです。
幸い、ZFSは増分バックアップをサポートしています。月曜日にデータセットのスナップショットを取り、火曜日にもう一度スナップショットを取る場合、月曜と火曜の両方でフルバックアップファイルを作成する必要はありません。火曜日のバックアップは、月曜日からの差分だけでよいのです。
説明のために、「diary-entries」データセットにもう少しデータを追加してみます。
echo "Upon reflection, I'm not ashamed of how much I enjoy You Belong with Me" \
> /mnt/pool1/diary-entries/2022-07-06.txt
そして、最新のエントリを含む新しいスナップショットを作成します。
zfs snapshot pool1/diary-entries@2022-07-06
最後に、「-i」フラグでベースとなるスナップショットを指定し、「2022-07-05」のスナップショットを基準とした増分バックアップを作成します。
zfs send \
--raw \
--verbose \
-i pool1/diary-entries@2022-07-05 \
pool1/diary-entries@2022-07-06 \
> /mnt/pool1/diary-entries-backup/snapshot@2022-07-05-to-2022-07-06
成功です!コマンドにより新しい増分バックアップが作成されました。
$ du -h /mnt/pool1/diary-entries-backup/*
24K /mnt/pool1/diary-entries-backup/snapshot@2022-07-05
6.5K /mnt/pool1/diary-entries-backup/snapshot@2022-07-05-to-2022-07-06
今回のデモではファイル自体がとても小さいのであまり意味がありませんが、それでも2つ目のスナップショットが1つ目よりかなり小さいことがわかります。これは「2022-07-05」スナップショット以降の変更点だけが含まれているためです。
バックアップから元のデータを復元できるか確認するまでテストは完了しません。そこで、増分バックアップを使って新しいデータセットを作成してみます。
# Recover from full backup.
zfs receive pool1/diary-entries-backup4@2022-07-05 \
< /mnt/pool1/diary-entries-backup/snapshot@2022-07-05
# Add changes since incremental backup.
zfs receive pool1/diary-entries-backup4 \
< /mnt/pool1/diary-entries-backup/snapshot@2022-07-05-to-2022-07-06
復元できました!

そして、2つのファイルがどちらもしっかり存在しています。
$ tail -n +1 /mnt/pool1/diary-entries-backup4/*
==> /mnt/pool1/diary-entries-backup4/2022-07-05.txt <==
I enjoy Taylor Swift, but I don't want anyone to know
==> /mnt/pool1/diary-entries-backup4/2022-07-06.txt <==
Upon reflection, I'm not ashamed of how much I enjoy Blank Space
まずはフルバックアップから復元し、その後に増分バックアップで更新する必要があることに注意してください。増分バックアップからいきなり復元を始めようとすると、ZFSはエラーで失敗します。
# This isn't going to work because it's an incremental backup.
$ zfs receive pool1/diary-entries-backup5 \
< /mnt/pool1/diary-entries-backup/snapshot@2022-07-05-to-2022-07-06
cannot receive incremental stream: destination 'pool1/diary-entries-backup5' does not exist
バックアップのスクリプト化
ZFSでデータセットをレプリケートする仕組みがわかったので、定期的なバックアップタスクを自動化するためのシェルスクリプトを作成します。
まず、私の環境固有の設定を定義するための「settings.sh」というファイルを作成します。
readonly POOL="mypool"
readonly BASE_DIR="/mnt/${POOL}/encrypted-backups"
readonly FULL_SNAPSHOTS_DIR="${BASE_DIR}/full-snapshots"
readonly INCREMENTAL_SNAPSHOTS_DIR="${BASE_DIR}/incremental-snapshots"
DATASETS=()
DATASETS+=("documents")
DATASETS+=("music")
DATASETS+=("emails")
readonly DATASETS
次に、データセットのフルバックアップを作成する「replicate-full-snapshots.sh」というスクリプトを作成します。
#!/bin/bash
# Create full snapshots of datasets in DATASETS array.
set -eux
. settings.sh
mkdir -p "${FULL_SNAPSHOTS_DIR}"
TIMESTAMP="$(date -Iseconds | sed 's/://g' | sed 's/+0000/Z/g')"
readonly TIMESTAMP
for DATASET in "${DATASETS[@]}"; do
# Take a snapshot.
SNAPSHOT_NAME="${POOL}/${DATASET}@${TIMESTAMP}"
zfs snapshot "${SNAPSHOT_NAME}"
# Write the snapshot to a file.
OUTPUT_FILENAME="${SNAPSHOT_NAME//${POOL}\/}"
zfs send --raw --verbose "${SNAPSHOT_NAME}" > "${FULL_SNAPSHOTS_DIR}/${OUTPUT_FILENAME}"
done
このスクリプトは「settings.sh」で定義した各データセットを順に処理し、それぞれの新しいスナップショットを作成してから、各スナップショットのフルバックアップを作成します。
次に、増分バックアップを作成する「replicate-incremental-snapshots.sh」というスクリプトを作成します。
#!/bin/bash
# Create incremental snapshots of datasets in DATASETS array relative to their
# last full snapshot.
set -eux
. settings.sh
mkdir -p "${INCREMENTAL_SNAPSHOTS_DIR}"
TIMESTAMP="$(date -Iseconds | sed 's/://g' | sed 's/+0000/Z/g')"
readonly TIMESTAMP
for DATASET in "${DATASETS[@]}"; do
# Take a snapshot.
INCREMENTAL_SNAPSHOT="${POOL}/${DATASET}@${TIMESTAMP}"
zfs snapshot "${INCREMENTAL_SNAPSHOT}"
# Find the most recent full snapshot.
BASE_SNAPSHOT_FILENAME="$(basename "$(ls -tr "${FULL_SNAPSHOTS_DIR}/${DATASET}"* | tail -1)")"
BASE_SNAPSHOT="${POOL}/${BASE_SNAPSHOT_FILENAME}"
# Write the incremental snapshot to a file.
OUTPUT_FILENAME="${INCREMENTAL_SNAPSHOT//${POOL}\/}"
OUTPUT_PATH="${INCREMENTAL_SNAPSHOTS_DIR}/${OUTPUT_FILENAME}"
zfs send --raw --verbose -i "${BASE_SNAPSHOT}" "${INCREMENTAL_SNAPSHOT}" \
> "${OUTPUT_PATH}"
done
「replicate-incremental-snapshots.sh」は各データセットの最新のフルバックアップを探し、それを基準とした増分バックアップを作成します。
なお、「replicate-incremental-snapshots.sh」はシンプルさを優先するため、ディスク容量を多少無駄にします。常に最新のフルバックアップを基準に増分バックアップを作成し、より新しい増分バックアップは無視します。つまり、月曜日にフルバックアップを作成し、その後5日間毎日増分バックアップを作成した場合、水曜日のバックアップには火曜日のバックアップと重複するデータが含まれる可能性が高く、容量を無駄にすることになります。増分バックアップの上にさらに増分バックアップを重ねることも検討しましたが、バックアップシステムに求める以上に複雑さやミスの可能性が増すため、やめました。
最後に、バックアップは復元できなければあまり意味がないので、バックアップファイルからZFSデータセットに戻すための便利スクリプト「snapshot-to-dataset.sh」を作成しました。
#!/bin/bash
#
# Recover a dataset from an encrypted snapshot.
#
# Usage:
# ./snapshot-to-dataset.sh new-dataset-name full-snapshot-path [incremental-snapshot-path]
set -ex
. settings.sh
NEW_DATASET_NAME="$1"
readonly NEW_DATASET_NAME
FULL_SNAPSHOT_PATH="$2"
readonly FULL_SNAPSHOT_PATH
INCREMENTAL_SNAPSHOT_PATH="$3"
readonly INCREMENTAL_SNAPSHOT_PATH
set -u
# Restore from base snapshot
zfs receive "${POOL}/${NEW_DATASET_NAME}" < "${FULL_SNAPSHOT_PATH}"
if [[ -n "${INCREMENTAL_SNAPSHOT_PATH}" ]]; then
# Update dataset to latest incremental snapshot
zfs receive "${POOL}/${NEW_DATASET_NAME}" < "${INCREMENTAL_SNAPSHOT_PATH}"
fi
これらのスクリプトはGitHubで公開しています。
便利スクリプトの実行例
私のスクリプトがどのように動くか、「diary-entries」のサンプルデータセットを使ってデモンストレーションします。
こちらが私の「settings.sh」ファイルです。
readonly POOL="pool1"
readonly BASE_DIR="/mnt/${POOL}/secure-backups"
readonly FULL_SNAPSHOTS_DIR="${BASE_DIR}/full-snapshots"
readonly INCREMENTAL_SNAPSHOTS_DIR="${BASE_DIR}/incremental-snapshots"
DATASETS=()
DATASETS+=("diary-entries")
readonly DATASETS
では、フルバックアップを実行してみます。
./replicate-full-snapshots.sh
うまくいったでしょうか。
$ du -h /mnt/pool1/secure-backups/full-snapshots/diary-entries*
24K /mnt/pool1/secure-backups/full-snapshots/diary-entries@2022-07-27T073416-0400
期待通り、バックアップファイルが作成されました。
次に、新しいデータを追加します。
echo "I've got a blank space, so I'll write a new diary entry" \
> /mnt/pool1/diary-entries/2022-07-27.txt
次に、「2022-07-27.txt」を含む増分バックアップを作成します。
./replicate-incremental-snapshots.sh
「incremental-snapshots」フォルダに新しいファイルができているはずです。
$ du -h /mnt/pool1/secure-backups/incremental-snapshots/diary-entries*
12K /mnt/pool1/secure-backups/incremental-snapshots/diary-entries@2022-07-27T074246-0400
そして、そこから復元できるか試してみましょう。私の「snapshot-to-dataset.sh」スクリプトの構文は次の通りでした。
./snapshot-to-dataset.sh new-dataset-name full-backup-file [incremental-backup-file]
これを踏まえて、バックアップからの復元を試してみます。
./snapshot-to-dataset.sh \
diary-entries-backup5 \
/mnt/pool1/secure-backups/full-snapshots/diary-entries@2022-07-27T073416-0400 \
/mnt/pool1/secure-backups/incremental-snapshots/diary-entries@2022-07-27T074246-0400
成功です!すべてのファイルを含む新しいデータセットが作成されました。
tail -n +1 /mnt/pool1/diary-entries-backup5/*
==> /mnt/pool1/diary-entries-backup5/2022-07-05.txt <==
I enjoy Taylor Swift, but I don't want anyone to know
==> /mnt/pool1/diary-entries-backup5/2022-07-06.txt <==
Upon reflection, I'm not ashamed of how much I enjoy Blank Space
==> /mnt/pool1/diary-entries-backup5/2022-07-27.txt <==
I've got a blank space, so I'll write a new diary entry
バックアップのスケジューリング
バックアップをスクリプト化できたので、定期的にバックアップを実行するスケジュールジョブを作成できます。幸い、これはTrueNASのWeb UIで簡単にできるので、「Tasks > Cron Jobs」からタスクを作成するだけです。
最初のcronジョブは、フルバックアップを作成するための月次タスクです。

毎月1日の午前3時に開始するようスケジュールしました。その時間が私が最も確実に寝ている時間だからです。
次に、月次のスナップショットを基準とした増分バックアップを作成する日次タスクを作ります。午前3時のフルバックアップが完了してから増分バックアップが開始されるように、午前4時に開始するよう設定します。

cronジョブが正常に実行されているか確認するには、「/var/log/cron」のログを確認します。
$ tail /var/log/cron
Jul 28 05:45:03 truenas 1 2022-07-28T08:45:03.757011-04:00 truenas.local cron 302 - - + OUTPUT_PATH=/mnt/pool1/secure-backups/incremental-snapshots/videos@2022-07-28T084502-0400
Jul 28 05:45:03 truenas 1 2022-07-28T08:45:03.757087-04:00 truenas.local cron 302 - - + [[ -f /mnt/pool1/secure-backups/incremental-snapshots/videos@2022-07-28T084502-0400 ]]
Jul 28 05:45:03 truenas 1 2022-07-28T08:45:03.757168-04:00 truenas.local cron 302 - - + zfs send --raw --verbose -i pool1/videos@2022-07-28T080351-0400 pool1/videos@2022-07-28T084502-0400
Jul 28 05:45:03 truenas 1 2022-07-28T08:45:03.761156-04:00 truenas.local cron 302 - - send from pool1/videos@2022-07-28T080351-0400 to pool1/videos@2022-07-28T084502-0400 estimated size is 624B
Jul 28 05:45:03 truenas 1 2022-07-28T08:45:03.761291-04:00 truenas.local cron 302 - - total estimated size is 624
Jul 28 05:45:03 truenas 1 2022-07-28T08:45:03.761877-04:00 truenas.local cron 302 - - + echo 'Finished replicating incremental snapshots'
Jul 28 05:45:03 truenas 1 2022-07-28T08:45:03.761958-04:00 truenas.local cron 302 - - Finished replicating incremental snapshots
バックアップ失敗時のアラート
2か月後にバックアップが失敗し始めたらどうなるでしょうか。バックアップが正常に動作しているか確認するために、毎日ログをチェックするわけにはいきません。
幸い、スケジュールされたタスクが実行されなかったときに通知してくれるサービスは数多くあります。私はCronitorが無料枠が充実していてセットアップも簡単だったので、使うことにしました。
Cronitorで、TrueNASサーバー上のフルバックアップと同じ「0 0 3 * *」というスケジュールで新しいモニターを作成しました。

Cronitorはこのモニター用に次のようなユニークなURLを生成します。
https://cronitor.link/p/88e0dba70a87424b83c5fd3e9227ac92/1bBG6q
フルバックアップのcronジョブが成功を報告するように、バックアップが正常に完了したときにCronitorへ通知するcurlコマンドをcronジョブに追加します。
![/mnt/pool1/secure-backups/scripts/replicate-full-snapshots.sh && curl --silent https://cronitor.link/p/[my telemetry id]?state=complete](https://mtlynch.io/zfs-encrypted-backups/add-cronitor.png)
増分バックアップのジョブでも同じ手順を繰り返せば完了です!
これで暗号化されたZFSデータセットのバックアップを作成する堅牢なシステムができました。万が一ジョブが失敗すれば、Cronitorからアラートが届きます。
注意:暗号化ルートをバックアップする
ZFSを別のZFSシステムではなくファイルにバックアップする際の重要な注意点をコメントで指摘してくれた@Invisibleさんと@adamkfさんに感謝します。
データセットの暗号化ルートは必ずバックアップしてください。
暗号化されたデータセットの子であるデータセットをバックアップする場合、子データセットの「zfs send」では、別のTrueNASシステムでスナップショットを復元するために必要なすべてのデータが取得されず、バックアップが無意味になってしまいます。
データセットの暗号化ルートを確認するには、「zfs get -r encryptionroot pool/dataset」を実行します。データセットに親の暗号化がない場合は、そのデータセット自身が暗号化ルートであることを示す出力が表示されます。
$ zfs get -r encryptionroot pool1/diary-entries | head -n 2
NAME PROPERTY VALUE SOURCE
pool1/diary-entries encryptionroot pool1/diary-entries -
データセット自身が暗号化ルートである場合は、追加のデータセットをバックアップする必要はありません。
スナップショットファイルから復元できることを確認するために、別のZFSシステム、たとえTrueNASのVMであっても、そこでバックアップをテストすることをお勧めします。
ソースコード
便利スクリプトはGitHubで公開しています。
記事をランダムに読む
コメント
ログインしてコメントする