개요

프로젝트를 진행하다 보면 라이브러리의 보안 취약점(CVE) 해결이나 버전 업그레이드는 피할 수 없는 숙제와 같습니다. 하지만 운영 중인 서비스에 바로 적용하기엔 리스크가 컸고(사실 사용자가 거의 없다시피 하지만 그러한 상황을 상정하고 ), 이를 안전하게 검증할 수 있는 독립적인 테스트 환경자동화된 파이프라인이 필요했습니다.


1. 초기 환경 구축

사실 우리 프로젝트는 2명으로 구성된 소규모 프로젝트였기에, 처음에는 main 브랜치에 푸시하면 바로 배포되는 단일 브랜치 파이프라인만으로도 충분했습니다. 협업 환경이 복잡하지 않아 정석적인 멀티 브랜치 운영의 필요성을 크게 느끼지 못했기 때문입니다. 하지만 마음 한편으로는 언젠가 프로젝트가 커지면 도입해야 할 ‘숙제’처럼 생각하고 있었습니다.

도입의 직접적인 트리거는 의존성 라이브러리 업데이트였습니다. 보안 취약점을 해결하기 위해 라이브러리 버전을 올려야 하는데, 이 작업이 기존 기능에 영향을 주는지 확인하려면 실제 환경과 유사한 곳에서의 테스트가 반드시 필요했습니다. 메인 배포 라인을 건드리지 않고 브랜치별로 안전하게 테스트를 수행하기 위해, 드디어 Jenkins Multibranch Pipeline을 구축하게 되었습니다.

graph TD
    Dev[🧑‍💻 개발자]
    
    subgraph GitHub ["GitHub Repository"]
        Main["branch: main"]
        Update["branch: feat/dependency-update"]
    end

    subgraph Jenkins ["Jenkins Multibranch Project"]
        Scanner["🔍 Repository Scan"]
        PipeMain["🔄 Pipeline: main (Deploy)"]
        PipeUpdate["🧪 Pipeline: update (Test Only)"]
    end

    Dev -- Push --> GitHub
    GitHub -- Webhook --> Scanner
    Scanner -- "Branch 감지" --> PipeMain
    Scanner -- "Branch 감지" --> PipeUpdate

웹훅 구성을 위한 추가 플러그인

Jenkins의 멀티 브랜치 파이프라인은 일반적인 Job과 달리 GitHub 웹훅 연동을 위해 별도의 플러그인(예: Multibranch Scan Webhook Trigger 또는 Remote Jenkinsfile Provider) 설치가 필수적입니다. 하지만 이 중 Multibranch Scan Webhook Trigger 플러그인은 업데이트가 오랫동안 멈춰 있어 조금 불안한 느낌이 들어었습니다.


2. 빌드 환경 분리

리소스 효율화와 환경 격리를 위해 빌드 전용 서버(@jw)를 운영 서버(@jh)와 분리했습니다. 두 서버는 물리적으로 떨어져 있어 Wireguard VPN으로 연결했으나, 두 장소 모두 공유기 기본 대역인 192.168.1.x를 사용하고 있어 사설 IP 충돌이 발생했습니다.

이를 해결하기 위해 @jw 서버의 Proxmox SDN(Software Defined Network) 기능을 활용했습니다. Proxmox 내부에서 172.16.1.x 대역의 가상 네트워크(VNet)를 생성하고 Jenkins Agent LXC를 이곳에 할당함으로써, 운영 대역과의 충돌 없이 독립적인 네트워크 망을 구축했습니다.

wireguard 설치 및 적용

# wireguard 설치
apt update && apt install wireguard -y
 
# wiregurad config 설정
# 여기에 wireguard 서버로 부터 받은 config 내용을 붙여넣기
nano /etc/wireguard/wg0.conf
 
# 실행
wg-quick up wg0
 
# 확인
wg show wg0

Wireguard 설치 후 실행 시 /usr/bin/wg-quick: line 32: resolvconf: command not found 에러가 뜰 경우 resolvconf를 설치하여 계속 진행합니다. 정상적으로 설정이 완료되면 다음과 같이 vpn이 추가됨을 확인할 수 있습니다.


3. 웹훅 트러블슈팅 및 한계점 인지

Jenkins 파이프라인 트리거를 위해 GitHub Webhook을 설정하던 중 403 Forbidden 에러가 계속 발생했습니다. 아래의 해결 방법들을 적용해도 계속 발생했는데, 원인은 설정 변경 전 실패했던 Push 이벤트를 재전송(Redeliver) 하여 테스트 했기 때문이었습니다.

새로운 Push 이벤트는 정상적으로 처리됨을 확인했으나, 이 과정에서 Jenkins의 Crumb 이슈 등 복잡한 설정 관리와 플러그인 의존성과 설정 관리 비용을 줄이기 위해 GitHub Actions로의 이전을 결정했습니다.


4. GitHub Actions 검토 및 도입 결정

Jenkins 운영의 피로도를 해결하기 위해 GitHub Actions로의 전환을 검토했습니다. 특히 다음과 같은 지점에서 명확한 이점을 확인했습니다.

  • 통합 가시성: 별도의 대시보드 없이 PR(Pull Request) 페이지에서 빌드 상태와 테스트 결과를 즉시 확인 가능.

  • 리소스 자유도: Self-hosted Runner를 운용함으로써 Private Repository의 실행 시간 제약 없이 내부 인프라 자원을 활용 가능.

  • 네이티브 통합: Webhook 설정의 번거로움 없이 GitHub 생태계 내에서 모든 워크플로우 제어 가능.

( Self-hosted Runner로 사용할 4-cpu, 8gb ram n100 pc 는 기존 jenkins 빌드에서도 별 무리 없이 사용 중이었습니다. )


5. 도커 이미지 태그 관리 전략

Github Actions로 넘어오면서 이미지 태깅 전략을 고도화했습니다. 기존 Jenkins의 로직을 쉘 스크립트로 구현하여 자동화했습니다.

태그 생성 스크립트 예시

if [[ "$BRANCH_NAME" == "main" ]]; then
  TAGS="${IMAGE_NAME}:latest"
  TAGS="${TAGS}\n${IMAGE_NAME}:prod-${GIT_COMMIT}"
  TAGS="${TAGS}\n${IMAGE_NAME}:prod-${GIT_COMMIT}-b${BUILD_NUMBER}"
elif [[ "$BRANCH_NAME" =~ ^(feat|fix)/([0-9]+) ]]; then
  TAGS="${IMAGE_NAME}:${TYPE}-${ISSUE_NUM}"
  TAGS="${TAGS}\n${IMAGE_NAME}:${TYPE}-${ISSUE_NUM}-b${BUILD_NUMBER}"
fi

왜 이런 전략이 필요한가?

  1. 추적성: 특정 이미지 태그만 보고도 어떤 커밋(GIT_COMMIT)에서 빌드되었는지 즉시 파악 가능합니다.

  2. 불변성: latest 태그는 계속 변하지만, 빌드 번호나 SHA가 포함된 태그는 고유하므로 안정적인 롤백 지점을 제공합니다.

  3. 병렬 배포: 기능별 브랜치가 각자의 이슈 번호 태그를 가지므로, 동일한 서버 내에서 여러 버전을 충돌 없이 테스트할 수 있습니다.

태그된 도커 이미지

빌드 #22 최신 이미지

빌드 #14 이슈 #175 이미지

6. 최종 시스템 구조 및 결과

마이그레이션 이후 구조는 훨씬 단순해졌으며 가시성은 높아졌습니다.

  • 백엔드: Gradle 테스트 실행 시 dorny/test-reporter를 도입하여 JUnit 테스트 결과를 GitHub UI상에서 가독성 있게 확인.
  • 프론트엔드: PR 발생 시 main과 머지하여 테스트 및 이미지 빌드 후 curl을 이용한 간단한 Smoke Test로 작동 확인.
	#!/bin/sh
 
	URL=${1:-http://localhost:3000}
	
	# curl 옵션 설명:
	# -f: HTTP 400/500 에러 시 실패 코드를 반환
	# -s: 진행 바 등 불필요한 출력 제거
	# -o /dev/null: 응답 본문을 무시하여 메모리 절약
	# -m 3: 응답 대기 시간을 3초로 제한 (타임아웃 방지)
	
	if curl -f -s -m 3 -o /dev/null "$URL"; then
	    # 성공 시 아무것도 출력하지 않고 종료 코드 0 반환
	    exit 0
	else
	    # 실패 시 표준 에러로 실패 로그를 남기고 종료 코드 1 반환
	    echo "Healthcheck failed for $URL" >&2
	    exit 1
	fi

변경된 워크플로 개요

graph TD
    Dev[🧑‍💻 개발자]

    subgraph GitHub ["GitHub Organization"]
        Repo[("📦 Repository")]
        Actions["⚡ GitHub Actions"]
    end

    subgraph Runners ["Self-hosted Runners"]
        BuildRunner["🏗️ Build Runner (jw)<br/>Label: build"]
        ProdRunner["🚢 Prod Runner (jh)<br/>Label: prod"]
    end

    Dev -- "Push/PR" --> Repo
    Actions -- "Trigger" --> BuildRunner
    BuildRunner -- "Push Image" --> GHCR[("🐳 GHCR")]
    Actions -- "Deploy" --> ProdRunner
    ProdRunner -- "Pull & Up" --> GHCR

결과

이번 마이그레이션을 통해 복잡한 Jenkins 관리에서 벗어나, 코드 중심의 안정적인 배포 환경을 구축했습니다. 특히 이미지 태깅 전략은 향후 서비스 확장 시 큰 도움이 될 것이라 생각합니다.

참고