
CI/CD 파이프라인을 처음 만들 때는 이틀이 걸렸다. 두 번째는 10분이었다.
멀티테넌트 블로그 프로젝트(adsense-blog)에 museck과 동일한 수준의 배포 파이프라인이 필요했다. git push 하면 자동으로 Docker 이미지 빌드하고, K8s에 롤링 업데이트하는 그 흐름 그대로. museck에서 이미 검증된 파이프라인이 있으니 그걸 복제하기로 했다.
museck에서 만든 GitOps 파이프라인은 이런 흐름이다.
새 프로젝트에 이 파이프라인을 복제할 때 바꿔야 하는 건 딱 세 군데다: 레지스트리 이미지 경로, homelab 레포의 overlay 경로, 그리고 Gitea Actions secrets.
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이 안 될 수 있다.
워크플로 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 경로.
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에서도 동일한 패턴을 쓰고 있어서 그대로 가져온 것이다.
유일하게 시간을 잡아먹은 건 secrets 설정이었다. 워크플로를 복사하고 push했는데 레지스트리 인증 에러가 났다. Gitea Actions secrets는 레포별로 설정해야 하는데, 새 레포에 secrets를 추가하는 걸 깜빡했다. secrets를 추가한 뒤에도 이미 실행 중인 워크플로에는 반영되지 않아서 재트리거가 필요했다.
체크리스트로 정리하면 이렇다:
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 트리거로 명시적으로 배포하는 것이 안전하다.