사내 서버에서 Jenkins CI/CD 구축하기 (Synology NAS + Spring Boot + Windows)

2026. 3. 24. 22:35·DevOps
반응형

시작하게 된 이유

원래 배포 방식이 너무 불편했다.

팀원 3명이 각자 로컬에서 개발하고, 합쳐서 개발 서버에 올려서 확인하는 방식이었는데 배포할 때마다 이 과정을 반복해야 했다.

  1. 로컬에서 빌드
  2. jar 파일 복사
  3. 개발 서버에 원격 접속
  4. jar 파일 직접 붙여넣기
  5. 수동으로 실행

사람이 직접 하다 보니 실수도 생기고, 누가 언제 배포했는지 추적도 안 되고, 무엇보다 매번 번거로웠다. 그래서 내가 Jenkins CI/CD 구축을 제안했고 직접 셋업했다.


환경

항목 내용

프로젝트 프로젝트
Jenkins 버전 2.541.1
Jenkins 서버 포트 8081
Jenkins 설치 경로 C:\Program Files\Jenkins
배포 서버 OS Windows Server
Java Eclipse Adoptium JDK 17.0.16.8-hotspot
빌드 도구 Gradle
Git 저장소 Synology NAS Git Server (192.168.x.xxx)
배포 경로 D:\프로젝트
앱 포트 [앱포트]

전체 네트워크 구조

이 부분을 처음에 생각없이 해서  삽질을 많이 했다.

개발 PC (외부)
  └─ git push → [공인IP]:[포트포워딩포트] (공유기 포트포워딩)
                        ↓
              Synology NAS 192.168.x.xxx:22 (SSH 실제 포트)

Jenkins 서버 (내부망)
  └─ git pull → 192.168.x.xxx:22 (내부니까 직접 SSH 포트로)

핵심은 개발 PC는 외부에서 접근하니까 포트포워딩된 [포트포워딩포트] 포트를 쓰고, Jenkins는 같은 내부망이니까 SSH 기본 포트인 22번을 직접 쓴다는 것. 이걸 처음에 헷갈려서 Jenkins에서도 [포트포워딩포트] 포트로 연결 시도했다가 Connection refused 가 떴었다.


전체 배포 흐름

로컬에서 git push (master 브랜치)
        ↓
Synology NAS Git 저장소에 코드 저장
        ↓
Jenkins Webhook으로 변경 감지
        ↓
[Checkout] SSH Agent로 NAS에서 소스코드 pull
        ↓
[Build] Gradle bootJar로 jar 빌드
        ↓
[Stop] [앱포트] 포트 기존 앱 종료
        ↓
[Deploy] 새 jar 파일 D:\프로젝트 에 복사
        ↓
[Start] javaw로 앱 백그라운드 실행
        ↓
배포 완료 → http://localhost:[앱포트]

Step 1. Synology NAS Git 서버 설정

Git Server 패키지 설치

NAS 관리자 페이지 → 패키지 센터 → Git Server 설치

Git 사용자 계정은 git으로 생성했고, git-shell로 제한해서 SSH 접속은 되지만 일반 쉘 명령어는 못 쓰도록 했다.

Git 저장소 경로:

/volume1/homes/git/프로젝트.git

SSH 키 생성

Jenkins 서버(Windows)에서 키 생성:

# PEM 형식으로 생성 (-m PEM 필수)
ssh-keygen -t rsa -b 4096 -m PEM -C "jenkins@project"

여기서 -m PEM 옵션이 중요하다. 이게 없으면 기본적으로 OPENSSH 형식으로 생성되는데, Jenkins에서 읽을 때 문제가 생긴다. (아래 삽질 파트에서 자세히)

PEM 형식으로 생성하면 키 첫 줄이 이렇게 나온다:

-----BEGIN RSA PRIVATE KEY-----   ← PEM 형식 (정상)
-----BEGIN OPENSSH PRIVATE KEY--- ← OPENSSH 형식 (Jenkins에서 문제)

NAS에 공개키 등록

NAS 터미널에 SSH로 직접 접속해서 등록한다:

# NAS에 관리자 계정으로 SSH 접속
ssh -p 22 [관리자계정]@192.168.x.xxx
sudo -i

# git 사용자 .ssh 폴더에 공개키 등록
nano /volume1/homes/git/.ssh/authorized_keys
# → 공개키(id_rsa.pub) 내용 전체 붙여넣기

# 권한 설정 (이거 안 하면 SSH 인증 안 됨)
chown git:users /volume1/homes/git/.ssh/authorized_keys
chmod 600 /volume1/homes/git/.ssh/authorized_keys

등록 후 테스트:

ssh -p 22 git@192.168.x.xxx
# 아래처럼 나오면 성공
# fatal: Interactive git shell is not enabled.
# (git 사용자는 shell이 제한되어 있어서 이 메시지가 정상)

Step 2. Jenkins 설치 및 설정

설치

공식 사이트에서 jenkins.msi 다운로드 후 설치.

설치 옵션:

  • 포트: 8081 (8080은 다른 서비스가 사용 중이었음)
  • 서비스 계정: LocalSystem (권한 문제 최소화)

초기 비밀번호 위치:

C:\ProgramData\Jenkins\.jenkins\secrets\initialAdminPassword

플러그인은 Install suggested plugins로 기본 설치. 이후에 SSH Agent Plugin 추가 설치 필요.

Tools 설정

Jenkins 관리 → Tools

JDK 설정:

Name: jdk-17
JAVA_HOME: C:\Program Files\Eclipse Adoptium\jdk-17.0.16.8-hotspot
Install automatically: 체크 해제 (이미 설치됨)

Gradle 설정:

Name: Gradle-8
Install automatically: 체크
Version: Gradle 9.3.0

Security 설정 (중요)

Jenkins 관리 → Security → Git Host Key Verification Configuration

Host Key Verification Strategy: Accept first connection

기본값인 "Known hosts file"로 두면 NAS 서버 키가 등록 안 되어 있어서 아래 에러가 난다:

No ED25519 host key is known for 192.168.x.xxx and you have requested strict checking.
Host key verification failed.

Accept first connection으로 바꾸면 첫 접속 시 자동으로 키를 수락한다.

Credential 등록

Jenkins 관리 → Credentials → System → Global credentials → Add

Kind: SSH Username with private key
ID: synology-git-ssh
Username: git
Private Key: Enter directly
  → id_rsa (개인키) 전체 내용 붙여넣기
  → -----BEGIN RSA PRIVATE KEY----- 부터 끝까지 전부
Passphrase: (비워둠)

SSH Agent Plugin 설치

Jenkins 관리 → Plugins → Available plugins → "SSH Agent" 검색 후 설치

이게 없으면 Pipeline에서 sshagent 사용 시 아래 에러가 난다:

No such DSL method 'sshagent' found among steps

설치 후 Jenkins 재시작:

Restart-Service Jenkins

Step 3. Pipeline 구성

Job 생성

Jenkins 대시보드 → 새로운 Item → Pipeline 선택

이름: 프로젝트-CICD

Jenkinsfile (최종 버전)

pipeline {
    agent any

    environment {
        APP_NAME = '프로젝트'
        JAR_PATH = 'build/libs'
        DEPLOY_DIR = 'D:\\프로젝트'
        APP_PORT = '[앱포트]'
    }

    tools {
        jdk 'jdk-17'
        gradle 'Gradle-8'
    }

    stages {
        stage('Checkout') {
            steps {
                echo '=== Git 코드 가져오기 ==='
                script {
                    sshagent(['synology-git-ssh']) {
                        bat """
                            git clone -b master ssh://git@192.168.x.xxx:22/volume1/homes/git/프로젝트.git . || git pull
                        """
                    }
                }
            }
        }

        stage('Build') {
            steps {
                echo '=== Gradle 빌드 시작 ==='
                bat 'gradle clean bootJar'
            }
        }

        stage('Stop Old Application') {
            steps {
                echo '=== 기존 애플리케이션 종료 ==='
                script {
                    bat """
                        for /f "tokens=5" %%a in ('netstat -ano ^| findstr :${APP_PORT}') do (
                            taskkill /F /PID %%a 2>nul
                        )
                    """
                }
            }
        }

        stage('Deploy') {
            steps {
                echo '=== 새 버전 배포 ==='
                bat """
                    if not exist ${DEPLOY_DIR} mkdir ${DEPLOY_DIR}
                    copy /Y ${JAR_PATH}\\*.jar ${DEPLOY_DIR}\\${APP_NAME}.jar
                """
            }
        }

        stage('Start Application') {
            steps {
                echo '=== 애플리케이션 시작 ==='
                bat """
                    cd ${DEPLOY_DIR}
                    start /B javaw -jar ${APP_NAME}.jar
                """
            }
        }
    }

    post {
        success {
            echo '✅ 배포 성공!'
        }
        failure {
            echo '❌ 배포 실패! 로그를 확인하세요.'
        }
    }
}

삽질 포인트 정리

1. 포트 혼동 (Connection refused)

처음에 Jenkins Pipeline URL을 이렇게 설정했다:

ssh://git@192.168.x.xxx:[포트포워딩포트]/...  ← 틀림

[포트포워딩포트]는 외부에서 접근할 때 쓰는 포트포워딩된 포트고, 내부망에서는 SSH 기본 포트 22를 직접 쓰면 된다.

ssh://git@192.168.x.xxx:22/...  ← 맞음

2. SSH 키 형식 문제 (error in libcrypto)

처음에 키를 이렇게 생성했다:

ssh-keygen -t rsa -b 4096 -C "jenkins@project"

그러면 OPENSSH 형식으로 생성되는데, Jenkins에서 이 키를 임시 파일로 저장하는 과정에서 아래 에러가 난다:

Load key "...\jenkins-gitclient-ssh[random].key": error in libcrypto
Permission denied (publickey,password).

Windows에서 직접 SSH 명령어 치면 잘 되는데 Jenkins에서만 안 되는 이유가 이것 때문이었다.

해결은 키를 PEM 형식으로 다시 생성하는 것:

ssh-keygen -t rsa -b 4096 -m PEM -C "jenkins@project"

PEM 형식은 Java SSH 라이브러리와 호환성이 좋아서 Jenkins에서 안정적으로 작동한다.


3. SSH Agent Plugin 없이 git 스텝 사용 (여전히 libcrypto 에러)

PEM으로 바꿔도 기존 방식으로는 에러가 계속 났다:

// 이 방식은 Jenkins가 키를 임시 파일로 저장해서 libcrypto 에러 발생
git branch: 'master',
    credentialsId: 'synology-git-ssh',
    url: 'ssh://git@192.168.x.xxx:22/...'

Jenkins의 기본 git 스텝은 SSH 키를 임시 파일로 저장하는 방식인데, 이 과정에서 Windows libcrypto 이슈가 생긴다.

SSH Agent Plugin을 쓰면 키를 메모리에 올려서 처리하기 때문에 임시 파일을 안 만든다:

// SSH Agent로 메모리에서 처리 → 에러 없음
sshagent(['synology-git-ssh']) {
    bat "git clone -b master ssh://git@192.168.x.xxx:22/... . || git pull"
}

 


4. Jenkins가 배포한 앱을 죽이는 문제

java -jar 로 앱을 실행하면 Jenkins 파이프라인이 끝날 때 같이 종료된다. Jenkins가 자신이 실행한 자식 프로세스를 정리하기 때문.

그래서 start /B javaw 로 Jenkins 프로세스 트리와 완전히 분리해서 실행한다:

start /B javaw -jar 프로젝트.jar

javaw는 콘솔 창 없이 백그라운드에서 실행되고, start /B로 완전히 분리된 독립 프로세스로 뜬다.


5. NAS 자동 업데이트 후 SSH 타임아웃

어느 날 갑자기 Jenkins 빌드가 실패하기 시작했다. 보니까 NAS에 SSH 연결이 안 되는 것.

Synology NAS가 자동 업데이트되면서 방화벽 규칙이 초기화됐다. Jenkins 서버 IP에서 포트 22로 들어오는 접근이 막혀버린 것.

NAS 제어판 → 보안 → 방화벽에서 규칙 추가:

출발지 IP: 192.168.x.xxx (Jenkins 서버)
포트: 22
동작: 허용

그리고 같은 일 반복 안 되도록 NAS 자동 업데이트를 수동 / 중요 보안 업데이트만 받도록 변경했다.


결과

  • 배포 방식: 원격 접속 후 jar 수동 복붙 → git push 한 번으로 끝
  • 배포 이력: Jenkins에서 누가 언제 어떤 커밋으로 배포했는지 확인 가능
  • 실수 제거: 수동 작업 없어지니까 복붙 실수 같은 것도 없어짐
  • 팀원 편의: 배포에 신경 안 써도 됨

느낀 점

NAS 사내 Git에 Jenkins 연결은  일반적인 GitHub 기준 레퍼런스가 안 맞는 경우가 많았다. SSH 키 형식 이슈는 에러 메시지만 봐서는 원인을 바로 알기 어려웠고, Jenkins가 내부적으로 키를 어떻게 처리하는지까지 파악하고 나서야 해결됐다. 직접 삽질하면서 SSH 인증 흐름이나 네트워크 포트 개념을 훨씬 잘 이해하게 된 작업이었다.

반응형

'DevOps' 카테고리의 다른 글

Docker 기본 개념과 명령어 정리  (3) 2026.03.25
폐쇄망 서버에 Docker 설치하기 (Rocky Linux 8)  (0) 2026.03.24
'DevOps' 카테고리의 다른 글
  • Docker 기본 개념과 명령어 정리
  • 폐쇄망 서버에 Docker 설치하기 (Rocky Linux 8)
fkqlaus
fkqlaus
안녕하세요 Java, Spring boot 공부하는 주니어 개발자입니다
  • fkqlaus
    개발자가 끄적끄적 블로그
    fkqlaus
  • 전체
    오늘
    어제
    • 분류 전체보기 (24)
      • Spring boot (3)
      • 프레임워크 (3)
      • Java (6)
      • DevOps (3)
      • DB (1)
      • CS (1)
      • GIS (1)
      • 알고리즘 문제풀이 (9)
      • 알고리즘 (0)
  • 인기 글

  • 태그

    list
    D2
    SWEA
    코딩테스트
    collection
    docker
    개발자
    완전탐색
    개발
    서버
    데이터베이스
    프로그래머스
    db
    컴퓨터
    cs
    spring
    DevOps
    iterator
    Java
    알고리즘
  • hELLO· Designed By정상우.v4.10.3
fkqlaus
사내 서버에서 Jenkins CI/CD 구축하기 (Synology NAS + Spring Boot + Windows)
상단으로

티스토리툴바