[메모장 프로젝트] Nignx 무중단 배포 (AWS 5)
1. EC2 생성, 보안 그룹 설정 + 자동화 쉘 스크립트
2. RDS PostgreSQL 생성, EC2 에 Redis 설치
3. GitHub Actions 로 자동 배포
4. 도메인 연결하기
5. Nignx 무중단 배포
자동 배포 구조
EC2 서버 파일 구조
우선 `EC2` 서버 파일 구조이다
~/memo-notepad/
├── deploy.sh
├── app.log
├── releases/
│ ├── notepad-20250718122228.jar
│ └── notepad-20250718122439.jar
└── config/
└── application-prod.yml
`deploy.sh` : 실제 배포 스크립트
`releases` : `jar` 파일 보관소
`config` : `application-prod.yml` 파일 보관소
app.log
gitHub Actions 에서는 배포한 뒤, 애플리케이션 log 을 볼 수 없어서 따로 저장해야 했다.
sudo nano /etc/logrotate.d/notepad
명령어를 통해 설정 파일을 생성했다.
/home/ubuntu/memo-notepad/app.log {
daily # 매일 회전
rotate 7 # 최근 7개까지 보관
compress # 오래된 로그는 gzip 압축
missingok # 파일 없어도 에러 안 냄
notifempty # 로그가 비어 있으면 회전 안 함
copytruncate # 실행 중인 프로세스를 끊지 않고 파일만 비움
}
`copytruncate` 은 `nohup` 처럼 백그라운드 실행 중에도 안전하게 로그를 비울 수 있다.
또한 해당 파일은 `root` 가 생성했기 때문에, 권한을 `ubuntu` 로 다 바꿔주어야 파일을 읽고 수정할 수 있다.
무중단 배포
무중단 배포 (Bule-Green 방식) 을 사용하여 2개의 포트를 번갈아 사용한다.
예를 들어 `8285` 포트로 이미 서비스가 실행 중이고 새로운 배포를 시도 중이다.
그렇게 배포를 통해 비어있던 `8284` 포트로 서비스를 실행하면,
`8284` 포트의 프로세스가 `state-up` 상태가 되는데 이를 `Nginx` 는 헬스 체크하여 `8284` 포트의 프로세스로 라우팅 하게 된다.
`8285` 포트의 프로세스는 `kill` 로 종료시킨다.
이렇게 2개의 포트로 배포를 진행하면 사용자는 서버가 내려가 있는 상황을 경험할 수 없을 것이다.
2개의 포트로 배포를 진행하려면, 하나의 `Jar` 파일로 덮어쓰면 문제가 발생할 수 있어
빌드 시점에 시간을 기반으로 고유한 이름의 `Jar` 파일을 생성하여 저장을 해야 한다.
그러면 `EC2` 에서 항상 새로운 파일로 실행하게 되고, 프록시만 새 포트로 변경하여 무중단 배포를 유지할 수 있다.
이후 `Jar` 파일이 5개가 넘어가면 오래된 파일부터 삭제한다.
application-prod.yml
application.yml 에서 기본 설정들은 같이 배포해도 되지만,
application-prod.yml 은 보안상 Github 에 올리면 안 된다.
따라서 `EC2` 서버에 따로 파일을 올려주고, 애플리케이션 실행 시점에 application-prod.yml 설정 파일을 사용하도록 한다.
EC2 무중단 배포하기
Actuator
해당 프로세스의 헬스 체크를 위해 `Actuator` 가 필요하다.
스프링에 `/actuator/health` 이런 식으로 요청을 보내는 것이기 때문에,
스프링에 해당 `Acutator` 의존관계도 추가해야 하고,
`SecurityConfig` 나 `JwtFilter` 에서 `/actuator/health` 경로를 허용해야 한다.
허용하지 않으면 해당 경로에서 `jwt` 인증이 필요하기 때문에 `status-up` 상태를 체크할 수 없다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-details: never
의존성도 추가하고, application-prod.yml 에 설정을 추가한다.
EC2 deploy.sh
JAR_NAME=$1
PORT1=8284
PORT2=8285
RELEASE_DIR=/home/ubuntu/memo-notepad/releases
CONFIG_DIR=/home/ubuntu/memo-notepad/config
if lsof -i :$PORT1 > /dev/null; then
OLD_PORT=$PORT1
NEW_PORT=$PORT2
elif lsof -i :$PORT2 > /dev/null; then
OLD_PORT=$PORT2
NEW_PORT=$PORT1
else
OLD_PORT=""
NEW_PORT=$PORT1
fi
echo "> 새 프로세스 실행: $NEW_PORT"
nohup java -Dserver.port=$NEW_PORT \
-Dspring.profiles.active=prod \
-Dspring.config.additional-location=file:$CONFIG_DIR/ \
-jar $RELEASE_DIR/$JAR_NAME > ~/memo-notepad/app.log 2>&1 &
echo "> 헬스체크 시작"
for i in {1..10}
do
sleep 3
RESPONSE=$(curl -s http://localhost:$NEW_PORT/actuator/health)
if echo "$RESPONSE" | grep '"status":"UP"' > /dev/null; then
echo "> 헬스체크 통과! 프록시 포트 변경: $NEW_PORT"
echo "proxy_pass http://localhost:$NEW_PORT;" | sudo tee /etc/nginx/includes/proxy_target.conf > /dev/null
sudo nginx -t
sudo nginx -s reload
if [[ -n "$OLD_PORT" ]]; then
echo "> 이전 프로세스 종료: $OLD_PORT"
PID_OLD=$(lsof -t -i:$OLD_PORT)
if [[ -n "$PID_OLD" ]]; then
kill -15 $PID_OLD
else
echo "> 이전 프로세스가 이미 종료되었거나 없습니다."
fi
fi
# 5개 이상이면 오래된 JAR 파일 정리
cd $RELEASES_DIR
ls -1tr | head -n -5 | xargs -d '\n' rm -f --
exit 0
fi
done
echo "> 헬스체크 실패. 롤백"
exit 1
1. 입력 인자
: 타임스탬프 형식으로 받은 Jar 파일을 `JAR_NAME` 에 저장한다.
2. 포트 설정
: `PORT1=8284`, `PORT2=8285`
: 현재 사용 중인 포트를 확인하여, 사용하지 않는 포트로 새 프로세스 실행
3. 새 프로세스 실행
: `nohup java -Dserver.port=$NEW_PORT ... -jar $RELEASE_DIR/$JAR_NAME` 로 백그라운드 실행
: 로그는 `app.log` 에 기록
4. 헬스 체크 (최대 10회, 3초 간격)
: `http://localhost:$NEW_PORT/actuator/health` 호출해서 `statue:up` 확인
5. Nginx 프록시 대상 변경 및 재시작
: `/etc/nginx/includes/proxy_target.conf` 에 새 포트로 `proxy_pass` 설정
: `nginx -t` 로 설정 문법 검사
: `nginx -s reload` 로 설정 적용
6. 이전 프로세스 종료
: 이전 포트에서 실행 중인 프로세스 `PID` 찾고, `kill -15` 시그널로 종료
7. 오래된 Jar 파일 정리
: `releases` 디렉토리에서 오래된 Jar 파일 5개만 남기고 삭제
8. 헬스 체크 실패 시
: 10 회 모두 실패하면 `exit 1`로 종료 (롤백 시그널)
EC2 Nginx 설정
그리고 무중단 배포를 위한 `Nginx` 도 설치해야 한다.
sudo apt update
sudo apt install nginx
위 명령어를 실행하여 `nginx` 를 설치해 준다.
sudo nano /etc/nginx/sites-available/default
해당 명령어로 nano 편집기를 통해 설정 파일을 열어서 수정해 준다.
server {
listen 80;
server_name notepad.today www.notepad.today;
location / {
include /etc/nginx/includes/proxy_target.conf;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Let's Encrypt 인증용
location ~ /.well-known/acme-challenge/ {
allow all;
}
# HTTP → HTTPS 리다이렉트
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name notepad.today www.notepad.today;
ssl_certificate /etc/letsencrypt/live/notepad.today/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/notepad.today/privkey.pem;
location / {
include /etc/nginx/includes/proxy_target.conf;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
`deploy.sh` 에서 변경해 준 `proxy_pass` 를 사용한다.
(`/etc/nginx/includes/proxy_target.conf` 경로에 `proxy_pass http://localhost:8285;` 이렇게 저장되어 있다.)
`HTTP`
: `Let's Encrypt` 인증 기관의 SSL 인증서 발급을 위해 `/.well-known/acme-challenge/` 경로를 열어둔다.
: 그 외 모든 요청은 `HTTPS` 로 리다이렉트
`HTTPS`
: `fullchain.pem` SSL 인증서, `privkey.pem` 개인키
: 모든 요청을 내부 서버로 프록시 한다.
(도메인이 없으면 `SSL` 인증서가 없기 때문에 `HTTPS` 에서 접근할 수 없다.)
EC2 프로세스 확인
sudo lsof -i :8284
해당 명령어를 통해 어느 포트에서 실행 중인지 확인할 수 있다.
그리고 도메인으로 접속하면 잘 나오는 것을 확인할 수 있다. 🙌🙌