무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
CI/CD K8s 배포 복제 — bauhaus 키 비주얼
💰 애드센스 블로그 네트워크
2026. 2. 28.

기존 프로젝트의 CI/CD를 새 프로젝트에 복제하기: Gitea Actions + ArgoCD

CI/CD 파이프라인을 처음 만들 때는 이틀이 걸렸다. 두 번째는 10분이었다.

멀티테넌트 블로그 프로젝트(adsense-blog)에 museck과 동일한 수준의 배포 파이프라인이 필요했다. git push 하면 자동으로 Docker 이미지 빌드하고, K8s에 롤링 업데이트하는 그 흐름 그대로. museck에서 이미 검증된 파이프라인이 있으니 그걸 복제하기로 했다.

파이프라인 전체 흐름

museck에서 만든 GitOps 파이프라인은 이런 흐름이다.

새 프로젝트에 이 파이프라인을 복제할 때 바꿔야 하는 건 딱 세 군데다: 레지스트리 이미지 경로, homelab 레포의 overlay 경로, 그리고 Gitea Actions secrets.

Dockerfile 재활용

museck의 Dockerfile을 거의 그대로 가져왔다. Next.js standalone 출력 + tini 조합은 프로젝트에 무관하게 동일하다.

# 핵심 구조 (museck과 동일)
FROM node:22-alpine AS base
RUN apk add --no-cache tini

# ... build stages ...

FROM base AS runner
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

tini를 PID 1로 두는 건 K8s 환경에서 SIGTERM을 제대로 전달받기 위해서다. Node.js가 직접 PID 1이 되면 graceful shutdown이 안 될 수 있다.

Gitea Actions 워크플로

워크플로 YAML도 museck 것을 복사해서 경로만 변경했다.

# .gitea/workflows/deploy.yaml
name: Deploy to Staging
on:
  push:
    branches: [master]

jobs:
  deploy:
    steps:
      - name: Build and push
        run: |
          docker build -t $REGISTRY/$IMAGE:$TAG .
          docker push $REGISTRY/$IMAGE:$TAG
      - name: Update staging tag
        run: |
          # homelab 레포 clone → kustomization.yaml 태그 변경 → push

바꿔야 하는 부분은 환경변수 세 개뿐이다: REGISTRY(레지스트리 호스트), IMAGE(이미지 이름), 그리고 homelab 레포에서 업데이트할 overlay 경로.

warm-cache로 콜드 스타트 방지

ISR 기반 페이지는 첫 요청 시 서버 렌더링 후 캐싱된다. 배포 직후 첫 방문자가 느린 응답을 받지 않도록, 주요 페이지에 미리 요청을 보내는 warm-cache 스크립트를 추가했다.

#!/bin/bash
# warm-cache.sh
DOMAINS=("trend.example.com" "cert.example.com")
for domain in "${DOMAINS[@]}"; do
  curl -s "https://${domain}/" > /dev/null
  curl -s "https://${domain}/sitemap.xml" > /dev/null
done

이 스크립트는 K8s 컨테이너가 ready 상태가 된 직후에 실행된다. museck에서도 동일한 패턴을 쓰고 있어서 그대로 가져온 것이다.

삽질: Gitea Actions secrets

유일하게 시간을 잡아먹은 건 secrets 설정이었다. 워크플로를 복사하고 push했는데 레지스트리 인증 에러가 났다. Gitea Actions secrets는 레포별로 설정해야 하는데, 새 레포에 secrets를 추가하는 걸 깜빡했다. secrets를 추가한 뒤에도 이미 실행 중인 워크플로에는 반영되지 않아서 재트리거가 필요했다.

체크리스트로 정리하면 이렇다:

  • Dockerfile 복사 (경로 수정 불필요)
  • 워크플로 YAML 복사 → 레지스트리 경로, overlay 경로 변경
  • Gitea Actions secrets 설정 (REGISTRY_PASSWORD, DEPLOY_TOKEN)
  • homelab 레포에 새 overlay 디렉터리 생성
  • ArgoCD에 새 Application 등록
  • /api/health 엔드포인트 확인

결과와 배운 것

museck의 파이프라인을 그대로 복사하니 95%가 동작했다. 나머지 5%는 secrets 설정과 overlay 경로 같은 환경별 차이뿐이었다.

CI/CD의 진짜 가치는 첫 번째 프로젝트가 아니라 두 번째 프로젝트에서 드러난다. 처음 파이프라인을 만들 때 "이걸 다른 프로젝트에서도 쓸 수 있게" 구조를 잡아두면, 이후에는 복사-붙여넣기에 경로 몇 개 바꾸는 것으로 끝난다.

재사용 가능한 인프라 패턴을 만드는 데 시간을 투자하는 건, 1인 개발자에게 특히 레버리지가 큰 작업이다.

자주 묻는 질문

Q. 기존 프로젝트의 CI/CD를 복제하는 데 실제로 얼마나 걸리나?

파이프라인 구조가 잘 잡혀 있다면 10~15분이면 된다. Dockerfile 복사, 워크플로 YAML에서 레지스트리 경로와 homelab overlay 경로 변경, Gitea Actions secrets 설정이 전부다. 단, 처음 만들 때 이 구조를 잡는 데는 꽤 시간이 들었다.

Q. warm-cache.sh는 왜 필요한가?

ISR(Incremental Static Regeneration) 기반 페이지는 첫 요청 시 서버에서 렌더링하고 캐싱한다. 배포 직후 첫 방문자가 느린 콜드 스타트를 경험하지 않도록, 배포 후 주요 페이지에 미리 요청을 보내 캐시를 워밍하는 스크립트다.

Q. ArgoCD auto-sync와 수동 sync의 차이는?

auto-sync는 Git 레포의 변경을 감지하면 자동으로 K8s에 반영한다. staging 환경에 적합하다. production은 수동 sync 또는 workflow_dispatch 트리거로 명시적으로 배포하는 것이 안전하다.

자주 묻는 질문

기존 프로젝트의 CI/CD를 복제하는 데 실제로 얼마나 걸리나?
파이프라인 구조가 잘 잡혀 있다면 10~15분이면 된다. Dockerfile 복사, 워크플로 YAML에서 레지스트리 경로와 homelab overlay 경로 변경, Gitea Actions secrets 설정이 전부다. 단, 처음 만들 때 이 구조를 잡는 데는 꽤 시간이 들었다.
warm-cache.sh는 왜 필요한가?
ISR(Incremental Static Regeneration) 기반 페이지는 첫 요청 시 서버에서 렌더링하고 캐싱한다. 배포 직후 첫 방문자가 느린 콜드 스타트를 경험하지 않도록, 배포 후 주요 페이지에 미리 요청을 보내 캐시를 워밍하는 스크립트다.
ArgoCD auto-sync와 수동 sync의 차이는?
auto-sync는 Git 레포의 변경을 감지하면 자동으로 K8s에 반영한다. staging 환경에 적합하다. production은 수동 sync 또는 workflow_dispatch 트리거로 명시적으로 배포하는 것이 안전하다.
💰 애드센스 블로그 네트워크(3/3)
Prev

멀티테넌트 블로그의 SEO 전략: 동적 sitemap, JSON-LD, canonical URL