오프라인에서 npm install을???

보안상의 이유로 인터넷에 연결되지 않은 내부 네트워크 호스트에 애플리케이션을 배포해야할 때가 있습니다.

이때는 Node도 이동식 저장 장치에 담아서 로컬 설치를 해야하고, 당연히 인터넷을 통해 패키지를 다운로드할 수 없습니다.

여러분도 잘 아시겠지만, Node는 인스톨러나 의존 패키지를 모두 포함한 아카이브(압축 파일)를 가지고 인터넷에 연결하지 않고 애플리케이션을 설치하는 방법을 공식적으로 제공하지 않습니다(!)

공식 가이드에서도 마치 항상 인터넷이 이용가능한 것 처럼 npm install 혹은 yarn install을 하라고 하는데요.

기본적으로 npm install을 하면 패키지 레지스트리 서버 http://registry.npmjs.org/에 연결해서 패키지를 다운로드하는 방식이기 때문에 인터넷이 없으면 안 됩니다.

현재 Node의 방식은 실행 환경에서도 패키지 매니저(npm, yarn) 설치를 강요하고 있습니다.

그러니까… 이것이 항상 드는 의문이었습니다.

왜 프로덕션 환경까지 가서도 npm install을 해야하는가!!!

어쨌든, 오프라인에 배포할 적절한 방법을 찾아야할 필요가 생겼습니다.

이를 위해 어떤 시도를 해보았는지 살펴보고, 찾아낸 적절한 방법이 무엇인지 공유해드리겠습니다.

삽질1: node_modules 포함해서 통째로 압축해서 배포

가장 먼저 시도해본 것은, node_modules 디렉터리를 포함해서 통째로 압축해서 들고 가는 방법입니다.

하지만…

image

무겁다!

The node_modules problem

현재 진행중인 프로젝트를 예로 들면, node_modules 디렉터리는 전체 파일 사이즈 200MB에 디스크 공간 300MB를 차지할 만큼 매우 무겁습니다.

파일 사이즈와 디스크 공간 사용량이 이렇게 많이 차이가 나는 것은 디렉터리가 많고 파일이 잘게 나누어져 있기 때문입니다.

파일시스템은 블럭 단위로 파일을 저장하는데, 단위 블럭 사이즈보다 작은 파일은 기본 단위 블럭 사이즈 만큼 디스크 공간을 차지합니다.

택시를 타고 1m를 가도 기본 요금을 내야하는 것과 마찬가지입니다.

즉 node_modules를 들고 가는 것은 비효율적입니다.

이건 압축을 하면 조금 나을 수 있지만, 또다른 단점이 더 있습니다.

node_modules는 패키지 매니저에 의존적입니다. npm이나 yarn의 버전에 따라 node_modules의 내용이 조금 차이가 난다고 합니다.

그래서 공식 가이드에서 여차하면 다 지우고 npm install 하라고 그랬던 것입니다.

결론: node_modules를 복사해서 옮기는 방법은 부적절하다.

삽질2: WebPack 번들링

웹팩은 애플리케이션 코드와 모든 의존 패키지를 모아서 하나의 파일로 뭉쳐줍니다. (번들링)

그럼 파일 하나로 만들어서 이것만 복사해서 배포하면 되지 않을까?

결론부터 말하면 안 됩니다.

먼저, 이름부터 조금 이상한 느낌이 듭니다. 웹이 아닌데 왜 팩을 사용해야 할까요.

그럼 웹팩 말고 비슷한 Parcel은? 마찬가지입니다.

우리가 생각하는 파일 한 개로 모아주는 번들링은 브라우저에서 실행되는 웹 애플리케이션 프로젝트에만 유효한 방법입니다.

일반적으로 브라우저용 패키지는 js파일과 브라우저에서 사용할 수 있는 파일로 구성되어 있습니다.

그런데 백엔드에서 실행되는 노드 애플리케이션은 파일 시스템에도 접근해야 하고 OS 서비스도 사용하기 때문에 js파일만 가지고 동작하지 않습니다. 즉 C++로 작성된 네이티브 모듈이 들어갑니다.

당연히 이런 것들을 js파일로 번들링 할 수가 없습니다.

결론: WebPack으로 번들링 안 된다. Parcel도 안 된다.

삽질3: 네이티브 패키징: pkg

pkg

Node실행 환경과 패키지를 모두 모아서 바로 실행할 수 있는(executable) 단 한 개의 파일을 만들어주는 바이너리 컴파일 도구입니다.

윈도우면 윈도우 리눅스면 리눅스, 그리고 CPU 아키텍처가 32비트이든 64비트이든, 인텔이든 arm이든 대상 환경에 맞게 실행 파일을 만들어줍니다.

심지어 Node도 실행 파일에 내장됩니다. Node를 설치하는 수고 마저 줄여주니까 어쩌면 매우 간단한 해결책이 될 수 있습니다.

그런데 Node 12 버전에서부터 이슈가 생겼는데, 이것이 아직 해결되지 않고 있습니다.

Random call stack size exceeded in binaries complied with node v12.2.0 #681

현재 프로젝트가 최신버전인 Node 12를 사용하고 있기 때문에, 아쉽게도 이 방법은 이용할 수 없었습니다.

그리고 이런 이슈가 있다는 것은 사실, 정상적으로 Node를 실행하는 것과 무언가 다르다는 것을 의미합니다.

엄격하게 말하자면 신뢰성이 떨어지는 방법이기도 합니다.

결론: pkg도 못 쓴다.

해결: yarn offline mirror

무스마는 npm 대신 yarn을 씁니다.

배포할 애플리케이션 프로젝트는 yarn의 workspaces 기능을 사용합니다. npm에는 아직 없는 기능입니다.

그래서 yarn을 기준으로 설명하겠습니다. npm에서 맞는 짝을 찾아보시든지, 이참에 yarn으로 바꿔보시지요

아래의 블로그에서 아이디어를 찾아냈습니다.

Running Yarn offline

yarn이 패키지 레지스트리 서버에서 패키지를 가져올 때, node_modules에 들어있는 디렉터리로 가져오는 것이 아니라, tgz 파일로 받아옵니다. 패키지 매니저는 그 파일의 압축을 풀어서 node_modules에 추가합니다.

offline mirror는 다운로드 받은 패키지 아카이브를 압축을 해제하기 전 상태로 보관하는 저장소입니다.

이 offline mirror는 패키지 레지스트리의 미러 역할을 하기 때문에, 인터넷에 연결하는 대신 offline mirror에서 패키지를 받아오도록 할 수 있습니다.

그렇다면 프로젝트 리포지터리에 offline mirror 디렉터리를 만들어놓고, 미리 패키지를 다운로드 한 다음 푸시(체크인)해서 공유를 할 수 있습니다.

이후에는 인터넷에 연결하지 않아도 offline mirror에서 패키지를 읽어서 node_modules를 재구성할 수 있습니다.

이때 offline mirror에 저장된 패키지들은 압축이 되어있기 때문에 node_modules 디렉터리보다 차지하는 용량이 적습니다.

지금 진행 중인 프로젝트를 예를 들면, node_modules가 300MB일 때, offline mirror 디렉터리는 50MB 정도 밖에 안 됩니다.

아무래도 복사하는데 시간도 덜 걸리겠지요?

따라해보기

현재 작업 디렉터리를 프로젝트 경로로 변경합니다.

$ cd <프로젝트 루트 경로>

.yarnrc 파일을 만들어줍니다.

$ cat > .yarnrc
yarn-offline-mirror "./npm_packages"
yarn-offline-mirror-pruning true
^Z

.yarnrc 파일의 내용은 이렇습니다.

yarn-offline-mirror "./npm_packages"
yarn-offline-mirror-pruning true

그리고 기존의 node_modules 디렉터리와 yarn.lock 파일을 삭제합니다.

$ rm -rf node_modules
$ rm yarn.lock

캐시를 삭제합니다.

$ yarn cache clean

다시 패키지를 받아옵니다.

$ yarn install

그러면 ./npm_packages 디렉터리에 *.tgz 파일들이 저장되고 yarn.lock 파일이 새로 생성됩니다.

이 디렉터리와 파일을 프로젝트 저장소에 다 올려놓습니다. (푸시)

오프라인에서 빌드 & 설치 방법

패키지를 설치할 때 이제 인터넷에 연결하지 않고 ./npm_packages로부터 설치할 수 있습니다.

명령어에 --offline 플래그를 사용합니다.

$ yarn install --offline

그리고 빌드를 합니다. (타입스크립트 프로젝트나 혹은 커스텀 빌드 과정)

빌드가 끝나면 --production 플래그를 붙여서 install을 한 번 더 합니다.

$ yarn install --offline --production

--production 플래그를 붙이면, package.json에서 dependencies에 나열된 패키지만 남기고 나머지는 정리됩니다.

즉, ./npm_packages가 더 가벼워집니다. (제 경우 25MB 정도로 줄었네요.)

이제 빌드 결과물(예를 들어 /dist 디렉터리)과 package.json 파일, npm_packages 디렉터리만 대상 환경으로 복사합니다.

그리고 마지막으로 install을 한 번 더 하면 배포가 끝납니다.

$ yarn install --offline --production

요약: 인터넷 가능할 때 미리 패키지를 받아서 offline mirror 디렉터리에 저장해놓고, 이후에는 offline 모드로 진행한다.

보너스: yarn.js

yarn install을 하려면 당연히 yarn이 설치되어 있어야합니다.

그런데 오프라인 환경에서 node도 겨우겨우 설치했는데, yarn까지 설치하라고 하면 정말 귀찮습니다. 가뜩이나 인터넷도 안 되는데 말이지요.

그런데 말입니다. Yarn Releases

yarn은 node로 바로 실행할 수 있는 js파일로도 제공됩니다.

최신버전인 yarn-1.17.3.js (4.85MB)를 받아서 프로젝트 저장소에 올려놓습니다.

그럼,

$ yarn install --offline --production

대신에

$ node ./yarn-1.17.3.js install --offline --production

이렇게 하면 됩니다.

보너스: circleci에서 yarn offline mirror 사용시

같은 CI 도구인 jenkins와는 다르게, circleci는 빌드를 할 때마다 새로 컨테이너를 만들고 가상 환경에서 모든 것을 새로 받아서 클린 빌드를 합니다.

그래서 npm 패키지도 다시 받아야 했습니다.

이 과정은 어쩌면 괜한 네트워크 자원 낭비라고 볼 수도 있습니다.

그래서 기존에 circleci 빌드 스크립트에는 이 낭비를 방지하기 위해 캐시를 저장하고 복구하는 구문이 들어갔었는데요.

예를 들면,

- restore_cache:
    keys:
      - yarn-{ { checksum "yarn.lock" } }
- run:
    name: Install Packages
    command: yarn install --cache-folder ~/.cache/yarn
- save_cache:
    key: yarn-{ { checksum "yarn.lock" } }
    paths:
      - ~/.cache/yarn

하지만 오늘 알려드린 offline mirror 기능을 이용하면 캐시 저장/복구 절차를 생략할 수 있습니다.

그냥 이렇게 하면 됩니다.

- run:
    name: Install Packages
    command: yarn install --offline

매우 간단하네요.

정리

  • node_modules를 복사해서 배포하는 것은 비추천
  • webpack(parcel)으로 서버 애플리케이션 번들링하지 말자.
  • pkg도 있지만, 이슈가 있다.
  • yarn offline mirror를 사용하자.

읽어주셔서 감사합니다.


References