GitHub Actions를 활용한 자동 배포 시스템 구축하기

김상진 ·2025년 3월 27일

infra

목록 보기
2/6
post-thumbnail

이번에 프로젝트에서 인프라 담당을 맡아 GitHub Actions 워크플로우를 구현한 경험을 공유하고자 합니다. 이 워크플로우는 코드가 main 브랜치에 푸시될 때마다 자동으로 애플리케이션을 빌드하고, 도커 이미지를 생성한 후, AWS EC2 인스턴스에 무중단 배포하는 CI/CD 파이프라인입니다.

전체 워크플로우 개요

워크플로우는 크게 세 단계로 구성됩니다:

  1. 태그 생성 및 릴리스: 새 버전 태그를 생성하고 GitHub 릴리스를 만듭니다.
  2. 도커 이미지 빌드 및 푸시: 애플리케이션의 도커 이미지를 빌드하고 GitHub Container Registry(ghcr.io)에 푸시합니다.
  3. AWS EC2 배포: AWS SSM을 통해 EC2 인스턴스에 접속하여 새 이미지를 배포합니다.

이제 각 단계를 자세히 살펴보겠습니다.

워크플로우 트리거 설정

name: deploy
on:
  push:
    paths:
      - ".github/workflows/**"
      - "src/**"
      - "build.gradle"
      - "settings.gradle"
      - "Dockerfile"
    branches:
      - main

이 워크플로우는 main 브랜치에 푸시가 발생하고, 그 변경사항이 지정된 경로 중 하나에 해당할 때만 실행됩니다. 이렇게 하면 불필요한 빌드와 배포를 방지할 수 있어 리소스를 절약할 수 있습니다.

태그 생성 및 릴리스 작업

jobs:
  makeTagAndRelease:
    runs-on: ubuntu-latest
    outputs:
      tag_name: ${{ steps.create_tag.outputs.new_tag }}
    steps:
      - uses: actions/checkout@v4
      - name: Create Tag
        id: create_tag
        uses: mathieudutour/github-tag-action@v6.2
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ steps.create_tag.outputs.new_tag }}
          release_name: Release ${{ steps.create_tag.outputs.new_tag }}
          body: ${{ steps.create_tag.outputs.changelog }}
          draft: false
          prerelease: false

이 작업은 새로운 Git 태그를 자동으로 생성하고, 그 태그를 기반으로 GitHub 릴리스를 만듭니다. 자동 버전 관리를 위해 mathieudutour/github-tag-action을 사용하는데, 이 액션은 커밋 메시지 형식에 따라 자동으로 버전을 증가시킵니다. 이 단계에서 생성된 태그는 이후 단계에서 도커 이미지 태그로 활용됩니다.

도커 이미지 빌드 및 푸시

buildImageAndPush:
  name: 도커 이미지 빌드와 푸시
  needs: makeTagAndRelease
  runs-on: ubuntu-latest
  env:
    DOCKER_IMAGE_NAME: cotree
  outputs:
    DOCKER_IMAGE_NAME: ${{ env.DOCKER_IMAGE_NAME }}
    OWNER_LC: ${{ env.OWNER_LC }}
  steps:
    - uses: actions/checkout@v4
    - name: .env 생성
      env:
        DOT_ENV: ${{ secrets.DOT_ENV }}
      run: echo "$DOT_ENV" > .env
    - name: Docker Buildx 설치
      uses: docker/setup-buildx-action@v2
    - name: 레지스트리 로그인
      uses: docker/login-action@v2
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    - name: set lower case owner name
      run: |
        echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV}
      env:
        OWNER: "${{ github.repository_owner }}"
    - name: 빌드 앤 푸시
      uses: docker/build-push-action@v3
      with:
        context: .
        push: true
        cache-from: type=registry,ref=ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:cache
        cache-to: type=registry,ref=ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:cache,mode=max
        tags: |
          ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:${{ needs.makeTagAndRelease.outputs.tag_name }},
          ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:latest

이 작업은 Dockerfile을 기반으로 도커 이미지를 빌드하고 GitHub Container Registry(ghcr.io)에 푸시합니다. 여기서 주목할 만한 점들:

  1. 환경 변수 설정: 배포에 필요한 환경 변수를 GitHub Secrets에서 가져와 .env 파일로 생성합니다.
  2. Docker Buildx 사용: 빌드 성능 향상을 위해 Docker Buildx를 설치합니다.
  3. 캐시 활용: cache-fromcache-to 옵션을 사용하여 빌드 속도를 향상시킵니다.
  4. 다중 태그 설정: 최신 버전(latest)과 특정 버전 태그를 모두 설정합니다.

AWS EC2 배포

deploy:
  runs-on: ubuntu-latest
  needs: [ buildImageAndPush ]
  env:
    DOCKER_IMAGE_NAME: ${{ needs.buildImageAndPush.outputs.DOCKER_IMAGE_NAME }}
    OWNER_LC: ${{ needs.buildImageAndPush.outputs.OWNER_LC }}
  steps:
    - uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-region: ${{ secrets.AWS_REGION }}
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    - name: 인스턴스 ID 가져오기
      id: get_instance_id
      run: |
        INSTANCE_ID=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=Team15-ec2-1" "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].InstanceId" --output text)
        echo "INSTANCE_ID=$INSTANCE_ID" >> $GITHUB_ENV
        echo $INSTANCE_ID
    - name: AWS SSM Send-Command
      uses: peterkimzz/aws-ssm-send-command@master
      id: ssm
      with:
        aws-region: ${{ secrets.AWS_REGION }}
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        instance-ids: ${{ env.INSTANCE_ID }}
        working-directory: /
        comment: Deploy
        command: |
          # 배포 스크립트 (무중단 배포 구현)

이 작업은 AWS EC2 인스턴스에 새 버전을 배포합니다. 가장 중요한 부분은 무중단 배포(Blue-Green Deployment) 전략을 구현한 스크립트입니다:

무중단 배포 전략 상세 분석

# 공통 변수
IMAGE="ghcr.io/${{ env.OWNER_LC }}/${{ env.DOCKER_IMAGE_NAME }}:latest"
VOLUME="/gen:/gen"
NETWORK="common"
HEALTH_ENDPOINT="/actuator/health"
TIMEOUT=60

# 현재 실행 중인 컨테이너 확인
if docker ps --format '{{.Names}}' | grep -q "app1_1"; then
  CURRENT="app1_1"
  NEXT="app1_2"
  CURRENT_PORT=8080
  NEXT_PORT=8081
else
  CURRENT="app1_2"
  NEXT="app1_1"
  CURRENT_PORT=8081
  NEXT_PORT=8080
fi

이 부분에서는 현재 실행 중인 컨테이너를 확인하고 그에 따라 새로 배포할 컨테이너와 포트를 결정합니다. 블루-그린 배포 전략을 위해 두 개의 컨테이너(app1_1app1_2)를 번갈아가며 사용합니다.

# 다음 컨테이너 실행
echo "Starting new container: $NEXT on port $NEXT_PORT..."
docker pull "$IMAGE"
docker stop "$NEXT" 2>/dev/null
docker rm "$NEXT" 2>/dev/null
docker run -d \
  -v $VOLUME \
  --network $NETWORK \
  --name "$NEXT" \
  -p "$NEXT_PORT":8080 \
  "$IMAGE"

새 버전을 다음 컨테이너로 실행합니다. 이미 존재할 경우를 대비해 기존 컨테이너를 중지하고 제거한 후 새로 실행합니다.

# 헬스체크 대기
echo "Waiting for health check..."
START_TIME=$(date +%s)
while true; do
  CONTENT=$(curl -s http://localhost:$NEXT_PORT$HEALTH_ENDPOINT)

  if [[ "$CONTENT" == *'"status":"UP"'* ]]; then
    echo "✅ $NEXT is UP!"
    break
  fi

  ELAPSED_TIME=$(( $(date +%s) - START_TIME ))
  if [[ $ELAPSED_TIME -ge $TIMEOUT ]]; then
    echo "❌ Timeout: $NEXT did not start in $TIMEOUT seconds."
    docker stop "$NEXT"
    docker rm "$NEXT"
    exit 1
  fi

  echo "⏳ Waiting for $NEXT to be UP..."
  sleep 5
done

새 컨테이너가 정상적으로 실행되었는지 확인하기 위해 헬스 체크를 수행합니다. Spring Boot의 Actuator 엔드포인트(/actuator/health)를 활용하여 서비스가 정상 작동하는지 확인하고, 타임아웃 시간 내에 정상화되지 않으면 배포를 중단합니다.

sleep 10 # ha proxy 가 사용하는 dns 캐시 때문에 서비스가 띄워지고도 10초 후에 발견될 수 도 있다. 그래서 기존 서버를 바로 내리지 않는다.

# 기존 컨테이너 중지 및 제거
echo "Stopping old container: $CURRENT"
docker stop "$CURRENT" 2>/dev/null
docker rm "$CURRENT" 2>/dev/null

# dangling image 제거
docker rmi $(docker images -f "dangling=true" -q) 2>/dev/null

echo "✅ Deployment complete. Running container: $NEXT on port $NEXT_PORT"

새 컨테이너가 정상 작동함을 확인한 후에야 기존 컨테이너를 중지하고 제거합니다. 이 과정에서 HAProxy의 DNS 캐시 문제를 고려하여 10초간 대기하는 부분이 중요합니다. 마지막으로 사용하지 않는 이미지를 정리하여 디스크 공간을 절약합니다.

워크플로우의 주요 특징

  1. 자동 버전 관리: 커밋에 따라 자동으로 버전 태그가 생성됩니다.
  2. Docker 캐싱: 빌드 시간을 단축하기 위해 레지스트리 캐싱을 활용합니다.
  3. 무중단 배포: Blue-Green 배포 전략으로 서비스 중단 없이 업데이트합니다.
  4. 헬스 체크: 새 버전이 제대로 작동하는지 검증 후 트래픽을 전환합니다.
  5. 자원 관리: 불필요한 컨테이너와 이미지를 정리하여 자원을 효율적으로 사용합니다.

결론

이 GitHub Actions 워크플로우를 통해 코드 변경사항이 main 브랜치에 푸시될 때마다 자동으로 배포 프로세스가 진행됩니다. 개발자는 코드 작성에만 집중할 수 있고, 배포 과정은 자동화된 시스템이 처리해줍니다. 또한 무중단 배포 전략을 통해 사용자 경험을 해치지 않고 새로운 기능을 안전하게 배포할 수 있습니다.

이러한 CI/CD 파이프라인은 개발 생산성을 크게 향상시키며, 배포 과정에서 발생할 수 있는 인적 오류를 최소화합니다. 여러분의 프로젝트에도 이러한 자동화 시스템을 도입해보는 것을 적극 추천합니다!

profile
알고리즘은 백준 허브를 통해 github에 꾸준히 올리고 있습니다.🙂

0개의 댓글