시작하게 된 이유
원래 배포 방식이 너무 불편했다.
팀원 3명이 각자 로컬에서 개발하고, 합쳐서 개발 서버에 올려서 확인하는 방식이었는데 배포할 때마다 이 과정을 반복해야 했다.
- 로컬에서 빌드
- jar 파일 복사
- 개발 서버에 원격 접속
- jar 파일 직접 붙여넣기
- 수동으로 실행
사람이 직접 하다 보니 실수도 생기고, 누가 언제 배포했는지 추적도 안 되고, 무엇보다 매번 번거로웠다. 그래서 내가 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 |