
1. 개요
1-1. Freestyle
1-2. Pipeline
2. 설계
2-1. ngrok
2-1-1. 설치 방법
2-2. master-agent 노드 분리
3. 테스트 환경 구성
3-1. 환경
3-2. 세팅
4. 테스트
5. 결론
Jenkins를 이용해 CI/CD를 구축하는 매우 다양한 방식이 있다. 그 중에서 가장 많이 이용되는 것이 Freestyle 그리고 Pipeline의 두가지 방식이다.

먼저 Freestyle 방식의 기본 형태는 아래와 같다.

이름은 'Freestyle'이지만 실제로는 정해진 틀이 있고 해당 형식에 따라 빈칸을 채워가는 방식으로 구성한다.
정해진 포맷이 있기 때문에 비교적 쉽게 프로젝트 생성이 가능하고 처음 배우는 단계에서 진입장벽이 낮다.
빌드나 테스트와 같이 스크립트가 필요한 작업은 Build Steps 단계에서 아래와 같이 스크립트를 작성한다.

하지만 정해진 포맷을 채우는 방식인 만큼 커스터마이징이 제한적이라는 단점이 있다.
예를 들어 Build 후 GitHub 두 repository에 처리해야할 작업이 있다면 Freestyle 프로젝트는 하나의 repository만 연동이 되기 때문에 적합하지 않을 것이다.
또한 위 포맷에서 정의한 작업들이 직렬로 실행된다는 단점이 있다.
예를 들어 A 애플리케이션과 B 애플리케이션이 둘 다 빌드가 되고 나서 각각 실행해주고 C 애플리케이션을 재가동 해야한다고 가정해보자.
Freestyle 방식에서는 A 애플리케이션 빌드 및 실행 스크립트를 실행하고, B 애플리케이션 빌드 및 실행 스크립트를 실행한 후 마지막으로 C 애플리케이션을 재기동하는 순서로 작업이 진행될 것이다.

A 애플리케이션과 B 애플리케이션은 서로 영향을 주지 않고 병렬적으로 실행이 가능한 작업이지만 Freestyle 프로젝트에서는 각각 프로세스가 직렬적으로 실행된다.
이러한 단점을 보완하기 위해 pipeline이 많이 사용된다. Pipeline의 기본 포맷은 아래와 같다.

기본적인 형식은 거의 비슷한 것을 알 수 있다. 가장 큰 차이점은 바로 Freestyle의 Build Steps 항목이 Pipeline으로 바꼈다는 점이다.
Pipeline에서는 빌드 작업을 Groovy DSL (Domain Specific Language) 기반의 Jenkins Pipeline DSL 문법으로 작성한다.
예를 들어 위에서 작성했던 A, B 애플리케이션 병렬 처리 후 C 애플리케이션 재가동 작업은 아래와 같이 작성할 수 있다.
pipeline {
agent none
stages {
stage('parallel pipeline') {
parallel {
stage('build app A') {
steps{
echo "build app A"
echo "start app A"
}
}
stage('build app B') {
steps{
echo "build app B"
echo "start app B"
}
}
}
}
stage('restart app C') {
steps {
echo "restart app C"
}
}
}
}
실행 결과

위 스크립트에서 A, B 애플리케이션의 빌드와 실행 작업은 아래의 그림과 같이 병렬적으로 실행된다.

뿐만 아니라 Pipeline 프로젝트는 스크립트 기반이기 때문에 커스터마이징이 자유롭다는 이점도 존재한다.
이 글에서는 Jenkins Pipeline을 이용한 CI/CD 파이프라인을 설계하고 해당 설계에 맞게 테스트 서버를 구축 해보려고 한다.

GitHub/GitLab의 main branch에 Push 또는 Merge 이벤트가 발생할 때마다 Webhook 요청을 Jenkins에게 보낸다.
Jenkins는 해당 작업을 담당하는 Agent에게 빌드 작업을 실행하도록 명령하고, 각 Agent는 빌드 및 테스트 후 기존 프로세스를 종료하고 새로 빌드한 파일을 실행한다.
여기서 ngrok이라는 프로그램이 사용된다. ngrok은 외부환경에서 로컬 또는 내부망으로 접속할 수 있게 해주는 도구다.
예를 들어 내부망의 192.168.100.1이라는 주소에 Jenkins 서버를 실행해뒀다면, GitHub에서 Jenkins에게 Push 이벤트가 발생했다는 것을 알려줄 수 없다.
ngrok은은 외부에서 접근할 수 있는 특정 도메인에 내부망 주소를 연결함으로써, 외부에서 SSL이 적용된 안전한 도메인으로 접근할 수 있게 해준다.
ngrok 대시보드로 접속한다. (회원가입이 아직 안되어 있다면 회원가입을 진행해준다)
Setup & Installation탭으로 이동해서 해당하는 OS를 선택한다.

아래와 같이 패키지 관리 도구를 사용해서 설치하는 법과 압축 파일을 다운로드 받아서 실행하는 법에 대해 안내되어있다.

가린 부분은 사용자의 개인 auth-token이다.
압축파일을 직접 다운로드 받아서 사용하려면 다운로드 탭을 클릭하여 다운로드 후 아래의 명령어로 실행할 수 있다.
# 압축파일 풀기
> sudo tar -xvzf ./ngrok-v3-stable-linux-amd64.tgz -C /usr/local/bin
# auth token 추가하여 환경설정
> ngrok config add-authtoken your-authtoken
# ngrok으로 8080포트 온라인 오픈
> ngrok http http://localhost:8080
설계도 이미지를 보면 ngrok가 설치되어 있는 Jenkins 서버가 있고, Agent-node들이 있는 WAS 서버들이 있다.
이 WAS 서버들은 실제로 Jenkins가 설치되어 있는건 아니고 Jenkins 서버에게 node로 등록이 되어있디.
Jenkins 서버가 모든 작업을 다 하고 각각의 서버에게 빌드된 파일을 전달하면 각 서버가 실행하는 방식은 매우 비효율적이다.
작업이 Jenkins 서버에게만 집중되어 있고 파일을 전달 하는 등 번거로운 작업을 요구한다.
하지만 이처럼 노드를 분리하여 master 노드는 중앙에서 관리하는 역할만 하고 실제 빌드와 테스트 작업은 agent들에게 위탁하면 훨씬 더 효율적인 구조를 만들 수 있다.

이 테스트 서버 구성에서는 Oracle VirtualBox를 사용해서 master node 하나 agent node 하나를 각각 생성하여 사용한다.
master node에는 ngrok과 Jenkins를 설치하고 agent node에는 java와 Git을을 설치해뒀다.



read 권한만 부여해서 token을 생성했다.
Jenkins와 WAS 서버가 이 토큰을 사용해서 repository를 읽을 수 있게 한다.
master node에서 ngrok을 실행시켜 Jenkins 서버를 외부에서 접근할 수 있게 열어준다.
> ngrok http http://localhost:8080

Forwarding 칸에 주소가 나온다.
https://--.ngrok-free.app 으로 접속하면 http://localhost:8080으로 포워딩해준다는 뜻이다.
접속하면 초기에 아래와 같은 화면이 나온다.

Visit Site를 클릭하면 그 이후는 계속해서 포워딩 해준다.

Jenkins 관리 > Nodes 선택

New Node 버튼 클릭

노드명 입력 후 'Permanent Agent' 선택 후 Create 버튼 클릭

세부정보 입력

Number of executors는 동시에 실행할 빌드 갯수를 나타낸다. 일반적으로 CPU 코어 갯수를 입력한다.
Remote root directory는 Jenkins가 해당 노드에 접속해 root directory로 사용할 경로를 나타낸다.
Labels는 여러개의 node들이 있을 때 연관이 있는 node들을 논리적으로 그룹핑하기 위해 사용된다.
Launch method는 어떻게 해당 노드에 접속할지를 나타낸다.
두가지 옵션이 있는데, 주로 Linux에서는 2번 옵션이 사용된다. agent 노드의 주소와 접속정보를 입력해준다.
정상적으로 생성된 모습

만약 모니터에 X 표시가 되어있다면

생성한 노드 클릭하여 상세페이지로 접속 > Launch agent 버튼 클릭

에러 로그를 확인 후 조치

새로운 Item 클릭

이름을 입력하고 Pipeline 선택 후 OK

Enable project-based security를 선택하고 Anonymous 사용자에게 Build 권한을 부여하자.
이 권한이 없으면 이후 build 작업이 실패한다.

Build Trigger에 GitLab Push 이벤트 발생 시를 선택하면 webhook URL이 표시된다.
여기서 도메인은 ngrok에서 발급받은 도메인을 사용할 예정이기 때문에 상관없고, 그 뒤의 경로만 이후에 붙여줄 것이다.

Pipeline script를 작성한다.
git 계정 정보를 credentials 관리에서 추가해주고 credentialsId를 사용해서 인증한다.

간단하게 요약하자면
git에서 repository를 clone 하고 JAR파일을 복사해서위 순서대로 진행된다.
pipeline {
agent { label 'test_label' }
tools {
// Install the Maven version configured as "M3" and add it to the path.
maven "M3"
}
environment {
DEPLOY_DIR = "/home/ubuntu/work" // 배포할 디렉토리 설정
JAR_NAME = "demo*.jar" // 빌드된 JAR 파일
TARGET_NAME = "cicd_test.jar" // 빌드 후 서버에 저장할 파일 이름
}
stages {
stage('Build') {
steps {
// credentials 사용하여 인증
git branch: 'main', credentialsId: 'cicdtestuser', url: 'https://gitlab.domain.com/ci_cd_test.git'
// Run Maven on a Unix agent.
sh "mvn -Dmaven.test.failure.ignore=true clean package"
}
}
stage('Deploy to Directory') {
steps {
// 배포 디렉토리 생성
sh "mkdir -p ${DEPLOY_DIR}"
// 빌드된 JAR 파일을 배포 디렉토리로 복사
sh "cp target/${JAR_NAME} ${DEPLOY_DIR}/${TARGET_NAME}"
}
}
stage('Run Application') {
steps {
// 기존 JAR 프로세스를 종료 (이미 실행 중인 경우)
sh """
CURRENT_PID=\$(ps -ef | grep 'java' | grep '${TARGET_NAME}' | awk '{print \$2}')
echo "Found CURRENT_PID: \$CURRENT_PID"
if [ -z "\${CURRENT_PID}" ]; then
echo '> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다.'
else
echo "> sudo kill -9 \${CURRENT_PID}"
sudo kill -9 \${CURRENT_PID}
sleep 10
fi
"""
// JAR 파일 실행
sh "nohup java -jar ${DEPLOY_DIR}/${TARGET_NAME} > ${DEPLOY_DIR}/app.log 2>&1 &"
}
}
}
post {
always {
// 빌드 결과 요약
echo "Pipeline completed."
}
failure {
// 실패 시 로그 확인
echo "Pipeline failed. Please check the logs."
}
}
}
GitLab > 프로젝트 > Setting > Webhook 메뉴로 이동한 후 Add new webhook 버튼을 클릭한다.

URL에 ngrok에서 발급받은 외부 접속용 도메인과 Jenkins에서 Pipeline 생성 시 받은 경로를 입력한다.

main 브랜치에 Push 이벤트가 발생할 때 webhook을 보내도록 설정했다.
Push 이벤트만 체크해둬도 Merge 이벤트가 발생할 때에도 webhook 요청을 보내준다.

생성 완료

Test > Push events 선택해서 테스트 해보자.

실패 예시
Jenkins 프로젝트 생성 시 Anonymous 사용자에게 빌드 권한을 주지 않으면 아래와 같이 Build 권한이 없다는 에러가 뜬다.

성공 예시


CI/CD 테스트용 브랜치를 생성해준다.
Controller에 테스트용 API를 추가한 후 origin으로 push한다.
@RestController
public class MainController {
// ...
@GetMapping("/ci-cd-test")
public String ci_cd_test() {
return "CI CD has been successfully implemented";
}
}
> git add ./src/main/java/com/example/demo/MainController.java
> git commit -m "testing ci cd"
> git push origin test#cicd


GitLab에 접속해 Merge 시킨다.
Build가 바로 시작됐다.

성공!

추가한 API도 정상적으로 응답한다.

이전에 GitHub Actions를 이용한 CI/CD 방법에 대해서도 작성한 적이 있다.
Jenkins Freestyle을 사용할 때는 몰랐는데 Jenkins Pipeline을 사용해보니 GitHub Actions와 유사하면서도 많이 다른 것 같다.
전체적인 느낌으로는 관리할 프로젝트가 많을수록 Jenkins가 적합하고 적을수록 GitHub Actions가 적합해 보인다.
Jenkins는 독립된 서버에서 실행이 되다 보니, 환경설정이나 구성 등 신경 쓸 부분이 더 많다.
그에 반해 GitHub Actions는 파일 하나만 작성하면 바로 적용이 가능하고 GitHub에서 바로 워크플로우를 관리할 수 있다는게 가장 큰 장점인 것 같다.
플러그인 수가 Jenkins가 훨씬 많다고 하는데 GitHub Actions를 사용하면서 플러그인이 없어서 불편했던 경험은 없다.
게다가 GitHub은 내장 클라우드 서버에서 빌드 작업 후 파일을 외부로 보내는 방식으로 구현 되는데 반해, Jenkins는 서버에 SSH로 접속해서 해당 서버에서 작업을 처리하는 방식이다 보니 서버에 부담이 간다는 단점이 있다.
일반적으로는 이게 큰 문제가 안되겠지만, 사이드 프로젝트를 진행하며 AWS 프리티어로 작업을 할 때는 이런 작업 하나하나가 부담이 된다.
구글링 해보니 서버가 뻗을 위험으로 인해 별도의 EC2 인스턴스를 하나 더 생성해서 빌드용으로 사용하는 경우도 있다고 한다.
하지만 다수의 사용자가 다수의 프로젝트를 관리해야하는 상황에서는 UI가 구현된 Jenkins가 더 편할 것 같다.
그리고 master-slave 방식의 구성이 가능하다는 점 또한 다수의 프로젝트를 사용한다면 매우 효율적인 운영 방식인듯하다.