지피지기 백전백퇴

(실패) NixOS 원격 빌드


문제의 시작

개인적으로 만들어서 쓰고 있는 GraphQL 서버는 Rust로 작성되었는데, NixOS에서 배포하는 과정에서 해당 Rust 프로젝트를 매번 빌드하느라 배포가 느리다는 문제가 있었다.

  • PC에서 dev 모드로 클린 빌드: 20.53초
  • PC에서 아주 작은 수정이 발생했을 때: 1.54초
  • PC에서 release 모드로 클린 빌드: 37.71초
  • 서버에서 nix build로 빌드: 4분 58.345초

짜잘한 수정 하나 할때마다 5분 가까이 기다려야 하니 미치고 팔짝뛸 지경이라, 어떻게든 이걸 빠르게 만들어보려고 했다.

접근

Nix에서 빌드하는 것이니만큼 incremental build는 꿈도 못꾸고, 단지 PC에서 클린 빌드 하는 수준의 속도가 나오면 좋겠다는 생각이었다.

방법 1: 서버의 binary cache에 소매넣기

처음에 멋도 모르고 생각한 아이디어 중 하나는, PC에서 적당히 aarch64용 크로스 컴파일한 서버 바이너리를 만들어서 서버의 빌드 캐시에 밀어넣으면, 서버 빌드 시점엔 해당 캐시를 알아서 가져다 쓰지 않을까…? 였다.

{ pkg-config, pkgsCross, ... }: pkgsCross.rustPlatform.buildRustPackage {
  pname = "example-server";
  version = "0.1.0";
  src = ./.;
  cargoLock = {
    lockFile = ./Cargo.lock;
  };

  target = "aarch64-unknown-linux-gnu";

  nativeBuildInputs = [
    pkg-config
  ];
  buildInputs = with pkgsCross; [ openssl sqlite ];
  doCheck = false;

  env = {
    CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = "${pkgsCross.stdenv.cc.targetPrefix}cc";
    CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER = "qemu-aarch64";
  };
}

기존 서버 빌드를 위와같은 파일로 리팩토링한 다음, pkgsCross를 제공해주면 되지 않을까…? 란 생각이었다.

하지만 NixOS에서 빌드하는 서버와 PC에서 빌드하는 서버 바이너리의 hash값이 같지 않아서 호환이 안된다는 것을 알게되어 실패.

방법 2: 로컬에서 빌드한 다음 원격 서버에 밀어넣기

로컬에서 전체 서버 바이너리를 모두 빌드한 다음, 서버에 통째로 배포하도록 하면 hash가 달라 고생하는 일은 없지 않을까?

이러한 용도로 만들어진 툴이:

가 있더라마는… 일단 PC는 NixOS를 돌리지 않는데 원격 서버의 정보를 상세하게 (파일시스템, 네트워크 설정 등등) 명시해야 쓸 수 있는 듯 보여 설정하는데 배보다 배꼽이 커지는 느낌이었다. 그래서 실패!

방법 3: PC를 서버에서 쓸 수 있는 빌드머신으로 제공하자

Nix에서는 remote builder를 지정할 수 있기 때문에, PC를 그 remote builder로 제공하면 자동적으로 NixOS를 빌드하는 과정에서 일부 패키지를 PC에서 빌드할 수 있게 될 것이다.

다만… PC는 x86이고 서버는 aarch64이기 때문에 빌드 시점에 host platform이 맞지 않다는 에러와 함께 실패.

이걸 고치려면 PC에서 aarch64 에뮬레이션을 돌려야 한다는 소리인데… 아 그건좀…

또다른 문제 4: 크로스 컴파일은 몹시 느리다

무슨 조화인지 x86 PC에서 aarch64용 크로스 컴파일을 하려고 들면 aarch64용 빌드 시스템을 새로 빌드하는 번거로운 작업이 들어간다. 작은 툴도 아니고 llvm과 gcc 등 아주 커다란 컴파일러 툴을 클린 빌드하는데, 4000개가 넘는 C 파일을 컴파일하는 과정을 보고있으면 울화통이 터져 지켜볼 수가 없을 지경이다.

  • PC에서 aarch64로 크로스 컴파일: 24분 45.84초;;;;;

결론: 그냥 살던대로 살아라

차라리 5분 기다리는게 낫지 이거 고치겠다고 20분 걸리는 aarch64용 크로스 빌드 시스템을 구축하는 양털깎이에 얼마나 시간을 낭비할 생각인가? 그냥 5분 걸리는 서버 빌드를 감수하는게 낫겠다는 결론.

후일담: 아 그냥 scratch 디렉토리를 적용해볼까

글 쓰기 전에는 생각하지 못한 아이디어라 여기 적는다. 이전의 다른글에서 쓴 방법을 응용해서, rust 프로젝트 코드를 rsync로 어딘가에 복사해 넣고 거기서 cargo build를 실행하는 것도 하나의 방법이 될 수 있겠다 싶다. portable한 설정을 만드는 게 포인트니까 rust 프로젝트를 굳이 nix로 빌드하지 않더라도 실행가능한 바이너리만 만들면 될 것 아닌가?

간략한 아이디어:

mkdir -p /srv/rust-server-scratch
rsync -avz ${rust-server-source-from-nix} /srv/rust-server-scratch

cd /srv/rust-server-scratch
cargo build --release

cp target/release/server ${rust-server-build-target}

이렇게 하면 될 것 같은데… 한번 시도해봐야겠다. 글쓰느라 시간을 너무 많이 써서 시도할 시간이 없어…