Network & Server Factory

개인 공부 기록

DevOps/CI & CD

[Jenkins] Jenkins 설치와 사용법 정리

1nfra 2026. 9. 27. 15:02
Jenkins는 코드를 가져와 테스트하고 빌드하는 일을 자동으로 돌려 주는 CI 서버입니다. 이 글에서는 Jenkins 2.568.3을 설치하고 잡 만들기, 빌드와 실패 확인, API로 빌드 실행, 에이전트 연결까지 직접 해 보면서 필요한 개념만 정리합니다.

1. Jenkins가 하는 일

[CI/CD] CI/CD와 GitOps 개념 정리에서 나눈 흐름에서 Jenkins는 CI를 맡습니다. 코드가 바뀌면 저장소에서 코드를 받아 테스트하고, 통과하면 배포할 결과물(이미지나 파일)을 만듭니다. 만든 결과물을 클러스터에 반영하는 CD는 [ArgoCD] ArgoCD 설치와 사용법 정리에서 다룬 ArgoCD가 맡습니다.

 

화면에 계속 나오는 용어는 다섯 가지입니다.

 

용어 뜻
Job Jenkins가 할 일의 정의, 어떤 명령을 어디서 실행할지 적은 설정
Build 잡을 한 번 실행한 결과, #1, #2처럼 번호가 붙고 콘솔 로그가 남음
Node, Agent 빌드를 실행하는 머신, 컨트롤러 안의 Built-In Node와 따로 붙인 에이전트가 있음
Executor 노드에서 빌드 하나를 실행하는 자리, 실행기 수만큼 동시에 빌드가 돎
Label linux, docker처럼 노드에 붙이는 이름, 잡은 라벨로 실행할 노드를 고름

 

Jenkins 본체를 컨트롤러라고 부릅니다. 컨트롤러는 화면과 API로 요청을 받아 빌드를 큐에 넣고, 라벨이 맞는 노드의 빈 실행기에 배정합니다. 잡 설정과 빌드 기록은 모두 컨트롤러의 JENKINS_HOME 디렉터리에 저장됩니다.

[[IMG1]]

컨트롤러는 요청을 받아 빌드를 배정하고 기록을 남기며, 실제 빌드는 에이전트에서 실행합니다

2. 실습 환경

항목 내용
Jenkins 2.568.3 LTS, jenkins.war를 OpenJDK 21로 실행
예제 앱 demo-api, 파이썬 함수 하나와 unittest 테스트 하나
Git 저장소 같은 서버의 bare 저장소, /srv/jenkins-lab2/git/demo-api.git
에이전트 같은 서버에서 다른 OS 사용자(jenkins-agent)로 실행

 

실습 환경은 Jenkins 플러그인 저장소에 접속할 수 없어서 플러그인 없이 Jenkins 기본 기능만 썼습니다. 그래서 저장소는 Git 플러그인 대신 셸의 git clone으로 받았고, Pipeline은 실행하지 않고 마지막에 예시로만 보여 드립니다. 예제 앱의 코드와 테스트는 아래와 같습니다.

# app.py
VERSION = "1.0.0"

def greet(name):
    return f"hello, {name}"

# test_app.py
import unittest
from app import greet

class GreetTest(unittest.TestCase):
    def test_greet(self):
        self.assertEqual(greet("jenkins"), "hello, jenkins")

3. 설치와 첫 설정

3.1 실행

Jenkins 문서에는 Docker 이미지, 리눅스 패키지, WAR 파일 등 여러 설치 방법이 있습니다. 여기서는 가장 단순한 WAR 파일로 실행했습니다. root가 아닌 jenkins 사용자로 실행하고, JENKINS_HOME은 설정과 빌드 기록이 쌓이는 디렉터리입니다.

java -jar jenkins.war --version
# jenkins 사용자로 컨트롤러 실행(설치 마법사 사용)
runuser -u jenkins -- env JENKINS_HOME=/srv/jenkins-lab2/controller \
  java -jar jenkins.war --httpListenAddress=127.0.0.1 --httpPort=8080 \
  > logs/controller.log 2>&1 &
until grep -q "fully up and running" logs/controller.log; do sleep 2; done
# 초기 관리자 비밀번호가 있는 곳(값은 출력하지 않음)
grep -o "Please use the following password.*" logs/controller.log
grep -o "This may also be found at: .*" logs/controller.log
ls -l controller/secrets/initialAdminPassword | awk '{print $1, $3, $NF}'
2.568.3
Please use the following password to proceed to installation:
This may also be found at: /srv/jenkins-lab2/controller/secrets/initialAdminPassword
-rw-r----- jenkins controller/secrets/initialAdminPassword

 

처음 실행하면 로그와 initialAdminPassword 파일에 초기 관리자 비밀번호가 남습니다. 브라우저로 http://127.0.0.1:8080에 접속하면 이 비밀번호를 넣는 화면이 나옵니다.

첫 접속 화면으로, 서버의 initialAdminPassword 파일에 있는 값을 넣어 잠금을 풉니다

3.2 설치 마법사

잠금을 풀면 설치 마법사가 이어집니다. 순서는 아래와 같습니다.

 

단계 할 일
Customize Jenkins 보통 Install suggested plugins를 고름, Git, Pipeline처럼 자주 쓰는 플러그인이 함께 설치됨
Create First Admin User 관리자 계정을 만듦, 이후에는 admin 초기 비밀번호 대신 이 계정으로 로그인
Instance Configuration Jenkins URL 확인, 알림 메일의 링크 등에 쓰이는 주소

 

실습 환경은 인터넷에 연결되어 있지 않아 플러그인 화면 대신 Offline 안내가 나왔고, Skip Plugin Installations로 넘어가 관리자 계정만 만들었습니다.

4. 첫 잡 만들기

4.1 Freestyle 잡 설정

New Item에서 이름을 demo-api로 적고 Freestyle project를 골랐습니다. Freestyle은 화면에서 빌드 단계를 하나씩 추가하는 가장 기본적인 잡입니다. 설정은 두 군데만 했습니다.

 

설정 위치 넣은 값
Build Steps Execute shell 추가, 저장소를 받아 테스트하고 tar.gz 파일을 만드는 명령
Post-build Actions Archive the artifacts 추가, Files to archive에 dist/*.tar.gz

 

rm -rf src dist
git clone -q /srv/jenkins-lab2/git/demo-api.git src
cd src
echo "commit: $(git log -1 --format='%h %s')"
python3 -m unittest -v
mkdir -p ../dist
tar czf ../dist/demo-api-$BUILD_NUMBER.tar.gz app.py

Build Steps의 Execute shell에 넣은 명령으로, 빌드할 때마다 이 셸 스크립트가 순서대로 실행됩니다

 

보통은 Git 플러그인을 설치해서 Source Code Management에 저장소 주소와 브랜치를 넣습니다. 그러면 Jenkins가 빌드 전에 코드를 받아 주고, 어떤 커밋을 빌드했는지도 기록합니다.

4.2 빌드와 콘솔 확인

잡 화면에서 Build Now를 누르면 빌드 #1이 시작됩니다. 결과는 빌드의 Console Output에서 봅니다.

빌드 #1의 콘솔로, 셸 명령이 + 표시와 함께 한 줄씩 찍히고 마지막에 Finished: SUCCESS가 나옵니다

 

콘솔의 [demo-api] $ /bin/sh -xe 줄이 중요합니다. -x는 실행하는 명령을 +로 보여 주고, -e는 명령 하나라도 실패하면 거기서 멈춥니다. 그래서 테스트가 실패하면 뒤의 tar 명령까지 가지 않고 빌드가 실패로 끝납니다.

4.3 테스트가 실패하면

개발자가 인사말을 바꾸고 테스트는 고치지 않은 채 push한 상황을 만들었습니다.

# 개발자가 인사말을 바꾸면서 테스트는 고치지 않고 push
cd /srv/jenkins-lab2/work/demo-api
sed -i 's/return f"hello, {name}"/return f"hi, {name}"/' app.py
git -c user.name=dev-lee -c user.email=dev@example.com commit -qam "demo-api: change greeting"
git push -q origin main
git log --oneline
d8c4314 demo-api: change greeting
e08497b demo-api: first version

 

다시 Build Now를 누르자 빌드 #2는 테스트에서 실패했습니다. 콘솔 뒷부분은 아래와 같습니다.

commit: d8c4314 demo-api: change greeting
+ python3 -m unittest -v
test_greet (test_app.GreetTest.test_greet) ... FAIL

======================================================================
FAIL: test_greet (test_app.GreetTest.test_greet)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/srv/jenkins-lab2/controller/workspace/demo-api/src/test_app.py", line 6, in test_greet
    self.assertEqual(greet("jenkins"), "hello, jenkins")
AssertionError: 'hi, jenkins' != 'hello, jenkins'
- hi, jenkins
?  ^
+ hello, jenkins
?  ^^^^


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
Build step 'Execute shell' marked build as failure
Archiving artifacts
Finished: FAILURE

#2는 빨간색 실패로 표시되고, Last Successful Artifacts에는 성공한 #1의 파일만 남아 있습니다

 

실패한 빌드는 배포 파일을 만들지 않으므로, 내려받을 수 있는 결과물은 항상 테스트를 통과한 빌드에서 나옵니다. CI를 두는 이유가 이것입니다.

5. API로 빌드 실행

매번 버튼을 누를 수는 없으니, 실제로는 다른 시스템이 Jenkins API를 불러 빌드를 시작합니다. [GitHub] 브랜치, Pull Request, Webhook 정리에서 다룬 GitHub Webhook도 결국 Jenkins에 이런 요청을 보내는 방식입니다(보통 GitHub 플러그인이 받아 처리합니다).

 

API를 부를 때는 로그인 비밀번호 대신 API 토큰을 씁니다. 토큰은 오른쪽 위 사용자 메뉴의 Security에서 API Token의 Add new token으로 만듭니다. 용도별로 이름을 붙여 만들고 필요 없으면 지울 수 있어서, 비밀번호를 스크립트에 넣는 것보다 안전합니다.

# 테스트를 바뀐 인사말에 맞게 고쳐서 push
cd /srv/jenkins-lab2/work/demo-api
sed -i 's/"hello, jenkins"/"hi, jenkins"/' test_app.py
git -c user.name=dev-lee -c user.email=dev@example.com commit -qam "test: update greeting"
git push -q origin main
cd /srv/jenkins-lab2

# 스크립트나 다른 시스템에서 빌드 시작: 사용자 이름 + API 토큰으로 POST
J=http://127.0.0.1:8080
TOKEN=$(cat .api_token)   # 사용자 메뉴 > Security > API Token에서 만든 값
curl -s -o /dev/null -w "HTTP %{http_code}\n" -u "admin:$TOKEN" -X POST "$J/job/demo-api/build"
sleep 15
# 마지막 빌드 결과와 아티팩트를 API로 확인
curl -sg -u "admin:$TOKEN" "$J/job/demo-api/lastBuild/api/json?tree=number,result,artifacts[fileName]" \
  | jq -c 'del(._class)'
HTTP 201
{"artifacts":[{"fileName":"demo-api-3.tar.gz"}],"number":3,"result":"SUCCESS"}

 

HTTP 201은 빌드가 큐에 들어갔다는 뜻입니다. 테스트를 고친 커밋으로 빌드 #3이 성공했고 demo-api-3.tar.gz가 만들어졌습니다. 참고로 같은 요청을 토큰이 아닌 비밀번호로 보내면 CSRF 보호에 막힙니다.

J=http://127.0.0.1:8080
# 로그인 비밀번호로 POST하면 CSRF 보호(crumb)에 막힘
curl -s -u "admin:$(cat .admin_pw)" -X POST "$J/job/demo-api/build" \
  | grep -o "Error 403 [^<]*"
# API 토큰으로는 crumb 없이 동작
curl -s -o /dev/null -w "HTTP %{http_code}\n" -u "admin:$(cat .api_token)" -X POST "$J/job/demo-api/build"
Error 403 No valid crumb was included in the request
HTTP 201

6. 에이전트 연결

6.1 빌드를 컨트롤러에서 돌리지 않기

지금까지의 빌드는 Built-In Node, 즉 컨트롤러에서 jenkins 사용자로 돌았습니다. 콘솔의 작업 공간도 /srv/jenkins-lab2/controller/workspace였습니다. 이 사용자는 JENKINS_HOME의 설정과 암호화 키를 모두 읽을 수 있어서, 빌드 스크립트가 컨트롤러의 비밀값에 접근할 수 있습니다.

 

그래서 Jenkins 문서(Controller Isolation)는 Built-In Node의 실행기를 0으로 두고 빌드는 에이전트에서 돌리라고 권합니다. 저도 설치 직후 가장 먼저 이 설정을 바꾸는 것을 권합니다.

6.2 노드 추가

Manage Jenkins > Nodes에서 Built-In Node의 Configure를 열어 Number of executors를 0으로 바꿨습니다. 그다음 New Node로 agent-1을 Permanent Agent로 만들었습니다.

agent-1 설정으로, 실행기 1개, 작업 디렉터리, 라벨 linux를 넣고 Usage는 라벨이 맞는 잡만 받도록 했습니다

 

항목 의미
Number of executors 이 노드에서 동시에 돌 빌드 수
Remote root directory 에이전트의 작업 디렉터리, 잡마다 workspace가 이 아래에 생김
Labels 잡이 이 노드를 고를 때 쓰는 이름
Usage Only build jobs with label expressions matching this node를 고르면 라벨을 지정한 잡만 받음
Launch method Launch agent by connecting it to the controller, 에이전트가 컨트롤러로 먼저 접속하는 방식

 

이 상태에서 빌드를 요청하면 어떻게 되는지 먼저 확인했습니다.

J=http://127.0.0.1:8080
TOKEN=$(cat .api_token)
# built-in 실행기 0, agent-1은 아직 연결 전인 상태에서 빌드 요청
curl -s -o /dev/null -w "HTTP %{http_code}\n" -u "admin:$TOKEN" -X POST "$J/job/demo-api/build"
sleep 15
# 큐에 남은 빌드와 대기 이유
curl -sg -u "admin:$TOKEN" "$J/queue/api/json?tree=items[id,task[name],why]" \
  | jq -r '.items[] | "\(.id) \(.task.name): \(.why)"'
# 대기 중인 빌드 취소
ID=$(curl -sg -u "admin:$TOKEN" "$J/queue/api/json?tree=items[id]" | jq '.items[0].id')
curl -s -o /dev/null -w "HTTP %{http_code}\n" -u "admin:$TOKEN" -X POST "$J/queue/cancelItem?id=$ID"
HTTP 201
4 demo-api: Waiting for next available executor
HTTP 204

 

Built-In Node는 실행기가 0이고, agent-1은 아직 연결 전인 데다 라벨이 맞는 잡만 받기 때문에 빌드가 실패하지도 않고 큐에서 계속 기다립니다. 빌드가 안 시작될 때는 이렇게 큐의 대기 이유부터 보면 됩니다.

6.3 에이전트 실행

노드를 만들면 노드 화면에 에이전트 실행 명령이 나옵니다. agent.jar를 컨트롤러에서 받아, 연결 비밀값과 함께 실행하는 명령입니다. 실습 Jenkins는 에이전트용 TCP 포트가 꺼져 있어서 화면에도 -webSocket 옵션이 붙은 명령이 나왔고, 그대로 실행했습니다.

J=http://127.0.0.1:8080
TOKEN=$(cat .api_token)
install -d -o jenkins-agent -m 700 /srv/jenkins-lab2/agent
cd /srv/jenkins-lab2/agent
# 에이전트 페이지에 나온 대로 agent.jar를 컨트롤러에서 받음
curl -sO $J/jnlpJars/agent.jar
# 연결 비밀값은 에이전트 사용자만 읽는 파일에 저장(출력하지 않음)
curl -s -u "admin:$TOKEN" $J/computer/agent-1/jenkins-agent.jnlp \
  | grep -oE '[0-9a-f]{64}' | install -o jenkins-agent -m 600 /dev/stdin secret-file
# 컨트롤러와 다른 OS 사용자로 에이전트 실행
runuser -u jenkins-agent -- java -jar agent.jar -url $J/ -secret @secret-file \
  -name "agent-1" -webSocket -workDir "/srv/jenkins-lab2/agent" > ../logs/agent.log 2>&1 &
for i in $(seq 60); do grep -q "INFO: Connected" ../logs/agent.log && break; sleep 1; done
grep -E "INFO: (WebSocket connection open|Connected)" ../logs/agent.log
sleep 3
curl -sg -u "admin:$TOKEN" "$J/computer/api/json?tree=computer[displayName,numExecutors,offline]" \
  | jq -c '.computer[] | del(._class)'
INFO: WebSocket connection open
INFO: Connected
{"displayName":"Built-In Node","numExecutors":0,"offline":false}
{"displayName":"agent-1","numExecutors":1,"offline":false}

 

비밀값은 다른 사용자가 ps로 볼 수 있는 명령줄에 넣지 않고 @파일로 넘겼습니다. 화면에도 이 방식의 명령이 함께 나옵니다.

6.4 잡을 에이전트에 배정

잡 설정의 General에서 Restrict where this project can be run을 켜고 Label Expression에 linux를 넣었습니다.

Label Expression에 linux를 넣으면 바로 아래에 이 라벨에 맞는 노드 수가 표시됩니다

J=http://127.0.0.1:8080
TOKEN=$(cat .api_token)
curl -s -o /dev/null -w "HTTP %{http_code}\n" -u "admin:$TOKEN" -X POST "$J/job/demo-api/build"
sleep 15
# 콘솔 앞부분: 어느 노드에서 실행됐는지
curl -s -u "admin:$TOKEN" "$J/job/demo-api/lastBuild/consoleText" | head -4
# 실행은 에이전트에서, 빌드 기록과 로그는 컨트롤러에 저장
ls agent/workspace/demo-api
ls controller/jobs/demo-api/builds/
HTTP 201
Started by user Admin
Running as SYSTEM
Building remotely on agent-1 (linux) in workspace /srv/jenkins-lab2/agent/workspace/demo-api
[demo-api] $ /bin/sh -xe /tmp/jenkins17566891189781421832.sh
dist
src
1
2
3
4
permalinks

 

콘솔에 Building remotely on agent-1이 찍히고 작업 공간이 에이전트 디렉터리로 바뀌었습니다. 빌드 자체는 에이전트에서 돌지만, 빌드 기록과 로그는 여전히 컨트롤러의 builds 디렉터리에 번호별로 저장됩니다.

7. 자주 막히는 지점

에이전트가 꺼지면 어떻게 되는지 확인했습니다.

J=http://127.0.0.1:8080
TOKEN=$(cat .api_token)
# 에이전트 프로세스를 멈춘 뒤 빌드 요청
pkill -u jenkins-agent java; sleep 5
curl -s -o /dev/null -w "HTTP %{http_code}\n" -u "admin:$TOKEN" -X POST "$J/job/demo-api/build"
sleep 8
curl -sg -u "admin:$TOKEN" "$J/queue/api/json?tree=items[task[name],why]" \
  | jq -r '.items[] | "\(.task.name): \(.why)"'
# 에이전트를 다시 실행하면 기다리던 빌드가 이어서 실행됨
cd agent
runuser -u jenkins-agent -- java -jar agent.jar -url $J/ -secret @secret-file \
  -name "agent-1" -webSocket -workDir "/srv/jenkins-lab2/agent" > ../logs/agent.log 2>&1 &
cd ..
sleep 25
curl -sg -u "admin:$TOKEN" "$J/job/demo-api/api/json?tree=builds[number,result]{0,3}" \
  | jq -c '.builds[] | del(._class)'
HTTP 201
demo-api: ‘agent-1’ is offline
{"number":5,"result":"SUCCESS"}
{"number":4,"result":"SUCCESS"}
{"number":3,"result":"SUCCESS"}

 

에이전트가 꺼져 있는 동안 빌드는 실패하지 않고 대기했고, 에이전트를 다시 띄우자 #5가 이어서 실행되어 성공했습니다. 반대로 말하면 에이전트가 죽어도 실패 알림이 오지 않으니, 에이전트 상태와 큐 대기 시간은 따로 지켜봐야 합니다. 실무에서는 에이전트를 systemd 서비스처럼 자동으로 다시 뜨게 해 두는 편을 권합니다.

 

증상 원인 확인과 해결
Waiting for next available executor 빈 실행기가 없음, 라벨 없는 잡인데 에이전트가 라벨 맞는 잡만 받음 잡에 라벨 지정, 노드 실행기 수와 Usage 확인
‘agent-1’ is offline 에이전트 프로세스 중지, 네트워크 끊김 에이전트 로그 확인 후 재실행, 서비스로 등록
There are no nodes with the label ‘docker’ 잡의 라벨식과 맞는 노드가 없음, 오타 포함 잡의 Label Expression과 노드 Labels 비교
셸 단계 중간에 FAILURE sh -xe라 명령 하나가 실패하면 멈춤 콘솔에서 마지막 + 줄과 그 아래 오류 확인
API 호출이 403 No valid crumb 비밀번호로 POST 요청 사용자 이름과 API 토큰으로 요청

8. 다음 단계: Jenkinsfile

Freestyle 잡은 설정이 Jenkins 화면에만 있어서 누가 언제 무엇을 바꿨는지 남지 않습니다. 그래서 실무에서는 빌드 절차를 Jenkinsfile이라는 파일로 저장소에 함께 두는 Pipeline 잡을 주로 씁니다. Jenkinsfile은 코드와 같은 PR에서 리뷰되고 되돌릴 수도 있습니다.

 

이 글의 demo-api 잡을 Jenkinsfile로 옮기면 아래와 같습니다. Pipeline 플러그인이 없는 실습 환경이라 실행하지 않은 예시입니다. Pipeline 잡에서는 Jenkinsfile이 들어 있는 저장소를 Jenkins가 먼저 받아 주므로 git clone 단계가 필요 없습니다.

pipeline {
    agent { label 'linux' }
    stages {
        stage('Test') {
            steps {
                sh 'python3 -m unittest -v'
            }
        }
        stage('Package') {
            steps {
                sh 'mkdir -p dist && tar czf dist/demo-api-$BUILD_NUMBER.tar.gz app.py'
            }
        }
    }
    post {
        success {
            archiveArtifacts artifacts: 'dist/*.tar.gz'
        }
    }
}

 

agent는 6.4의 Restrict where this project can be run, 두 stage는 Execute shell, post의 archiveArtifacts는 Archive the artifacts에 해당합니다. 화면에서 하던 설정이 그대로 코드가 된다고 보면 됩니다.

9. 정리

  • Jenkins는 잡에 적힌 명령을 빌드마다 실행하고, 콘솔 로그와 결과물을 빌드 번호별로 남기는 CI 서버입니다.
  • 빌드는 Built-In Node가 아닌 에이전트에서 돌리고, 잡은 라벨로 노드를 고르며, 빌드가 안 시작되면 큐의 대기 이유부터 봅니다.
  • 다른 시스템에서는 API 토큰으로 빌드를 시작하고, 빌드 절차는 Freestyle보다 저장소의 Jenkinsfile로 관리하는 편이 좋습니다.

 

Jenkins가 만든 결과물을 쿠버네티스에 배포하는 과정은 [ArgoCD] ArgoCD 설치와 사용법 정리에서 이어서 볼 수 있습니다.

참고

728x90
서울
--:--:--
-전체 글
-카테고리
오늘 방문