백엔드 개발을 공부하면서 API를 만들고 기능을 구현하는 경험은 많이 해봤지만, 배포는 제대로 해본 적이 없었다. 프로젝트를 진행하며 배포 환경 때문에 예상치 못한 문제를 겪기도 했고 실제 서비스가 어떻게 서버에서 실행되는지 궁금하기도 했다. 그래서 이번에는 Jenkins를 이용한 자동 배포 강의를 따라가며 직접 CI/CD 환경을 구축해보았다.
1. Jenkins 서버 구축
Jenkins 서버 생성
먼저, Jenkins를 설치할 서버를 생성한다. 서버 위에 Jenkins를 설치하고 접속한다.
https://github.com/lleellee0/application-deploy-advanced/blob/main/1_0_jenkins_setting_cmd
그다음 서버 ip:8080으로 브라우저에 접속해서 초기 비밀번호를 입력하면 된다.

2. SSH Key 기반 인증 구성
Jenkins가 서버에 자동으로 접속하려면 비밀번호 입력 없이 SSH 인증이 가능해야 한다.
이를 위해 공개키(Public Key)와 개인키(Private Key)를 생성한다.
Jenkins 서버에서 SSH Key 생성
Jenkins 컨테이너에 접속한다.
// 컨테이너 접속
docker exec -it {컨테이너id} /bin/bash
// 키 생성 rsa 알고리즘, 4096bit로
ssh-keygen -t rsa -b 4096
생성 후 공개키를 확인한다.
cat /var/jenkins_home/.ssh/id_rsa.pub
Application Server에 공개키 등록
Application Server에서 다음 파일을 연다. 위에서 확인한 공개키를 추가한다.
vi ~/.ssh/authorized_keys
왜 공개키를 등록해야 하는가?
SSH 공개키 인증 방식은 다음과 같이 동작한다.
Jenkins
├─ Private Key 보유
└─ SSH 접속 시도
Application Server
└─ Public Key 보유
서버는 Jenkins가 제시한 개인키가 자신이 저장한 공개키와 쌍이 맞는지 검증한다.
즉, authorized_keys 파일은 "이 공개키를 가진 사용자의 접속을 허용한다"는 의미이다. 따라서 Jenkins가 비밀번호 없이 SSH 접속할 수 있게 된다.
3. 파이프라인 생성
item을 생성해서 pipeline을 선택해서 만들어주면 된다. Pipeline은 빌드·배포 과정을 코드로 관리할 수 있어서 버전 관리와 재사용이 쉽고 복잡한 CI/CD 흐름을 구성하기 편하기 때문에 사용한다.

그 다음 코드를 작성해주면 된다.

4. Jenkins Credentials 등록
SSH 설정 후에도 빌드가 실패할 수 있다. 대표적으로 다음과 같은 오류가 발생한다.
sshagent step requires a credential
이를 해결하기 위해 Jenkins Credential에 개인키를 등록해야 한다.
Jenkins 개인키 확인
cat /var/jenkins_home/.ssh/id_rsa
전체 내용을 복사한다.
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
Jenkins Credential 생성
Jenkins에서 credential을 설정해줘야한다. 설정 이모티콘을 눌러서 jenkins 관리에 들어오면 Security - Credentials 가 있다.


Domains: Global에 들어가서 Add 버튼을 누르고 SSH Username with private key를 선택하면 된다.

입력해야할 값은 아래와 같다.
ID : deploy_ssh_key
Username : root
Private Key : id_rsa 내용 전체
sshagent 사용
파이프라인에서 등록한 Credential을 사용한다.
sshagent(['deploy_ssh_key']) {
sh "ssh root@$serverIp"
}
이렇게 하면 Jenkins가 해당 개인키를 사용해 SSH 인증을 수행한다.
5. Java 설치
Spring Boot 애플리케이션을 실행하려면 Java가 설치되어 있어야 한다.
sudo dnf install java-17-openjdk
6. 방화벽 설정
Spring Boot는 기본적으로 8080 포트를 사용한다. 외부에서 접근하려면 해당 포트를 열어야 한다.
sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent
sudo firewall-cmd --reload
왜 Application Server는 포트를 열어야 하는가?
Application Server는 외부 사용자의 요청을 받아야 한다.
따라서
Client
↓
8080
↓
Application Server
형태로 접근이 가능해야 한다.
7. SCP를 이용한 파일 전송
Jenkins는 빌드된 JAR 파일을 Application Server로 복사해야 한다. 이때 사용하는 명령어가 SCP이다.
scp app.jar root@server:/root/
SCP는 SSH 프로토콜을 이용해 파일을 안전하게 전송한다.
8. 애플리케이션 실행
초기에는 다음과 같이 실행하였다.
java -jar app.jar
이렇게 되면 Spring Boot는 종료되지 않는 프로세스인데 Jenkins는 명령이 끝날 때까지 기다리기 때문에 Jenkins가 영원히 대기하게 된다.
nohup 사용
따라서 백그라운드로 실행해야 한다.
nohup java -jar app.jar > app.log 2>&1 &
의미는 다음과 같다.
nohup : 터미널 종료 후에도 실행 유지
> : 로그 파일 저장
2>&1 : 에러 로그도 함께 저장
& : 백그라운드 실행
9. 배포 성공 여부 검증
애플리케이션을 실행했다고 해서 배포가 성공한 것은 아니다. 실제로는 실행 직후 예외가 발생하거나 설정 문제로 인해 정상적으로 기동되지 않을 수도 있다. 따라서 배포 후에는 애플리케이션이 정상적으로 실행되었는지 확인하는 과정이 필요하다.
이번 실습에서는 로그 파일에 특정 문자열이 존재하는지 확인하는 방식으로 배포 성공 여부를 검증했다.
grep "Started" app.log
Spring Boot는 정상 실행 시 다음 로그를 출력한다.
Started Application in 5.123 seconds
Jenkins는 해당 문자열 존재 여부를 확인해 성공 여부를 판단할 수 있다.
현재 방식의 한계
현재 파이프라인에서는 애플리케이션 실행 후 일정 시간 동안 대기한 뒤 로그를 확인한다.
sleep 20
하지만 애플리케이션 규모나 서버 상태에 따라 실행 완료까지 걸리는 시간은 달라질 수 있다.
예를 들어 애플리케이션이 정상적으로 실행되기까지 30초가 걸리는 경우, Jenkins는 20초 후 로그를 확인했을 때 아직 시작 로그를 찾지 못해 배포 실패로 판단할 수 있다. 하지만 실제로는 이후 정상적으로 기동될 수 있으므로 고정된 대기 시간만으로 배포 성공 여부를 판단하는 것은 한계가 있다.
이를 보완하기 위해 2가지 방식을 사용할 수 있다.
Health Check
응답
curl localhost:8080/actuator/health
{
"status":"UP"
}
반복 Polling
for i in {1..30}
do
curl localhost:8080/actuator/health && break
sleep 5
done
10. GitHub Webhook 설정
현재는 Jenkins에서 직접 빌드를 실행해야 한다. 하지만, GitHub Push 이벤트를 이용하면 자동 배포가 가능하다.
Webhook 등록
GitHub
Repository
→ Settings
→ Webhooks
추가
Payload URL
http://{jenkins-url}/github-webhook/
Content Type
application/json
이후 Push 이벤트를 선택한다.
Jenkins 설정
아까 개인키를 입력해줬던 것과 같이 이번에도 Credential에서 github token을 입력해주면 된다.

Username : GitHub ID
Password : Personal Access Token
ID : github-token

11. 기존 프로세스 종료 문제
배포를 여러 번 수행하면 다음 오류가 발생한다. 이미 이전 애플리케이션이 실행 중이기 때문이다.
Web server failed to start.
Port 8080 was already in use.
따라서 기존 프로세스를 종료하고 새 프로세스를 실행할 수 있도록 해줘야한다.
lsof -ti:8080
PID 확인 후
kill -9 PID
또는
pkill -f app.jar
를 사용하여 기존 프로세스를 종료할 수 있다.
하지만 이 방식은 또 다른 문제를 발생시킨다.
12. 무중단 배포
현재 방식은 다음 순서로 동작한다.
기존 애플리케이션 종료
↓
새 애플리케이션 실행
↓
사용자 접속 가능
기존 프로세스를 종료한 시점부터 새로운 애플리케이션이 완전히 실행될 때까지는 요청을 처리할 수 없다. 이를 다운타임(Downtime)이라고 한다.
이를 보완하기 위해 아래와 같은 방식이 있다고 한다.
- Blue-Green Deployment
- Rolling Deployment
- Canary Deployment
- Kubernetes Deployment
- Nginx Reverse Proxy 기반 무중단 배포
예를 들어 Blue-Green 방식은 다음과 같이 동작한다.
8081 버전 실행
↓
정상 확인
↓
Nginx 트래픽 전환
↓
8080 종료
기존 서비스가 동작하는 상태에서 새로운 버전을 먼저 실행한 뒤 트래픽을 전환하기 때문에 사용자는 서비스 중단을 거의 느끼지 못한다.
현재 Jenkins + SCP + nohup 방식은 가장 기본적인 자동 배포 구조이다. 이후 Docker, Nginx, Kubernetes 등을 도입하면 무중단 배포까지 확장할 수 있다.
정리
이번 실습에서는 Jenkins를 이용해 다음 과정을 자동화하였다.
- GitHub 소스코드 다운로드
- Maven 빌드
- SCP를 이용한 JAR 파일 전송
- SSH 원격 실행
- 배포 성공 여부 검증
- GitHub Webhook 기반 자동 배포
'Infra' 카테고리의 다른 글
| [CI/CD] Jenkins로 처음 구축해본 CI/CD 파이프라인3 (0) | 2026.03.27 |
|---|---|
| [NGINX] Jenkins로 처음 구축해본 CI/CD 파이프라인2 (3) | 2026.03.24 |
| [k8s] 로컬로 쿠버네티스 구축하기 - minikube, kubectl (0) | 2026.03.08 |
| [k8s] 쿠버네티스란? (0) | 2026.03.04 |
| [AWS] Elastic Beanstalk로 FastAPI + Redis CRUD API 배포하기(2) (1) | 2025.06.07 |