Home EC2 단일 서버 Blue/Green 무중단 배포 (Nginx + 포트 스위칭)
Post
Cancel

EC2 단일 서버 Blue/Green 무중단 배포 (Nginx + 포트 스위칭)

개요

ALB나 Auto Scaling Group 없이 EC2 단일 서버에서 Blue/Green 무중단 배포를 구현했습니다.

Nginx의 proxy_pass 포트를 배포 시마다 교체하는 방식으로, 추가 인프라 비용 없이 다운타임 제로 배포가 가능합니다.


기존 In-place 배포의 문제

1
2
stop.sh  → 기존 프로세스 종료   ← 이 순간 서비스 중단
start.sh → 새 버전 실행

앱 재시작 시간(보통 30초~1분) 동안 503 에러가 발생합니다.


Blue/Green 포트 스위칭 방식

1
2
3
4
5
6
7
8
9
EC2 한 대에서 두 포트를 번갈아 사용

[현재 상태]  Nginx :443 → 8081 (v1 운영 중)
[배포 시작]  8080 포트에 v2 조용히 실행
[헬스체크]   8080 헬스체크 통과 확인
[트래픽 전환] Nginx proxy_pass: 8081 → 8080 (무중단)
[구버전 종료] 8081 프로세스 종료

[다음 배포]  역할 교체: 8081에 v3 배포 → 8080 → 8081 전환

사용자 입장에서는 트래픽 전환 순간 끊김이 없습니다.


전체 구조

1
2
3
4
5
6
appspec.yml
  ├── AfterInstall  → stop.sh   (비활성 포트 정리)
  ├── ApplicationStart → start.sh  (비활성 포트에 새 버전 실행)
  └── ValidateService
        ├── health.sh  (비활성 포트 헬스체크)
        └── switch.sh  (Nginx 트래픽 전환 + 구버전 종료)

appspec.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: 0.0
os: linux
files:
  - source: build/libs/
    destination: /home/ec2-user/app/
permissions:
  - object: /home/ec2-user/app/
    owner: ec2-user
    group: ec2-user
hooks:
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas: ec2-user
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 120
      runas: ec2-user
  ValidateService:
    - location: scripts/health.sh
      timeout: 60
      runas: ec2-user
    - location: scripts/switch.sh
      timeout: 30
      runas: root

배포 스크립트

stop.sh

배포 전 비활성 포트의 기존 프로세스를 정리합니다.

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
INACTIVE_PORT=$(cat /home/ec2-user/app/inactive_port 2>/dev/null || echo "8081")

PID=$(lsof -ti:$INACTIVE_PORT 2>/dev/null)
if [ -n "$PID" ]; then
  echo "비활성 포트($INACTIVE_PORT) 프로세스 종료: PID=$PID"
  kill -15 $PID
  sleep 3
fi
echo "정리 완료"

start.sh

active_port 파일을 읽어 현재 운영 포트를 확인하고, 반대 포트에 새 버전을 실행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash
APP_DIR=/home/ec2-user/app
LOG_DIR=/home/ec2-user/logs

# 현재 활성 포트 확인 → 반대 포트에 배포
ACTIVE_PORT=$(cat $APP_DIR/active_port 2>/dev/null || echo "8080")
if [ "$ACTIVE_PORT" = "8080" ]; then
  INACTIVE_PORT="8081"
else
  INACTIVE_PORT="8080"
fi

echo "활성 포트: $ACTIVE_PORT → 배포 대상 포트: $INACTIVE_PORT"
echo $INACTIVE_PORT > $APP_DIR/inactive_port

# SSM Parameter Store에서 환경변수 로드
get_ssm() {
  aws ssm get-parameter \
    --name "/puppynote/prd/$1" \
    --with-decryption \
    --region ap-northeast-2 \
    --query "Parameter.Value" \
    --output text
}

export DB_HOST=$(get_ssm DB_HOST)
export DB_USERNAME=$(get_ssm DB_USERNAME)
export DB_PASSWORD=$(get_ssm DB_PASSWORD)
# ... 나머지 환경변수

JAR_FILE=$(ls $APP_DIR/*.jar | head -1)

nohup java \
  -XX:+UseContainerSupport \
  -XX:MaxRAMPercentage=75.0 \
  -XX:+UseG1GC \
  -Dspring.profiles.active=prd \
  -Dserver.port=$INACTIVE_PORT \
  -jar $JAR_FILE \
  > $LOG_DIR/app-$INACTIVE_PORT.log 2>&1 &

echo "앱 시작됨: PID=$! / 포트: $INACTIVE_PORT"

health.sh

비활성 포트에서 새 버전이 정상 기동됐는지 확인합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
INACTIVE_PORT=$(cat /home/ec2-user/app/inactive_port 2>/dev/null || echo "8081")

echo "헬스체크 대상 포트: $INACTIVE_PORT"

for i in {1..12}; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$INACTIVE_PORT/health-check)
  if [ "$STATUS" = "200" ]; then
    echo "헬스체크 성공"
    exit 0
  fi
  echo "대기 중... ($i/12)"
  sleep 5
done

echo "헬스체크 실패"
exit 1

switch.sh

헬스체크 통과 후 Nginx의 proxy_pass 포트를 교체하고 구버전을 종료합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
APP_DIR=/home/ec2-user/app
INACTIVE_PORT=$(cat $APP_DIR/inactive_port)
ACTIVE_PORT=$(cat $APP_DIR/active_port 2>/dev/null || echo "8080")

echo "Nginx 트래픽 전환: $ACTIVE_PORT$INACTIVE_PORT"

# nginx.conf의 proxy_pass 포트 교체
sudo sed -i "s/proxy_pass http:\/\/localhost:$ACTIVE_PORT/proxy_pass http:\/\/localhost:$INACTIVE_PORT/" /etc/nginx/nginx.conf
sudo nginx -t && sudo systemctl reload nginx

# 구버전 프로세스 종료
OLD_PID=$(lsof -ti:$ACTIVE_PORT 2>/dev/null)
if [ -n "$OLD_PID" ]; then
  echo "구버전 종료: 포트=$ACTIVE_PORT PID=$OLD_PID"
  kill -15 $OLD_PID
fi

# 활성 포트 업데이트
echo $INACTIVE_PORT > $APP_DIR/active_port
echo "전환 완료: 현재 활성 포트 → $INACTIVE_PORT"

Nginx 설정

proxy_pass에 포트를 직접 명시합니다. switch.sh의 sed가 이 포트 숫자를 교체합니다.

1
2
3
4
5
6
7
location / {
    proxy_pass http://localhost:8081;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

EC2 초기 설정 (1회)

1
2
3
4
5
6
# 초기 활성 포트 설정 (nginx의 현재 proxy_pass 포트와 일치)
echo "8081" > /home/ec2-user/app/active_port

# switch.sh가 sudo nginx 명령을 실행할 수 있도록 권한 부여
echo "ec2-user ALL=(ALL) NOPASSWD: /usr/sbin/nginx, /bin/systemctl reload nginx, /usr/bin/sed" \
  | sudo tee /etc/sudoers.d/puppynote

In-place vs Blue/Green 비교

항목In-placeBlue/Green (포트 스위칭)
다운타임있음 (재시작 시간)없음
롤백재배포 필요구버전 프로세스 살아있으면 즉시
메모리1개 프로세스배포 중 잠시 2개 동시 실행
추가 인프라불필요불필요
복잡도단순다소 복잡

마무리

ALB, Auto Scaling Group 없이도 Nginx 포트 스위칭만으로 무중단 배포가 가능합니다.

소규모 서비스에서 인프라 비용을 최소화하면서 무중단 배포를 원할 때 효과적인 방법입니다.

This post is licensed under CC BY 4.0 by the author.