Migrating my NAS from CoreOS/Flatcar Linux to NixOS

Michael Stapelberg

NASをCoreOS/Flatcar LinuxからNixOSへ移行する

原文は Michael Stapelberg により に公開されました。 このブログを購読する

この記事では、既存のLinuxサーバーをNixOSへ移行する方法を紹介します。私の場合は、Network Attached Storage(NAS)用PCに入っていたCoreOS/Flatcar Linuxからの移行です。

以前のCoreOS環境がどうなっていたか(Dockerコンテナを起動するsystemdユニットが多数)、まず動かすためにどうやって中間状態(NixOS上でDockerを使う状態)へ移行したか、そして最終的にすべてのユニットをDockerからネイティブなNixOSモジュールへ段階的に移行したかを、詳しく見ていきます。

NixOSをご存じない方は、NixOSが何であり、どのようなことが可能になるのかを理解するために、NixOSウェブサイトのトップページを読むことをおすすめします。

このブログ記事の想定読者は、NAS用途でNixOSを試してみたいと考えていて、具体例を見ながらシステムの設定方法を理解したい方です。

これらの例は、まず私のブログ記事「How I like to install NixOS (declaratively)」の手順に従ってから、興味のあるセクションを読み進める形で試すことができます。完全な設定を直接見たい方は、結論までスキップしてください。

2023年に自作したPC NAS

背景・経緯

過去10年間、NASの用途でいくつもの異なるOSを使ってきました。2台のNASシステム storage2 と storage3 の変遷は次のとおりです。

storage2storage3詳細(ブログ記事)
2013qnap上のDebianqnap上のDebianWake-On-LAN with Debian on a qnap TS-119P2+
2016PC上のCoreOSPC上のCoreOSGigabit NAS (running CoreOS)
2023PC上のCoreOSPC上のUbuntu+ZFSMy all-flash ZFS NAS build
2025PC上のNixOSPC上のUbuntu+ZFS→ 現在地 ←
?PC上のNixOSPC上のNixOS+ZFSさらに多くのPCをNixOSに移行するのは時間の問題でしょう ;)

私のNASに求めるソフトウェア要件

  • (この記事はソフトウェアのみが対象です! 私の使い方やハードウェア選定に関する要件については、私の記事「My all-flash ZFS NAS build(2023)」の「Design Goals」を参照してください。)
  • リモート管理: ネットワークストレージの構築における設定をバージョン管理し、メインPCで管理できるモデルがとても気に入っています。バックアップ環境へのアクセスが必要になったときに、PCから数分でNASを再インストールして復旧できるのは嬉しい特性です。
  • 自動アップデートと簡単なロールバック: すべての環境を手作業でアップデートするのは御免です。したがって自動アップデートは必須ですが、アップデートが失敗したときに迅速かつ簡単に復旧できる手段もまた必須です。
    • CoreOS/FlatcarはA/Bアップデート方式(アップデートに失敗したら古いパーティションを起動)でそれを実現していましたが、NixOSは「ジェネレーション」という概念(アップデートに失敗したら古いジェネレーションを選択)で、よりきめ細かく実現しています。

なぜCoreOS/FlatcarからNixOSへ移行するのか?

CoreOSを使い始めた頃、Dockerはまだかなり新しい技術でした。Dockerコンテナを使えばサービスを均一に扱える点が気に入っていました。結局のところ、どのサービスも何らかのポート(HTTPやPostgresなど)を公開するわけですから、安定したOS上でより新しいバージョンのソフトウェアを動かす柔軟性が得られますし、アップデートで何かが壊れた場合には古いバージョンに戻すこともできます。

10年以上経った今、Dockerは確立された技術になりました。コンテナアプローチのさまざまな利点は、今では当たり前のものとして受け入れられています。

そこで、Flatcar Linuxに満足できなくなった理由を挙げます。

R1. cloud-initが非推奨になった

CoreOS cloud-initプロジェクトは、ある時点でIgnitionに置き換えられ非推奨となりました。Ignitionは明らかに強力ですが、ホビーユーザーが始めるにはより面倒です。私の理解では、設定をどこかのURLでホストし、カーネルパラメータで指定しなければなりません。単にファイルをコピーするだけの昔のやり方は、もはやサポートされていないようです。

Ignitionは他の面でも不便に感じます。YAMLはサポートされなくなりJSONのみになり、手で書くのはあまり楽しくありません。また、フォーマットもかなり頻繁に変わるようです。

結果として、私はcloud-initからIgnitionへの移行を一度も行わず、長く非推奨のままのOSの使い方に依存し続けるのは良くない状態でした。

R2. コンテナの劣化(Bitrot)

あるとき、Docker Hub上の自分のコンテナを監査してみると、そのほとんどがかなり古くなっていることに気づきました。しばらくの間、Docker HubはGitHubから取得したDockerfileに基づく自動ビルドを提供していました。しかし現在は自動ビルドにサブスクリプションが必要で、自分のコンピュータを使うためだけにサブスクリプションを契約するつもりはありません。

R3. 中央サービスへの依存

もしDockerがいつかDocker Hubの運用を停止すれば、NASにソフトウェアをデプロイできなくなります。これは決して仮定の話ではありません。2023年、Docker Hubは無料枠でのOrganizationの終了を発表し、コミュニティの反発を受けて撤回しました。

私のようなホビーユーザーに無料サービスを提供し続けられるのがいつまでなのか、誰にもわかりません。

R4. FlatcarではImmichを試せなかった

とどめとなったのは、NASシステムでImmichを試せないことに気づいたときでした。Immichのような最新のウェブアプリケーションは複数のDockerコンテナ(PostgresやRedisなど)を必要とし、サポートされているインストール方法としてDocker Composeしか提供していません。

残念ながら、FlatcarにはDocker Composeが含まれていません

進行中の作業としてImmichを非Docker Composeシステム向けに再パッケージする気にはなれなかったので、Immichのようなソフトウェアを直接実行することも、Docker Composeすら実行できないシステムでは、もはや私のニーズには不十分だと判断しました。

理由のまとめ

以上の理由から、自動コンテナビルドをセットアップし、自分で中央レジストリを運用しなければならず、それでもImmichのようなよく知られたオープンソースソフトウェアを実行できないことになります。

そこで、10年ぶりにNixOSを再び試してみることにしました。現在最も人気のある宣言的なソリューションで、コミュニティも大きく、パッケージの選択肢も豊富だからです。

私の状況でNixOSはどうでしょうか?

  • 同じ: NixOSシステムをアップデートするための自動ジョブもセットアップする必要があります。
    • 私はすでにgokrazyデバイスのアップデート用にそうしたジョブを持っています。
    • Dockerのpushは非同期です。pushが成功した後も、ターゲットホストで更新されたコンテナをpullし、影響を受けるサービスを再起動するための追加の自動化が必要ですが、NixOSにはそれらすべてが含まれています。
  • 良い点: 中央レジストリがありません。NixOSなら、ビルド結果をSSH経由でターゲットホストに直接pushできます。
  • 良い点: NixOSで利用可能なソフトウェアの数ははるかに多く(例えばImmichも含まれます)、NixOSモジュールは一般的に個々のDockerコンテナよりも高い抽象度で表現されているため、より少ない設定行数で多くの機能を設定できます。

VMでのプロトタイピング

NAS環境は毎日動く必要があるので、システムに変更を加える前にVMで望む設定をプロトタイプしたかったのです。これはより安全なだけでなく、障害を事前に発見でき、コミットする前にNixOSを扱う感覚を掴むこともできます。

以前のテストインストールからNixOS設定をコピーし(「How I like to install NixOS (declaratively)」を参照)、以下のコマンドでVMイメージをビルドしてQEMUで起動しました。

nix build .#nixosConfigurations.storage2.config.system.build.vm

export QEMU_NET_OPTS=hostfwd=tcp::2222-:22
export QEMU_KERNEL_PARAMS=console=ttyS0
./result/bin/run-nixplay-vm

以下の設定手順はこのVMで試すことができ、十分に納得できたら、実際のマシンで同じ手順を繰り返して移行できます。

移行

実際のシステムの移行にあたって、1時間程度の典型的な作業セッションで達成できる次のマイルストーンを定義しました。

  • M1. NixOSをインストールする
  • M2. リモートディスクアンロックを設定する
  • M3. アクセス用にSambaを設定する
  • M4. バックアップ用にSSH/rsyncを設定する
  • それ以外はあれば嬉しいもので、別の日のセッションに先送りできます。

実際、この計画通りに進みました。NixOSの実際のインストールと、マイルストーンM4までの設定は1時間強で完了しました。その他の「あれば嬉しい」項目は、時間が許すときに数日〜数週間かけて行いました。

ヒント: 2000年代にインストーラーのバグでデータを失って以来、システムディスクを再インストールする際はすべてのデータディスクを物理的に切断する(SATAケーブルを抜く)習慣を身につけました。

M1. NixOSをインストールする

「How I like to install NixOS (declaratively)」に従った後の、初期のconfiguration.nixは次のとおりです。

{ modulesPath, lib, pkgs, ... }:
{
  imports = [
    (modulesPath + "/installer/scan/not-detected.nix")
    ./hardware-configuration.nix
    ./disk-config.nix
  ];
  nix.settings.trusted-users = [ "michael" "root" ];
  nix.gc = {
    automatic = true;
    dates = "weekly";
    options = "--delete-older-than 7d";
  };
  boot.loader.systemd-boot = {
    enable = true;
    configurationLimit = 10;
  };
  boot.loader.efi.canTouchEfiVariables = true;
  networking.hostName = "storage2";
  time.timeZone = "Europe/Zurich";
  services.resolved.enable = true;
  networking.useDHCP = false;
  systemd.network.enable = true;
  systemd.network.networks."10-e" = {
    matchConfig.Name = "e*";
    networkConfig = {
      IPv6AcceptRA = true;
      DHCP = "yes";
    };
  };
  users.mutableUsers = false;
  security.sudo.wheelNeedsPassword = false;
  users.users.michael = {
    openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5secret"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5key"
    ];
    isNormalUser = true;
    description = "Michael Stapelberg";
    extraGroups = [ "networkmanager" "wheel" ];
    initialPassword = "secret";
    shell = pkgs.zsh;
  };
  environment.systemPackages = with pkgs; [
    git rsync zsh vim emacs wget curl
  ];
  programs.zsh.enable = true;
  services.openssh.enable = true;
  system.stateVersion = "25.05";
}

以降のすべてのセクションでは、このconfiguration.nix内での変更について説明します。

自宅ネットワーク内のすべてのデバイスはDHCPでIPアドレスを取得します。IPアドレスを固定したい場合は、ルーター側でそれに応じて設定します。

私のNAS PCはIPアドレスに関して一つの特徴があります。IPv4とIPv6の両方で到達可能で、IPv6アドレスはIPv4アドレスから導き出せるようになっています。

そこで、上記のsystemd-networkd設定を、動的に設定されるIPv6ネットワーク内で静的なIPv6アドレスを設定するように変更しました。

systemd.network.networks."10-e" = {
  matchConfig.Name = "e*";
  networkConfig = {
    IPv6AcceptRA = true;
    DHCP = "yes";
  };
  ipv6AcceptRAConfig = {
    Token = "::10:0:0:252";
  };
};

✅ これでマイルストーンM1は達成です。

M2. リモートディスクアンロックを設定する

起動時に暗号化されたディスクをアンロックするために、wget(1)cryptsetup(8)を使ってNASとリモートサーバー間で鍵ファイルを分割するカスタムsystemdサービスユニットを用意しています(=攻撃者は両方の断片を入手しなければアンロックできません)。

CoreOS/Flatcarでは、cloud-init設定は次のようになっていました。

coreos:
  units:
    - name: unlock.service
      command: start
      content: |
        [Unit]
        Description=unlock hard drive
        Wants=network.target
        After=systemd-networkd-wait-online.service
        Before=samba.service
        [Service]
        Type=oneshot
        RemainAfterExit=yes
        ExecStart=/bin/sh -c "c=0; while [ $c -lt 5 ]; do /bin/ping6 -n -c 1 r.zekjur.net && break; c=$((c+1)); sleep 1; done"
        ExecStart=/bin/sh -c "[ -e \"/dev/mapper/S5SSNF0T205183F_crypt\" ] || (echo -n my_local_secret && wget ... https://r.zekjur.net:8443/nascrypto) | /sbin/cryptsetup --key-file=- luksOpen ..."
        ExecStart=/bin/sh -c "vgchange -ay"
        ExecStart=/bin/mount /dev/mapper/data-data /srv

これを次のようなNixOS設定に変換しました。

systemd.services.unlock = {
  wantedBy = [ "multi-user.target" ];
  description = "unlock hard drive";
  wants = [ "network.target" ];
  after = [ "systemd-networkd-wait-online.service" ];
  serviceConfig = {
    Type = "oneshot";
    RemainAfterExit = "yes";
    ExecStart = [
      ''/bin/sh -c "c=0; while [ $c -lt 5 ]; do ${pkgs.iputils}/bin/ping -n -c 1 r.zekjur.net && break; c=$((c+1)); sleep 1; done"''
      ''/bin/sh -c "[ -e \"/dev/mapper/S5SSNF0T205183F_crypt\" ] || (echo -n my_local_secret && ${pkgs.wget}/bin/wget --retry-connrefused --ca-directory=/dev/null --ca-certificate=/etc/ssl/certs/r.zekjur.net.crt -qO - https://r.zekjur.net:8443/sdb2_crypt) | ${pkgs.cryptsetup}/bin/cryptsetup --key-file=- luksOpen /dev/disk/by-id/ata-Samsung_SSD_870_QVO_8TB_S5SSNF0T205183F S5SSNF0T205183F_crypt"''
      ''/bin/sh -c "[ -e \"/dev/mapper/S5SSNJ0T205991B_crypt\" ] || (echo -n my_local_secret && ${pkgs.wget}/bin/wget ... https://r.zekjur.net:8443/sdc2_crypt) | ${pkgs.cryptsetup}/bin/cryptsetup --key-file=- luksOpen /dev/disk/by-id/ata-Samsung_SSD_870_QVO_8TB_S5SSNJ0T205991B S5SSNJ0T205991B_crypt"''
      ''/bin/sh -c "${pkgs.lvm2}/bin/vgchange -ay"''
      ''/run/wrappers/bin/mount /dev/mapper/data-data /srv''
    ];
  };
};

カスタムTLS証明書ファイルもディスクに保存する必要があります。そのためにenvironment.設定を使えます。

environment.etc."ssl/certs/r.zekjur.net.crt".text = ''
-----BEGIN CERTIFICATE-----
MIID8TCCAlmgAwIBAgIRAPWwvYWpoH+lGKv6rxZvC4MwDQYJKoZIhvcNAQELBQAw
[...]
-----END CERTIFICATE-----
'';

${pkgs.wget}のような参照はNixストアへのパスに置き換えられます(→ nix.devドキュメント)。CoreOS/Flatcarでは、ベースイメージに含まれる(最小限の)ソフトウェアだけを使うか、Dockerに頼るしかありませんでした。NixOSではnixpkgsで利用可能なすべてのパッケージを使えます。

デプロイしてrebootした後、/srv配下でアンロックされたディスクにアクセスできるようになりました! 🎉

% df -h /srv
Filesystem             Size  Used Avail Use% Mounted on
/dev/mapper/data-data   15T   14T  342G  98% /srv

ファイルを一覧したところ、古いシステムと新しいシステムでグループIDが異なっていることに気づきました。これは、目的のグループIDを明示的に指定することで修正できます。

users.groups.michael = {
  gid = 1000;  # for consistency with storage3
};

✅ M2は完了です。

M3. アクセス用にSambaを設定する

リモートディスクアンロックはsystemdサービスレベルで設定したい一方、SambaについてはDockerを使いたいと考えました。まず古い(動作している)Dockerベースのセットアップをそのまま移行し、後でNixに変換するためです。

Docker NixOSモジュールを有効にします。これによりDockerが必要とするデーモンや、その他動作に必要なものがセットアップされます。

virtualisation.docker.enable = true;

これは他のサービスがDockerを使うにはすでに十分ですが、デバッグのためにdockerコマンドを対話的に実行できるようにもしたいので、dockersystemPackagesに追加しました。

environment.systemPackages = with pkgs; [
  git rsync zsh vim emacs wget curl
  docker
];

この設定をデプロイした後、docker run -ti debianを実行して動作を確認できます。

cloud-init版のsambaは次のようになっていました。

[Unit]
Description=samba server
After=docker.service unlock.mount
Requires=docker.service unlock.mount
[Service]
Restart=always
StartLimitInterval=0
ExecStartPre=-/usr/bin/docker pull stapelberg/docker-samba:latest
ExecStartPre=-/usr/bin/docker kill smb
ExecStartPre=-/usr/bin/docker rm smb
ExecStartPre=/usr/bin/docker run --name smb-prep stapelberg/docker-samba sh -c 'adduser ... && sed -i ... /etc/samba/smb.conf'
ExecStartPre=/usr/bin/docker commit smb-prep smb-prepared
ExecStart=/usr/bin/docker run -p 137:137 -p 138:138 -p 139:139 -p 445:445 --tmpfs=/run -v /srv/data:/srv/data --name smb -t smb-prepared /usr/sbin/smbd --foreground --debug-stdout --no-process-group

これをNixOSに1対1で翻訳できます。

systemd.services.samba = {
  wantedBy = [ "multi-user.target" ];
  description = "samba server";
  after = [ "unlock.service" ];
  requires = [ "unlock.service" ];
  serviceConfig = {
    Restart = "always";
    StartLimitInterval = 0;
    ExecStartPre = [
      ''-${pkgs.docker}/bin/docker pull stapelberg/docker-samba:latest''
      ''-${pkgs.docker}/bin/docker kill smb''
      ''-${pkgs.docker}/bin/docker rm smb''
      ''-${pkgs.docker}/bin/docker run --name smb-prep stapelberg/docker-samba sh -c 'adduser --quiet --disabled-password --gecos "" --uid 29901 michael && sed -i "s,\\[global\\],[global]\\nserver multi channel support = yes\\naio read size = 1\\naio write size = 1,g" /etc/samba/smb.conf' ''
      ''-${pkgs.docker}/bin/docker commit smb-prep smb-prepared''
    ];
    ExecStart = ''-${pkgs.docker}/bin/docker run -p 137:137 -p 138:138 -p 139:139 -p 445:445 --tmpfs=/run -v /srv/data:/srv/data --name smb -t smb-prepared /usr/sbin/smbd --foreground --debug-stdout --no-process-group'';
  };
};

✅ これでネットワーク経由でファイルを管理できるようになり、M3は完了です!

参照: あれば嬉しい追加項目: N5. NixOS版samba

M4. バックアップ用にSSH/rsyncを設定する

データのバックアップにはSSH経由のrsyncを使っています。このSSHアクセスは、rrsync(Dockerコンテナ内)を使ってrsyncコマンドのみを実行できるように制限しています。SSHのauthorized_keys(5)を設定するには、次のようにします。

users.users.root.openssh.authorizedKeys.keys = [
  ''command="${pkgs.docker}/bin/docker run --log-driver none -i -e SSH_ORIGINAL_COMMAND -v /srv/backup/midna:/srv/backup/midna stapelberg/docker-rsync /srv/backup/midna" ssh-rsa AAAAB3Npublickey root@midna''
];

✅ テストバックアップが成功すれば、マイルストーンM4は完了です!

参照: あれば嬉しい追加項目: N6. NixOS版rrsync

あれば嬉しい追加項目

N1. Prometheus Node Exporter

すべてのマシンをPrometheus(とGrafana)で監視するのが好きです。ネットワーク接続と認証にはTailscaleのメッシュVPNを使っています。

Tailscaleをインストールするには、そのNixOSモジュールを有効にし、tailscaleコマンドを利用可能にします。

services.tailscale.enable = true;
environment.systemPackages = with pkgs; [ tailscale ];

デプロイ後、sudo tailscale upを実行し、ブラウザでログインリンクを開きます。

Prometheus Node Exporterも、そのNixOSモジュールを通じて簡単に有効化できます。

services.prometheus.exporters.node = {
  enable = true;
  listenAddress = "storage2.example.ts.net";
};

しかし、これはまだ信頼性がありません。システム起動時にTailscaleの起動に時間がかかると、Node ExporterがTailscaleのIPアドレスでまだリッスンできないために再起動の上限を使い切ってしまう可能性があります。サービスが最終的に起動するように、無限再起動を有効にできます。

systemd.services."prometheus-node-exporter" = {
  startLimitIntervalSec = 0;
  serviceConfig = {
    Restart = "always";
    RestartSec = 1;
  };
};

N2. 信頼性の高いマウント

セットアップを移行している最中に、unlock.serviceから直接mount(8)を呼び出すのは信頼性が低く、systemdにマウントを管理させる方が良いことに気づきました。

fileSystems."/srv" = {
  device = "/dev/mapper/data-data";
  fsType = "ext4";
  options = [
    "nofail"
    "x-systemd.requires=unlock.service"
  ];
};

その後、unlock.serviceからmount(8)の呼び出しを削除できます。

@@ -247,7 +247,10 @@
         ''/bin/sh -c "${pkgs.lvm2.bin}/bin/vgchange -ay"''
-        ''/run/wrappers/bin/mount /dev/mapper/data-data /srv''
+        # Let systemd mount /srv based on the fileSystems./srv
+        # declaration to prevent race conditions: mount
+        # might not succeed while the fsck is still in progress,
+        # for example, which otherwise makes unlock.service fail.

systemdサービスでは、/srvマウントユニットに依存できるようになります。

systemd.services.jellyfin = {
 unitConfig.RequiresMountsFor = [ "/srv" ];
 wantedBy = [ "srv.mount" ];
};

N3. nginx-healthz

電力節約のため、使っていないときはNASの電源を切っています。

バックアップのオーケストレーションはWake-on-LANで起動をかけ、バックアップジョブを開始する前にNASが完全に起動して/srvマウントが完了するまで待つ必要があります。

そのために、/srvマウントに依存するウェブサーバー(ファイルなし)を設定しています。ウェブサーバーがHTTPリクエストに応答するようになれば、/srvがマウントされたことがわかります。

cloud-init設定は次のようになっていました。

[Unit]
Description=nginx for /srv health check
Wants=network.target
After=srv.mount
Requires=srv.mount
[Service]
Restart=always
ExecStartPre=/bin/sh -c 'systemctl is-active docker.service'
ExecStartPre=/usr/bin/docker pull nginx:1
ExecStartPre=-/usr/bin/docker kill nginx-healthz
ExecStart=/usr/bin/docker run --name nginx-healthz --publish 10.0.0.252:8200:80 --log-driver=journald nginx:1

Docker版(Flatcar Linuxから移植)は次のようになります。

systemd.services.healthz = {
  description = "nginx for /srv health check";
  wants = [ "network.target" ];
  unitConfig.RequiresMountsFor = [ "/srv" ];
  wantedBy = [ "srv.mount" ];
  startLimitIntervalSec = 0;
  serviceConfig = {
    Restart = "always";
    ExecStartPre = [
      ''/bin/sh -c 'systemctl is-active docker.service' ''
      ''-${pkgs.docker}/bin/docker pull nginx:1''
      ''-${pkgs.docker}/bin/docker kill nginx-healthz''
    ];
    ExecStart = [ ''-${pkgs.docker}/bin/docker run --name nginx-healthz --publish 10.0.0.252:8200:80 --log-driver=journald nginx:1'' ];
  };
};

この設定は、DockerからNixOSへ移行するとずっとシンプルになります。

# Signal readiness on HTTP port 8200 once /srv is mounted:
networking.firewall.allowedTCPPorts = [ 8200 ];
services.caddy = {
  enable = true;
  virtualHosts."http://10.0.0.252:8200".extraConfig = ''
    respond "ok"
  '';
};
systemd.services.caddy = {
  unitConfig.RequiresMountsFor = [ "/srv" ];
  wantedBy = [ "srv.mount" ];
};

N4. NixOS版Jellyfin

Docker版(Flatcar Linuxから移植)は次のようになります。

networking.firewall.allowedTCPPorts = [ 4414 8096 ];
systemd.services.jellyfin = {
  wantedBy = [ "multi-user.target" ];
  description = "jellyfin";
  after = [ "docker.service" "srv.mount" ];
  requires = [ "docker.service" "srv.mount" ];
  startLimitIntervalSec = 0;
  serviceConfig = {
    Restart = "always";
    ExecStartPre = [
      ''-${pkgs.docker}/bin/docker pull lscr.io/linuxserver/jellyfin:latest''
      ''-${pkgs.docker}/bin/docker rm jellyfin''
    ];
    ExecStart = [ ''-${pkgs.docker}/bin/docker run --rm --net=host --name=jellyfin -e TZ=Europe/Zurich -v /srv/jellyfin/config:/config -v /srv/data/movies:/data/movies:ro -v /srv/data/series:/data/series:ro -v /srv/data/mp3:/data/mp3:ro lscr.io/linuxserver/jellyfin:latest'' ];
  };
};

これまでと同様に、NixOSのjellyfinを使うと設定はシンプルになります。

services.jellyfin = {
  enable = true;
  openFirewall = true;
};
systemd.services.jellyfin = {
  unitConfig.RequiresMountsFor = [ "/srv" ];
  wantedBy = [ "srv.mount" ];
};

しばらくの間、古い場所(Dockerコンテナ内の/data/movies)から新しい場所(/srv/data/movies)への互換性のためのシンボリックリンクも設定していましたが、Jellyfinで奇妙な問題に遭遇し、結局Jellyfinの状態全体を再初期化することになりました。必要な設定は行数が増えましたが、それを独自のファイルに分けるのがすっきりすると感じたので、その方法を紹介します。

上記の行をconfiguration.nixから削除し、jellyfin.nixに移動します。

{
  config, lib, pkgs, modulesPath, ...
}:
{
  services.jellyfin = {
    enable = true;
    openFirewall = true;
    dataDir = "/srv/jellyfin";
    cacheDir = "/srv/jellyfin/config/cache";
  };
  systemd.services.jellyfin = {
    unitConfig.RequiresMountsFor = [ "/srv" ];
    wantedBy = [ "srv.mount" ];
  };
}

そして、configuration.niximportsjellyfin.nixを追加します。

imports = [
  ./hardware-configuration.nix
  ./jellyfin.nix
];

N5. NixOS版samba

NixOSのSambaを使うために、M3のsystemd.services.samba設定を次のものに置き換えました。

services.samba = {
  enable = true;
  openFirewall = true;
  settings = {
    "global" = {
      "map to guest" = "bad user";
    };
    "data" = {
      "path" = "/srv/data";
      "comment" = "public data";
      "read only" = "no";
      "create mask" = "0775";
      "directory mask" = "0775";
      "guest ok" = "yes";
    };
  };
};
system.activationScripts.samba_user_create = ''
  smb_password="secret"
  echo -e "$smb_password\n$smb_password\n" | ${lib.getExe' pkgs.samba "smbpasswd"} -a -s michael
'';

注: アクティベーションスクリプトでsambaパスワードを設定する方法は小規模なセットアップでは機能しますが、sambaパスワードをNixストアの外に置きたい場合は別のアプローチが必要です。別のマシンでは、secretsの管理にsops-nixを使っていて、smbpasswdの呼び出しを次のようにリファクタリングすると確実に動作することがわかりました。

let
  setPasswords = pkgs.writeShellScript "samba-set-passwords" ''
    set -euo pipefail
    for user in michael; do
        smb_password="$(cat /run/secrets/samba_passwords/$user)"
        echo -e "$smb_password\n$smb_password\n" | ${lib.getExe' pkgs.samba "smbpasswd"} -a -s $user
    done
  '';
in {
  services.samba = { /* …as above… */ }
  systemd.services.samba-smbd.serviceConfig.ExecStartPre = [ "${setPasswords}" ];
  sops.secrets."samba_passwords/michael" = {
    restartUnits = [ "samba-smbd.service" ];
  };
}

また、NixOSはデフォルトではユーザーごとにグループを作成しないことにも気づきましたが、私はそのように権限を管理するのに慣れています。グループは次のように簡単に宣言できます。

users.groups.michael = {
  gid = 1000; # for consistency with storage3
};
users.users.michael = {
  extraGroups = [
    "wheel"
    "docker"
    "michael"
  ];
};

N6. NixOS版rrsync

Docker版(Flatcar Linuxから移植)は次のようになります。

users.users.root.openssh.authorizedKeys.keys = [
  ''command="${pkgs.docker}/bin/docker run --log-driver none -i -e SSH_ORIGINAL_COMMAND -v /srv/backup/midna:/srv/backup/midna stapelberg/docker-rsync /srv/backup/midna" ssh-rsa AAAAB3Npublickey root@midna''
];

NixOSのrrsyncを使うために、設定を次のように変更しました。

users.users.root.openssh.authorizedKeys.keys = [
  ''command="${pkgs.rrsync}/bin/rrsync /srv/backup/midna" ssh-rsa AAAAB3Npublickey root@midna''
];

N7. sync.plスクリプト

Docker版(Flatcar Linuxから移植)は次のようになります。

users.users.root.openssh.authorizedKeys.keys = [
  ''command="${pkgs.docker}/bin/docker run --log-driver none -i -e SSH_ORIGINAL_COMMAND -v /srv/data:/srv/data -v /root/.ssh:/root/.ssh:ro -v /etc/ssh:/etc/ssh:ro -v /etc/static/ssh:/etc/static/ssh:ro -v /nix/store:/nix/store:ro stapelberg/docker-sync",no-port-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3Npublickey sync@dr''
];

sync.plを配布するために以下のDockerfileを管理するのをやめたかったのです。

FROM debian:stable
RUN apt-get update \
    && apt-get install -y rsync ssh perl
ADD sync.pl /usr/bin/
ENTRYPOINT ["/usr/bin/sync.pl"]

Dockerコンテナをなくすために、sync.plファイルを、sync.pl PerlスクリプトをNixストアに書き込むNix式に翻訳しました。

{ pkgs }:
pkgs.writers.writePerlBin "syncpl" { libraries = []; } ''
# This script is run via ssh from dornröschen.
use strict;
use warnings;
use Data::Dumper;
if (my ($destination) = ($ENV{SSH_ORIGINAL_COMMAND} =~ /^([a-z0-9.]+)$/)) {
    print STDERR "rsync version: " . `${pkgs.rsync}/bin/rsync --version` . "\n\n";
    my @rsync = (
        "${pkgs.rsync}/bin/rsync",
        "-e", "ssh",
        "--max-delete=-1", "--verbose", "--stats",
        "-ax", "--ignore-existing", "--omit-dir-times",
        "/srv/data/", ''$ {destination}:/",
    );
    print STDERR "running: " . Dumper(\@rsync) . "\n";
    exec @rsync;
} else {
    print STDERR "Could not parse SSH_ORIGINAL_COMMAND.\n";
}
''

このファイルは、configuration.nixでインポートし、NixOS設定のpkgs式を渡すことで参照できます。

{ modulesPath, lib, pkgs, ... }:
let
  syncpl = import ./syncpl.nix { pkgs = pkgs; };
in {
  imports = [ ./hardware-configuration.nix ];
  users.users.root.openssh.authorizedKeys.keys = [
    ''command="${syncpl}/bin/syncpl",no-port-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3Npublickey sync@dr''
  ];
  environment.systemPackages = [ syncpl ];
}

これは動作しますが、最良のアプローチでしょうか? いくつか考えを挙げます。

  • このスクリプトをNix式の中で管理することで、エディタのPerlサポートが使えなくなります。
    • おそらく、sync.plを別ファイルとして保持したまま、Nix式内で文字列補間を使ってrsyncバイナリへの絶対パスをスクリプトに注入することもできるでしょう。
  • 別の代替案として、Nix式にラッパースクリプトを追加して$PATHrsyncが含まれるようにすれば、スクリプトで絶対パスが不要になるでしょう。
  • このような小さなグルースクリプトについては、設定ディレクトリ内のファイルが一つ減るため、内容をNix式の中に「インライン」で管理する方が簡単だと考えています。

N8. 設定の共有

すべてのNixOSシステムでユーザー設定がどこでも同一になるように設定したいと考えています。

それを実現するために、configuration.nixの一部をuser-settings.nixに抽出し、それを出力として提供する付随するflake.nixを宣言できます。

これらのファイルをgitリポジトリで公開した後、flake.nixでそのリポジトリを参照できます。

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
    stapelbergnix.url = "github:stapelberg/nix";
  };
  outputs = { self, nixpkgs, stapelbergnix }: let
    system = "x86_64-linux";
    pkgs = import nixpkgs { inherit system; config.allowUnfree = false; };
  in {
    nixosConfigurations.storage2 = nixpkgs.lib.nixosSystem {
      inherit system; inherit pkgs;
      modules = [
        ./configuration.nix
        stapelbergnix.lib.userSettings
        stapelbergnix.lib.systemdBoot
      ];
    };
    formatter.${system} = pkgs.nixfmt-tree;
  };
}

user-settings.nixで宣言されたものは、すべてconfiguration.nixから削除できるようになりました!

N9. immichを試す!

CoreOS/Flatcarから切り替える動機の一つはImmichを試せなかったことだったので、NixOSで試してみましょう。

services.immich = {
  enable = true;
  host = "10.0.0.252";
  port = 2283;
  openFirewall = true;
  mediaLocation = "/srv/immich";
};
systemd.services."immich-server" = {
  unitConfig.RequiresMountsFor = [ "/srv" ];
  wantedBy = [ "srv.mount" ];
};

結論

完全な設定ディレクトリはGitHubで見つけることができます

このNixOSセットアップにはかなり満足しています! 以前(CoreOS/Flatcar)はベースシステムは宣言的に管理できましたが、さらに大量のDockerコンテナを管理しなければなりませんでした。NixOSでは、すべて(あるいは理にかなう範囲のすべて)を宣言的に管理できます。

SSH+rsyncベースのバックアップ基盤のようなカスタム設定も、きれいに、一箇所で、望む抽象度・再利用度のレベルで構造化して表現できます。

少なくとももう1台のシステムをNixOSで管理することを考えているなら、おすすめします! 今後のプロジェクトの一つとして、もう一つのNASであるstorage3もUbuntu ServerからNixOSに変換して、手作業での管理を減らす予定です。設定全体をコピーするだけで別のシステムをセットアップできたり、使い捨てのVMでアイデアを試せるのは、本当に素晴らしいワークフローです 🥰

……ただし、管理するシステムが1台だけなら、おそらくこれらすべては複雑すぎるでしょう。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント