GitHub Actions 기반 Blue-Green 배포를 GitLab CI/CD로 이전한 과정

GitHub Actions 무료 사용량 한계에 도달하면서 Podman, Caddy, Infisical 기반 무중단 배포 환경을 GitLab CI/CD로 이전한 기록입니다.

Share
GitHub Actions 기반 Blue-Green 배포를 GitLab CI/CD로 이전한 과정

vibePulse는 Podman, Caddy, Infisical을 기반으로 Blue-Green 무중단 배포를 운영하고 있습니다.

서비스 규모는 크지 않지만, 배포 과정에서는 Web, API, Worker, DB Migration 이미지를 각각 빌드하고 배포합니다. 초기에는 GitHub Actions를 사용해 큰 문제 없이 운영하고 있었습니다.

하지만 예상보다 빠르게 한계에 도달했습니다.

프로젝트 하나를 운영한 지 한 달도 되지 않았는데 GitHub Actions 무료 제공 시간 2,000분의 90% 이상을 사용했다는 경고를 받았습니다.

Blue-Green 배포 구조에서는 컨테이너 이미지를 자주 빌드하고 배포하게 됩니다. 지금은 작은 프로젝트라도 서비스가 성장하고 배포 빈도가 늘어나면 GitHub Actions 사용량은 계속 증가할 가능성이 높았습니다.

결국 비용 문제는 시간 문제라고 판단했습니다.

그래서 이번 작업의 목표는 명확했습니다.

  • GitHub Actions → GitLab CI/CD 이전
  • 기존 Blue-Green 배포 구조 유지
  • 무중단 배포 유지
  • 자동 롤백 유지
  • Podman, Caddy, Infisical 구성 유지

즉, 새로운 배포 구조를 만드는 것이 아니라 기존 배포 시스템을 GitHub에서 GitLab로 이사시키는 작업이었습니다.


기존 배포 구조

이전 작업을 시작하기 전에 현재 구조를 다시 정리했습니다.

Build

→ Push Image

→ SSH

→ Blue Slot 기동

→ Health Check

→ DB Migration

→ Caddy Switch

→ Public Health Check

→ Old Slot 제거

트래픽은 항상 Caddy를 통해 전달되며, 새로운 버전은 사용 중이지 않은 슬롯에 먼저 배포됩니다.

정상 동작이 확인되면 트래픽을 전환하고, 문제가 발생하면 롤백하는 방식입니다.

기존 아키텍처


GitLab Runner 구축

먼저 GitLab Runner를 설치하고 등록했습니다.

정상 등록 여부를 확인했습니다.

Running with gitlab-runner 19.1.0
on vibepulse-podman-runner

Runner 등록은 문제없이 완료되었습니다.

하지만 첫 번째 Deploy Job이 실행되지 않았습니다.

deploy
Pending

This job has not started yet
This job is in pending state and is waiting to be picked by a runner

처음에는 Runner 상태 문제라고 생각했습니다.

하지만 원인은 단순했습니다.

GitLab Job에 지정된 Tag와 Runner Tag가 일치하지 않았습니다.

Runner Tag를 수정한 후 Deploy Job이 정상적으로 실행되었습니다.


기존 Blue-Green 구조 점검

CI/CD 플랫폼을 바꾸기 전에 먼저 배포 스크립트가 어떤 구조인지 확인했습니다.

grep -nE "blue|green|ACTIVE|Caddy|health|rollback|compose" scripts/deploy.sh

결과를 확인해 보니 이미 필요한 기능이 대부분 구현되어 있었습니다.

blue)  echo "3110 3111 3112"
green) echo "3210 3211 3212"

switching Caddy

public health gate

rollback

즉, 배포 전략 자체를 다시 만들 필요는 없었습니다.

GitHub Actions에 의존하던 부분만 제거하고 GitLab CI/CD에 연결하면 되는 상태였습니다.


Blue-Green 포트 구성

슬롯별 포트 구성은 다음과 같습니다.

SlotWebAPIWorker
Blue311031113112
Green321032113212

트래픽 전환은 모두 Caddy가 담당합니다.


Caddy 구성 정리

현재 Caddy 상태를 확인했습니다.

sudo systemctl enable --now caddy
sudo systemctl status caddy --no-pager

결과는 정상입니다.

Active: active (running)

방화벽도 함께 확인했습니다.

sudo systemctl is-active firewalld
sudo firewall-cmd --state
active
running

허용된 서비스는 다음과 같습니다.

dhcpv6-client
http
https
ssh

80, 443 포트 모두 정상적으로 개방되어 있었습니다.


Caddy Import 구조 적용

기존 Caddyfile은 기본 상태였습니다.

:80 {
    root * /usr/share/caddy
    file_server
}

하지만 deploy.sh를 확인해 보니 배포 시 다음 파일을 생성하도록 되어 있었습니다.

/etc/caddy/sites/vibepulse.caddy

즉, Import 기반 구성을 전제로 작성된 스크립트였습니다.

그래서 Caddyfile을 다음과 같이 수정했습니다.

import /etc/caddy/sites/*

적용 과정은 다음과 같습니다.

sudo mkdir -p /etc/caddy/sites

printf 'import /etc/caddy/sites/*\n' | sudo tee /etc/caddy/Caddyfile

sudo caddy validate --config /etc/caddy/Caddyfile

sudo systemctl reload caddy

결과는 정상입니다.

Valid configuration
Reloaded Caddy

GitHub 환경변수 의존성 제거

GitLab에서 첫 배포를 시도하자 GitHub 시절의 흔적이 드러나기 시작했습니다.

OWNER required

첫 번째 에러는 다음과 같았습니다.

OWNER required

원인은 다음 코드였습니다.

: "${OWNER:?OWNER required}"

GitHub Actions에서는 자연스럽게 사용되던 값이 GitLab에서는 존재하지 않았습니다.

해결 방법은 간단했습니다.

export OWNER="$CI_REGISTRY_IMAGE"

GitLab Registry 경로를 그대로 활용하도록 변경했습니다.


HEALTH_URL required

두 번째 에러입니다.

HEALTH_URL required

원인은 동일했습니다.

: "${HEALTH_URL:?HEALTH_URL required}"

환경별 Health URL을 동적으로 읽어오도록 수정했습니다.

eval "HEALTH_URL=\${${DEPLOY_PREFIX}_HEALTH_URL}"

이후 환경에 따라 적절한 URL이 자동으로 선택되었습니다.


Registry 구조 재설계

다음 문제는 이미지 Pull 단계에서 발생했습니다.

manifest unknown

로그를 확인해 보니 이미지 경로가 이상했습니다.

ghcr.io/registry.goodtek.xyz/...

GitHub Container Registry 기준으로 작성된 구조가 GitLab Registry와 충돌하고 있었습니다.

기존 구조

ghcr.io/<owner>/vibepulse-web
ghcr.io/<owner>/vibepulse-api
ghcr.io/<owner>/vibepulse-worker

변경 후

registry.goodtek.xyz/goodtek/vibepulse/web
registry.goodtek.xyz/goodtek/vibepulse/api
registry.goodtek.xyz/goodtek/vibepulse/worker
registry.goodtek.xyz/goodtek/vibepulse/db-migrate

Registry 구조를 GitLab 기준으로 재설계했습니다.


Compose 및 Deploy 스크립트 수정

기존 Compose 설정은 GHCR을 직접 참조하고 있었습니다.

기존 설정입니다.

image: ghcr.io/${GHCR_OWNER}/vibepulse-web:${IMAGE_TAG}

변경 후에는 Registry 종류와 무관하게 사용할 수 있도록 수정했습니다.

image: ${IMAGE_REGISTRY}/web:${IMAGE_TAG}

API와 Worker도 동일하게 변경했습니다.

image: ${IMAGE_REGISTRY}/api:${IMAGE_TAG}
image: ${IMAGE_REGISTRY}/worker:${IMAGE_TAG}

배포 스크립트에서도 GitHub Registry 관련 변수들을 제거했습니다.

기존:

GHCR_OWNER
GHCR_USER
GHCR_PULL_TOKEN

변경:

IMAGE_REGISTRY

이미지 Pull 코드 역시 단순해졌습니다.

img="${IMAGE_REGISTRY}/${svc}:${IMAGE_TAG}"

Registry 인증 문제

배포 도중 또 하나의 문제가 발생했습니다.

403 Forbidden

원인은 서버 측 Registry 인증 실패였습니다.

GitHub Container Registry 기준으로 작성된 로그인 방식을 GitLab Registry 기준으로 변경한 뒤 정상적으로 이미지 Pull이 가능해졌습니다.


최종 Pipeline

최종 Pipeline은 다음과 같이 구성했습니다.

CI

→ Build Web

→ Build API

→ Build Worker

→ Build DB-Migrate

→ Deploy

실행 결과는 다음과 같습니다.

Pipeline #17

Passed

build:web
build:api
build:worker
build:db-migrate

deploy

모든 Job이 정상적으로 통과했습니다.


GitHub Actions와 GitLab CI/CD 비교

이번 이전 작업에서 변경된 요소를 정리하면 다음과 같습니다.

항목이전이후
CI/CDGitHub ActionsGitLab CI/CD
RegistryGHCRGitLab Registry
RunnerGitHub Hosted RunnerSelf-hosted Runner
Secret 관리InfisicalInfisical
Reverse ProxyCaddyCaddy
RuntimePodmanPodman
DeploymentBlue-GreenBlue-Green
Rollback자동자동

배포 전략 자체는 거의 변경되지 않았습니다.


최종 Blue-Green 배포 흐름

최종 배포 흐름은 다음과 같습니다.


마치며

이번 이전 작업을 통해 다시 한번 확인할 수 있었습니다.

좋은 배포 구조는 특정 CI/CD 플랫폼에 의존하지 않습니다.

CI/CD는 교체할 수 있어야 하고, 배포 전략은 독립적으로 유지될 수 있어야 합니다.

이번 GitLab CI/CD 이전 과정에서도 Blue-Green 배포, 자동 롤백, Health Check, Secret 관리 구조를 거의 수정하지 않고 유지할 수 있었습니다. 이는 배포 전략이 GitHub Actions 자체에 종속되어 있지 않았기 때문입니다.

결과적으로 GitHub Actions 사용량과 비용 부담은 줄이면서도 기존 무중단 배포 경험은 그대로 유지할 수 있었습니다.

앞으로 다른 CI/CD 플랫폼으로 이전하더라도 동일한 원칙을 적용할 수 있을 것이라고 생각합니다.

배포 전략은 남기고, CI/CD는 교체할 수 있게 만드는 것.

이번 이전 작업에서 얻은 가장 큰 교훈이었습니다.

Read more

AI와 함께 코딩할 때 먼저 배운 것: 만든 것과 만들려던 것을 구분하기

AI와 함께 코딩할 때 먼저 배운 것: 만든 것과 만들려던 것을 구분하기

AI와 함께 코딩하면 계획, 프롬프트, 문서, 실제 코드가 쉽게 섞입니다. 이번 TASK-013에서는 공개 상태 페이지를 만들려 했지만, 실제로 ship된 것은 실시간 대시보드, 알림 테스트 상태 추적, 디자인 정합 작업이었습니다. 이번 글에서는 TASK 이름보다 머지된 코드를 기준으로 사실을 검증한 과정과, Realtime 아키텍처·비동기 상태 모델·공통 계약 관리 등 AI와 함께 개발할 때 놓치기 쉬운 기준들을 정리합니다. Build in Public 관점에서 ‘만들려던 것’과 ‘실제로 만든 것’을 구분하는 방법을 공유합니다.

By ● goodtek
감지 → 판단 → 알림: vibePulse의 심장을 만든 6개의 TASK

감지 → 판단 → 알림: vibePulse의 심장을 만든 6개의 TASK

goodtek이 만들고 있는 vibePulse는 "내가 만든 웹/API가 지금 살아 있는가"를 확인하고, 죽으면 즉시 알려주는 초간단 생존 확인 SaaS입니다. 이번 글은 그 핵심 파이프라인 — 모니터를 만들고(TASK-006), 주기적으로 찌르고(007), 장애를 판단하고(008), 신호가 끊기면 알아채고(009), 알림을 큐에 태워(010), 슬랙·카카오톡으로 보내기까지(011) — 를 한 호흡에 만든 기록입니다.

By ● goodtek
TASK-005 OAuth 로그인 구현

TASK-005 OAuth 로그인 구현

vibePulse TASK-005에서는 OAuth 로그인 흐름을 구현했습니다. 처음에는 이메일, 카카오, 네이버까지 함께 고려했지만, 초기 타겟인 바이브코더와 개발자에게 가장 빠르게 닿는 Google·GitHub 로그인에 집중하기로 결정했습니다. 인증 범위를 줄이는 과정, Better Auth 구현, allowlist, 온보딩 연결, i18n UX와 로그인 화면 개선까지 실제 시행착오를 정리했습니다.

By ● goodtek