4P by GN⁺ | ★ favorite | 댓글 1개
  • Docker 이미지를 이미 배포 단위로 쓰는 환경이라면, Nix는 워크플로를 크게 바꾸지 않고 결정적 빌드와 의존성 고정을 실험할 수 있는 진입점이 됨
  • 일반적인 docker build는 Ubuntu 저장소나 외부 다운로드처럼 공개 인터넷 상태에 기대기 쉬워, 시간이 지난 뒤 같은 이미지를 재현하기 어려움
  • Nix는 필요한 의존성을 패키지 단위로 알고 있어 dockerTools.buildLayeredImage변경된 레이어만 다시 업로드하는 구성이 가능함
  • Go 기반 douglas-adams-quotes 예시는 pkgs.buildGoModule로 바이너리를 만들고 nix build.#docker, docker load <./result로 일반 Docker 이미지처럼 로드해 배포할 수 있음을 보여줌
  • 모노레포의 여러 서비스가 레이어와 Nix 캐시를 공유하면, 과거 커밋의 이미지 재현과 반복 배포에서 빌드 시간·비용을 줄일 수 있음

Docker 이미지 빌드에 Nix를 붙이는 이유

  • Nix는 패키지 관리자, 언어, 운영체제라는 세 가지 성격을 함께 가짐
    • Nix 언어로 패키지 빌드 지시를 작성함
    • Nix 패키지 관리자가 그 지시를 바탕으로 소프트웨어, 도구, NixOS 이미지, 컨테이너 이미지 등을 만들 수 있음
  • 기존 CI/CD 파이프라인에 Nix를 도입하기는 쉽지 않음
    • Nix는 많은 개발자가 익숙한 방식과 다름
    • 새 스타트업이나 홈랩처럼 모든 것을 새로 시작할 수 있는 환경이 아니면 도입 장벽이 큼
  • 이미 Docker를 쓰는 조직은 컨테이너 이미지 빌드부터 Nix를 적용해 기존 배포 흐름을 크게 깨지 않고 실험할 수 있음

Docker 빌드가 흔들리는 지점

  • Docker와 컨테이너화는 널리 채택되어, Docker 이미지는 인터넷의 사실상 범용 패키지 형식처럼 쓰임
    • Fly.io, Railway, Render 같은 플랫폼은 임의의 VM 이미지나 tarball 형태의 Linux 프로그램 대신 Docker 이미지를 사용함
  • Docker 빌드는 항상 결정적이지 않음
    • 인터넷에서 흔히 보는 Dockerfile은 대부분 잘 빌드되지만, 실패하는 작은 비율이 운영 문제로 이어질 수 있음
  • 가장 큰 취약점 중 하나는 Docker 빌드가 공개 인터넷에 접근한다는 점임
    • Ubuntu 저장소에서 패키지를 내려받는 데 필요함
    • 나중에 같은 이미지를 재생성해야 할 때 당시 Ubuntu 저장소의 정확한 상태를 되돌리기 어려움
    • Ubuntu 18.04는 해당 연도에 지원 종료를 앞두고 있어, 어떤 것이 그 버전에 의존하는지 장애가 난 뒤에야 알게 될 수 있음
  • 단순한 방식으로 Docker 이미지에 패키지를 추가하면 낭비 공간이 생길 수 있음
    • 빌드 초기에 apt-get upgrade를 실행하면 컨테이너 이미지 안의 파일이 교체될 수 있음
    • 교체 전 파일이 레이어에 남아 그림자 복사본처럼 누적될 수 있음

Nix가 Docker 레이어를 다루는 방식

  • Nix는 필요한 의존성을 사전에 알고, 이를 가능한 적은 Docker 레이어로 나눌 수 있음
    • 코드 한 줄을 바꿨다고 aptnpm이 의존성을 다시 설치할 필요가 없음
    • 실제로 바뀐 최소한의 내용만 이미지 업데이트에 반영할 수 있음
  • dockerTools는 Nix 패키지와 그 의존성을 Docker 이미지에 넣는 도구 모음임
  • Nix로 만드는 Docker 이미지는 크게 두 방식으로 나뉨
    • 비레이어 이미지: 프로그램, 의존성, TLS 루트 인증서 같은 추가 항목을 하나의 폴더에 넣고 단일 레이어 이미지로 노출함
    • 레이어 이미지: 각 의존성을 별도 이미지 레이어로 배치해 실제 변경된 부분만 업로드할 수 있음
  • 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년치 사이드 프로젝트, 실험, 도구가 모인 모노레포임
    • 여러 프로젝트가 약 세 개 플랫폼에 배포되어 있음
    • 공통 배포 기반으로 수렴하기 위해 Docker 이미지로 넣는 작업을 진행해 왔음
  • 한 서비스 업데이트를 푸시하면 다른 서비스 대부분과 공유되는 업데이트 일부도 함께 푸시됨
    • 이 방식은 여러 프로젝트에서 시간과 비용을 절약함

Docker 캐시만으로 따라가기 어려운 부분

  • Docker 캐싱을 이용하면 이론상 Nix만큼 효율적으로 이미지를 빌드할 수도 있음
  • 하지만 Docker 방식으로 같은 효과를 내려면 빌드 단계가 유지보수하기 어려워질 수 있음
    • 공유 라이브러리를 별도 레이어에 설치해야 함
    • 빌드 중 네트워크 스택을 다시 켜야 하므로 재현성이 약해짐
    • 검색 경로, 컴파일러 플래그, CGo 관련 설정을 다시 조정해야 함
  • Nix의 dockerTools.buildLayeredImage는 패키지를 컨테이너에 넣는 세부 사항을 대신 처리해 설정을 단순하게 만듦

과거 시점 재현과 Nix 캐시

  • Nix의 큰 장점 중 하나는 과거 시점의 소프트웨어를 정확히 다시 빌드할 수 있다는 점임
    • 나중에 특정 고객 환경의 버그를 재현해야 할 때 같은 Docker 이미지를 다시 만들 수 있음
    • 패키지 빌드는 소프트웨어와 의존성 전체 상태를 한 시점으로 고정하는 역할을 함
  • XeDN은 몇 년간 작업해 온 프로젝트이며, 14개월 전 버전은 다음 명령으로 빌드할 수 있음
nix build github:Xe/x/567fdc2#xedn-docker
  • 이 명령은 GitHub repo Xe/x의 특정 커밋에서 xedn-docker 대상을 빌드함
    • Docker 데몬에 로드하면 당시와 같은 바이트의 이미지가 만들어짐
    • 예시에는 Go 1.19도 포함됨
  • 일반 Docker 빌드만으로 같은 재현을 하려면 많은 저장 비용이 필요할 수 있음
  • Nix 캐시는 Nix 명령의 출력을 저장해 나중에 다시 빌드하지 않도록 함
    • 개발자 노트북마다 nokogiri 같은 패키지를 새로 빌드하지 않아도 됨
    • 이미 클라우드에서 빌드된 결과를 받을 수 있음
  • Garnix는 모든 flakes 프로젝트의 CI에 쓰이며, 커밋마다 빌드 상태를 보고함
    • 홈랩 머신 설정도 Garnix로 빌드해, 매일 저녁 업데이트할 때 자체 빌드 대신 Garnix 캐시에서 최신 설정을 가져옴

배포 산출물로서의 결론

  • Nix는 Docker 이미지 빌더로서 Docker의 이미지 빌더보다 낫다고 볼 수 있음
  • Nix는 결과를 지정하게 만들고, 그 결과에 도달하기 위한 세부 절차를 직접 나열하지 않게 함
  • 이미 Docker를 쓰는 환경에서는 Docker 이미지 빌드부터 Nix를 적용하는 방식이 현실적인 도입 경로가 됨
  • Nix로 만든 Docker 이미지는 모노레포의 여러 부분 사이에서 레이어를 공유할 수 있음
  • 바이너리 캐시 덕분에 과거에 이미 빌드된 코드를 다시 빌드하지 않아도 됨
  • 최종 산출물은 AWS, Google Cloud, Fly.io 같은 플랫폼에 배포 가능한 일반 컨테이너 이미지임

댓글과 토론

Hacker News 의견들
  • Nix를 좋아하려고 여러 번 시도했지만 이제는 포기해야겠다는 느낌임
    Nix를 돌리는 시스템이 2대 있는데 건드리기가 무섭고, 예전에 둘 다 망가져서 처음부터 재설치한 적도 있음. 이론상 Nix는 멱등적이고 결정적이지만, 실제로는 무엇에 대해 결정적인가를 이해해야 하고, 의존하는 모든 부분의 동작을 깊이 알지 못하면 이상한 결과와 기괴하고 도움이 안 되는 오류, 혹은 아무 피드백도 없는 상황을 맞게 됨
    문서는 완전하고 기술적으로 맞더라도 끔찍하게 난해하고, 튜토리얼은 80% 경로까지만 데려다줌. 그 밖으로 나가면 직접 만들어야 하는데, 그건 수년의 경험 지식과 좌절을 요구해서 더 이상 감당하고 싶지 않음. Docker의 장점은 오히려 혼돈 자체라서 셸과 배포판 패키지 관리자만 대충 알아도 거의 뭐든 만들 수 있고, 문제가 생기면 수십 년 된 도구들 덕분에 진단과 수정이 훨씬 쉬움
    Nix는 Emacs처럼 인내심과 깊고 비전적인 지식만 있으면 뭐든 할 수 있지만, 완전히 빠져들거나 거리를 두고 지켜보는 수밖에 없어 보임

    • 같은 길을 겪었고, 결정적 빌드는 좋아하지만 Dockerfile은 평균 개발자에게 결정적으로 보이는 것이 가장 큰 문제라고 봄
      같은 머신에서 연속으로 두 번 빌드하면 같은 결과가 나오지만, 시간이 지나 패키지 관리자와 베이스 이미지 태그가 갱신되면 한 달 뒤 재빌드 결과가 완전히 달라짐. 팀이 관리하는 컨테이너가 40개쯤 되면 컨테이너 고치는 일이 업무의 큰 부분이 됨
      그래서 이론상 Nix가 완벽해야 하지만, 너무 달라서 벤더 도구는 Nix에서 안 돌아가고 오류가 나도 웹에서 빠르게 해결책을 찾기 어려움. 그 답답함 때문에 https://www.stablebuild.com을 만들었고, Docker 기반 결정적 빌드를 Ubuntu, Debian, Alpine 컨테이너 위에서 제공함. 불변 Docker Hub 풀스루 캐시, Ubuntu/Debian/Alpine 패키지 저장소의 일일 전체 복사본, 인기 PPA와 PyPI 인덱스 일일 복사본, 임의 파일/URL 불변 캐시로 구성되어 있음
      실무에서는 쓰기 쉽고 디버그하기 쉬우며 소프트웨어 호환성도 넓어서, StableBuild로 옮긴 컨테이너에서는 비결정성 때문에 생긴 문제가 0건이었음
    • 문서가 난해한 정도가 아니라, 특히 새 CLI와 flakes 쪽은 그냥 틀린 경우도 많고 엣지 케이스도 아님
      nix develop이 문서처럼 동작하지 않는 이유와 그렇게 동작하게 만드는 방법을 이해하려고 시간을 보낸 적이 있음. 문서에서는 기본적으로 빌드 시점 환경에 들어간다고 하지만 실제 기본값은 밀폐되어 있지 않고, 명령줄 옵션 이름도 헷갈려서 소스에서 지식을 캐내야 했음
      재현성을 깨는 엣지 케이스도 더 명시적으로 다뤄줬으면 함. 부동소수점 코드는 연산 순서에 민감하고 운영체제 선점으로 상태가 새어 나올 수도 있는데, obvious한 것도 명시하지 않으면 사람들이 스스로 발등을 찍게 됨
    • 그렇게까지 나쁘지는 않지만, 표준 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보다 더 진지한 컴퓨터 과학에 기반하고, 내부 배관은 아직 문제가 있지만 더 나쁘지는 않은데, 사용자-facing 도구와 문서는 주류 채택 수준이 아님
    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인데, 이 글 덕분에 팀 전체가 많이 웃었음
      한동안 이걸 만들어왔고, 더 낫게 만들기 위해 우선순위에 둘 만한 것이 있으면 듣고 싶음
    • 개발자 도구가 성공하려면 가장 흔한 작업에 대해 엔지니어가 도구를 잘못 쓰는 방법이 최소 세 가지는 있어도, 비명시적 기술 부채를 남긴 채 “완료”는 할 수 있어야 한다고 봄
      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는 그렇게 하지 않아서 선택지는 두 가지임
      첫째, 교차 컴파일을 하는 방법이지만 glibc까지 교차 컴파일 가능해야 하고, 커뮤니티 의존성 대부분은 되더라도 일부 패키지는 작성자가 교차 컴파일을 신경 쓰지 않았을 수 있음. 게다가 표준 Nix 캐시를 채우는 Hydra는 교차 컴파일 빌드를 하지 않아서 길게 빌드하다 실패할 수도 있음
      둘째, 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를 쓰는데 그럭저럭 잘 됨
      버그가 한두 개 있지만 주로 볼륨 관련으로 보이고, Docker 이미지 빌드는 괜찮게 처리함. 더 거친 부분은 속도인데, 하드웨어와 Docker가 Linux VM을 에뮬레이션해야 한다는 사실이 겹쳐서 한 방을 먹임
  • Nix로 Java 애플리케이션용 Docker 이미지를 빌드한 경험은 그다지 좋지 않았음
    gradle2nix가 폐기된 뒤에는 Gradle 기반 Java 애플리케이션을 위한 명확한 대안이 없어 보임. 예전에 친구에게 간단한 Spring Boot 애플리케이션의 가능한 한 작은 Docker 이미지를 만들어보자고 했는데, Nix로 만든 결과물은 Nix 없이 만든 이미지의 두 배 크기였음
    코드는 여기서 볼 수 있음: https://github.com/jossephus/Docker_challenge/blob/main/flak...

    • 이유는 JDK가 두 개 들어가기 때문임
      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를 하나만 포함하도록 수정하고, 그다음 위 방법들로 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 스타트업 두 곳인 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 바이너리 하나를 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줄은 꽤 많은 일을 하고 있음
    • 바이너리 하나와 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....