4 điểm bởi GN⁺ 2024-03-17 | 1 bình luận | Chia sẻ qua WhatsApp
  • Nếu môi trường của bạn đã dùng ảnh Docker như đơn vị triển khai, Nix là một điểm vào để thử nghiệm build có tính xác định và cố định phụ thuộc mà không phải thay đổi lớn quy trình làm việc
  • docker build thông thường dễ phụ thuộc vào trạng thái Internet công khai như kho Ubuntu hay các bản tải bên ngoài, nên sau một thời gian sẽ khó tái tạo lại đúng cùng một ảnh
  • Nix biết các phụ thuộc cần thiết ở cấp độ gói, nên với dockerTools.buildLayeredImage có thể cấu hình để chỉ tải lên lại những layer đã thay đổi
  • Ví dụ douglas-adams-quotes dựa trên Go cho thấy có thể tạo binary bằng pkgs.buildGoModule, rồi dùng nix build.#docker, docker load <./result để nạp và triển khai như một ảnh Docker thông thường
  • Khi nhiều dịch vụ trong monorepo chia sẻ layer và bộ nhớ đệm Nix, có thể giảm thời gian và chi phí build trong các lần tái tạo ảnh từ commit cũ và triển khai lặp lại

Vì sao gắn Nix vào quá trình build ảnh Docker

  • Nix đồng thời mang ba tính chất: trình quản lý gói, ngôn ngữ, và hệ điều hành
    • Dùng ngôn ngữ Nix để viết chỉ thị build gói
    • Trình quản lý gói Nix dựa trên các chỉ thị đó để tạo phần mềm, công cụ, ảnh NixOS, ảnh container, v.v.
  • Đưa Nix vào pipeline CI/CD hiện có không hề dễ
    • Nix khác với cách làm mà nhiều lập trình viên đã quen
    • Nếu không phải môi trường có thể bắt đầu lại mọi thứ từ đầu như startup mới hay homelab, rào cản áp dụng là khá lớn
  • Với các tổ chức đã dùng Docker, có thể thử áp dụng Nix từ khâu build ảnh container mà không phá vỡ đáng kể luồng triển khai hiện có

Những điểm dễ lung lay của Docker build

  • Docker và container hóa đã được chấp nhận rộng rãi, đến mức ảnh Docker được dùng như định dạng gói phổ thông trên Internet
    • Các nền tảng như Fly.io, Railway, Render dùng ảnh Docker thay vì ảnh VM tùy ý hay chương trình Linux dạng tarball
  • Docker build không phải lúc nào cũng có tính xác định
    • Phần lớn Dockerfile ngoài Internet đều build được, nhưng một tỷ lệ lỗi nhỏ vẫn có thể dẫn đến sự cố vận hành
  • Một trong những điểm yếu lớn nhất là Docker build truy cập Internet công khai
    • Điều này cần thiết để tải gói từ kho Ubuntu
    • Khi cần tái tạo lại cùng một ảnh sau này, rất khó khôi phục đúng trạng thái của kho Ubuntu tại thời điểm đó
    • Ubuntu 18.04 sắp hết hỗ trợ trong năm nay, và có thể chỉ khi xảy ra sự cố mới biết thứ gì đang phụ thuộc vào phiên bản đó
  • Cách đơn giản để thêm gói vào ảnh Docker có thể tạo ra không gian lãng phí
    • Nếu chạy apt-get upgrade ở đầu quá trình build, các tệp bên trong ảnh container có thể bị thay thế
    • Các tệp trước khi bị thay thế có thể vẫn nằm lại trong layer và tích tụ như các bản sao bị che khuất

Cách Nix xử lý các layer Docker

  • Nix biết trước các phụ thuộc cần thiết và có thể chia chúng thành số lượng layer Docker ít nhất có thể
    • Chỉ vì thay đổi một dòng mã không có nghĩa apt hay npm phải cài lại phụ thuộc
    • Chỉ phần thay đổi tối thiểu thực sự mới cần được phản ánh vào bản cập nhật ảnh
  • dockerTools là bộ công cụ để đưa các gói Nix và phụ thuộc của chúng vào ảnh Docker
  • Ảnh Docker tạo bằng Nix nhìn chung chia thành hai cách
    • Ảnh không phân layer: đặt chương trình, phụ thuộc, các mục bổ sung như chứng chỉ gốc TLS vào một thư mục rồi phơi bày thành ảnh một layer
    • Ảnh phân layer: đặt từng phụ thuộc vào layer ảnh riêng để chỉ phải tải lên phần thực sự thay đổi
  • Docker cũng có kho lưu trữ dựa trên nội dung ở bên trong, nhưng chỉ với docker build thì khó tận dụng đầy đủ
    • Ảnh phân layer của Nix dùng layer theo từng gói, nên các phụ thuộc như glibc chỉ cần tải lên một lần
    • Tuy nhiên nếu thư viện đó cần sửa đổi thì layer tương ứng vẫn phải được tải lên lại

Ví dụ dịch vụ Go: Douglas Adams Quotes

bin = pkgs.buildGoModule {
  pname = "douglas-adams-quotes";
  inherit version;
  src = ./.;
  vendorHash = null;
};
  • Định nghĩa này dùng mẫu Go module để cấu hình trình biên dịch Go, trình biên dịch C cho CGo, và tải các phụ thuộc bên ngoài
    • pname là tên gói
    • version được tạo tự động từ commit Git của dịch vụ
    • src = ./.; dùng mã nguồn trong thư mục làm việc hiện tại
    • Nếu không có phụ thuộc ngoài thư viện chuẩn thì có thể để vendorHash = null
    • Nếu có phụ thuộc bên ngoài, có thể chỉ định hash của toàn bộ phụ thuộc hoặc dùng gomod2nix
  • Gói được build bằng lệnh sau
nix build .#bin
  • Ảnh Docker phân layer có thể được tạo bằng dockerTools.buildLayeredImage
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • Khi chỉ định binary máy chủ đã build trong config.Cmd, các thành phần cần thiết sẽ được sao chép kèm theo
    • glibc và các mục cần thiết để chạy cũng được bao gồm
  • Có thể thêm các gói bổ sung như chứng chỉ gốc CA vào 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";
};
  • Ảnh được tạo và nạp vào Docker daemon bằng các lệnh sau
nix build .#docker
docker load < ./result
  • Nếu mở bằng dive, có thể thấy mỗi layer thêm các gói khác nhau từ nixpkgs, và cuối cùng các mục được liên kết tượng trưng vào root của ảnh

Hiệu quả của việc chia sẻ layer trong monorepo

  • Nếu có nhiều dịch vụ trong cùng một kho, các ảnh Docker tạo bằng Nix có thể chia sẻ layer
    • Việc này diễn ra mà không cần cấu hình riêng
    • Nếu chỉ làm bằng Docker, thường phải tạo nhiều base image dùng chung, và một số dịch vụ có thể chứa công cụ hay nội dung không cần thiết mà chúng không dùng
  • Ví dụ, kho Xe/x là một monorepo tập hợp 10 năm side project, thử nghiệm và công cụ
    • Nhiều dự án trong đó đang được triển khai trên khoảng ba nền tảng
    • Nhằm hội tụ về một nền tảng triển khai chung, tác giả đã tiến hành đóng gói chúng thành ảnh Docker
  • Khi đẩy bản cập nhật của một dịch vụ, một phần cập nhật được chia sẻ với phần lớn các dịch vụ khác cũng sẽ được đẩy theo
    • Cách này giúp tiết kiệm thời gian và chi phí trên nhiều dự án

Những gì khó đạt được chỉ với cache của Docker

  • Về lý thuyết, dùng cơ chế cache của Docker cũng có thể build ảnh hiệu quả gần như Nix
  • Nhưng để đạt hiệu quả tương tự theo cách của Docker, các bước build có thể trở nên khó bảo trì
    • Cần cài thư viện dùng chung vào layer riêng
    • Phải bật lại network stack trong lúc build nên khả năng tái lập kém hơn
    • Phải điều chỉnh lại search path, cờ trình biên dịch và cấu hình liên quan đến CGo
  • dockerTools.buildLayeredImage của Nix xử lý thay các chi tiết khi đưa gói vào container, giúp cấu hình đơn giản hơn

Tái tạo trạng thái quá khứ và bộ nhớ đệm Nix

  • Một trong những ưu điểm lớn của Nix là có thể build lại chính xác phần mềm ở một thời điểm trong quá khứ
    • Khi cần tái hiện lỗi ở môi trường của một khách hàng sau này, có thể dựng lại cùng một ảnh Docker
    • Build gói có vai trò cố định toàn bộ trạng thái của phần mềm và các phụ thuộc tại một thời điểm
  • XeDN là dự án đã được phát triển trong vài năm, và phiên bản từ 14 tháng trước có thể được build bằng lệnh sau
nix build github:Xe/x/567fdc2#xedn-docker
  • Lệnh này build mục tiêu xedn-docker từ một commit cụ thể của GitHub repo Xe/x
    • Nếu nạp vào Docker daemon, ảnh tạo ra sẽ có cùng từng byte như thời điểm đó
    • Ví dụ này cũng bao gồm Go 1.19
  • Nếu chỉ dùng Docker build thông thường để tái tạo tương tự, có thể sẽ cần rất nhiều chi phí lưu trữ
  • Bộ nhớ đệm Nix lưu đầu ra của các lệnh Nix để không phải build lại sau này
    • Mỗi laptop của lập trình viên không cần tự build lại các gói như nokogiri
    • Có thể nhận kết quả đã được build sẵn trên đám mây
  • Garnix được dùng làm CI cho mọi dự án flakes và báo cáo trạng thái build theo từng commit
    • Cấu hình máy homelab cũng được build bằng Garnix, nên khi cập nhật mỗi tối sẽ lấy cấu hình mới nhất từ bộ nhớ đệm Garnix thay vì tự build

Kết luận với tư cách là đầu ra triển khai

  • Có thể xem Nix là một trình dựng ảnh Docker tốt hơn chính trình dựng ảnh của Docker
  • Nix buộc bạn chỉ định kết quả mong muốn, thay vì phải tự liệt kê chi tiết các bước để đi tới kết quả đó
  • Trong môi trường đã dùng Docker, áp dụng Nix bắt đầu từ khâu build ảnh Docker là một lộ trình triển khai thực tế
  • Ảnh Docker tạo bằng Nix có thể chia sẻ layer giữa nhiều phần trong monorepo
  • Nhờ bộ nhớ đệm nhị phân, không cần build lại mã đã từng được build trong quá khứ
  • Đầu ra cuối cùng vẫn là một ảnh container thông thường có thể triển khai lên các nền tảng như AWS, Google Cloud, Fly.io

1 bình luận

 
GN⁺ 2024-03-17
Ý kiến trên Hacker News
  • Tôi đã nhiều lần cố gắng thích Nix, nhưng giờ cảm giác là nên bỏ cuộc
    Tôi có 2 hệ thống chạy Nix và sợ động vào chúng; trước đây cả hai từng hỏng đến mức tôi phải cài lại từ đầu. Về lý thuyết, Nix có tính lũy đẳng và tất định, nhưng trên thực tế bạn phải hiểu nó tất định đối với điều gì, và nếu không hiểu sâu hành vi của mọi phần mà nó phụ thuộc vào, bạn sẽ gặp các kết quả kỳ lạ, các lỗi quái dị chẳng giúp ích gì, hoặc thậm chí không có phản hồi nào
    Tài liệu, dù đầy đủ và đúng về mặt kỹ thuật, vẫn khó hiểu một cách khủng khiếp, còn tutorial thì chỉ đưa bạn đi được khoảng 80% chặng đường. Ra ngoài phạm vi đó thì bạn phải tự dựng lấy, mà việc đó đòi hỏi nhiều năm kinh nghiệm và thất vọng, điều tôi không còn muốn chịu nữa. Điểm mạnh của Docker lại chính là sự hỗn loạn đó: chỉ cần biết sơ sơ về shell và trình quản lý gói của distro là gần như có thể làm được bất cứ thứ gì, và khi có vấn đề, nhờ các công cụ đã tồn tại hàng chục năm, việc chẩn đoán và sửa lỗi dễ hơn nhiều
    Nix giống Emacs ở chỗ, nếu có đủ kiên nhẫn cùng kiến thức sâu và mang tính tầm nhìn, bạn có thể làm bất cứ điều gì; nhưng có vẻ hoặc là phải dấn thân hoàn toàn, hoặc chỉ đứng từ xa quan sát

    • Tôi cũng đã đi qua con đường đó, và dù thích các bản build tất định, tôi nghĩ vấn đề lớn nhất là Dockerfile trông có vẻ tất định đối với lập trình viên trung bình
      Build liên tiếp hai lần trên cùng một máy thì sẽ ra cùng kết quả, nhưng theo thời gian, khi trình quản lý gói và tag của base image được cập nhật, kết quả build lại sau một tháng có thể hoàn toàn khác. Khi nhóm phải quản lý khoảng 40 container, việc sửa container trở thành một phần lớn của công việc
      Vì vậy về lý thuyết Nix lẽ ra phải hoàn hảo, nhưng nó quá khác biệt: công cụ của vendor không chạy được trên Nix, và khi gặp lỗi cũng khó tìm nhanh lời giải trên web. Vì sự bức bối đó tôi đã tạo https://www.stablebuild.com, cung cấp build tất định dựa trên Docker trên các container Ubuntu, Debian, Alpine. Nó gồm cache pull-through bất biến cho Docker Hub, bản sao đầy đủ hằng ngày của các kho gói Ubuntu/Debian/Alpine, bản sao hằng ngày của các PPA phổ biến và chỉ mục PyPI, cùng cache bất biến cho tệp/URL tùy ý
      Trong thực tế, nó dễ dùng, dễ debug và có khả năng tương thích phần mềm rộng, nên với các container đã chuyển sang StableBuild, số vấn đề phát sinh do tính không tất định là 0
    • Tài liệu không chỉ khó hiểu; đặc biệt ở phần CLI mới và flakes, nhiều chỗ đơn giản là sai, và cũng không phải chỉ là edge case
      Tôi từng mất thời gian để hiểu vì sao nix develop không hoạt động như tài liệu mô tả, và làm sao để nó hoạt động như vậy. Tài liệu nói rằng mặc định bạn sẽ vào môi trường tại thời điểm build, nhưng mặc định thực tế lại không được đóng kín, tên các tùy chọn dòng lệnh cũng gây nhầm lẫn, nên tôi phải đào kiến thức từ mã nguồn
      Tôi cũng mong các edge case phá vỡ tính tái lập được nêu rõ hơn. Mã dấu phẩy động nhạy với thứ tự phép toán, và trạng thái có thể rò rỉ do cơ chế preempt của hệ điều hành; nếu ngay cả những điều hiển nhiên cũng không được nói rõ, mọi người sẽ tự bắn vào chân mình
    • Không tệ đến mức đó, nhưng ngay cả với cấu hình NixOS tiêu chuẩn, vẫn còn vô số trạng thái không thể tái lập trong tài khoản người dùng và bên trong hệ thống
      Nếu dùng thiết lập “Erase your darlings”, có thể loại bỏ hầu hết trạng thái không thể tái lập bên ngoài tài khoản người dùng. Hơi phiền, nhưng trong NixOS thì có gì mà không phiền đâu
      https://grahamc.com/blog/erase-your-darlings/
      Tôi không bận tâm phần bên trong tài khoản người dùng, và cũng không thích Home Manager
    • Ở công ty tôi dùng cả Docker lẫn NixOS, nhưng chưa từng gặp các vấn đề nói ở trên
      Docker thì ổn, nhưng trên Mac hiệu năng không tốt lắm; còn với Nix, việc cài đặt và khiến mọi thứ hoạt động theo cùng một cách trên nhiều máy là chuyện rất đơn giản nên tôi thích điểm đó. Tài liệu Nix tệ thật, nhưng ChatGPT-4 rất hữu ích trong việc xử lý vấn đề với Nix
      Cảm giác 90% vấn đề với Nix nảy sinh vì cố làm những việc không theo “cách của Nix”
    • Đáng thử một distro bất biến như Fedora Atomic
      Tôi đã dùng gần như mọi trình quản lý gói của các distro và bằng cách nào đó đều từng làm chúng hỏng, dù không làm gì đặc biệt. Fedora Kinoite vẫn chịu được việc thêm/xóa layer, cập nhật hằng ngày, thậm chí rebase từ Silverblue. Cá nhân tôi nghĩ rpm-ostree có thể sẽ thay thế Nix
  • Tôi xem Nix và NixOS đang ở trạng thái tương tự như git trước GitHub
    Ý tưởng cốt lõi dựa trên khoa học máy tính nghiêm túc hơn so với các cách làm hiện có như SVN hay Docker; phần “đường ống” bên trong vẫn còn vấn đề nhưng không tệ hơn, còn các công cụ và tài liệu hướng tới người dùng thì chưa đạt mức được chấp nhận rộng rãi
    Việc flox ra mắt có thể đã thay đổi điều này: https://flox.dev. Gần như mượt mà, và cũng không ngạc nhiên vì họ đến từ DE Shaw
    Nix không có nhiều ý nghĩa nếu thiếu flakes và nix-command, nhưng cả hai đều được tài liệu ghi là thử nghiệm và mặc định cũng bị tắt. Tài liệu đang tốt hơn nhưng vẫn còn thiếu, còn nixlang thì rất hay nếu học kỹ, nhưng không thể chấp nhận như một rào cản gia nhập cho số đông. nix-env -iA foo thực tế trong đa số trường hợp không phải là hành vi bạn muốn, nên Nix giống một vũ khí bí mật cho các công ty đủ sức duy trì chuyên môn nội bộ hơn là một trình quản lý gói theo cách nó được quảng bá
    flox hạ thấp rào cản để “dùng thử một lần là có ngay trải nghiệm tốt hơn”. Nix/NixOS, hoặc một thứ tương tự, cuối cùng sẽ đẩy Docker lùi lại như Subversion, nhưng điều đó sẽ không xảy ra cho đến khi có khoảnh khắc kiểu GitHub, rồi sẽ xảy ra cùng một lúc
    Phần lớn các phàn nàn về Nix trong thread này về mặt kỹ thuật là sai, nhưng hoàn toàn dễ hiểu, và quan trọng hơn là không phải lỗi của người dùng. Tôi biết thế hệ từng biết một thế giới không có git/GitHub đang ít dần, nhưng sẽ đáng nghe Linus giải thích trước những người vượt qua tiêu chuẩn tuyển dụng thời kỳ đầu của Google rằng tại sao họ nên quan tâm đến một công cụ mà họ cảm thấy phức tạp
    https://youtu.be/MjIPv8a0hU8?si=QC0UnHXRdMpp2tI4

    • Tôi là Ron từ flox.dev; bài viết này đã khiến cả đội cười rất nhiều
      Chúng tôi đã xây dựng nó một thời gian, và nếu có điều gì đáng ưu tiên để làm nó tốt hơn thì tôi rất muốn nghe
    • Tôi nghĩ để một công cụ dành cho developer thành công, với những tác vụ phổ biến nhất, kỹ sư phải có ít nhất ba cách dùng sai công cụ, nhưng vẫn có thể “hoàn tất” trong khi để lại nợ kỹ thuật ngầm
      git làm được như vậy, còn Nix thì chưa, nên tôi không chắc một khoảnh khắc kiểu GitHub có giúp được không
  • Bài blog này thiếu phần giải thích vì sao các layer Docker dùng chung lại hữu ích
    Lý do là caching: càng nhiều image dùng chung cùng một layer thì càng cache được nhiều thứ hơn, nhờ đó container khởi động nhanh hơn
    Docker yếu ở điểm này vì để hưởng lợi từ caching, khi build image bạn cần tạo ra các layer giống với layer hiện có nhiều nhất có thể. Ví dụ nếu hôm nay chạy apt-get install python3 mà không có bản cập nhật mới nào, lẽ ra phải tạo ra layer chính xác giống hệt hôm qua, nhưng Docker layer được cache bằng hash của file, nên mọi file phải giống hệt nhau, kể cả metadata như thời điểm tạo
    Nix vốn đã lưu dependency bằng hash, nên với cùng phiên bản và cùng cấu hình, layer sẽ luôn giống nhau

    • Có vẻ nên diễn đạt thế này: định dạng Dockerfile buộc phải có quan hệ phân cấp giữa các layer
      Dependency thường tạo thành graph chứ không phải tree, nên việc này nhanh chóng trở nên phiền phức. Các công cụ thay thế như Nix, và có lẽ Bazel, không có ràng buộc này và có thể ánh xạ dependency graph vào Docker layer để đạt caching chi tiết. Đây là cách không thể biểu đạt bằng Dockerfile
    • Trong Docker, nếu layer đã được cache thì layer chứa apt-get sẽ không tự động bị vô hiệu hóa
      Nó chỉ thay đổi khi dùng --no-cache hoặc khi có thay đổi ở layer phía trên
  • Gần đây tôi đã vật lộn 2–3 ngày để build image Docker trên Darwin, nên bài này khiến tôi có cảm giác như vũ trụ đang trêu ngươi mình
    Nix rõ ràng là công cụ tốt nhất cho mục tiêu mong muốn, nhưng nó có những góc tối bị bỏ hoang hút cạn linh hồn. Tôi thích nó, nhưng đôi khi có cảm giác như mình là Morty bị kéo vào chuyến phiêu lưu Compilerland của Rick

    • Vấn đề lớn nằm ở chính thiết kế của Docker
      Về bản chất, Docker gần như là một nhà tù chứa các binary Linux. Trên Linux, bạn build binary rồi đưa vào container là xong, và mã Nix cũng đơn giản. Nếu build được code thì tạo container chỉ là thêm một bước nữa
      Nhưng Docker yêu cầu binary Linux, còn trên Mac thì Docker Desktop khởi chạy một VM Linux, thực hiện mọi việc trong đó rồi che giấu sự thật ấy. Nix không làm như vậy, nên có hai lựa chọn
      Thứ nhất là cross-compile, nhưng khi đó phải cross-compile được cả glibc; phần lớn dependency cộng đồng có thể làm được, nhưng một số package có thể chưa được tác giả quan tâm đến cross-compile. Hơn nữa, Hydra — nơi lấp đầy cache Nix tiêu chuẩn — không build theo kiểu cross-compile, nên bạn có thể build rất lâu rồi thất bại
      Thứ hai là gắn một Linux builder vào Mac và gửi các tác vụ build x86_64-linux sang đó. Có thể là máy vật lý, VM, thậm chí container Docker NixOS cũng được
      Cách 1 trông có vẻ là cách đúng, nhưng cách 2 thực dụng hơn. Vấn đề bạn gặp có lẽ nhiều khả năng là do đang thử cách 1, và việc này đòi hỏi nhiều kinh nghiệm không chỉ về Nix mà cả cross-compile. Sẽ tốt nếu Hydra cũng build cross-compile từ Darwin sang Linux để cung cấp cache và ngăn tình trạng vỡ build, nhưng chi phí cũng sẽ tăng. Có vẻ nên thử giải pháp số 2
      Hình như từng có giải pháp chính thức: https://ryantm.github.io/nixpkgs/builders/special/darwin-bui...
    • Dùng Orbstack thì việc này chạy trơn tru không lỗi
      Ở một số stack, cross-compile không thật sự vững chắc, nên tôi cross-compile {aarch64,amd64} x {linux,darwin} bằng Docker. Tôi đang chạy nhiều Docker trên Darwin aarch64 để compile tất cả, và trải nghiệm khá tốt
    • Ý bạn là kiểu chuyến phiêu lưu 20 phút này à
      https://github.com/gytis-ivaskevicius/high-quality-nix-conte...
    • macOS chắc chắn gập ghềnh hơn; ở đó tôi dùng colima và nhìn chung khá ổn
      Có một hai bug, nhưng chủ yếu có vẻ liên quan đến volume, còn build image Docker thì xử lý ổn. Phần gập ghềnh hơn là tốc độ: phần cứng cộng với việc Docker phải giả lập VM Linux khiến nó bị chậm đáng kể
  • Trải nghiệm build Docker image cho ứng dụng Java bằng Nix không được tốt lắm
    Sau khi gradle2nix bị bỏ, dường như không có lựa chọn thay thế rõ ràng nào cho các ứng dụng Java dựa trên Gradle. Trước đây tôi từng rủ một người bạn thử tạo Docker image nhỏ nhất có thể cho một ứng dụng Spring Boot đơn giản, nhưng kết quả tạo bằng Nix lại có kích thước gấp đôi image tạo không dùng Nix
    Có thể xem mã tại đây: https://github.com/jossephus/Docker_challenge/blob/main/flak...

    • Lý do là vì có hai JDK được đưa vào
      Cả zulu và JDK mà gradle đưa vào qua đối số jdk đều được kèm theo. Nhìn vào gradleGen trong nixpkgs sẽ hiểu ý này. Xin lỗi gradle2nix, tôi đang làm một bản cải thiện ít kiểu hack hơn
    • Tôi đã không dùng Java hơn 10 năm nên không giúp được nhiều, nhưng từng đóng gói một ứng dụng vào container 70MB gồm cả Python, toàn bộ dependency, busybox và tini
      Đại khái là như thế này: https://gist.github.com/takeda/17b6b645ad4758d5aaf472b84447b...
      Tôi link mọi thứ với musl, khi biên dịch Python thì tắt toàn bộ các package ứng dụng không dùng đến, và loại bỏ những phần không dùng trong boto3/botocore. Chỉ riêng boto3/botocore đã hơn 100MB
      Các package Nix về cơ bản nhắm tới hệ điều hành NixOS, nên trong bối cảnh thông thường có nhiều dung lượng đĩa, người ta muốn bật đầy đủ tính năng. Vì vậy kéo theo rất nhiều dependency không cần thiết. Ngược lại, Alpine image được thiết kế cho Docker, mục tiêu là tắt các tính năng phụ trong package, nên kết quả nhỏ hơn
      Muốn tạo image nhỏ thì phải dùng override để tắt những thứ không cần. Ví dụ zulu có các thứ như alsa, fontconfig, freetype, xorg, cups, gtk, cairo, ffmpeg. Bạn tôi đã cẩn thận chỉ trích xuất các file cần thiết rồi đưa vào container, còn phía Nix thì coi như đã bundle toàn bộ package zulu cùng mọi dependency của nó
      Trước hết cần sửa để chỉ đưa vào một JDK, rồi sau đó có thể giảm thêm kích thước JDK bằng các cách ở trên. Dùng openjdk_headless có thể sẽ đơn giản hơn
      https://github.com/NixOS/nixpkgs/blob/master/pkgs/developmen...
    • Nếu là Java OCI container tối thiểu thì khó thắng được jib
      https://github.com/GoogleContainerTools/jib/tree/master/jib-...
    • openjdk_headless bỏ qua các dependency GTK và X mà Spring không cần
    • Tôi đã trực tiếp tham gia thử thách và dọn dẹp lại một chút mã Nix; điểm mấu chốt có vẻ là tạo một JRE thật nhỏ
      Tôi chuyển từ Zulu sang dựa trên bản build OpenJDK headless của Nixpkgs để loại bỏ dependency vào thư viện GUI, rồi dùng pkgs.jre_minimaljlink để tạo một JRE tối thiểu tùy chỉnh
      Kích thước image là 161MB, lớn hơn một chút so với image demo_jlink. Lý do là JRE khoảng 90MB vì thực sự bao gồm tất cả các module cần thiết để chạy ứng dụng. Lệnh gọi jdeps trong Dockerfile_jlink không phát hiện được tất cả module, nên đang tạo JRE chỉ với java.base. Nếu JRE tối thiểu của tôi cũng chỉ đưa java.base vào thì kích thước JRE khoảng 50MB, và image container bị hỏng là 117MB theo Podman
      Tôi cũng đã loại bỏ copyToRoot sai trong lời gọi dockerTools.buildImage. Chỉ riêng string context của config.Cmd cũng đã đưa app vào image, còn mã cũ đang copy app thêm một lần nữa. Ngoài ra tôi đổi sang dockerTools.buildLayeredImage để đưa từng store path vào từng layer image riêng; điều này tốt cho khả năng mở rộng dung lượng khi chia sẻ dependency giữa nhiều container image, nhưng không ảnh hưởng đến thử nghiệm với một image đơn lẻ
      Phần còn lại chủ yếu là tối ưu kích thước JRE. glibc là dependency lớn tiếp theo, khoảng 30MB, có vẻ vì Nixpkgs build glibc để hỗ trợ nhiều locale và bảng mã ký tự. Tôi không biết việc tách phần này thành derivation hoặc output riêng để có thể chọn dùng có khả thi và thực dụng hay không. Nếu tạo nhiều image bằng dockerTools.buildLayeredImage, với điều kiện dùng cùng một commit Nixpkgs, glibc và các dependency còn lại đều sẽ được chia sẻ
      https://github.com/max-privatevoid/hackernews-docker-challen...
  • Nếu đã dùng Nix rồi thì rất tuyệt, và hy vọng quản lý gói theo kiểu khai báo như Nix hay Guix sẽ phổ biến rộng rãi hơn
    Nếu bạn đang dùng Docker nhưng muốn đưa Nix vào dần dần, cũng có cách tiếp cận trong bài thuyết trình này: https://youtu.be/l17oRkhgqHE
    Thay vì chuyển ngay toàn bộ cấu hình và phần build container sang Nix, bạn có thể giữ Dockerfile và để nó build cấu hình Nix. Nhược điểm lớn nhất là hoàn toàn không tận dụng được layer, còn ưu điểm là có thể thay đổi Dockerfile từng bước trong khi tái sử dụng hạ tầng hoặc tự động hóa Docker hiện có

    • Bài viết này cũng dùng cùng cách tiếp cận
      [1] https://mitchellh.com/writing/nix-with-dockerfiles
    • Một trục lập luận của bài là build bằng Docker không tái lập được, còn Nix thì tái lập được
      Nhưng có lẽ phần lớn tính không tái lập đó là do không có gì đảm bảo một trong các layer Docker sẽ tiếp tục còn dùng được. Nếu vậy thì tôi cũng tò mò liệu Nix có đảm bảo rằng các phiên bản gói sẽ mãi mãi nằm trong kho lưu trữ hay không
  • Hơi lạc đề, nhưng họa sĩ minh họa Annie Ruygt, người vẽ hình cho slide này, cũng đã làm logo và art thương hiệu cho hai startup YC là RethinkDB(rethinkdb.com) và Pachyderm(pachyderm.io)
    Cô ấy từng làm ở Pachyderm, còn Pachyderm là công ty do các cựu kỹ sư RethinkDB sáng lập. Tác phẩm của cô ấy thật sự rất tốt

  • Tương đối gần đây, theo đề xuất của đội hạ tầng, tôi đã dành khoảng nửa ngày để thử build image nền CI bằng Nix, nhưng image rất lớn và một phần không chạy được vì vấn đề liên kết
    Điều đặc biệt khó chịu là khi tạo image đa kiến trúc, nó lại cố thực sự chạy thứ gì đó của kiến trúc khác, và chỉ hỗ trợ ảo hóa phần cứng bằng qemu. Máy build và workstation của tôi là VM nên không có cái đó, còn binfmt-misc thì có. Nếu nó fork/exec mkdir arm64 để tạo /tmp thì đã chạy được, nhưng layer Docker chỉ là file tar, nên có thể tạo thư mục như dưới đây
    echo "tmp uid=0 gid=0 time=0 mode=0755 type=dir" | bsdtar -cf - @-
    Rất có thể layer chính xác này đã tồn tại đâu đó, nên người dùng thậm chí có thể không cần tải xuống
    Mỗi lần thử Nix, tôi lại có cảm giác chỉ cần thêm vài tháng nữa là có thể dùng nó thường xuyên. nixpkgs có gần như mọi gói tôi muốn và cài trên workstation thì ổn. Nhưng yêu cầu “cần bash, python, build-essential, Bazel” trông không giống mục tiêu của một Docker image builder. Nếu chỉ để đưa một binary Go vào Docker image thì không cần Nix. Lấy distroless rồi bỏ ứng dụng vào file tar là thành container. Cá nhân tôi dùng rules_oci của Bazel; bên trong nó cũng chỉ làm đến mức đó, cộng thêm chút thông minh để build binary cho nhiều kiến trúc, tạo YAML image index và push lên registry

    • Binary cho kiến trúc khác lẽ ra phải cross-compile được mà không cần thực sự chạy
      Tất nhiên file build của gói phải hỗ trợ điều đó. Nói qemu chỉ hỗ trợ ảo hóa phần cứng cũng có vẻ không đúng. qemu vẫn dùng được cho cả những kiến trúc chỉ có thể mô phỏng bằng phần mềm
      Ví dụ tối thiểu được đề cập ở đây: https://discourse.nixos.org/t/how-do-i-get-a-shell-nix-with-...
      Tôi không định nói rằng nó phải đơn giản như chỉ dùng pkgCross, nhưng tôi tò mò bạn đã gặp vấn đề cụ thể nào trong quy trình thông thường
      https://nix.dev/tutorials/cross-compilation.html
    • Có lẽ thứ đang được nói đến là Nix Docker image chính thức, và nó quả thật lớn
      Tôi đã dùng nó nhiều năm trong vài dự án, nhưng nếu kích thước là vấn đề thì có thể tạo image rất nhỏ chỉ chứa những gì được chỉ định theo cách bài viết đề cập
      [1] https://hub.docker.com/r/nixos/nix/tags
    • Cross-compile sang Docker image chính là mục đích tôi dùng Nix
      Tôi còn dùng cả musl, build được những image nhỏ nhất so với bất kỳ công cụ nào khác, nhanh và nhất quán trong CI, cache cũng hoạt động tốt. Tôi không hiểu rõ là thứ gì đã được chạy
    • Tôi tò mò file Nix và Docker cuối cùng trông như thế nào, và liệu có phải dùng buildFHSEnv để hỗ trợ các binary bên thứ ba kỳ lạ hay không
      Có vẻ Nix cần thêm nhiều bài viết giải thích cách chuyển đổi mềm mại, từng phần một, từ các hệ thống hiện có
  • Là kỹ sư nền tảng, tôi muốn thích Nix, nhưng nó không dễ với tất cả những người khác
    Tôi cũng nghĩ trải nghiệm dành cho lập trình viên hiện vẫn khá tệ. Ví dụ, tôi thích trải nghiệm lập trình viên của devbox hơn vì có thể thêm gói như devbox add python@3.11
    Nhìn vào flake.nix dài 120 dòng thì cũng khó nói chính xác là “dễ hơn”
    https://github.com/Xe/douglas-adams-quotes/blob/main/flake.n...

    • So sánh đó hơi không công bằng
      Flake được liên kết định nghĩa cách build binary Go, định nghĩa Docker image dùng binary đó làm entrypoint, và còn định nghĩa một module NixOS tạo service systemd chạy binary Go. Ngoài ra còn bao gồm cả test NixOS để kiểm tra module NixOS đó có tạo service systemd đúng như mong đợi hay không
      Framework test của NixOS khá ấn tượng, và các test chạy bên trong QEMU VM chạy kèm NixOS. Những phần liên quan đến bài viết được liên kết chỉ là định nghĩa binary Go và Docker image, cùng một phần boilerplate xung quanh
    • 120 dòng đó khó xem là ví dụ đại diện; nếu là một service đơn lẻ thì có lẽ đã viết inline thay vì tạo module mới
      Ngoài ra, nhiều dòng tương ứng 1:1 với phần mô tả service systemd inline, nên dù dùng hệ thống nào thì chúng cũng không biến mất. Nó còn có một cách khá hay để khai báo nhiều hệ thống bằng override
      Ví dụ này giống kiểu “hãy thử làm một việc nhỏ theo cách như trong một dự án lớn nghiêm túc”; nếu xử lý như việc dùng một lần thì tôi nghĩ có thể rút xuống khoảng 40 dòng
    • 120 dòng này đang làm khá nhiều việc
    • Cần 120 dòng code để xử lý một binary và cấu hình systemd
      Binary đó và file cấu hình là dạng gần nhất với một “flat pack” mà bạn có thể có trên Linux, và ví dụ này cho thấy rõ chúng ta đã bỏ lỡ điều gì giữa rừng và cây
  • Guix cũng có một tùy chọn Docker dễ dùng và khá ổn là guix pack -f docker
    Guix còn có ưu điểm là dùng Guile/Scheme, một ngôn ngữ đã được sử dụng, thay vì một ngôn ngữ chuyên dụng riêng
    [1] https://guix.gnu.org/manual/en/html_node/Invoking-guix-pack....