
"왜 이 작업을 해야 하나?"
새로운 기능을 만들거나 개선할 때, 항상 이 질문부터 시작합니다. 기술은 문제를 해결하는 도구일 뿐이고, 도구를 먼저 고르는 건 올바른 순서가 아니라고 생각합니다. 피드줍줍 팀에서 무중단 배포 파이프라인을 구축하기로 결정한 과정도 마찬가지였습니다.
배포란 무엇일까?
배포는 작성한 코드를 사용자가 실제로 사용할 수 있도록 만드는 과정입니다. 개발 환경에서 작동하는 애플리케이션을 운영 서버에 올리고, 사용자의 요청이 새 버전으로 전달되게 합니다.
전통적인 배포 과정은 단순합니다.
- 기존 서버를 중지
- 새 버전을 배포
- 서버를 다시 시작
문제는 이 과정에서 서비스가 중단된다는 점입니다. 서버를 중지하는 순간부터 다시 시작할 때까지, 사용자는 서비스를 이용할 수 없습니다.
왜 무중단이어야 하는가
"배포할 때 잠깐 서비스가 안 되는 게 뭐가 문제인가?"
처음에는 저도 이렇게 생각했습니다. 배포는 자주 하는 일도 아니고, 새벽 시간에 하면 사용자 영향도 최소화할 수 있지 않나? 하지만 우리 팀의 개발 문화와 서비스 특성을 고려하니 생각이 달라졌습니다.
1. 배포는 자주 일어난다
피드줍줍 팀은 빠른 피드백을 중요하게 생각하고 있습니다. 기능을 오래 개발하고 한 번에 배포하는 게 아니라, 작은 단위로 빠르게 배포해서 사용자 반응을 확인합니다. 레벨 3에서도 기존 목표(8주)보다 4주 앞당겨 배포했고, 이후 피드백을 받아가며 개선해 왔습니다.
이런 문화에서 배포는 일주일에도 여러 번 일어날 수 있습니다. 배포할 때마다 서비스가 중단된다면, 사용자 경험은 계속 깨집니다.
2. 언제 배포할지는 비즈니스가 결정해야 한다
새벽 배포는 개발자에게 편하지만, 비즈니스에는 제약입니다. 긴급 버그 수정이 필요한데 새벽까지 기다려야 한다면? 중요한 기능 출시 시점을 새벽으로 맞춰야 한다면?
배포 시간이 기술적 제약에 묶이면, 비즈니스 의사결정도 묶입니다. 무중단 배포는 "언제든 배포할 수 있다"는 자유를 줍니다.
3. 서비스 신뢰도는 쌓이는 것이다
사용자는 한 번의 다운타임을 기억합니다. "가끔 안 돼요"라는 인식이 생기면, 서비스에 대한 신뢰가 떨어집니다. 특히 우리 서비스처럼 사용자가 지속적으로 이용하는 경우, 작은 중단도 큰 불편함이 됩니다.
배포로 인한 다운타임은 피할 수 있는 것이고, 피할 수 있는데 피하지 않는 건 사용자를 소홀히 하는 것이라 생각합니다.
그럼 무중단 배포란 무엇일까요?
무중단 배포란 무엇인가
무중단 배포는 서비스 중단 없이 새 버전을 배포하는 방법입니다. 핵심은 "구버전과 신버전의 트래픽을 중단 없이 옮기는 것"입니다.
간단한 예시로 이해할 수 있습니다.
- 현재 서버 A가 서비스 중
- 서버 B를 새 버전으로 준비
- B가 정상 작동하는지 확인
- 사용자 트래픽을 A에서 B로 옮김
- A를 종료
이 과정에서 사용자는 A든 B든 항상 작동하는 서버에 연결되므로, 서비스 중단을 경험하지 않습니다.
무중단 배포의 종류
무중단 배포에는 여러 전략이 있습니다. 각각은 트레이드오프가 존재합니다.
1. 롤링 배포 (Rolling Deployment)
서버를 하나씩 순차적으로 업데이트합니다.

V1 = 구 버전, V2 = 신 버전
장점:
- 추가 인스턴스가 필요 없어 비용 효율적입니다
- 구현이 비교적 간단합니다
단점:
- 배포 중 구버전과 신버전을 동시에 서비스합니다
- 롤백이 느립니다 (다시 롤링해야 함)
- 데이터베이스 스키마 변경 시 호환성 문제가 생길 수 있습니다
2. 카나리 배포 (Canary Deployment)
소수 사용자에게만 신버전을 먼저 제공합니다.

V1 = 구 버전, V2 = 신 버전
장점:
- 점진적으로 영향 범위를 확대할 수 있습니다
- 문제 발생 시 영향받는 사용자가 제한적입니다
- A/B 테스트에도 활용할 수 있습니다
단점:
- 트래픽을 세밀하게 제어하는 라우팅 로직이 필요합니다
- 모니터링과 의사결정이 복잡합니다
- 완전한 전환까지 시간이 오래 걸립니다
3. 블루-그린 배포 (Blue-Green Deployment)
두 개의 동일한 환경을 유지하고 한 번에 전환합니다.

V1 = 구 버전, V2 = 신 버전
장점:
- 빠른 롤백이 가능합니다 (트래픽만 다시 돌리면 됨)
- 배포 중 버전이 섞이지 않습니다
단점:
- 두 배의 인프라 비용이 듭니다 (일시적)
왜 블루-그린 배포를 선택했는가
피드줍줍 팀은 세 가지 전략을 모두 검토했습니다. 각각의 장단점을 팀의 상황에 대입해 봤습니다.
1. 빠른 롤백 속도
"배포에 문제가 생기면 얼마나 빨리 복구할 수 있는가?"
우리는 배포를 자주 하기로 했습니다. 배포가 잦을수록 문제가 생길 확률도 높아집니다. 중요한 건 문제가 생기지 않게 하는 것보다, 문제가 생겼을 때 빠르게 복구하는 것입니다.
블루-그린 배포에서는 트래픽을 다시 Blue로 돌리기만 하면 됩니다. 30초면 이전 상태로 돌아갑니다. 롤링 배포는 다시 순차적으로 배포해야 하고, 카나리는 트래픽 비율을 조정하며 롤백해야 합니다.
빠른 롤백은 심리적 안정감도 줍니다. "문제 생기면 바로 되돌릴 수 있다"는 확신이 있으면, 배포에 대한 두려움이 줄어듭니다.
2. 버전 혼재 문제
롤링 배포와 카나리 배포는 구버전과 신버전이 동시에 실행됩니다. 이는 예상치 못한 문제를 만들 수 있습니다.
예를 들어
- 세션 데이터 형식이 바뀌었는데, 구버전이 신버전의 세션을 읽으려 할 때
- API 응답 형식이 바뀌었는데, 프론트엔드가 두 형식을 모두 처리해야 할 때
- 데이터베이스 스키마가 바뀌었는데, 구버전과 신버전이 모두 호환되어야 할 때
물론 하위 호환성을 유지하며 개발하면 되지만, 이는 개발 복잡도를 높입니다. 특히 빠르게 개발하고 배포하는 환경에서는 부담이 큽니다.
블루-그린은 명확합니다. 어느 순간에도 한 버전만 서비스합니다. 하위 호환성 걱정 없이 개발할 수 있습니다.
3. 비용 고려
블루-그린의 가장 큰 단점은 비용입니다. 배포 시 두 배의 인스턴스가 필요합니다.
하지만 이는 배포 중에만 필요합니다. 배포가 끝나면 Blue 환경을 종료하므로, 평소에는 추가 비용이 없습니다. 배포에 10분 정도 걸린다면, 10분 동안만 인스턴스가 2배로 늘어나는 것입니다.
그리고 이 비용은 다른 비용과 비교해야 합니다
- 배포 실패로 서비스가 중단된 시간의 기회비용
- 버전 혼재로 인한 버그를 디버깅하는 시간
- 롤백이 느려서 사용자가 불편을 겪는 시간
개발자의 시간과 사용자 경험을 생각하면, 블루-그린의 인프라 비용은 충분히 투자할 만하다고 판단했습니다.
4. 팀의 상황 고려
카나리 배포는 트래픽 라우팅, 모니터링, 의사결정이 복잡합니다. 몇 퍼센트까지 늘릴지, 언제 다음 단계로 넘어갈지, 어떤 지표를 봐야 하는지 등을 결정해야 합니다.
우리 팀은 인프라보다 백엔드 개발에 집중해야 했습니다. 블루-그린은 간단합니다. Green 환경을 띄우고, 테스트하고, 트래픽을 전환합니다. 복잡한 의사결정이 없습니다.
"완벽한 것보다 작동하는 것이 낫다"는 원칙에 따라, 팀에서 관리할 수 있는 수준의 복잡도를 선택했습니다.
구현: 생각을 코드로
선택의 근거를 정리했으니, 이제 구현 이야기입니다.
기술 스택 선택
블루-그린 배포를 구현하는 방법은 여러 가지입니다
- Kubernetes
- AWS CodeDeploy
- Docker Swarm
- 직접 구현
- 등등..
피드줍줍 팀은 AWS CodeDeploy를 선택했습니다. 이유는 다음과 같았습니다.
- 이미 AWS 인프라를 사용 중
- Auto Scaling Group과 통합이 쉽다
- 직접 구현하는 것보다 검증된 솔루션이다
- Kubernetes는 우리 규모에 오버엔지니어링이다
CodePipeline, CodeBuild는 왜 사용하지 않았는가?
AWS는 완전한 CI/CD 파이프라인을 위해 CodePipeline, CodeBuild, CodeDeploy를 함께 제공합니다.

하지만 우리는 CodeDeploy만 사용하기로 했습니다. 이유는 세 가지입니다.
1. 이미 작동하는 파이프라인을 마이그레이션 할 필요가 없다.
GitHub Actions로 구축한 빌드 파이프라인이 이미 잘 작동하고 있었습니다. 코드 컴파일, 테스트, Docker 이미지 빌드까지 모두 안정적으로 수행되고 있었습니다.

작동하는 시스템을 굳이 마이그레이션할 이유가 없었습니다. CodePipeline과 CodeBuild가 하는 역할을 GitHub Actions가 이미 수행하고 있었기 때문입니다.
2. CodeDeploy를 사용한 이유는 명확하다.
반대로 CodeDeploy를 도입한 이유는 확실했습니다.
- Blue-Green 트래픽 전환 로직: CodeDeploy가 자동으로 처리
- ASG 생성 및 관리: Green 환경 프로비저닝을 자동화
- 배포 안전성: 헬스 체크 실패 시 자동 롤백을 지원
이런 기능들을 GitHub Actions에서 직접 구현하는 건 복잡하고 오류가 발생하기 쉽습니다.
3. CodeBuild는 비용이 추가된다.
CodeBuild는 빌드 시간에 따라 비용이 청구됩니다.
- 분당 $0.005 (일반 인스턴스 기준)
- 빌드에 5분이 걸린다면 배포 1회당 $0.025
GitHub Actions는 퍼블릭 저장소에서 무료이고, 프라이빗 저장소도 월 2,000분을 무료로 제공합니다. 우리 팀의 빌드 빈도를 고려하면 GitHub Actions로 충분했습니다.
"필요한 부분만 도입한다"는 원칙에 따라, 배포 자동화만 CodeDeploy로 마이그레이션 했습니다. 전체 파이프라인을 AWS로 옮기는 것보다, 각 도구의 강점을 활용하는 하이브리드 방식이 피드줍줍 팀에게 더 적합했습니다.
무중단 배포 흐름: 7단계 파이프라인
배포 파이프라인을 전체적으로 보면 7단계입니다
- Push - be/production 브랜치에 소스코드 푸시
- Trigger - GitHub Actions 워크플로우 시작
- Push - Docker 이미지를 Docker Hub에 푸시
- Upload Scripts - S3에 배포 아티팩트 업로드
- Run CodeDeploy - AWS CodeDeploy 배포 시작
- Copy Scripts - Green ASG 인스턴스에 스크립트 복사
- Pull Image - 새 Docker 이미지를 다운로드하고 컨테이너 실행
각 단계는 명확한 책임을 가집니다. "코드를 빌드한다", "이미지를 배포한다", "트래픽을 전환한다". 단계를 쪼개니 문제가 생겼을 때 어디서 실패했는지 바로 알 수 있었습니다.
GitHub Actions: 빌드와 배포의 분리
워크플로우는 be-deploy-prod.yml 파일에 정의했습니다. be/production 브랜치의 backend/** 경로에 변경사항을 푸시하면 자동으로 실행됩니다.
Job은 Build와 Deploy 두 개로 나눴습니다.
Build Job
Build Job은 GitHub Actions의 ubuntu-latest 환경에서 실행됩니다
- 소스코드 체크아웃 및 서브모듈 최신화
- JDK 21 설정
- Gradle로 JAR 빌드
- Docker 이미지 빌드
- Docker Hub에 이미지 푸시
Deploy Job
Deploy Job은 조금 특별합니다. GitHub Actions 서버가 아닌, 운영 환경 EC2에서 실행됩니다 (self-hosted runner)
이유는 보안 때문입니다. 우아한테크코스에서 제공하는 AWS 환경은 S3로의 접근 권한을 EC2 IAM Role로만 제한하고 있습니다.
GitHub Actions 환경에 S3 접근 권한을 주면, GitHub Secrets가 노출될 경우 S3가 노출될 위험이 있습니다.
Deploy Job이 하는 일
1. 배포 아티팩트 준비
서브모듈에서 다음 파일들을 가져옵니다.
- appspec.yml (CodeDeploy 배포 명세)
- scripts/*.sh (배포 스크립트)
- docker-compose.prod.yml (Docker Compose 설정)
- .env (환경 변수)
특히 docker-compose.yml을 매번 복사하는 이유는, compose 파일의 설정이 바뀔 때마다 AMI를 다시 만드는 게 비효율적이기 때문입니다.
그리고 준비한 파일들을 deployment.zip으로 압축합니다.
2. S3 업로드
deployment.zip을 S3 버킷에 업로드합니다. CodeDeploy는 나중에 이 파일을 다운로드해서 배포합니다.
3. CodeDeploy 배포 실행
aws deploy create-deployment \
--application-name feed-zupzup-prod-deploy \
--deployment-group-name feedzupzup-prod-deploy-group \
--s3-location bucket=...,key=...,bundleType=zip
aws deploy create-deployment 명령어로 배포를 생성합니다. 그리고 aws deploy wait deployment-successful로 배포가 끝날 때까지 기다립니다.
이 명령어가 끝나면 배포가 완료된 것입니다.
CodeDeploy: Blue-Green 배포
CodeDeploy Agent가 실제 배포를 진행합니다. 네 단계로 구성됩니다.

1단계: Green ASG 인스턴스 프로비저닝
새 버전을 배포할 Green 환경의 인스턴스를 시작합니다.
EC2 인스턴스가 부팅되고, 네트워크가 설정되고, CodeDeploy Agent가 준비되는 시간입니다. 하지만 사용자는 Blue 환경을 계속 사용하므로 문제가 없습니다.
2단계: Green 환경에 새 버전 배포
Green ASG의 인스턴스에서 다음 이벤트가 순서대로 실행됩니다.
| 이벤트 | 설명 |
| ApplicationStop | 실행 중인 프로세스 정리 |
| DownloadBundle | S3에서 deployment.zip 다운로드 |
| BeforeInstall | 배포 전 준비 작업 |
| Install | 파일을 EC2에 복사 |
| AfterInstall | afterinstall.sh 스크립트 실행 |
| ApplicationStart | 새 애플리케이션 시작 |
| ValidateService | 헬스 체크 |
여기서 작성한 afterinstall.sh 스크립트가 실행됩니다.
3단계: Green으로 트래픽 라우팅
Blue 환경에서 Green 환경으로 트래픽을 점진적으로 옮깁니다.
1. BeforeAllowTraffic
- 준비 작업
2. AllowTraffic
- 로드밸런서 재설정
- Blue → Green 트래픽 전환
- ALB 대상 등록
- Health Check 확인 (1분)
- 세션 드레이닝 시작 (기존 Blue 연결 유지)
- 신규 요청을 Green으로 라우팅
- 트래픽 전환 검증
3. AfterAllowTraffic
- 정리 작업
Blue에 연결된 기존 사용자는 계속 Blue를 사용하고, 새 요청만 Green으로 갑니다. 이렇게 하면 사용자는 전환을 느끼지 못합니다.
4단계: Blue 환경 정리
트래픽이 완전히 전환되면 Blue를 종료합니다.
1. BeforeBlockTraffic
- Blue 차단 전 준비
2. BlockTraffic
- 로드밸런서에서 Blue 완전 제거
- 남은 연결 모두 완료 대기
- 타임아웃된 연결들 확인
- Graceful shutdown 대기
- 강제 종료
3. AfterBlockTraffic
- Blue 인스턴스 종료
Graceful shutdown을 위해 진행 중인 요청이 모두 완료될 때까지 기다립니다.
appspec.yml: 배포 동작 정의
CodeDeploy는 appspec.yml 파일로 배포 동작을 정의합니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/deploy
file_exists_behavior: OVERWRITE
permissions:
- object: /
pattern: "**"
owner: ubuntu
group: ubuntu
mode: 755
hooks:
AfterInstall:
- location: scripts/afterinstall.sh
timeout: 180
runas: ubuntu
- files: deployment.zip의 내용을 /home/ubuntu/deploy에 복사
- permissions: 파일 권한 설정 (소유자 ubuntu, 권한 755)
- hooks: AfterInstall 이후 afterinstall.sh 실행
afterinstall.sh: 실제 배포 로직
afterinstall.sh 스크립트가 실제 배포를 진행합니다. 5단계로 구성했습니다.
1. 서버 이름 생성
INSTANCE_ID=$(curl -s http://{IP}/latest/meta-data/instance-id)
SERVER_NAME="prod-${INSTANCE_ID}"
EC2 메타데이터 서버에서 인스턴스 ID를 조회해서 서버 이름을 만듭니다. 이를 통해 모니터링할 때도 어느 인스턴스인지 파악할 수 있습니다.
2. 환경 변수 파일 생성
cat > $DEPLOY_DIR/.env << EOF
SERVER_NAME=${SERVER_NAME}
SERVER_IMAGE_TAG=${SERVER_IMAGE_TAG}
EOF
.env 파일에 SERVER_NAME과 SERVER_IMAGE_TAG를 설정합니다.
3. 설정 파일 복사
sudo cp $DEPLOY_DIR/docker-compose.yml /home/ubuntu/docker/
sudo cp $DEPLOY_DIR/.env /home/ubuntu/docker/
sudo cp $DEPLOY_DIR/config.alloy /home/ubuntu/docker/data/alloy/
docker-compose.yml, .env, config.alloy를 최종 위치로 복사합니다.
4. Docker 컨테이너 재시작
cd /home/ubuntu/docker
docker-compose down
docker-compose up -d
여기서 의문이 생길 수 있습니다. "왜 기존 컨테이너를 중지하나? Green은 새 인스턴스 아닌가?"
맞습니다. 하지만 EC2 시작 템플릿의 User Data에서 컨테이너를 미리 실행합니다. 이유는 Auto Scaling 때문입니다.
스케일 아웃으로 인스턴스가 생성될 때는 CodeDeploy가 실행되지 않습니다. 그래도 서비스는 시작되어야 합니다.
5. 헬스 체크
for i in {1..60}; do
if curl -s -f http://localhost:80/actuator/health > /dev/null 2>&1; then
echo "✅ 배포 성공"
exit 0
fi
sleep 2
done
echo "❌ 배포 실패"
exit 1
최대 60회(2초 간격, 총 2분) 동안 헬스 체크를 시도합니다. 실패하면 배포가 중단되고 Blue 환경이 유지됩니다.
이게 블루-그린의 핵심 안전장치입니다. Green이 제대로 작동하지 않으면 트래픽 전환이 일어나지 않습니다.
실제 배포는 어떻게 진행되는가
지금까지 각 단계를 설명했지만, 실제로 배포가 어떻게 흘러가는지 정리해 보겠습니다.
타임라인
GitHub Actions 실행 흐름
1. 개발자가 be/production에 코드 푸시
2. GitHub Actions 워크플로우 시작
3. JAR 빌드 완료
4. Docker 이미지 빌드 및 푸시 완료
5. 배포 아티팩트 준비 및 S3 업로드
6. CodeDeploy 배포 생성
CodeDeploy 실행 흐름
1. Green 인스턴스 프로비저닝
2. Green에 새 버전 배포
3. afterinstall.sh 실행
1. Docker 컨테이너 시작
2. 헬스 체크 시작
3. 헬스 체크 성공
4. Green으로 트래픽 라우팅
1. ALB에 Green 등록
2. Health Check 확인 완료
3. 신규 요청 Green으로 전환
4. Blue 세션 드레이닝 시작
5. Blue 환경 정리
1. Blue 트래픽 완전 차단
2. Blue 인스턴스 종료
배포 완료
전체 과정이 약 15~20분 정도 소요됩니다. 이 중 사용자에게 영향을 주는 시간은 0분입니다. 항상 Blue든 Green이든 작동하는 서버가 있기 때문입니다.
문제가 생기면?
만약 헬스 체크가 실패한다면?
배포가 즉시 중단됩니다. Green 인스턴스는 종료되고, Blue는 계속 서비스합니다. 사용자는 아무것도 모릅니다.
이게 블루-그린의 가장 큰 장점입니다. 실패해도 안전합니다.
개선 계획
글을 쓰면서 떠오른 개선하고 싶은 부분들도 같이 정리하려고 합니다.
1. 이미지 태그 단순화
현재는 커밋 SHA로 이미지를 태깅합니다. 원래 롤백을 위해 여러 버전을 이미지로 갖고 있으려 했지만, 블루-그린에서는 필요 없습니다. Blue 환경 자체가 이전 버전이기 때문입니다. 이렇게 하면 .env 파일도 단순해집니다. (물론 커밋 해시로 hub에서 버전 관리는 여전히 필요합니다!)
2. Alloy 설정 자동화
현재 config.alloy 파일은 수동으로 관리합니다. 이것도 docker-compose처럼 서브모듈에서 자동으로 가져오도록 개선하면 좋을 것 같습니다. alloy 설정 배포 또한 자동화할 수 있습니다.
3. 헬스 체크를 validateService 시점에 진행
현재는 afterinstall.sh 스크립트 내부에서 헬스 체크를 진행합니다. 하지만 CodeDeploy는 ValidateService라는 별도의 이벤트를 제공합니다. 헬스 체크를 ValidateService 훅으로 분리하면 책임이 더 명확해집니다.
- AfterInstall: 컨테이너 실행까지만 담당
- ValidateService: 서비스가 정상 작동하는지 검증
또한 CodeDeploy의 라이프사이클과 더 자연스럽게 맞아떨어집니다. ValidateService는 "배포된 서비스를 검증한다"는 명시적인 의도를 가진 단계이기 때문입니다.
참고 자료
AWS 공식 문서
- AWS CodeDeploy 문서
- AWS CodeDeploy - Blue/Green 배포
- AWS CodeDeploy - AppSpec 파일 참조
- AWS CodeBuild 가격 정책
- AWS CodePipeline 문서
- AWS Auto Scaling Groups
https://docs.aws.amazon.com/codedeploy/
docs.aws.amazon.com
'woowacourse > project' 카테고리의 다른 글
| 파일 다운로드 경험 개선: 비동기 작업 분리, Polling 기반 진행률 추적하기 (0) | 2025.10.12 |
|---|---|
| 병렬 처리하면 무조건 좋을까? : BlockingQueue로 파일 다운로드 성능 & OOM 개선하기 (0) | 2025.10.07 |
| 실시간 로그 모니터링 시스템 구축하기 (Alloy, Loki, Grafana) (4) | 2025.08.03 |
| 로깅, 왜 해야할까? (1) | 2025.08.02 |