현재의 개발에서는 지속적 배포(Continuous Deployment, CD)를 통해 빠르고 안정적인 서비스 제공이 필수가 되었습니다. 특히, MSA(Microservices Architecture)를 채택한 프로젝트에서는 각 서비스의 독립성과 배포 효율성을 극대화하는 파이프라인 설계가 중요합니다.
이번 글에서는 멀티모듈 MSA 프로젝트를 기반으로, Github Actions와 Docker, 그리고 EC2를 활용해 배포 자동화를 구성한 경험과 주요 고려 사항을 공유하려 합니다.
이 글을 통해 다음 내용을 얻을 수 있습니다
- 멀티모듈 프로젝트에서 CI/CD의 필요성과 구성 전략
- Github Actions의 workflow를 활용한 단계별 자동화
- Docker 이미지를 빌드하고, EC2로 배포하는 실무적인 접근법
- 실사용 중 발생했던 이슈와 해결 방안
프로젝트는 멀티모듈 기반의 MSA(Microservices Architecture)로 설계되었으며, 각 기능별로 독립적인 모듈로 분리되어 있습니다. 이러한 구조를 통해 서비스 간 의존성을 최소화하고, 독립적인 배포 및 확장이 가능하도록 구성했습니다.
아래는 주요 기능별로 모듈이 구성된 방식입니다
공통 모듈 (GlowGrow)
- GlowGrow-common: 공통 유틸리티, DTO, 예외 처리 등 모든 서비스에서 사용하는 공통 코드
- GlowGrow-security: JWT 기반 인증/인가 및 보안 관련 설정
- GlowGrow-kafka: Kafka 관련 설정
- GlowGrow-redis: Redis 관련 설정
도메인 서비스 모듈
- Auth: JWT를 활용한 로그인/회원 인증 기능
- Post: 게시글 및 프로필 관리, S3 이미지 업로드, 검색 및 인기 정렬 기능
- Reservation: 디자이너 예약 타임테이블 관리, 리뷰/신고 생성 및 상태 관리
- Grade: 예약/리뷰/신고 데이터를 기반으로 회원 등급 계산 및 관리
- Payment: Toss API를 활용한 결제 및 정산 관리
- Promotion: 프로모션/쿠폰 관리 및 Redis 동시성 처리
- Notification: Kafka 이벤트 기반 알림 처리
- Chat: WebSocket 기반 실시간 채팅, Kafka 브로커를 통한 메시지 전달
- Multimedia: 파일 업로드/다운로드 및 관리
인프라/운영 관련 모듈
- Gateway: API Gateway를 통한 클라이언트 요청 라우팅 및 인증 처리
- Eureka: 서비스 디스커버리 및 레지스트리
- Logging/Monitoring: Loki, Prometheus, Grafana를 활용한 로그 및 성능 모니터링
배포 자동화와 관련된 도구는 많지만 Github Actions + Docker 조합은 구성과 사용이 비교적 간단하기에 선택했습니다. 또한 아래와 같은 이유들로 배포 구성 기술로 채택했습니다.
완벽한 자동화
환경 간 일관성 보장
Docker는 “한 번 빌드하면 어디서나 실행 가능”한 컨테이너 환경을 제공합니다.
로컬 개발 환경과 프로덕션 환경의 차이를 없애고, 모든 서비스가 동일한 상태로 배포되도록 보장합니다.
이를 통해 “내 로컬에서는 잘 되는데” 라는 문제를 미연에 방지할 수 있습니다.
멀티모듈 MSA를 위한 최적화
CD 파이프라인은 소스 코드 변경 사항이 자동으로 빌드되고 테스트되며, 최종적으로 배포되는 단계를 자동화합니다. 주요 단계는 아래와 같습니다:
1. 개발 브랜치 및 PR 관리
dev
브랜치로 PR을 통해 병합됩니다.dev
브랜치에서는 테스트를 통해 기능의 정상 동작 여부를 확인합니다.2.운영 브랜치로 병합 및 배포
main
브랜치로 병합되면, Github Actions 워크플로우가 실행되어 자동으로 EC2에 배포됩니다.모듈별 Dockerfile 관리
docker-compose.yml
에서 통합 관리됩니다.통합된 docker-compose.yml
depends_on 설정으로 서비스 간 의존성을 제어합니다.
Github Actions는 push 이벤트를 감지하여 자동으로 워크플로우를 실행합니다.
파이프라인의 주요 단계: 코드 푸시 → 빌드 → 이미지 생성 및 푸시 → 배포
name: CD with Gradle on: push: branches: [ "main" ] # 실제 실행될 내용들을 정의합니다. jobs: build: name: Build runs-on: ubuntu-latest # 각 서비스를 모두 빌드할 수 있도록 변수로 지정합니다. # https://docs.github.com/ko/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow strategy: matrix: service: [eureka, auth, gateway, GlowGrow-users, notification, payment, post, promotion, reservation] steps: - name: Checkout uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Build with Gradle run: ./gradlew :${{matrix.service}}:clean :${{matrix.service}}:build -x test --no-daemon Docker: name: Build docker image and Push to registry needs: build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Configure AWS credentials 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: web docker build and push run: | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} # docker compose 를 이용해서 여러 이미지를 모두 빌드하고, 별도의 script를 사용해서 이미지를 push 합니다. - name: Give execution permission run: chmod +x ./dockerTagAndPush.sh - name: Build, Tag and Push docker image to Hub run: | docker compose build ./dockerTagAndPush.sh env: DOCKER_HUB_NAMESPACE: ${{ secrets.DOCKER_HUB_NAMESPACE }} Deploy: name: Deploy needs: Docker runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # docker compose로 container를 실행하기 위해 docker-compose.yml 을 EC2로 복사합니다. - name: Copy Docker compose file to EC2 uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_KEY }} source: "docker-compose.yml" target: "/home/ubuntu" # target 은 디렉토리임. target directory 아래에 같은 이름의 파일로 옮겨진다. # ssh를 통해 EC2에 접속하고 docker container를 재시작합니다. - name: Deploy to EC2 uses: appleboy/ssh-action@v1.0.3 env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} DOCKER_HUB_NAMESPACE: ${{ secrets.DOCKER_HUB_NAMESPACE }} with: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_KEY }} port: 22 envs: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, DOCKER_HUB_NAMESPACE script: | sudo docker-compose down # 이미지 업데이트 sudo docker-compose pull # 컨테이너 실행 SPRING_PROFILES_ACTIVE=prod sudo docker-compose --env-file /home/ubuntu/.env up -d
main
브랜치를 대상으로 워크플로우가 실행됩니다.main
브랜치 병합 시 실제 배포가 시작됩니다../gradlew :${{matrix.service}}:build
명령으로 각 서비스 독립적으로 빌드.-x test
로 배포 시 제외 가능합니다.dockerTagAndPush.sh
스크립트를 통해 이미지 생성.SPRING_PROFILES_ACTIVE=prod.
워크플로우에서 Docker 부분에서 dockerTagAndPush.sh
를 실행하는 모습을 볼 수 있습니다.
해당 쉘 스크립트는 각 서비스 모듈의 Docker 이미지를 빌드하고, Docker hub 와 같은 이미지 레지스트리로 push 합니다.
# 모든 서비스 도커 이미지를 빌드합니다. services=( "glowgrow-eureka" "glowgrow-gateway" "glowgrow-auth" "glowgrow-user" "glowgrow-payment" "glowgrow-notification" "glowgrow-post" "glowgrow-promotion" "glowgrow-reservation" "glowgrow-multimedia" ) # 도커 이미지에 commit hash를 기반으로한 이미지 태그를 설정합니다. commit_hash=$(git rev-parse --short HEAD) for service in "${services[@]}" do imageName="$DOCKER_HUB_NAMESPACE/$service" # 워크플로우에서 env 로 넣어준 네임스페이스 # 도커 이미지 빌드 (해당 service 디렉토리에 Dockerfile이 있어야 합니다.) docker build -t "$imageName:latest" "./$service" # 이미지를 구분하기 위해서 latest 이외의 태그를 추가합니다. docker tag "$imageName:latest" "$imageName:$commit_hash" # Docker Hub에 push docker push "$imageName:latest" docker push "$imageName:$commit_hash" echo "$service 이미지가 빌드되어 Docker hub에 푸쉬되었습니다." done echo "모든 서비스의 이미지 빌드 및 푸쉬가 완료되었습니다."
1. 멀티모듈 간 의존성 문제
2. Docker 이미지 빌드 최적화
멀티모듈 MSA에서 GithubActions 와 Docker 조합으로 CD 를 구성하는 방법을 학습하고, 그 장점에 대해서 알 수 있었습니다.
또한 Docker 빌드 최적화에 대해서도 학습하고 적용해 보았습니다.