Nix를 사용해 Rust와 BPF 시스템 소프트웨어 개발 환경을 재현 가능하게 만들고, 의존성 변화로 인한 문제를 줄이는 방법을 살펴봅니다.
프로그래밍을 처음 시작했을 때 엄청나게 답답했던 점은 프로젝트를 만드는 데 더 많은 시간을 쓸 수 없고, 대신 주의를 분산시키는 것처럼 보이는 오류와 싸워야 했다는 것입니다. 이런 커다란 시간 낭비는 진전을 막고 _재미있는 일_에 집중하지 못하게 하는 불편함 그 이상으로 보이지 않았습니다.
더 나쁜 것은, 꽤 자주 잘 작동하던 것이 더는 작동하지 않는데 왜 그런지 분명하지 않았다는 점입니다. 당시에는 무슨 일이 일어났을지 가설조차 세울 지식이 없었습니다.
몇 년이 지난 지금, 엔지니어로서 역량이 높아졌음에도 특히 시스템 소프트웨어를 작업할 때 비슷한 감정을 느끼는 경향이 있습니다. 어제 빌드되거나 실행되던 프로젝트가 오늘은 같은 방식으로 작동하지 않습니다. 이제는 그 이유로 가능한 몇 가지를 생각하고, 이론을 검증하는 등의 일을 할 수 있으니 좋습니다. 하지만 이는 종종 큰 시간 투자이며, 더 중요하게는 그다지 즐겁지 않고 제가 하고 싶은 일, 즉 실제로 소프트웨어를 만드는 일에서 시간을 빼앗는 작업입니다. 재미있는 일 말입니다.
대부분의 현대 프로그래밍 언어에는 의존성을 가져오는 데 도움을 주는 패키지 관리자(bundler, cargo, npm 등)가 있으며, 무엇보다 사용자가 알지 못하는 사이 의존성이 변경되지 않도록 최선을 다합니다. 이는 보안에 필수적이고, 여러 머신 간 일관성을 유지하는 데도 도움이 됩니다. 이는 그리 오래전까지 대부분의 사람이 소프트웨어를 만들던 방식에서 크게 개선된 것입니다.
하지만 이것만으로 완벽하지는 않습니다. 다른 변동성의 원인이 있기 때문입니다. 컴파일러나 인터프리터 버전, 시스템이 제공하는 라이브러리, 환경 변수 등이 그렇습니다.
이것으로도 충분하지 않다면, 시스템 수준 소프트웨어를 다룰 때는 네이티브 라이브러리를 사용할 수 있고 대부분의 다른 소프트웨어는 결코 하지 않을 방식으로 운영체제를 활용할 수도 있습니다! 표면적이 넓을수록 의존하는 소프트웨어가 시간이 지남에 따라 동작을 바꿀 가능성도 커집니다.
운영체제 커널은 어떨까요? 추상적인 관점에서 보면, 코드가 인터페이스하는 또 하나의 _라이브러리_입니다. 이는 Linux 커널에서 가장 빠르게 진화하는 부분들로 우리를 이끕니다. 예를 들면 BPF 서브시스템, 멋진 새 sched_ext 스케줄러, io_uring 등이 있습니다.
운영체제 전체를 보면, 평범해 보이는 정기 시스템 업데이트가 컴파일러나 링커 같은 툴체인의 일부를 업데이트할 수 있습니다. 코드가 사용하는 시스템 라이브러리가 변경될 수도 있고, 커널 자체가 바뀔 수도 있습니다! 이로 인해 코드가 빌드되지 않거나, 더 나쁘게는 동작이 바뀔 수 있습니다.
데이터를 확인해 보지는 않았지만, BPF 생태계가 Linux에서 가장 빠르게 진화하는 생태계 중 하나가 아니라면 놀랄 것입니다. 이는 모든 커널 릴리스에 흥미로운 새 기능과 수정 사항이 포함된다는 뜻이므로 훌륭합니다. 하지만 개발 환경과 프로덕션 환경에 복잡함을 더할 수 있습니다. 그뿐 아니라 Rust로 작성된 기본적인 libbpf 기반 애플리케이션의 경우, 다음에 의존합니다.
rustc) 및 관련 도구(cargo)clang). 일반적으로 BPF 코드는 clang으로 컴파일되지만, 대부분의 Linux 시스템은 다른 네이티브 코드에 gcc를 사용합니다.libelf, zlib, libbpf, libc)vmlinux.h), 그리고 빌드 시 생성한다면 시스템의 BTF 정보 및 헤더 파일을 생성할 bpftoolld.so)env)이 목록이 완전하다는 데 내기를 건다면 아마 질 것입니다. 하지만 적어도 우리가 다루는 문제를 보여 주기에는 충분하길 바랍니다.
단 한 번의 apt/dnf/$YOUR_PACKAGE_MANAGER 실행으로 소프트웨어 의존성의 상당 부분이 바뀔 가능성이 있습니다. 기본적으로 완전히 이해할 수 있고 우리가 제어하는 방정식의 일부는 Rust 의존성뿐입니다(SCM에 Cargo.lock이 있다고 가정할 때). 기본적으로 벤더링되므로 libbpf도 아마 그럴 것입니다.
운이 나쁘다면 차이점을 찾아내는 부담은 여러분에게 있습니다. 시스템의 이전 상태가 무엇이었는지 알아내는 일은 대개 매우 어려운 작업입니다.
현대 의존성 관리자 이면의 핵심 아이디어 중 하나는 잠금 파일의 포함입니다. 이는 특정 시점에 “고정된” 의존성의 직렬화된 모습을 나타냅니다. 다른 데이터와 함께 패키지 위치, 정확한 버전, 변조되거나 손상되지 않았음을 보장하는 체크섬을 포함합니다.
위 목록의 모든 항목에 이를 적용할 수 있다면 멋지지 않을까요? 이 아이디어에는 여러 구현이 있으며, 그중 하나가 Nix입니다. Nix는 다음과 같습니다.
여기서는 대부분의 Linux 배포판과 MacOS 같은 다른 운영체제에서 작동하는 패키지 관리자 및 빌드 시스템 부분을 말합니다. 실제로 이를 거의 2년간 사용해 왔지만, 저는 NixOS의 새 사용자입니다. 언어, 개발자 환경, Nix의 내부 작동 방식 등에 관해 저보다 Nix를 훨씬 더 잘 아는 사람들이 온라인에 훌륭한 콘텐츠를 많이 만들어 두었습니다.
단순화한 정신적 모델로, 알아두면 유용한 부분은 “공식” Nix 패키지가 nixpkgs라는 이 저장소에 있다는 점과, Nix용 Cargo.lock 같은 것을 원한다면 아마 flakes를 사용해야 한다는 점입니다. flakes를 사용하면 nixpkgs 저장소의 특정 리비전을 선택할 수 있고, 사용하는 모든 의존성은 그 안에 정의되고 빌드된 내용과 일치합니다.
제가 사용하는 것과 비슷한 Nix flake가 있는 runqslower를 참고하세요. 초보자가 더 쉽게 이해할 수 있도록 충분히 주석을 달았지만, 너무 장황해지지는 않으려 했습니다.
Nix를 설치한 뒤 간단히 nix develop를 실행하면 필요한 모든 소프트웨어가 포함된 환경을 가져옵니다. 가장 좋은 점은 당연히 재현 가능하다는 것입니다. 지금 빌드되고 실행된다면 내일도 실행되고 빌드될 것입니다. 그렇지 않다면 Nix와 Cargo가 제어할 수 없는 다른 환경 변화가 원인임을 알 수 있습니다.
이 모든 것이 좋지만, 지금까지 이 설정이 어떻게 도움이 되었는지 보여 주는 구체적인 사례만 한 것은 없습니다.
Nix를 사용하는 프로젝트의 기여자는 서로 다른 배포판이나 운영체제를 실행하더라도, 거의 깨지지 않는 일관된 개발 환경을 갖습니다.
패키지 관리자와 빌드 시스템은 밀접하게 연관되어 있으므로, 변경 사항을 이분 탐색하는 일은 익숙한 방식보다 조금 더 쉬울 수 있습니다. 실제 사례로, 제 프로젝트 중 하나의 BPF 코드는 clang 19로 컴파일하면 제 커널에서 잘 로드되었지만 clang 21을 사용하면 받아들여지지 않았습니다.
다른 clang/LLVM 버전을 선택하는 코드는 10줄도 되지 않았고, 오래지 않아 BPF용 ISA v3가 기본값이 된 것이 일부 오래된 커널에서 문제를 일으킨다는 사실이 분명해졌습니다.
제가 사용하는 C++로 작성된 프로젝트 중 하나가 프로덕션에서 충돌했지만 Nix에서만 그랬습니다. 이는 다른 소프트웨어 패키저보다 더 엄격한 보안 정책과 기본값으로 -DD_FORTIFY_SOURCE=2를 사용하기 때문입니다. 발견하기 매우 어려웠고 어쩌면 데이터 손상까지 일으켰을 정의되지 않은 동작이, 이 플래그 덕분에 명확한 충돌로 나타났습니다. 이 문제는 업스트림에 공유되었고 수정은 몇 달 전에 배포되었습니다.
익숙하지 않을 수도 있는 빌드 시스템을 패치할 필요가 없습니다. 또한 buck2/bazel을 사용해 본 적이 있다면 규칙과 비슷한 Nix 파생식도 잘 조합됩니다.
제대로 된 빌드 시스템이므로, 다시 빌드해야 하는 것만 다시 빌드됩니다. 이 개념이 익숙하지 않다면, 다른 셀을 참조하는 셀이 있는 좋아하는 스프레드시트 소프트웨어를 생각해 보세요. 다시 계산해야 하는 데이터만 새로 고쳐집니다.
속도의 또 다른 원천은 재현 가능하기 때문에 Nix와 다른 빌드 시스템이 수행할 수 있는 영리한 캐싱입니다. clang/LLVM 이분 탐색 과정에서 수많은 캐시 적중이 있었으므로 수백만 줄의 코드를 다시 빌드하지 않았습니다.
이 글이 조금 길어지고 있으니, 관심이 있다면 향후 글에서 이야기할 수 있을 것 같습니다.
이 설정은 저에게 가장 큰 기쁨을 주는 부분에 집중하도록 도와주었고 대부분 제 방해가 되지 않았습니다. 이것이 바로 빌드 시스템과 패키지 관리자에 바라는 모습입니다.
Nix 생태계와 도구는 방대하며 저는 그중 작은 일부만 알고 있습니다. 하지만 계속 사용하고 NixOS를 배우기를 기대하고 있습니다.
참고: 이 글을 작성하는 데 AI는 사용되지 않았습니다.