
이번에 프로젝트에서 인프라 담당을 맡아 GitHub Actions 워크플로우를 구현한 경험을 공유하고자 합니다. 이 워크플로우는 코드가 main 브랜치에 푸시될 때마다 자동으로 애플리케이션을 빌드하고, 도커 이미지를 생성한 후, AWS EC2 인스턴스에 무중단 배포하는 CI/CD 파이프라인입니다.
워크플로우는 크게 세 단계로 구성됩니다:
이제 각 단계를 자세히 살펴보겠습니다.
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)에 푸시합니다. 여기서 주목할 만한 점들:
.env 파일로 생성합니다.cache-from과 cache-to 옵션을 사용하여 빌드 속도를 향상시킵니다.latest)과 특정 버전 태그를 모두 설정합니다.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_1과 app1_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초간 대기하는 부분이 중요합니다. 마지막으로 사용하지 않는 이미지를 정리하여 디스크 공간을 절약합니다.
이 GitHub Actions 워크플로우를 통해 코드 변경사항이 main 브랜치에 푸시될 때마다 자동으로 배포 프로세스가 진행됩니다. 개발자는 코드 작성에만 집중할 수 있고, 배포 과정은 자동화된 시스템이 처리해줍니다. 또한 무중단 배포 전략을 통해 사용자 경험을 해치지 않고 새로운 기능을 안전하게 배포할 수 있습니다.
이러한 CI/CD 파이프라인은 개발 생산성을 크게 향상시키며, 배포 과정에서 발생할 수 있는 인적 오류를 최소화합니다. 여러분의 프로젝트에도 이러한 자동화 시스템을 도입해보는 것을 적극 추천합니다!