콘텐츠로 이동

CI/CD 운영

windforce 플랫폼 자체(server·worker 바이너리와 임베드된 콘솔)를 클러스터에 배포하는 방법을 다룬다. 핵심은 한 가지다 — 배포는 릴리스 태그 v<semver>를 push할 때 일어나고, 그 뒤는 전부 자동이다. 수동 docker pushhelm upgrade는 쓰지 않는다.

이 페이지의 "배포"는 플랫폼 배포다. 콘솔의 Deploy 버튼(사용자가 작성한 액션을 git에 export하는 흐름)은 완전히 다른 층이다 — 그건 콘솔 가이드앱·액션을 참고한다. 이 페이지는 windforce 바이너리·콘솔 그 자체를 어떻게 빌드해 클러스터에 올리느냐다.

두 가지 트리거 — 검증과 배포

코드를 main에 push하는 것과 릴리스 태그를 push하는 것은 결과가 다르다.

행위 빌드되는 이미지 배포되나?
git push (main) 검증 이미지 main-<run>-<sha> 아니오 — 빌드만 하고 끝
git tag v<semver> + push 배포 이미지 v<semver> — Flux가 자동 롤아웃

main 머지는 "이 코드가 빌드는 되는가"를 확인하는 검증 단계일 뿐이다. 무엇을 언제 배포할지는 릴리스 태그라는 명시적 행위가 정한다. 그래서 main에 머지가 쌓여도 운영 환경은 가만히 있고, 릴리스 준비가 됐을 때 태그를 쳐야 비로소 롤아웃된다.

파이프라인 한눈에

flowchart TD
    PUSHM["git push main (코드)"] --> CIV["CI: 검증 이미지 빌드<br/>(main-run-sha)"]
    CIV --> GHCRV[("ghcr (검증 이미지)<br/>배포 안 됨")]
    TAG["git tag v-semver + push"] --> CID["CI: 배포 이미지 빌드<br/>(인클러스터 arm64 러너)"]
    CID --> GHCR[("ghcr.io<br/>windforce:v-semver")]
    GHCR --> IP["Flux ImagePolicy<br/>(semver, v* 만)"]
    IP --> IUA["ImageUpdateAutomation<br/>(태그 bump 커밋 → git)"]
    IUA --> GIT[("git: deploy/clusters<br/>(클러스터 상태 정본)")]
    GIT --> HR["Flux HelmRelease"]
    HR --> ROLL["클러스터 롤아웃<br/>(server + worker + 임베드 콘솔)"]

각 단계가 하는 일:

  • CI 빌드 — 빌드는 클러스터 안의 arm64 러너 pod(GitHub Actions Runner Controller, ARC)에서 돈다. 클러스터 노드가 arm64라 네이티브로 빌드해 ghcr에 push한다. 별도 CI 서버는 없고, push 인증은 워크플로의 GITHUB_TOKEN을 쓴다.
  • 단일 이미지 — 멀티스테이지 빌드로 콘솔(SPA)을 server 바이너리에 임베드한다. 그래서 콘솔은 별도 이미지가 아니라 server 바이너리에 들어가고, 하나의 windforce 이미지가 API와 콘솔을 모두 서빙한다.
  • Flux 이미지 자동화ImagePolicy가 ghcr에 올라온 태그 중 semver(v*)만 배포 후보로 본다. 새 v<semver>가 보이면 ImageUpdateAutomation이 그 태그를 git의 HelmRelease에 커밋한다.
  • HelmRelease 롤아웃 — git이 클러스터 상태의 정본이다. 커밋이 들어가면 Flux가 HelmRelease를 reconcile해 server·worker를 새 이미지로 굴린다.

검증 이미지 main-<run>-<sha>는 semver 형식이 아니므로 ImagePolicy가 거른다 — 그래서 빌드만 되고 결코 배포되지 않는다.

배포하기

릴리스 준비가 됐으면 태그를 push한다.

git tag v0.1.1
git push origin v0.1.1

# 빌드 진행 확인
gh run watch

# 빌드가 끝나면 Flux가 자동으로 ghcr를 스캔 → 커밋 → 롤아웃

태그 push 후엔 기다리면 된다. 즉시 반영을 보려면 아래 강제 reconcile로 단계를 당길 수 있다.

지금 무엇이 떠 있나

배포 상태의 정본 가시성은 Grafana 대시보드다. CLI로 즉답이 필요하면:

flux get all -A            # GitRepository / Kustomization / HelmRelease / Image* 한눈에
flux get image all         # 스캔된 태그·해소된 정책·자동화 상태

kubectl -n windforce get helmrelease windforce

# 실제로 배포된 이미지 태그
kubectl -n windforce get deploy windforce-server \
  -o jsonpath='{.spec.template.spec.containers[0].image}'

강제 reconcile

Flux의 주기적 스캔을 기다리기 싫을 때, 순서대로 당긴다.

flux reconcile source git flux-system
flux reconcile image repository windforce      # ghcr 재스캔
flux reconcile image update windforce          # 태그 bump 커밋·push
flux reconcile kustomization flux-system --with-source
flux reconcile helmrelease windforce -n windforce

롤백

직전 정상 릴리스로 되돌린다. 주의할 점이 하나 있다 — semver 정책은 항상 가장 높은 버전을 고른다. 그래서 옛 태그를 다시 미는 것만으로는 롤백되지 않는다(자동화가 다시 최신 버전을 집어온다).

올바른 절차:

# 1) 자동화를 멈춘다 (최신 버전을 다시 집지 못하게)
flux suspend image update windforce

# 2) HelmRelease의 image.tag 를 직전 정상 v<semver> 로 되돌려 커밋
#    (또는 자동화가 만든 bump 커밋을 git revert)

# 3) 안정화 후, 다음 정상 릴리스를 태그하고 자동화 재개
flux resume image update windforce

차트만 바꿀 때

values나 템플릿(예: 리소스 한도, HTTPRoute)만 손볼 때는 이미지를 다시 빌드할 필요가 없다. Flux가 차트를 git에서 직접 읽기 때문이다. 차트 변경을 push하면 Flux Kustomization이 그대로 반영한다. (빌드 워크플로는 deploy/** 변경을 무시하므로 차트 push가 불필요한 이미지 빌드를 일으키지도 않는다.)

부트스트랩 함정 (1회 — 또는 재해 복구)

새 클러스터에 Flux를 처음 세우거나 다시 깔 때 마주치는 함정이 셋 있다. 모두 부트스트랩 시 1회성이지만, 순서를 어기면 막히므로 미리 알아둔다.

부트스트랩 명령의 핵심은 두 플래그다 — read-write 키이미지 컨트롤러 포함:

flux bootstrap github \
  --owner <owner> --repository <repo> --branch main \
  --path deploy/clusters/<cluster> --personal \
  --read-write-key=true \
  --components-extra=image-reflector-controller,image-automation-controller

이미지 pull 자격(ghcr private 패키지)도 미리 둬야 한다 — windforce 네임스페이스(pull용)와 flux-system 네임스페이스(ImageRepository 스캔용) 양쪽에 dockerconfigjson Secret이 필요하다.

함정 1 — read-only deploy key

bootstrap 기본 deploy key는 read-only다. 하지만 이미지 자동화는 태그 bump를 git에 push해야 하므로, read-only 키면 failed to push ... key ... marked as read only로 막힌다. --read-write-key=true가 필요하다.

주의: 재부트스트랩은 기존 키를 교체하지 않는다(secret이 "up to date"로 보여 건너뛴다). 키를 바꾸려면 먼저 지운다.

gh api -X DELETE repos/<owner>/<repo>/keys/<id>        # GitHub deploy key 삭제
kubectl -n flux-system delete secret flux-system        # 클러스터 secret 삭제
# 그 다음 다시 bootstrap (위 명령)

함정 2 — 이미지 컨트롤러가 prune됨

--components-extra 없이 재부트스트랩하면 image-reflector / image-automation 컨트롤러가 빠진다. 그러면 남아 있던 Image* 커스텀 리소스가 finalizer를 처리할 컨트롤러를 잃어, 관련 CRD가 Terminating에 묶인다.

복구 — finalizer를 먼저 떼어내고, CRD 종료를 기다린 뒤, 컨트롤러를 포함해 재부트스트랩한다.

for k in imagepolicy imagerepository imageupdateautomation; do
  for cr in $(kubectl -n flux-system get $k -o name); do
    kubectl -n flux-system patch $cr --type=merge \
      -p '{"metadata":{"finalizers":null}}'
  done
done

함정 3 — 차트 라벨의 +

Flux가 git에서 차트를 패키징하면 차트 버전이 0.1.0+<sha>가 된다. +는 k8s 라벨로 부적합이라 HelmRelease가 Invalid value ... must be ...로 실패한다. 차트의 helm.sh/chart 라벨 헬퍼가 +를 새니타이즈하면 해결된다(현재 차트에는 이미 반영돼 있다 — 새 차트를 만들 땐 같은 패턴을 유지한다).

ARC 러너 메모

빌드를 돌리는 인클러스터 러너(ARC)에 관한 운영 메모.

  • 러너 스케일셋arc-runners 네임스페이스, repo 스코프, containerMode=dind, minRunners=0/maxRunners=2. 유휴 시 러너 pod는 0개다(빌드가 들어올 때만 뜬다).
  • ghcr 패키지 접근 — private 패키지는 빌드 레포에 Actions repo-access를 부여해야 push가 된다(패키지 → Manage Actions access → 빌드 레포 Write). 누락되면 빌드 push가 403 Forbidden으로 실패한다.
  • 빌드 캐시 — BuildKit registry cache(type=registry)로 ephemeral 러너에서도 Go·레이어 캐시를 유지한다.

더 보기