
프로젝트를 마무리하면서 젠킨스로 Synology NAS DS220+에 백엔드 서버를 배포하는 과정에서 젠킨스를 사용해보았다.
해당 과정에 대해 정리해보려 한다.
해당 과정을 진행하기에 DS220+의 기본 2GB 메모리는 너무 작다. 필자는 추가 메모리 16GB를 증량하여 총 18GB의 메모리로 운용중이다.
시놀로지 NAS의 메모리 증량은 아래 글 참고
'참고 글'
(필자는 다양한 램을 시도하여 보았으나 2R 로 시작하는 램만 성공하였다)
Jenkins란 소프트웨어 개발 시 CI/CD를 제공하는 툴이다.
CI/CD(Continuous Integration/Continuous Deployment)는 소프트웨어의 지속적 통합 / 지속적 배포를 말하는데, 이 과정을 자동적으로 진행해준다.
젠킨스의 특징은 아래와 같다.
GitHub Actions 를 비롯한 많은 CI/CD 툴이 존재하지만, 젠킨스를 선택한 이유는 아래와 같다.
1. 다양한 플러그인 생태계
위와같은 사유로 CI/CD 툴로 Jenkins를 선택하게 되었다.

시놀로지 나스의 Docker 컨테이너를 관리하는 프로그램인 Container Manager가 설정되어있다는 가정 하에 진행한다.
UI를 통한 Jenkins 설치는 아래 글을 참고하자.
UI를 통한 Jenkins 설치
시놀로지에서 지원하는 UI툴을 통해 설치해도 되지만, 편리한 명령어 사용을 위해 Putty를 통해 NAS에 접속한다.
Synology NAS의 제어판 > 터미널 및 SNMP > 터미널의 SSH 서비스 활성화를 통해 접속을 활성화할 수 있다.
docker run -itd --name jenkins -p 9090:8080 jenkins/jenkins:lts
위 명령어를 통해 도커에 Jenkins를 최신 버전으로 설치 한다.
포트 설정은 개인에 맞게 하면 된다.
docker ps
설치 후 위 명령어를 통해 젠킨스가 작동하는 지 확인해준다. 아래처럼 Jenkins 컨테이너가 뜬다면 성공이다.

NAS 내부에서는 Jenkins에 접근이 가능하지만, 외부 환경에서 젠킨스에 접속하려면 포트 설정이 필요하다.
제어판 > 외부 엑세스 > 라우터 구성 > 생성 에서
내장 응용 프로그램 선택 후 'Docker Jenkins' 를 찾아서 활성화 해준다.
(보이지 않으면 사용자 지정 포트에서 설정해줘도 괜찮다)
포트 개방시 아래와 같이 젠킨스 포트가 활성화 된것이 보인다. (보안 사유로 가린 것이 많은 것을 양해 바란다)

추가로 외부 접속까지 허용할 필요가 있다면 공유기 등의 포트 포워딩 설정을 진행한다.
NAS의 외부 접속 IP 혹은 내부 IP를 통해 젠킨스에 접속한다.
나의 경우 내부 접속 IP가 192.168.0.3 인 관계로
192.168.0.3:9090 으로 접속해주면 됐다.
접속하면 젠킨스 초기 비밀번호를 입력하여 Unlock 하라는 페이지가 뜬다.

위에서 접속했던 대로 NAS에 SSH 접속을 통해 접속한 후, 설치한 젠킨스 컨테이너 내부에 접속해야 한다.
아래 명령어를 통해 젠킨스 컨테이너에 접속 후
docker exec -it jenkins /bin/bash
아래 명령어로 initialPassword를 얻는다
cat /var/jenkins_home/secrets/initialAdminPassword
패스워드를 입력하면 플러그인 설치 화면이 뜬다.
좌측의 추천 플러그인 설치를 선택했다.

캡쳐하지 못했으나,
각종 플러그인을 설치하는 화면이 뜨고 (DS220+ 기준 20분 가량 소요되었다), 어드민 계정을 생성하는 화면이 뜬다.
해당 단계를 완료하면 젠킨스 접속이 완료된다.
위 단계로 초기 설정을 마쳤다
젠킨스 메인 화면에서 새로운 작업을 선택 한다.

위 화면에서 아이템 명을 입력한 후 아래에서 Pipeline을 선택한다.

Advanced Project Option
필요 시 작성하도록 한다.
Pipeline
Definition에서 Pipeline script from SCM을 선택한 후 SCM 에 Git을 선택한다.
Repository URL을 마찬가지로 깃허브 레포지토리 URL을 채워넣고 Credentials는 해당되는 경우 작성한다.
Branches to build는 푸시를 감지하고 빌드할 브랜치를 선택한다. 기본은 */master로 되어있는데, 깃허브 정책 변경에 따라 기본 브랜치가 main으로 바뀐지 꽤 지났으므로 */main으로 수정해준다.
마지막으로 Script Path는 JenkinsFile이 위치할 경로를 작성해준다.
필자의 프로젝트는 최상위 경로에 위치할 예정이라 파일명인 Jenkinsfile로만 작성해둔다

위와같이 실행하면 파이프라인 아이템 생성이 완료된다.
각 아이템에서 빌드 및 실행시에 사용할 환경변수를 설정해 줄 필요가 있다.
GitHub에 올린 코드를 가져오기 때문에, Secret 값들은 환경변수 처리후 Jenkins에서 빌드 시에 삽입해주는 것이다.
젠킨스 관리(설정) > Security > Credential 메뉴 진입
아래와 같이 저장소가 보인다.

(global) 부분을 눌러서 진입 후 우 상단의
+Add Credentials 선택
kind에서 Secret text 선택
ID에는 환경변수 명을,
Secret에 값을 넣는다.
필요시 Description을 작성한다.

Create를 누르면 저장 완료된다.
위 과정을 반복해 필요한 환경변수를 모두 작성해준다.
깃허브에서 푸시 될 때마다 젠킨스에 알려주도록 웹 훅 설정이 필요하다.
깃허브 > 해당 레포지토리 > Setting > Webhooks에 접속 후
Payload URL은 외부에서 접속 가능한 Jenkins URL + /github-webhook/
Contents type 은 application/json
나머지는 아래와 같이 선택 후 생성한다

기본적인 설정은 모두 완료되었다.
젠킨스 파이프라인 코드를 작성하는데에는
1) Web UI 를 통해서 직접 스크립트 작성
2) SCM 통해서 Jenkinsfile에 스크립트 작성
등의 방식이 있다고 한다.
우리는 2번 방식을 선택하였기에, 레포지토리에 Jenkinsfile을 작성해줘야한다.
Jenkinsfile의 파이프라인을 작성하는데에는 역시 두가지 방식이 있는데
1) Declarative Pipline
2) Scripted Pipeline
이렇게 두가지 방식이 있다고 한다.
각 방식의 장단점을 간단히 살펴보자면
1. 선언적 파이프라인
장점
2. 스크립트 파이프라이
장점
따라서 필자는 복잡한 워크플로우를 구현할 필요는 없기때문에 선언적 파이프라인으로 구현하였다.
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
#빌드단계를 정의
stage('Build') {
steps {
//
}
}
#테스트 단계를 정의
stage('Test') {
steps {
//
}
}
#배포 단계를 정의
stage('Deploy') {
steps {
//
}
}
}
}
pipeline
Stage
stage블록은 전체 파이프라인에서 개념적으로 구별되는 작업의 하위집합 ('빌드', '테스트', '배포')을 정의하는 블록Step
환경변수 값을 설정해주는 부분의 블럭이다.
서비스 명, 이미지 명, 각종 포트 및 환경변수를 설정해준다.
단순 선언부이다.
environment {
SERVICE_NAME = 'cranebackend_v2'
IMAGE_TAG = "cranebackend_v2:latest"
LOCAL_PORT = "8900"
// ✅ 환경 변수 설정
JWT_SECRET = credentials('JWT_SECRET')
REDIS_HOST = credentials('REDIS_HOST')
REDIS_PORT = credentials('REDIS_PORT')
MYSQL_URL_V2 = credentials('MYSQL_URL_V2')
MYSQL_USER = credentials('MYSQL_USER')
MYSQL_PASSWORD = credentials('MYSQL_PASSWORD')
TZ = "Asia/Seoul"
SLACK_CHANNEL = "build-deploy"
SLACK_SUCCESS_COLOR = "#2C953C"
SLACK_FAIL_COLOR = "#FF3232"
SLACK_MESSAGE_UNIT = "=================================================="
SLACK_DURATION_TIME_MESSAGE = ""
SLACK_MESSAGE_BUILDER = ""
}
//슬랙 알림 부분은 무시하자
github 코드를 가져와 변경사항이 있는지 감지하는 부분이다.
변경사항이 없는 경우 error를 발생시켜 단계를 중단시킨다.
stage('Check Changes') {
steps {
script {
def changes = sh(script: "git diff --name-only HEAD^", returnStdout: true).trim()
if (!changes) {
currentBuild.result = 'NOT_BUILT'
error('No changes in cranebackend_v2 directory, skipping build')
}
echo "✅ cranebackend_v2 변경 사항 감지됨. 빌드를 진행합니다."
}
}
}
레포지토리의 브랜치를 checkout 해오는 스테이지이다.
가장 최근 커밋을 가져오게 되어있다.
stage('Checkout') {
steps {
git branch: 'main',
url: 'https://github.com/CraneWebProject/Crane_web_backend_v2'
script {
def gitCommit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
echo "✅ 현재 빌드하는 커밋: ${gitCommit}"
}
}
post {
failure {
slackSend (
channel: SLACK_CHANNEL,
color: SLACK_FAIL_COLOR,
message: "${SLACK_MESSAGE_BUILDER}" + stageFailSlackMessage("Git Checkout") + "${SLACK_MESSAGE_UNIT}"
)
}
}
}
shell script를 실행시켜 빌드를 진행하는 단계.
빌드 실패 시 슬랙 메시지를 보내는 부분이 있다.
슬랙 세팅을 해주지 않았다면 제외해도 된다.
stage('Build') {
steps {
script{
long startTime = new Date().getTime()
sh '''
chmod +x gradlew
./gradlew clean build -x test
'''
long endTime = new Date().getTime()
SLACK_DURATION_TIME_MESSAGE = getStageDurationMessage(startTime, endTime)
SLACK_MESSAGE_BUILDER += ":white_check_mark: Build 성공! (${SLACK_DURATION_TIME_MESSAGE}) \n"
}
}
post {
success {
echo "✅ Build 단계 완료"
}
failure {
error "❌ Build 단계 실패"
slackSend (
channel: SLACK_CHANNEL,
color: SLACK_FAIL_COLOR,
message: "${SLACK_MESSAGE_BUILDER}" + stageFailSlackMessage("Build") + "${SLACK_MESSAGE_UNIT}"
)
}
}
}
도커 컨테이너를 빌드하고 기존 컨테이너 삭제 후, 새로운 컨테이너를 실행하는 단계이다.
위 단계를 실행하기 위해서는 프로젝트에 알맞는 Dockerfile이 필요하다.
환경변수는 빌드시, 실행시 모두 필요하기때문에 모두 작성해서 넣어준다.
stage('Docker Build & Run') {
steps {
script {
long startTime = new Date().getTime()
sh '''
echo "🔍 현재 REDIS_HOST 값: $REDIS_HOST"
echo "🔍 현재 REDIS_PORT 값: $REDIS_PORT"
docker build -t $IMAGE_TAG \
--build-arg TZ=$TZ \
--build-arg JWT_SECRET=$JWT_SECRET \
--build-arg REDIS_HOST=$REDIS_HOST \
--build-arg REDIS_PORT=$REDIS_PORT \
--build-arg MYSQL_URL_V2=$MYSQL_URL_V2 \
--build-arg MYSQL_USER=$MYSQL_USER \
--build-arg MYSQL_PASSWORD=$MYSQL_PASSWORD \
.
# 기존 컨테이너 종료 후 삭제
docker stop $SERVICE_NAME || true
docker rm $SERVICE_NAME || true
# 새 컨테이너 실행
docker run -d --name $SERVICE_NAME \
-e TZ=$TZ \
-e JWT_SECRET=$JWT_SECRET \
-e REDIS_HOST=$REDIS_HOST \
-e REDIS_PORT=$REDIS_PORT \
-e MYSQL_URL_V2=$MYSQL_URL_V2 \
-e MYSQL_USER=$MYSQL_USER \
-e MYSQL_PASSWORD=$MYSQL_PASSWORD \
-p $LOCAL_PORT:8900 $IMAGE_TAG
'''
long endTime = new Date().getTime()
SLACK_DURATION_TIME_MESSAGE = getStageDurationMessage(startTime, endTime)
SLACK_MESSAGE_BUILDER += ":white_check_mark: Deploy 성공 (${SLACK_DURATION_TIME_MESSAGE}) \n"
SLACK_MESSAGE_BUILDER += ":tada: `${env.JOB_NAME}` 배포 파이프라인이 성공적으로 완료되었습니다. :beer:\n" + "${env.SLACK_MESSAGE_UNIT}"
}
}
}
성공 및 실패 결과를 슬랙 메시지로 전송하는 단계이다.
상술하였듯, 슬랙 세팅을 원하지 않는경우 제외하면 된다.
post {
success {
echo """
===========================================
✅ Pipeline Successfully Completed
Service: ${SERVICE_NAME}
Image: ${IMAGE_TAG}
Port: ${LOCAL_PORT}
===========================================
"""
slackSend (
channel: SLACK_CHANNEL,
color: SLACK_SUCCESS_COLOR,
message: SLACK_MESSAGE_BUILDER
)
}
failure {
echo """
===========================================
❌ Pipeline Failed
Service: ${SERVICE_NAME}
Stage: ${currentBuild.result}
===========================================
"""
slackSend (
channel: SLACK_CHANNEL,
color: SLACK_FAIL_COLOR,
message: "${SLACK_MESSAGE_BUILDER}" + stageFailSlackMessage("Deploy") + "${SLACK_MESSAGE_UNIT}"
)
}
always {
cleanWs()
}
}
// Stage 경과 시간을 계산하여 메시지를 생성하는 함수
def getStageDurationMessage(long startTime, long endTime) {
long durationMillis = endTime - startTime
long durationSeconds = (long) (durationMillis / 1000) % 60
long durationMinutes = (long) (durationMillis / (1000 * 60)) % 60
def durationMessage = ""
if (durationMinutes > 0) {
durationMessage += "${durationMinutes}분 "
}
durationMessage += "${durationSeconds}초"
return durationMessage
}
// 슬랙 실패 메세지를 생성하는 함수
def stageFailSlackMessage(stageName) {
return ":alert: ${stageName} 단계에서 배포가 실패하였습니다. \n"
}
참고를 위해 덧붙인다. 각 프로젝트에 알맞은 Dockerfile을 작성한다.
#dockerfile
FROM openjdk:17
WORKDIR /app
#환경변수 정의
ARG JWT_SECRET
ARG MYSQL_PASSWORD
ARG MYSQL_URL_V2
ARG MYSQL_USER
ARG REDIS_HOST
ARG REDIS_PORT
# 환경변수 설정
ENV JWT_SECRET=${JWT_SECRET}
ENV MYSQL_PASSWORD=${MYSQL_PASSWORD}
ENV MYSQL_URL_V2=${MYSQL_URL_V2}
ENV MYSQL_USER=${MYSQL_USER}
ENV REDIS_HOST=${REDIS_HOST}
ENV REDIS_PORT=${REDIS_PORT}
COPY build/libs/CraneWebBackend_v2-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8900
ENTRYPOINT ["java", "-jar", "app.jar"]
위 과정을 모두 마치면, 설정해둔 레포지토리의 해당 브랜치에 커밋이 될때마다 자동으로 감지하여 빌드가 실행된다.
생성된 아이템들의 빌드 상태는 아래와 같이 젠킨스 메인에서 한눈에 볼 수 있다.

생성된 도커 컨테이너는 SSH 접속을 통해
docker ps 명령어나, 아래와 같이 시놀로지 웹의 Container Manager를 통해 조회할 수 있다.
