[에이전트] 10. 배포 : 단일 바이너리와 폐쇄망 반입 — 구형 CPU에서 안 뜨던 날

폐쇄망 사내 에이전트 10편 - 배포

작성자

카테고리:

* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

(폐쇄망 사내 에이전트 ⑩) 배포

폐쇄망 프로젝트는 배포가 곧 제품이다. 아무리 잘 돌아도 인터넷 없는 PC에 통째로 들고 들어가 설치되지 않으면 없는 물건이다. 에어의 배포는 bun build --compile 단일 바이너리 2종(CLI·서버) + 네이티브 런타임 + 모델 매니페스트 + 웹 UI를 묶어 5개 타깃(윈도우 x64, 리눅스 x64/arm64, macOS x64/arm64)으로 나간다. 이번 편은 그 배포에서 터진 사고 두 건이다.

사고 1: 구형 CPU에서 뜨지도 않는다

어느 PC에서 air.exe --version조차 안 떴다. 모델이 무거워서가 아니다 — 실행 파일이 시작조차 못 한다. 원인 분석이 이 프로젝트에서 제일 하드코어한 실측이다.

commit 5c8961b  (2026-08-15)
fix(dist): x64 바이너리를 bun -baseline 타깃으로 — 구형 CPU에서 뜨지도 않았다

  bun build --compile이 exe 앞머리에 심는 bun 런타임의 기본 x64 빌드가
  AVX2를 전제로 컴파일돼 있었다. 실측: air.exe에 VEX 명령 726,493개
  (ymm 283,256 · zmm 6,940) — 전부 CPU 지원 여부를 보는 분기가 없는
  무조건 실행 코드다.

바이너리를 디스어셈블해 VEX 인코딩 명령을 세어본 것이다. AVX2 없는 구형 사무 PC는 첫 명령에서 죽는다. 해법은 x64 세 타깃을 -baseline(SSE4.2 하한)으로 옮기는 것 — 재빌드 후 세 바이너리 모두 baseline 런타임이 박힌 걸 확인했고, 크기는 오히려 123.5→122.8MB로 줄었다. 재미있는 건 동봉한 llama.cpp 네이티브는 원래 안전했다는 것 — CPU 기능별 빌드 14종을 실행 시점에 점수로 고르는 구조라서다. 병목은 오직 bun 런타임이었다. 같은 커밋에서 추론 스레드 기본값도 4로 못박았다 — 코어가 아주 많은 서버 CPU에서 세그폴트가 난 사고의 재발 방지다. 손잡이만 만들고 기본값을 열어두면 사고는 그대로 재현된다.

사고 2: 반입 절차에 구멍이 있었다

폐쇄망 반입은 절차가 생명이다. 개발 머신에서 --pack으로 의존물을 전부 모으고 → 매체로 옮겨 → 폐쇄망에서 --from으로 오프라인 설치. 그런데 이 수집기(fetch-vendor)의 타깃 맵에 두 타깃이 빠져 있었다.

commit e1e9592  (2026-08-15)
fix(dist): fetch-vendor가 macos-x64·linux-arm64도 덮는다 — 폐쇄망 반입 구멍 마감

  빌드 머신에 캐시가 우연히 있으면 되고, 없으면
  자동 다운로드를 시도하다 폐쇄망에서 멈추는 상태였다.

  이름은 추측하지 않고 실측했다:
  linux-arm64 → bun-linux-aarch64 — 타깃 이름은 arm64인데
  bun 표기는 aarch64다. 이 맵이 "추측하지 않는다"고 못박은
  바로 그 갈림이 실제로 있었다.

코드로 — ‘추측하지 않는다’가 코드에 박힌 모양

반입 수집기의 타깃 맵이다. 주석이 그대로 운영 문서 역할을 한다.

// distribution/fetch-vendor.ts
/** 여기 없는 타깃은 추측하지 않는다. bun의 아키 표기(arm64 vs aarch64)가
 *  캐시 이름과 패키지 이름에서 갈릴 수 있어, 확인하지 않은 이름을 적으면
 *  "받았는데 안 쓰이는" 파일이 된다. */
const BUN_RUNTIME: Record<string, string> = {
    "windows-x64": "bun-windows-x64-baseline",
    "linux-x64":   "bun-linux-x64-baseline",
    "macos-x64":   "bun-darwin-x64-baseline",
    // 주의: 타깃 이름은 linux-arm64인데 bun 표기는 aarch64다 —
    // 이 맵이 "추측하지 않는다"고 못박은 바로 그 갈림이다.
    "linux-arm64": "bun-linux-aarch64",
};

맵에 없는 타깃을 만나면 어떻게 하나? 조용히 건너뛰지 않고, 이름을 확인한 뒤 표에 넣으라고 주석이 지시한다. 실제로 이 규칙 덕에 macos-x64·linux-arm64 누락이 ‘결함’으로 문서에 먼저 잡혔고, 나중에 실측 이름으로 채워졌다. 수집 결과는 파일마다 sha256을 기록해 반입 후 대조한다 — 매체를 거치는 파일은 경로 어디서든 깨질 수 있다는 전제다.

반입 절차의 전체 그림

[개발망]  bun run distribution/fetch-vendor.ts --pack
          → vendor-*.tgz 4종 + sha256 목록
[매체]    USB/반입 시스템으로 이동 (여기서 파일이 깨질 수 있다)
[폐쇄망]  fetch-vendor.ts --from ./vendor
          → 오프라인 설치, sha256 대조
검증:     캐시 삭제 후 재현 + 설치본 해시 = 원본 해시 (67892bb6…)

포인트는 마지막 줄이다. 검증을 개발 머신의 캐시를 지운 상태에서 했다는 것 — 폐쇄망 절차의 적은 ‘내 머신에선 되는데’이고, 그 착각의 근원이 캐시다.

✅ 확인

  • 배포 바이너리의 CPU 하한(SSE4.2/AVX2 등)을 알고 있고, 가장 오래된 현장 PC에서 실행을 확인했다.
  • 오프라인 설치를 캐시 없는 깨끗한 환경에서 끝까지 재현했다.
  • 반입물 전체에 체크섬 목록이 있고 설치 후 대조한다.
  • 아키텍처 이름 매핑(arm64/aarch64, x64/amd64)을 실측으로만 채웠다.

“우연히 되는 상태”가 제일 위험하다 — 개발 머신에선 항상 되니까 구멍이 안 보인다. 그리고 이름 규칙의 디테일: 같은 아키텍처를 한쪽은 arm64, 다른 쪽은 aarch64라 부른다. 맵 주석에 “이름은 추측하지 않는다”고 미리 못박아 뒀는데, 그 경고가 실제 상황이 된 것이다. 검증도 반입답게 했다 — 수집 후 캐시를 지우고 오프라인 설치를 재현, 설치본 sha256이 원본과 동일함을 확인.

폐쇄망 배포 체크리스트

  • 하한 CPU를 정하고 그 아래로 빌드한다 — 개발 머신 기준으로 빌드하면 현장에서 안 뜬다.
  • 의존물 수집은 전 타깃을 명시적으로 — ‘캐시에 있어서 되는’ 상태를 성공으로 치지 않는다.
  • 검증은 깨끗한 환경에서 오프라인 재현 + 체크섬 대조.
  • 성능 손잡이(스레드 등)는 안전한 기본값을 못박고 여는 건 사용자 몫으로.

자주 묻는 질문 (FAQ)

단일 바이너리 배포의 함정이 있나요?

런타임이 바이너리 안에 박히므로, 그 런타임이 어떤 CPU 명령을 전제로 빌드됐는지가 그대로 하한이 됩니다. 이 프로젝트는 AVX2 전제의 기본 빌드 때문에 구형 PC에서 실행조차 안 됐고, baseline(SSE4.2) 타깃으로 바꿔 해결했습니다.

폐쇄망 반입 절차는 어떻게 검증하나요?

개발 머신의 캐시를 지운 상태에서 수집→매체→오프라인 설치를 끝까지 재현하고, 설치 산출물의 체크섬을 원본과 대조합니다. 온라인 환경에서 ‘되는 것처럼 보이는’ 검증은 폐쇄망에서 의미가 없습니다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다