💠프로젝트 및 경험/프로젝트

[메모장 프로젝트] Nignx 무중단 배포 (AWS 5)

2025. 7. 18. 23:55
728x90


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

해당 명령어를 통해 어느 포트에서 실행 중인지 확인할 수 있다.

 

그리고 도메인으로 접속하면 잘 나오는 것을 확인할 수 있다. 🙌🙌

https://notepad.today

 

 

 

 

 

728x90