예전에 Git-ops: fleet 글에서 Rancher Fleet으로 홈랩 앱들을 배포하기 시작했다고 썼었는데요. 그렇게 2년 넘게 쓰다 보니 GitRepo가 어느새 5개가 되어 있었습니다.
문제는 이 5개가 전부 같은 Gitea 저장소, 같은 브랜치를 바라보고 있었다는 겁니다. 차이는 딱 두 가지, paths:(어느 디렉터리를 볼지)와 targetNamespace:(어느 네임스페이스에 깔지)뿐이었죠.
# 이런 게 5개 (myapps, monitoring, diagnose-tools, public-services, namespace-free)
spec:
repo: http://gitea.../continuous-delivery-intergrated.git
branch: main
paths: [/argocd-myapps/argocd-sync]
targetNamespace: myapps(디렉터리 이름에 argocd가 들어간 건 ArgoCD 쓰던 시절의 흔적이고, 지금은 ArgoCD 없이 Fleet만 씁니다. ㅎㅎ)
이걸 GitRepo 1개로 합치고, “무엇을 어디에 배포할지”는 GitRepo 객체가 아니라 git 안의 파일(fleet.yaml, .fleetignore)로만 결정되게 만드는 게 목표였습니다. GitRepo 객체는 kubectl로 만든 명령형 리소스라 git에 이력이 안 남거든요.
결론부터 말하면 성공했고, 그 과정에서 Fleet 문서에는 잘 안 나오는 함정들을 꽤 밟았습니다.
🛡️ 0단계: 지우기 전에 안전망부터
GitRepo를 지우면 → 그 GitRepo가 만든 Bundle이 지워지고 → Bundle이 배포한 워크로드도 전부 지워집니다. 5개를 1개로 합친다는 건 4개를 지운다는 뜻이니, 아무 준비 없이 하면 클러스터의 앱 절반이 날아갑니다. 그래서 먼저 모든 fleet.yaml에 이 두 줄을 넣었습니다.
keepResources: true # Bundle이 삭제돼도 리소스는 남겨둠
helm:
takeOwnership: true # 새 Bundle이 기존 리소스를 "입양"할 수 있게keepResources는 Bundle이 삭제될 때만 동작하기 때문에 평소 운영에는 영향이 없습니다. (적용하면 Bundle마다 내용이 같은 helm upgrade가 한 번씩 돌 뿐입니다.)
특히 신경 쓴 건 CNPG(CloudNativePG) DB 클러스터였습니다. 이 클러스터 매니페스트에는 bootstrap.recovery(S3 백업에서 복원)가 들어 있는데, 이건 Cluster CR이 처음 생성될 때만 동작합니다. 이미 180일 넘게 돌고 있는 클러스터에 재적용해도 아무 일도 안 일어나지만, 만약 CR이 삭제됐다가 다시 만들어지면… S3 백업에서 새로 복원하겠죠. keepResources가 바로 그 상황을 막아줍니다.
🔧 1단계: targetNamespace 걷어내기
GitRepo가 1개가 되면 targetNamespace를 GitRepo에 둘 수 없으니, 각 fleet.yaml에 직접 namespace:를 넣었습니다.
그런데 이걸 하다 보니 targetNamespace가 그동안 숨겨주던 버그들이 나왔습니다.
- Uptime Kuma 매니페스트 12개에
namespace: default가 하드코딩 - PowerDNS ConfigMap에는 존재하지도 않는
namespace: powerdns가 하드코딩
targetNamespace가 전부 덮어쓰고 있어서 지금까지 아무 문제 없이 돌았던 겁니다. 다 실제 네임스페이스로 고쳤습니다.
🕳️ 2단계: paths의 함정
이제 GitRepo 하나가 저장소 전체를 보게 해야 합니다. 직관적으로는 paths: ["/"]면 될 것 같죠? 안 됩니다.
Stalled: no resource found at the following paths to deploy: [/]여러 조합을 실제로(테스트 브랜치에서) 다 돌려본 결과를 표로 정리하면 이렇습니다.
| GitRepo 설정 | 결과 |
|---|---|
paths:에 5개 디렉터리 명시 | ✅ 정상 (18개 Bundle) |
paths: ["/"] (어떤 변형이든) | ❌ 치명적 오류. /를 “그 자리에 매니페스트가 있는 Bundle 하나”로 해석하는데 /엔 바로 렌더할 게 없으니 재귀 전에 죽음 |
paths: 생략, 그 외 아무것도 안 함 | ⚠️ 18개 Bundle은 정상인데, 저장소 루트 Bundle이 하나 더 생겨서 모든 하위 Bundle의 파일을 중복으로 포함 + 일부러 꺼둔 out-of-sync/ 아래 fleet.yaml까지 Bundle로 만들어버림 |
paths: 생략 + 루트 fleet.yaml + 루트 .fleetignore(아래 참고) | ✅ 정상, 루트 Bundle 없음 |
핵심은 paths: ["/"]와 paths를 아예 생략하는 건 완전히 다르다는 겁니다. 생략하면 저장소 전체에서 fleet.yaml을 자동 탐색합니다.
.fleetignore에 대해 알게 된 것들
.fleetignore는 각 Bundle 루트에서만 읽힙니다. 상위 디렉터리의.fleetignore가 하위 Bundle로 상속되지 않습니다. 그래서 “저장소 루트에.fleetignore하나로 전부 관리”는 불가능합니다..fleetignore는 Bundle에 어떤 파일이 들어갈지를 거를 뿐,fleet.yaml이 Bundle로 발견되는 것 자체는 막지 못합니다. 무시된 디렉터리 안의fleet.yaml도 Bundle이 됩니다.- 루트
.fleetignore에서 최상위 디렉터리를 제외하려면 맨 이름만 먹힙니다.argocd-myapps는 되고,/argocd-myapps,argocd-myapps/,argocd-myapps/**는 전부 안 됩니다. (이거 찾느라 한참 걸렸습니다…)
최종 레시피
repo-root/
├── fleet.yaml # name: git-root, defaultNamespace: default (앵커 역할, 실제 Bundle은 안 생김)
├── .fleetignore # argocd-myapps / argocd-monitoring / ... (맨 이름으로!) + 잡동사니 패턴
├── argocd-myapps/argocd-sync/
│ ├── fleet.yaml # name: myapps, namespace: myapps
│ └── .fleetignore # __archive 등 (하위 Bundle용은 여전히 필요)
└── argocd-namespace-free/out-of-sync/.../fleet.yaml.disabled # 자동 탐색에 안 걸리게 이름 변경루트 .fleetignore가 최상위 앱 디렉터리들을 루트 Bundle에서 빼주고, 각 앱 디렉터리는 자기 fleet.yaml로 독립된 Bundle이 됩니다. 꺼둬야 하는 fleet.yaml은 .fleetignore로 막을 수 없으니 이름을 바꿔버렸습니다.
참고로 fleet.yaml에 name:을 쓰면 Bundle 이름이 GitRepo 접두사 없이 딱 그 이름이 됩니다. 덕분에 Bundle 이름도 myapps, monitoring, wordpress-redis처럼 깔끔해졌습니다.
💥 사고: paused GitRepo도 Bundle을 뺏어간다
테스트하는 과정에서 사고가 한 번 있었습니다. 운영 GitRepo와 같은 저장소, 같은 Bundle 이름을 가진 테스트 GitRepo를 paused: true로 만들었는데요. “멈춰 있으니 아무것도 안 하겠지?”라고 생각했습니다.
아니었습니다. paused는 “배포”만 멈출 뿐, Bundle 소유권은 그대로 가져갑니다. 운영 GitRepo의 상태가 순식간에 0/0이 됐습니다.
테스트 GitRepo를 지우고 강제 동기화하니 18개 중 15개는 돌아왔는데, Helm 차트 기반 Bundle 3개(Redis 2개, 앱 1개)가 이 에러로 멈췄습니다.
cannot re-use a name that is still in use소유권이 왔다 갔다 하는 사이에 Helm 릴리스의 deployed 리비전이 사라지고 superseded만 남은 상태였습니다. helm rollback <release> <revision> -n <ns>로 새 deployed 리비전을 만들어주니, Fleet이 다시 helm upgrade로 입양해서 18/18로 복구됐습니다. 워크로드 자체는 그동안 계속 돌고 있었습니다. (keepResources 만세)
교훈: Bundle 이름이 겹치는 GitRepo를 두 번째로 만들지 마세요. paused여도요. 구조를 테스트하려면 Bundle 이름을 다르게 하거나, 차라리 운영 GitRepo를 직접 패치하세요. 되돌리는 건 한 줄입니다.
🔀 전환
실제 전환 순서는 이랬습니다.
- 기존 GitRepo 스펙 백업
- 기존 GitRepo 5개 삭제 →
keepResources덕분에 워크로드 78개, CNPG, Helm 릴리스 시크릿 전부 그대로 유지 - 새 GitRepo를
paused: true로 생성해서 Bundle 목록 확인 (이번엔 겹치는 GitRepo가 없으니 안전) paused: false→ 약 2분 30초 동안 새 Bundle들이 기존 리소스를 입양 (중간에contents ... not found같은 일시 오류가 보였지만 스스로 해결)- 18/18 Ready, 리소스 410/410, 전환 전과 동일
이후 마지막으로 GitRepo에서 paths:를 제거하고(위 레시피로), 일회용 안전망이었던 keepResources는 걷어냈습니다. takeOwnership은 나중에 Bundle 이름을 바꿀 일이 생길 때 유용해서 남겨뒀습니다. 지금 GitRepo 스펙은 이게 전부입니다.
spec:
repo: http://gitea.../continuous-delivery-intergrated.git
branch: main
correctDrift:
enabled: true🎁 덤: comparePatches에서 operations 대신 jsonPointers
Fleet 에이전트 로그에 계속 이런 에러가 찍히고 있었습니다.
Failed to normalize obj with json patch ... Unable to remove nonexistent key원인은 diff.comparePatches에서 operations: [{op: remove, path: ...}]를 쓴 것이었습니다. 차트는 supplementalGroups: [] 같은 빈 배열을 렌더하는데, API 서버가 빈 배열을 지워버리니 “없는 키를 지우려 한다”며 에러가 나는 거죠. jsonPointers:로 바꾸면 경로가 있든 없든 그냥 비교에서 무시해서 에러가 안 납니다.
diff:
comparePatches:
- apiVersion: apps/v1
kind: StatefulSet
name: wordpress-redis-node
jsonPointers:
- /spec/template/spec/securityContext/supplementalGroups📝 정리
- GitRepo를 지우면 워크로드도 지워집니다. 구조 변경 전엔
keepResources: true. paths: ["/"]는 동작하지 않습니다. 전체를 보려면paths를 생략하고, 루트fleet.yaml+ 루트.fleetignore(맨 이름)로 루트 Bundle을 비우세요..fleetignore는 상속되지 않고, Bundle 발견을 막지도 못합니다.- paused GitRepo도 Bundle을 뺏어갑니다. 이름 겹치는 GitRepo는 절대 두 개 만들지 말 것.
targetNamespace를 걷어내면 그동안 숨어 있던 하드코딩 네임스페이스가 드러납니다. 좋은 대청소 기회입니다. ㅎㅎ

답글 남기기