EC2 환경에서 Nginx 무중단 배포 설정
기존에 배포되어 있는 서비스들은 모두 EC2에 직접 jar 파일을 배포하는 방식으로, 중단 배포를 진행하고 있었습니다.
이러한 문제를 해결하기 위해서 신규 서버부터는 Docker, Nginx를 활용하여 무중단 배포 환경을 구축하려고 합니다.
ECS를 활용하면 더 편리하고, 가용성 있는 환경을 구축할 수 있겠지만, 스타트업의 특성상 MVP 서비스를 위해 ECS의 비용을 지출하는 것이 과한 소비라고 판단했습니다.
추후에 다시 Nginx를 구축해야 하는 일이 발생할 수 있기 때문에, 이에 대한 글을 작성해놓으려고 합니다.
ubuntu 환경
Nginx 설치 및 서비스 활성화
먼저 패키지 저장소 업데이트를 진행합니다.
sudo apt-get update -qq
이후, Nginx를 설치하고, 서비스를 활성화합니다.
sudo apt-get install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
Start nginx 이슈
start nginx를 진행할 때, 아래와 같은 에러가 발생할 수 있습니다.
Job for nginx.service failed because the control process exited with error code.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.
그렇다면, sudo systemctl status nginx.service를 사용해서 현재 어떤 문제로 인하여, start를 할 수 없는지 확인을 해봐야합니다.
× nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Tue 2025-09-23 01:32:52 UTC; 56s ago
Docs: ...
Sep 23 01:32:51 ip-172-31-33-120 nginx[1518857]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Sep 23 01:32:51 ip-172-31-33-120 nginx[1518857]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
...
Sep 23 01:32:52 ip-172-31-33-120 systemd[1]: nginx.service: Control process exited, code=exited, status=1/FAILURE
Sep 23 01:32:52 ip-172-31-33-120 systemd[1]: nginx.service: Failed with result 'exit-code'.
Sep 23 01:32:52 ip-172-31-33-120 systemd[1]: Failed to start nginx.service - A high performance web server and a reverse prox>
이미 80 port를 사용중이라는 이야기로, 기존에 배포된 Docker Server를 중단하고 설정을 진행했습니다. 실시간으로 운영중인 서비스라면, 트래픽이 없는 시간에 잠시 운영 공지를 띄우고 작업을 진행해야할 것 같습니다.
컨테이너가 자동으로 재시작 할 수 있으므로 sudo docker update --restart=no <container-name> 으로 잠시 막아줄 필요가 있을수 있습니다.
Nginx 확인
잘 설치가 되었는지, Nginx 버전과, 서비스 상태, 접근 테스트를 진행하였습니다.
# 버전 확인
~$ nginx -v
nginx version: nginx/1.24.0 (Ubuntu)
# 서비스 상태 확인
~$ sudo systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-09-23 01:40:35 UTC; 3min 1s ago
Docs: ...
# 기본 페이지 접근 테스트
~$ curl -I http://localhost
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Tue, 23 Sep 2025 01:43:42 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 23 Sep 2025 01:32:27 GMT
Connection: keep-alive
ETag: "68d1f8ab-267"
Accept-Ranges: bytes
blue-green 설정
blue-green 설정을 하기 전에, default 설정을 제거하여 설정 충돌을 방지해야 합니다.
왜?
default 설정을 보게 되면, 80번 포트를 사용하는 기본 서버 설정이 이미 작성되어 있습니다. 해당 default 설정을 남겨둔 채, 새로운 설정 파일에서 똑같이 80번 포트를 사용하도록 작성하면, 두 개의 다른 설정 파일이 동시에 80번 포트를 사용한다고 판단하기 때문에 "Address already in use"와 오류를 발생시키고 시작에 실패하게 됩니다.
# default 삭제
sudo rm -f /etc/nginx/sites-enabled/default
# Blue-Green 설정 파일 생성
```bash
sudo vim /etc/nginx/sites-available/<application>
작성이 완료되었다면, 생성된 파일을 sites-enabled 쪽으로 심볼릭 링크를 생성해줍니다.
심볼릭 링크는 바로가기와 같은 개념으로, Nginx에서는 모든 설정 원본 파일은 /etc/nginx/sites-available/에 안전하게 저장하고, 실제로 활성화하고 싶은 사이트의 설정 파일만 골라서, 심볼릭 링크를/etc/nginx/sites-enabled/ 디렉토리에 만듭니다.
sudo ln -sf /etc/nginx/sites-available/<application> /etc/nginx/sites-enabled/
Blue-Green 배포를 위한 전체 Nginx 설정 (참고용)
# Upstream 서버의 응답 시간까지 기록하는 상세 로그 포맷 정의
# 문제 발생 시 'rt'(Request Time), 'urt'(Upstream Response Time) 등으로 병목 구간을 쉽게 찾을 수 있습니다.
log_format combined_with_upstream '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time" '
'upstream="$upstream_addr"';
# Blue 환경 애플리케이션 서버 그룹 (포트 8080)
upstream blue {
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
# keepalive: Upstream(애플리케이션) 서버와 연결을 재사용하여 TCP 핸드셰이크 오버헤드를 줄여줍니다.
keepalive 32;
}
# Green 환경 애플리케이션 서버 그룹 (포트 8081)
upstream green {
server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# API 요청 속도 제한 설정(서비스 트래픽, API 요청 패턴 고려하여 수정 필요)
# $binary_remote_addr: IP를 기반으로 요청을 추적
# zone=api:10m: 'api'라는 이름의 10MB 공유 메모리 공간 생성
# rate=10r/s: 초당 10개의 요청만 허용
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# 메인 서버 블록: 실제 서비스 트래픽 처리
server {
listen 80;
server_name api.domain.kr localhost; # 실제 서비스 도메인 및 테스트용 localhost
# 응답 헤더에서 Nginx 버전 정보를 숨깁니다.
server_tokens off;
# 위에서 정의한 상세 로그 포맷을 사용합니다.
access_log /var/log/nginx/access.log combined_with_upstream;
# ALB와 같은 프록시 뒤에 있을 때 실제 클라이언트 IP를 찾도록 설정
set_real_ip_from 10.0.0.0/8; # VPC 내부 IP 대역 예시
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# 웹 브라우저 보안을 강화하는 HTTP 헤더 추가
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 로드밸런서나 모니터링 시스템이 Nginx 자체의 동작 상태를 확인할 수 있는 헬스체크 엔드포인트
location /nginx-health {
access_log off;
return 200 "healthy\n";
}
# API 요청 처리
location /api/ {
# API 요청에 대해 초당 10개를 초과하는 요청은 잠시 지연시킴 (최대 20개까지 버스트 허용)
limit_req zone=api burst=20 nodela_y;
# 배포 스크립트가 이 부분을 'http://blue' 또는 'http://green'으로 동적 변경
proxy_pass http://blue;
# Upstream과의 keepalive 연결을 활성화하기 위한 필수 설정
proxy_http_version 1.1;
proxy_set_header Connection "";
# 백엔드 애플리케이션으로 실제 요청 정보를 전달하는 헤더 설정
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; # http vs https 구분
# 타임아웃 설정
proxy_connect_timeout 60s; # Upstream 연결 타임아웃
proxy_send_timeout 60s; # Upstream으로 요청 전송 타임아웃
proxy_read_timeout 300s; # AI 처리 등 긴 작업 시간을 고려해 응답 대기 시간을 5분으로 설정
# Upstream 응답 버퍼링 설정
proxy_buffering on;
# 파일 업로드 최대 크기 설정
client_max_body_size 50M;
# Upstream 서버 장애 시 다른 서버로 재시도하는 정책
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 2; # 최대 2회 재시도
proxy_next_upstream_timeout 30s; # 재시도 시 타임아웃
}
# 루트 경로("/") 접근 시 liveness check 엔드포인트로 리다이렉트
location = / {
return 302 /api/v1/system/liveness;
}
# .git, .env 등 숨김 파일 접근 차단
location ~ /\. {
deny all;
}
}
# Default Server 블록: 매칭되는 server_name이 없을 때의 처리
server {
listen 80 default_server;
server_name _; # 매칭되는 호스트가 없을 경우
# AWS ALB 헬스체커의 요청은 정상(200)으로 응답
if ($http_user_agent = "ELB-HealthChecker/2.0") {
return 200 "healthy\n";
}
# 그 외 모든 알 수 없는 요청은 로그를 남기지 않고 연결을 즉시 끊어버림 (return 444)
# 이는 불필요한 스캐닝 공격 등을 방어하는 데 효과적입니다.
return 444;
}
설정 파일 테스트, 적용
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
위 명령어를 통해 syntax is ok 또는 test is successful 응답을 받으면 됩니다.
# 설정 적용
sudo systemctl reload nginx
# 서비스 상태 확인
sudo systemctl status nginx
다시 한 번 nginx 을 재시작하고, 서비스 상태를 확인합니다. 이전과 같이 Active의 값이 active (running)으로 구성되어 있으면 완료되었습니다.
systemctl restart가 아닌 reload를 선택한 이유는, restart와 달리 종료 후 발생하는 다운타임 없이 교체하며 연결 손실 없이 설정을 적용할 수 있기 때문입니다.
방화벽 설정 및 동작 테스트
# HTTP 트래픽 허용
$ sudo ufw allow 'Nginx HTTP'
Rules updated
Rules updated (v6)
# SSH 접속 허용 (중요!)
$ sudo ufw allow OpenSSH
Rules updated
Rules updated (v6)
# 방화벽 상태 확인
$ sudo ufw status
Status: active
To Action From
-- ------ ----
Nginx HTTP ALLOW Anywhere
OpenSSH ALLOW Anywhere
Nginx HTTP (v6) ALLOW Anywhere (v6)
OpenSSH (v6) ALLOW Anywhere (v6)
# 방화벽 상태가 inactive라면
$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
여기서 궁금한 것은 'Nginx HTTP' 이건 예약어인지 궁금했다.
예약어는 아니고 ufw에 등록된 어플리케이션 프로필의 이름이라고 하며, Nginx HTTP라는 이름으로 방화벽 규칙을 쉽게 추가할 수 있도록
하는 편의 기능입니다.
# 프로필 목록 확인 방법
$ sudo ufw app list
Available applications:
Nginx Full
Nginx HTTP
Nginx HTTPS
OpenSSH
방화벽까지 설정한 후에는 잘 동작하는지 이어서 테스트를 해보겠습니다.
$ curl -f http://localhost/nginx-health
healthy
$ curl http://localhost/nginx-status
backend:
server: ip-17...
$ curl -H "Host: api.domain.kr" http://localhost/nginx-health
healthy
$ curl -H "Host: api.domain.kr" \
-H "X-Forwarded-Proto: https" \
-H "X-Forwarded-For: 1.2.3.4" \
http://localhost/nginx-health
healthy
nginx-health 이슈
$ curl -f http://localhost/nginx-health
curl: (52) Empty reply from server
이와 같은 이슈는 Nginx가 요청 헤더의 Host 값을 기준으로 어떤 server 블록에서 요청을 처리할지 결정하기 때문입니다. curl http://localhost 요청 시 Host 헤더가 localhost로 전달되는데, server_name에 localhost가 없으면 일치하는 서버를 찾지 못해 default_server 블록으로 넘어가게 되고, 여기서 return 444;에 의해 연결이 끊어지며 발생하는 문제입니다.
server {
listen 80;
server_name api.domain.kr localhost;
server_tokens off; # 보안 강화: Nginx 버전 정보 숨김
위 스크립트에서 localhost가 누락이 되어있어서 발생하는 문제이므로, 추가하셔도 되고 아래처럼 Host를 추가 후 테스트를 진행해도 됩니다.
$ curl -H "Host: api.domain.kr" http://localhost/nginx-health
healthy
두 명령어 모두, 이슈가 발생한다면 sudo tail -f /var/log/nginx/error.log를 통해 에러를 확인하고, 다른 터미널에서 curl을 보내어 새롭게 출력이 되는 로그가 있는지 추가적으로 확인한 후 이슈를 해결해야 합니다.
최종 시스템 점검
마지막으로, 프로세스랑 로그들을 확인하겠습니다.
# Nginx 프로세스 확인
$ ps aux | grep nginx
root 1520036 0.0 0.1 21752 3876 ? Ss 02:32 0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data 1520515 0.0 0.2 23284 4992 ? S 03:28 0:00 nginx: worker process
www-data 1520516 0.0 0.2 23284 4956 ? S 03:28 0:00 nginx: worker process
ubuntu 1520538 0.0 0.1 7076 2140 pts/3 S+ 03:31 0:00 grep --color=auto nginx
# 접근 로그 확인
$ sudo tail -f /var/log/nginx/access.log
172.31.45.39 - - [23/Sep/2025:03:29:04 +0000] "GET /api/v1/system/liveness HTTP/1.1" 200 8 "-" "ELB-HealthChecker/2.0"
172.31.10.58 - - [23/Sep/2025:03:29:07 +0000] "GET /api/v1/system/liveness HTTP/1.1" 200 8 "-" "ELB-HealthChecker/2.0"
# 활성 설정 확인
$ sudo nginx -T | grep server_name
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
server_name api.domain.kr localhost;
server_name _;
# Blue-Green Upstream 설정 블록만 확인
$ awk '/^upstream/,/^\}/' /etc/nginx/sites-available/application
upstream blue {
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
keepalive 32; # 성능 향상: 연결 재사용
}
upstream green {
server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
keepalive 32; # 성능 향상: 연결 재사용
}
# 또는, 현재 활성화된 upstream만 정확히 확인하는 방법
$ grep 'proxy_pass http://' /etc/nginx/sites-available/application
proxy_pass http://green;
모든 설정이 완료되었다면, 배포는 다음과 같은 로직으로 동작하여 무중단 전환을 수행하게 됩니다.
- 현재 Nginx가 바라보는 Upstream 확인 (예: blue)
- 새로운 버전의 애플리케이션을 비활성 Upstream 포트(예: 8081)로 실행
- 새 애플리케이션의 Health Check (예: curl http://localhost:8081/actuator/health)
- Health Check 성공 시, Nginx 설정 파일의 proxy_pass 대상을 green으로 변경
# sed 명령어로 'blue'를 'green'으로 치환
sudo sed -i 's/proxy_pass http:\/\/blue;/proxy_pass http:\/\/green;/' /etc/nginx/sites-available/<application>
- sudo nginx -s reload 명령어로 중단 없이 설정 적용
- 이전 버전의 애플리케이션(blue) 컨테이너 종료
무중단 배포를 위한 스크립트는 별도로 작성해야 합니다.
배포 스크립트 예시 (실무 테스트 X)
아래 예시는 LLM이 작성한 것이므로, 참고용으로만 사용할 것을 권장합니다.
제가 실무에서 적용한 배포 스크립트와 다릅니다.
#!/bin/bash
set -e
# --- 설정 변수 ---
CONFIG_PATH="/etc/nginx/sites-available/my-app"
UPSTREAM_BLUE_PORT=8080
UPSTREAM_GREEN_PORT=8081
# --- 로직 시작 ---
# 1. 현재 활성 Upstream과 Port 식별
CURRENT_UPSTREAM_LINE=$(grep 'proxy_pass http://' "$CONFIG_PATH")
if [[ "$CURRENT_UPSTREAM_LINE" == *"blue"* ]]; then
CURRENT_PORT=$UPSTREAM_BLUE_PORT
TARGET_PORT=$UPSTREAM_GREEN_PORT
TARGET_NAME="green"
else
CURRENT_PORT=$UPSTREAM_GREEN_PORT
TARGET_PORT=$UPSTREAM_BLUE_PORT
TARGET_NAME="blue"
fi
echo "현재 활성 포트: $CURRENT_PORT -> 배포 타겟 포트: $TARGET_PORT ($TARGET_NAME)"
# 2. 새 애플리케이션 헬스체크 (동적으로 타겟 포트 사용)
echo "새 애플리케이션($TARGET_NAME) 헬스체크 시작..."
HEALTH_CHECK_URL="http://localhost:$TARGET_PORT/api/v1/system/liveness"
# 1분 동안 5초 간격으로 헬스체크 시도
for i in {1..12}; do
if curl -s -f "$HEALTH_CHECK_URL" > /dev/null; then
echo "헬스체크 성공!"
break
fi
echo "헬스체크 실패 ($i/12). 5초 후 재시도..."
sleep 5
done
if ! curl -s -f "$HEALTH_CHECK_URL" > /dev/null; then
echo "최종 헬스체크 실패. 배포를 중단합니다."
exit 1
fi
# 3. Nginx 설정 변경 (정확한 패턴 매칭을 위한 sed 사용)
echo "Nginx 트래픽을 $TARGET_NAME 으로 전환합니다..."
sudo sed -i "s|proxy_pass http://.*|proxy_pass http://$TARGET_NAME;|" "$CONFIG_PATH"
# 4. 설정 테스트 및 적용
sudo nginx -t && sudo nginx -s reload || { echo "Nginx reload 실패. 롤백이 필요할 수 있습니다."; exit 1; }
echo "성공적으로 $TARGET_NAME 환경으로 전환되었습니다."
# 5. (선택) 이전 버전 컨테이너 정리 로직 추가
echo "이전 버전($CURRENT_PORT)의 컨테이너를 정리합니다..."
# docker stop ...
'Server > Infra' 카테고리의 다른 글
| 운영중인 AWS ECR를 Terraform으로 가져오기 (0) | 2025.09.27 |
|---|---|
| N100 미니PC를 이용한 나만의 서버 구축기 - 3 / K3s 설치 (0) | 2025.05.29 |
| N100 미니PC를 이용한 나만의 서버 구축기 - 2 / VPN 설정 (0) | 2025.05.27 |
| N100 미니PC를 이용한 나만의 서버 구축기 - 1 / Proxmox 설정 (1) | 2025.05.27 |
| SQS 메시지를 한 번만 처리하기 위한 고민 (FIFO, DB 멱등성, 그리고 현실적 고려사항) (0) | 2025.05.13 |