3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NixOSで最小構成からこつこつ環境構築する 2. Dendritic Patternで叩き台をつくる

3
Last updated at Posted at 2026-01-04

設定ファイルの構成を考える

設定項目は徐々に増えていくのが確実ですので、最初からモジュールに分割して管理していきます。しかしモジュールを追加、分割するたびにインポートする側のコードが長くなる、数が増える、ディレクトリが深くなる、などしてきますので見通しが悪くなり管理大変です。
GitHubでStarのたくさんついた設定をまねて何度か書いてはリファクタリングして破棄してを繰り返していましたが、Dendritic Patternというパターンですすめていこうと思います。nixpkgsのコミッターでもあるPol Dellaiera氏のブログポストRefactoring My Infrastructure As Code Configurationsを一読されることをおすすめします1

Dendritic Patternにリファクタリングする

ということで、Dendritic Pattern に沿って前回の設定をリファクタリングしていきます。

  • flake化
  • モジュールへの分割
  • ディレクトリ構成を自由化するimport-treeの導入
  • flake-partsによるモジュールのクラス分け
  • ホスト別の設定への骨組み設計

の順にすすめます。

ただflake化してみる

~/infra/以下に作成していきます。
前回あつかった2つのモジュールが必要ですが、ロックできない絶対パスへのアクセスはimpureとなり許されないのでコピーします。

モジュールをコピーする
[~]$ mkdir infra
[~]$ cd infra
[~/infra]$ cp /etc/nixos/* ./

最初のflake.nixを作成します。
ついでにnixpkgはunstableを使うように変更しています。

infra/flake.nix ただflake化しただけ
{
  description = "My NixOS configuration`";

  inputs = {
    nixpkgs.url = "github:/nixos/nixpkgs/nixpkgs-unstable";
  };

  outputs =
    { nixpkgs, ...}:
    {
      nixosConfigurations.x1 = nixpkgs.lib.nixosSystem {
        modules = [ ./configuration.nix ];
      };
    };
}
ビルド&スィッチ
[~/infra]$ sudo nixos-rebuild switch --flake .#x1

はい、できました。

configuration.nixを複数のモジュールに分割する

では今の設定をモジュールに分割していきます。

今のconfiguration.nixはコメントを外して整頓するとこれだけです。
ホスト名の設定とnetworkmanagerグループへの参加だけ追加しました。

configuration.nix
{ config, lib, pkgs, ... }:
{
  imports = [ ./hardware-configuration.nix ];

  nix.settings.experimental-features = [ "nix-command" "flakes" ];

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  networking.hostName = "x1"; # 追加
  networking.networkmanager.enable = true;

  time.timeZone = "Asia/Tokyo";
  
  users.users.naogami = {
    isNormalUser = true;
    extraGroups = [ "wheel" "networkmanager" ]; # "networkmanager"追加
    packages = with pkgs; [
    ];
  };
  
  services.openssh.enable = true;
  
  system.stateVersion = "25.11";
}

これを機能別に分けてみます。
modules/以下にあとあとわかりやすよう適宜ディレクトリに分類して配置します。
ひとまず、

  • base/ ホストによらず共有できそうなもの
  • users/ ユーザー毎の設定
  • openssh/ openssh関連

としました。
hardware-configuration.nixモジュールもmodules/以下に移動しておきます。

modules/base/system/nix.nix
{
  nix = {
    settings = {
      experimental-features = [ "nix-command" "flakes" ];
    };
  };
}
modules/base/boot/loader.nix
{
  boot.loader = {
    systemd-boot.enable = true;
    efi.canTouchEfiVariables = true;
  };
}
modules/base/networking/networking.nix
{
  networking = {
    hostName = "x1";
    networkmanager.enable = true;
  };
}
modules/base/i18n/timezone.nix
{
  time.timeZone = "Asia/Tokyo";
}
modules/users/naogami.nix
{ pkgs, ... }:
{
  users.users.naogami = {
    isNormalUser = true;
    extraGroups = [ "wheel" "networkmanager" ];
    packages = with pkgs; [
    ];
  };
}
module/openssh/opennssh.nix
{
  services.openssh.enable = true;
}
modules/base/system/state-version.nix
{
  system.stateVersion = "25.11";
}

みてわかるとおりファイルを分けただけです。
これらを使用するようにflake.nixを書き換えます。

infra/flake.nixn 設定をモジュールに分割
{
  description = "My NixOS configuration`";

  inputs = {
    nixpkgs.url = "github:/nixos/nixpkgs/nixpkgs-unstable";
  };

  outputs =
    { nixpkgs, ...}:
    {
      nixosConfigurations.x1 = nixpkgs.lib.nixosSystem {
        modules = [
          ./modules/hardware-configuration.nix
          
          ./modules/base/system/nix.nix
          ./modules/base/boot/loader.nix
          ./modules/base/networking/networking.nix
          ./modules/base/i18n/timezone.nix
          ./modules/users/naogami.nix
          ./modules/openssh/openssh.nix
          ./modules/base/system/state-version.nix
        ];
      };
    };
}

ビルドが通ることを確認したらいらなくなったconfiguration.nixを削除します。

ビルド確認後にconfiguration.nixを削除
[~/infra]$ sudo nixos-rebuild switch --flake .#x1
[~/infra]$ rm configuration.nix

import-treeでモジュールを一括インポートする

すぐに気づくことですが、すでにモジュールパスの羅列がしんどいです。
各ディレクトリにdefault.nixを配置してそちらにインポートするコードを書けば多少ましではありますが、モジュールを足すたびに複数のファイルを修正しないといけませんし、ディレクトリ構成の変更をしようと思うと大仕事になります。

そこで、import-treeを使用します。これは指定したディレクトリ以下のnixファイルを再帰的にインポートするツールです。

例えば、

modules/
  a.nix
  subdir/
    b.nix

というディレクトリ構成であれば、以下の2つは等価となります。

imports = [ (import-tree ./modules) ];
imports = [
  {
    imports = [
      ./modules/a.nix
      ./modules/subdir/b.nix
    ];
  }
];

これを使うとflake.nixは以下のように書き直せます。

infra/flake.nixn import-treeでモジュールをインポート
{
  description = "My NixOS configuration`";

  inputs = {
    nixpkgs.url = "github:/nixos/nixpkgs/nixpkgs-unstable";

    import-tree.url = "github:vic/import-tree";
  };

  outputs =
    { nixpkgs, import-tree, ...}:
    {
      nixosConfigurations.x1 = nixpkgs.lib.nixosSystem {
        modules = [ (import-tree ./modules) ];
      };
    };
}

大変すっきりかけました。

ただ、今後home-managerやdarwinなどを導入した場合、それぞれのモジュールが混在するようになってきます。構成毎に似たようなモジュールを用意してメンテナンスしないといけないので大変です。またアーキテクチャの異なる複数のホストにも対応したいところです。

flake-partsを使うとこうした問題に対応しやすくなります。

flake-partsを使用する

とりあえず適用してみる

ひとまず先のflakes.nixflake-partsを使して公式ドキュメント に従って書き換えてみます。

infra/flake.nix flake-partsを適用
{
  description = "My NixOS configuration`";

  inputs = {
    nixpkgs.url = "github:/nixos/nixpkgs/nixpkgs-unstable";

    import-tree.url = "github:vic/import-tree";

    flake-parts = {
      url = "github:hercules-ci/flake-parts";
      inputs.nixpkgs-lib.follows = "nixpkgs";
    };
  };

  outputs =
    { flake-parts, import-tree, nixpkgs, ... }@inputs:
    flake-parts.lib.mkFlake { inherit inputs; } {
      flake = {
        nixosConfigurations.x1 = nixpkgs.lib.nixosSystem {
          modules = [ (import-tree ./modules) ];
        };
      };
        
      systems = [ "x86_64-linux" ];
    };
}

systemsアトリビュートにはperSystemアトリビュートをビルドする対象のアーキテクチャを記載します。systemsごとにperSystemがビルドされますが、perSystem内ではシステムはconfigを介してアクセスできます。すなわち、

In the top level
(getSystem "<some architecture>").packages.hello

perSystem内だと

In perSystem
config.packages.hello

と簡便にかけるようになります。注意が必要なのはここでのconfigperSystemに渡されるアトリビュートセットのconfigで、トップレベルのconfigとは別物でかつそれをシャドウイングしてしまうという点です2

ここにはビルドすべきperSystemはないのですが定義しておかないとエラーとなりますので設定しておきます。

このように単一の設定を行うなら導入のメリットは特にないと思います。課題であった複数の対象向けのモジュールの混在ができるよう変更していきます。import-treeはディレクトリ構造に従ってmodules以下すべてのモジュールをインポートしてしまいますので、モジュールのクラス分けを行うためにflake-partsの提供する機能を使います。

flake-parts.flakeModules.modulesによるクラス分け

flake-parts.flakeModules.modulesモジュールの機能

flake-parts.flakeModules.modulesモジュールをインポートします3

これもモジュールにしてしまいしょう。

modules/以下にディレクトリ

  • fundamentals/ システム基幹部分に関連するもの

を追加してflake-parts.nixを配置します。

modules/fundamentals/flake-parts.nix
{ inputs, ... }:
{
  imports = [ inputs.flake-parts.flakeModules.modules ];
}

flake-parts.flakeModules.modulesはオプションflake.modulesを宣言します。このオプション以下にアトリビュートとしてモジュールを定義すると、そのアトリビュートがモジュールのclassとなり、config.flake.modules.<クラス名前>でトップレベルからアクセスできるようになります。モジュールは自由に配置可能となり、単一ファイルにNixOSとhome-managerのように異なるクラスの設定を混在させて書くことも可能となります。

ホスト別の設定

まず、複数のホストをカバーできるようにしていきます。ホスト固有の設定モジュールはmodules/hosts/以下に配置します。
hardware-configuration.nixもホスト固有ですので便宜上ここに移動しますが、クラス分けのために直接インポートせずにラッパーを介します。読み込まれないようアンダーバーをつけた名前に変更します。

[~/infra/modules]$ mkdir -p hosts/x1
[~/infra/modules]$ mv hardware-configuration.nix hosts/x1/_hwconf.nix

そしてnixosクラス以下に"hosts/x1"として配置します。

infra/modules/hosts/x1/hardware.nix
{
  flake.modules.nixos."hosts/x1" = { imports = [ ./_hwconf.nix ]; };
}

これでモジュールhardware-configuration.nix(と同値のモジュール)はトップレベルからconfig.flake.modules.nixos."hosts/x1"を介してアクセスすることができるようになります。
もうひとつ、このホストに必要とされる機能のモジュールを記載します。

infra/modules/hosts/x1/modules.nix
{
  config,
  ...
}:
{
  flake.modules.nixos."hosts/x1" = {
    imports =
      # Import the nixos modules for the host `x1`.
      with config.flake.modules.nixos;
      [
        base
        openssh

        # Users
        root
        naogami
      ];
  };
}

クラス分けに対応したモジュールへの書き換え

先の設定で使ったモジュールを変更していきます。

config.flake.modules.nixos.base以下にはほとんどが共有しそうなモジュールを、config.flake.modules.nixos.openssh以下にはsshのモジュールをおきます。
このホストのユーザーの情報はconfig.flake.modules.nixos.<ユーザー名>とします。
ファイルはmodules/以下どこにおいてもよいのですが、先のディレクトリ構成をそのままつかいます。

nixos.base.boot
loader.nixを変更

他のホストでデュアルブートもできるよう、ブートローダーをgrubに変更します。

modules/base/boot/loader.nix
{
  flake.modules.nixos.base = {
    boot.loader = {
      efi.canTouchEfiVariables = true;
      grub = {
        enable = true;
        devices = [ "nodev" ];
        efiSupport = true;
        useOSProber = true;
      };
    };
  };
}
nixos.base.i18n
locale.nixを追加
modules/base/i18n/locale.nix
{
  flake.modules.nixos.base = {
    i18n.defaultLocale = "en_US.UTF-8";
    i18n.extraLocaleSettings = {
      LC_ADDRESS = "ja_JP.UTF-8";
      LC_IDENTIFICATION = "ja_JP.UTF-8";
      LC_MEASUREMENT = "ja_JP.UTF-8";
      LC_MONETARY = "ja_JP.UTF-8";
      LC_NAME = "ja_JP.UTF-8";
      LC_NUMERIC = "ja_JP.UTF-8";
      LC_PAPER = "ja_JP.UTF-8";
      LC_TELEPHONE = "ja_JP.UTF-8";
    };
  };
}
timezone.nixを変更
modules/base/i18n/timezone.nix
{
  flake.modules.nixos.base = {
    time.timeZone = "Asia/Tokyo";
  };
}
nixos.base.networking
networking.nixを変更

ホスト名を反映させるように変更します。
hostConfigは後出です。

modules/base/networking/networking.nix
{
  flake.modules.nixos.base =
    { hostConfig, ... }:
    {
      networking = {
        hostName = hostConfig.name;
        networkmanager.enable = true;
        firewall.enable = true;
      };

      systemd = {
        services.NetworkManager-wait-online.enable = false;
        network.wait-online.enable = false;
      };
    };
}
nixos.base.system
nix.nixを変更

ブログ記事を参考に設定を追加します。

modules/base/system/nix.nix
{
  flake.modules.nixos.base = {
    nix = {
      extraOptions = ''
        connect-timeout = 5
        log-lines = 50
        min-free = 128000000
        max-free = 1000000000
        fallback = true
      '';
      optimise.automatic = true;
      settings = {
        trusted-users = [
          "root"
        ];
        auto-optimise-store = true;
        experimental-features = [
          "nix-command"
          "flakes"
        ];
        warn-dirty = false;
        tarball-ttl = 60 * 60 * 24;
      };
    };
  };
}
state-versions.nixを変更
modules/base/system/state-versions.nix
{
  flake.modules.nixos.base = {
    system = {
      stateVersion = "25.11";
    };
  };
}

nixos.openssh

openssh.nixを変更

標準的な設定を少し追加します。

modules/openssh/openssh.nix
{
  flake.modules.nixos.openssh = {
    services.openssh = {
      enable = true;
      openFirewall = true;
      settings = {
        PermitRootLogin = "no";
        PasswordAuthentication = true; # あとで無効化の予定
        KbdInteractiveAuthentication = false;
        X11Forwarding = false;
      };
    };
  };
}

nixos.users

root.nixを追加
modules/users/root.nix
{
  flake.modules.nixos.root =
    { pkgs, ... }:
    {
      users.users.root = {
        shell = pkgs.fish;
        initialPassword = "id";
      };
    };
}
naogami.nixを変更

メタ情報を定義して利用していますが後出です。
シェルをfishにしています。

modules/users/naogami.nix
{
  config,
  ...
}:
{
  flake = {
    meta.users = {
      naogami = {
        email = "xxx@yyy";
        name = "NaoGami";
        username = "naogami";
      };
    };

    modules.nixos.naogami =
      { pkgs, ... }:
      {
        programs.fish.enable = true;

        users.users.naogami = {
          description = config.flake.meta.users.naogami.name;
          isNormalUser = true;
          createHome = true;
          extraGroups = [
            "networkmanager"
            "wheel"
          ];
          shell = pkgs.fish;
          initialPassword = "id";
        };

        nix.settings.trusted-users = [ config.flake.meta.users.naogami.username ];
      };
  };
}

fundamentals

今回の設定の骨格の中心となるものを追加します。

メタ情報を管理する

ユーザー情報などこのflake全体で参照できるメタ情報を定義できるようにオプションflake.metaを宣言します。

meta.nixを追加
modules/fundamentals/meta.nix
{ lib, ... }:
{
  options.flake.meta = lib.mkOption {
    type = with lib.types; lazyAttrsOf anything;
  };
}
nixpkgsを管理する
unfree.nixを追加

nixpkgs.config.allowUnfreePredicateにpredicatorを定義することでunfree packageを許可するか否かパッケージ毎に判定できます。ここでは、ホワイトリストとしてnixpkgs.allowedUnfreePackagesをオプションに宣言しておいて各モジュールから設定しやすくしておきます。

nix/modules/fundamentals/unfree.nix
{ lib, config, ... }:
{
  options.nixpkgs.allowedUnfreePackages = lib.mkOption {
    type = lib.types.listOf lib.types.str;
    default = [ ];
  };

  config.flake = {
    modules =
      let
        predicate = pkg: builtins.elem (lib.getName pkg) config.nixpkgs.allowedUnfreePackages;
      in
      {
        nixos.base.nixpkgs.config.allowUnfreePredicate = predicate;
      };

    meta.nixpkgs.allowedUnfreePackages = config.nixpkgs.allowedUnfreePackages;
  };
}
nixpkgs.nixを追加

nixpkgs.allowedUnfreePackagesをパッケージに反映します。
flake-partsのマニュアル推奨に従ってパッケージ定義はperSystemに記載します。systemアトリビュートにはビルドの対象となるアーキテクチャーが定義されてきます。

将来的にはここでoverlayを導入することができます。

modules/fundamentals/nixpkgs.nix
{
  config,
  lib,
  nixpkgs,
  ...
}:
{
  perSystem =
    { system, ... }:
    let
      predicate = pkg: builtins.elem (lib.getName pkg) config.nixpkgs.allowedUnfreePackages;
    in
    {
      _module.args.pkgs = import nixpkgs {
        inherit system;
        
        config.allowUnfreePredicate = predicate;
        
        overlays = [ ];
      };
    };
}
ホストに応じたflake出力を作成する
host-machines.nixの作成

ホスト毎のflake出力を作成します。
nixosクラスに属し"hosts/"をプレフィックスに持つモジュールを抽出し、ホスト毎にモジュールをマージしてnixosConfigurationsに渡します。

Pol Dellaiera氏によるコードで、ここがキモです

modules/fundamentals/host-machines.nix
{
  inputs,
  lib,
  config,
  ...
}:
let
  prefix = "hosts/";
in
{
  flake.nixosConfigurations = lib.pipe config.flake.modules.nixos [
    (lib.filterAttrs (name: _: lib.hasPrefix prefix name))
    (lib.mapAttrs' (
      name: module:
      let
        specialArgs = {
          inherit inputs;
          hostConfig = {
            name = lib.removePrefix prefix name;
          };
        };
      in
      {
        name = lib.removePrefix prefix name;
        value = inputs.nixpkgs.lib.nixosSystem {
          inherit specialArgs;
          modules = [ module ];
        };
      }
    ))
  ];
}

クラス分けに対応したflake.nixへの修正

flake.nixosConfigurations.<ホスト名>host-machines.nixで定義されるようになりました。それにあわせてoutputsを変更します。

infra/flake.nix
{
  description = "My NixOS configuration`";

  inputs = {
    nixpkgs.url = "github:/nixos/nixpkgs/nixpkgs-unstable";

    import-tree.url = "github:vic/import-tree";

    flake-parts = {
      url = "github:hercules-ci/flake-parts";
      inputs.nixpkgs-lib.follows = "nixpkgs";
    };
  };

  outputs =
    { flake-parts, import-tree, ... }@inputs:
    flake-parts.lib.mkFlake { inherit inputs; } {
      imports = [ (import-tree ./modules) ];
      
      systems = [ "x86_64-linux" ];
    };
}

ビルドが通ることを確認して叩き台作成は終了となります。

  1. 氏のレポジトリからは沢山知見をいただきました。ありがとうございました。

  2. 難解なエラーメッセージと相まって非常にわかりにくいエラーの原因となって困ったことがあります。ドキュメントは隅々まで読まないといけないのですが、辛いですね。シャドウイングを避けた命名をしてもらいたいと思うのはわたしだけでしょうか。

  3. ”モジュール”が3つも続いてこれまた混乱しますがflake-partsが提供しているオプショナルなモジュールのひとつです ... そういえば「オプション」も一般的なoptionと用語としてのoptionとの混在も困りますね...)。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?