4 ポイント 投稿者 GN⁺ 2024-03-17 | 1件のコメント | WhatsAppで共有
  • Dockerイメージをすでにデプロイ単位として使っている環境なら、Nixはワークフローを大きく変えずに決定論的ビルドと依存関係の固定を試せる入口になる
  • 一般的な docker build はUbuntuリポジトリや外部ダウンロードのような公開インターネットの状態に依存しがちで、時間が経ったあとに同じイメージを再現するのが難しい
  • Nixは必要な依存関係をパッケージ単位で把握しているため、dockerTools.buildLayeredImage変更されたレイヤーだけを再アップロードする構成が可能
  • Goベースの douglas-adams-quotes の例は、pkgs.buildGoModule でバイナリを作り、nix build.#dockerdocker load <./result で通常のDockerイメージのようにロードしてデプロイできることを示している
  • モノレポ内の複数サービスがレイヤーとNixキャッシュを共有すれば、過去コミットのイメージ再現や反復的なデプロイでビルド時間とコストを削減できる

DockerイメージビルドにNixを組み合わせる理由

  • Nixはパッケージマネージャー言語オペレーティングシステムという3つの性格をあわせ持つ
    • Nix言語でパッケージのビルド指示を書く
    • Nixパッケージマネージャーはその指示をもとに、ソフトウェア、ツール、NixOSイメージ、コンテナイメージなどを作れる
  • 既存のCI/CDパイプラインにNixを導入するのは簡単ではない
    • Nixは多くの開発者が慣れている方法とは異なる
    • 新しいスタートアップやホームラボのようにすべてを一から始められる環境でなければ、導入のハードルは高い
  • すでにDockerを使っている組織は、コンテナイメージのビルドからNixを適用することで、既存のデプロイフローを大きく壊さずに試せる

Dockerビルドが揺らぐポイント

  • Dockerとコンテナ化は広く採用されており、Dockerイメージはインターネット上の事実上の汎用パッケージ形式のように使われている
    • Fly.io、Railway、Renderのようなプラットフォームは、任意のVMイメージやtarball形式のLinuxプログラムではなくDockerイメージを使う
  • Dockerビルドは常に決定論的とは限らない
    • インターネットでよく見かけるDockerfileのほとんどは問題なくビルドされるが、失敗するわずかな割合が運用上の問題につながり得る
  • 最大の弱点の1つは、Dockerビルドが公開インターネットにアクセスする点である
    • Ubuntuリポジトリからパッケージをダウンロードするために必要になる
    • 後で同じイメージを再生成する必要があるとき、当時のUbuntuリポジトリの正確な状態を戻すのは難しい
    • Ubuntu 18.04はその年にサポート終了を控えており、何がそのバージョンに依存しているのか、障害が起きてから初めて分かる可能性がある
  • 単純な方法でDockerイメージにパッケージを追加すると、無駄な容量が生じることがある
    • ビルドの初期に apt-get upgrade を実行すると、コンテナイメージ内のファイルが置き換えられる可能性がある
    • 置き換え前のファイルがレイヤーに残り、シャドウコピーのように蓄積されることがある

NixがDockerレイヤーを扱う方法

  • Nixは必要な依存関係を事前に把握し、それらをできるだけ少ないDockerレイヤーに分けられる
    • コードを1行変えたからといって、aptnpm が依存関係を再インストールする必要はない
    • 実際に変わった最小限の内容だけをイメージ更新に反映できる
  • dockerTools は、Nixパッケージとその依存関係をDockerイメージに入れるためのツール群である
  • Nixで作るDockerイメージは大きく2つの方式に分かれる
    • 非レイヤーイメージ: プログラム、依存関係、TLSルート証明書のような追加項目を1つのフォルダに入れ、単一レイヤーのイメージとして公開する
    • レイヤーイメージ: 各依存関係を別々のイメージレイヤーに配置し、実際に変更された部分だけをアップロードできる
  • Docker内部にもコンテンツベースのストレージはあるが、docker build だけではそれを十分に活用しにくい
    • Nixのレイヤーイメージはパッケージごとのレイヤーを使うため、glibc のような依存関係は一度だけアップロードすればよい
    • ただし、そのライブラリに修正が必要な場合、そのレイヤーは再アップロードされる必要がある

Goサービスの例: Douglas Adams Quotes

bin = pkgs.buildGoModule {
  pname = "douglas-adams-quotes";
  inherit version;
  src = ./.;
  vendorHash = null;
};
  • この定義はGoモジュールテンプレートを使い、Goコンパイラ、CGo用Cコンパイラ、外部依存関係のダウンロードを設定する
    • pname はパッケージ名である
    • version はサービスのGitコミットから自動生成される
    • src = ./.; は現在の作業ディレクトリのソースコードを使う
    • 標準ライブラリ以外の依存関係がなければ、vendorHash = null を置ける
    • 外部依存関係がある場合は、すべての依存関係のハッシュを指定するか、gomod2nix を使える
  • パッケージは次のコマンドでビルドする
nix build .#bin
  • Dockerレイヤーイメージは dockerTools.buildLayeredImage で作成できる
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • config.Cmd にビルド済みのサーバーバイナリを指定すると、必要な項目も一緒にコピーされる
    • glibc と実行に必要な項目も含まれる
  • CAルート証明書のような追加パッケージは contents に入れられる
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  contents = with pkgs; [ cacert ];
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • イメージは次のコマンドで作成し、Dockerデーモンにロードする
nix build .#docker
docker load < ./result
  • dive で開いてみると、各レイヤーがnixpkgsの別々のパッケージを追加し、最後に項目がイメージルートへシンボリックリンクされる

モノレポでレイヤーを共有する効果

  • 同じリポジトリに複数のサービスがある場合、Nixで作ったDockerイメージはレイヤーを共有できる
    • 追加設定なしで共有される
    • Dockerだけでこれを行おうとすると、共通ベースイメージを複数作ることになり、一部サービスが使わないツールや不要な内容が入る可能性がある
  • 例として Xe/x リポジトリは、10年分のサイドプロジェクト、実験、ツールが集まったモノレポである
    • 複数のプロジェクトがおよそ3つのプラットフォームにデプロイされている
    • 共通のデプロイ基盤へ収束させるため、Dockerイメージに入れる作業を進めてきた
  • あるサービスのアップデートをプッシュすると、他のサービスの大半と共有されるアップデートの一部も一緒にプッシュされる
    • この方式は複数のプロジェクトで時間とコストを節約する

Dockerキャッシュだけでは追いつきにくい部分

  • Dockerキャッシュを使えば、理論上はNixと同じくらい効率的にイメージをビルドできる場合もある
  • しかしDocker方式で同じ効果を出そうとすると、ビルド手順の保守が難しくなり得る
    • 共有ライブラリを別レイヤーにインストールしなければならない
    • ビルド中にネットワークスタックを再び有効にする必要があるため、再現性が弱くなる
    • 検索パス、コンパイラフラグ、CGo関連設定を再調整しなければならない
  • Nixの dockerTools.buildLayeredImage は、パッケージをコンテナに入れる細部を代わりに処理し、設定をシンプルにする

過去時点の再現とNixキャッシュ

  • Nixの大きな利点の1つは、過去時点のソフトウェアを正確に再ビルドできる点である
    • 後で特定の顧客環境のバグを再現する必要があるとき、同じDockerイメージを再び作れる
    • パッケージビルドは、ソフトウェアと依存関係全体の状態をある時点に固定する役割を果たす
  • XeDN は数年にわたって取り組んできたプロジェクトであり、14か月前のバージョンは次のコマンドでビルドできる
nix build github:Xe/x/567fdc2#xedn-docker
  • このコマンドは GitHub repo Xe/x の特定コミットから xedn-docker ターゲットをビルドする
    • Dockerデーモンにロードすると、当時と同じバイト列のイメージが作られる
    • 例にはGo 1.19も含まれる
  • 通常のDockerビルドだけで同じ再現を行うには、多くのストレージコストが必要になる可能性がある
  • NixキャッシュはNixコマンドの出力を保存し、後で再ビルドしなくて済むようにする
    • 開発者のノートPCごとに nokogiri のようなパッケージを新しくビルドしなくてもよい
    • すでにクラウドでビルドされた結果を受け取れる
  • GarnixはすべてのflakesプロジェクトのCIに使われ、コミットごとにビルド状態を報告する
    • ホームラボのマシン設定もGarnixでビルドし、毎晩アップデートするときに自前でビルドする代わりにGarnixキャッシュから最新設定を取得する

デプロイ成果物としての結論

  • NixはDockerイメージビルダーとして、Dockerのイメージビルダーより優れていると見なせる
  • Nixは結果を指定させ、その結果に到達するための細かな手順を直接列挙させない
  • すでにDockerを使っている環境では、DockerイメージビルドからNixを適用する方法が現実的な導入経路になる
  • Nixで作ったDockerイメージは、モノレポの複数部分の間でレイヤーを共有できる
  • バイナリキャッシュのおかげで、過去にすでにビルドされたコードを再ビルドしなくて済む
  • 最終的な成果物は、AWS、Google Cloud、Fly.ioのようなプラットフォームにデプロイ可能な通常のコンテナイメージである

1件のコメント

 
GN⁺ 2024-03-17
Hacker News のコメント
  • Nix を好きになろうと何度も試みましたが、もう諦めるべきだという気がしています
    Nix を動かしているシステムが2台ありますが、触るのが怖く、以前はどちらも壊れて最初から再インストールしたこともあります。理論上、Nix は冪等で決定的ですが、実際には何に対して決定的なのかを理解する必要があり、依存しているすべての部分の挙動を深く知らないと、奇妙な結果や不可解で役に立たないエラー、あるいはフィードバックがまったくない状況に遭遇します
    ドキュメントは完全で技術的に正しくても、ひどく難解で、チュートリアルは80%の道のりまでしか連れていってくれません。そこから先は自分で作る必要がありますが、それには何年分もの経験知と挫折が必要で、もう耐えたくありません。Docker の利点はむしろ混沌そのもので、シェルとディストリビューションのパッケージマネージャをざっくり知っているだけでほとんど何でも作れますし、問題が起きても何十年も使われてきたツールのおかげで診断と修正がずっと簡単です
    Nix は Emacs のように、忍耐力と深く先見的な知識さえあれば何でもできますが、完全にのめり込むか、距離を置いて眺めるしかないように見えます

    • 同じ道をたどりました。決定的ビルドは好きですが、Dockerfile の最大の問題は、平均的な開発者には決定的に見えることだと思っています
      同じマシンで続けて2回ビルドすれば同じ結果になりますが、時間が経ってパッケージマネージャとベースイメージタグが更新されると、1か月後の再ビルド結果はまったく別物になります。チームが管理するコンテナが40個ほどになると、コンテナの修正が業務の大きな部分になります
      そのため理論上は Nix が完璧なはずですが、あまりにも違いすぎてベンダー製ツールは Nix 上で動かず、エラーが出ても Web で素早く解決策を見つけるのが難しいです。そのもどかしさから https://www.stablebuild.com を作り、Ubuntu、Debian、Alpine コンテナ上で Docker ベースの決定的ビルドを提供しています。不変の Docker Hub プルスルーキャッシュ、Ubuntu/Debian/Alpine パッケージリポジトリの日次フルコピー、人気の PPA と PyPI インデックスの日次コピー、任意のファイル/URL の不変キャッシュで構成されています
      実務では使いやすくデバッグしやすく、ソフトウェア互換性も広いため、StableBuild に移行したコンテナでは非決定性によって起きた問題は0件でした
    • ドキュメントは難解というだけでなく、特に新しい CLI と flakes 周りは単に間違っている場合も多く、エッジケースでもありません
      nix develop がドキュメントどおりに動作しない理由と、そう動作させる方法を理解するのに時間を費やしたことがあります。ドキュメントでは基本的にビルド時の環境に入るとされていますが、実際のデフォルトは密閉されておらず、コマンドラインオプション名も紛らわしいため、ソースから知識を掘り出す必要がありました
      再現性を壊すエッジケースも、もっと明示的に扱ってほしいです。浮動小数点コードは演算順序に敏感で、OS のプリエンプションによって状態が漏れ出すこともあります。明白なことでも明示しなければ、人々は自分で痛い目を見ることになります
    • そこまでひどくはありませんが、標準的な NixOS 設定でもユーザーアカウントとシステム内部には再現不可能な状態が非常に多く残っています
      “Erase your darlings” 設定を使うと、ユーザーアカウント外の再現不可能な状態をほとんどなくせます。少し面倒ですが、NixOS で面倒でないものなどあるのか、という感じです
      https://grahamc.com/blog/erase-your-darlings/
      ユーザーアカウント内部は気にしておらず、Home Manager は好きではありません
    • 職場で Docker と NixOS の両方を使っていますが、上で述べられている問題は経験したことがありません
      Docker は悪くありませんが、Mac では性能がいまひとつです。一方 Nix は複数のマシンに同じ方法でインストールして動かせるのが些細な作業で済むので気に入っています。Nix のドキュメントはひどいですが、ChatGPT-4 は Nix の問題解決には非常に役立ちました
      Nix の問題の90%は「Nix 流」ではないことをしようとしたために起きていた印象です
    • Fedora Atomic のようなイミュータブルなディストリビューションを一度試してみる価値があります
      ほぼすべてのディストリビューションのパッケージマネージャを使ってきましたし、特別なことをしなくても何らかの形ですべて壊したことがあります。Fedora Kinoite はレイヤーの追加/削除、毎日のアップデート、Silverblue からのリベースまでしてもよく耐えています。個人的には rpm-ostree が Nix を置き換えることになりそうです
  • NixとNixOSは、GitHub以前のgitに近い状態だと思う
    基本的なアイデアは、従来方式であるSVNやDockerよりも、より本格的な計算機科学に基づいていて、内部の配管にはまだ問題があるものの、それ以上に悪いわけではない。ただ、ユーザー向けのツールやドキュメントは主流として採用される水準にはない
    floxの登場で、これは変わったかもしれない: https://flox.dev。ほとんどシームレスで、DE Shaw出身なら驚きでもない
    Nixはflakesとnix-commandなしではあまり意味がないが、どちらも実験的だとドキュメント化されており、デフォルトでもオフになっている。ドキュメントは改善されてきているものの、まだ不十分で、nixlangはきちんと学べば素晴らしいが、主流への参入障壁としては受け入れられない。nix-env -iA fooは実際には望む動作でないことがほとんどなので、Nixは宣伝されているようなパッケージマネージャーというより、社内の専門知識を抱えられる企業にとっての秘密兵器に近い
    floxは「一度試してすぐにより良い体験を得る」ためのハードルを下げてくれる。Nix/NixOS、あるいはそれに近い何かは、いずれDockerをSubversionのように後方へ押しやるだろうが、GitHubのような瞬間が来るまでは起きず、来たら一気に起きるはず
    このスレッドにあるNixへの不満の大半は、技術的には間違っているが十分に理解できるし、さらに重要なのはユーザーのせいではないということだ。git/GitHubのない世界を知る世代が減っているのは分かっているが、初期Googleの採用基準を通過した人たちを前に、Linusが、なぜ複雑に感じるツールに関心を持つべきなのかを説明しているのを聞いてみるといい
    https://youtu.be/MjIPv8a0hU8?si=QC0UnHXRdMpp2tI4

    • flox.devのRonだけど、この投稿のおかげでチーム全体が大いに笑った
      しばらくこれを作ってきたので、より良くするために優先すべきことがあれば聞きたい
    • 開発者ツールが成功するには、最も一般的な作業について、エンジニアがそのツールを間違って使う方法が少なくとも3つはあっても、暗黙の技術的負債を残したまま「完了」にはできる必要があると思う
      gitはそうだが、Nixはまだそうではないので、GitHubのような瞬間が助けになるのかはよく分からない
  • このブログ記事では、共有Dockerレイヤーがなぜ有用なのかの説明が抜けている
    理由はキャッシュで、より多くのイメージが同じレイヤーを共有するほど、より多くのものをキャッシュできるため、コンテナの起動が速くなる
    Dockerがここで弱い理由は、キャッシュの利点を得るには、イメージをビルドするときに既存のレイヤーとできるだけ同じレイヤーが生成される必要があるからだ。たとえば今日apt-get install python3を実行して新しい更新がなければ、昨日とまったく同じレイヤーが生成されるべきだが、Dockerレイヤーはファイルのハッシュでキャッシュされるため、作成時刻のようなメタデータまで含めて、すべてのファイルが完全に同一でなければならない
    Nixはすでに依存関係をハッシュで保存しているため、同じバージョンと同じ設定ならレイヤーは常に同じになる

    • こう表現するほうが正しそうだ: Dockerfile形式はレイヤー間に階層関係を強制する
      依存関係は通常ツリーではなくグラフを形成するため、すぐに面倒になる。Nixや、おそらくBazelのような代替ツールにはこの制約がなく、依存関係グラフをDockerレイヤーにマッピングして細粒度のキャッシュを実現できる。Dockerfileでは表現できない方法だ
    • Dockerでは、レイヤーがキャッシュされている場合、apt-getを含むレイヤーが自動的に無効化されることはない
      --no-cacheを使うか、上位レイヤーに変更がある場合にだけ変わる
  • ここ2〜3日、Darwin で Docker イメージをビルドしようと格闘していたので、この記事は宇宙が自分をからかっているように感じる
    Nix は目指すゴールには間違いなく最高のツールだが、魂を吸い取られるような暗く見捨てられた片隅がある。好きではあるけれど、時々 Rick のコンパイラランドの冒険に連れて行かれた Morty になった気分になる

    • 大きな問題は Docker の設計そのもの
      Docker は本質的に Linux バイナリを収める牢獄に近い。Linux ではバイナリをビルドしてコンテナに入れれば終わりで、Nix のコードも単純。コードをビルドできるなら、コンテナ作成はもう一段階にすぎない
      しかし Docker は Linux バイナリを要求し、Mac では Docker Desktop が Linux VM を立ち上げ、すべての作業をその中で行ったうえで、その事実を隠している。Nix はそうしないので、選択肢は2つある
      1つ目はクロスコンパイルする方法だが、glibc までクロスコンパイル可能でなければならず、コミュニティ依存関係の大半はできるとしても、一部のパッケージは作者がクロスコンパイルを考慮していないかもしれない。さらに、標準の Nix キャッシュを埋める Hydra はクロスコンパイルビルドを行わないため、長時間ビルドした末に失敗することもある
      2つ目は、Mac に Linux ビルダーを接続し、x86_64-linux のビルドジョブをそちらに送る方法。物理マシン、VM、さらには NixOS Docker コンテナでも可能
      1番は正しいやり方に見えるが、2番のほうが実用的。遭遇している問題は、おそらく1番を試しているからである可能性が高く、これは Nix だけでなくクロスコンパイルの経験もかなり要求される。Hydra が Darwin から Linux へのクロスコンパイルもビルドしてキャッシュを提供し、壊れないようにしてくれればよいが、コストも増えるだろう。2番の解決策を試すほうがよさそう
      公式の解決策があった気がする: https://ryantm.github.io/nixpkgs/builders/special/darwin-bui...
    • Orbstack を使うと、この作業はまったく問題なくうまくいく
      一部のスタックではクロスコンパイルがあまり堅牢ではないため、Docker で {aarch64,amd64} x {linux,darwin} をクロスコンパイルしている。Darwin aarch64 上で Docker を複数動かして全部コンパイルしており、体験は良かった
    • こういう20分の冒険のことを言っているのかな
      https://github.com/gytis-ivaskevicius/high-quality-nix-conte...
    • macOS は確かにもっと荒削りで、そこでは colima を使っているが、まずまずうまく動く
      バグが1つ2つあるが、主にボリューム関連のようで、Docker イメージのビルドは問題なく処理してくれる。より荒い部分は速度で、ハードウェアと Docker が Linux VM をエミュレートしなければならないという事実が重なって、かなり効いてくる
  • NixでJavaアプリケーション用のDockerイメージをビルドした経験は、あまり良くなかった
    gradle2nixが廃止された後は、GradleベースのJavaアプリケーション向けの明確な代替手段がないように見える。以前、友人にシンプルなSpring Bootアプリケーションのできるだけ小さいDockerイメージを作ってみようと持ちかけたところ、Nixで作った成果物はNixなしで作ったイメージの2倍のサイズになった
    コードはここで見られる: https://github.com/jossephus/Docker_challenge/blob/main/flak...

    • 理由はJDKが2つ入るため
      zuluと、gradleがjdk引数として含めるJDKが一緒に入る。nixpkgsのgradleGenを見ると意味が分かるはず。gradle2nixについては申し訳なく思っていて、よりハックっぽくない改善作業を進めている
    • Javaは10年以上使っていないのであまり手伝えないが、Pythonとすべての依存関係、busybox、tiniまで含めて70MBのコンテナにアプリケーションを入れたことはある
      だいたいこんな感じだった: https://gist.github.com/takeda/17b6b645ad4758d5aaf472b84447b...
      すべてをmuslでリンクし、Pythonをコンパイルする際にアプリケーションで使わないパッケージをすべて無効化し、boto3/botocoreで使わない部分を削除した。boto3/botocoreだけでも100MBを超える
      Nixパッケージは基本的にNixOSオペレーティングシステムを対象にしているため、ディスク容量に余裕がある通常の状況では全機能が有効になっていることが望まれる。そのため不要な依存関係が多く付いてくる。一方でAlpineイメージはDocker向けに設計されており、パッケージの付加機能をオフにすることを目標としているので、結果がより小さくなる
      小さいイメージを作るにはoverrideで不要なものをオフにする必要がある。例えばzuluにはalsa、fontconfig、freetype、xorg、cups、gtk、cairo、ffmpegといったものが入る。友人は必要なファイルだけを慎重に抽出してコンテナに入れたが、Nix側はzuluパッケージ全体とすべての依存関係をバンドルしていたようなもの
      まずJDKを1つだけ含めるように修正し、その後、上の方法でJDKサイズをさらに減らせる。openjdk_headlessを使うともっと簡単かもしれない
      https://github.com/NixOS/nixpkgs/blob/master/pkgs/developmen...
    • 最小のJava OCIコンテナならjibに勝つのは難しい
      https://github.com/GoogleContainerTools/jib/tree/master/jib-...
    • openjdk_headlessはSpringに不要なGTKとXの依存関係をスキップする
    • 実際にチャレンジに参加してNixコードを少し整理してみたが、核心は非常に小さいJREを作ることに見える
      Zuluの代わりにNixpkgsのheadless OpenJDKビルドを基準に変更してGUIライブラリの依存関係を取り除き、pkgs.jre_minimaljlinkでカスタムの最小JREを作った
      イメージサイズは161MBで、demo_jlinkイメージより少し大きい。実際にはアプリケーション実行に必要なすべてのモジュールを含めているため、JREが約90MBになるからだ。Dockerfile_jlinkjdeps呼び出しはすべてのモジュールを検出できておらず、java.baseだけでJREを作っていた。私の最小JREもjava.baseだけを入れればJREサイズは約50MBになり、壊れたコンテナイメージはPodman基準で117MBになる
      dockerTools.buildImage呼び出しで誤っていたcopyToRootも削除した。config.Cmdの文字列コンテキストだけでアプリはイメージに入るのに、既存コードはアプリをもう一度コピーしていた。また、dockerTools.buildLayeredImageに変更して各store pathを個別のイメージレイヤーに入れた。複数のコンテナイメージ間で依存関係を共有する場合の容量スケーラビリティには有利だが、単一イメージの実験には影響しない
      残っているのは主にJREサイズの最適化だ。glibcが次に大きい依存関係で約30MBだが、Nixpkgsが多くのロケールと文字エンコーディングをサポートするようにglibcをビルドしているためと思われる。これを別のderivationやoutputに分けて選択可能にすることが可能なのか、実用的なのかは分からない。複数のイメージをdockerTools.buildLayeredImageで作れば、同じNixpkgsコミットを使う前提でglibcと残りの依存関係はすべて共有される
      https://github.com/max-privatevoid/hackernews-docker-challen...
  • すでに Nix を導入しているなら素晴らしいし、Nix や Guix のような宣言的パッケージ管理がもっと広く普及するとよいと思う
    すでに Docker を使っているが Nix を段階的に導入したいなら、この発表のアプローチもある: https://youtu.be/l17oRkhgqHE
    設定とコンテナビルドをいきなりすべて Nix に移すのではなく、Dockerfile を維持したまま Nix の設定をビルドさせることができる。最大の欠点はレイヤーをまったく活用できない点で、利点は Dockerfile を段階的に変えながら既存の Docker インフラや自動化を再利用できる点だ

    • この記事も同じアプローチを使っている
      [1] https://mitchellh.com/writing/nix-with-dockerfiles
    • 記事の軸の一つは、Docker ビルドは再現可能ではないが Nix は再現可能だという点だ
      ただ、その非再現性のかなりの部分は、Docker レイヤーのいずれかが今後も利用可能である保証がないことによるのではないかと思う。だとすると、Nix ではパッケージバージョンがリポジトリに永遠に残る保証があるのかも気になる
  • 唐突だが、このスライドの図を作ったイラストレーターの Annie Ruygt は、YC スタートアップ 2 社である RethinkDB(rethinkdb.com) と Pachyderm(pachyderm.io) のロゴやブランドアートも手がけている
    Pachyderm で働いていたことがあり、Pachyderm は元 RethinkDB エンジニアたちが創業した会社だ。作品が本当に良い

  • 比較的最近、インフラチームの勧めで Nix を使って CI ベースイメージをビルドしようと半日ほど費やしたが、イメージは巨大で、リンクの問題のせいで一部が動作しなかった
    特に苛立たしかったのは、マルチアーキテクチャイメージを作るときに別アーキテクチャの何かを実際に実行しようとし、qemu のハードウェア仮想化しかサポートしていない点だった。ビルドマシンとワークステーションは VM なのでそれがなく、binfmt-misc はある。arm64 の mkdir を fork/exec して /tmp を作っていたなら動いたはずだが、Docker レイヤーは単なる tar ファイルなので、次のようにディレクトリを作れる
    echo "tmp uid=0 gid=0 time=0 mode=0755 type=dir" | bsdtar -cf - @-
    この正確なレイヤーはどこかにすでに存在している可能性も高く、ユーザーがダウンロードする必要すらないかもしれない
    Nix を試すたびに、あと数か月もすれば日常的に使えるようになりそうだという感覚を受ける。nixpkgs には欲しいパッケージがほぼすべてあり、ワークステーションには問題なくインストールできる。しかし「bash、python、build-essential、Bazel が必要だ」という要件は、Docker イメージビルダーの目標のようには見えない。Go バイナリ 1 つを Docker イメージに入れる用途なら Nix は不要だ。distroless を持ってきてアプリケーションを tar ファイルに入れればコンテナになる。個人的には Bazel の rules_oci を使っているが、内部でやっていることもその程度で、複数アーキテクチャ向けのバイナリをビルドし、イメージインデックス YAML を作ってレジストリにプッシュする程度の知能が少し加わっているだけだ

    • 別アーキテクチャ向けのバイナリは、実際に実行しなくてもクロスコンパイルできるはずだ
      もちろん、パッケージのビルドファイルがそれをサポートしている必要はある。qemu がハードウェア仮想化しかサポートしていないという話も正しくないように見える。ソフトウェアエミュレーションしかできないアーキテクチャにも qemu は使える
      最小例はここで扱っている: https://discourse.nixos.org/t/how-do-i-get-a-shell-nix-with-...
      pkgCross を使うくらい単純であるべきだ、とまで言うつもりはないが、一般的な手順の中でどんな具体的な問題に遭遇したのか気になる
      https://nix.dev/tutorials/cross-compilation.html
    • 言っているのはおそらく公式の Nix Docker イメージのことだろうが、それは確かに大きい
      いくつかのプロジェクトで数年間使ってきたが、サイズが問題なら、記事で触れられている方式で、指定したものだけが入った非常に小さなイメージを作れる
      [1] https://hub.docker.com/r/nixos/nix/tags
    • Docker イメージへのクロスコンパイルこそ、Nix を使った用途だった
      musl まで使い、どんなツールで作ったものよりも小さいイメージを CI で高速かつ一貫してビルドでき、キャッシュもうまく動作した。何が実行されたという話なのか、よく分からない
    • 最終的な Nix と Docker ファイルがどのようなものだったのか、そして妙なサードパーティ製バイナリをサポートするために buildFHSEnv を使う必要があったのか気になる
      Nix には、既存システムから部分ごとにスムーズに移行する方法を説明する記事がもっと必要だと思う
  • プラットフォームエンジニアとして Nix を好きになりたいが、ほかのみんなにとって簡単というわけではない
    開発者体験もまだかなり悪いと思う。たとえば devbox add python@3.11 のようにパッケージを追加できるので、devbox の開発者体験のほうを好んでいる
    120行の flake.nix を見ても、正直「より簡単」とは言いにくい
    https://github.com/Xe/douglas-adams-quotes/blob/main/flake.n...

    • その比較は少し不公平だ
      リンク先の flake は Go バイナリのビルド方法を定義し、そのバイナリをエントリーポイントとして使う Docker イメージを定義し、Go バイナリを実行する systemd サービスを作る NixOS モジュールも定義している。さらに、その NixOS モジュールが期待どおりに systemd サービスを作るか確認する NixOS テストまで含まれている
      NixOS のテストフレームワークはかなり印象的で、テストは NixOS を一緒に起動する QEMU VM の中で動く。リンク先の記事に関係があるのは、Go バイナリと Docker イメージの定義、そして周辺のボイラープレートの一部だけだ
    • その120行は代表例として見るのは難しく、単一サービスなら新しいモジュールを作らずインラインで書いたはずだ
      また、多くの行はインラインの systemd サービス記述と1対1で対応しているので、どんなシステムを使ってもなくならない。複数のシステムを override で宣言する洗練された方法も入っている
      この例は「些細なことを大規模で本格的なプロジェクトでやるようにやってみよう」に近く、単発として扱うなら40行程度まで減らせそうだ
    • この120行はかなり多くのことをしている
    • 1つのバイナリと systemd 設定を扱うのに120行のコードが使われている
      そのバイナリと設定ファイルは、Linux で得られる「flat pack」に最も近い形だが、この例は、私たちが森と木のどちらを見落としているのかをよく示している
  • Guix にも guix pack -f docker という簡単で良い Docker オプションがある
    Guix には、独自の専用言語ではなく、すでに使われている言語である Guile/Scheme を使うという利点もある
    [1] https://guix.gnu.org/manual/en/html_node/Invoking-guix-pack....