My First Impressions of Nix

Michael Lynch

Nixを初めて使ってみた感想

Nixは、ソースファイルをもとにソフトウェア環境を構成するためのツールです。最近はHacker NewsやTwitterでNixの名前を見かけることがどんどん増えてきました。その発想に魅力を感じ、ここ数週間あれこれ試しています。

Infrastructure as Codeとの歩み

10年前、私はSaltというツールに出会いました。コンピューターの構成をソースコードで定義できるものです。自分のPCやVMに何がインストールされているかをGitリポジトリで管理できるという発想が気に入りました。マシンを初期化しても、構成ツールを再実行すれば同じ状態に戻せるのです。

Ansibleという、同じ発想をよりうまく実現していると感じられるツールを見つけるまで、数年間Saltをいじっていました。

私は開発のすべてを自宅サーバー上のVMで行っています。プロジェクトごとにVMを分け、すべてAnsibleで管理しています。

Ansibleの問題点

Ansibleの最大の問題は、とにかく遅いことです。VM1台に対して実行するのに、たいてい10〜15分かかります。

たとえば、新しいaptパッケージfooを入れたいとします。sudo apt install --yes fooを実行して5秒で手に入れるでしょうか。それともAnsibleのロールを開き、fooをインストールするステップを追加して、プレイブックを実行し、15分待つでしょうか。言うまでもなく、前者を選ぶことが多くなり、結果として環境は本来それを表現しているはずのAnsibleファイルから乖離していきます。

もう一つの問題は、Ansibleの変更に後方互換性がないことです。新しいAnsibleの機能を使うためにプレイブックを更新すると、すべてのプレイブックを互換性があるように更新しなければなりません。でも、すべてのプレイブックを書き直してテストし直す気にはなれず、古いバージョンに縛られてしまいます。私は今でも3年前にリリースされたAnsible 2.9を使い続けています。

Nixに惹かれた理由

NixやNixOSについて語る人をますます見かけるようになりました。私が面白いと感じている開発者の多くが、Nixを試した経験を語っています。

最も印象に残ったのは、VagrantやPacker、Consul、Terraformといった広く使われているオープンソースのインフラツールを数多く生み出してきたHashicorpの共同創業者、Mitchell Hashimoto氏の推薦でした。彼はNixを「ここ数年で学んだ技術の中で、最も良い影響を与えてくれたナンバーワンの技術」と呼んでいます。

私のこれまでのNixの歩み。Nixはここ数年で学んだ技術の中で、最も良い影響を与えてくれたものだと今でも思っています。

Nixの発想はAnsibleとよく似ていると感じます。Nixでも構成をコードで定義し、それをもとにシステムをその状態に持っていきます。

NixとAnsibleの違い

NixがAnsibleと異なる重要な点がいくつかあり、そこに魅力を感じています。

NixはAnsibleより高速です

Ansibleには、構成対象のシステムの「状態」という概念がありません。あるVMでAnsibleの実行に15分かかったとして、1分後に同じプレイブックを再実行しても、やはり10分ほどかかります。前回からパッケージの更新がなければ少しは時間が短縮されますが、やっていることはほぼ同じです。

Ansibleが「さっき構成したばかりだから、やることはない」と判断することはありません。前回実行してから何が起きたか分からないため、毎回すべての構成をやり直さなければなりません。

一方、Nixには状態という概念があります。200行のNix設定ファイルに1行だけ変更を加えた場合、残りの199行に関わる作業をすべてやり直す必要はありません。システムの状態を設定ファイルと突き合わせて、1行分の変更だけを適用すればよいと判断できます。そして、その変更はたいてい数秒で終わります。

追記(2023-06-19):より経験豊富な読者の方から、Nixは私が想定していたような意味では状態を持たないとご指摘いただきました。

Nixが速いのはステートフルだからではありません。関数型的で再現可能だからこそ、正確性を損なうことなくキャッシュを利用できるのです。

私は、Nixがどのタスクを実行してどのシステム状態に至ったかを記録しているのだと考えていました。たとえば、タスクXとYを実行すると状態Aになり、タスクX、Y、Zを実行すると状態Bになるとします。状態AにあるNixシステムは、状態Bに行くためにタスクZだけを実行すればよいと思っていたのです。

今の理解では、状態AにあるNixシステムに状態Bになるよう指示すると、NixはタスクX、Y、Zを実行します。ただし、タスクXとYの結果はキャッシュされているため、ほぼ一瞬で完了します。

Nixはローカル環境の構成に最適化されています

Ansibleはネットワーク越しにシステムを構成するように設計されています。localhostを対象に指定することもできますが、それはAnsibleが最適化している使い方ではありません。

Nixは、今いる環境そのものを構成するように設計されています。Nixでは環境にあるべきものを定義し、Nixがその場で環境を作り上げます。NixOSではOS全体を定義し、NixOSがOSをその状態に持っていきます。ファイルシステムやLinuxカーネル、ブートローダーといった低レベルな部分まで変更できます。

これにより、Ansibleで抱えていた「一つのプレイブックを新しいバージョンのAnsibleに対応させるために、システム上のすべてのプレイブックを更新しなければならない」という問題が解消されます。異なるバージョンのNixで動くNixシステムを複数共存させても問題ありません。Ansible 2.9を必要とするファイル、2.10を必要とするファイル、2.14を必要とするファイルが混在していると、そのすべてをやりくりするのは大きな手間です。

Nixの変更はアトミックです

Ansibleでは、構成の途中で失敗すると、システムが中途半端な状態で残ってしまいがちです。

Nixでは変更はアトミックです。Nixはシステムを望みの状態にするか、できなければ変更前の状態にロールバックします。

役に立ったNix関連のリソース

Nixについてよく見かける不満の一つに、ドキュメントが不足している、あるいは間違っている、質が低いというものがあります。私の感想としては、ドキュメントが経験豊富なNixユーザー向けに書かれているように感じます。

見つけたNixのドキュメントの多くが、「次の数行を追加するだけです!」といった調子です。

は?

どのファイルの、どこに追加するのでしょうか?

これまでに見つけた中で最も役に立ったリソースは次のとおりです。

  • Zero to Nix:私が見つけた中で最も優れた入門チュートリアル集です。Determinate Systemsが作成しており、初心者向けのブログ記事も公開しています。
  • NixOS for the Impatient:以前にも何度かNixOSのインストールに挑戦しましたが、この記事で「思ったより簡単だ」とようやく納得でき、最後までやり切る後押しになりました。
  • Julia Evans氏による「Some notes on using nix」:Julia氏もNix初心者なので、彼女が遭遇した壁や、エコシステムについてまだ不慣れな中でどう乗り越えたかを知ることができ、参考になりました。

失敗その1:VM上でのNixOS

ProxmoxのVM上で「NixOS for the Impatient」チュートリアルに従ったところ、最初はすべてうまくいきました。チュートリアルでホスト名を変更する箇所まで進み、反映させるために再起動したのですが、VMが奇妙な状態に陥りました。起動はするもののログインできないのです。ログイン画面でパスワードを入力すると、画面がそのままフリーズしてしまいました。

ProxmoxのVMサーバーにNixOSは正常にインストールできましたが、2回目の起動でログイン後にハングしました。

失敗その2:Raspberry Pi 4上でのNixOS

VMがダメだったので、次はベアメタルが論理的な選択だと考えました。手元に予備のRaspberry Pi 4があったので、実験にちょうどよいと思いました。

Raspberry Pi 4にNixOSをインストールするための、公式に見えるチュートリアルを2つ見つけました。

どちらのチュートリアルも、すでにNix環境で実行していることが前提でした。microSDを準備しようとしている私のメインPCはWindows 10なので、Nixは入っていません。

NixOSのダウンロードページには64ビットARM用イメージがあります。Raspberry Pi 4は64ビットARMに対応しているので、これを試してみることにしました。

microSDの書き込みには普段使っているBalena Etcherを使いました。最初の危険信号は、Etcherが「ちょっと待って、それは起動可能なイメージですらないよ」と警告してきたことでした。

パーティションテーブルが見つかりません。これは起動可能なイメージではないようです。イメージにパーティションテーブルが含まれておらず、お使いのデバイスでは認識または起動できない可能性があります。

それでも続行しました!しかしmicroSDから起動してみると、Piも同じく起動可能なイメージではないと判断し、そのまま止まってしまいました。

「進行状況: ブートモード USB-MSDを試行中」と表示されたPiの起動画面

同じイメージを公式のRaspberry Pi Imagerでも書き込んでみましたが、結果は同じでした。

追記:最終的に解決できました

成功:DellのミニPCへのNixOSインストール

仕事用のテストは主にDell Optiplex 7040で行っています。初期化してもよいベアメタルマシンがそれしか手元になかったので、そこで試してみました。

すべてが「NixOS for the Impatient」のとおりにうまくいきました。インストールは開始から完了まで約10分かかりました。そのうち7分はファイルのコピーでした。今回はテスト用のデバイスなので暗号化はスキップし、再起動のたびにパスワードを入力する手間を省きました。

Dell Optiplex 7040へのNixOSのインストール(ファイルのコピー部分は早送りしています)

結果として、完全に動作するNixOSを手に入れることができました!

失敗その3:再びRaspberry Pi 4でのNixOS

動作するNixOSマシンが手に入ったので、もう一度Raspberry Piに挑戦してみたくなりました。前回の障害はmicroSDイメージを準備するためのNix環境がなかったことでしたが、今はそれがあります。

nix.devのチュートリアルに従ったところ、前回よりは先に進めましたが、やはり起動できませんでした。Piはカラフルな画面が表示された段階で止まってしまいます。

NixOSシステムからNixOS Pi aarch64用microSDイメージを書き込んでPiを起動したところ、カラフルな画面で止まってしまいました。

追記:最終的に解決できました

タスク1:SSHアクセスの確保

さて、Dell Optiplex上の動作するNixOSの話に戻ります。

NixOSシステムにメインのマシンからSSHで接続する必要がありました。そのためにはSSH鍵をシステムに登録しなければなりません。

NixOSでコンソールアプリを開き、sudo nano /etc/nixos/configuration.nixと入力しました。次に、environment.systemPackagesの部分に次の行を追加しました。

  environment.systemPackages = with pkgs; [
    vim
    curl
  ];

変更を反映させるために、次のコマンドを実行しました。

sudo nixos-rebuild switch

これでvimcurlが使えるようになったので、GitHubからSSHの公開鍵を取得しました。

sudo mkdir -p /etc/nixos/ssh

GITHUB_USERNAME='mtlynch'
curl "https://github.com/${GITHUB_USERNAME}.keys" | \
  sudo tee --append /etc/nixos/ssh/authorized_keys

次に、sudo vim /etc/nixos/configuration.nixを実行し、次の行を追加しました。

  # Enable the OpenSSH daemon.
  services.openssh.enable = true;
  users.users.mike.openssh.authorizedKeys.keyFiles = [
    /etc/nixos/ssh/authorized_keys
  ];

最後に再ビルドして再起動しました。再起動は必須ではなかったかもしれません。

sudo nixos-rebuild switch && sudo reboot

そして成功です!その後はメインのPCからNixOSシステムにSSHで接続できるようになりました。

タスク2:Gnomeの不要なアプリを削除する

OSで最初に気になったのは、不要なものが多いことでした。連絡先や天気アプリなど、使わないプリインストールアプリがたくさん入っていました。

削除方法を調べたところ、これらはGnomeシェルのデフォルトアプリケーションであることがわかりました。/etc/nixos/configuration.nixに次の1行を追加すれば無効にできます。

services.gnome.core-utilities.enable = false;

あるいは、次のように一つずつ除外することもできます。

 environment.gnome.excludePackages = with pkgs.gnome; [
    baobab      # disk usage analyzer
    cheese      # photo booth
    eog         # image viewer
    epiphany    # web browser
    gedit       # text editor
    simple-scan # document scanner
    totem       # video player
    yelp        # help viewer
    evince      # document viewer
    file-roller # archive manager
    geary       # email client
    seahorse    # password manager

    gnome-calculator
    gnome-calendar
    gnome-characters
    gnome-clocks
    gnome-contacts
    gnome-font-viewer
    gnome-logs
    gnome-maps
    gnome-music
    gnome-screenshot
    gnome-system-monitor
    gnome-weather
    gnome-disk-utility
    pkgs.gnome-connections
  ];

私は「すべて無効にする」方を選び、再ビルドしました。

sudo nixos-rebuild switch

すると、見事に不要なアプリがすべて消えました。

タスク3:システムモニターを戻す(失敗)

残しておいてもよいと思ったGnomeツールはシステムモニターだけでした。environment.systemPackagesのリストに追加して再ビルドしてみましたが、失敗しました。

$ sudo nixos-rebuild switch
building Nix...
building the system configuration...
error: undefined variable 'gnome-system-monitor'

       at /etc/nixos/configuration.nix:130:5:

          129|     curl
          130|     gnome-system-monitor
             |     ^
          131|   ];
(use '--show-trace' to show detailed location information)

gnome-shell-system-monitorなど、他の名前も試してみましたが、インストールする方法がわかりませんでした。

追記(2023-06-19):正しいパッケージ名はgnome.gnome-system-monitorだと読者の方に教えていただきました。私に足りなかったのは、search.nixos.orgでパッケージを検索できるという知識でした。

次に理解したいこと

NixとNixOSを使い始めて数日、今のところ満足しています。聞いていたとおりの印象です。うまく使えば非常に強力になり得ますが、初期投資が大きく、情報をかき集める必要があります。

まだ表面をなぞっただけなので、次に学びたいことを挙げておきます。

NixOSでVS Code Remote SSHを使う

私は開発のすべてをVS CodeのRemote SSHで行っています。VS CodeからNixOSシステムにリモート接続しようとすると、インストールに失敗します。VS Codeはターゲットシステムに何らかのサーバーをインストールする必要があるのですが、おそらくNixOSへのインストール方法を認識できていないのでしょう。

nixos-vscode-serverというGitリポジトリがあり、おそらくこれが解決策になると思います。まだ試してはいません。

Nixの主要な概念はどうつながっているのか

「flakes」や「derivations」といった言葉を目にしますが、今のところ意味がわかりません。Nixの言語構文もまだ理解できていませんが、JavaScriptやPythonに似ているので、なんとなく推測しながら進めている段階です。ただ、Nixを効果的に使うには、やはり言語を学ぶ必要があります。

再現性はいつ担保されるのか

Nixについて語られる際に、最もよく挙げられる特徴の一つが再現性(deterministic)であることです。

今のところ、どう再現性が保たれているのかがわかりません。パッケージを指定する際、バージョン番号はおろか整合性チェック用のハッシュすら指定していません。同じNix設定を1年後に実行したら、指定したvimcurlパッケージのバージョンが変わっているため、異なるシステムができあがるのではないでしょうか。

もっと厳密にパッケージのバージョンを指定する方法があるのだと思いますが、まだ学べていません。

誰を信頼しているのか

パッケージを指定する際、私は次のような素朴なパッケージ名のリストを書いていました。

  environment.systemPackages = with pkgs; [
    vim
    curl
  ];

このようにパッケージ名だけで指定できるということは、Nixがデフォルトのリポジトリからパッケージを取得しているはずです。リポジトリは複数あるのでしょうか。どのリポジトリを使うかはどうやって選ぶのでしょうか。

原文は Michael Lynch により に公開されました。

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