메랜샵 - 회원 1만명 돌파 후 마주한 현실: AWS 블루-그린 배포 적용

Murhyun2·2025년 8월 24일
3

메랜샵

목록 보기
4/7
post-thumbnail

성장의 기쁨과 새로운 과제

메랜샵이 출시 한 달 만에 회원 1만명을 돌파했습니다. 정말 기쁜 일이었지만, 동시에 예상치 못한 문제들이 연달아 발생했습니다.

아래 코드, 엔티티명, API 경로, 데이터 값은 모두 임의의 예시이며 실제 서비스와 무관합니다.

가장 큰 문제는 배포였습니다.

기존에는 새로운 기능을 배포할 때마다 1-2분간 서비스가 완전히 중단되었습니다. 사용자들이 거래를 진행하던 중에 갑자기 서버가 멈추는 상황이 발생하니 최대한 사용자들이 적은 새벽 시간대에 배포를 했죠.

더 심각했던 것은 트래픽이 순간적으로 몰릴 때였습니다. 홍보로 인해 특정 시간대에 사용자들이 몰려들면 서버가 버티지 못하고 느려지는 현상이 발생했습니다.

"거래 등록하려고 하는데 계속 로딩만 돌아요"
"새벽에 가끔 서비스가 안 되네요"

실제 사용자들의 피드백이었습니다.

기존 구조

문제의 근본 원인은 단일 EC2 인스턴스에 모든 것을 몰아넣은 구조에 있었습니다.

[단일 EC2 인스턴스]
├── Nginx (React 프론트엔드 서빙, 리버스 프록시)
├── Docker (Spring Boot 백엔드)
└── Docker (MySQL 데이터베이스)

이 구조의 문제점들:

  • 배포 시 전체 서비스 중단: 새 인스턴스 생성 → 초기 캐싱 → 구 인스턴스 종료
  • 확장성 부족: 트래픽 급증 시 수동으로 대응해야 함
  • 단일 장애점: 하나의 EC2가 문제생기면 전체 서비스 마비
  • 리소스 비효율: 프론트엔드와 백엔드가 같은 서버 자원을 공유

만명이 넘는 회원을 가진 서비스라기에는 명백히 부족한 구조였습니다.

해결 전략: 전체 아키텍처 재설계

문제를 근본적으로 해결하기 위해 전체 시스템을 재설계하기로 결정했습니다.

목표

  • 완전 무중단 배포: 사용자가 서비스 중단을 전혀 느끼지 못하게
  • 자동 확장: 트래픽 급증 시 자동으로 서버 확장
  • 장애 격리: 각 컴포넌트 분리로 장애 전파 방지
  • 배포 속도 향상: CI/CD 파이프라인 최적화

새로운 아키텍처 설계

[프론트엔드]
S3 + CloudFront (정적 호스팅)

[백엔드]
ALB →
[Blue Environment][Green Environment]
├── ASG (Auto Scaling Group)
├── EC2 Instances (n개)
└── Target Group

[데이터베이스]
RDS (MySQL)

[CI/CD]
GitHub Actions → ECR → CodeDeploy

핵심 변경 사항:

  • 프론트엔드 분리: S3 + CloudFront로 CDN 활용
  • 블루-그린 배포: ALB + CodeDeploy로 무중단 배포
  • 오토스케일링: ASG로 자동 서버 확장/축소
  • 데이터베이스 분리: RDS로 관리형 데이터베이스 사용

이제 각 단계별로 실제 구현 과정과 마주했던 문제들을 살펴보겠습니다.

1단계: 프론트엔드 분리 - nginx에서 S3+CloudFront로

첫 번째로 프론트엔드를 백엔드에서 완전히 분리했습니다.

기존 방식의 문제점

nginx# EC2 내 nginx.conf
server {
    listen 80;
    
    # React 빌드 파일 서빙
    location / {
        root /var/www/html;
        try_files $uri $uri/ /index.html;
    }
    
    # API 요청 프록시
    location /api {
        proxy_pass http://localhost:8080;
    }
}

이 구조에서는 프론트엔드 업데이트만 해도 전체 서버를 재배포해야 했습니다.

S3 + CloudFront 도입

S3 정적 웹사이트 호스팅 설정:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::mashop/*"
    }
  ]
}

CloudFront 배포 설정:

  • Origin: S3 버킷
  • Default Root Object: index.html
  • Error Pages: 404 → /index.html (SPA 라우팅 지원)
  • Caching: 정적 자원 캐싱으로 빠른 로딩

개선 효과

  • 프론트엔드 독립 배포: 백엔드와 완전히 분리
  • 전세계 CDN: CloudFront로 빠른 로딩 속도
  • 비용 절감: EC2 리소스를 백엔드에만 집중
  • 확장성: 트래픽이 늘어도 S3가 자동 처리

프론트엔드 분리로 이제 백엔드에만 집중할 수 있게 되었습니다.

2단계: 블루-그린 배포 구현 - 가장 복잡했던 여정

이제 본격적으로 무중단 배포를 구현해야 했습니다. 이 과정이 전체 프로젝트에서 가장 복잡하고 어려웠던 부분이었습니다.

AWS 서비스 조합

  • ALB (Application Load Balancer): 트래픽 라우팅
  • ASG (Auto Scaling Group): 인스턴스 관리
  • CodeDeploy: 블루-그린 배포 자동화
  • ECR (Elastic Container Registry): Docker 이미지 저장
  • IAM: 권한 관리

1) ALB와 Target Group 설정

가장 먼저 AWS 콘솔에서 두 개의 Target Group을 생성했습니다.

Target Group 설정:
- Name: mashop-blue-tg / mashop-green-tg
- Protocol: HTTP
- Port: 8080
- VPC: 기존 VPC 선택
- Health Check Path: /health

핵심 설정:
- Health Check: /health 엔드포인트로 인스턴스 상태 확인
- Healthy Threshold: 2회 연속 성공 시 healthy 판정
- Unhealthy Threshold: 3회 연속 실패 시 unhealthy 판정

2) 보안 그룹 설정 - 가장 중요한 보안 요소

ALB에서 EC2로만 트래픽이 흐르도록 보안 그룹을 설정했습니다.

ALB 보안 그룹 (mashop-alb-sg):

Inbound:
- HTTP (80) - 0.0.0.0/0 (전체 인터넷)
- HTTPS (443) - 0.0.0.0/0 (전체 인터넷)
Outbound: All traffic

EC2 보안 그룹 (mashop-ec2-sg):

Inbound:
- HTTP (8080) - Source: mashop-alb-sg (ALB 보안그룹만 허용)
- SSH (22) - My IP (관리용)
Outbound: All traffic

이렇게 설정하면 외부에서 EC2에 직접 접근이 불가능하고, 반드시 ALB를 거쳐야만 백엔드에 접근할 수 있게 됩니다.

3) CodeDeploy 애플리케이션 설정

# appspec.yml
version: 0.0
os: linux
hooks:
  BeforeInstall:
    - location: scripts/stop_application.sh
      timeout: 120
      runas: root
  ApplicationStart:
    - location: scripts/start_application.sh
      timeout: 300
      runas: root
  ValidateService:
    - location: scripts/validate_service.sh
      timeout: 50
      runas: root
  AfterAllowTraffic:
    - location: scripts/smoke_alb.sh
      timeout: 150
      runas: root
      

가장 복잡했던 IAM 권한 설정.. 😂
CodeDeploy가 ASG와 ALB를 조작할 수 있도록 권한을 설정하는 것이 가장 까다로웠습니다.

4) 트러블슈팅: 스모크 테스트 실패

배포 과정에서 스모크 테스트가 계속 실패하는 문제가 발생했습니다.

  • 문제: 새로운 인스턴스가 시작된 직후 Health Check가 실패
  • 원인: Spring Boot 애플리케이션 완전 기동 전에 Health Check 시작
  • 해결: smoke_alb.sh에 재시도 로직 및 대기시간 추가

5) 트래픽 전환 과정

CodeDeploy의 블루-그린 배포 과정:

  1. Green 환경 생성: 새로운 ASG에 인스턴스 시작
  2. Health Check: Green 인스턴스들이 정상 동작하는지 확인
  3. 스모크 테스트: /health, /api/status 엔드포인트 테스트
  4. 트래픽 전환: ALB가 Blue → Green으로 트래픽 라우팅
  5. Blue 환경 정리: 기존 인스턴스들 종료

핵심은 이 모든 과정이 자동으로 진행되며, 문제가 생기면 자동으로 롤백된다는 점이었습니다.

3단계: 오토스케일링 도입 - 트래픽 급증에 대비한 자동 확장

블루-그린 배포를 구현했지만, 여전히 한 가지 불안 요소가 남아있었습니다. 예전에 홍보로 인해 사용자가 순간적으로 몰려서 서버가 느려졌던 경험이 있었기 때문입니다.

"거래 등록하려고 하는데 계속 로딩만 돌아요"

이런 상황이 다시 발생하지 않도록 Auto Scaling Group(ASG)을 통한 자동 확장 시스템을 구축했습니다.

ASG 기본 설정

먼저 인스턴스 타입을 기존 t3.micro에서 t3.small로 업그레이드했습니다.
더 안정적인 성능을 위한 선택이었죠.

Auto Scaling Group 설정:
- 최소 인스턴스: 1개
- 원하는 용량: 1개  
- 최대 인스턴스: 3개
- 인스턴스 타입: t3.small
- 가용 영역: ap-northeast-2a, ap-northeast-2c

CloudWatch 메트릭 기반 스케일링 정책

가장 중요한 부분은 언제 스케일링을 할 것인가를 결정하는 정책이었습니다.

스케일 아웃 정책 (Scale Out)
조건: CPU 사용률이 70% 이상
기간: 2분간 연속 유지
액션: 인스턴스 1개 추가
쿨다운: 5분

CPU 70%를 기준으로 설정한 이유는 CloudWatch 지표에 몇 분간의 지연이 있기 때문입니다. 80-90%로 설정하면 실제 부하가 임계점에 도달했을 때 이미 늦은 상황이 될 수 있어서, 여유있게 70%로 설정했습니다.

스케일 인 정책 (Scale In)

조건: CPU 사용률이 30% 미만
기간: 15분간 연속 유지  
액션: 인스턴스 1개 제거
쿨다운: 10분

스케일 인은 좀 더 신중하게 접근했습니다. 트래픽이 일시적으로 줄어들었다가 다시 늘어날 수 있기 때문에, 15분간 안정적으로 낮은 상태를 유지해야만 인스턴스를 제거하도록 설정했습니다.

실제 테스트 - stress 명령어로 검증

설정이 올바르게 동작하는지 확인하기 위해 직접 부하 테스트를 진행했습니다.

# EC2 인스턴스에서 부하 테스트
sudo yum install -y stress
stress -c 2 -t 900s  # 2개 CPU 코어에 15분간 부하

테스트 결과:

  • 약 3-4분 후 CloudWatch에서 CPU 사용률 70% 초과 감지
  • 5분 쿨다운 후 새로운 인스턴스 생성 시작
  • 새 인스턴스가 완전히 기동되고 ALB에 등록되기까지 약 2-3분
  • 총 8-9분 만에 자동 확장 완료
    부하를 중단한 후에는 예상대로 15분 정도 지켜본 뒤 추가 인스턴스가 자동으로 종료되었습니다.

메트릭 조합 - CPU만으로는 부족했던 이유

처음에는 CPU만 모니터링했는데, 실제 운영해보니 다른 지표도 함께 봐야 한다는 것을 깨달았습니다.

추가 모니터링 지표:
- NetworkIn/NetworkOut: API 호출 급증 감지
- TargetResponseTime: 응답 시간 지연 감지  
- RequestCountPerTarget: 요청 분산 상태 확인

특히 TargetResponseTime이 중요했습니다. CPU는 여유로워도 데이터베이스 쿼리가 복잡해서 응답이 지연되는 경우가 있었거든요.

물론 완벽하지는 않았습니다.

한계:

  • CloudWatch 지연으로 인한 2-3분의 반응 지연
  • 새 인스턴스 부팅 시간 추가 2-3분
  • 급작스러운 트래픽 스파이크에는 여전히 취약

개선 방향:

  • 예측 스케일링(Predictive Scaling) 도입 검토
  • 더 빠른 부팅을 위한 AMI 최적화 (이후 단계에서 적용)
  • Lambda와 연동한 커스텀 메트릭 활용

실제 효과 - 안전장치로서의 역할

이미 인스턴스 사양을 올렸기에 다행히 ASG 설정 이후 실제로 스케일링이 필요할 정도의 대규모 트래픽은 아직 경험하지 않았습니다. 하지만 심리적 안정감이 컸죠. 이제는 예상치 못한 상황에 대한 자동 대응 체계가 갖춰진 상태입니다.

4단계: 데이터베이스 마이그레이션 - 메이플랜드 점검시간 활용

회원 수가 1만 명을 넘어서면서 단일 EC2 내의 MySQL Docker 컨테이너로는 한계가 명확했습니다. 관리형 데이터베이스 RDS로 마이그레이션하기로 결정했습니다.

가장 큰 고민은 언제 마이그레이션을 할 것인가였습니다. 메랜샵은 메이플랜드 자리 거래 서비스라 메이플랜드 서버 점검시간을 활용하기로 했습니다.

마이그레이션 과정

# 1. 기존 DB 덤프 생성
mysqldump -u root -p ma_db > backup.sql

# 2. RDS 인스턴스 생성 및 설정
# - db.t3.micro (추후 모니터링 후 확장 예정)  
# - 다중 AZ 배치로 가용성 확보

# 3. 데이터 복구
mysql -h rds-endpoint -u admin -p ma_db < backup.sql

# 4. 애플리케이션 설정 변경
# application.yml의 datasource URL 변경

메이플랜드 점검시간을 활용했기 때문에 사용자들에게는 거의 영향을 주지 않았습니다.

5단계: CI/CD 최적화 - AMI와 GitHub Actions 캐싱

AMI 도입으로 배포 시간 단축

기존에는 EC2 인스턴스가 새로 생성될 때마다 초기 설정 스크립트를 실행해야 했습니다:

# 기존 user-data.sh

#!/bin/bash
set -euxo pipefail

# 1. 2GB 가상 메모리(Swap) 설정
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# 재부팅 시에도 활성화되도록 /etc/fstab에 추가
echo '/swapfile none swap sw 0 0' | tee -a /etc/fstab

# 2. Docker 설치 및 설정
yum install -y docker
systemctl start docker
systemctl enable docker
# ec2-user가 sudo 없이 docker 명령어를 사용하도록 권한 설정
usermod -aG docker ec2-user
echo ">>> Docker 설치 및 설정 완료"

# 3. CodeDeploy 에이전트 설치
yum install -y ruby wget
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x ./install
./install auto
echo ">>> CodeDeploy 에이전트 설치 완료"

# 4. API 디렉토리 생성
mkdir -p /home/ec2-user/api
chown -R ec2-user:ec2-user /home/ec2-user/api
echo ">>> API 디렉토리 생성 완료"

# ... 기타 설정들

문제점:

  • 매번 패키지 업데이트와 Docker 설치로 3-4분 소요

해결책:

  • 사전에 모든 설정이 완료된 커스텀 AMI 생성
    - 필요한 패키지 모두 설치 완료
    - Docker 및 기본 설정 완료
    - CodeDeploy 에이전트 설치 완료

결과:

  • 인스턴스 부팅 시간을 3-4분 → 1분으로 단축

GitHub Actions 빌드 캐싱

CI/CD 파이프라인의 빌드 시간도 최적화했습니다.

# .github/workflows/deploy.yml (핵심 부분)

      # --- 2. Java 및 Gradle 설정 ---
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: gradle
      # ... 기타 설정들
      
      # --- 4. Docker 이미지 빌드 및 ECR 푸시 (캐싱 적용) ---
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          # 캐싱을 위한 빌더를 사용하도록 설정
          driver-opts: image=moby/buildkit:v0.14.0        

      - name: Build, tag, and push image to Amazon ECR with cache
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker buildx build \
            --platform linux/arm64 \
            --tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
            --tag $ECR_REGISTRY/$ECR_REPOSITORY:latest \
            --push \
            --cache-from type=registry,ref=$ECR_REGISTRY/$ECR_REPOSITORY:cache \
            --cache-to type=registry,ref=$ECR_REGISTRY/$ECR_REPOSITORY:cache,mode=max,image-manifest=true,oci-mediatypes=true .

Docker 이미지 레이어를 ECR에 캐시로 저장해두고, 다음 빌드 시 변경되지 않은 레이어는 재사용하여 빌드 속도를 획기적으로 높이는 원리입니다

개선 결과:

  • 기존: 1분 21초 (매번 의존성 다운로드)
  • 개선 후: 31초 (캐시 활용)
  • 약 62% 시간 단축

최종 성과 - 완전 무중단 + 자동 확장 시스템 달성

Before vs After

기존 아키텍처:

[단일 EC2]
├── Nginx (React 서빙)
├── Docker (Spring Boot)
└── Docker (MySQL)

문제점:
- 배포 시 1-2분 서비스 중단
- 트래픽 급증 시 수동 대응
- 단일 장애점 존재
새로운 아키텍처:

[Frontend] S3 + CloudFront
[Backend] ALB → ASG (1~3 인스턴스) 
[Database] RDS MySQL
[CI/CD] GitHub Actions → ECR → CodeDeploy

달성한 것:
- 완전 무중단 배포
- 자동 확장/축소 (CPU 70% 기준)  
- 장애 격리 및 복구 자동화
- 배포 시간 대폭 단축

수치로 보는 개선 효과

항목기존개선 후
배포 다운타임1-2분0초
CI/CD 빌드 시간1분 21초31초
인스턴스 확장수동자동 (8-9분)
트래픽 대응단일 서버최대 3배 확장

남은 과제와 향후 계획

  • 예측 스케일링 도입으로 반응 속도 개선
  • RDS 성능 모니터링 및 최적화
  • 장애 알람 시스템 고도화

마치며

드디어 끝마친 이번 아키텍처 개선을 통해 10만 명 그 이상까지도 안정적으로 서비스할 수 있는 기반을 마련했습니다.

단일 EC2 인스턴스 비용에서 ALB, RDS, 데이터 전송 비용 등이 추가되었지만, 이는 유연한 운영과 안정성을 위한 투자라고 생각했습니다. 또한 S3의 저렴한 비용 덕분에 트래픽 대비 비용 효율은 오히려 개선되었습니다.

무엇보다 이번 개선을 통해 사용자들은 더 이상 예기치 못한 서비스 중단 없이 안정적으로 서비스를 이용할 수 있어 뿌듯했습니다. (동시에, 새벽 배포로 인한 스트레스가 사라진 것이 가장 큰 성과라고 생각합니다. 😊)

profile
왜?를 생각하며 개발하기

0개의 댓글