김솔비 블로그
기술 블로그

[Part 3] GitHub Self-Hosted Runner 실무 적용기

4분 읽기시리즈 4/6

이번 프로젝트는 단순한 자동 배포가 아니라 운영 환경까지 고려한 CI/CD 파이프라인 구축을 목표로 했다.

주요 요구사항은 다음과 같았다.

  • 개발 서버 자동 배포
  • 운영 서버 자동 배포
  • Slack 배포 알림
  • Health Check 기반 배포 검증
  • 자동 롤백
  • 수동 롤백 지원
  • 배포 이력 관리

이를 위해 GitHub Actions와 Self-Hosted Runner를 활용하여 다음과 같은 구조를 구성하였다.

GitHub
   │
   ├── dev 브랜치
   │      ↓
   │  Self-Hosted Runner
   │      ↓
   │ Docker Build
   │      ↓
   │ GHCR Push
   │      ↓
   │ Docker Compose Deploy
   │      ↓
   │ Health Check
   │
   └── prod 브랜치
          ↓
    GitHub Hosted Runner
          ↓
       SSH
          ↓
      운영 서버
          ↓
      Health Check
          ↓
       Rollback

브랜치 전략

개발 환경과 운영 환경을 명확하게 분리하기 위해 브랜치 기반 배포 전략을 적용하였다.

브랜치목적배포 방식
dev개발 서버자동 배포
prod운영 서버자동 또는 수동 배포

개발자는 dev 브랜치에 Push만 하면 자동으로 개발 서버에 반영되며, 운영 환경은 prod 브랜치 기준으로 관리된다.

Docker 기반 표준화된 배포 환경

모든 서비스는 Docker 이미지 기반으로 배포되도록 구성하였다.

구성 서비스

  • Backend (Spring Boot)
  • Web (Vite)
  • Admin (React)

Backend는 Spring Boot JAR 기반 이미지로 빌드했고,

docker
FROM eclipse-temurin:17-jre COPY build/libs/*.jar app.jar ENTRYPOINT ["java","-jar","/app/app.jar"]

Frontend(Admin, Web)는 Nginx 기반 정적 파일 이미지로 배포하였다.

docker
FROM nginx:stable-alpine COPY --from=build /app/dist /usr/share/nginx/html

이를 통해 개발 환경과 운영 환경의 실행 환경을 동일하게 유지할 수 있었다.

서비스별 Runner 분리

초기에는 하나의 Runner로 모든 서비스를 처리하려고 했지만 서비스 간 배포 대기가 발생할 수 있었다.

이를 해결하기 위해 서비스별 Runner를 구성하였다.

admin-runner
web-runner
backend-runner

각 Runner는 Label을 통해 특정 Workflow만 수행하도록 구성하였다.

yaml
runs-on: - self-hosted - dev - backend

이 구조를 통해 서비스 간 독립적인 배포가 가능해졌다.

개발 서버 배포 프로세스

개발 브랜치에 코드가 Push되면 다음 순서로 배포가 진행된다.

Git Push
 ↓
Docker Build
 ↓
GHCR Push
 ↓
Docker Compose Pull
 ↓
Docker Compose Up
 ↓
Health Check
 ↓
Slack 알림

배포 과정에서 다음 태그를 함께 관리하였다.

backend:dev
backend:dev-a1b2c3d

특정 버전으로 복구할 수 있도록 SHA 기반 태그도 함께 생성하였다.

운영 서버 배포 프로세스

운영 환경은 안정성을 위해 별도의 절차를 적용하였다.

prod 브랜치 Push
 ↓
Docker Build
 ↓
GHCR Push
 ↓
운영 서버 배포
 ↓
Health Check
 ↓
성공 또는 Rollback

운영 배포 전에는 현재 버전의 이미지와 설정 파일을 백업하도록 구성하였다.

Health Check 기반 배포 검증

배포 성공 여부를 단순 컨테이너 실행 상태로 판단하지 않았다.

Spring Boot Actuator를 이용하여 실제 서비스 상태를 검증하였다.

yaml
healthcheck: test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:8080/actuator/health || exit 1" ]

Health Check에 실패하면 배포 실패로 판단한다.

자동 롤백

운영 환경에서는 Health Check 실패 시 자동 롤백을 수행하도록 구성하였다.

신규 버전 배포
 ↓
Health Check 실패
 ↓
LAST_SUCCESSFUL_SHA 조회
 ↓
이전 버전 재배포
 ↓
서비스 복구

이를 통해 장애 발생 시 빠르게 서비스 복구가 가능하도록 하였다.

수동 롤백 기능

자동 롤백 외에도 특정 버전으로 되돌릴 수 있도록 Workflow Dispatch 기능을 활용하였다.

GitHub Actions에서 직접 실행할 수 있도록 구성하였다.

action = rollback
target_sha = a1b2c3d

입력된 SHA에 해당하는 이미지를 다시 배포하여 특정 시점으로 복구할 수 있다.

백업 및 보관 정책

배포 전 다음 자원을 자동 백업하도록 구성하였다.

  • Docker Image
  • Compose 파일
  • 서비스 디렉토리
  • 배포 로그

백업 데이터는 최신 30개만 유지하도록 회전 정책을 적용하였다.

bash
ls -1dt */ | tail -n +31 | xargs -r rm -rf

이를 통해 디스크 사용량이 무한정 증가하는 문제를 방지하였다.

Slack 알림

배포 결과는 Slack으로 전송하였다.

성공 시

배포 성공
배포자
커밋 SHA
소요 시간

실패 시

배포 실패
자동 롤백 여부
로그 확인 요청

운영자가 별도 서버에 접속하지 않아도 배포 상태를 즉시 확인할 수 있도록 구성하였다.