[Github Actions + Docker + EC2] 로 배포 자동화 구성하기

twonezero·6일 전
0
post-thumbnail

현재의 개발에서는 지속적 배포(Continuous Deployment, CD)를 통해 빠르고 안정적인 서비스 제공이 필수가 되었습니다. 특히, MSA(Microservices Architecture)를 채택한 프로젝트에서는 각 서비스의 독립성과 배포 효율성을 극대화하는 파이프라인 설계가 중요합니다.

이번 글에서는 멀티모듈 MSA 프로젝트를 기반으로, Github Actions와 Docker, 그리고 EC2를 활용해 배포 자동화를 구성한 경험과 주요 고려 사항을 공유하려 합니다.

이 글을 통해 다음 내용을 얻을 수 있습니다

  • 멀티모듈 프로젝트에서 CI/CD의 필요성과 구성 전략
  • Github Actions의 workflow를 활용한 단계별 자동화
  • Docker 이미지를 빌드하고, EC2로 배포하는 실무적인 접근법
  • 실사용 중 발생했던 이슈와 해결 방안

1. 프로젝트 구성

프로젝트는 멀티모듈 기반의 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

배포 자동화와 관련된 도구는 많지만 Github Actions + Docker 조합은 구성과 사용이 비교적 간단하기에 선택했습니다. 또한 아래와 같은 이유들로 배포 구성 기술로 채택했습니다.

완벽한 자동화

  • GitHub Actions는 GitHub 리포지토리와 긴밀히 통합됩니다. 코드가 푸시되거나 PR(Pull Request)이 생성되면 자동으로 빌드, 테스트, 배포 과정을 실행합니다. 이를 통해 개발자는 코어 로직 개발에 더 집중할 수 있습니다.

환경 간 일관성 보장

  • Docker는 “한 번 빌드하면 어디서나 실행 가능”한 컨테이너 환경을 제공합니다.

  • 로컬 개발 환경과 프로덕션 환경의 차이를 없애고, 모든 서비스가 동일한 상태로 배포되도록 보장합니다.

  • 이를 통해 “내 로컬에서는 잘 되는데” 라는 문제를 미연에 방지할 수 있습니다.

멀티모듈 MSA를 위한 최적화

  • GitHub Actions와 Docker의 조합은 특히 마이크로서비스 아키텍처(MSA)에서 그 진가를 발휘합니다. 각 서비스는 독립적인 Docker 이미지를 생성하므로 개별적으로 빌드 및 배포가 가능합니다.
  • 이를 통해 하나의 서비스 업데이트가 다른 서비스에 영향을 미치지 않도록 합니다.

2. CD 파이프라인 설계

배포 자동화의 목표 및 주요 단계

CD 파이프라인은 소스 코드 변경 사항이 자동으로 빌드되고 테스트되며, 최종적으로 배포되는 단계를 자동화합니다. 주요 단계는 아래와 같습니다:

1. 개발 브랜치 및 PR 관리

  • 각 서비스는 독립적으로 기능 개발 후, dev 브랜치로 PR을 통해 병합됩니다.
  • dev 브랜치에서는 테스트를 통해 기능의 정상 동작 여부를 확인합니다.

2.운영 브랜치로 병합 및 배포

  • main 브랜치로 병합되면, Github Actions 워크플로우가 실행되어 자동으로 EC2에 배포됩니다.
  • 병합 즉시 빌드 및 테스트가 수행됩니다.
  • 배포 과정은 이미지 생성, Docker Hub 업로드, EC2로 전송 및 컨테이너 실행을 포함합니다.

각 모듈의 독립성과 통합 배포 전략

모듈별 Dockerfile 관리

  • 각 모듈은 자체 Dockerfile을 보유하며, docker-compose.yml에서 통합 관리됩니다.
    예: eureka, gateway, reservation 등 서비스가 개별적으로 빌드 가능.

통합된 docker-compose.yml

  • 루트 컨텍스트에서 docker-compose.yml을 사용해 모든 서비스를 정의하고, 환경 설정(.env)을 기반으로 관리합니다.

depends_on 설정으로 서비스 간 의존성을 제어합니다.

  • 동일한 네트워크에서 여러 컨테이너가 통신하도록 설계

3. Github Actions Workflow

워크플로우의 단계별 구성 소개

Github Actions는 push 이벤트를 감지하여 자동으로 워크플로우를 실행합니다.
파이프라인의 주요 단계: 코드 푸시 → 빌드 → 이미지 생성 및 푸시 → 배포

  • CD.yml
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

1. 코드 푸시 및 브랜치 감지

  • main 브랜치를 대상으로 워크플로우가 실행됩니다.
  • main 브랜치 병합 시 실제 배포가 시작됩니다.

2. 빌드 및 테스트

  • Gradle을 사용하여 각 서비스 모듈을 빌드합니다.
  • ./gradlew :${{matrix.service}}:build 명령으로 각 서비스 독립적으로 빌드.
  • 테스트는 -x test로 배포 시 제외 가능합니다.

3. 도커 이미지 생성 및 푸시

  • docker compose와 dockerTagAndPush.sh 스크립트를 통해 이미지 생성.
  • 생성된 이미지를 Docker Hub에 태그별로 푸시합니다 (latest, commit_hash).

4. EC2로 배포

  • EC2에 docker-compose.yml 파일 전송.
  • 기존 컨테이너를 종료(docker-compose down) 후 이미지를 업데이트(docker-compose pull).
  • 프로파일별 환경 설정 적용: SPRING_PROFILES_ACTIVE=prod.

Docker 이미지를 멀티모듈로 빌드

워크플로우에서 Docker 부분에서 dockerTagAndPush.sh 를 실행하는 모습을 볼 수 있습니다.
해당 쉘 스크립트는 각 서비스 모듈의 Docker 이미지를 빌드하고, Docker hub 와 같은 이미지 레지스트리로 push 합니다.

  • dockerTagAndPush.sh
# 모든 서비스 도커 이미지를 빌드합니다.
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 "모든 서비스의 이미지 빌드 및 푸쉬가 완료되었습니다."

5. 주요 이슈와 해결 방법

1. 멀티모듈 간 의존성 문제

  • 문제: 각 모듈이 공통 라이브러리를 의존할 경우 Docker 빌드가 실패하거나 누락이 됩니다.
  • 해결: Dockerfile의 컨텍스트를 루트에서 시작하여 의존하는 모듈을 참조해야 합니다.
    멀티모듈에서의 Docker 빌드 문제

2. Docker 이미지 빌드 최적화

  • 문제: 만약 서비스 모듈이나 공통 모듈 추가로 인한 빌드 속도 및 크기 문제
  • 해결
    • 빌드 후 Docker 로 실행할 것이기에 jdk 이미지 대신에 빌드만을 위한 jre 관련 이미지 사용
    • Gradle의 빌드 캐시와 병렬 빌드 옵션 활용.
    • 의존하는 모듈만 copy
    • Docker 이미지는 변경된 서비스만 재빌드하여 효율성 증대.
      멀티모듈에서의 Dockerfile 최적화하기

6. 결론 및 배운 점

멀티모듈 MSA에서 GithubActions 와 Docker 조합으로 CD 를 구성하는 방법을 학습하고, 그 장점에 대해서 알 수 있었습니다.
또한 Docker 빌드 최적화에 대해서도 학습하고 적용해 보았습니다.

향후 개선 방안

  • Blue-Green Deployment 도입
    • 배포 중 서비스 중단 없는 업데이트 가능.
  • Helm 및 Kubernetes
    • 컨테이너 오케스트레이션 도입으로 확장성과 유연성 개선.
profile
소소한 행복을 즐기는 백엔드 개발자입니다😉

0개의 댓글