- 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 레이어로 나눌 수 있음
- 코드 한 줄을 바꿨다고
apt나 npm이 의존성을 다시 설치할 필요가 없음
- 실제로 바뀐 최소한의 내용만 이미지 업데이트에 반영할 수 있음
dockerTools는 Nix 패키지와 그 의존성을 Docker 이미지에 넣는 도구 모음임
- Nix로 만드는 Docker 이미지는 크게 두 방식으로 나뉨
- 비레이어 이미지: 프로그램, 의존성, TLS 루트 인증서 같은 추가 항목을 하나의 폴더에 넣고 단일 레이어 이미지로 노출함
- 레이어 이미지: 각 의존성을 별도 이미지 레이어로 배치해 실제 변경된 부분만 업로드할 수 있음
- Docker 내부에도 콘텐츠 기반 저장소가 있지만,
docker build만으로는 이를 충분히 활용하기 어려움
- Nix의 레이어 이미지는 패키지별 레이어를 사용하므로
glibc 같은 의존성은 한 번만 업로드하면 됨
- 단, 해당 라이브러리에 수정이 필요하면 그 레이어는 다시 업로드되어야 함
Go 서비스 예시: Douglas Adams Quotes
- 예시 서비스는 Douglas Adams 인용구를 제공하는 Go 프로그램임
- Go 모듈 프로젝트는
pkgs.buildGoModule로 Nix 패키지를 정의함
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에 빌드된 서버 바이너리를 지정하면 필요한 항목이 함께 복사됨
- 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 같은 플랫폼에 배포 가능한 일반 컨테이너 이미지임