본문 바로가기
기술 공부/DevOps

Self-hosted GitHub Runner를 통한 CI/CD 자동 배포 파이프라인 구축

by soy-ul 2025. 11. 2.
반응형

 

안녕하세요. 오랜만에 인사드립니다. 😊

오늘은 GitHub Actions의 Self-Hosted Runner를 활용하여 완전 자동화된 배포 시스템 구축 과정을 공유하고자 합니다. 기존 인프라를 활용하여 코드 배포 자동화를 달성하는 방법에 대해 다뤄보겠습니다.

 

🔹  개요

이전까지는 코드 변경이 있을 때마다 서버에 SSH 접속하여 수동으로 빌드-배포하는 방식을 사용해왔으나 최근 프로젝트 인원들과 GitHub Oranizations을 활용하여 공유된 코드들을 개발/관리하고 있었습니다. 

하지만 개발이 진행되면서 코드 관리나 배포 과정에서 몇 가지 문제점들이 발견되었습니다. 

해결이 필요했던 문제들

빌드 환경의 일관성 부족

  • 개발자 로컬 환경, 개발 서버, 운영 서버에 각각 코드가 존재
  • 어느 환경에 어떤 버전의 코드가 배포되어 있는지 파악이 어려움
  • 코드가 분산이 되어 있기 때문에 관리 포인트가 증가

배포 프로세스의 표준화 부재

  • 배포 실패 시 롤백 절차가 명확하지 않음

이러한 문제들을 해결하기 위해 GitHub Actions와 Self-Hosted Runner를 활용한 CI/CD 파이프라인을 구축하게 되었습니다. 

 

🔹  GitHub Runner 란?

GitHub Actions는 GitHub에서 제공하는 CI/CD 플랫폼입니다. 코드 저장소에서 직접 소프트웨어 개발 워크플로우를 자동화할 수 있게 해줍니다. 이 플랫폼의 핵심 구성 요소가 바로 Runner입니다.

Runner는 GitHub Actions 워크플로우를 실행하는 서버로, 크게 두 가지 유형으로 나뉩니다:

1. GitHub-hosted Runners

  • GitHub에서 제공하는 가상 머신
  • Ubuntu, Windows, macOS 환경 제공
  • 매 작업마다 새로운 가상 머신 인스턴스 생성
  • 무료 사용량 제한 있음 (월 2,000분)

2. Self-hosted Runners

  • 사용자가 직접 운영하는 서버
  • 자체 인프라에서 실행
  • 커스텀 환경 구성 가능
  • 지속적인 상태 유지 가능
  • 무제한 사용 가능

Self-hosted Runner를 선택한 이유:

프로젝트에서 Self-Hosted Runner를 선택한 이유는 다음과 같습니다. 

  1. 비용 절약 : 구축한 가상의 리눅스 서버에 Runner를 구성하여 비용 없이 무제한 빌드가 가능합니다. 
  2. 성능 최적화 : 전용 하드웨어 리소스로 빠른 빌드 속도 확보
  3. 커스텀 환경 : 프로젝터 전용 환경으로 설정을 자유롭게 구성 가능

 

🔹 시스템 아키텍처

전체 워크플로우

 

주요 구성 요소

1. Self-hosted Runner 서버

  • 역할: GitHub Actions 워크플로우 실행
  • 특징:
    • Go 빌드 환경 구축
    • 빌드 캐시 및 모듈 캐시 유지
    • SSH를 통한 운영서버 접근 권한

2. 운영서버

  • 역할: 실제 백엔드 서비스 운영
  • 특징:
    • 소스 코드 없음: 기존의 Go 소스 코드는 백업 후 삭제
    • 바이너리 + 구성 파일: 컴파일된 바이너리와 config.yaml 등 실행에 필요한 파일들만 보유
    • 간소화된 환경: 코드 관리 포인트 제거로 서버 환경 단순화
    • 서비스 재실행 bash 스크립트를 통한 백엔드 프로세스 재시작

 

🔹  워크플로우 구성

최적화 전략의 핵심

CI/CD 파이프라인의 효율성을 높이기 위해서 세 가지 단계로 워크플로우를 구성하였습니다.:

1단계: Quick Check (변경사항 감지)

모든 커밋에 대해 빌드를 수행하면 불필요한 리소스 낭비가 발생하였습니다. 따라서, 실제 Go 코드가 변경되었을 때만 빌드를 진행하도록 구성했습니다.

name: Quick Check (Change Detection)

on:
  push:
    branches: [main]

jobs:
  quick-check:
    runs-on: self-hosted
    outputs:
      should_build: ${{ steps.changes.outputs.backend }}
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      
      - uses: dorny/paths-filter@v3
        id: changes
        with:
          filters: |
            backend:
              - 'cmd/**'
              - 'internal/**'
              - 'pkg/**'
              - 'go.mod'
              - 'go.sum'

핵심 포인트:

  • paths-filter 액션을 사용하여 프로젝트 사용 경로(지정한 특정 경로 - cmd/, internal, ..., go.sum)의 파일 변경 감지
  • Go 소스 코드와 의존성 파일만 체크하여 불필요한 빌드 방지
  • 빌드 여부를 다음 잡에 전달

 

2단계: Optimized Build (최적화된 빌드)

Go 언어의 특성을 활용하여 빌드 속도를 극적으로 개선했습니다.

name: Optimized Build

on:
  workflow_run:
    workflows: ["Quick Check (Change Detection)"]
    types: [completed]

jobs:
  build:
    runs-on: self-hosted
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Clean Go Cache
        run: |
          # 매 빌드마다 새로운 캐시 디렉토리 생성
          CLEAN_CACHE_DIR="/tmp/go-cache-$(date +%s)"
          CLEAN_MOD_DIR="/tmp/go-mod-$(date +%s)"
          mkdir -p "$CLEAN_CACHE_DIR" "$CLEAN_MOD_DIR"
          
          echo "GOCACHE=$CLEAN_CACHE_DIR" >> $GITHUB_ENV
          echo "GOMODCACHE=$CLEAN_MOD_DIR" >> $GITHUB_ENV
      
      - name: Build
        run: |
          cd cmd/dnote
          CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
            go build -o project-backend -trimpath \
            -ldflags="-s -w" .
      
      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: project-backend
          path: cmd/project/project-backend

최적화 기법:

  • 매 빌드마다 독립된 캐시 디렉토리 사용으로 캐시 충돌 방지
  • trimpath로 빌드 경로 정보 제거하여 바이너리 크기 감소
  • -ldflags="-s -w"로 디버그 정보 제거하여 최종 바이너리 크기 최소화

3단계: Production Deploy (자동 배포)

빌드된 바이너리를 운영 서버에 자동으로 배포합니다.

name: Production Deploy

on:
  workflow_run:
    workflows: ["Optimized Build"]
    types: [completed]

jobs:
  deploy:
    runs-on: self-hosted
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    
    steps:
      - name: Download Artifact
        uses: actions/download-artifact@v4
        with:
          name: dnote-backend
          github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
          run-id: ${{ github.event.workflow_run.id }}
      
      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -p 22 192.168.0.10 >> ~/.ssh/known_hosts
      
      - name: Deploy to Production
        run: |
          # 바이너리 전송
          scp -P 22 project-backend \
            "${{ secrets.SSH_USER }}@192.168.0.10:/opt/project/"
          
          # 서비스 재시작
          ssh -p 22 "${{ secrets.SSH_USER }}@192.168.0.10" << 'EOF'
            # ChangeStream 토큰 초기화
            mongo --quiet --eval 'db.getSiblingDB("searchnote").resume_tokens.deleteMany({})'
            
            # 서비스 재시작
            sudo systemctl restart project-backend.service
            
            # 상태 확인 (별도 세션에서)
            sleep 2
            sudo systemctl status project-backend.service --no-pager
          EOF

배포 방식의 핵심:

  • 바이너리 배포: 소스 코드가 아닌 컴파일된 바이너리만 서버에 전송
  • 빠른 배포: 빌드는 Runner에서 수행, 서버는 바이너리만 받아서 실행
  • 안전한 재시작: bash 스크립트를 통한 백엔드 프로세스 재시작
  • ChangeStream Resume Token 초기화로 MongoDB 연결 문제 방지
  • 배포 후 자동 상태 확인

 

🔹 Self-hosted Runner 설정

Runner 설치 및 등록

GitHub 저장소의 Settings > Actions > Runners 메뉴에서 새로운 Self-hosted Runner를 추가할 수 있습니다.

Runner를 등록할 OS(Windows, Linux, MacOS) 별 구성 스크립트를 제공해주었기 때문에 쉽게 구성할 수 있었습니다. 

 

환경 설정

Runner 서버에 필요한 도구들을 설치합니다:

# Go 설치 (최신 버전)
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz

# 환경변수 설정
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc

# Git 설치
sudo apt-get update
sudo apt-get install git

# SSH 클라이언트 설정 (배포용)
sudo apt-get install openssh-client
 
 

 

🔹 보안 설정

GitHub Secrets 관리

민감한 정보는 GitHub Secrets에 저장하여 안전하게 관리해야 합니다. 

저장소 Settings > Secrets and variables > Actions에서 다음 시크릿을 추가:

SSH_PRIVATE_KEY # 운영서버 접근용 SSH 개인키
SSH_USER # 운영서버 사용자명
GITHUB_TOKEN # 아티팩트 다운로드용 (기본 제공됨)

 

🔹 트러블슈팅

작업 간 발생했던 문제들

1. Go 모듈 캐시 충돌

문제: 이전 빌드의 캐시가 새로운 의존성과 충돌하여 빌드 실패

해결: 매 빌드마다 새로운 캐시 디렉토리를 생성하여 깨끗한 환경에서 빌드하도록 개선

CLEAN_CACHE_DIR="/tmp/go-cache-$(date +%s)"
CLEAN_MOD_DIR="/tmp/go-mod-$(date +%s)"

 

2. SSH 연결 실패

문제: 운영서버 SSH 연결이 간헐적으로 실패

해결:

  • SSH 키 권한을 600으로 명확히 설정
  • known_hosts에 서버 정보를 사전 등록하여 호스트 확인 단계 생략
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p 11122 [운영서버주소] >> ~/.ssh/known_hosts

 

3. 서비스 재시작 실패

문제: 배포 후 MongoDB ChangeStream 관련 에러로 서비스 시작 실패

해결:

  • 서비스 재시작 전에 ChangeStream 토큰 파일 삭제
  • restart.sh 스크립트로 안정적인 재시작 프로세스 구현
# ChangeStream 토큰 파일 삭제
rm -f internal/sync/*_resume_token.bin

# 서비스 재시작
./restart.sh

🔹 성과 및 개선점

CI/CD 파이프라인 구축을 통해 다음과 같은 성과를 얻을 수 있었습니다:

 

코드 버전 관리 개선 

  • 서버 환경이 깔끔해지고 관리 포인트 감소
  • 어떤 커밋이 어느 서버에 배포되었는지 명확히 추적
  • 로그 시스템도 바이너리 실행 환경에 최적화

배포 안정성 향상 

  • 표준화된 배포 프로세스로 휴먼 에러 제거
  • Runner에서 일관된 빌드 환경 보장
  • 배포 실패 시 자동 감지

개발 효율성 증대 

  • main 브랜치에 푸시하면 자동으로 배포
  • 배포 과정 모니터링 가능

 

향후 개선 계획

현재 시스템이 잘 작동하고 있지만, 아래 항목들에 대한 개선을 계획중입니다. 

배포 승인 프로세스 

  • Pull Request 기반 배포 승인 시스템 구축 (개선 중)
  • 자동 빌드 후 수동 승인을 통한 배포
  • 배포 전 검토 단계 추가로 안정성 강화
  1. 테스트 자동화
    • 단위 테스트 추가
    • 통합 테스트 자동 실행
    • 테스트 실패 시 배포 중단
  2. 알림 시스템 
    • Slack 연동
    • 배포 상태 실시간 알림
    • 에러 발생 시 즉시 알림

 

🔹 마치며

Self-hosted GitHub Runner를 활용한 CI/CD 파이프라인 구축은 중소규모 프로젝트에서도 충분히 적용 가능한 방법입니다.

특히 이미 서버 인프라를 보유하고 있다면, GitHub-hosted Runner의 시간 제한 없이 무제한으로 자동화를 활용할 수 있다는 점이 큰 장점이라 생각합니다. 

처음 동작하기까지의 과정이 다소 복잡하였지만, 한 번 구축해두면 그 이후로는 정말 편리하게 사용할 수 있었습니다.

제가 원했던 코드 작성에만 집중할 수 있는 환경을 만들어준다는 점에서, CI/CD 파이프라인 구축은 충분히 투자할 가치가 있는 작업이라고 생각합니다.

이 글이 CI/CD 파이프라인 구축을 고민하는 분들에게 도움이 되었으면 좋겠습니다.

반응형