CI/CD 운영¶
windforce 플랫폼 자체(server·worker 바이너리와 임베드된 콘솔)를 클러스터에 배포하는 방법을 다룬다. 핵심은 한 가지다 — 배포는 릴리스 태그 v<semver>를 push할 때 일어나고, 그 뒤는 전부 자동이다. 수동 docker push나 helm 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·레이어 캐시를 유지한다.
더 보기¶
- 시크릿 관리 (SOPS + age) — 클러스터 시크릿(DB 자격·이미지 pull 토큰 등)을 암호화한 채 git에 선언하고 Flux가 복호하는 흐름.
- 콘솔 가이드 — 사용자 앱의 Deploy 버튼(액션 git export)은 이쪽 — 플랫폼 배포와 다른 층.
- CI/CD 운영 가이드 (원문) — 구성 요소 표·이미지 태그 규칙 등 클러스터 고유 상세.
- ADR-0024: CI/CD — 인클러스터 빌드 + Flux GitOps — 이 모델을 택한 결정 근거.