티스토리 뷰
✏️ 1. 배포 방식에 대한 고민
사람들과 팀 플젝을 하면서 느꼈던 문제점 중 하나는 서버를 배포할 때마다 서비스가 잠깐씩 멈춘다는 점이었다.
기존에는 단일 EC2 서버에서 애플리케이션을 교체하는 방식으로 배포를 진행하였는데, 이 방식은 단순하지만 다음과 같은 문제가 있었다.
- 배포 중 서버 재시작으로 인한 다운타임 발생
- 배포 실패 시 즉각적인 롤백 어려움
- 사용자가 배포 타이밍에 따라 오류를 경험할 수 있음
사용자가 있는 서비스라면 배포 때문에 서비스가 끊기는 경험은 반드시 피해야 한다고 생각하여서 무중단 배포를 직접 구축해보기로 하였다.
✏️ 2. Blue-Green 배포를 선택한 이유
무중단 배포를 구현하는 방법에는 여러 가지가 있다. 대표적으로 Rolling 배포와 Blue-Green 배포와 카나리 배포 방식이 있다. 이 3개의 배포에 관한 설명은 추후 게시글을 작성할 예정이다. 이 얘기는 뒤로하고 Blue-Green 배포를 선택하기로 하였다. Blue-Green 배포 방식을 선택한 이유는 다음과 같다.
- 배포 전에 충분히 검증할 수 있는 구조
- 문제 발생 시 즉시 롤백 가능
- 트래픽 전환 방식이기 때문에 구조가 명확함
즉, Blue-Green 배포는 간단히 말하면 두 개의 동일환 환경을 운영하고, 트래픽만 전환하는 방식이다.
Blue는 현재 서비스 중인 서버, Green은 새 버전을 배포한 서버를 의미한다. 그래서 새로운 버전을 바로 사용자에게 노출하는 것이 아니라 검증된 이후에만 트래픽을 넘긴다는 점이 핵심이다.
✏️ 3. 전체 아키텍쳐
이번 배포 구조는 다음과 같이 구성하였다.
GitHub Actions → Docker 이미지 생성 → DockerHub Push → S3 업로드 → CodeDeploy → EC2 배포 → Nginx 트래픽 전환
주요 구성 요소는 다음과 같고 이 구조에서 중요한 역할을 Nginx가 어떤 컨테이너로 트래픽을 보낼지 결정하는 것이다.
- EC2: 애플리케이션 실행 서버
- Docker: 컨테이너 기반 배포
- Nginx: 트래픽 라우팅(포트 스위칭)
- CodeDeploy: 배포 자동화
- S3: 배포 파일 저장
- GitHub Actions: CI/CD 파이프라인
✏️ 4. 배포 전략 설계
배포 흐름은 아래와 같이 설계하였고 간단하게 요약하면 검증->전환->정리이다.
- 현재 서비스 중인 포트 확인
- 반대 포트에 새로운 컨테이너 실행
- 헬스 체크 수행
- 성공 시 Nginx 트래픽 전환
- 기존 컨테이너 종료
✏️ 5. CodeDeploy를 선택한 이유
우선 배포 자동화를 위해 AWS CodeDeploy를 선택하였고 이유는 다음과 같다.
특히 CodeDeploy의 장점은 서버에 직접 SSH로 접속하지 않아도 되고, 배포 과정에서 실핼할 스크립트를 정의할 수 있기 때문에 Blue-Green 배포 로직을 직접 구현할 수 있다.
- EC2, S3와 자연스럽게 연동
- 배포를 자동으로 실행할 수 있음
- appspec.yml을 통해 배포 단계 제어 가능
✏️ 6. AWS 세팅
6-1. IAM 생성

6-2. 역할 생성






6-3. S3 생성



6-4. EC2 설정



6-5. CodeDeploy 생성





✏️ 7. EC2 설정
7-1. Docker 설치
sudo apt update
sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker ubuntu
7-2. Docker 설치
sudo apt install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx
7-3. CodeDeploy Agent 설치
sudo apt install -y ruby wget
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto
sudo systemctl start codedeploy-agent
sudo systemctl enable codedeploy-agent
7-4. 설치 확인
docker --version
nginx -v
sudo systemctl status codedeploy-agent

7-5. 가비아 설정

7-6. Nginx 설정
### Cerbot 설치
sudo apt install -y certbot python3-certbot-nginx
### SSL 인증서 발급
sudo certbot --nginx -d lgenius.site -d www.lgenius.site
### 자동 갱신 확인
sudo certbot renew --dry-run
server {
listen 80;
server_name lgenius.site www.lgenius.site;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name lgenius.site www.lgenius.site;
ssl_certificate /etc/letsencrypt/live/lgenius.site/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lgenius.site/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
set $service_port 8080;
location / {
proxy_pass http://127.0.0.1:$service_port;
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;
}
}
✏️ 8. 프로젝트 설정
8-1. Dockerfile
FROM openjdk:17-jdk-jammy
WORKDIR /app
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
8-2. appspec.yml
version: 0.0
os: linux
hooks:
ApplicationStart:
- location: scripts/deploy.sh
timeout: 300
runas: ubuntu
8-3. scripts/deploy.sh
#!/bin/bash
set -e
BLUE_PORT=8080
GREEN_PORT=8081
NGINX_CONF="/etc/nginx/sites-available/default"
IMAGE="pooreumjung/lgenius-backend:latest"
ENV_FILE="/home/ubuntu/app/.env.prod"
echo "=== Blue-Green Deploy Start ==="
# 0. Docker 네트워크 생성 (없으면)
docker network create lgenius-net 2>/dev/null || true
# 1. Redis 실행 여부 확인
if ! docker ps | grep -q lgenius-redis; then
echo "Redis 컨테이너가 실행 중이 아닙니다. Redis를 먼저 실행합니다."
docker run -d \
--name lgenius-redis \
--network lgenius-net \
--restart unless-stopped \
redis:7-alpine
fi
# 2. 현재 nginx가 바라보는 포트 확인
CURRENT_PORT=$(grep -oP 'set \$service_port \K[0-9]+' $NGINX_CONF | head -n 1)
if [ -z "$CURRENT_PORT" ]; then
CURRENT_PORT=$BLUE_PORT
fi
if [ "$CURRENT_PORT" = "$BLUE_PORT" ]; then
NEW_PORT=$GREEN_PORT
else
NEW_PORT=$BLUE_PORT
fi
echo "현재 포트: $CURRENT_PORT"
echo "새 포트: $NEW_PORT"
# 3. 최신 이미지 pull
echo "이미지 pull 중..."
docker pull $IMAGE
# 4. 기존 NEW_PORT 컨테이너 제거
docker rm -f lgenius-backend-${NEW_PORT} 2>/dev/null || true
# 5. 새 컨테이너 실행
echo "새 컨테이너 실행 (${NEW_PORT})"
docker run -d \
--name lgenius-backend-${NEW_PORT} \
--network lgenius-net \
-p ${NEW_PORT}:8080 \
--env-file ${ENV_FILE} \
--restart unless-stopped \
$IMAGE
# 6. 헬스체크 (최대 120초)
echo "헬스체크 확인 중..."
SUCCESS=false
for i in {1..24}
do
if curl -fsS --connect-timeout 3 --max-time 5 http://localhost:${NEW_PORT}/actuator/health > /dev/null 2>&1; then
SUCCESS=true
echo "헬스체크 성공"
break
fi
echo "헬스체크 재시도 ($i/24)"
sleep 5
done
if [ "$SUCCESS" != "true" ]; then
echo "헬스체크 최종 실패 → 롤백"
docker logs --tail 50 lgenius-backend-${NEW_PORT}
docker stop lgenius-backend-${NEW_PORT}
docker rm lgenius-backend-${NEW_PORT}
exit 1
fi
# 7. nginx 포트 전환
echo "nginx 전환 중..."
sudo sed -i "s/set \$service_port .*/set \$service_port $NEW_PORT;/" $NGINX_CONF
sudo nginx -t
sudo systemctl reload nginx
echo "nginx 전환 완료"
# 8. 기존 컨테이너 종료
echo "기존 컨테이너 종료 (${CURRENT_PORT})"
docker stop lgenius-backend-${CURRENT_PORT} 2>/dev/null || true
docker rm lgenius-backend-${CURRENT_PORT} 2>/dev/null || true
# 9. 사용하지 않는 이미지 정리
docker image prune -f
echo "=== 배포 완료 (포트: $NEW_PORT) ==="
8-4. application-prod.yml
spring:
datasource:
url: jdbc:mysql://${DB_URL}:3306/${DB_NAME}?serverTimezone=Asia/Seoul
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
data:
redis:
host: lgenius-redis
port: 6379
oauth:
kakao:
client-id: ${KAKAO_CLIENT_ID}
client-secret: ${KAKAO_CLIENT_SECRET}
redirect-uri: ${KAKAO_REDIRECT_URI}
auth-base-url: https://kauth.kakao.com
api-base-url: https://kapi.kakao.com
jwt:
secret: ${JWT_SECRET}
access-token-expiration: 3600000
refresh-token-expiration: 604800000
server:
forward-headers-strategy: framework
8-5. ci.yml
name: CI
on:
pull_request:
branches: [ main, develop, deploy/#7/cicd ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 코드 체크아웃
uses: actions/checkout@v4
- name: JDK 17 설정
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: application-prod.yml 생성
run: |
mkdir -p src/main/resources
echo "${{ secrets.APPLICATION_PROD_YML }}" > src/main/resources/application-prod.yml
- name: Gradle 권한 부여
run: chmod +x gradlew
- name: Gradle 빌드 (테스트 제외)
run: ./gradlew build -x test
8-6. cd.yml
name: CD
on:
push:
branches: [ main, deploy/#7/cicd ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 코드 체크아웃
uses: actions/checkout@v4
- name: JDK 17 설정
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: application-prod.yml 생성
run: |
mkdir -p src/main/resources
cat > src/main/resources/application-prod.yml << 'EOF'
${{ secrets.APPLICATION_PROD_YML }}
EOF
- name: Gradle 권한 부여
run: chmod +x gradlew
- name: Gradle 빌드 (테스트 제외)
run: ./gradlew clean build -x test
- name: DockerHub 로그인
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker 이미지 빌드 및 푸시
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/lgenius-backend:latest
- name: 배포 파일 zip 압축
run: |
zip -r deploy.zip appspec.yml scripts/
- name: S3 업로드
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: S3에 업로드
run: |
aws s3 cp deploy.zip s3://${{ secrets.S3_BUCKET }}/deploy.zip
- name: CodeDeploy 배포 트리거
run: |
aws deploy create-deployment \
--application-name ${{ secrets.CODEDEPLOY_APP }} \
--deployment-group-name ${{ secrets.CODEDEPLOY_GROUP }} \
--s3-location bucket=${{ secrets.S3_BUCKET }},key=deploy.zip,bundleType=zip
✏️ 9. 배포 상태 확인


✏️ 10. 한계 및 단점
이번 구조는 안정적인 무중단 배포를 가능하게 했지만, 분명 몇 가지 한계점도 존재한다.
우선 CodeDeploy의 경우, AWS 서비스와의 통합은 편리하지만 디버깅이 쉽지 않다는 단점이 있다. 배포가 실패했을 때 로그가 여러 위치에 분산되어 있어 원인을 파악하는 데 시간이 걸릴 수 있었고, 배포 과정이 내부적으로 추상화되어 있어 세밀한 제어에는 한계가 있었다.
또한 Blue-Green 배포 자체의 단점으로는 두 개의 환경을 동시에 유지해야 하기 때문에 리소스 비용이 증가한다는 점이 있다. 특히 EC2 기반 환경에서는 동일한 애플리케이션을 이중으로 운영해야 하므로 비용 부담이 발생할 수 있다.
그리고 구조가 단순한 단일 서버 배포에 비해 구성 복잡도가 증가한다는 점도 고려해야 한다. Nginx, Docker, CodeDeploy, CI/CD 파이프라인까지 여러 요소가 결합되면서 초기 구축과 운영 난이도가 높아졌다.
마지막으로 상태를 가지는 서비스의 경우 세션이나 캐시를 외부 시스템등으로 분리하지 않으면 Blue-Green 전환 시 문제가 발생할 수 있어, 아키텍쳐 설계에 추가적인 고려가 필요했다.
비용과 복잡성이라는 trade-off가 존재했지만, 무중단 배포와 빠른 롤백을 통한 서비스 안전성 확보가 중요하다고 판단하였기에 이 방식을 택하여 배포를 진행하였고 배포도 성공적으로 마무리 하였다. 플젝을 하면서 느끼는 부분이지만 인프라에 관한 지식이 너무 부족한 것 같다. 앞으로는 인프라와 관련된 글들을 공부하고 게시글로 작성해야겠다.
'Back-End > 개인 공부' 카테고리의 다른 글
| [Spring Boot / 스프링 부트] toString()을 왜 오버라이딩해야 하는가 - 보안 로깅 관점 (0) | 2026.03.28 |
|---|---|
| [Spring Boot / 스프링 부트] 카카오 & 네이버 소셜 로그인 구현하기 (OAuth 2.0) (0) | 2026.03.28 |
| [AWS] - SSH 터널링 구성과 AWS Subnet 구조 (0) | 2026.03.09 |
| [Spring Boot / 스프링 부트] - Flyway와 Hibernate의 실행 순위 비교 (0) | 2026.01.18 |
| [AWS] AWS CLI를 이용해서 S3파일 여러 개 다운받기 (0) | 2026.01.02 |
- Total
- Today
- Yesterday
- 반복문
- java
- 스프링 부트 crud 게시판 구현
- BFS
- HTML5
- C++
- Do it!
- 자바스크립트
- 백준
- 이분 매칭
- 백준 풀이
- 우선순위 큐
- 자료구조
- DP
- 투 포인터
- 에라토스테네스의 체
- C++ Stack
- CSS
- 유니온 파인드
- 세그먼트 트리
- 알고리즘 공부
- 유클리드 호제법
- c++ string
- 카운팅 정렬
- js
- html
- 자바
- DFS
- 알고리즘
- 스택
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
