🍆

自作NASを構築した(Ubuntu Server+OpenZFS)

に公開

こんにちは。みなさんはどんなストレージを使っていますか?

筆者は主としてApple製品を使っているため、iCloud上のストレージ200GBのために、毎月1200円も支払っています[1]。このまま毎月1200円も支払い続けたら、1年で1.5万円、10年で15万円も、自分のものではないストレージのために支払い続けることになります。

というわけで、一念発起して、NASサーバーを導入することにしました。この記事では、NASを組み立てるまでの流れと、その後のソフトウェア面での構築について説明します。

サーバとして使うマシンを用意する

自作PCをNASとする、いわば自作NASを作るにあたって、次のような部品を選定しました。


制作にあたって購入した部品

まず最初に決めたのがケースです。それほど多くはありませんでしたが、NASとして運用するためのPCケースを販売しているメーカーがいくつかあり、中でもコンパクトでかわいらしいデザインの『Jonsbo N2 Black』を選びました。一辺が約20cmで、3.5インチドライブベイを5つ搭載しているケースです。

ケースはMini-ITX専用に作られていました。NASとして運用するにはそこまで高い性能が必要なわけではありませんので、ここでは中古の12世代 Pentiumと、それに対応しているマザーボードを選びました。

また、HDDはSeagate社のIronWolfを選びました。これは最上位モデルのIronWolf Proのひとつ下のモデルです。最上位モデルはAI需要のためか手に入りづらくなっていました。最上位モデル比べるとやや性能は劣りますが、NASとして運用できます。予算と相談しつつ、RAID5相当の構成で、しばらくは困らなそうな範囲ということで、8TBを3台ぶん購入しました。

その他の部品は以下のようになりました。

部品 製品名
ケース Jonsbo N2 Black
マザーボード ASUS PRIME H610I-PLUS D4
CPU Intel Pentium Gold G7400
HDD Seagate IronWolf ST8000VN002(×3)
SSD Apacer AP256GAS2280P4X-1
メモリ Crucial CT2K16G4DFRA32A
電源ユニット 玄人志向 KRPW-SX400W/90+
ケースファン Noctua NF-A12×15

組み上がった様子は次のようになります。


組み上がった様子

背面のケースファンは、付属のものからNoctuaのものに取り替えて、ファンガードをグリル型のものにしました。音が若干気になったというのと、英語圏のレビュー動画『JONSBO N2 NAS Case review + Build』で「HDD側から外に空気を出すためのものなのだから、メッシュにする意味がない」という指摘をされていたためです。


付属のケースファンをNoctuaのものにした様子

Ubuntu Serverをインストールし、NASにする

パソコンを組んだだけではNASにはなりません。ここからは、先程のマシンをNASとして使えるようにするための手順を紹介します。

OpenZFSでHDDをRAID構成にする

この記事では、NASを構成するためにOpenZFSというファイルシステムを利用します。OpenZFSは、Oracle Solarisのためのファイルシステムとして開発されていた「ZFS」を、LinuxやFreeBSDなどの複数のOSに向けて移植したものです。ZFSを利用すると、複数のHDDでディスクアレイを構成し信頼性を向上させるRAIDや、チェックサムによる破損の検出、データセット単位での圧縮率や暗号化などを実現できます。NAS専用OSとして著名な「TrueNAS」でもZFSが利用されています。

OpenZFSは、Ubuntuではzfsutils-linuxというパッケージとして提供されています[2]。以下のコマンドでインストールできます。

sudo apt install zfsutils-linux

次に、HDDのIDを確認します。HDDは通常 /dev/sda といったデバイス名で指定できますが、これではSATAの接続順序を入れ替えた場合などに正常に動作しなくなるためです。

$ ls -l /dev/disk/by-id
total 0
lrwxrwxrwx 1 root root  9 Feb  8 00:22 ata-ST8000VN002-XXXXXXXXXXXXXXX -> ../../sda
lrwxrwxrwx 1 root root  9 Feb  8 00:22 ata-ST8000VN002-YYYYYYYYYYYYYYY -> ../../sdc
lrwxrwxrwx 1 root root  9 Feb  8 00:22 ata-ST8000VN002-ZZZZZZZZZZZZZZZ -> ../../sdb

次に、これらのHDDを組み合わせて、ひとつのストレージプールを作ります。ストレージプールとは、複数のvdev(virtual device/仮想デバイス)を組み合わせた、ZFSの概念です。vdevにはHDDやSSDといったleaf vdev(物理デバイスに対応)と、それをさらに束ねるtop-level vdev(ミラーリングやRAIDZグループなどに対応)があります[3]

以下のコマンドで、ストレージプールを作ります。ここでは、通常のRAIDにおけるRAID5に相当する構成にしたいので、シングルパリティのRAIDZ構成であるRAIDZ-1のtop-level vdevを作り、そこに3つのHDDをleaf vdevを含めます。

sudo zpool create tank \
  raidz1 \
  /dev/disk/by-id/ata-ST8000VN002-XXXXXXXXXXXXXXX \
  /dev/disk/by-id/ata-ST8000VN002-YYYYYYYYYYYYYYY \
  /dev/disk/by-id/ata-ST8000VN002-ZZZZZZZZZZZZZZZ

コマンドライン引数の構造がいささか複雑ですが、ここではraidz1はvdev同士を区切る役割を果たしており、後続するvdevをその配下にグルーピングします[4]。なお、ZFSでは慣習としてストレージプールに tank という名前をつけることが多いようですので、そのようにしました[5]

ストレージプールが正常に作成されたことを確認します。3つのleaf vdevがtop-level vdev raidz1-0 の配下にあり、それがストレージプール tank の配下にあることが確認できます。

$ sudo zpool status
  pool: tank
 state: ONLINE
config:

        NAME                                 STATE     READ WRITE CKSUM
        tank                                 ONLINE       0     0     0
          raidz1-0                           ONLINE       0     0     0
            ata-ST8000VN002-XXXXXXXXXXXXXXX  ONLINE       0     0     0
            ata-ST8000VN002-YYYYYYYYYYYYYYY  ONLINE       0     0     0
            ata-ST8000VN002-ZZZZZZZZZZZZZZZ  ONLINE       0     0     0

ashift=12compression=lz4 を設定すべきか?

なお、本記事の執筆時点では「ストレージプール作成時に ashift=12compression=lz4 を設定しろ」という旨の情報を多く目にしました。ところが、最終的には、筆者の環境ではいずれのオプションも必要がないと判断しました。ここで、ashiftは ZFS が内部で使うセクタの長さ。compression は圧縮方式を指定するためのオプションです。

ashift は内部で判定して自動的に適切な値を使うようになってくれているようで、今回は何もしなくても12になりました。compression の値は、デフォルトで「on」となっていました。

$ sudo zdb -C tank | grep ashift
                ashift: 12
$ zfs get all tank | grep compression
tank  compression           on                     default

念のため「on」の意味を確認しましょう。zfsprops(7)を見ると各プロパティの意味と振る舞いが載っています。

When set to on (the default), indicates that the current default compression algorithm should be used. (中略) The current default compression algorithm is either lzjb or, if the lz4_compress feature is enabled, lz4.

以下のコマンドで lz4_compress フィーチャーが有効かを確認できます。筆者の環境では、何もしなくても有効になっていました。

$ zpool get feature@lz4_compress
NAME  PROPERTY              VALUE                 SOURCE
tank  feature@lz4_compress  active                local

というわけで、プール作成時には特にオプションを指定せずに、デフォルトのまま作成しました。今のところ、これで問題なく使えています。

SMBを使ってiPhoneやMacから読み書きする

SMBプロトコルでは、LAN上のシステムにファイルやプリンタを提供できます。SMBを使うことで、WindowsやmacOSなどのクライアントから、サーバ上のファイルシステムにアクセスすることができます。

Linuxでは、Sambaというソフトウェアによって、SMBを使ったファイルの配信が行えます。まず、『Install and Configure Samba | Ubuntu』を参考にしてSambaのインストールを行います。

次に、以下のコマンドを実行して、Sambaのためのデータセットをストレージプールの中に作ります。なお、tank/以下にディレクトリを切る代わりにデータセットを作成することによって、圧縮率や割当などのさまざまなプロパティを設定することができます[6]。また、作成されたデータセットはデフォルトで同名のパスにマウントされます[7]

sudo zfs create tank/share

Sambaの設定ファイルの末尾に以下を追加します。pathとして先ほど作成した/tank/shareを設定することで、SMBでの共有を可能にしています。

/etc/samba/smb.conf
[sambashare]
    comment = Samba on Ubuntu
    path = /tank/share
    read only = no
    browsable = yes
    vfs objects = catia fruit streams_xattr

Sambaを再起動します。

sudo systemctl restart smbd

これでLAN上のデバイスからNASに接続できるようになります。iOSでは以下のように表示されます。


iOSデバイスからSambaでNASに接続している様子。

なお、筆者の環境では vfs objects として catia fruit streams_xattr を設定しないと、iOSデバイスから書き込みができませんでした[8]。これらは、Apple製のSMBクライアントとの互換性を保つために必要なオプションのようです[9][10]

vfs objects を設定していないと「Read-Only」の表示が出てしまう。

ZFSのSMB機能は使わないのか?

ZFSには、データセットのプロパティとして、SMBでの共有機能が備わっています。これをうまく使えば、Sambaを別途設定する必要は無さそうに思います[11]

$ sudo zfs set sharesmb=on tank/share
$ sudo zfs get sharesmb tank/share
NAME        PROPERTY  VALUE     SOURCE
tank/share  sharesmb  on        local

ところが、こちらの方法で設定したSMBでは、Sambaを使って設定できるよりも限られた設定しか行なえないようでした。筆者の調べた限りでは、上述の vfs objects を設定する方法もありませんでした。よって、この記事では Smaba を使った構成としました。

「スクラブ」でストレージプールのエラーを検出する

ZFSでは、読み書きが行われたデータについては都度チェックサムが検証されますが、読み書きされていないデータについては検証が行われません。触っていないデータが知らず知らずのうちに破損していることを防ぐためにはスクラブと呼ばれる操作を行います。

スクラブは、指定したストレージプールのチェックサムを、能動的にすべて検証する機能です。加えて、RAIDZやミラーリングなどを使って冗長性を確保している場合には、自動的にこれを復元します[12]

スクラブは zpool scrub ストレージプール で行うことができます。

sudo zpool scrub tank

しばらくしてから zpool status ストレージプール を実行すると、結果が確認できます。結果は「No known data errors」。問題ありませんでした。

$ zpool status tank
  pool: tank
 state: ONLINE
  scan: scrub repaired 0B in 00:01:26 with 0 errors on Sun Feb  8 14:02:56 2026
config:

        NAME                                 STATE     READ WRITE CKSUM
        tank                                 ONLINE       0     0     0
          raidz1-0                           ONLINE       0     0     0
            ata-ST8000VN002-2ZM188_WPV2XR8L  ONLINE       0     0     0
            ata-ST8000VN002-2ZM188_WPV2Z7XF  ONLINE       0     0     0
            ata-ST8000VN002-2ZM188_WPV2Z8EL  ONLINE       0     0     0

errors: No known data errors

なお、Ubuntu版のOpenZFSでは、スクラブを定期的に実行してくれるcrontabが同梱されています[13]。デフォルトの設定では、毎月第2日曜日に実行されるようになっており、特別な設定を行わなくても自動的に整合性を検証してくれるようになっています。

$ cat /etc/cron.d/zfsutils-linux
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# TRIM the first Sunday of every month.
24 0 1-7 * * root if [ $(date +\%w) -eq 0 ] && [ -x /usr/lib/zfs-linux/trim ]; then /usr/lib/zfs-linux/trim; fi

# Scrub the second Sunday of every month.
24 0 8-14 * * root if [ $(date +\%w) -eq 0 ] && [ -x /usr/lib/zfs-linux/scrub ]; then /usr/lib/zfs-linux/scrub; fi

Ubuntu以外のディストリビューションでは、systemdタイマーが配布されているため、そちらを利用するとよいでしょう[14]

「ZED」を使ってスクラブの結果をSlackで通知する

ZED(ZFS Event Daemon)とは、ZFSで発生したイベント(zevent)を監視し、対応するタスクを実行する機能です[15]

ZEDでは、イベントが発生するとZEDLETという実行ファイルを順に実行していきます。有効なZEDLETは、デフォルトでは /etc/zfs/zed.d に配置されています[16]

筆者のZEDではデフォルトで /etc/zfs/zed.d/scrub_finish-notify.sh というZEDLETが有効になっていました。このZEDLETは、 RESILVER_FINISH および SCRUB_FINISH というzeventに反応するようになっているため、今回の用途に適しています[17]

ZEDからSlackにメッセージを送るには、Incoming Webhookを使ったアプリケーションを作成します。『Sending messages using incoming webhooks | Slack Developer Docs』を参考に、Slackワークスペースにアプリをインストールし、Webhook URLを取得してください。

次に、ZEDのデフォルトの設定ファイル /etc/zfs/zed.dZED_SLACK_WEBHOOK_URL を編集し、先程取得したSlackのWebhook URLを設定します。

zed.rc
##
# Slack Webhook URL.
# This allows posting to the given channel and includes an access token.
#   <https://api.slack.com/incoming-webhooks>
# Disabled by default; uncomment to enable.
#
-#ZED_SLACK_WEBHOOK_URL
+ZED_SLACK_WEBHOOK_URL="https://hooks.slack.com/services/XXXXXXXXXXXX"

設定が終わったら、もう一度手動でスクラブを実行します。

sudo zpool scrub tank

しばらくすると、スクラブが終了し結果がSlackに通知されました。結果のメッセージは以下のようになります。


SlackでZEDを使ってスクラブの結果を受け取る様子

まとめ

この記事では、Ubuntu ServerにOpenZFSをインストールして、自宅サーバをNASとして運用する方法を見ていきました。OpenZFSにはストレージプールという概念があり、信頼性が高いRAIDZとして構成できることや、ZEDを使ったイベントの通知方法、Sambaを使ったSMBプロトコルでの共有方法を確認しました。

なお、筆者はこの記事で紹介したものとほとんど同じ構成でNASを運用しています。NASではSMBの他に、NFSを使ってLAN内の他のアプリケーションから書き込んでいます。特に、セルフホストできるGoogle Photoの代替アプリ「Immich」とは、非常にスムーズに組み合わせることができました。

再現可能なソースコードは、Ansible PlaybookとしてGitHub上で公開していますので、ぜひご確認ください。

https://github.com/neet/homelab

脚注
  1. 厳密にはApple Oneというプランのため、ストレージだけに1200円も支払っているわけではありません。 ↩︎

  2. Ubuntu — OpenZFS documentation ↩︎

  3. A vdev (virtual device) is a fundamental building block of ZFS storage pools. https://openzfs.github.io/openzfs-docs/Basic Concepts/VDEVs.html#vdevs ↩︎

  4. Keywords like mirror and raidz are used to distinguish where a group ends and another begins. zpoolconcepts.7 — OpenZFS documentation ↩︎

  5. zfs - Why are all the zpools named "tank"? - Server Fault ↩︎

  6. Every dataset has a set of properties that export statistics about the dataset as well as control various behaviors. zfsprops.7 — OpenZFS documentation ↩︎

  7. By default, file systems are mounted under /path, where path is the name of the file system in the ZFS namespace zfsconcepts.7 — OpenZFS documentation ↩︎

  8. Samba share is readonly from iOS, but writable from other OSes - Ask Ubuntu ↩︎

  9. vfs_fruit ↩︎

  10. 3.13. MacOS クライアント向けの Samba の設定 | さまざまな種類のサーバーのデプロイメント | Red Hat Enterprise Linux | 8 | Red Hat Documentation ↩︎

  11. Controls whether the file system is shared by using Samba USERSHARES and what options are to be used. zfsprops.7 — OpenZFS documentation ↩︎

  12. The scrub examines all data in the specified pools to verify that it checksums correctly zpool-scrub.8 — OpenZFS documentation ↩︎

  13. addition of zfsutils-linux scrub every 2nd sunday · Issue #9858 · openzfs/zfs ↩︎

  14. zpool-scrub.8 — OpenZFS documentation zpool-scrub.8 — OpenZFS documentation ↩︎

  15. The ZED (ZFS Event Daemon) monitors events generated by the ZFS kernel module. zed.8 — OpenZFS documentation ↩︎

  16. The default directory for enabled ZEDLETs. zed.8 — OpenZFS documentation ↩︎

  17. Send notification in response to a RESILVER_FINISH or SCRUB_FINISH. https://github.com/ubuntu/zfs/blob/64822c7a29068d25efe430d9d1692efc31987bf7/cmd/zed/zed.d/scrub_finish-notify.sh#L3-L7 ↩︎

Discussion