본문 바로가기
CI. CD/Jenkins

[Jenkins] Node.js 다중 버전 환경 완벽 통제 가이드

엔터프라이즈 환경에서 젠킨스(Jenkins) 서버 단 1대로 수십 개의 파이프라인을 운영하다 보면 필연적으로 '버전 충돌(Version Conflict)'이라는 거대한 장벽에 부딪히게 됩니다. 가장 대표적인 생태계가 바로 프론트엔드의 Node.js 입니다.
5년 전에 만들어진 구형 레거시 어드민 프로젝트는 반드시 Node.js v14 환경에서만 빌드되며, 오늘 새로 런칭한 최신 React 프로젝트는 Node.js v20 환경을 요구합니다. 만약 젠킨스 호스트 서버 리눅스에 apt-get install nodejs로 14 버전을 깔아버리면 새 프로젝트가 빌드 에러를 뿜고, 20 버전으로 올려버리면 레거시 프로젝트가 죽어버립니다.

 

이번엔 인프라 엔지니어를 골머리 앓게 만드는 "하나의 젠킨스 서버에서 수십 가지의 Node.js 버전을 충돌 없이 동적으로 스위칭하는 마법의 베스트 프랙티스"에 대하여 다뤄보겠습니다.

1. nvm(Node Version Manager)의 치명적 함정

로컬 PC에서 개발할 때는 nvm을 설치하여 nvm use 14, nvm use 20 처럼 자유롭게 버전을 바꿉니다. 하지만 이를 젠킨스 쉘 스크립트(sh) 안에서 사용하려고 하면 십중팔구 "nvm: command not found" 에러를 맞고 좌절하게 됩니다.

젠킨스의 sh 스텝은 기본적으로 /bin/sh -xe 기반의 비대화형(Non-interactive) 쉘로 동작하기 때문에, 사용자의 .bashrc나 .zshrc에 등록된 nvm 초기화 스크립트를 전혀 읽어오지 못합니다. 억지로 스크립트 안에 nvm을 로드하는 코드를 덕지덕지 바를 수도 있지만, 이는 유지보수 관점에서 최악의 안티 패턴(Anti-pattern)입니다.

 

2. 정석 해결책: Global Tool Configuration (글로벌 툴)

젠킨스는 이러한 다중 버전 관리 문제를 해결하기 위해 전역 도구(Global Tool)라는 아주 훌륭한 시스템을 기본 내장하고 있습니다.

  1. 플러그인 설치: [플러그인 관리]에서 NodeJS Plugin을 검색하여 설치합니다.
  2. 버전 등록: [Jenkins 관리] -> [Global Tool Configuration] 메뉴로 들어가면 NodeJS 섹션이 생겨 있습니다. 여기서 Add NodeJS 버튼을 여러 번 눌러 필요한 버전들을 세팅합니다.
  3. 식별자 명명:
    • Name: Node-14 / Version: NodeJS 14.21.3
    • Name: Node-18 / Version: NodeJS 18.17.1
    • Name: Node-20 / Version: NodeJS 20.10.0

이렇게 등록하고 저장해두면, 젠킨스가 알아서 인터넷에서 해당 버전의 Node.js 바이너리를 다운받아 자체적인 캐시 폴더에 예쁘게 보관해 둡니다.

 

3. tools 블록으로 파이프라인에 주입하기

이제 여러분의 Jenkinsfile 최상단에 tools 블록을 열고, 아까 등록했던 식별자 이름(Name)을 그대로 적어주기만 하면 끝납니다. 젠킨스는 파이프라인이 시작될 때 해당 버전의 Node.js 경로를 시스템 PATH 환경변수 최우선 순위로 밀어 넣어버립니다.

pipeline {
    agent any
    
    //  핵심: Global Tool에 등록된 'Node-18' 환경을 이 파이프라인 전체에 주입합니다.
    tools {
        nodejs 'Node-18' 
    }
    
    stages {
        stage('Check Version') {
            steps {
                // 이 스텝에서 실행되는 node는 무조건 호스트 서버의 버전이 아닌 18 버전이 됩니다.
                sh 'node -v'  // 출력: v18.17.1
                sh 'npm -v'
            }
        }
        stage('Build React App') {
            steps {
                sh 'npm ci'
                sh 'npm run build'
            }
        }
    }
}

 

4. 고급 심화: Docker Agent를 활용한 궁극의 격리

만약 글로벌 툴 세팅조차 귀찮고, 서버에 어떠한 파일도 남기기 싫다면 에서 배웠던 Docker Agent 방식이 최고의 베스트 프랙티스입니다. 각 파이프라인마다 자신에게 딱 맞는 도커 이미지를 입고 빌드한 뒤 사라지면 됩니다.

pipeline {
    //  궁극의 격리: Docker Hub에 있는 공식 Node.js 20 버전 컨테이너 안에서 빌드합니다.
    agent {
        docker {
            image 'node:20.10-alpine'
            // npm 캐시를 유지하기 위한 볼륨 마운트 ( 참고)
            args '-v $HOME/.npm:/root/.npm'
        }
    }
    stages {
        stage('Build') {
            steps {
                sh 'node -v' // 출력: v20.10.x
                sh 'npm ci && npm run build'
            }
        }
    }
}

실무에서는 프론트엔드 프로젝트들이 아주 빠르게 버전을 업데이트하므로, Global Tool 방식보다는 Docker Agent 방식을 압도적으로 선호합니다. 개발자가 Jenkinsfile의 image 'node:20' 숫자 하나만 바꾸면 즉각적으로 환경이 업그레이드되기 때문입니다.

 

5. 마무리 및 주요 발전 방향

지금까지 수많은 언어와 버전이 얽히고설킨 끔찍한 스파게티 서버 환경을 방지하고, 젠킨스의 Global Tool과 Docker Agent를 활용하여 100% 무결점 샌드박스 빌드 환경을 제공하는 Node.js 파이프라인 구축 비법에 대해 다루어 보았습니다. 이제 어떤 프로젝트가 들어와도 버전 충돌 걱정 없이 깔끔하게 빌드할 수 있습니다.

열심히 빌드해서 build/ 폴더 안에 훌륭한 .zip 결과물이나 .jar 파일을 만들어 냈습니다. 그런데 파이프라인이 끝나는 순간 젠킨스 워크스페이스가 싹 지워지면서 이 결과물도 함께 우주로 날아가 버린다면 어떨까요? 나중에 운영 서버 배포가 실패해서 "어제 만든 이전 버전 파일로 급하게 롤백(Rollback)해줘!"라고 요청이 왔을 때 식은땀을 흘리게 될 것입니다. 본 설명에서는 "빌드된 결과물(전리품)은 무조건 안전한 금고에 보관하라!" 과거의 자산을 소중하게 지켜주는 'Artifact(아티팩트) 보관 및 다운로드 기술'에 대해 아주 치밀하게 파헤쳐 보겠습니다!


파이프라인이 성공적으로 동작하여 수백 메가바이트짜리 .jar 파일이나 프론트엔드 build.zip 파일이 완성되었습니다. 이 결과물을 운영 서버에 쏘아 보내 배포(Deploy)까지 무사히 마쳤습니다. "아싸, 퇴근이다!" 하고 젠킨스를 닫는 순간, 청천벽력 같은 전화가 걸려옵니다.

"방금 배포한 버전에 치명적인 결제 버그가 있습니다! 당장 1시간 전에 배포했던 어제 버전 파일로 롤백(Rollback)해 주세요!!"

만약 여러분이 파이프라인 최하단에 cleanWs()로 워크스페이스를 싹 비워버렸고, 어제 빌드된 .jar 파일을 그 어디에도 백업해두지 않았다면 어떻게 될까요? 옛날 소스코드를 다시 GIT에서 Checkout 받아 10분 동안 땀을 뻘뻘 흘리며 재빌드해야 합니다. 그 10분 동안 회사는 수백만 원의 매출 손실을 입게 됩니다.

본 글에서는 인프라 엔지니어의 수명과 직결되는 빌드 산출물(Artifact)의 영구 보존 및 다운로드 아카이빙(Archiving) 기법에 대하여 다뤄보겠습니다.

1. 아티팩트(Artifact) 란 무엇인가?

아티팩트는 '인공물', '가공품'이라는 뜻으로, CI/CD 세계에서는 "소스코드를 컴파일하고 패키징하여 탄생한 최종 실행 가능한 결과물 파일"을 의미합니다. 자바의 .war, .jar, 안드로이드의 .apk, iOS의 .ipa, C++의 .exe 등이 모두 아티팩트입니다.

젠킨스는 빌드를 수행할 때 임시 작업 공간(Workspace)을 사용합니다. 파이프라인이 끝나면 다음 빌드를 위해 이 공간을 깨끗하게 청소(Clean)하는 것이 정석입니다. 따라서 이 공간에 생성된 전리품(아티팩트)을 안전한 별도의 금고 창고에 차곡차곡 쌓아두는 명령어가 반드시 필요합니다.


2. archiveArtifacts 지시어의 위력

Declarative Pipeline에서는 파이프라인이 모든 작업을 끝마친 직후인 post 블록에서 아티팩트를 보관하는 것이 원칙입니다. 성공(Success)했을 때만 유의미한 결과물이 나오므로 success 블록 안에 archiveArtifacts 스텝을 선언합니다.

pipeline {
    agent any
    stages {
        stage('Build Spring Boot') {
            steps {
                echo "메이븐(Maven)으로 컴파일을 시작합니다..."
                sh 'mvn clean package -DskipTests'
            }
        }
    }
    
    post {
        // 빌드가 무사히 성공했을 때만 실행됩니다.
        success {
            // target/ 폴더 밑에 생성된 모든 .jar 파일을 젠킨스 내부 스토리지로 복사(아카이브)합니다.
            archiveArtifacts artifacts: 'target/**/*.jar', 
                             allowEmptyArchive: false, // 파일이 없으면 에러 발생!
                             onlyIfSuccessful: true    // 성공한 빌드에서만 보관
        }
        always {
            // 아카이브(복사)가 끝났으므로 안심하고 워크스페이스의 찌꺼기들을 싹 지워버립니다.
            cleanWs()
        }
    }
}

3. 보관된 아티팩트의 활용과 롤백(Rollback) 시나리오

위 스크립트를 통해 보관된 파일들은 젠킨스 웹 대시보드의 해당 빌드 번호 페이지(예: #42) 중앙에 [Artifacts]라는 예쁜 폴더 아이콘과 함께 노출됩니다. 누구나 브라우저에서 클릭 한 번으로 이 .jar 파일을 다운로드(Download) 받을 수 있습니다.

실무 롤백 시나리오:
운영 서버에 V2 버전 배포가 실패했습니다. 인프라 관리자는 젠킨스에 접속해 어제 성공했던 V1 버전 빌드(예: #41) 페이지로 들어갑니다. 화면에 고스란히 남아있는 V1 아티팩트(.jar)를 수동으로 다운받아 서버에 밀어 넣거나, 별도의 롤백 전용 파이프라인을 만들어 "가장 마지막으로 성공한 빌드(Last Successful Build)의 아티팩트를 끌어와서 덮어씌워라"라고 자동화할 수 있습니다. 이것이 진정한 장애 대응(MTTR 최소화)의 정석입니다.


4. 주의사항: 디스크 폭발 방어 (Artifact Rotation)

아티팩트는 보통 수십~수백 MB의 덩치를 자랑합니다. 이를 무한정 보관하면 에서 경고했듯 서버 하드디스크가 터져버립니다. 반드시 파이프라인 상단 options 블록에 artifactNumToKeepStr 설정을 주어, "최근 5개의 아티팩트만 보관하고 오래된 것은 쿨하게 지워라"라는 명령을 병행해야 합니다.


5. 마무리 및 주요 발전 방향

지금까지 휘발성 작업 공간에서 만들어진 황금 같은 결과물을 영구히 보존하고, 최악의 장애 상황에서 버튼 클릭 하나로 이전 버전의 무기를 꺼내 올 수 있게 해주는 'Artifact 아카이빙 기술'에 대해 다루어 보았습니다. 이제 여러분은 장애가 터져도 식은땀을 흘리지 않고 커피를 마시며 여유롭게 롤백 버튼을 누를 수 있습니다.

지금까지 우리는 '단일 파이프라인' 안에서 모든 것을 해결해 왔습니다. 하지만 회사가 커져서 "A 앱이 빌드된 후에, 그 결과물을 받아서 B 앱과 C 앱이 동시에 빌드되고, 둘 다 성공하면 통합 배포 파이프라인을 실행하라"는 거대한 오케스트레이션 요구사항이 생기면 어떻게 해야 할까요? 파일 하나에 수천 줄을 짜야 할까요?

 

다음 글에서는 거대한 파이프라인을 레고 블록처럼 작게 쪼개어 연쇄적으로 호출하는 폭포수 아키텍처, 'Upstream / Downstream Job 연결 마스터 가이드'에 대해 실무 핵심 노하우와 함께 자세히 살펴보겠습니다.