처음 젠킨스를 도입했을 때, 우리는 AWS EC2 t3.medium 서버 한 대에 젠킨스를 설치하고 모든 것을 거기서 해결했습니다. 소스코드를 다운받고, 무거운 자바(Java) 컴파일을 돌리고, 도커 이미지를 구워서 압축하고 푸시하는 이 모든 '막노동'을 젠킨스 서버 혼자서 처리했죠. 파이프라인이 5~10개일 때는 괜찮았습니다. 하지만 회사가 폭풍 성장하여 개발자가 100명이 넘어가고, 출근 직후인 오전 10시에 50개의 빌드가 동시에 트리거(Trigger)되는 순간 재앙이 시작됩니다. CPU 사용률 100%, 메모리(RAM) 풀, 디스크 I/O 병목으로 인해 젠킨스 웹 브라우저 UI 자체가 접속 불가 상태로 하얗게 뻗어버립니다.
서버 한 대로는 더 이상 버틸 수 없는 물리적 한계점(Bottleneck)에 도달한 것입니다.
이번 글에서는 "젠킨스 본체는 우아하게 지휘(Orchestration)만 하고, 실제 무거운 노동은 수십 대의 워커(Worker) 서버들에게 분산시켜 병렬로 처리하는" 엔터프라이즈 확장의 궁극기, 'Jenkins Master-Slave (Controller-Agent) 분산 아키텍처'에 대하여 다뤄보겠습니다.
1. Master(Controller)와 Slave(Agent)의 역할 분담
요즘은 정치적 올바름(PC) 이슈로 Master-Slave 대신 Controller-Agent 라는 명칭을 주로 사용합니다. 이 둘의 역할은 완벽하게 쪼개져야 합니다.
- Master (Controller): 두뇌입니다. 개발자의 브라우저 접속(UI)을 처리하고, Github 웹훅을 받아 파이프라인 스케줄을 짜며, 파이프라인이 잘 도는지 모니터링합니다. 절대로 자기 자신의 디스크에서
npm install이나mvn package같은 무거운 노동을 해서는 안 됩니다! - Slave (Agent): 근육입니다. 마스터의 명령을 받아 실제로 땀을 흘리며 소스코드를 받고 빌드하는 일개미 워커 노드들입니다. CPU와 RAM이 무지막지하게 큰 머신 여러 대를 준비합니다. 윈도우 앱을 빌드하기 위한 Windows Agent, iOS 앱을 굽기 위한 Mac Agent 등 운영체제별로 다양하게 붙일 수 있습니다.
2. 두 가지 연결 방식: SSH vs JNLP (Inbound)
마스터가 에이전트를 부려먹으려면 네트워크 통신 채널이 필요합니다. 실무에서 쓰이는 두 가지 대표적인 프로토콜이 있습니다.
1. SSH 방식 (가장 흔하고 쉬움): 마스터가 에이전트 리눅스 서버의 22번 포트(SSH)로 접속해 들어가서 agent.jar를 몰래 실행시키고 노예 계약을 맺는 방식입니다. 사내망에서 에이전트 서버로 접속이 자유롭게 뚫려있을 때 씁니다.
2. JNLP / Inbound 방식 (방화벽 우회용): 마스터는 AWS 클라우드에 있고, 에이전트 서버는 꽉 막힌 회사 내부망 PC인 경우 마스터가 먼저 뚫고 들어올 수 없습니다. 이럴 때는 내부망 PC(에이전트)가 마스터의 50000번 포트로 "저 일하러 왔습니다!" 하고 먼저 연결(Outbound)을 시도하여 맺어지는 방식입니다.
3. agent { label } 매칭 시스템의 마법
에이전트 10대를 젠킨스 마스터에 연결해 두었다고 가정해 봅시다. 3대는 프론트엔드용(Node.js 설치됨), 3대는 백엔드용(Java 설치됨), 4대는 인공지능용(GPU 탑재됨)입니다. 특정 파이프라인이 아무 에이전트에나 가서 빌드되면 안 되겠죠? 이때 라벨(Label)이라는 개념이 등장합니다.
// [프론트엔드 프로젝트의 Jenkinsfile]
pipeline {
// 젠킨스 마스터에게 외칩니다: "내 빌드는 'frontend-node' 라벨이 붙은 에이전트 머신들 중에서 지금 노는 놈 아무한테나 던져주세요!"
agent {
label 'frontend-node'
}
stages {
stage('Build UI') {
steps {
echo "프론트엔드 전용 고성능 에이전트에서 빌드가 쾌적하게 실행됩니다."
sh 'npm ci && npm run build'
}
}
}
}
이렇게 agent { label '...' }을 지정하면, 젠킨스 마스터는 수십 대의 워커 노드 팜(Farm) 중에서 해당 라벨을 가진 유휴(Idle) 장비를 기가 막히게 찾아내서 빌드 작업을 던져줍니다. 에이전트를 수평(Scale-out)으로 계속 늘려나가기만 하면 파이프라인 1,000개가 동시에 돌아도 마스터는 끄떡없습니다.
4. 마무리 및 주요 발전 방향
지금까지 젠킨스 단일 서버의 비명을 멈추게 하고, 무한한 스케일아웃 확장이 가능한 진정한 의미의 '엔터프라이즈 빌드 공장', Master-Agent 분산 아키텍처에 대해 다루어 보았습니다. 이제 여러분의 CI/CD 인프라는 넷플릭스나 구글 부럽지 않은 대규모 처리 능력을 갖추었습니다.
자, 길고 길었던 젠킨스 완벽 가이드 대장정의 마지막 장이 눈앞에 왔습니다. 인프라도 탄탄하고 파이프라인도 완벽한데... 어느 날 새벽 갑자기 젠킨스가 알 수 없는 이유로 죽어버리거나 디스크가 터지는 등 "인프라 엔지니어의 심장을 철렁하게 만드는 3대장 최악의 장애"가 터지곤 합니다. 젠킨스 가이드의 최종화, 이어지는 대망의 피날레 포스팅에서는 "겪어보지 않으면 절대 모른다! 데브옵스 실무에서 피눈물 흘리며 깨달은 Jenkins 최악의 장애 대처 Best 3와 응급 복구(Troubleshooting) 비법"에 대해 모든 것을 쏟아부어 파헤쳐 보겠습니다!
길고 길었던 대장정의 마지막 장에 오신 것을 환영합니다! 그동안 우리는 도커(Docker) 띄우기부터 파이프라인 문법, 보안, 분산 아키텍처까지 젠킨스의 모든 찬란한 기능들을 완벽히 마스터했습니다. 여러분은 이제 회사에서 "CI/CD 마스터"라는 칭호를 얻었을 것입니다. 하지만 인프라 엔지니어의 진짜 실력은 평화로울 때가 아니라, 모두가 퇴근한 금요일 밤 11시에 시스템이 완전히 먹통이 되었을 때 어떻게 응급 복구를 해내느냐에서 판가름 납니다. 수년간 수백 대의 젠킨스 서버를 운영하며 필자가 피눈물을 흘리며 겪었던 가장 끔찍하고 빈번한 젠킨스 장애 3가지와, 그 장애를 3분 만에 진압하는 '응급 처치(Troubleshooting) 노하우'를 다뤄보겠습니다.
장애 1순위: "No space left on device" (디스크 풀)
증상: 젠킨스 UI 접속 시 HTTP 500 에러 창만 덩그러니 뜨거나, 모든 파이프라인이 시작하자마자 java.io.IOException: No space left on device 에러를 뿜으며 즉사합니다.
원인: 에서 배운 Discard Old Builds를 설정하지 않아 낡은 아티팩트가 가득 차거나, 도커 기반 빌드를 너무 많이 돌려서 호스트 리눅스 서버에 <단말기 없는 찌꺼기 도커 이미지(Dangling Images)>가 수십 기가바이트(GB) 쌓였기 때문입니다.
응급 복구(Troubleshooting): 절대 당황하지 말고 SSH로 서버에 접속합니다. 다음 두 줄의 터미널 명령어를 날리면 5분 안에 디스크 50%가 즉각 확보되고 젠킨스가 숨을 쉬기 시작합니다.
# 1. 젠킨스 Workspace에 남겨진 30일 이상 된 오래된 임시 파일들을 강제로 모조리 찾아 삭제합니다.
find /var/jenkins_home/workspace/ -type f -mtime +30 -exec rm -f {} \;
# 2. 도커가 쓰레기통에 남겨둔 이름 없는(Dangling) 이미지와 캐시를 싹 비웁니다.
docker system prune -a --volumes -f
장애 2순위: java.lang.OutOfMemoryError (OOM 데스매치)
증상: 젠킨스가 미친 듯이 느려지다가 브라우저 무한 로딩이 걸립니다. 로그를 까보면 OutOfMemoryError: Java heap space라는 섬뜩한 문구가 찍혀있고 젠킨스 프로세스 자체가 리눅스 OS에 의해 강제 킬(Kill) 당해 있습니다.
원인: 젠킨스는 기본적으로 자바(Java) 애플리케이션입니다. 1만 줄이 넘어가는 엄청난 빌드 로그 텍스트를 파이프라인 UI에 뿌려주려고 힙(Heap) 메모리에 전부 밀어 넣거나, 수백 개의 git fetch가 동시에 발생하면 메모리가 폭주합니다.
응급 복구(Troubleshooting): 젠킨스 컨테이너를 띄우는 docker-compose.yml 또는 구동 스크립트에 자바 가비지 컬렉터(GC) 튜닝과 메모리 상한선(Xmx) 환경변수를 충분히 설정하고 재시작해야 합니다.
// docker-compose.yml 예시
environment:
# 기본 메모리가 모자랍니다. Heap 메모리를 4GB(4096m)로 뻥튀기하고,
# 일시 정지(Stop-the-world) 시간이 짧은 G1GC 가비지 컬렉터를 강제로 주입합니다.
- JAVA_OPTS=-Xmx4096m -Xms2048m -XX:+UseG1GC -XX:+ExplicitGCInvokesConcurrent
장애 3순위: 플러그인 의존성 지옥 (Update Hell)
증상: [플러그인 관리] 메뉴에 뜬 빨간색 "업데이트 가능" 버튼들이 거슬려서 무심코 [모두 업데이트]를 누르고 젠킨스를 재시작했습니다. 그런데 젠킨스가 켜지다 말고 Failed to load plugin: workflow-api 같은 에러를 수십 개 뱉으며 영원히 부팅되지 않습니다.
원인: A 플러그인을 최신 버전으로 올렸는데, 이 녀석이 B 플러그인의 구버전과 호환되지 않아 서로 충돌하며 코어 엔진의 오작동을 유발한 것입니다. 실무 젠킨스 운영의 가장 끔찍한 금기어는 "묻지마 플러그인 업데이트"입니다.
응급 복구(Troubleshooting): UI에 접속이 안 되니 터미널로 뚫고 들어가서 수동으로 찌그러진 플러그인을 뽑아내야 합니다.
# 1. 젠킨스의 플러그인이 모여있는 폴더로 진입
cd /var/jenkins_home/plugins/
# 2. 에러 로그에서 말썽을 일으킨 플러그인(예: workflow-api)의 .jpi (또는 .hpi) 파일을 찾아 강제 백업/삭제
mv workflow-api.jpi workflow-api.jpi.bak
mv workflow-api.jpi.disabled workflow-api.jpi.bak.disabled
# 3. 젠킨스 컨테이너 강제 재시작
docker restart jenkins
이렇게 수동으로 문제의 플러그인을 비활성화(제거)하면 간신히 젠킨스 UI가 되살아납니다. 이후 호환되는 안정화(Stable) 버전의 플러그인을 찾아 수동으로 .hpi 파일을 업로드하여 설치해야 합니다.
에필로그: 진정한 CI/CD 장인으로의 길
축하합니다! 디스크 풀을 정리하고, OOM을 잡고, 플러그인 지옥에서 살아 돌아온 당신은 이제 어떠한 장애에도 굴하지 않는 강철의 멘탈을 가진 진정한 데브옵스/CI-CD 엔지니어입니다.
'CI. CD > Jenkins' 카테고리의 다른 글
| [Jenkins] 파이프라인 나누기(Upstream / Downstream) (0) | 2026.10.15 |
|---|---|
| [Jenkins] Node.js 다중 버전 환경 완벽 통제 가이드 (0) | 2026.10.15 |
| [Jenkins] Trivy로 컨테이너 보안 취약점 차단하기 (0) | 2026.10.14 |
| [Jenkins] Retry 블록 네트워크 장애를 극복하는 파이프라인 (0) | 2026.10.14 |
| [Jenkins] Timeout의 파이프라인 무한 루프 방어 (0) | 2026.10.12 |