GitHub Actions로 Docker 이미지 자동 빌드 및 배포 파이프라인 구축하기
들어가며
회사에서 GitLab에서 GitHub로 이전하면서 CI/CD 파이프라인을 새로 구축해야 했습니다. 특히 Docker 컨테이너로 서비스를 운영하고 있어 이미지 빌드와 배포 프로세스의 자동화가 필요했습니다.
이 글에서는 기존 수동 배포 방식의 문제점을 해결하기 위해 GitHub Actions를 도입하고, 요금과 운영 효율성을 고려해 셀프호스팅 러너와 레지스트리를 구성한 경험을 공유합니다.
문제 상황: 수동 배포의 한계
기존 배포 프로세스
이전에는 개발자가 직접 Docker 이미지를 빌드하고 레지스트리 서버에 업로드하는 수동 방식을 사용했습니다:
- 개발자가 로컬에서
docker build실행 - 빌드된 이미지를
docker save로 tar 파일로 추출 scp로 레지스트리 서버에 전송- 레지스트리 서버에서
docker load로 이미지 로드 - 버전 태그를 수동으로 관리
문제점
1. 인적 오류 가능성
- 버전 태그를 잘못 입력하거나 중복 사용
- 빌드 환경 차이로 인한 불일치
- 배포 절차를 놓치는 경우
2. 확장성 문제
- 서비스가 늘어날수록 배포 작업이 기하급수적으로 증가
- 각 서비스마다 배포 담당자가 달라 일관성 부족
- 배포 시간과 리소스 낭비
3. 형상 관리 어려움
- 어떤 버전이 언제 배포되었는지 추적 어려움
- 롤백이 필요할 때 이전 버전을 찾기 어려움
- Git 커밋과 배포 버전 간 연결이 약함
4. 개발 생산성 저하
- 배포에 시간을 많이 소비
- 반복적인 작업으로 인한 피로도 증가
- 실제 개발에 집중하기 어려움
해결 전략 수립
1. GitHub Actions 도입 결정
GitHub로 이전하면서 CI/CD 자동화를 위해 GitHub Actions를 선택했습니다. 주요 이유:
- GitHub 통합: 코드 저장소와 CI/CD가 같은 플랫폼에 있어 관리가 용이
- 무료 제공량: 공개 리포지토리는 무료, 비공개도 일정량 무료 제공
- 풍부한 액션 생태계: Docker 빌드, 배포 등 다양한 액션 제공
- 워크플로우 유연성: YAML로 복잡한 파이프라인 구성 가능
2. 레지스트리 선택: GHCR vs 셀프호스팅
처음에는 GitHub Container Registry(GHCR)를 사용하려고 했습니다.
GHCR의 장점:
- GitHub과 완전 통합
- 별도 인프라 관리 불필요
- GitHub Actions에서 인증 자동 처리
하지만 요금 문제로 셀프호스팅 선택:
GitHub Packages(GHCR 포함)는 다음과 같은 요금 정책을 가집니다:
- 공개 리포지토리: 무료
- 비공개 리포지토리:
- 저장소: 500MB 무료, 초과 시 GB당 요금
- 데이터 전송: 1GB/월 무료, 초과 시 요금
여러 서비스를 운영하고 이미지 크기가 큰 경우 비용이 빠르게 증가할 수 있어, 기존에 구축된 팀 도커 레지스트리 서버를 활용하기로 결정했습니다.
셀프호스팅 레지스트리의 장점:
- 요금 부담 없음 (인프라 비용만)
- 내부 네트워크에서 빠른 전송 속도
- 보안 정책에 맞춘 접근 제어 가능
- 기존 인프라 활용
3. GitHub Actions Runner: 셀프호스팅 선택
GitHub Actions는 두 가지 러너 옵션을 제공합니다:
GitHub 호스팅 러너:
- GitHub에서 제공하는 가상 머신
- 설정 없이 바로 사용 가능
- 비공개 리포지토리는 사용량에 따라 요금 부과
셀프호스팅 러너:
- 사용자가 직접 관리하는 서버에 러너 설치
- GitHub Actions 이용 요금 무료 (인프라 비용만)
- 빌드 환경을 직접 제어 가능
셀프호스팅 러너를 선택한 이유:
- 비용 절감: 여러 서비스를 빌드할 때 GitHub 호스팅 러너 요금이 누적됨
- 빌드 속도: 내부 네트워크에서 레지스트리로 빠르게 푸시 가능
- 환경 제어: 필요한 도구와 설정을 미리 구성 가능
- 보안: 내부 네트워크에서만 동작하도록 구성 가능
GitHub Actions 워크플로우 구현
기본 구조
워크플로우는 다음과 같은 단계로 구성됩니다:
- 트리거: 특정 브랜치에 코드 푸시 시 실행
- 버전 생성: 기존 태그를 기반으로 자동 버전 증가
- Docker 이미지 빌드: Dockerfile을 사용해 이미지 생성
- Git 태그 생성: 버전 태그 자동 생성
- 레지스트리 배포: 팀 레지스트리 서버로 이미지 푸시
워크플로우 YAML 파일
name: Build Docker Image
on:
push:
branches:
- develop # develop 브랜치에 푸시 시 실행
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # 동시 실행 시 이전 작업 취소
env:
IMAGE_NAME: my-service
DOCKER_BUILDKIT: 1 # BuildKit 사용으로 빌드 속도 향상
permissions:
contents: write # 태그 생성 및 릴리스 생성을 위해 필요
jobs:
build:
runs-on: self-hosted # 셀프호스팅 러너 사용
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 모든 태그 정보를 가져오기 위해 필요
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate version (auto increment)
id: version
run: |
# 최신 태그 찾기 (v0.0.* 형식)
LATEST_TAG=$(git tag -l "v0.0.*" | sort -V | tail -n1)
if [ -z "$LATEST_TAG" ]; then
# 태그가 없으면 0.0.1로 시작
VERSION="0.0.1"
else
# 마지막 숫자 추출 후 1 증가 (호환성을 위해 sed 사용)
LAST_NUM=$(echo $LATEST_TAG | sed 's/.*\.//')
NEXT_NUM=$((LAST_NUM + 1))
VERSION="0.0.${NEXT_NUM}"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Determined version: $VERSION"
- name: Build Docker image
working-directory: apps/my-service # 서비스별 디렉토리
env:
IMAGE_TAG: ${{ steps.version.outputs.version }}
run: |
echo "Building image ${IMAGE_NAME}:${IMAGE_TAG}"
docker buildx build --platform linux/amd64 --load \
--build-arg BUILD_HASH=${{ github.sha }} \
--build-arg USE_SLIM=true \
-t ${IMAGE_NAME}:${IMAGE_TAG} \
-t ${IMAGE_NAME}:latest \
.
# 빌드 성공 확인
if ! docker images ${IMAGE_NAME}:${IMAGE_TAG} | grep -q ${IMAGE_TAG}; then
echo "ERROR: Docker image build failed!"
exit 1
fi
echo "Docker image built successfully"
docker images ${IMAGE_NAME}:${IMAGE_TAG}
- name: Create Git tag
if: github.event_name == 'push'
run: |
TAG=v${{ steps.version.outputs.version }}
git fetch --tags
# 태그가 이미 존재하는지 확인
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists, skipping tag creation."
else
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git tag "$TAG"
git push origin "$TAG"
echo "Created and pushed tag: $TAG"
fi
- name: Push to team registry
if: github.event_name == 'push'
env:
IMAGE_TAG: ${{ steps.version.outputs.version }}
run: |
# 팀 레지스트리 서버에 이미지 푸시
docker tag ${IMAGE_NAME}:${IMAGE_TAG} \
registry.example.com/${IMAGE_NAME}:${IMAGE_TAG}
docker tag ${IMAGE_NAME}:${IMAGE_TAG} \
registry.example.com/${IMAGE_NAME}:latest
# 보안을 위해 --password-stdin 사용
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin registry.example.com
docker push registry.example.com/${IMAGE_NAME}:${IMAGE_TAG}
docker push registry.example.com/${IMAGE_NAME}:latest
echo "Image pushed to team registry"
주요 기능 설명
1. 버전 자동 증가 로직
LATEST_TAG=$(git tag -l "v0.0.*" | sort -V | tail -n1)
if [ -z "$LATEST_TAG" ]; then
VERSION="0.0.1"
else
# 호환성을 위해 sed 사용 (grep -P는 GNU grep 전용)
LAST_NUM=$(echo $LATEST_TAG | sed 's/.*\.//')
NEXT_NUM=$((LAST_NUM + 1))
VERSION="0.0.${NEXT_NUM}"
fi
- 기존 태그 중
v0.0.*패턴을 찾아 최신 버전 확인 - 마지막 숫자를 추출해 1 증가 (sed를 사용해 호환성 확보)
- 태그가 없으면
0.0.1로 시작
버전 관리 전략:
v0.0.1,v0.0.2, ... 형식으로 패치 버전 자동 증가- 필요시
v0.1.0,v1.0.0등으로 수동 태그 생성 가능 - Git 태그와 Docker 이미지 태그를 일치시켜 형상 관리 용이
참고: 이 로직은 v0.0.* 패턴만 처리합니다. v0.1.0 이상의 버전으로 전환하려면 수동으로 태그를 생성하거나 로직을 확장해야 합니다.
2. Concurrency 제어
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
- 같은 브랜치에서 여러 커밋이 빠르게 푸시되면 이전 빌드를 취소
- 불필요한 빌드 방지 및 리소스 절약
3. 레지스트리 자동 푸시
- 빌드된 이미지를 자동으로 팀 레지스트리 서버에 푸시
- 버전 태그와 latest 태그를 모두 푸시하여 형상 관리 용이
셀프호스팅 러너 설정
러너 설치
# GitHub Actions 러너 다운로드 및 설치
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.311.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz
# 러너 등록 (리포지토리 레벨 또는 조직 레벨)
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO --token YOUR_TOKEN
# 러너 서비스로 등록 (자동 시작)
sudo ./svc.sh install
sudo ./svc.sh start
러너 환경 준비
셀프호스팅 러너 서버에 다음을 설치해야 합니다:
- Docker 및 Docker Buildx
- Git
- 기타 빌드에 필요한 도구
보안 고려사항
- 러너는 내부 네트워크에서만 접근 가능하도록 설정
- Secrets는 GitHub에 저장하고 러너로 전달
- 레지스트리 인증 정보는 GitHub Secrets로 관리
- Docker 로그인 시
--password-stdin사용 (비밀번호가 프로세스 목록에 노출되지 않도록)
레지스트리 배포 전략
빌드된 이미지는 자동으로 팀 레지스트리 서버에 푸시됩니다:
- 빌드 후 자동으로 레지스트리 서버에 푸시
- 운영 서버에서 레지스트리에서 pull하여 배포
- 장점: 자동화된 배포 파이프라인 구성 가능
- 단점: 레지스트리 서버 관리 필요
레지스트리 푸시 예시
- name: Push to team registry
env:
IMAGE_NAME: my-service
IMAGE_TAG: ${{ steps.version.outputs.version }}
run: |
# 보안을 위해 --password-stdin 사용 (비밀번호가 프로세스 목록에 노출되지 않음)
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin registry.example.com
docker tag ${IMAGE_NAME}:${IMAGE_TAG} \
registry.example.com/${IMAGE_NAME}:${IMAGE_TAG}
docker push registry.example.com/${IMAGE_NAME}:${IMAGE_TAG}
개선 효과
Before (수동 배포)
- 배포 시간: 서비스당 약 10-15분
- 인적 오류: 버전 태그 실수, 빌드 환경 차이
- 형상 관리: 어려움
- 확장성: 서비스 증가 시 배포 부담 급증
After (자동화)
- 배포 시간: 코드 푸시 후 자동 실행 (개발자 개입 최소화)
- 인적 오류: 버전 자동 생성으로 실수 방지
- 형상 관리: Git 태그와 이미지 태그 자동 일치
- 확장성: 서비스 추가 시 워크플로우 파일만 복사/수정
정량적 개선
- 배포 시간: 90% 감소 (10-15분 → 1-2분, 대부분 자동)
- 배포 실수: 거의 제로 (자동화로 인적 오류 제거)
- 개발 생산성: 배포 작업 시간을 개발에 집중 가능
추가 고려사항
1. 멀티 서비스 관리
여러 서비스를 운영하는 경우:
- 각 서비스마다 워크플로우 파일 생성
- 공통 부분은 재사용 가능한 액션이나 템플릿으로 분리
- Monorepo 구조라면 매트릭스 전략 사용 고려
2. 브랜치 전략
on:
push:
branches:
- develop # 개발 환경 배포
- staging # 스테이징 환경 배포
- main # 프로덕션 환경 배포
- 브랜치별로 다른 레지스트리나 태그 전략 사용 가능
- 환경별 이미지 분리로 롤백 용이
3. 빌드 캐시 활용
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=moby/buildkit:latest
network=host
- Docker BuildKit 캐시로 빌드 속도 향상
- 레이어 캐시 활용
4. 보안 강화
- 이미지 스캔 도구 통합 (Trivy, Snyk 등)
- 취약점이 발견되면 빌드 실패 처리
- 최소 권한 원칙으로 Secrets 관리
5. 모니터링 및 알림
- 빌드 실패 시 Slack/이메일 알림
- 빌드 시간 추적 및 최적화
- 레지스트리 사용량 모니터링
트러블슈팅
문제 1: 버전 충돌
증상: 같은 버전 태그가 이미 존재하는 경우
해결: 태그 생성 전 존재 여부 확인 로직 추가 (위 예시에 포함)
문제 2: 빌드 시간이 오래 걸림
해결:
- Docker BuildKit 사용
- 멀티스테이지 빌드로 이미지 크기 최적화
- 빌드 캐시 활용
- 불필요한 의존성 제거
문제 3: 러너 서버 부하
해결:
- 여러 러너 인스턴스 구성
- 러너 라벨로 작업 분산
- 빌드 큐 관리
마무리
GitHub Actions를 활용한 Docker 이미지 자동 빌드 및 배포 파이프라인을 구축하면서 다음과 같은 성과를 얻었습니다:
- 자동화로 인한 생산성 향상: 개발자가 배포에 신경 쓸 필요 없이 코드에만 집중
- 형상 관리 개선: Git 태그와 이미지 태그의 일관성 확보
- 비용 최적화: 셀프호스팅 러너와 레지스트리로 운영 비용 절감
- 확장성: 서비스가 늘어나도 워크플로우만 추가하면 됨
이 파이프라인은 지속적으로 개선해 나가고 있으며, 보안 스캔, 자동 테스트, 배포 자동화 등 추가 기능을 고려하고 있습니다.
참고 자료: